Skip to content
Open
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
10 changes: 10 additions & 0 deletions docs/annotations/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ Specifies the TTL (time to live) for the resource's DNS records.
The value may be specified as either a duration or an integer number of seconds.
It must be between 1 and 2,147,483,647 seconds.

## external-dns.alpha.kubernetes.io/gateway-hostname-source

Specifies where to get the domain for `Route` resource. This annotation should be present on the actual `Route` resource, not the `Gateway` resource itself.

If the value is `defined-hosts-only`, use only the domains from the `Route` spec.

If the value is `annotation-only`, use only the domains from the `Route` annotations.

If the annotation is not present, use the domains from both the spec and annotations.

## Provider-specific annotations

Some providers define their own annotations. Cloud-specific annotations have keys prefixed as follows:
Expand Down
32 changes: 32 additions & 0 deletions docs/sources/gateway-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,35 @@ spec:
- --registry=txt
- --txt-owner-id=my-identifier
```

## Annotations

### external-dns.alpha.kubernetes.io/gateway-hostname-source

**Why is this needed:**
In certain scenarios, conflicting DNS records can arise when External DNS processes both the hostname annotations and the hostnames defined in the `*Route` spec. For example:

- A CNAME record (`company.public.example.com -> company.private.example.com`) is used to direct traffic to private endpoints (e.g., AWS PrivateLink).
- Some third-party services require traffic to resolve publicly to the Gateway API load balancer, but the hostname (`company.public.example.com`) must remain unchanged to avoid breaking the CNAME setup.
- Without this annotation, External DNS may override the CNAME record with an A record due to conflicting hostname definitions.

**Usage:**
By setting the annotation `external-dns.alpha.kubernetes.io/gateway-hostname-source: annotation-only`, users can instruct External DNS
to ignore hostnames defined in the `HTTPRoute` spec and use only the hostnames specified in annotations. This ensures
compatibility with complex DNS configurations and avoids record conflicts.

**Example:**

```yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
annotations:
external-dns.alpha.kubernetes.io/gateway-hostname-source: annotation-only
external-dns.alpha.kubernetes.io/hostname: company.private.example.com
spec:
hostnames:
- company.public.example.com
```

In this example, External DNS will create DNS records only for `company.private.example.com` based on the annotation, ignoring the `hostnames` field in the `HTTPRoute` spec. This prevents conflicts with existing CNAME records while enabling public resolution for specific endpoints.
2 changes: 2 additions & 0 deletions source/annotations/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,6 @@ const (
ControllerValue = "dns-controller"
// The annotation used for defining the desired hostname
InternalHostnameKey = "external-dns.alpha.kubernetes.io/internal-hostname"
// The annotation used for defining the desired hostname source for gateways
GatewayHostnameSourceKey = "external-dns.alpha.kubernetes.io/gateway-hostname-source"
)
30 changes: 22 additions & 8 deletions source/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ import (
)

const (
gatewayGroup = "gateway.networking.k8s.io"
gatewayKind = "Gateway"
gatewayGroup = "gateway.networking.k8s.io"
gatewayKind = "Gateway"
gatewayHostnameSourceAnnotationOnlyValue = "annotation-only"
gatewayHostnameSourceDefinedHostsOnlyValue = "defined-hosts-only"
)

type gatewayRoute interface {
Expand Down Expand Up @@ -401,13 +403,14 @@ func (c *gatewayRouteResolver) resolve(rt gatewayRoute) (map[string]endpoint.Tar

func (c *gatewayRouteResolver) hosts(rt gatewayRoute) ([]string, error) {
var hostnames []string
var annotationHostnames []string
for _, name := range rt.Hostnames() {
hostnames = append(hostnames, string(name))
}
// TODO: The ignore-hostname-annotation flag help says "valid only when using fqdn-template"
// but other sources don't check if fqdn-template is set. Which should it be?
if !c.src.ignoreHostnameAnnotation {
hostnames = append(hostnames, annotations.HostnamesFromAnnotations(rt.Metadata().Annotations)...)
annotationHostnames = append(annotationHostnames, annotations.HostnamesFromAnnotations(rt.Metadata().Annotations)...)
}
// TODO: The combine-fqdn-annotation flag is similarly vague.
if c.src.fqdnTemplate != nil && (len(hostnames) == 0 || c.src.combineFQDNAnnotation) {
Expand All @@ -417,12 +420,23 @@ func (c *gatewayRouteResolver) hosts(rt gatewayRoute) ([]string, error) {
}
hostnames = append(hostnames, hosts...)
}
// This means that the route doesn't specify a hostname and should use any provided by
// attached Gateway Listeners. This is only useful for {HTTP,TLS}Routes, but it doesn't
// break {TCP,UDP}Routes.
if len(rt.Hostnames()) == 0 {
hostnames = append(hostnames, "")

hostNameAnnotation, hostNameAnnotationExists := rt.Metadata().Annotations[gatewayHostnameSourceKey]
if !hostNameAnnotationExists {
// This means that the route doesn't specify a hostname and should use any provided by
// attached Gateway Listeners. This is only useful for {HTTP,TLS}Routes, but it doesn't
// break {TCP,UDP}Routes.
if len(rt.Hostnames()) == 0 {
hostnames = append(hostnames, "")
}

hostnames = append(hostnames, annotationHostnames...)
}

if strings.ToLower(hostNameAnnotation) == gatewayHostnameSourceAnnotationOnlyValue {
return annotationHostnames, nil
}

return hostnames, nil
}

Expand Down
80 changes: 80 additions & 0 deletions source/gateway_httproute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1580,6 +1580,86 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) {
"Parent reference gateway-namespace/other-gateway not found in routeParentRefs for HTTPRoute route-namespace/test",
},
},
{
title: "SourceAnnotation",
config: Config{
GatewayNamespace: "gateway-namespace",
},
namespaces: namespaces("gateway-namespace", "route-namespace"),
gateways: []*v1beta1.Gateway{
{
ObjectMeta: objectMeta("gateway-namespace", "test"),
Spec: v1.GatewaySpec{
Listeners: []v1.Listener{{
Protocol: v1.HTTPProtocolType,
AllowedRoutes: allowAllNamespaces,
}},
},
Status: gatewayStatus("1.2.3.4"),
},
},
routes: []*v1beta1.HTTPRoute{
{
ObjectMeta: metav1.ObjectMeta{
Name: "route-test",
Namespace: "test",
Annotations: map[string]string{gatewayHostnameSourceKey: "defined-hosts-only", hostnameAnnotationKey: "test.org.internal"},
},
Spec: v1.HTTPRouteSpec{
Hostnames: hostnames("test.example.internal"),
CommonRouteSpec: v1.CommonRouteSpec{
ParentRefs: []v1.ParentReference{
gwParentRef("gateway-namespace", "test"),
},
},
},
Status: httpRouteStatus(gwParentRef("gateway-namespace", "test")),
},
},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("test.example.internal", "A", "1.2.3.4"),
},
},
{
title: "OnlyAnnotationHost",
config: Config{
GatewayNamespace: "gateway-namespace",
},
namespaces: namespaces("gateway-namespace", "route-namespace"),
gateways: []*v1beta1.Gateway{
{
ObjectMeta: objectMeta("gateway-namespace", "test"),
Spec: v1.GatewaySpec{
Listeners: []v1.Listener{{
Protocol: v1.HTTPProtocolType,
AllowedRoutes: allowAllNamespaces,
}},
},
Status: gatewayStatus("1.2.3.4"),
},
},
routes: []*v1beta1.HTTPRoute{
{
ObjectMeta: metav1.ObjectMeta{
Name: "route-test",
Namespace: "test",
Annotations: map[string]string{gatewayHostnameSourceKey: "annotation-only", hostnameAnnotationKey: "test.org.internal"},
},
Spec: v1.HTTPRouteSpec{
Hostnames: hostnames("test.example.internal"),
CommonRouteSpec: v1.CommonRouteSpec{
ParentRefs: []v1.ParentReference{
gwParentRef("gateway-namespace", "test"),
},
},
},
Status: httpRouteStatus(gwParentRef("gateway-namespace", "test")),
},
},
endpoints: []*endpoint.Endpoint{
newTestEndpoint("test.org.internal", "A", "1.2.3.4"),
},
},
}
for _, tt := range tests {

Expand Down
1 change: 1 addition & 0 deletions source/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const (
ingressHostnameSourceKey = annotations.IngressHostnameSourceKey
controllerAnnotationValue = annotations.ControllerValue
internalHostnameAnnotationKey = annotations.InternalHostnameKey
gatewayHostnameSourceKey = annotations.GatewayHostnameSourceKey

EndpointsTypeNodeExternalIP = "NodeExternalIP"
EndpointsTypeHostIP = "HostIP"
Expand Down
Loading