Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support dual stack for gateway api #4469

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 74 additions & 38 deletions docs/sources/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:

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`.

The targets from each parent Gateway matching the *Route are then combined and de-duplicated.
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.

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.

## 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.
15 changes: 15 additions & 0 deletions source/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,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 {
Expand Down Expand Up @@ -235,6 +239,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
Expand Down Expand Up @@ -609,3 +614,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"
}
}
}
45 changes: 45 additions & 0 deletions source/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"strings"
"testing"

"sigs.k8s.io/external-dns/endpoint"
v1 "sigs.k8s.io/gateway-api/apis/v1"
)

Expand Down Expand Up @@ -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)
}
})
}
}
Loading