Skip to content

Commit b8cba40

Browse files
feat(source): add unstructured source
Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
1 parent e8bb3d8 commit b8cba40

File tree

9 files changed

+725
-329
lines changed

9 files changed

+725
-329
lines changed

docs/flags.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
| `--exclude-target-net=EXCLUDE-TARGET-NET` | Exclude target nets (optional) |
2929
| `--[no-]exclude-unschedulable` | Exclude nodes that are considered unschedulable (default: true) |
3030
| `--[no-]expose-internal-ipv6` | When using the node source, expose internal IPv6 addresses (optional, default: false) |
31-
| `--fqdn-template=""` | A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional). Accepts comma separated list for multiple global FQDN. |
3231
| `--gateway-label-filter=""` | Filter Gateways of Route endpoints via label selector (default: all gateways) |
3332
| `--gateway-name=""` | Limit Gateways of Route endpoints to a specific name (default: all names) |
3433
| `--gateway-namespace=""` | Limit Gateways of Route endpoints to a specific namespace (default: all namespaces) |
@@ -49,8 +48,7 @@
4948
| `--target-net-filter=TARGET-NET-FILTER` | Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional) |
5049
| `--[no-]traefik-enable-legacy` | Enable legacy listeners on Resources under the traefik.containo.us API Group |
5150
| `--[no-]traefik-disable-new` | Disable listeners on Resources under the traefik.io API Group |
52-
| `--unstructured-fqdn-resource=UNSTRUCTURED-FQDN-RESOURCE` | When using the unstructured-fqdn source, specify resources in resource.version.group format (e.g., virtualmachineinstances.v1.kubevirt.io); specify multiple times for multiple resources |
53-
| `--fqdn-target-template=""` | When using the unstructured source, specify the target FQDN template for DNS records |
51+
| `--unstructured-resource=UNSTRUCTURED-RESOURCE` | When using the unstructured source, specify resources in resource.version.group format (e.g., virtualmachineinstances.v1.kubevirt.io, configmap.v1); specify multiple times for multiple resources |
5452
| `--events-emit=EVENTS-EMIT` | Events that should be emitted. Specify multiple times for multiple events support (optional, default: none, expected: RecordReady, RecordDeleted, RecordError) |
5553
| `--provider-cache-time=0s` | The time to cache the DNS provider record list requests. |
5654
| `--domain-filter=` | Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional) |
@@ -182,5 +180,8 @@
182180
| `--webhook-provider-read-timeout=5s` | The read timeout for the webhook provider in duration format (default: 5s) |
183181
| `--webhook-provider-write-timeout=10s` | The write timeout for the webhook provider in duration format (default: 10s) |
184182
| `--[no-]webhook-server` | When enabled, runs as a webhook server instead of a controller. (default: false). |
183+
| `--fqdn-template=""` | A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional). Accepts comma separated list for multiple global FQDN. |
184+
| `--fqdn-target-template=""` | When using the unstructured source, specify the target FQDN template for DNS records |
185+
| `--fqdn-host-target-template=""` | When using the unstructured source, specify a template that returns host:target pairs (e.g., '{{range .Object.endpoints}}{{.targetRef.name}}.svc.example.com:{{index .addresses 0}},{{end}}'). Mutually exclusive with --fqdn-template and --fqdn-target-template |
185186
| `--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, digitalocean, dnsimple, exoscale, gandi, godaddy, google, inmemory, linode, ns1, oci, ovh, pdns, pihole, plural, rfc2136, scaleway, skydns, transip, webhook) |
186187
| `--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) |

docs/sources/unstructured.md

