diff --git a/CHANGELOG.md b/CHANGELOG.md index bc767b1c9e9..bf5a00ee051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ internal API changes are not present. Main (unreleased) ----------------- +- Support specifying DNS discovery mode prefixes in `--cluster.join-addresses` flag. (@x1unix) + ### Features - Add `otelcol.connector.count` component to count the number of spans, metrics, and logs passing through it. (@hhertout) diff --git a/docs/sources/reference/cli/run.md b/docs/sources/reference/cli/run.md index ccebe50fb31..29eeb178e99 100644 --- a/docs/sources/reference/cli/run.md +++ b/docs/sources/reference/cli/run.md @@ -132,6 +132,7 @@ Since Windows doesn't use the interface names `eth0` or `en0`, Windows users mus 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). In both cases, the port number can be specified with a `:` suffix. If ports aren't provided, default of the port used for the HTTP listener is used. If you don't provide the port number explicitly, you must ensure that all instances use the same port for the HTTP listener. +Optionally, you may specify a DNS query type as a prefix for each address. See [join addresses format](#join-address-format) for more information. The `--cluster.enable-tls` flag can be set to enable TLS for peer-to-peer communications. 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 By default, the cluster name is empty, and any node that doesn't set the flag can join. Attempting to join a cluster with a wrong `--cluster.name` results in a "failed to join memberlist" error. +### Join Address Format + +The `--cluster.join-addresses` flag supports DNS names with discovery mode prefix. +You select a discovery mode by adding one of the following supported prefixes to the address: + +* **`dns+`**\ +The domain name after the prefix is looked up as an A/AAAA query.\ +For example: `dns+alloy.local:11211`. +* **`dnssrv+`**\ +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.\ +For example: `dnssrv+_alloy._tcp.alloy.namespace.svc.cluster.local`. +* **`dnssrvnoa+`**\ +The domain name after the prefix is looked up as a SRV query, with no A/AAAA lookup made after that.\ +For example: `dnssrvnoa+_alloy-memberlist._tcp.service.consul` + +If no prefix is provided, Alloy will attempt to resolve the name using both A/AAAA and DNSSRV queries. + ### Clustering states Clustered {{< param "PRODUCT_NAME" >}}s are in one of three states: diff --git a/internal/service/cluster/discovery/join_peers.go b/internal/service/cluster/discovery/join_peers.go index bde3354f99b..f5262ca6fbc 100644 --- a/internal/service/cluster/discovery/join_peers.go +++ b/internal/service/cluster/discovery/join_peers.go @@ -6,6 +6,7 @@ import ( "fmt" "net" "strconv" + "strings" "github.com/go-kit/log" "github.com/samber/lo" @@ -29,7 +30,9 @@ func newWithJoinPeers(opts Options) DiscoverFn { defer span.End() // Use these resolvers in order to resolve the provided addresses into a form that can be used by clustering. + // NOTE: dnsSDURLResolver should be above other DNS resolvers. resolvers := []addressResolver{ + dnsSDURLResolver(opts, ctx), ipResolver(opts.Logger), dnsAResolver(opts, ctx), dnsSRVResolver(opts, ctx), @@ -128,6 +131,7 @@ func dnsSRVResolver(opts Options, ctx context.Context) addressResolver { if srvLookup == nil { srvLookup = net.LookupSRV } + return dnsResolver(opts, ctx, "SRV", func(addr string) ([]string, error) { _, addresses, err := srvLookup("", "", addr) result := make([]string, 0, len(addresses)) @@ -138,6 +142,57 @@ func dnsSRVResolver(opts Options, ctx context.Context) addressResolver { }) } +const ( + dnsSchemeDNS = "dns+" + dnsSchemeDNSSRV = "dnssrv+" + dnsSchemeDNSSRVNOA = "dnssrvnoa+" +) + +// dnsSDURLResolver handles DNS-SD URLs which explicitly states what DNS query should be used for host resolve. +// +// Example: `dnssrv+_memcached._tcp.memcached.namespace.svc.cluster.local` +// +// Resolver rejects any non-URL values. +func dnsSDURLResolver(opts Options, ctx context.Context) addressResolver { + srvLookup := opts.lookupSRVFn + if srvLookup == nil { + srvLookup = net.LookupSRV + } + + return func(addr string) ([]string, error) { + var ( + nextAddr string + nextResolver addressResolver + ) + + switch { + case strings.HasPrefix(addr, dnsSchemeDNS): + nextAddr = addr[len(dnsSchemeDNS):] + nextResolver = dnsAResolver(opts, ctx) + case strings.HasPrefix(addr, dnsSchemeDNSSRV): + nextAddr = addr[len(dnsSchemeDNSSRV):] + nextResolver = dnsSRVResolver(opts, ctx) + case strings.HasPrefix(addr, dnsSchemeDNSSRVNOA): + nextAddr = addr[len(dnsSchemeDNSSRVNOA):] + nextResolver = dnsResolver(opts, ctx, "SRVNOA", func(addr string) ([]string, error) { + // NOTE: the only difference between SRVNOA and SRV, as SRV request should do N+1 query for A/AAAA. + _, addresses, err := srvLookup("", "", addr) + result := make([]string, 0, len(addresses)) + for _, a := range addresses { + result = append(result, a.Target) + } + return result, err + }) + + default: + // skip and pass control to a next resolver. + return nil, nil + } + + return nextResolver(nextAddr) + } +} + func dnsResolver(opts Options, ctx context.Context, recordType string, dnsLookupFn func(string) ([]string, error)) addressResolver { return func(addr string) ([]string, error) { _, span := opts.Tracer.Tracer("").Start( diff --git a/internal/service/cluster/discovery/peer_discovery_test.go b/internal/service/cluster/discovery/peer_discovery_test.go index d8663230cbf..79b715a0b98 100644 --- a/internal/service/cluster/discovery/peer_discovery_test.go +++ b/internal/service/cluster/discovery/peer_discovery_test.go @@ -424,6 +424,39 @@ func TestPeerDiscovery(t *testing.T) { }, expected: []string{"10.10.10.11:8888"}, }, + { + name: "dnssrvnoa records are parsed", + args: Options{ + JoinPeers: []string{"dnssrvnoa+_alloy-memberlist._tcp.service.consul", "dns+host2:7777"}, + DefaultPort: 8888, + Logger: logger, + Tracer: tracer, + lookupIPFn: func(name string) ([]net.IP, error) { + if name == "host2" { + return []net.IP{ + net.ParseIP("192.168.1.10"), + }, nil + } + + return nil, fmt.Errorf("unexpected name %q", name) + }, + lookupSRVFn: func(service, proto, name string) (string, []*net.SRV, error) { + if name == "_alloy-memberlist._tcp.service.consul" { + return "", []*net.SRV{ + {Target: "10.10.10.10"}, + {Target: "10.10.10.11"}, + }, nil + } + + return "", nil, fmt.Errorf("unexpected name %q", name) + }, + }, + expected: []string{ + "10.10.10.10:8888", + "10.10.10.11:8888", + "192.168.1.10:7777", + }, + }, { name: "go discovery factory error", args: Options{