Skip to content
Draft
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
20 changes: 12 additions & 8 deletions docs/annotations/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The following table documents which sources support which annotations:
[^1]: Unless the `--ignore-hostname-annotation` flag is specified.
[^2]: Only behaves differently than `hostname` for `Service`s of type `ClusterIP` or `LoadBalancer`.
[^3]: Also supported on `Pods` referenced from a headless `Service`'s `Endpoints`.
[^4]: For Gateway API sources, annotation placement differs by type. See [Gateway API Annotation Placement](#gateway-api-annotation-placement) for details.
[^4]: For Gateway API sources, annotations support inheritance from Gateway to Route. See [Gateway API Annotation Inheritance](#gateway-api-annotation-inheritance) for details.
[^5]: The annotation must be on the listener's `VirtualService`.

## external-dns.alpha.kubernetes.io/access
Expand Down Expand Up @@ -305,12 +305,16 @@ Specifies the set identifier for DNS records generated by the resource.
A set identifier differentiates among multiple DNS record sets that have the same combination of domain and type.
Which record set or sets are returned to queries is then determined by the configured routing policy.

## Gateway API Annotation Placement
## Gateway API Annotation Inheritance

When using Gateway API sources (`gateway-httproute`, `gateway-grpcroute`, `gateway-tlsroute`, etc.), annotations
are read from different resources: **Gateway resource** reads only `target` annotation, while **Route resources**
(HTTPRoute, GRPCRoute, TLSRoute, etc.) read all other annotations (`hostname`, `ttl`, `controller`, and
provider-specific annotations like `cloudflare-*`, `aws-*`, `scw-*`).
When using Gateway API sources (`gateway-httproute`, `gateway-grpcroute`, `gateway-tlsroute`, etc.), ExternalDNS
supports **annotation inheritance** from Gateway to Route resources:

For more details and comprehensive examples, see the
[Gateway API documentation](../sources/gateway-api.md#annotations).
- **Gateway annotations** serve as **defaults** for all attached Routes
- **Route annotations** **override** Gateway annotations when the same key is specified
- All annotation types are supported: `target`, `ttl`, `hostname`, `controller`, and provider-specific annotations

This allows you to configure centralized defaults on the Gateway while still enabling per-Route customization.

For detailed examples including internal/external routing scenarios, see the
[Gateway API documentation](../sources/gateway-api.md#annotation-inheritance).
113 changes: 75 additions & 38 deletions docs/sources/gateway-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,24 @@ requests/connections won't recognize additional hostnames from the annotation.

## Annotations

### Annotation Placement
### Annotation Inheritance

ExternalDNS reads different annotations from different Gateway API resources:
ExternalDNS supports annotation inheritance from Gateway to Route resources. This allows you to:

- **Gateway annotations**: Only `external-dns.alpha.kubernetes.io/target` is read from Gateway resources
- **Route annotations**: All other annotations (hostname, ttl, controller, provider-specific) are read from Route
resources (HTTPRoute, GRPCRoute, TLSRoute, TCPRoute, UDPRoute)
- Set **default annotations** on Gateway that apply to all attached Routes
- **Override** specific annotations on individual Routes when needed

This separation aligns with Gateway API architecture where Gateway defines infrastructure (IP addresses, listeners)
and Routes define application-level DNS records.
**Inheritance rules:**

1. Gateway annotations serve as **defaults** for all Routes attached to that Gateway
2. Route annotations **override** Gateway annotations when both specify the same key
3. Each Route is independent — one Route's annotations don't affect other Routes

This approach reduces configuration duplication while maintaining flexibility for per-Route customization.

### Examples

#### Example: Cloudflare Proxied Records
#### Example: Centralized Defaults with Per-Route Overrides

```yaml
apiVersion: gateway.networking.k8s.io/v1
Expand All @@ -59,8 +63,12 @@ metadata:
name: my-gateway
namespace: default
annotations:
# ✅ Correct: target annotation on Gateway
external-dns.alpha.kubernetes.io/target: "203.0.113.1"
# Default target for all Routes (intranet)
external-dns.alpha.kubernetes.io/target: "172.16.6.6"
# Default TTL for all Routes
external-dns.alpha.kubernetes.io/ttl: "300"
# Default Cloudflare proxy setting
external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"
spec:
gatewayClassName: cilium
listeners:
Expand All @@ -72,15 +80,32 @@ spec:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-route
name: internal-api
# No annotations — inherits all defaults from Gateway:
# target=172.16.6.6, ttl=300, cloudflare-proxied=true
spec:
parentRefs:
- name: my-gateway
hostnames:
- api.internal.example.com
rules:
- backendRefs:
- name: api-service
port: 8080
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: external-api
annotations:
# ✅ Correct: provider-specific annotations on HTTPRoute
external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"
external-dns.alpha.kubernetes.io/ttl: "300"
# Override: use external IP instead of intranet
external-dns.alpha.kubernetes.io/target: "1.2.3.4"
# Override: shorter TTL for external endpoint
external-dns.alpha.kubernetes.io/ttl: "60"
# Inherits cloudflare-proxied=true from Gateway
spec:
parentRefs:
- name: my-gateway
namespace: default
hostnames:
- api.example.com
rules:
Expand All @@ -89,57 +114,69 @@ spec:
port: 8080
```

#### Example: AWS Route53 with Routing Policies
**Result:**

- `api.internal.example.com` → A record `172.16.6.6`, TTL 300, Cloudflare proxied
- `api.example.com` → A record `1.2.3.4`, TTL 60, Cloudflare proxied

#### Example: Different Record Types (A vs CNAME)

Routes can override Gateway's IP target with a hostname to create CNAME records:

```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: aws-gateway
name: my-gateway
annotations:
# ✅ Correct: target annotation on Gateway
external-dns.alpha.kubernetes.io/target: "alb-123.us-east-1.elb.amazonaws.com"
# Default: A record pointing to load balancer IP
external-dns.alpha.kubernetes.io/target: "10.0.0.1"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: weighted-route
name: cdn-route
annotations:
# ✅ Correct: AWS-specific annotations on HTTPRoute
external-dns.alpha.kubernetes.io/aws-weight: "100"
external-dns.alpha.kubernetes.io/set-identifier: "backend-v1"
# Override: CNAME record pointing to CDN
external-dns.alpha.kubernetes.io/target: "cdn.cloudprovider.com"
spec:
parentRefs:
- name: aws-gateway
- name: my-gateway
hostnames:
- app.example.com
- static.example.com
```

### Common Mistakes
**Result:** `static.example.com` → CNAME record `cdn.cloudprovider.com`

❌ **Incorrect**: Placing provider-specific annotations on Gateway
#### Example: AWS Route53 with Routing Policies

```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: aws-gateway
annotations:
# ❌ These annotations are ignored on Gateway
external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"
external-dns.alpha.kubernetes.io/ttl: "300"
```

❌ **Incorrect**: Placing target annotation on HTTPRoute

```yaml
external-dns.alpha.kubernetes.io/target: "alb-123.us-east-1.elb.amazonaws.com"
# Default set-identifier for all Routes
external-dns.alpha.kubernetes.io/set-identifier: "default"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: weighted-route
annotations:
# ❌ This annotation is ignored on Routes
external-dns.alpha.kubernetes.io/target: "203.0.113.1"
# Override set-identifier for this specific Route
external-dns.alpha.kubernetes.io/set-identifier: "backend-v1"
external-dns.alpha.kubernetes.io/aws-weight: "100"
spec:
parentRefs:
- name: aws-gateway
hostnames:
- app.example.com
```

For a complete list of supported annotations, see the
[annotations documentation](../annotations/annotations.md#gateway-api-annotation-placement).
[annotations documentation](../annotations/annotations.md).

## Manifest with RBAC

Expand Down
52 changes: 43 additions & 9 deletions source/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,21 +227,25 @@ func (src *gatewayRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpo
}

// Get Route hostnames and their targets.
hostTargets, err := resolver.resolve(rt)
result, err := resolver.resolve(rt)
if err != nil {
return nil, err
}
if len(hostTargets) == 0 {
if len(result.hostTargets) == 0 {
log.Debugf("No endpoints could be generated from %s %s/%s", src.rtKind, meta.Namespace, meta.Name)
continue
}

// Merge Gateway and Route annotations.
// Route annotations override Gateway annotations.
mergedAnnots := mergeAnnotations(result.gwAnnotations, annots)

// Create endpoints from hostnames and targets.
var routeEndpoints []*endpoint.Endpoint
resource := fmt.Sprintf("%s/%s/%s", kind, meta.Namespace, meta.Name)
providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(annots)
ttl := annotations.TTLFromAnnotations(annots, resource)
for host, targets := range hostTargets {
providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(mergedAnnots)
ttl := annotations.TTLFromAnnotations(mergedAnnots, resource)
for host, targets := range result.hostTargets {
routeEndpoints = append(routeEndpoints, EndpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...)
}
log.Debugf("Endpoints generated from %s %s/%s: %v", src.rtKind, meta.Namespace, meta.Name, routeEndpoints)
Expand All @@ -266,6 +270,25 @@ type gatewayListeners struct {
listeners map[v1.SectionName][]v1.Listener
}

// resolveResult contains the result of resolving a Route to its targets and Gateway annotations.
type resolveResult struct {
hostTargets map[string]endpoint.Targets
gwAnnotations map[string]string
}

// mergeAnnotations merges Gateway and Route annotations.
// Route annotations take precedence over Gateway annotations.
func mergeAnnotations(gateway, route map[string]string) map[string]string {
merged := make(map[string]string, len(gateway)+len(route))
for k, v := range gateway {
merged[k] = v
}
for k, v := range route {
merged[k] = v
}
return merged
}

func newGatewayRouteResolver(src *gatewayRouteSource, gateways []*v1beta1.Gateway, namespaces []*corev1.Namespace) *gatewayRouteResolver {
// Create Gateway Listener lookup table.
gws := make(map[types.NamespacedName]gatewayListeners, len(gateways))
Expand All @@ -292,18 +315,19 @@ func newGatewayRouteResolver(src *gatewayRouteSource, gateways []*v1beta1.Gatewa
}
}

func (c *gatewayRouteResolver) resolve(rt gatewayRoute) (map[string]endpoint.Targets, error) {
func (c *gatewayRouteResolver) resolve(rt gatewayRoute) (*resolveResult, error) {
rtHosts, err := c.hosts(rt)
if err != nil {
return nil, err
}
hostTargets := make(map[string]endpoint.Targets)
var gwAnnotations map[string]string

routeParentRefs := rt.ParentRefs()

if len(routeParentRefs) == 0 {
log.Debugf("No parent references found for %s %s/%s", c.src.rtKind, rt.Metadata().Namespace, rt.Metadata().Name)
return hostTargets, nil
return &resolveResult{hostTargets: hostTargets, gwAnnotations: gwAnnotations}, nil
}

meta := rt.Metadata()
Expand Down Expand Up @@ -341,6 +365,16 @@ func (c *gatewayRouteResolver) resolve(rt gatewayRoute) (map[string]endpoint.Tar
continue
}

// Capture Gateway annotations from first matched Gateway for inheritance.
// Route annotations will override these in Endpoints().
if gwAnnotations == nil {
gwAnnotations = gw.gateway.Annotations
}

// Merge Gateway and Route annotations for target resolution.
// Route target annotation overrides Gateway target annotation.
mergedAnnots := mergeAnnotations(gw.gateway.Annotations, meta.Annotations)

// Match the Route to all possible Listeners.
match := false
section := sectionVal(ref.SectionName, "")
Expand Down Expand Up @@ -377,7 +411,7 @@ func (c *gatewayRouteResolver) resolve(rt gatewayRoute) (map[string]endpoint.Tar
if !ok {
continue
}
override := annotations.TargetsFromTargetAnnotation(gw.gateway.Annotations)
override := annotations.TargetsFromTargetAnnotation(mergedAnnots)
hostTargets[host] = append(hostTargets[host], override...)
if len(override) == 0 {
for _, addr := range gw.gateway.Status.Addresses {
Expand All @@ -396,7 +430,7 @@ func (c *gatewayRouteResolver) resolve(rt gatewayRoute) (map[string]endpoint.Tar
for host, targets := range hostTargets {
hostTargets[host] = uniqueTargets(targets)
}
return hostTargets, nil
return &resolveResult{hostTargets: hostTargets, gwAnnotations: gwAnnotations}, nil
}

func (c *gatewayRouteResolver) hosts(rt gatewayRoute) ([]string, error) {
Expand Down
Loading