Skip to content

Commit

Permalink
Merge pull request #4469 from thameezb/feat-support-dual-stack-gatewa…
Browse files Browse the repository at this point in the history
…y-api

feat: support dual stack for gateway api
  • Loading branch information
k8s-ci-robot authored Sep 1, 2024
2 parents 4944248 + e9968d8 commit ccab9a9
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 38 deletions.
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 @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
}
}
}
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)
}
})
}
}

0 comments on commit ccab9a9

Please sign in to comment.