dnscontrol/documentation/language-reference/domain-modifiers/IGNORE_EXTERNAL_DNS.md
tridion f1b30a1a04
feat: Add IGNORE_EXTERNAL_DNS() for Kubernetes external-dns coexistence (#3869)s
## Summary

This PR adds a new domain modifier `IGNORE_EXTERNAL_DNS()` that
automatically detects and ignores DNS records managed by Kubernetes
[external-dns](https://github.com/kubernetes-sigs/external-dns)
controller.

**Related Issue:** This addresses the feature request discussed in
StackExchange/dnscontrol#935 (Idea: Ownership system), where
@tlimoncelli indicated openness to accepting a PR for this
functionality.

## Problem

When running DNSControl alongside Kubernetes external-dns, users face a
challenge:

- **external-dns** dynamically creates DNS records based on Kubernetes
Ingress/Service resources
- Users cannot use `IGNORE()` because they cannot predict which record
names external-dns will create
- Using `NO_PURGE()` is too broad - it prevents DNSControl from cleaning
up any orphaned records

The fundamental issue is that `IGNORE()` requires static patterns known
at config-time, but external-dns creates records dynamically at runtime.

## Solution

`IGNORE_EXTERNAL_DNS()` solves this by detecting external-dns managed
records at runtime:

```javascript
D("example.com", REG_CHANGEME, DnsProvider(DSP_MY_PROVIDER),
    IGNORE_EXTERNAL_DNS(),  // Automatically ignore external-dns managed records
    A("@", "1.2.3.4"),
    CNAME("www", "@")
);
```

### How It Works

external-dns uses a TXT record registry to track ownership. For each
managed record, it creates a TXT record like:

- `a-myapp.example.com` → TXT containing
`heritage=external-dns,external-dns/owner=...`
- `cname-api.example.com` → TXT containing
`heritage=external-dns,external-dns/owner=...`

This PR:
1. Scans existing TXT records for the `heritage=external-dns` marker
2. Parses the TXT record name prefix (e.g., `a-`, `cname-`) to determine
the managed record type
3. Automatically adds those records to the ignore list during diff
operations

## Changes

| File | Purpose |
|------|---------|
| `models/domain.go` | Add `IgnoreExternalDNS` field to DomainConfig |
| `pkg/js/helpers.js` | Add `IGNORE_EXTERNAL_DNS()` JavaScript helper |
| `pkg/diff2/externaldns.go` | Core detection logic for external-dns TXT
records |
| `pkg/diff2/externaldns_test.go` | Unit tests for detection logic |
| `pkg/diff2/handsoff.go` | Integrate external-dns detection into
handsoff() |
| `pkg/diff2/diff2.go` | Pass IgnoreExternalDNS flag to handsoff() |
| `commands/types/dnscontrol.d.ts` | TypeScript definitions for IDE
support |
| `documentation/.../IGNORE_EXTERNAL_DNS.md` | User documentation |

## Design Philosophy

This follows DNSControl's pattern of convenience builders (like
`M365_BUILDER`, `SPF_BUILDER`, `DKIM_BUILDER`) that make complex
operations simple. Just as those builders abstract away implementation
details, `IGNORE_EXTERNAL_DNS()` abstracts away the complexity of
detecting external-dns managed records.

## Testing

All unit tests pass:
```
go test ./pkg/diff2/... -v  # Tests detection logic
go test ./pkg/js/...        # Tests JS helpers
go build ./...              # Builds successfully
```

## Caveats Documented

- Only supports TXT registry (the default for external-dns)
- Requires external-dns to use default naming conventions
- May need updates if external-dns changes its registry format

---------

Co-authored-by: Tom Limoncelli <6293917+tlimoncelli@users.noreply.github.com>
2025-12-03 08:56:55 -05:00

196 lines
7.1 KiB
Markdown

---
name: IGNORE_EXTERNAL_DNS
parameters:
- prefix
parameter_types:
prefix: string?
---
`IGNORE_EXTERNAL_DNS` makes DNSControl automatically detect and ignore DNS records
managed by Kubernetes external-dns.
## Background
[External-dns](https://github.com/kubernetes-sigs/external-dns) is a popular
Kubernetes controller that synchronizes exposed Kubernetes Services and Ingresses
with DNS providers. It creates DNS records automatically based on annotations on
your Kubernetes resources.
External-dns uses TXT records to track ownership of the DNS records it manages.
These TXT records contain metadata in this format:
```
"heritage=external-dns,external-dns/owner=<owner-id>,external-dns/resource=<resource>"
```
When you have both DNSControl and external-dns managing the same DNS zone, conflicts
can occur. DNSControl will try to delete records created by external-dns, and
external-dns will recreate them, leading to an endless update cycle.
## How it works
When `IGNORE_EXTERNAL_DNS` is enabled, DNSControl will:
1. Scan existing TXT records for the external-dns heritage marker (`heritage=external-dns`)
2. Parse the TXT record name to determine which DNS record it manages
3. Automatically ignore both the TXT ownership record and the corresponding DNS record
External-dns creates TXT records with prefixes based on record type:
- `a-<name>` for A records
- `aaaa-<name>` for AAAA records
- `cname-<name>` for CNAME records
- `ns-<name>` for NS records
- `mx-<name>` for MX records
- `srv-<name>` for SRV records
- `txt-<name>` for TXT records (when external-dns manages TXT records)
For example, if external-dns creates an A record at `myapp.example.com`, it will
also create a TXT record at `a-myapp.example.com` containing the heritage information.
## Usage
{% code title="dnsconfig.js" %}
```javascript
// Default: detect standard external-dns prefixes (a-, cname-, etc.)
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
IGNORE_EXTERNAL_DNS(),
// Your static DNS records managed by DNSControl
A("www", "1.2.3.4"),
A("mail", "1.2.3.5"),
MX("@", 10, "mail"),
// Records created by external-dns (from Kubernetes Ingresses/Services)
// will be automatically detected and ignored
);
```
{% endcode %}
## Custom Prefix Support
If your external-dns is configured with a custom `--txt-prefix` (as documented in the
[external-dns TXT registry docs](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/registry/txt.md#prefixes-and-suffixes)),
pass that prefix to `IGNORE_EXTERNAL_DNS()`:
{% code title="dnsconfig.js" %}
```javascript
// If external-dns is configured with --txt-prefix="extdns-"
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
IGNORE_EXTERNAL_DNS("extdns-"),
A("www", "1.2.3.4"),
);
```
{% endcode %}
This will match TXT records like `extdns-www`, `extdns-api`, etc.
Without a prefix argument, it detects:
- The default `%{record_type}-` format (prefixes like `a-`, `cname-`, etc.)
- Legacy format (TXT record with same name as managed record)
## Example scenario
Suppose you have:
- A Kubernetes cluster running external-dns with `--txt-owner-id=my-cluster`
- An Ingress resource that creates an A record for `myapp.example.com` pointing to `10.0.0.1`
External-dns will create:
1. An A record: `myapp.example.com``10.0.0.1`
2. A TXT record: `a-myapp.example.com``"heritage=external-dns,external-dns/owner=my-cluster,external-dns/resource=ingress/default/myapp"`
With `IGNORE_EXTERNAL_DNS` enabled, DNSControl will:
- Detect the TXT record at `a-myapp.example.com` as an external-dns ownership record
- Ignore both the TXT record and the A record at `myapp.example.com`
- Only manage the records you explicitly define in your `dnsconfig.js`
## Comparison with other options
| Feature | Use case |
|---------|----------|
| `IGNORE_EXTERNAL_DNS` | Automatically ignore all external-dns managed records |
| `IGNORE("*.k8s", "A,AAAA,CNAME,TXT")` | Ignore records under a specific subdomain pattern |
| `NO_PURGE` | Don't delete any records (less precise, records may accumulate) |
## Caveats
### One per domain
Only one `IGNORE_EXTERNAL_DNS()` should be used per domain. If you call it multiple
times, the last prefix wins. If you have multiple external-dns instances with
different prefixes managing the same zone, use `IGNORE()` patterns for additional
prefixes.
### TXT Registry Format
This feature relies on external-dns's [TXT registry](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/registry/txt.md),
which is the default registry type. The TXT record content format is well-documented:
```
"heritage=external-dns,external-dns/owner=<owner-id>,external-dns/resource=<resource>"
```
This feature detects the `heritage=external-dns` marker in TXT records to identify
external-dns managed records.
### Custom Prefix Support
This feature supports custom prefixes configured via external-dns's `--txt-prefix` flag.
If you're using a custom prefix, pass it to `IGNORE_EXTERNAL_DNS()`:
```javascript
// If external-dns uses --txt-prefix="extdns-"
IGNORE_EXTERNAL_DNS("extdns-")
// If external-dns uses --txt-prefix="myprefix-%{record_type}-"
IGNORE_EXTERNAL_DNS("myprefix-") // The record type part is handled automatically
// If external-dns uses --txt-prefix="extdns-%{record_type}." (period format)
// This is recommended for apex domain support per external-dns docs
IGNORE_EXTERNAL_DNS("extdns-") // Works with both hyphen and period format
```
Without a prefix argument, it detects:
- Default format: `%{record_type}-` prefix (e.g., `a-`, `cname-`)
- Legacy format: Same name as managed record (no prefix)
#### Period Format for Apex Domains
If you need external-dns to manage apex (root) domain records, the external-dns
documentation recommends using a prefix with `%{record_type}` followed by a period:
```yaml
# external-dns deployment args
args:
- --txt-prefix=extdns-%{record_type}.
```
This creates TXT records like `extdns-a.www` for the `www` A record, and `extdns-a`
for the apex A record. DNSControl's `IGNORE_EXTERNAL_DNS` supports both formats:
- Hyphen format: `extdns-a-www` (from `--txt-prefix=extdns-` with default `%{record_type}-`)
- Period format: `extdns-a.www` (from `--txt-prefix=extdns-%{record_type}.`)
**Note:** Suffix-based naming (`--txt-suffix`) is not currently supported.
### Unsupported Registries
External-dns supports multiple registry types. This feature **only** supports:
-**TXT registry** (default) - Stores metadata in TXT records
The following registries are **not supported**:
-**DynamoDB registry** - Stores metadata in AWS DynamoDB
-**AWS-SD registry** - Stores metadata in AWS Service Discovery
-**noop registry** - No metadata persistence
### Legacy TXT Format
External-dns versions prior to v0.16 created TXT records without the record type
prefix (e.g., `myapp.example.com` instead of `a-myapp.example.com`). This legacy
format is supported but may match more records than intended since the record type
cannot be determined.
## See also
* [`IGNORE`](IGNORE.md) for manually ignoring specific records with glob patterns
* [`NO_PURGE`](NO_PURGE.md) for preventing deletion of all unmanaged records
* [External-dns documentation](https://github.com/kubernetes-sigs/external-dns)