Skip to content

Commit aeee16a

Browse files
feat(source/gateway-api): add Gateway resource source
Add a new `gateway` source type that watches Gateway resources directly and publishes DNS records (A/AAAA/CNAME) from status.addresses when the gateway is annotated with external-dns.alpha.kubernetes.io/hostname. Unlike the route sources (gateway-httproute etc.) which resolve hostnames by traversing Route → parentRef → Gateway, this source targets the Gateway object itself — useful when DNS is managed at the infrastructure level rather than per-route. Supported features: - Hostname annotation (external-dns.alpha.kubernetes.io/hostname) - Target override annotation (external-dns.alpha.kubernetes.io/target) - TTL annotation, provider-specific annotations - GatewayName / GatewayNamespace / GatewayLabelFilter / AnnotationFilter - FQDN template (--fqdn-template flag) - Controller annotation mismatch filtering Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 51995b8 commit aeee16a

7 files changed

Lines changed: 481 additions & 4 deletions

File tree

docs/flags.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ tags:
4141
| `--[no-]ignore-ingress-tls-spec` | Ignore the spec.tls section in Ingress resources (default: false) |
4242
| `--[no-]ignore-non-host-network-pods` | Ignore pods not running on host network when using pod source (default: false) |
4343
| `--ingress-class=INGRESS-CLASS` | Require an Ingress to have this class name; specify multiple times to allow more than one class (optional; defaults to any class) |
44-
| `--label-filter=""` | Filter resources queried for endpoints by label selector; currently supported by source types crd, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, ingress, node, openshift-route, service and ambassador-host |
44+
| `--label-filter=""` | Filter resources queried for endpoints by label selector; currently supported by source types crd, gateway, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, ingress, node, openshift-route, service and ambassador-host |
4545
| `--managed-record-types=A...` | Record types to manage; specify multiple times to include many; (default: A,AAAA,CNAME) (supported records: A, AAAA, CNAME, NS, SRV, TXT) |
4646
| `--namespace=""` | Limit resources queried for endpoints to a specific namespace (default: all namespaces) |
4747
| `--nat64-networks=NAT64-NETWORKS` | Adding an A record for each AAAA record in NAT64-enabled networks; specify multiple times for multiple possible nets (optional) |
@@ -196,4 +196,4 @@ tags:
196196
| `--kube-api-qps=5` | Maximum QPS to the Kubernetes API server from this client. |
197197
| `--kube-api-burst=10` | Maximum burst for throttle to the Kubernetes API server from this client. |
198198
| `--provider=provider` | The DNS provider where the DNS records will be created (required, options: akamai, alibabacloud, aws, aws-sd, azure, azure-dns, azure-private-dns, civo, cloudflare, coredns, dnsimple, exoscale, gandi, godaddy, google, inmemory, linode, ns1, oci, ovh, pdns, pihole, plural, rfc2136, scaleway, skydns, transip, webhook) |
199-
| `--source=source` | The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, pod, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, istio-gateway, istio-virtualservice, contour-httpproxy, gloo-proxy, fake, connector, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress, f5-virtualserver, f5-transportserver, traefik-proxy, unstructured) |
199+
| `--source=source` | The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, pod, gateway, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, istio-gateway, istio-virtualservice, contour-httpproxy, gloo-proxy, fake, connector, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress, f5-virtualserver, f5-transportserver, traefik-proxy, unstructured) |

docs/sources/gateway-api.md

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,66 @@
1-
# Gateway API Route Sources
1+
# Gateway API Sources
22

3-
This describes how to configure ExternalDNS to use Gateway API Route sources.
3+
This describes how to configure ExternalDNS to use Gateway API sources.
44
It is meant to supplement the other provider-specific setup tutorials.
55