Lines changed: 175 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@ Use this source when:
1111
- The resource exposes DNS-relevant data (hostnames, IPs, endpoints) in `.spec` or `.status`
1212
- A built-in source exists but only supports an older API version than you're using
1313
- You want to experiment with custom controllers or meshes but keep external-dns
14-
15-
Example CRDs:
16-
17-
- KubeVirt VirtualMachineInstances
18-
- Crossplane managed resources (RDS, ElastiCache, S3, etc.)
19-
- ArgoCD Applications
14+
- Create DNS entries based on any attribute of any Kubernetes object
15+
- Use ConfigMaps as a lightweight DNS registry without needing custom CRDs
16+
- Crossplane managed resources (RDS, ElastiCache, S3, etc.)
17+
- Support Endpoints directly
18+
- Allows the community to support new CRDs via configuration rather than code changes
2019

2120
> **Note**: Prefer built-in sources when available (e.g., `istio-virtualservice`, `gateway-httproute`) as they provide optimized handling for those resource types.
2221
@@ -65,13 +64,32 @@ status:
6564
status: "True"
6665
```
6766

67+
**ACK FieldExport** - AWS Controllers for Kubernetes can export resource status (RDS endpoints, S3 bucket URLs) to ConfigMaps via FieldExport, enabling dynamic DNS records
68+
69+
```yaml
70+
# FieldExport copies S3 bucket URL to ConfigMap
71+
apiVersion: services.k8s.aws/v1alpha1
72+
kind: FieldExport
73+
spec:
74+
from:
75+
path: ".status.location"
76+
resource:
77+
group: s3.services.k8s.aws
78+
kind: Bucket
79+
name: my-bucket
80+
to:
81+
kind: configmap
82+
name: bucket-dns
83+
```
84+
6885
## Configuration
6986

7087
| Flag | Description |
7188
|--------------------------------|--------------------------------------------------------------------|
72-
| `--unstructured-fqdn-resource` | Resources to watch in `resource.version.group` format (repeatable) |
89+
| `--unstructured-resource` | Resources to watch in `resource.version.group` format (repeatable) |
7390
| `--fqdn-template` | Go template for DNS names |
7491
| `--fqdn-target-template` | Go template for DNS targets |
92+
| `--fqdn-host-target-template` | Go template returning `host:target` pairs (mutually exclusive with above two) |
7593
| `--label-filter` | Filter resources by labels |
7694
| `--annotation-filter` | Filter resources by annotations |
7795

@@ -93,12 +111,41 @@ Templates have access to typed-style fields and raw object data:
93111

94112
## Examples
95113

114+
### ConfigMap DNS Registry
115+
116+
Use ConfigMaps as a lightweight DNS registry without needing custom CRDs. Useful for GitOps workflows where teams manage DNS entries via ConfigMaps in their namespaces.
117+
118+
```yaml
119+
apiVersion: v1
120+
kind: ConfigMap
121+
metadata:
122+
name: api-dns
123+
namespace: production
124+
labels:
125+
external-dns.alpha.kubernetes.io/dns-controller: "dns-controller"
126+
data:
127+
hostname: api.example.com
128+
target: 10.0.0.100
129+
```
130+
131+
```bash
132+
external-dns \
133+
--source=unstructured \
134+
--unstructured-resource=configmaps.v1 \
135+
--fqdn-template='{{index .Object.data "hostname"}}' \
136+
--fqdn-target-template='{{index .Object.data "target"}}' \
137+
--label-filter='external-dns.alpha.kubernetes.io/controller=dns-controller'
138+
139+
# Result:
140+
# api.example.com -> 10.0.0.100 (A)
141+
```
142+
96143
### Crossplane RDS Instance
97144

98145
```bash
99146
external-dns \
100147
--source=unstructured \
101-
--unstructured-fqdn-resource=rdsinstances.v1alpha1.rds.aws.crossplane.io \
148+
--unstructured-resource=rdsinstances.v1alpha1.rds.aws.crossplane.io \
102149
--fqdn-template='{{.Name}}.db.example.com' \
103150
--fqdn-target-template='{{.Status.atProvider.endpoint.address}}'
104151
```
@@ -108,8 +155,8 @@ external-dns \
108155
```bash
109156
external-dns \
110157
--source=unstructured \
111-
--unstructured-fqdn-resource=virtualmachineinstances.v1.kubevirt.io \
112-
--unstructured-fqdn-resource=rdsinstances.v1alpha1.rds.aws.crossplane.io \
158+
--unstructured-resource=virtualmachineinstances.v1.kubevirt.io \
159+
--unstructured-resource=rdsinstances.v1alpha1.rds.aws.crossplane.io \
113160
--fqdn-template='{{.Name}}.{{.Kind}}.example.com' \
114161
--fqdn-target-template='{{.Status.endpoint}}'
115162
```
@@ -132,14 +179,16 @@ spec:
132179
```bash
133180
external-dns \
134181
--source=unstructured \
135-
--unstructured-fqdn-resource=ipaddresspools.v1beta1.metallb.io \
182+
--unstructured-resource=ipaddresspools.v1beta1.metallb.io \
136183
--fqdn-template='{{index .Annotations "external-dns.alpha.kubernetes.io/hostname"}}' \
137-
--fqdn-target-template='{{index .Spec.addresses 0}}'
184+
--fqdn-target-template='{{$addr := index .Spec.addresses 0}}{{if contains $addr "/32"}}{{trimSuffix $addr "/32"}}{{else}}{{$addr}}{{end}}'
138185
139186
# Result:
140-
# lb.example.com -> 192.168.10.11/32 (CNAME)
187+
# lb.example.com -> 192.168.10.11 (A)
141188
```
142189

190+
> **Tip**: Use `contains` with `trimSuffix` to extract the IP from `/32` CIDR notation.
191+
143192
### Apache APISIX Route
144193

145194
```yaml
@@ -167,7 +216,7 @@ status:
167216
```bash
168217
external-dns \
169218
--source=unstructured \
170-
--unstructured-fqdn-resource=apisixroutes.v2.apisix.apache.org \
219+
--unstructured-resource=apisixroutes.v2.apisix.apache.org \
171220
--fqdn-template='{{.Name}}.route.example.com' \
172221
--fqdn-target-template='{{.Status.apisix.gateway}}'
173222
@@ -198,7 +247,7 @@ spec:
198247
```bash
199248
external-dns \
200249
--source=unstructured \
201-
--unstructured-fqdn-resource=certificates.v1.cert-manager.io \
250+
--unstructured-resource=certificates.v1.cert-manager.io \
202251
--fqdn-template='{{index .Spec.dnsNames 0}}' \
203252
--fqdn-target-template='{{index .Annotations "external-dns.alpha.kubernetes.io/target"}}'
204253
@@ -231,7 +280,7 @@ status:
231280
```bash
232281
external-dns \
233282
--source=unstructured \
234-
--unstructured-fqdn-resource=nodes.v3.management.cattle.io \
283+
--unstructured-resource=nodes.v3.management.cattle.io \
235284
--fqdn-template='{{.Spec.hostname}}.nodes.example.com' \
236285
--fqdn-target-template='{{(index .Status.internalNodeStatus.addresses 0).address}}' \
237286
--label-filter='node-role.kubernetes.io/controlplane=true'
@@ -240,6 +289,116 @@ external-dns \
240289
# my-node-1.nodes.example.com -> 203.0.113.10 (A)
241290
```
242291

