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
4 changes: 2 additions & 2 deletions docs/flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ tags:
| `--[no-]ignore-ingress-tls-spec` | Ignore the spec.tls section in Ingress resources (default: false) |
| `--[no-]ignore-non-host-network-pods` | Ignore pods not running on host network when using pod source (default: false) |
| `--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) |
| `--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 |
| `--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 |
| `--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) |
| `--namespace=""` | Limit resources queried for endpoints to a specific namespace (default: all namespaces) |
| `--nat64-networks=NAT64-NETWORKS` | Adding an A record for each AAAA record in NAT64-enabled networks; specify multiple times for multiple possible nets (optional) |
Expand Down Expand Up @@ -196,4 +196,4 @@ tags:
| `--kube-api-qps=5` | Maximum QPS to the Kubernetes API server from this client. |
| `--kube-api-burst=10` | Maximum burst for throttle to the Kubernetes API server from this client. |
| `--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) |
| `--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) |
| `--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) |
62 changes: 60 additions & 2 deletions docs/sources/gateway-api.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,66 @@
# Gateway API Route Sources
# Gateway API Sources

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

## Gateway Source (`--source=gateway`)

The `gateway` source watches `Gateway` resources directly and creates DNS records
for each Gateway that has the `external-dns.alpha.kubernetes.io/hostname` annotation.
Targets are taken from `gateway.status.addresses` (or overridden via the
`external-dns.alpha.kubernetes.io/target` annotation).

### When to use

Use `--source=gateway` when you want DNS records tied to the Gateway itself rather
than individual routes. This is useful when:

- All routes share the same DNS name as the Gateway's external address.
- You manage hostnames at the Gateway level rather than per-route.

### How it works

1. ExternalDNS lists all `Gateway` objects in the configured namespace.
2. For each Gateway with `external-dns.alpha.kubernetes.io/hostname`, it reads the value as one or more comma-separated hostnames.
3. Targets are sourced from `gateway.status.addresses`. IP addresses produce A/AAAA records; hostnames produce CNAME records.
4. The `external-dns.alpha.kubernetes.io/target` annotation overrides `status.addresses`.

### Example

```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-gateway
namespace: default
annotations:
external-dns.alpha.kubernetes.io/hostname: app.example.com
external-dns.alpha.kubernetes.io/ttl: "300"
spec:
gatewayClassName: cilium
listeners:
- name: https
protocol: HTTPS
port: 443
status:
addresses:
- type: IPAddress
value: 203.0.113.1
```

This produces an A record: `app.example.com → 203.0.113.1`.

### Supported flags

| Flag | Effect on `gateway` source |
|------|---------------------------|
| `--gateway-name` | Limit to a single Gateway by name |
| `--gateway-namespace` | Limit to Gateways in a specific namespace |
| `--gateway-label-filter` | Filter Gateways by label selector |
| `--annotation-filter` | Filter Gateways by annotation selector |
| `--ignore-hostname-annotation` | Skip the hostname annotation (useful with `--fqdn-template`) |
| `--fqdn-template` | Generate hostnames from a Go template applied to the Gateway object |

## Supported API Versions

ExternalDNS uses Gateway API CRDs, which are distributed at different versions in Standard and/or
Expand Down
1 change: 1 addition & 0 deletions docs/sources/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Sources are responsible for:
| **f5-transportserver** | annotation | all,single | false | false | false | load balancers | TransportServer.cis.f5.com |
| **f5-virtualserver** | annotation | all,single | false | false | false | load balancers | VirtualServer.cis.f5.com |
| **fake** | | | true | true | false | testing | Fake Endpoints |
| **gateway** | annotation,label | all,single | true | false | true | gateway api | Gateway.gateway.networking.k8s.io |
| **gateway-grpcroute** | annotation,label | all,single | true | false | true | gateway api | GRPCRoute.gateway.networking.k8s.io |
| **gateway-httproute** | annotation,label | all,single | true | false | true | gateway api | HTTPRoute.gateway.networking.k8s.io |
| **gateway-tcproute** | annotation,label | all,single | true | false | true | gateway api | TCPRoute.gateway.networking.k8s.io |
Expand Down
147 changes: 147 additions & 0 deletions source/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,153 @@ type gatewayRouteSource struct {
ignoreHostnameAnnotation bool
}

func NewGatewaySource(ctx context.Context, clients ClientGenerator, config *Config) (Source, error) {
return newGatewaySource(ctx, clients, config, newGatewayInformerFactory)
}

// +externaldns:source:name=gateway
// +externaldns:source:category=Gateway API
// +externaldns:source:description=Creates DNS entries from Gateway API Gateway resources annotated with external-dns.alpha.kubernetes.io/hostname
// +externaldns:source:resources=Gateway.gateway.networking.k8s.io
// +externaldns:source:filters=annotation,label
// +externaldns:source:namespace=all,single
// +externaldns:source:fqdn-template=true
// +externaldns:source:provider-specific=true
type gatewayResourceSource struct {
gwName string
gwNamespace string
gwLabels labels.Selector
gwAnnotations labels.Selector
gwInformer informers_v1.GatewayInformer

templateEngine template.Engine
ignoreHostnameAnnotation bool
}

func newGatewaySource(
ctx context.Context,
clients ClientGenerator,
config *Config,
newInformerFactory func(gateway.Interface, string, labels.Selector) gwinformers.SharedInformerFactory,
) (Source, error) {
gwLabels, err := getLabelSelector(config.GatewayLabelFilter)
if err != nil {
return nil, err
}
gwAnnotations, err := getLabelSelector(config.AnnotationFilter)
if err != nil {
return nil, err
}

client, err := clients.GatewayClient()
if err != nil {
return nil, err
}

gwInformerFactory := newInformerFactory(client, config.GatewayNamespace, gwLabels)
gwInformer := gwInformerFactory.Gateway().V1().Gateways()
gwInformer.Informer() // Register with factory before starting.

gwInformerFactory.Start(ctx.Done())
if err := informers.WaitForCacheSync(ctx, gwInformerFactory); err != nil {
return nil, err
}

return &gatewayResourceSource{
gwName: config.GatewayName,
gwNamespace: config.GatewayNamespace,
gwLabels: gwLabels,
gwAnnotations: gwAnnotations,
gwInformer: gwInformer,
templateEngine: config.TemplateEngine,
ignoreHostnameAnnotation: config.IgnoreHostnameAnnotation,
}, nil
}

func (src *gatewayResourceSource) AddEventHandler(_ context.Context, handler func()) {
log.Debug("Adding event handlers for Gateway")
informers.MustAddEventHandler(src.gwInformer.Informer(), eventHandlerFunc(handler))
}

func (src *gatewayResourceSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, error) {
gateways, err := src.gwInformer.Lister().Gateways(src.gwNamespace).List(src.gwLabels)
if err != nil {
return nil, err
}

var endpoints []*endpoint.Endpoint
for _, gw := range gateways {
if src.gwName != "" && src.gwName != gw.Name {
continue
}

meta := &gw.ObjectMeta
annots := meta.Annotations

if !src.gwAnnotations.Matches(labels.Set(annots)) {
continue
}

if annotations.IsControllerMismatch(meta, gatewayKind) {
continue
}

hostnames, err := src.hostnames(gw)
if err != nil {
return nil, err
}
if len(hostnames) == 0 {
log.Debugf("No endpoints could be generated from Gateway %s/%s", gw.Namespace, gw.Name)
continue
}

targets := src.targets(gw)
if len(targets) == 0 {
log.Debugf("No targets found for Gateway %s/%s", gw.Namespace, gw.Name)
continue
}

resource := fmt.Sprintf("gateway/%s/%s", gw.Namespace, gw.Name)
providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(annots)
ttl := annotations.TTLFromAnnotations(annots, resource)

var gwEndpoints []*endpoint.Endpoint
for _, host := range hostnames {
gwEndpoints = append(gwEndpoints, endpoint.EndpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...)
}
log.Debugf("Endpoints generated from Gateway %s/%s: %v", gw.Namespace, gw.Name, gwEndpoints)
endpoints = append(endpoints, gwEndpoints...)
}
return MergeEndpoints(endpoints), nil
}

func (src *gatewayResourceSource) hostnames(gw *v1.Gateway) ([]string, error) {
var hostnames []string
if !src.ignoreHostnameAnnotation {
hostnames = append(hostnames, annotations.HostnamesFromAnnotations(gw.Annotations)...)
}
if src.templateEngine.IsConfigured() && (len(hostnames) == 0 || src.templateEngine.Combining()) {
hosts, err := src.templateEngine.ExecFQDN(gw)
if err != nil {
return nil, err
}
hostnames = append(hostnames, hosts...)
}
return hostnames, nil
}

func (src *gatewayResourceSource) targets(gw *v1.Gateway) endpoint.Targets {
override := annotations.TargetsFromTargetAnnotation(gw.Annotations)
if len(override) > 0 {
return override
}
var targets endpoint.Targets
for _, addr := range gw.Status.Addresses {
targets = append(targets, addr.Value)
}
return targets
}

func newGatewayRouteSource(
ctx context.Context,
clients ClientGenerator,
Expand Down
Loading