6+
## Gateway Source (`--source=gateway`)
7+
8+
The `gateway` source watches `Gateway` resources directly and creates DNS records
9+
for each Gateway that has the `external-dns.alpha.kubernetes.io/hostname` annotation.
10+
Targets are taken from `gateway.status.addresses` (or overridden via the
11+
`external-dns.alpha.kubernetes.io/target` annotation).
12+
13+
### When to use
14+
15+
Use `--source=gateway` when you want DNS records tied to the Gateway itself rather
16+
than individual routes. This is useful when:
17+
18+
- All routes share the same DNS name as the Gateway's external address.
19+
- You manage hostnames at the Gateway level rather than per-route.
20+
21+
### How it works
22+
23+
1. ExternalDNS lists all `Gateway` objects in the configured namespace.
24+
2. For each Gateway with `external-dns.alpha.kubernetes.io/hostname`, it reads the value as one or more comma-separated hostnames.
25+
3. Targets are sourced from `gateway.status.addresses`. IP addresses produce A/AAAA records; hostnames produce CNAME records.
26+
4. The `external-dns.alpha.kubernetes.io/target` annotation overrides `status.addresses`.
27+
28+
### Example
29+
30+
```yaml
31+
apiVersion: gateway.networking.k8s.io/v1
32+
kind: Gateway
33+
metadata:
34+
name: my-gateway
35+
namespace: default
36+
annotations:
37+
external-dns.alpha.kubernetes.io/hostname: app.example.com
38+
external-dns.alpha.kubernetes.io/ttl: "300"
39+
spec:
40+
gatewayClassName: cilium
41+
listeners:
42+
- name: https
43+
protocol: HTTPS
44+
port: 443
45+
status:
46+
addresses:
47+
- type: IPAddress
48+
value: 203.0.113.1
49+
```
50+
51+
This produces an A record: `app.example.com → 203.0.113.1`.
52+
53+
### Supported flags
54+
55+
| Flag | Effect on `gateway` source |
56+
|------|---------------------------|
57+
| `--gateway-name` | Limit to a single Gateway by name |
58+
| `--gateway-namespace` | Limit to Gateways in a specific namespace |
59+
| `--gateway-label-filter` | Filter Gateways by label selector |
60+
| `--annotation-filter` | Filter Gateways by annotation selector |
61+
| `--ignore-hostname-annotation` | Skip the hostname annotation (useful with `--fqdn-template`) |
62+
| `--fqdn-template` | Generate hostnames from a Go template applied to the Gateway object |
63+
664
## Supported API Versions
765

866
ExternalDNS uses Gateway API CRDs, which are distributed at different versions in Standard and/or

docs/sources/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Sources are responsible for:
3232
| **f5-transportserver** | annotation | all,single | false | false | false | load balancers | TransportServer.cis.f5.com |
3333
| **f5-virtualserver** | annotation | all,single | false | false | false | load balancers | VirtualServer.cis.f5.com |
3434
| **fake** | | | true | true | false | testing | Fake Endpoints |
35+
| **gateway** | annotation,label | all,single | true | false | true | gateway api | Gateway.gateway.networking.k8s.io |
3536
| **gateway-grpcroute** | annotation,label | all,single | true | false | true | gateway api | GRPCRoute.gateway.networking.k8s.io |
3637
| **gateway-httproute** | annotation,label | all,single | true | false | true | gateway api | HTTPRoute.gateway.networking.k8s.io |
3738
| **gateway-tcproute** | annotation,label | all,single | true | false | true | gateway api | TCPRoute.gateway.networking.k8s.io |

source/gateway.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,153 @@ type gatewayRouteSource struct {
151151
ignoreHostnameAnnotation bool
152152
}
153153