292+
### ACK FieldExport with ConfigMap
293+
294+
Use AWS Controllers for Kubernetes (ACK) to dynamically populate ConfigMaps with resource endpoints. FieldExport copies values from ACK-managed resources (RDS, S3, ElastiCache) to ConfigMaps, which external-dns can then use for DNS records.
295+
296+
```yaml
297+
# 1. ACK creates an S3 bucket
298+
apiVersion: s3.services.k8s.aws/v1alpha1
299+
kind: Bucket
300+
metadata:
301+
name: app-assets
302+
namespace: default
303+
spec:
304+
name: my-app-assets-bucket
305+
---
306+
# 2. FieldExport copies the bucket URL to a ConfigMap
307+
apiVersion: services.k8s.aws/v1alpha1
308+
kind: FieldExport
309+
metadata:
310+
name: export-bucket-url
311+
namespace: default
312+
spec:
313+
from:
314+
path: ".status.location"
315+
resource:
316+
group: s3.services.k8s.aws
317+
kind: Bucket
318+
name: app-assets
319+
to:
320+
kind: configmap
321+
name: app-assets-dns
322+
namespace: default
323+
---
324+
# 3. ConfigMap is populated by FieldExport
325+
apiVersion: v1
326+
kind: ConfigMap
327+
metadata:
328+
name: app-assets-dns
329+
namespace: default
330+
labels:
331+
app.kubernetes.io/managed-by: ack-fieldexport
332+
data:
333+
default.export-bucket-url: "https://my-app-assets-bucket.s3.amazonaws.com/"
334+
```
335+
336+
```bash
337+
external-dns \
338+
--source=unstructured \
339+
--unstructured-resource=configmaps.v1 \
340+
--fqdn-template='{{if eq .Kind "ConfigMap"}}{{.Name}}.cdn.example.com{{end}}' \
341+
--fqdn-target-template='{{if eq .Kind "ConfigMap"}}{{$url := index .Object.data "default.export-bucket-url"}}{{trimSuffix (trimPrefix $url "https://") "/"}}{{end}}' \
342+
--label-filter='app.kubernetes.io/managed-by=ack-fieldexport'
343+
344+
# Result:
345+
# app-assets-dns.cdn.example.com -> my-app-assets-bucket.s3.amazonaws.com (CNAME)
346+
```
347+
348+
### EndpointSlice for Headless Services
349+
350+
Create per-pod DNS records from EndpointSlice resources for headless services. Each pod gets its own DNS entry pointing to its IP address.
351+
352+
```yaml
353+
apiVersion: discovery.k8s.io/v1
354+
kind: EndpointSlice
355+
metadata:
356+
name: test-headless-abc12
357+
namespace: default
358+
labels:
359+
endpointslice.kubernetes.io/managed-by: endpointslice-controller.k8s.io
360+
kubernetes.io/service-name: test-headless
361+
service.kubernetes.io/headless: ""
362+
addressType: IPv4
363+
endpoints:
364+
- addresses:
365+
- 10.244.1.2
366+
conditions:
367+
ready: true
368+
nodeName: worker1
369+
targetRef:
370+
kind: Pod
371+
name: app-abc12
372+
namespace: default
373+
- addresses:
374+
- 10.244.2.3
375+
- 10.244.2.4
376+
conditions:
377+
ready: true
378+
nodeName: worker2
379+
targetRef:
380+
kind: Pod
381+
name: app-def34
382+
namespace: default
383+
ports:
384+
- name: http
385+
port: 80
386+
protocol: TCP
387+
```
388+
389+
```bash
390+
external-dns \
391+
--source=unstructured \
392+
--unstructured-resource=endpointslices.v1.discovery.k8s.io \
393+
--fqdn-host-target-template='{{if and (eq .Kind "EndpointSlice") (hasKey .Labels "service.kubernetes.io/headless")}}{{range $ep := .Object.endpoints}}{{if $ep.conditions.ready}}{{range $ep.addresses}}{{$ep.targetRef.name}}.pod.com:{{.}},{{end}}{{end}}{{end}}{{end}}'
394+
395+
# Result:
396+
# app-abc12.pod.com -> 10.244.1.2 (A)
397+
# app-def34.pod.com -> 10.244.2.3, 10.244.2.4 (A)
398+
```
399+
400+
The `--fqdn-host-target-template` flag returns `host:target` pairs, enabling 1:1 mapping between hostnames and targets. Useful when a Kubernetes resource contains arrays where each element should produce its own DNS record (e.g., EndpointSlice endpoints, multi-host configurations).
401+
243402
## RBAC
244403

245404
Grant external-dns access to your custom resources:

0 commit comments

Comments
 (0)