diff --git a/docs/sources/gateway.md b/docs/sources/gateway.md index 7482aaa58..61913e84f 100644 --- a/docs/sources/gateway.md +++ b/docs/sources/gateway.md @@ -5,7 +5,7 @@ sources create DNS entries based on their respective `gateway.networking.k8s.io` ## Filtering the Routes considered -These sources support the `--label-filter` flag, which filters *Route resources +These sources support the `--label-filter` flag, which filters \*Route resources by a set of labels. ## Domain names @@ -16,67 +16,103 @@ of [domain names from the *Route](#domain-names-from-route). It then iterates over each of the `status.parents` with a [matching Gateway](#matching-gateways) and at least one [matching listener](#matching-listeners). For each matching listener, if the -listener has a `hostname`, it narrows the set of domain names from the *Route to the portion +listener has a `hostname`, it narrows the set of domain names from the \*Route to the portion that overlaps the `hostname`. If a matching listener does not have a `hostname`, it uses the un-narrowed set of domain names. ### Domain names from Route -The set of domain names from a *Route is sourced from the following places: +The set of domain names from a \*Route is sourced from the following places: -* If the *Route is a GRPCRoute, HTTPRoute, or TLSRoute, adds each of the`spec.hostnames`. +- If the \*Route is a GRPCRoute, HTTPRoute, or TLSRoute, adds each of the`spec.hostnames`. -* Adds the hostnames from any `external-dns.alpha.kubernetes.io/hostname` annotation on the *Route. -This behavior is suppressed if the `--ignore-hostname-annotation` flag was specified. +- Adds the hostnames from any `external-dns.alpha.kubernetes.io/hostname` annotation on the \*Route. + This behavior is suppressed if the `--ignore-hostname-annotation` flag was specified. -* If no endpoints were produced by the previous steps -or the `--combine-fqdn-annotation` flag was specified, then adds hostnames -generated from any`--fqdn-template` flag. +- If no endpoints were produced by the previous steps + or the `--combine-fqdn-annotation` flag was specified, then adds hostnames + generated from any`--fqdn-template` flag. -* If no endpoints were produced by the previous steps, each -attached Gateway listener will use its `hostname`, if present. +- If no endpoints were produced by the previous steps, each + attached Gateway listener will use its `hostname`, if present. ### Matching Gateways -Matching Gateways are discovered by iterating over the *Route's `status.parents`: +Matching Gateways are discovered by iterating over the \*Route's `status.parents`: -* Ignores parents with a `parentRef.group` other than -`gateway.networking.k8s.io` or a `parentRef.kind` other than `Gateway`. +- Ignores parents with a `parentRef.group` other than + `gateway.networking.k8s.io` or a `parentRef.kind` other than `Gateway`. -* If the `--gateway-namespace` flag was specified, ignores parents with a `parentRef.namespace` other -than the specified value. +- If the `--gateway-namespace` flag was specified, ignores parents with a `parentRef.namespace` other + than the specified value. -* If the `--gateway-label-filter` flag was specified, ignores parents whose Gateway does not match the -specified label filter. +- If the `--gateway-label-filter` flag was specified, ignores parents whose Gateway does not match the + specified label filter. -* Ignores parents whose Gateway either does not exist or has not accepted the route. +- Ignores parents whose Gateway either does not exist or has not accepted the route. ### Matching listeners Iterates over all listeners for the parent's `parentRef.sectionName`: -* Ignores listeners whose `protocol` field does not match the kind of the *Route per the following table: +- Ignores listeners whose `protocol` field does not match the kind of the \*Route per the following table: -| kind | protocols | -|------------|-------------| -| GRPCRoute | HTTP, HTTPS | -| HTTPRoute | HTTP, HTTPS | -| TCPRoute | TCP | -| TLSRoute | TLS | -| UDPRoute | UDP | +| kind | protocols | +| --------- | ----------- | +| GRPCRoute | HTTP, HTTPS | +| HTTPRoute | HTTP, HTTPS | +| TCPRoute | TCP | +| TLSRoute | TLS | +| UDPRoute | UDP | -* If the parent's `parentRef.port` port is specified, ignores listeners without a matching `port`. +- If the parent's `parentRef.port` port is specified, ignores listeners without a matching `port`. -* Ignores listeners which specify an `allowedRoutes` which does not allow the route. +- Ignores listeners which specify an `allowedRoutes` which does not allow the route. ## Targets -The targets of the DNS entries created from a *Route are sourced from the following places: +The targets of the DNS entries created from a \*Route are sourced from the following places: -1. If a matching parent Gateway has an `external-dns.alpha.kubernetes.io/target` annotation, uses -the values from that. +1. If a matching parent Gateway has an `external-dns.alpha.kubernetes.io/target` annotation, uses + the values from that. -2. Otherwise, iterates over that parent Gateway's `status.addresses`, -adding each address's `value`. +2. Otherwise, iterates over that parent Gateway's `status.addresses`, + adding each address's `value`. -The targets from each parent Gateway matching the *Route are then combined and de-duplicated. +The targets from each parent Gateway matching the \*Route are then combined and de-duplicated. + +## Dualstack Routes + +Gateway resources may be served from an external-loadbalancer which may support both IPv4 and "dualstack" (both IPv4 and IPv6) interfaces. +External DNS Controller uses the `external-dns.alpha.kubernetes.io/dualstack` annotation to determine this. If this annotation is +set to `true` then ExternalDNS will create two records (one A record +and one AAAA record) for each hostname associated with the Route resource. + +Example: + +```yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + annotations: + external-dns.alpha.kubernetes.io/dualstack: "true" + name: echo +spec: + hostnames: + - echoserver.example.org + rules: + - backendRefs: + - group: "" + kind: Service + name: echo + port: 1027 + weight: 1 + matches: + - path: + type: PathPrefix + value: /echo +``` + +The above HTTPRoute resource is backed by a dualstack Gateway. +ExternalDNS will create both an A `echoserver.example.org` record and +an AAAA record of the same name, that each are aliases for the same LB. diff --git a/source/gateway.go b/source/gateway.go index 33ca684af..5d8474999 100644 --- a/source/gateway.go +++ b/source/gateway.go @@ -45,6 +45,10 @@ import ( const ( gatewayGroup = "gateway.networking.k8s.io" gatewayKind = "Gateway" + // gatewayAPIDualstackAnnotationKey is the annotation used for determining if a Gateway Route is dualstack + gatewayAPIDualstackAnnotationKey = "external-dns.alpha.kubernetes.io/dualstack" + // gatewayAPIDualstackAnnotationValue is the value of the Gateway Route dualstack annotation that indicates it is dualstack + gatewayAPIDualstackAnnotationValue = "true" ) type gatewayRoute interface { @@ -236,6 +240,7 @@ func (src *gatewayRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpo for host, targets := range hostTargets { endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) } + setDualstackLabel(rt, endpoints) log.Debugf("Endpoints generated from %s %s/%s: %v", src.rtKind, meta.Namespace, meta.Name, endpoints) } return endpoints, nil @@ -610,3 +615,13 @@ func selectorsEqual(a, b labels.Selector) bool { } return true } + +func setDualstackLabel(rt gatewayRoute, endpoints []*endpoint.Endpoint) { + val, ok := rt.Metadata().Annotations[gatewayAPIDualstackAnnotationKey] + if ok && val == gatewayAPIDualstackAnnotationValue { + log.Debugf("Adding dualstack label to GatewayRoute %s/%s.", rt.Metadata().Namespace, rt.Metadata().Name) + for _, ep := range endpoints { + ep.Labels[endpoint.DualstackLabelKey] = "true" + } + } +} diff --git a/source/gateway_test.go b/source/gateway_test.go index 940622e95..19d603a24 100644 --- a/source/gateway_test.go +++ b/source/gateway_test.go @@ -20,6 +20,7 @@ import ( "strings" "testing" + "sigs.k8s.io/external-dns/endpoint" v1 "sigs.k8s.io/gateway-api/apis/v1" ) @@ -245,3 +246,47 @@ func TestIsDNS1123Domain(t *testing.T) { }) } } + +func TestDualStackLabel(t *testing.T) { + tests := []struct { + desc string + in map[string](string) + setsLabel bool + }{ + { + desc: "empty-annotation", + setsLabel: false, + }, + { + desc: "correct-annotation-key-and-value", + in: map[string]string{gatewayAPIDualstackAnnotationKey: gatewayAPIDualstackAnnotationValue}, + setsLabel: true, + }, + { + desc: "correct-annotation-key-incorrect-value", + in: map[string]string{gatewayAPIDualstackAnnotationKey: "foo"}, + setsLabel: false, + }, + { + desc: "incorrect-annotation-key-correct-value", + in: map[string]string{"FOO": gatewayAPIDualstackAnnotationValue}, + setsLabel: false, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + endpoints := make([]*endpoint.Endpoint, 0) + endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3")) + + rt := &gatewayHTTPRoute{} + rt.Metadata().Annotations = tt.in + + setDualstackLabel(rt, endpoints) + got := endpoints[0].Labels[endpoint.DualstackLabelKey] == "true" + + if got != tt.setsLabel { + t.Errorf("setDualstackLabel(%q); got: %v; want: %v", tt.in, got, tt.setsLabel) + } + }) + } +}