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 2 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
42 changes: 42 additions & 0 deletions docs/sources/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,45 @@ the values from that.
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 alias 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:
konghq.com/strip-path: "true"
external-dns.alpha.kubernetes.io/dualstack: "true"
name: echo
spec:
hostnames:
- echoserver.example.org
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: kong
namespace: kong
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)
}
})
}
}