Skip to content

Commit 3e7291f

Browse files
lindleywhitelongquanzheng
authored andcommitted
Support DNS SRV Records within Ringpop (#4614)
1 parent cfa1fa2 commit 3e7291f

File tree

3 files changed

+216
-2
lines changed

3 files changed

+216
-2
lines changed

common/config/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ type (
114114
Ringpop struct {
115115
// Name to be used in ringpop advertisement
116116
Name string `yaml:"name" validate:"nonzero"`
117-
// BootstrapMode is a enum that defines the ringpop bootstrap method
117+
// BootstrapMode is a enum that defines the ringpop bootstrap method, currently supports: hosts, files, custom, dns, and dns-srv
118118
BootstrapMode BootstrapMode `yaml:"bootstrapMode"`
119119
// BootstrapHosts is a list of seed hosts to be used for ringpop bootstrap
120120
BootstrapHosts []string `yaml:"bootstrapHosts"`

common/config/ringpop.go

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ const (
5757
// BootstrapModeDNS represents a list of hosts passed in the configuration
5858
// to be resolved, and the resulting addresses are used for bootstrap
5959
BootstrapModeDNS
60+
// BootstrapModeDNSSRV represents a list of DNS hosts passed in the configuration
61+
// to resolve secondary addresses that DNS SRV record would return resulting in
62+
// a host list that will contain multiple dynamic addresses and their unique ports
63+
BootstrapModeDNSSRV
6064
)
6165

6266
const (
@@ -130,6 +134,8 @@ func parseBootstrapMode(
130134
return BootstrapModeCustom, nil
131135
case "dns":
132136
return BootstrapModeDNS, nil
137+
case "dns-srv":
138+
return BootstrapModeDNSSRV, nil
133139
}
134140
return BootstrapModeNone, errors.New("invalid or no ringpop bootstrap mode")
135141
}
@@ -143,7 +149,7 @@ func validateBootstrapMode(
143149
if len(rpConfig.BootstrapFile) == 0 {
144150
return fmt.Errorf("ringpop config missing bootstrap file param")
145151
}
146-
case BootstrapModeHosts, BootstrapModeDNS:
152+
case BootstrapModeHosts, BootstrapModeDNS, BootstrapModeDNSSRV:
147153
if len(rpConfig.BootstrapHosts) == 0 {
148154
return fmt.Errorf("ringpop config missing boostrap hosts param")
149155
}
@@ -263,6 +269,7 @@ func (factory *RingpopFactory) getChannel(
263269

264270
type dnsHostResolver interface {
265271
LookupHost(ctx context.Context, host string) (addrs []string, err error)
272+
LookupSRV(ctx context.Context, service, proto, name string) (cname string, addrs []*net.SRV, err error)
266273
}
267274

268275
type dnsProvider struct {
@@ -322,6 +329,84 @@ func (provider *dnsProvider) Hosts() ([]string, error) {
322329
return results, nil
323330
}
324331

332+
type dnsSRVProvider struct {
333+
UnresolvedHosts []string
334+
Resolver dnsHostResolver
335+
Logger log.Logger
336+
}
337+
338+
func newDNSSRVProvider(
339+
hosts []string,
340+
resolver dnsHostResolver,
341+
logger log.Logger,
342+
) *dnsSRVProvider {
343+
344+
set := map[string]struct{}{}
345+
for _, hostport := range hosts {
346+
set[hostport] = struct{}{}
347+
}
348+
349+
var keys []string
350+
for key := range set {
351+
keys = append(keys, key)
352+
}
353+
354+
return &dnsSRVProvider{
355+
UnresolvedHosts: keys,
356+
Resolver: resolver,
357+
Logger: logger,
358+
}
359+
}
360+
361+
func (provider *dnsSRVProvider) Hosts() ([]string, error) {
362+
var results []string
363+
resolvedHosts := map[string][]string{}
364+
365+
for _, service := range provider.UnresolvedHosts {
366+
serviceParts := strings.Split(service, ".")
367+
if len(serviceParts) <= 2 {
368+
provider.Logger.Error("could not seperate service name from domain", tag.Address(service))
369+
return nil, errors.New("could not seperate service name from domain. check host configuration")
370+
}
371+
serviceName := serviceParts[0]
372+
domain := strings.Join(serviceParts[1:], ".")
373+
resolved, exists := resolvedHosts[serviceName]
374+
if !exists {
375+
_, addrs, err := provider.Resolver.LookupSRV(context.Background(), serviceName, "tcp", domain)
376+
377+
if err != nil {
378+
provider.Logger.Error("could not resolve host", tag.Address(serviceName), tag.Error(err))
379+
return nil, errors.New(fmt.Sprintf("could not resolve host: %s.%s", serviceName, domain))
380+
}
381+
382+
var targets []string
383+
for _, record := range addrs {
384+
target, err := provider.Resolver.LookupHost(context.Background(), record.Target)
385+
386+
if err != nil {
387+
provider.Logger.Warn("could not resolve srv dns host", tag.Address(record.Target), tag.Error(err))
388+
continue
389+
}
390+
for _, host := range target {
391+
targets = append(targets, net.JoinHostPort(host, fmt.Sprintf("%d", record.Port)))
392+
}
393+
}
394+
resolvedHosts[serviceName] = targets
395+
resolved = targets
396+
}
397+
398+
for _, r := range resolved {
399+
results = append(results, r)
400+
}
401+
402+
}
403+
404+
if len(results) == 0 {
405+
return nil, errors.New("no hosts found, and bootstrap requires at least one")
406+
}
407+
return results, nil
408+
}
409+
325410
func newDiscoveryProvider(
326411
cfg *Ringpop,
327412
logger log.Logger,
@@ -339,6 +424,8 @@ func newDiscoveryProvider(
339424
return jsonfile.New(cfg.BootstrapFile), nil
340425
case BootstrapModeDNS:
341426
return newDNSProvider(cfg.BootstrapHosts, net.DefaultResolver, logger), nil
427+
case BootstrapModeDNSSRV:
428+
return newDNSSRVProvider(cfg.BootstrapHosts, net.DefaultResolver, logger), nil
342429
}
343430
return nil, fmt.Errorf("unknown bootstrap mode")
344431
}

common/config/ringpop_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"fmt"
2626
"testing"
2727
"time"
28+
"net"
2829

2930
"github.com/stretchr/testify/require"
3031
"github.com/stretchr/testify/suite"
@@ -93,6 +94,8 @@ func (s *RingpopSuite) TestCustomMode() {
9394

9495
type mockResolver struct {
9596
Hosts map[string][]string
97+
SRV map[string][]net.SRV
98+
suite *RingpopSuite
9699
}
97100

98101
func (resolver *mockResolver) LookupHost(ctx context.Context, host string) ([]string, error) {
@@ -103,6 +106,22 @@ func (resolver *mockResolver) LookupHost(ctx context.Context, host string) ([]st
103106
return addrs, nil
104107
}
105108

109+
func (resolver *mockResolver) LookupSRV(ctx context.Context, service string, proto string, name string) (string, []*net.SRV, error) {
110+
var records []*net.SRV
111+
srvs, ok := resolver.SRV[service]
112+
if !ok {
113+
return "", nil, fmt.Errorf("Host was not resolved: %s", service)
114+
}
115+
116+
for _, record := range srvs {
117+
var srvRecord net.SRV
118+
srvRecord = record
119+
records = append(records, &srvRecord)
120+
}
121+
122+
return "test", records, nil
123+
}
124+
106125
func (s *RingpopSuite) TestDNSMode() {
107126
var cfg Ringpop
108127
err := yaml.Unmarshal([]byte(getDNSConfig()), &cfg)
@@ -165,6 +184,102 @@ func (s *RingpopSuite) TestDNSMode() {
165184
s.NotNil(err, "error should be returned when no hosts")
166185
}
167186

187+
func (s *RingpopSuite) TestDNSSRVMode() {
188+
var cfg Ringpop
189+
err := yaml.Unmarshal([]byte(getDNSSRVConfig()), &cfg)
190+
s.Nil(err)
191+
s.Equal("test", cfg.Name)
192+
s.Equal(BootstrapModeDNSSRV, cfg.BootstrapMode)
193+
s.Nil(cfg.validate())
194+
logger := loggerimpl.NewNopLogger()
195+
f, err := cfg.NewFactory(nil, "test", logger)
196+
s.Nil(err)
197+
s.NotNil(f)
198+
199+
s.ElementsMatch(
200+
[]string{
201+
"service-a.example.net",
202+
"service-b.example.net",
203+
"unknown-duplicate.example.net",
204+
"unknown-duplicate.example.net",
205+
"badhostport",
206+
},
207+
cfg.BootstrapHosts,
208+
)
209+
210+
provider := newDNSSRVProvider(
211+
cfg.BootstrapHosts,
212+
&mockResolver{
213+
SRV: map[string][]net.SRV{
214+
"service-a": []net.SRV{{ Target:"az1-service-a.addr.example.net", Port: 7755}, {Target: "az2-service-a.addr.example.net", Port: 7566}},
215+
"service-b": []net.SRV{{ Target:"az1-service-b.addr.example.net", Port: 7788}, {Target: "az2-service-b.addr.example.net", Port: 7896}},
216+
},
217+
Hosts: map[string][]string{
218+
"az1-service-a.addr.example.net": []string{"10.0.0.1"},
219+
"az2-service-a.addr.example.net": []string{"10.0.2.0", "10.0.2.3"},
220+
"az1-service-b.addr.example.net": []string{"10.0.3.0", "10.0.3.3"},
221+
"az2-service-b.addr.example.net": []string{"10.0.3.1"},
222+
},
223+
suite: s,
224+
},
225+
logger,
226+
)
227+
cfg.DiscoveryProvider = provider
228+
s.ElementsMatch(
229+
[]string{
230+
"service-a.example.net",
231+
"service-b.example.net",
232+
"unknown-duplicate.example.net",
233+
"badhostport",
234+
},
235+
provider.UnresolvedHosts,
236+
"duplicate entries should be removed",
237+
)
238+
239+
//Expect unknown-duplicate.example.net to not resolve
240+
_, err = cfg.DiscoveryProvider.Hosts()
241+
s.NotNil(err)
242+
243+
//Remove known bad hosts from Unresolved list
244+
provider.UnresolvedHosts = []string{
245+
"service-a.example.net",
246+
"service-b.example.net",
247+
"badhostport",
248+
}
249+
250+
//Expect badhostport to not seperate service name
251+
_, err = cfg.DiscoveryProvider.Hosts()
252+
s.NotNil(err)
253+
254+
255+
//Remove known bad hosts from Unresolved list
256+
provider.UnresolvedHosts = []string{
257+
"service-a.example.net",
258+
"service-b.example.net",
259+
}
260+
261+
hostports, err := cfg.DiscoveryProvider.Hosts()
262+
s.Nil(err)
263+
s.ElementsMatch(
264+
[]string{
265+
"10.0.0.1:7755",
266+
"10.0.2.0:7566", "10.0.2.3:7566",
267+
"10.0.3.0:7788", "10.0.3.3:7788",
268+
"10.0.3.1:7896",
269+
},
270+
hostports,
271+
)
272+
273+
cfg.DiscoveryProvider = newDNSProvider(
274+
cfg.BootstrapHosts,
275+
&mockResolver{Hosts: map[string][]string{}},
276+
logger,
277+
)
278+
hostports, err = cfg.DiscoveryProvider.Hosts()
279+
s.Nil(hostports)
280+
s.NotNil(err, "error should be returned when no hosts")
281+
}
282+
168283
func (s *RingpopSuite) TestInvalidConfig() {
169284
var cfg Ringpop
170285
s.NotNil(cfg.validate())
@@ -207,3 +322,15 @@ bootstrapHosts:
207322
- badhostport
208323
maxJoinDuration: 30s`
209324
}
325+
326+
func getDNSSRVConfig() string {
327+
return `name: "test"
328+
bootstrapMode: "dns-srv"
329+
bootstrapHosts:
330+
- service-a.example.net
331+
- service-b.example.net
332+
- unknown-duplicate.example.net
333+
- unknown-duplicate.example.net
334+
- badhostport
335+
maxJoinDuration: 30s`
336+
}

0 commit comments

Comments
 (0)