Skip to content

Commit d2fda91

Browse files
x1unixdehaansa
andauthored
feat: support DNS SD URLs (#5034)
* feat: support DNS SD URLs for --cluster.join-addresses * feat: update TestPeerDiscovery * chore: update CHANGELOG.md * chore: update `alloy run` doc * Update CHANGELOG.md Co-authored-by: Sam DeHaan <sam.dehaan@grafana.com> --------- Co-authored-by: Sam DeHaan <sam.dehaan@grafana.com>
1 parent 1aef0ac commit d2fda91

File tree

4 files changed

+108
-0
lines changed

4 files changed

+108
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ internal API changes are not present.
1010
Main (unreleased)
1111
-----------------
1212

13+
- Support specifying DNS discovery mode prefixes in `--cluster.join-addresses` flag. (@x1unix)
14+
1315
### Features
1416

1517
- Add `otelcol.connector.count` component to count the number of spans, metrics, and logs passing through it. (@hhertout)

docs/sources/reference/cli/run.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ Since Windows doesn't use the interface names `eth0` or `en0`, Windows users mus
132132
The comma-separated list of addresses provided in `--cluster.join-addresses` can either be IP addresses or DNS names to lookup (supports SRV and A/AAAA records).
133133
In both cases, the port number can be specified with a `:<port>` suffix. If ports aren't provided, default of the port used for the HTTP listener is used.
134134
If you don't provide the port number explicitly, you must ensure that all instances use the same port for the HTTP listener.
135+
Optionally, you may specify a DNS query type as a prefix for each address. See [join addresses format](#join-address-format) for more information.
135136

136137
The `--cluster.enable-tls` flag can be set to enable TLS for peer-to-peer communications.
137138
Additional arguments are required to configure the TLS client, including the CA certificate, the TLS certificate, the key, and the server name.
@@ -171,6 +172,23 @@ When `--cluster.name` is provided, nodes only join peers who share the same clus
171172
By default, the cluster name is empty, and any node that doesn't set the flag can join.
172173
Attempting to join a cluster with a wrong `--cluster.name` results in a "failed to join memberlist" error.
173174

175+
### Join Address Format
176+
177+
The `--cluster.join-addresses` flag supports DNS names with discovery mode prefix.
178+
You select a discovery mode by adding one of the following supported prefixes to the address:
179+
180+
* **`dns+`**\
181+
The domain name after the prefix is looked up as an A/AAAA query.\
182+
For example: `dns+alloy.local:11211`.
183+
* **`dnssrv+`**\
184+
The domain name after the prefix is looked up as a SRV query, and then each SRV record is resolved as an A/AAAA record.\
185+
For example: `dnssrv+_alloy._tcp.alloy.namespace.svc.cluster.local`.
186+
* **`dnssrvnoa+`**\
187+
The domain name after the prefix is looked up as a SRV query, with no A/AAAA lookup made after that.\
188+
For example: `dnssrvnoa+_alloy-memberlist._tcp.service.consul`
189+
190+
If no prefix is provided, Alloy will attempt to resolve the name using both A/AAAA and DNSSRV queries.
191+
174192
### Clustering states
175193

176194
Clustered {{< param "PRODUCT_NAME" >}}s are in one of three states:

internal/service/cluster/discovery/join_peers.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"net"
88
"strconv"
9+
"strings"
910

1011
"github.com/go-kit/log"
1112
"github.com/samber/lo"
@@ -29,7 +30,9 @@ func newWithJoinPeers(opts Options) DiscoverFn {
2930
defer span.End()
3031

3132
// Use these resolvers in order to resolve the provided addresses into a form that can be used by clustering.
33+
// NOTE: dnsSDURLResolver should be above other DNS resolvers.
3234
resolvers := []addressResolver{
35+
dnsSDURLResolver(opts, ctx),
3336
ipResolver(opts.Logger),
3437
dnsAResolver(opts, ctx),
3538
dnsSRVResolver(opts, ctx),
@@ -128,6 +131,7 @@ func dnsSRVResolver(opts Options, ctx context.Context) addressResolver {
128131
if srvLookup == nil {
129132
srvLookup = net.LookupSRV
130133
}
134+
131135
return dnsResolver(opts, ctx, "SRV", func(addr string) ([]string, error) {
132136
_, addresses, err := srvLookup("", "", addr)
133137
result := make([]string, 0, len(addresses))
@@ -138,6 +142,57 @@ func dnsSRVResolver(opts Options, ctx context.Context) addressResolver {
138142
})
139143
}
140144

145+
const (
146+
dnsSchemeDNS = "dns+"
147+
dnsSchemeDNSSRV = "dnssrv+"
148+
dnsSchemeDNSSRVNOA = "dnssrvnoa+"
149+
)
150+
151+
// dnsSDURLResolver handles DNS-SD URLs which explicitly states what DNS query should be used for host resolve.
152+
//
153+
// Example: `dnssrv+_memcached._tcp.memcached.namespace.svc.cluster.local`
154+
//
155+
// Resolver rejects any non-URL values.
156+
func dnsSDURLResolver(opts Options, ctx context.Context) addressResolver {
157+
srvLookup := opts.lookupSRVFn
158+
if srvLookup == nil {
159+
srvLookup = net.LookupSRV
160+
}
161+
162+
return func(addr string) ([]string, error) {
163+
var (
164+
nextAddr string
165+
nextResolver addressResolver
166+
)
167+
168+
switch {
169+
case strings.HasPrefix(addr, dnsSchemeDNS):
170+
nextAddr = addr[len(dnsSchemeDNS):]
171+
nextResolver = dnsAResolver(opts, ctx)
172+
case strings.HasPrefix(addr, dnsSchemeDNSSRV):
173+
nextAddr = addr[len(dnsSchemeDNSSRV):]
174+
nextResolver = dnsSRVResolver(opts, ctx)
175+
case strings.HasPrefix(addr, dnsSchemeDNSSRVNOA):
176+
nextAddr = addr[len(dnsSchemeDNSSRVNOA):]
177+
nextResolver = dnsResolver(opts, ctx, "SRVNOA", func(addr string) ([]string, error) {
178+
// NOTE: the only difference between SRVNOA and SRV, as SRV request should do N+1 query for A/AAAA.
179+
_, addresses, err := srvLookup("", "", addr)
180+
result := make([]string, 0, len(addresses))
181+
for _, a := range addresses {
182+
result = append(result, a.Target)
183+
}
184+
return result, err
185+
})
186+
187+
default:
188+
// skip and pass control to a next resolver.
189+
return nil, nil
190+
}
191+
192+
return nextResolver(nextAddr)
193+
}
194+
}
195+
141196
func dnsResolver(opts Options, ctx context.Context, recordType string, dnsLookupFn func(string) ([]string, error)) addressResolver {
142197
return func(addr string) ([]string, error) {
143198
_, span := opts.Tracer.Tracer("").Start(

internal/service/cluster/discovery/peer_discovery_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,39 @@ func TestPeerDiscovery(t *testing.T) {
424424
},
425425
expected: []string{"10.10.10.11:8888"},
426426
},
427+
{
428+
name: "dnssrvnoa records are parsed",
429+
args: Options{
430+
JoinPeers: []string{"dnssrvnoa+_alloy-memberlist._tcp.service.consul", "dns+host2:7777"},
431+
DefaultPort: 8888,
432+
Logger: logger,
433+
Tracer: tracer,
434+
lookupIPFn: func(name string) ([]net.IP, error) {
435+
if name == "host2" {
436+
return []net.IP{
437+
net.ParseIP("192.168.1.10"),
438+
}, nil
439+
}
440+
441+
return nil, fmt.Errorf("unexpected name %q", name)
442+
},
443+
lookupSRVFn: func(service, proto, name string) (string, []*net.SRV, error) {
444+
if name == "_alloy-memberlist._tcp.service.consul" {
445+
return "", []*net.SRV{
446+
{Target: "10.10.10.10"},
447+
{Target: "10.10.10.11"},
448+
}, nil
449+
}
450+
451+
return "", nil, fmt.Errorf("unexpected name %q", name)
452+
},
453+
},
454+
expected: []string{
455+
"10.10.10.10:8888",
456+
"10.10.10.11:8888",
457+
"192.168.1.10:7777",
458+
},
459+
},
427460
{
428461
name: "go discovery factory error",
429462
args: Options{

0 commit comments

Comments
 (0)