154+
func NewGatewaySource(ctx context.Context, clients ClientGenerator, config *Config) (Source, error) {
155+
return newGatewaySource(ctx, clients, config, newGatewayInformerFactory)
156+
}
157+
158+
// +externaldns:source:name=gateway
159+
// +externaldns:source:category=Gateway API
160+
// +externaldns:source:description=Creates DNS entries from Gateway API Gateway resources annotated with external-dns.alpha.kubernetes.io/hostname
161+
// +externaldns:source:resources=Gateway.gateway.networking.k8s.io
162+
// +externaldns:source:filters=annotation,label
163+
// +externaldns:source:namespace=all,single
164+
// +externaldns:source:fqdn-template=true
165+
// +externaldns:source:provider-specific=true
166+
type gatewayResourceSource struct {
167+
gwName string
168+
gwNamespace string
169+
gwLabels labels.Selector
170+
gwAnnotations labels.Selector
171+
gwInformer informers_v1.GatewayInformer
172+
173+
templateEngine template.Engine
174+
ignoreHostnameAnnotation bool
175+
}
176+
177+
func newGatewaySource(
178+
ctx context.Context,
179+
clients ClientGenerator,
180+
config *Config,
181+
newInformerFactory func(gateway.Interface, string, labels.Selector) gwinformers.SharedInformerFactory,
182+
) (Source, error) {
183+
gwLabels, err := getLabelSelector(config.GatewayLabelFilter)
184+
if err != nil {
185+
return nil, err
186+
}
187+
gwAnnotations, err := getLabelSelector(config.AnnotationFilter)
188+
if err != nil {
189+
return nil, err
190+
}
191+
192+
client, err := clients.GatewayClient()
193+
if err != nil {
194+
return nil, err
195+
}
196+
197+
gwInformerFactory := newInformerFactory(client, config.GatewayNamespace, gwLabels)
198+
gwInformer := gwInformerFactory.Gateway().V1().Gateways()
199+
gwInformer.Informer() // Register with factory before starting.
200+
201+
gwInformerFactory.Start(ctx.Done())
202+
if err := informers.WaitForCacheSync(ctx, gwInformerFactory); err != nil {
203+
return nil, err
204+
}
205+
206+
return &gatewayResourceSource{
207+
gwName: config.GatewayName,
208+
gwNamespace: config.GatewayNamespace,
209+
gwLabels: gwLabels,
210+
gwAnnotations: gwAnnotations,
211+
gwInformer: gwInformer,
212+
templateEngine: config.TemplateEngine,
213+
ignoreHostnameAnnotation: config.IgnoreHostnameAnnotation,
214+
}, nil
215+
}
216+
217+
func (src *gatewayResourceSource) AddEventHandler(_ context.Context, handler func()) {
218+
log.Debug("Adding event handlers for Gateway")
219+
informers.MustAddEventHandler(src.gwInformer.Informer(), eventHandlerFunc(handler))
220+
}
221+
222+
func (src *gatewayResourceSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, error) {
223+
gateways, err := src.gwInformer.Lister().Gateways(src.gwNamespace).List(src.gwLabels)
224+
if err != nil {
225+
return nil, err
226+
}
227+
228+
var endpoints []*endpoint.Endpoint
229+
for _, gw := range gateways {
230+
if src.gwName != "" && src.gwName != gw.Name {
231+
continue
232+
}
233+
234+
meta := &gw.ObjectMeta
235+
annots := meta.Annotations
236+
237+
if !src.gwAnnotations.Matches(labels.Set(annots)) {
238+
continue
239+
}
240+
241+
if annotations.IsControllerMismatch(meta, gatewayKind) {
242+
continue
243+
}
244+
245+
hostnames, err := src.hostnames(gw)
246+
if err != nil {
247+
return nil, err
248+
}
249+
if len(hostnames) == 0 {
250+
log.Debugf("No endpoints could be generated from Gateway %s/%s", gw.Namespace, gw.Name)
251+
continue
252+
}
253+
254+
targets := src.targets(gw)
255+
if len(targets) == 0 {
256+
log.Debugf("No targets found for Gateway %s/%s", gw.Namespace, gw.Name)
257+
continue
258+
}
259+
260+
resource := fmt.Sprintf("gateway/%s/%s", gw.Namespace, gw.Name)
261+
providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(annots)
262+
ttl := annotations.TTLFromAnnotations(annots, resource)
263+
264+
var gwEndpoints []*endpoint.Endpoint
265+
for _, host := range hostnames {
266+
gwEndpoints = append(gwEndpoints, endpoint.EndpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...)
267+
}
268+
log.Debugf("Endpoints generated from Gateway %s/%s: %v", gw.Namespace, gw.Name, gwEndpoints)
269+
endpoints = append(endpoints, gwEndpoints...)
270+
}
271+
return MergeEndpoints(endpoints), nil
272+
}
273+
274+
func (src *gatewayResourceSource) hostnames(gw *v1.Gateway) ([]string, error) {
275+
var hostnames []string
276+
if !src.ignoreHostnameAnnotation {
277+
hostnames = append(hostnames, annotations.HostnamesFromAnnotations(gw.Annotations)...)
278+
}
279+
if src.templateEngine.IsConfigured() && (len(hostnames) == 0 || src.templateEngine.Combining()) {
280+
hosts, err := src.templateEngine.ExecFQDN(gw)
281+
if err != nil {
282+
return nil, err
283+
}
284+
hostnames = append(hostnames, hosts...)
285+
}
286+
return hostnames, nil
287+
}
288+
289+
func (src *gatewayResourceSource) targets(gw *v1.Gateway) endpoint.Targets {
290+
override := annotations.TargetsFromTargetAnnotation(gw.Annotations)
291+
if len(override) > 0 {
292+
return override
293+
}
294+
var targets endpoint.Targets
295+
for _, addr := range gw.Status.Addresses {
296+
targets = append(targets, addr.Value)
297+
}
298+
return targets
299+
}
300+
154301
func newGatewayRouteSource(
155302
ctx context.Context,
156303
clients ClientGenerator,

0 commit comments

Comments
 (0)