mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-12-09 05:36:27 +08:00
Merge branch 'main' into branch_allrecs
This commit is contained in:
commit
691764ee29
23 changed files with 1844 additions and 160 deletions
190
commands/types/dnscontrol.d.ts
vendored
190
commands/types/dnscontrol.d.ts
vendored
|
|
@ -1671,6 +1671,196 @@ declare function HTTPS(name: string, priority: number, target: string, params: s
|
|||
*/
|
||||
declare function IGNORE(labelSpec: string, typeSpec?: string, targetSpec?: string): DomainModifier;
|
||||
|
||||
/**
|
||||
* `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
|
||||
*
|
||||
* ```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
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* ## 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()`:
|
||||
*
|
||||
* ```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"),
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* 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)
|
||||
*
|
||||
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/ignore_external_dns
|
||||
*/
|
||||
declare function IGNORE_EXTERNAL_DNS(prefix?: string): DomainModifier;
|
||||
|
||||
/**
|
||||
* `IGNORE_NAME(a)` is the same as `IGNORE(a, "*", "*")`.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@
|
|||
* [FRAME](language-reference/domain-modifiers/FRAME.md)
|
||||
* [HTTPS](language-reference/domain-modifiers/HTTPS.md)
|
||||
* [IGNORE](language-reference/domain-modifiers/IGNORE.md)
|
||||
* [IGNORE_EXTERNAL_DNS](language-reference/domain-modifiers/IGNORE_EXTERNAL_DNS.md)
|
||||
* [IGNORE_NAME](language-reference/domain-modifiers/IGNORE_NAME.md)
|
||||
* [IGNORE_TARGET](language-reference/domain-modifiers/IGNORE_TARGET.md)
|
||||
* [IMPORT_TRANSFORM](language-reference/domain-modifiers/IMPORT_TRANSFORM.md)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
---
|
||||
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)
|
||||
|
|
@ -22,13 +22,18 @@ Example:
|
|||
{
|
||||
"bind": {
|
||||
"TYPE": "BIND",
|
||||
"directory": "myzones",
|
||||
"filenameformat": "%U.zone"
|
||||
"directory": "myzones"
|
||||
}
|
||||
}
|
||||
```
|
||||
{% endcode %}
|
||||
|
||||
As of v4.2.0 `dnscontrol push` will create subdirectories along the path to
|
||||
the filename. This includes both the portion of the path created by the
|
||||
`directory` setting and the `filenameformat` setting. For security reasons, the
|
||||
automatic creation of subdirectories is disabled if `dnscontrol` is running as
|
||||
root. (Running DNSControl as root is not recommended in general.)
|
||||
|
||||
## Meta configuration
|
||||
|
||||
This provider accepts some optional metadata in the `NewDnsProvider()` call.
|
||||
|
|
@ -85,43 +90,59 @@ DNSControl does not handle special serial number math such as "looping through z
|
|||
# filenameformat
|
||||
|
||||
The `filenameformat` parameter specifies the file name to be used when
|
||||
writing the zone file. The default (`%U.zone`) is acceptable in most cases: the
|
||||
writing the zone file. The default (`%c.zone`) is acceptable in most cases: the
|
||||
file name is the name as specified in the `D()` function plus ".zone".
|
||||
|
||||
The filenameformat is a string with a few printf-like `%` verbs:
|
||||
|
||||
* The domain name without tag (the `example.com` part of `example.com!tag`):
|
||||
* `%D` as specified in `D()` (no IDN conversion, but downcased)
|
||||
* `%I` converted to IDN/Punycode (`xn--...`) and downcased.
|
||||
* `%N` converted to Unicode (downcased first)
|
||||
* `%T` the split horizon tag, or "" (the `tag` part of `example.com!tag`)
|
||||
* `%?x` this returns `x` if the split horizon tag is non-null, otherwise nothing. `x` can be any printable but is usually `!`.
|
||||
* `%U` short for "%I%?!%T". This is the universal, canonical, name for the domain used for comparisons within DNSControl. This is best for filenames which is why it is used in the default.
|
||||
* `%%` `%`
|
||||
* ordinary characters (not `%`) are copied unchanged to the output stream
|
||||
* FYI: format strings must not end with an incomplete `%` or `%?`
|
||||
| Verb | Description | `EXAMple.com` | `EXAMple.com!MyTag` | `рф.com!myTag` |
|
||||
| ------- | ------------------------------------------------- | ------------- | ------------------- | -------------------- |
|
||||
| `%T` | the tag | "" (null) | `myTag` | `myTag` |
|
||||
| `%c` | canonical name, globally unique and comparable | `example.com` | `example.com!myTag` | `xn--p1ai.com!myTag` |
|
||||
| `%a` | ASCII domain (Punycode, downcased) | `example.com` | `example.com` | `xn--p1ai.com` |
|
||||
| `%u` | Unicode domain (non-Unicode parts downcased) | `example.com` | `example.com` | `рф.com` |
|
||||
| `%r` | Raw (unmodified) Domain from `D()` (risky!) | `EXAMple.com` | `EXAMple.com` | `рф.com` |
|
||||
| `%f` | like `%c` but better for filenames (`%a%?_%T`) | `example.com` | `example.com_myTag` | `xn--p1ai.com_myTag` |
|
||||
| `%F` | like `%f` but reversed order (`%T%?_%a`) | `example.com` | `myTag_example.com` | `myTag_xn--p1ai.com` |
|
||||
| `%?x` | returns `x` if tag exists, otherwise "" | "" (null) | `x` | `x` |
|
||||
| `%%` | a literal percent sign | `%` | `%` | `%` |
|
||||
| `a-Z./` | other printable characters are copied exactly | `a-Z./` | `a-Z./` | `a-Z./` |
|
||||
| `%U` | (deprecated, use `%c`) Same as `%D%?!%T` (risky!) | `example.com` | `example.com!myTag` | `рф.com!myTag` |
|
||||
| `%D` | (deprecated, use `%r`) mangles Unicode (risky!) | `example.com` | `example.com` | `рф.com` |
|
||||
|
||||
Typical values:
|
||||
* `%?x` is typically used to generate an optional `!` or `_` if there is a tag.
|
||||
* `%r` is considered "risky" because it can produce a domain name that is not
|
||||
canonical. For example, if you use `D("FOO.com")` and later change it to `D("foo.com")`, your file names will change.
|
||||
* Format strings must not end with an incomplete `%` or `%?`
|
||||
* Generating a filename without a tag is risky. For example, if the same
|
||||
`dnsconfig.js` has `D("example.com!inside", DSP_BIND)` and
|
||||
`D("example.com!outside", DSP_BIND)`, both will use the same filename.
|
||||
DNSControl will write both zone files to the same file, flapping between the
|
||||
two. No error or warning will be output.
|
||||
|
||||
* `%U.zone` (The default)
|
||||
* `example.com.zone` or `example.com!tag.zone`
|
||||
* `%T%?_%I.zone` (optional tag and `_` + domain + `.zone`)
|
||||
* `tag_example.com.zone` or `example.com.zone`
|
||||
* `db_%T%?_%D`
|
||||
* `db_inside_example.com` or `db_example.com`
|
||||
Useful examples:
|
||||
|
||||
{% hint style="warning" %}
|
||||
**Warning** DNSControl will not warn you if two zones generate the same
|
||||
filename. Instead, each will write to the same place. The content would end up
|
||||
flapping back and forth between the two. The best way to prevent this is to
|
||||
always include the tag (`%T`) or use `%U` which includes the tag.
|
||||
{% endhint %}
|
||||
| Verb | Description | `EXAMple.com` | `EXAMple.com!MyTag` | `рф.com!myTag` |
|
||||
| ------------ | ----------------------------------- | ------------------ | ------------------------ | ------------------------- |
|
||||
| `%c.zone` | Default format (v4.28 and later) | `example.com.zone` | `example.com!myTag.zone` | `xn--p1ai.com!myTag.zone` |
|
||||
| `%U.zone` | Default format (pre-v4.28) (risky!) | `example.com.zone` | `example.com!myTag.zone` | `рф.com!myTag.zone` |
|
||||
| `db_%f` | Recommended in a popular DNS book | `db_example.com` | `db_example.com_myTag` | `db_xn--p1ai.com_myTag` |
|
||||
| `db_%a%?_%T` | same as above but using `%?_` | `db_example.com` | `db_example.com_myTag` | `db_xn--p1ai.com_myTag` |
|
||||
|
||||
(new in v4.2.0) `dnscontrol push` will create subdirectories along the path to
|
||||
the filename. This includes both the portion of the path created by the
|
||||
`directory` setting and the `filenameformat` setting. The automatic creation of
|
||||
subdirectories is disabled if `dnscontrol` is running as root for security
|
||||
reasons.
|
||||
Compatibility notes:
|
||||
|
||||
* `%D` should not be used. It downcases the string in a way that is probably
|
||||
incompatible with Unicode characters. It is retained for compatibility with
|
||||
pre-v4.28 releases. If your domain has capital Unicode characters, backwards
|
||||
compatibility is not guaranteed. Use `%r` instead.
|
||||
* `%U` relies on `%D` which is deprecated. Use `%c` instead.
|
||||
* As of v4.28 the default format string changed from `%U.zone` to `%c.zone`. This
|
||||
should only matter if your `D()` statements included non-ASCII (Unicode)
|
||||
runes that were capitalized.
|
||||
* If you are using pre-v4.28 releases the above table is slightly misleading
|
||||
because uppercase ASCII letters do not always work. If you are using
|
||||
pre-v4.28 releases, assume the above table lists `example.com` instead
|
||||
of `EXAMpl.com`.
|
||||
|
||||
# FYI: get-zones
|
||||
|
||||
|
|
@ -132,7 +153,8 @@ any files named `*.zone` and assumes they are zone files.
|
|||
dnscontrol get-zones --format=nameonly - BIND all
|
||||
```
|
||||
|
||||
If `filenameformat` is defined, `dnscontrol` makes a guess at which
|
||||
filenames are zones but doesn't try to hard to get it right, which is
|
||||
mathematically impossible in some cases. Feel free to file an issue if
|
||||
your format string doesn't work. I love a challenge!
|
||||
If `filenameformat` is defined, `dnscontrol` makes a guess at which filenames
|
||||
are zones by reversing the logic of the format string. It doesn't try very hard
|
||||
to get this right, as getting it right in all situations is mathematically
|
||||
impossible. Feel free to file an issue if find a situation where it doesn't
|
||||
work. I love a challenge!
|
||||
|
|
|
|||
|
|
@ -19,17 +19,17 @@ Example:
|
|||
|
||||
**API Token**
|
||||
|
||||
You can create a Vercel API Token via [Vercel Account Settngs](https://vercel.com/account/settings/tokens).
|
||||
You can create a Vercel API Token via [Vercel Account Settings - Tokens](https://vercel.com/account/settings/tokens).
|
||||
|
||||
**How to grab team ID**
|
||||
|
||||
Log in to your Vercel account and navigate to `https://vercel.com`. Switch to your desired team with the Vercel team switcher if needed.
|
||||
|
||||

|
||||

|
||||
|
||||
Now you can find your team ID in your browser's address bar, copy the path (**without** any leading `/` or trailing `/`) and paste it into your `creds.json` file.
|
||||
Now you can find your team ID in your browser's address bar, copy the path (**without** any leading `/` or trailing `/`) and paste it into your `creds.json` file's `team_id` field.
|
||||
|
||||

|
||||

|
||||
|
||||
If you are familiar with the Vercel API, you can also grab your team ID via Vercel's [Teams - List all teams API](https://vercel.com/docs/rest-api/reference/endpoints/teams/list-all-teams). In response's `id` field you will able to see a string starts with `team_`, and in response's `slug` field you will able to see a string consists of a slugified version of your team name. Both `id` and `slug` can be used as `team_id` for your `creds.json`.
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ In this case, you should use an empty string as the team ID:
|
|||
```
|
||||
{% endcode %}
|
||||
|
||||
It is also possible to manually migrate your domains from your Vercel account to your personal team via the link mentioned above. Whether you choose to migrate your domains or not is up to you, this provider supports both cases.
|
||||
It is possible to manually migrate your domains from your Vercel account to your personal team via the link mentioned above. Whether you choose to migrate your domains or not is up to you, this provider supports both scenarios.
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
@ -82,11 +82,11 @@ You can add your own records and Vercel will prefer your created records over th
|
|||
As of November 2025, the known system-managed records are:
|
||||
|
||||
- `CAA 0 issue "letsencrypt.org"`
|
||||
- Vercel uses Let's Encrypt to issue certificates for your project deployed on Vercel, thus Vercel automatically creates a CAA record to ensure Let's Encrypt can issue certificates, but you can always add your own CAA records.
|
||||
- Vercel uses Let's Encrypt to issue certificates for your project deployed on Vercel, thus Vercel automatically creates a CAA record to ensure Let's Encrypt can issue certificates, but you can always add your own CAA records (you can even add your own `letsencrypt.org` CAA record and Vercel will dedupe that).
|
||||
- `CNAME cname.vercel-dns.com.`
|
||||
- Vercel uses a CNAME record to point your deployed project to their infrastructure, but you can always add your own CNAME records (which allows you to put a third-party CDN in front of Vercel's infrastructure).
|
||||
- Vercel always add a CNAME record to point your deployed project to their infrastructure, but you can always add your own A/AAAA/ALIAS/CNAME records (which allows you to put a third-party CDN in front of Vercel's infrastructure), and Vercel will always prefer your records over their default CNAME.
|
||||
|
||||
In Vercel's API, those system-managed records will have their `creator` set to `system`. We use this to identify and ignore system-managed records, to prevent DNSControl from interfering with them. You won't see them in `dnscontrol diff` or `dnscontrol preview`.
|
||||
In Vercel's API, those system-managed records will have their `creator` set to `system`. We use this to identify and ignore system-managed records, to prevent DNSControl from interfering with them. You won't see them in `dnscontrol preview` or `dnscontrol get-zone`.
|
||||
|
||||
### Comment
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ In the future, we might use the comment field to store additional metadata for o
|
|||
As of November 2025, Vercel has a bug that does not accept CAA records with any extra fields that are not `cansigncansignhttpexchanges`:
|
||||
|
||||
```
|
||||
# OK
|
||||
# OK
|
||||
CAA 0 issue "letsencrypt.org"
|
||||
CAA 0 issuewild "letsencrypt.org"
|
||||
CAA 0 issue "digicert.com; cansignhttpexchanges=yes"
|
||||
|
|
@ -135,10 +135,12 @@ We will burst through half of the quota, and then it spreads the requests evenly
|
|||
|
||||
If you are mass migrating your DNS records from another provider to Vercel, we recommended to upload a BIND zone file via [Vercel's DNS Dashboard](https://vercel.com/dashboard/domains). You can use DNSControl to manage your DNS records afterwards.
|
||||
|
||||
> This provider does not utilize that feature, as "upload a BIND zone file" is not documented and is not part of Vercel's public API.
|
||||
|
||||
### Change Record Type
|
||||
|
||||
Vercel does not allow the record type to be changed after creation. If you try to update a record with a different type (e.g. changing `A` to `CNAME/ALIAS`), we will delete the old record and create a new one. This will count as two separate requests, which may exceed the rate limit. Also be careful about the downtime caused by the deletion and creation of records.
|
||||
|
||||
### Minimum TTL
|
||||
|
||||
Vercel enforces a minimum TTL of 60 seconds (1 minute) for all records. We will always override the TTL to 60 seconds if you try to set a lower TTL.
|
||||
Vercel enforces a minimum TTL of 60 seconds (1 minute) for all records. We will always silently override the TTL to 60 seconds if you try to set a lower TTL.
|
||||
|
|
|
|||
|
|
@ -2068,6 +2068,12 @@ func makeTests() []*TestGroup {
|
|||
// every quarter. There may be library updates, API changes,
|
||||
// etc.
|
||||
|
||||
// This SHOULD be the last test. We do this so that we always
|
||||
// leave zones with a single TXT record exclaming our success.
|
||||
// Nothing depends on this record existing or should depend on it.
|
||||
testgroup("final",
|
||||
tc("final", txt("final", `TestDNSProviders was successful!`)),
|
||||
),
|
||||
}
|
||||
|
||||
return tests
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const (
|
|||
DomainTag = "dnscontrol_tag" // A copy of DomainConfig.Tag
|
||||
DomainUniqueName = "dnscontrol_uniquename" // A copy of DomainConfig.UniqueName
|
||||
DomainNameRaw = "dnscontrol_nameraw" // A copy of DomainConfig.NameRaw
|
||||
DomainNameIDN = "dnscontrol_nameidn" // A copy of DomainConfig.NameIDN
|
||||
DomainNameASCII = "dnscontrol_nameascii" // A copy of DomainConfig.NameASCII
|
||||
DomainNameUnicode = "dnscontrol_nameunicode" // A copy of DomainConfig.NameUnicode
|
||||
)
|
||||
|
||||
|
|
@ -40,6 +40,9 @@ type DomainConfig struct {
|
|||
Unmanaged []*UnmanagedConfig `json:"unmanaged,omitempty"` // IGNORE()
|
||||
UnmanagedUnsafe bool `json:"unmanaged_disable_safety_check,omitempty"` // DISABLE_IGNORE_SAFETY_CHECK
|
||||
|
||||
IgnoreExternalDNS bool `json:"ignore_external_dns,omitempty"` // IGNORE_EXTERNAL_DNS
|
||||
ExternalDNSPrefix string `json:"external_dns_prefix,omitempty"` // IGNORE_EXTERNAL_DNS prefix
|
||||
|
||||
AutoDNSSEC string `json:"auto_dnssec,omitempty"` // "", "on", "off"
|
||||
// DNSSEC bool `json:"dnssec,omitempty"`
|
||||
|
||||
|
|
@ -71,7 +74,7 @@ func (dc *DomainConfig) PostProcess() {
|
|||
|
||||
// Turn the user-supplied name into the fixed forms.
|
||||
ff := domaintags.MakeDomainNameVarieties(dc.Name)
|
||||
dc.Tag, dc.NameRaw, dc.Name, dc.NameUnicode, dc.UniqueName = ff.Tag, ff.NameRaw, ff.NameIDN, ff.NameUnicode, ff.UniqueName
|
||||
dc.Tag, dc.NameRaw, dc.Name, dc.NameUnicode, dc.UniqueName = ff.Tag, ff.NameRaw, ff.NameASCII, ff.NameUnicode, ff.UniqueName
|
||||
|
||||
// Store the FixForms is Metadata so we don't have to change the signature of every function that might need them.
|
||||
// This is a bit ugly but avoids a huge refactor. Please avoid using these to make the future refactor easier.
|
||||
|
|
@ -79,7 +82,7 @@ func (dc *DomainConfig) PostProcess() {
|
|||
dc.Metadata[DomainTag] = dc.Tag
|
||||
}
|
||||
//dc.Metadata[DomainNameRaw] = dc.NameRaw
|
||||
//dc.Metadata[DomainNameIDN] = dc.Name
|
||||
//dc.Metadata[DomainNameASCII] = dc.Name
|
||||
//dc.Metadata[DomainNameUnicode] = dc.NameUnicode
|
||||
dc.Metadata[DomainUniqueName] = dc.UniqueName
|
||||
}
|
||||
|
|
@ -233,7 +236,7 @@ func (dc *DomainConfig) GetPopulateCorrections(providerName string) []*Correctio
|
|||
func MakeFakeDomainConfig(domain string) *DomainConfig {
|
||||
v := domaintags.MakeDomainNameVarieties(domain)
|
||||
return &DomainConfig{
|
||||
Name: v.NameIDN,
|
||||
Name: v.NameASCII,
|
||||
NameRaw: v.NameRaw,
|
||||
NameUnicode: v.NameUnicode,
|
||||
}
|
||||
|
|
|
|||
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -6,7 +6,7 @@
|
|||
"": {
|
||||
"dependencies": {
|
||||
"@umbrelladocs/linkspector": "^0.3.13",
|
||||
"prettier": "^3.6.2"
|
||||
"prettier": "^3.7.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
|
|
@ -715,7 +715,8 @@
|
|||
"version": "0.0.1367902",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz",
|
||||
"integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==",
|
||||
"license": "BSD-3-Clause"
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
|
|
@ -2221,9 +2222,9 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"version": "3.7.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
|
||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"@umbrelladocs/linkspector": "^0.3.13",
|
||||
"prettier": "^3.6.2"
|
||||
"prettier": "^3.7.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -241,6 +241,8 @@ func byHelperStruct(fn func(cc *CompareConfig) (ChangeList, int), existing model
|
|||
dc.Unmanaged,
|
||||
dc.UnmanagedUnsafe,
|
||||
dc.KeepUnknown,
|
||||
dc.IgnoreExternalDNS,
|
||||
dc.ExternalDNSPrefix,
|
||||
)
|
||||
if err != nil {
|
||||
return ByResults{}, err
|
||||
|
|
|
|||
230
pkg/diff2/externaldns.go
Normal file
230
pkg/diff2/externaldns.go
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
package diff2
|
||||
|
||||
// This file implements the IGNORE_EXTERNAL_DNS feature that automatically
|
||||
// detects and ignores DNS records managed by Kubernetes external-dns.
|
||||
//
|
||||
// External-dns uses TXT records to track ownership of DNS records it manages.
|
||||
// The TXT record format is:
|
||||
// "heritage=external-dns,external-dns/owner=<owner-id>,external-dns/resource=<resource>"
|
||||
//
|
||||
// External-dns TXT record naming conventions:
|
||||
// - For A records: prefix + original name (e.g., "a-myapp.example.com" for "myapp.example.com")
|
||||
// - For CNAME records: prefix + original name (e.g., "cname-myapp.example.com")
|
||||
// - Default prefixes: "a-", "aaaa-", "cname-", "ns-", "mx-"
|
||||
// - Can also use --txt-prefix or --txt-suffix flags in external-dns
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
)
|
||||
|
||||
const (
|
||||
// externalDNSHeritage is the heritage value that external-dns uses in its TXT records
|
||||
externalDNSHeritage = "heritage=external-dns"
|
||||
)
|
||||
|
||||
// externalDNSManagedRecord represents a record managed by external-dns
|
||||
type externalDNSManagedRecord struct {
|
||||
Label string // The label of the managed record (without domain suffix)
|
||||
RecordType string // The type of the managed record (A, AAAA, CNAME, etc.)
|
||||
}
|
||||
|
||||
// isExternalDNSTxtRecord checks if a TXT record is an external-dns ownership record.
|
||||
// It returns true and the managed record info if it is, false otherwise.
|
||||
// customPrefix is an optional prefix that external-dns was configured with (e.g., "extdns-").
|
||||
func isExternalDNSTxtRecord(rec *models.RecordConfig, domain string, customPrefix string) (bool, *externalDNSManagedRecord) {
|
||||
if rec.Type != "TXT" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Get the TXT record content
|
||||
target := rec.GetTargetTXTJoined()
|
||||
|
||||
// Check if it contains the external-dns heritage marker
|
||||
if !strings.Contains(target, externalDNSHeritage) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// This is an external-dns TXT record. Now we need to figure out what record it manages.
|
||||
// External-dns TXT record naming:
|
||||
// - New format with record type prefix: "a-myapp.example.com" manages "myapp.example.com" A record
|
||||
// - Old format without type: "myapp.example.com" (legacy, manages the record with same name)
|
||||
// - With custom prefix: e.g., "externaldns-a-myapp.example.com"
|
||||
// - With custom suffix: e.g., "myapp-externaldns.example.com"
|
||||
|
||||
label := rec.GetLabel()
|
||||
managed := parseExternalDNSTxtLabel(label, customPrefix)
|
||||
|
||||
return true, managed
|
||||
}
|
||||
|
||||
// parseExternalDNSTxtLabel parses an external-dns TXT record label to extract
|
||||
// the managed record information.
|
||||
//
|
||||
// External-dns uses these prefixes by default (when using %{record_type} in prefix):
|
||||
// - "a-" for A records
|
||||
// - "aaaa-" for AAAA records
|
||||
// - "cname-" for CNAME records
|
||||
// - "ns-" for NS records
|
||||
// - "mx-" for MX records
|
||||
//
|
||||
// Without %{record_type}, it just uses the prefix directly, and the record type
|
||||
// is encoded as "a-", "cname-", etc. at the start of the label.
|
||||
//
|
||||
// If customPrefix is non-empty, it will be stripped first before looking for
|
||||
// record type prefixes.
|
||||
func parseExternalDNSTxtLabel(label string, customPrefix string) *externalDNSManagedRecord {
|
||||
workingLabel := label
|
||||
|
||||
// If a custom prefix is specified, strip it first
|
||||
if customPrefix != "" {
|
||||
if strings.HasPrefix(strings.ToLower(workingLabel), strings.ToLower(customPrefix)) {
|
||||
workingLabel = workingLabel[len(customPrefix):]
|
||||
} else {
|
||||
// Custom prefix specified but not found - this might be a legacy record
|
||||
// Continue with original label
|
||||
}
|
||||
}
|
||||
|
||||
// Standard prefixes used by external-dns
|
||||
// Supports both hyphen format (a-www) and period format (a.www)
|
||||
// Period format is used when --txt-prefix includes %{record_type}.
|
||||
prefixes := []struct {
|
||||
prefix string
|
||||
recordType string
|
||||
}{
|
||||
{"aaaa.", "AAAA"}, // Period format - must check before "a."
|
||||
{"aaaa-", "AAAA"}, // Hyphen format - must check before "a-"
|
||||
{"a.", "A"}, // Period format
|
||||
{"a-", "A"}, // Hyphen format
|
||||
{"cname.", "CNAME"},
|
||||
{"cname-", "CNAME"},
|
||||
{"ns.", "NS"},
|
||||
{"ns-", "NS"},
|
||||
{"mx.", "MX"},
|
||||
{"mx-", "MX"},
|
||||
{"srv.", "SRV"},
|
||||
{"srv-", "SRV"},
|
||||
{"txt.", "TXT"},
|
||||
{"txt-", "TXT"},
|
||||
}
|
||||
|
||||
for _, p := range prefixes {
|
||||
if strings.HasPrefix(strings.ToLower(workingLabel), p.prefix) {
|
||||
managedLabel := workingLabel[len(p.prefix):]
|
||||
// managedLabel is already lowercase from the prefix match
|
||||
// Handle the case where the managed label is empty (apex domain)
|
||||
if managedLabel == "" {
|
||||
managedLabel = "@"
|
||||
}
|
||||
return &externalDNSManagedRecord{
|
||||
Label: managedLabel,
|
||||
RecordType: p.recordType,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If custom prefix was specified and stripped, check if the remaining label
|
||||
// is a record type indicator (for period format apex domains: extdns-a. at apex becomes extdns-a)
|
||||
if customPrefix != "" && workingLabel != label {
|
||||
// Check if remaining label is just a record type (apex domain with period format)
|
||||
// e.g., prefix "extdns-" with label "extdns-a" → workingLabel "a" → apex A record
|
||||
apexRecordTypes := map[string]string{
|
||||
"a": "A",
|
||||
"aaaa": "AAAA",
|
||||
"cname": "CNAME",
|
||||
"ns": "NS",
|
||||
"mx": "MX",
|
||||
"srv": "SRV",
|
||||
"txt": "TXT",
|
||||
}
|
||||
if recType, ok := apexRecordTypes[strings.ToLower(workingLabel)]; ok {
|
||||
return &externalDNSManagedRecord{
|
||||
Label: "@",
|
||||
RecordType: recType,
|
||||
}
|
||||
}
|
||||
|
||||
// The prefix was stripped but no record type found
|
||||
// This means it's a simple prefix like "extdns-" without record type
|
||||
// We can't determine the record type, so match all types
|
||||
if workingLabel == "" {
|
||||
workingLabel = "@"
|
||||
}
|
||||
return &externalDNSManagedRecord{
|
||||
Label: workingLabel,
|
||||
RecordType: "", // Empty means match any type
|
||||
}
|
||||
}
|
||||
|
||||
// No recognized prefix - this might be a legacy format or custom prefix
|
||||
// In legacy format, the TXT record has the same name as the managed record
|
||||
// We can't determine the record type in this case, so we'll match all types
|
||||
return &externalDNSManagedRecord{
|
||||
Label: label,
|
||||
RecordType: "", // Empty means match any type
|
||||
}
|
||||
}
|
||||
|
||||
// findExternalDNSManagedRecords scans the existing records for external-dns TXT records
|
||||
// and builds a map of records that are managed by external-dns.
|
||||
// Returns a map keyed by "label:type" -> true for managed records
|
||||
// customPrefix is an optional prefix that external-dns was configured with (e.g., "extdns-").
|
||||
func findExternalDNSManagedRecords(existing models.Records, domain string, customPrefix string) map[string]bool {
|
||||
managed := make(map[string]bool)
|
||||
|
||||
// Scan all external-dns TXT records
|
||||
for _, rec := range existing {
|
||||
isExtDNS, info := isExternalDNSTxtRecord(rec, domain, customPrefix)
|
||||
if isExtDNS && info != nil {
|
||||
// Mark the TXT record itself as managed
|
||||
txtKey := rec.GetLabel() + ":TXT"
|
||||
managed[txtKey] = true
|
||||
|
||||
// Mark the record that this TXT record manages
|
||||
if info.RecordType != "" {
|
||||
// Specific record type
|
||||
key := info.Label + ":" + info.RecordType
|
||||
managed[key] = true
|
||||
} else {
|
||||
// Legacy format - we need to find matching records
|
||||
// We'll mark this label as managed for common record types
|
||||
for _, rtype := range []string{"A", "AAAA", "CNAME", "NS", "MX", "SRV"} {
|
||||
key := info.Label + ":" + rtype
|
||||
managed[key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return managed
|
||||
}
|
||||
|
||||
// filterExternalDNSRecords takes a list of existing records and returns those
|
||||
// that should be ignored because they are managed by external-dns.
|
||||
// customPrefix is an optional prefix that external-dns was configured with (e.g., "extdns-").
|
||||
func filterExternalDNSRecords(existing models.Records, domain string, customPrefix string) models.Records {
|
||||
managedMap := findExternalDNSManagedRecords(existing, domain, customPrefix)
|
||||
if len(managedMap) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var ignored models.Records
|
||||
for _, rec := range existing {
|
||||
key := rec.GetLabel() + ":" + rec.Type
|
||||
if managedMap[key] {
|
||||
ignored = append(ignored, rec)
|
||||
}
|
||||
}
|
||||
|
||||
return ignored
|
||||
}
|
||||
|
||||
// GetExternalDNSIgnoredRecords returns the records that should be ignored
|
||||
// because they are managed by external-dns. This is called from handsoff()
|
||||
// when IgnoreExternalDNS is enabled for a domain.
|
||||
// customPrefix is an optional prefix that external-dns was configured with (e.g., "extdns-").
|
||||
func GetExternalDNSIgnoredRecords(existing models.Records, domain string, customPrefix string) models.Records {
|
||||
return filterExternalDNSRecords(existing, domain, customPrefix)
|
||||
}
|
||||
528
pkg/diff2/externaldns_test.go
Normal file
528
pkg/diff2/externaldns_test.go
Normal file
|
|
@ -0,0 +1,528 @@
|
|||
package diff2
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
)
|
||||
|
||||
func makeTestRecord(name, rtype, target, domain string) *models.RecordConfig {
|
||||
rc := &models.RecordConfig{
|
||||
Type: rtype,
|
||||
}
|
||||
rc.SetLabel(name, domain)
|
||||
if rtype == "TXT" {
|
||||
rc.SetTargetTXT(target)
|
||||
} else {
|
||||
rc.SetTarget(target)
|
||||
}
|
||||
return rc
|
||||
}
|
||||
|
||||
func TestIsExternalDNSTxtRecord(t *testing.T) {
|
||||
domain := "example.com"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
record *models.RecordConfig
|
||||
wantIsExtDNS bool
|
||||
wantLabel string
|
||||
wantRecordType string
|
||||
}{
|
||||
{
|
||||
name: "external-dns A record TXT",
|
||||
record: makeTestRecord("a-myapp", "TXT", "heritage=external-dns,external-dns/owner=my-cluster,external-dns/resource=ingress/default/myapp", domain),
|
||||
wantIsExtDNS: true,
|
||||
wantLabel: "myapp",
|
||||
wantRecordType: "A",
|
||||
},
|
||||
{
|
||||
name: "external-dns AAAA record TXT",
|
||||
record: makeTestRecord("aaaa-myapp", "TXT", "heritage=external-dns,external-dns/owner=my-cluster", domain),
|
||||
wantIsExtDNS: true,
|
||||
wantLabel: "myapp",
|
||||
wantRecordType: "AAAA",
|
||||
},
|
||||
{
|
||||
name: "external-dns CNAME record TXT",
|
||||
record: makeTestRecord("cname-www", "TXT", "heritage=external-dns,external-dns/owner=default", domain),
|
||||
wantIsExtDNS: true,
|
||||
wantLabel: "www",
|
||||
wantRecordType: "CNAME",
|
||||
},
|
||||
{
|
||||
name: "external-dns apex A record TXT",
|
||||
record: makeTestRecord("a-", "TXT", "heritage=external-dns,external-dns/owner=k8s", domain),
|
||||
wantIsExtDNS: true,
|
||||
wantLabel: "@",
|
||||
wantRecordType: "A",
|
||||
},
|
||||
{
|
||||
name: "non-external-dns TXT record",
|
||||
record: makeTestRecord("myapp", "TXT", "some random txt content", domain),
|
||||
wantIsExtDNS: false,
|
||||
},
|
||||
{
|
||||
name: "A record (not TXT)",
|
||||
record: makeTestRecord("myapp", "A", "1.2.3.4", domain),
|
||||
wantIsExtDNS: false,
|
||||
},
|
||||
{
|
||||
name: "SPF record",
|
||||
record: makeTestRecord("@", "TXT", "v=spf1 include:_spf.google.com ~all", domain),
|
||||
wantIsExtDNS: false,
|
||||
},
|
||||
{
|
||||
name: "DKIM record",
|
||||
record: makeTestRecord("selector._domainkey", "TXT", "v=DKIM1; k=rsa; p=MIGfMA0G...", domain),
|
||||
wantIsExtDNS: false,
|
||||
},
|
||||
{
|
||||
name: "external-dns with quoted heritage",
|
||||
record: makeTestRecord("a-test", "TXT", "\"heritage=external-dns,external-dns/owner=test\"", domain),
|
||||
wantIsExtDNS: true,
|
||||
wantLabel: "test",
|
||||
wantRecordType: "A",
|
||||
},
|
||||
{
|
||||
name: "external-dns legacy format (no prefix)",
|
||||
record: makeTestRecord("legacy-app", "TXT", "heritage=external-dns,external-dns/owner=old-cluster", domain),
|
||||
wantIsExtDNS: true,
|
||||
wantLabel: "legacy-app",
|
||||
wantRecordType: "", // Empty means match any type
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotIsExtDNS, gotInfo := isExternalDNSTxtRecord(tt.record, domain, "")
|
||||
|
||||
if gotIsExtDNS != tt.wantIsExtDNS {
|
||||
t.Errorf("isExternalDNSTxtRecord() isExtDNS = %v, want %v", gotIsExtDNS, tt.wantIsExtDNS)
|
||||
}
|
||||
|
||||
if tt.wantIsExtDNS {
|
||||
if gotInfo == nil {
|
||||
t.Errorf("isExternalDNSTxtRecord() returned nil info for external-dns record")
|
||||
return
|
||||
}
|
||||
if gotInfo.Label != tt.wantLabel {
|
||||
t.Errorf("isExternalDNSTxtRecord() label = %q, want %q", gotInfo.Label, tt.wantLabel)
|
||||
}
|
||||
if gotInfo.RecordType != tt.wantRecordType {
|
||||
t.Errorf("isExternalDNSTxtRecord() recordType = %q, want %q", gotInfo.RecordType, tt.wantRecordType)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExternalDNSTxtLabel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
label string
|
||||
wantLabel string
|
||||
wantRecordType string
|
||||
}{
|
||||
{
|
||||
name: "A record prefix",
|
||||
label: "a-myapp",
|
||||
wantLabel: "myapp",
|
||||
wantRecordType: "A",
|
||||
},
|
||||
{
|
||||
name: "AAAA record prefix",
|
||||
label: "aaaa-myapp",
|
||||
wantLabel: "myapp",
|
||||
wantRecordType: "AAAA",
|
||||
},
|
||||
{
|
||||
name: "CNAME record prefix",
|
||||
label: "cname-www",
|
||||
wantLabel: "www",
|
||||
wantRecordType: "CNAME",
|
||||
},
|
||||
{
|
||||
name: "NS record prefix",
|
||||
label: "ns-subdomain",
|
||||
wantLabel: "subdomain",
|
||||
wantRecordType: "NS",
|
||||
},
|
||||
{
|
||||
name: "MX record prefix",
|
||||
label: "mx-mail",
|
||||
wantLabel: "mail",
|
||||
wantRecordType: "MX",
|
||||
},
|
||||
{
|
||||
name: "apex domain (A)",
|
||||
label: "a-",
|
||||
wantLabel: "@",
|
||||
wantRecordType: "A",
|
||||
},
|
||||
{
|
||||
name: "uppercase prefix (should handle case)",
|
||||
label: "A-myapp",
|
||||
wantLabel: "myapp",
|
||||
wantRecordType: "A",
|
||||
},
|
||||
{
|
||||
name: "no recognized prefix (legacy)",
|
||||
label: "myapp",
|
||||
wantLabel: "myapp",
|
||||
wantRecordType: "",
|
||||
},
|
||||
{
|
||||
name: "subdomain with dots in name",
|
||||
label: "a-sub.domain",
|
||||
wantLabel: "sub.domain",
|
||||
wantRecordType: "A",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := parseExternalDNSTxtLabel(tt.label, "")
|
||||
|
||||
if got.Label != tt.wantLabel {
|
||||
t.Errorf("parseExternalDNSTxtLabel() Label = %q, want %q", got.Label, tt.wantLabel)
|
||||
}
|
||||
if got.RecordType != tt.wantRecordType {
|
||||
t.Errorf("parseExternalDNSTxtLabel() RecordType = %q, want %q", got.RecordType, tt.wantRecordType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindExternalDNSManagedRecords(t *testing.T) {
|
||||
domain := "example.com"
|
||||
|
||||
existing := models.Records{
|
||||
// External-dns managed records
|
||||
makeTestRecord("a-myapp", "TXT", "heritage=external-dns,external-dns/owner=cluster1", domain),
|
||||
makeTestRecord("myapp", "A", "10.0.0.1", domain),
|
||||
makeTestRecord("cname-www", "TXT", "heritage=external-dns,external-dns/owner=cluster1", domain),
|
||||
makeTestRecord("www", "CNAME", "myapp.example.com.", domain),
|
||||
// Non-external-dns records
|
||||
makeTestRecord("static", "A", "1.2.3.4", domain),
|
||||
makeTestRecord("@", "TXT", "v=spf1 -all", domain),
|
||||
makeTestRecord("@", "MX", "mail.example.com.", domain),
|
||||
}
|
||||
|
||||
managed := findExternalDNSManagedRecords(existing, domain, "")
|
||||
|
||||
// Check that expected keys are present
|
||||
expectedKeys := []string{
|
||||
"a-myapp:TXT", // The TXT record itself
|
||||
"myapp:A", // The A record it manages
|
||||
"cname-www:TXT", // The TXT record itself
|
||||
"www:CNAME", // The CNAME record it manages
|
||||
}
|
||||
|
||||
for _, key := range expectedKeys {
|
||||
if !managed[key] {
|
||||
t.Errorf("Expected key %q to be marked as managed", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that non-external-dns records are not marked
|
||||
notExpected := []string{
|
||||
"static:A",
|
||||
"@:TXT",
|
||||
"@:MX",
|
||||
}
|
||||
|
||||
for _, key := range notExpected {
|
||||
if managed[key] {
|
||||
t.Errorf("Key %q should not be marked as managed", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetExternalDNSIgnoredRecords(t *testing.T) {
|
||||
domain := "example.com"
|
||||
|
||||
existing := models.Records{
|
||||
// External-dns managed records
|
||||
makeTestRecord("a-myapp", "TXT", "heritage=external-dns,external-dns/owner=cluster1", domain),
|
||||
makeTestRecord("myapp", "A", "10.0.0.1", domain),
|
||||
// Non-external-dns records
|
||||
makeTestRecord("static", "A", "1.2.3.4", domain),
|
||||
makeTestRecord("@", "TXT", "v=spf1 -all", domain),
|
||||
}
|
||||
|
||||
ignored := GetExternalDNSIgnoredRecords(existing, domain, "")
|
||||
|
||||
if len(ignored) != 2 {
|
||||
t.Errorf("Expected 2 ignored records, got %d", len(ignored))
|
||||
}
|
||||
|
||||
// Verify the ignored records
|
||||
foundTXT := false
|
||||
foundA := false
|
||||
for _, rec := range ignored {
|
||||
if rec.GetLabel() == "a-myapp" && rec.Type == "TXT" {
|
||||
foundTXT = true
|
||||
}
|
||||
if rec.GetLabel() == "myapp" && rec.Type == "A" {
|
||||
foundA = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundTXT {
|
||||
t.Error("Expected TXT record a-myapp to be ignored")
|
||||
}
|
||||
if !foundA {
|
||||
t.Error("Expected A record myapp to be ignored")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetExternalDNSIgnoredRecords_NoExternalDNS(t *testing.T) {
|
||||
domain := "example.com"
|
||||
|
||||
existing := models.Records{
|
||||
makeTestRecord("static", "A", "1.2.3.4", domain),
|
||||
makeTestRecord("@", "TXT", "v=spf1 -all", domain),
|
||||
makeTestRecord("www", "CNAME", "static.example.com.", domain),
|
||||
}
|
||||
|
||||
ignored := GetExternalDNSIgnoredRecords(existing, domain, "")
|
||||
|
||||
if len(ignored) != 0 {
|
||||
t.Errorf("Expected 0 ignored records when no external-dns records exist, got %d", len(ignored))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetExternalDNSIgnoredRecords_LegacyFormat(t *testing.T) {
|
||||
domain := "example.com"
|
||||
|
||||
// Legacy format: TXT record has same name as the record it manages (no type prefix)
|
||||
existing := models.Records{
|
||||
makeTestRecord("legacyapp", "TXT", "heritage=external-dns,external-dns/owner=old-cluster", domain),
|
||||
makeTestRecord("legacyapp", "A", "10.0.0.1", domain),
|
||||
makeTestRecord("legacyapp", "AAAA", "::1", domain),
|
||||
}
|
||||
|
||||
ignored := GetExternalDNSIgnoredRecords(existing, domain, "")
|
||||
|
||||
// Legacy format should match the TXT and common record types
|
||||
if len(ignored) < 3 {
|
||||
t.Errorf("Expected at least 3 ignored records for legacy format, got %d", len(ignored))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetExternalDNSIgnoredRecords_CustomPrefix(t *testing.T) {
|
||||
domain := "example.com"
|
||||
|
||||
// Custom prefix format: e.g., "extdns-www" for "www" record
|
||||
existing := models.Records{
|
||||
makeTestRecord("extdns-www", "TXT", "heritage=external-dns,external-dns/owner=k3s-cluster", domain),
|
||||
makeTestRecord("www", "A", "10.0.0.1", domain),
|
||||
makeTestRecord("extdns-api", "TXT", "heritage=external-dns,external-dns/owner=k3s-cluster", domain),
|
||||
makeTestRecord("api", "CNAME", "app.example.com.", domain),
|
||||
// Non-external-dns records
|
||||
makeTestRecord("static", "A", "1.2.3.4", domain),
|
||||
}
|
||||
|
||||
// Without custom prefix, only the TXT records themselves should be detected
|
||||
// (but the A/CNAME won't be linked because "extdns-" isn't a known type prefix)
|
||||
ignoredDefault := GetExternalDNSIgnoredRecords(existing, domain, "")
|
||||
|
||||
// With custom prefix, both TXT and their managed records should be detected
|
||||
ignoredCustom := GetExternalDNSIgnoredRecords(existing, domain, "extdns-")
|
||||
|
||||
// Custom prefix should find more records
|
||||
if len(ignoredCustom) <= len(ignoredDefault) {
|
||||
t.Errorf("Expected custom prefix to find more records: default=%d, custom=%d",
|
||||
len(ignoredDefault), len(ignoredCustom))
|
||||
}
|
||||
|
||||
// Should find: extdns-www:TXT, www:A/AAAA/CNAME/etc, extdns-api:TXT, api:A/AAAA/CNAME/etc
|
||||
if len(ignoredCustom) < 4 {
|
||||
t.Errorf("Expected at least 4 ignored records with custom prefix, got %d", len(ignoredCustom))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExternalDNSTxtLabel_CustomPrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
label string
|
||||
customPrefix string
|
||||
wantLabel string
|
||||
wantRecordType string
|
||||
}{
|
||||
{
|
||||
name: "custom prefix with record type",
|
||||
label: "extdns-a-myapp",
|
||||
customPrefix: "extdns-",
|
||||
wantLabel: "myapp",
|
||||
wantRecordType: "A",
|
||||
},
|
||||
{
|
||||
name: "custom prefix without record type",
|
||||
label: "extdns-www",
|
||||
customPrefix: "extdns-",
|
||||
wantLabel: "www",
|
||||
wantRecordType: "", // No record type in this format
|
||||
},
|
||||
{
|
||||
name: "custom prefix apex domain",
|
||||
label: "extdns-",
|
||||
customPrefix: "extdns-",
|
||||
wantLabel: "@",
|
||||
wantRecordType: "",
|
||||
},
|
||||
{
|
||||
name: "prefix not found - fallback to legacy",
|
||||
label: "other-www",
|
||||
customPrefix: "extdns-",
|
||||
wantLabel: "other-www",
|
||||
wantRecordType: "",
|
||||
},
|
||||
// Period format tests (--txt-prefix=extdns-%{record_type}.)
|
||||
{
|
||||
name: "period format A record",
|
||||
label: "extdns-a.myapp",
|
||||
customPrefix: "extdns-",
|
||||
wantLabel: "myapp",
|
||||
wantRecordType: "A",
|
||||
},
|
||||
{
|
||||
name: "period format AAAA record",
|
||||
label: "extdns-aaaa.myapp",
|
||||
customPrefix: "extdns-",
|
||||
wantLabel: "myapp",
|
||||
wantRecordType: "AAAA",
|
||||
},
|
||||
{
|
||||
name: "period format CNAME record",
|
||||
label: "extdns-cname.www",
|
||||
customPrefix: "extdns-",
|
||||
wantLabel: "www",
|
||||
wantRecordType: "CNAME",
|
||||
},
|
||||
{
|
||||
name: "period format apex A record",
|
||||
label: "extdns-a",
|
||||
customPrefix: "extdns-",
|
||||
wantLabel: "@",
|
||||
wantRecordType: "A",
|
||||
},
|
||||
{
|
||||
name: "period format apex AAAA record",
|
||||
label: "extdns-aaaa",
|
||||
customPrefix: "extdns-",
|
||||
wantLabel: "@",
|
||||
wantRecordType: "AAAA",
|
||||
},
|
||||
{
|
||||
name: "period format apex CNAME record",
|
||||
label: "extdns-cname",
|
||||
customPrefix: "extdns-",
|
||||
wantLabel: "@",
|
||||
wantRecordType: "CNAME",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := parseExternalDNSTxtLabel(tt.label, tt.customPrefix)
|
||||
|
||||
if got.Label != tt.wantLabel {
|
||||
t.Errorf("parseExternalDNSTxtLabel() Label = %q, want %q", got.Label, tt.wantLabel)
|
||||
}
|
||||
if got.RecordType != tt.wantRecordType {
|
||||
t.Errorf("parseExternalDNSTxtLabel() RecordType = %q, want %q", got.RecordType, tt.wantRecordType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetExternalDNSIgnoredRecords_PeriodFormat tests the period format used when
|
||||
// external-dns is configured with --txt-prefix=prefix-%{record_type}.
|
||||
// This creates TXT records like "extdns-a.www" for "www" A record.
|
||||
func TestGetExternalDNSIgnoredRecords_PeriodFormat(t *testing.T) {
|
||||
domain := "example.com"
|
||||
|
||||
existing := models.Records{
|
||||
// Period format TXT records (from --txt-prefix=extdns-%{record_type}.)
|
||||
makeTestRecord("extdns-a.www", "TXT", "heritage=external-dns,external-dns/owner=k3s-cluster", domain),
|
||||
makeTestRecord("www", "A", "10.0.0.1", domain),
|
||||
makeTestRecord("extdns-cname.api", "TXT", "heritage=external-dns,external-dns/owner=k3s-cluster", domain),
|
||||
makeTestRecord("api", "CNAME", "app.example.com.", domain),
|
||||
// Non-external-dns records
|
||||
makeTestRecord("static", "A", "1.2.3.4", domain),
|
||||
}
|
||||
|
||||
ignored := GetExternalDNSIgnoredRecords(existing, domain, "extdns-")
|
||||
|
||||
// Should find: extdns-a.www:TXT, www:A, extdns-cname.api:TXT, api:CNAME
|
||||
if len(ignored) < 4 {
|
||||
t.Errorf("Expected at least 4 ignored records with period format, got %d", len(ignored))
|
||||
}
|
||||
|
||||
// Verify specific records are ignored
|
||||
found := make(map[string]bool)
|
||||
for _, rec := range ignored {
|
||||
key := rec.GetLabel() + ":" + rec.Type
|
||||
found[key] = true
|
||||
}
|
||||
|
||||
expectedKeys := []string{"extdns-a.www:TXT", "www:A", "extdns-cname.api:TXT", "api:CNAME"}
|
||||
for _, key := range expectedKeys {
|
||||
if !found[key] {
|
||||
t.Errorf("Expected record %q to be ignored", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetExternalDNSIgnoredRecords_PeriodFormatApex tests apex domain detection
|
||||
// with the period format (--txt-prefix=prefix-%{record_type}.).
|
||||
// For apex domains, the TXT label becomes "extdns-a" (just prefix + record type).
|
||||
func TestGetExternalDNSIgnoredRecords_PeriodFormatApex(t *testing.T) {
|
||||
domain := "example.com"
|
||||
|
||||
existing := models.Records{
|
||||
// Period format apex A record: TXT at "extdns-a" manages "@" A record
|
||||
makeTestRecord("extdns-a", "TXT", "heritage=external-dns,external-dns/owner=k3s-cluster", domain),
|
||||
makeTestRecord("@", "A", "10.0.0.1", domain),
|
||||
// Period format apex AAAA record
|
||||
makeTestRecord("extdns-aaaa", "TXT", "heritage=external-dns,external-dns/owner=k3s-cluster", domain),
|
||||
makeTestRecord("@", "AAAA", "::1", domain),
|
||||
// Non-external-dns apex records (should not be ignored)
|
||||
makeTestRecord("@", "MX", "mail.example.com.", domain),
|
||||
makeTestRecord("@", "TXT", "v=spf1 -all", domain),
|
||||
}
|
||||
|
||||
ignored := GetExternalDNSIgnoredRecords(existing, domain, "extdns-")
|
||||
|
||||
// Should find: extdns-a:TXT, @:A, extdns-aaaa:TXT, @:AAAA
|
||||
if len(ignored) != 4 {
|
||||
t.Errorf("Expected 4 ignored records for apex with period format, got %d", len(ignored))
|
||||
for _, rec := range ignored {
|
||||
t.Logf(" ignored: %s:%s", rec.GetLabel(), rec.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify specific records are ignored
|
||||
found := make(map[string]bool)
|
||||
for _, rec := range ignored {
|
||||
key := rec.GetLabel() + ":" + rec.Type
|
||||
found[key] = true
|
||||
}
|
||||
|
||||
expectedKeys := []string{"extdns-a:TXT", "@:A", "extdns-aaaa:TXT", "@:AAAA"}
|
||||
for _, key := range expectedKeys {
|
||||
if !found[key] {
|
||||
t.Errorf("Expected record %q to be ignored", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify MX and SPF TXT are NOT ignored
|
||||
notExpectedKeys := []string{"@:MX", "@:TXT"}
|
||||
for _, key := range notExpectedKeys {
|
||||
if found[key] {
|
||||
t.Errorf("Record %q should NOT be ignored", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -106,6 +106,8 @@ func handsoff(
|
|||
unmanagedConfigs []*models.UnmanagedConfig,
|
||||
unmanagedSafely bool,
|
||||
noPurge bool,
|
||||
ignoreExternalDNS bool,
|
||||
externalDNSPrefix string,
|
||||
) (models.Records, []string, error) {
|
||||
var msgs []string
|
||||
|
||||
|
|
@ -120,6 +122,16 @@ func handsoff(
|
|||
punct = "."
|
||||
}
|
||||
|
||||
// Process IGNORE_EXTERNAL_DNS feature:
|
||||
var externalDNSIgnored models.Records
|
||||
if ignoreExternalDNS {
|
||||
externalDNSIgnored = GetExternalDNSIgnoredRecords(existing, domain, externalDNSPrefix)
|
||||
if len(externalDNSIgnored) != 0 {
|
||||
msgs = append(msgs, fmt.Sprintf("%d records not being deleted because of IGNORE_EXTERNAL_DNS%s", len(externalDNSIgnored), punct))
|
||||
msgs = append(msgs, reportSkips(externalDNSIgnored, !printer.SkinnyReport)...)
|
||||
}
|
||||
}
|
||||
|
||||
// Process IGNORE*() and NO_PURGE features:
|
||||
ignorable, foreign, err := processIgnoreAndNoPurge(domain, existing, desired, absences, unmanagedConfigs, noPurge)
|
||||
if err != nil {
|
||||
|
|
@ -147,9 +159,25 @@ func handsoff(
|
|||
}
|
||||
}
|
||||
|
||||
// Check for conflicts between desired records and external-dns managed records.
|
||||
// This warns users when they define a record that external-dns is also managing.
|
||||
if ignoreExternalDNS && len(externalDNSIgnored) > 0 {
|
||||
externalDNSConflicts := findExternalDNSConflicts(desired, externalDNSIgnored)
|
||||
if len(externalDNSConflicts) != 0 {
|
||||
msgs = append(msgs, fmt.Sprintf("WARNING: %d records are defined in your config but also managed by external-dns:", len(externalDNSConflicts)))
|
||||
for _, r := range externalDNSConflicts {
|
||||
msgs = append(msgs, fmt.Sprintf(" %s %s %s", r.GetLabelFQDN(), r.Type, r.GetTargetCombined()))
|
||||
}
|
||||
msgs = append(msgs, "Consider removing these from your config or from external-dns to avoid conflicts.")
|
||||
}
|
||||
// Filter out conflicts from externalDNSIgnored to avoid duplicates in desired
|
||||
externalDNSIgnored = filterOutConflicts(externalDNSIgnored, externalDNSConflicts)
|
||||
}
|
||||
|
||||
// Add the ignored/foreign items to the desired list so they are not deleted:
|
||||
desired = append(desired, ignorable...)
|
||||
desired = append(desired, foreign...)
|
||||
desired = append(desired, externalDNSIgnored...)
|
||||
return desired, msgs, nil
|
||||
}
|
||||
|
||||
|
|
@ -285,3 +313,50 @@ func matchTarget(targetGlob glob.Glob, targetName string) bool {
|
|||
}
|
||||
return targetGlob.Match(targetName)
|
||||
}
|
||||
|
||||
// findExternalDNSConflicts returns records that appear in both desired and externalDNSIgnored.
|
||||
// This helps identify when a user has defined a record in their config that is also
|
||||
// being managed by external-dns.
|
||||
func findExternalDNSConflicts(desired, externalDNSIgnored models.Records) models.Records {
|
||||
// Build a map of desired records keyed by label:type
|
||||
desiredMap := make(map[string]bool)
|
||||
for _, rec := range desired {
|
||||
key := rec.GetLabel() + ":" + rec.Type
|
||||
desiredMap[key] = true
|
||||
}
|
||||
|
||||
// Find any external-dns ignored records that are also in desired
|
||||
var conflicts models.Records
|
||||
for _, rec := range externalDNSIgnored {
|
||||
key := rec.GetLabel() + ":" + rec.Type
|
||||
if desiredMap[key] {
|
||||
conflicts = append(conflicts, rec)
|
||||
}
|
||||
}
|
||||
return conflicts
|
||||
}
|
||||
|
||||
// filterOutConflicts removes records from externalDNSIgnored that are in conflicts.
|
||||
// This prevents duplicates when appending externalDNSIgnored to desired.
|
||||
func filterOutConflicts(externalDNSIgnored, conflicts models.Records) models.Records {
|
||||
if len(conflicts) == 0 {
|
||||
return externalDNSIgnored
|
||||
}
|
||||
|
||||
// Build a set of conflict keys
|
||||
conflictSet := make(map[string]bool)
|
||||
for _, rec := range conflicts {
|
||||
key := rec.GetLabel() + ":" + rec.Type
|
||||
conflictSet[key] = true
|
||||
}
|
||||
|
||||
// Filter out conflicts
|
||||
var filtered models.Records
|
||||
for _, rec := range externalDNSIgnored {
|
||||
key := rec.GetLabel() + ":" + rec.Type
|
||||
if !conflictSet[key] {
|
||||
filtered = append(filtered, rec)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,3 +237,204 @@ _2222222222222222.cr CNAME _333333.nnn.acm-validations.aws.
|
|||
FOREIGN:
|
||||
`)
|
||||
}
|
||||
|
||||
// Test_ignore_external_dns tests the IGNORE_EXTERNAL_DNS feature
|
||||
// using the full handsoff() function.
|
||||
func Test_ignore_external_dns(t *testing.T) {
|
||||
domain := "f.com"
|
||||
|
||||
// Existing zone has external-dns managed records
|
||||
existing := models.Records{
|
||||
// External-dns TXT ownership record
|
||||
makeTestRecord("a-myapp", "TXT", "heritage=external-dns,external-dns/owner=k8s-cluster", domain),
|
||||
// The A record managed by external-dns
|
||||
makeTestRecord("myapp", "A", "10.0.0.1", domain),
|
||||
// Static record not managed by external-dns
|
||||
makeTestRecord("static", "A", "1.2.3.4", domain),
|
||||
// Another external-dns managed record
|
||||
makeTestRecord("cname-api", "TXT", "heritage=external-dns,external-dns/owner=k8s-cluster", domain),
|
||||
makeTestRecord("api", "CNAME", "myapp.f.com.", domain),
|
||||
}
|
||||
|
||||
// Desired only has the static record
|
||||
desired := models.Records{
|
||||
makeTestRecord("static", "A", "1.2.3.4", domain),
|
||||
}
|
||||
|
||||
// Call handsoff with IGNORE_EXTERNAL_DNS enabled
|
||||
result, msgs, err := handsoff(
|
||||
domain,
|
||||
existing,
|
||||
desired,
|
||||
nil, // absences
|
||||
nil, // unmanagedConfigs
|
||||
false, // unmanagedSafely
|
||||
false, // noPurge
|
||||
true, // ignoreExternalDNS
|
||||
"", // externalDNSPrefix (empty = default)
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that external-dns records are in the result (so they won't be deleted)
|
||||
foundMyappA := false
|
||||
foundMyappTXT := false
|
||||
foundApiCNAME := false
|
||||
foundApiTXT := false
|
||||
foundStatic := false
|
||||
|
||||
for _, rec := range result {
|
||||
switch {
|
||||
case rec.GetLabel() == "myapp" && rec.Type == "A":
|
||||
foundMyappA = true
|
||||
case rec.GetLabel() == "a-myapp" && rec.Type == "TXT":
|
||||
foundMyappTXT = true
|
||||
case rec.GetLabel() == "api" && rec.Type == "CNAME":
|
||||
foundApiCNAME = true
|
||||
case rec.GetLabel() == "cname-api" && rec.Type == "TXT":
|
||||
foundApiTXT = true
|
||||
case rec.GetLabel() == "static" && rec.Type == "A":
|
||||
foundStatic = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundMyappA {
|
||||
t.Error("Expected myapp A record to be preserved")
|
||||
}
|
||||
if !foundMyappTXT {
|
||||
t.Error("Expected a-myapp TXT record to be preserved")
|
||||
}
|
||||
if !foundApiCNAME {
|
||||
t.Error("Expected api CNAME record to be preserved")
|
||||
}
|
||||
if !foundApiTXT {
|
||||
t.Error("Expected cname-api TXT record to be preserved")
|
||||
}
|
||||
if !foundStatic {
|
||||
t.Error("Expected static A record to be preserved")
|
||||
}
|
||||
|
||||
// Check that we got a message about external-dns records
|
||||
foundMsg := false
|
||||
for _, msg := range msgs {
|
||||
if strings.Contains(msg, "IGNORE_EXTERNAL_DNS") {
|
||||
foundMsg = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundMsg {
|
||||
t.Error("Expected message about IGNORE_EXTERNAL_DNS records")
|
||||
}
|
||||
}
|
||||
|
||||
// Test_ignore_external_dns_custom_prefix tests IGNORE_EXTERNAL_DNS with custom prefix
|
||||
func Test_ignore_external_dns_custom_prefix(t *testing.T) {
|
||||
domain := "f.com"
|
||||
|
||||
// Existing zone has external-dns managed records with custom prefix "extdns-"
|
||||
existing := models.Records{
|
||||
// External-dns TXT ownership record with custom prefix
|
||||
makeTestRecord("extdns-www", "TXT", "heritage=external-dns,external-dns/owner=k3s-cluster", domain),
|
||||
// The A record managed by external-dns
|
||||
makeTestRecord("www", "A", "10.0.0.1", domain),
|
||||
// Static record
|
||||
makeTestRecord("static", "A", "1.2.3.4", domain),
|
||||
}
|
||||
|
||||
// Desired only has the static record
|
||||
desired := models.Records{
|
||||
makeTestRecord("static", "A", "1.2.3.4", domain),
|
||||
}
|
||||
|
||||
// Call handsoff with custom prefix
|
||||
result, _, err := handsoff(
|
||||
domain,
|
||||
existing,
|
||||
desired,
|
||||
nil, // absences
|
||||
nil, // unmanagedConfigs
|
||||
false, // unmanagedSafely
|
||||
false, // noPurge
|
||||
true, // ignoreExternalDNS
|
||||
"extdns-", // externalDNSPrefix
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that external-dns records with custom prefix are preserved
|
||||
foundWwwA := false
|
||||
foundWwwTXT := false
|
||||
|
||||
for _, rec := range result {
|
||||
switch {
|
||||
case rec.GetLabel() == "www" && rec.Type == "A":
|
||||
foundWwwA = true
|
||||
case rec.GetLabel() == "extdns-www" && rec.Type == "TXT":
|
||||
foundWwwTXT = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundWwwA {
|
||||
t.Error("Expected www A record to be preserved with custom prefix")
|
||||
}
|
||||
if !foundWwwTXT {
|
||||
t.Error("Expected extdns-www TXT record to be preserved with custom prefix")
|
||||
}
|
||||
}
|
||||
|
||||
// Test_ignore_external_dns_conflict tests conflict detection
|
||||
func Test_ignore_external_dns_conflict(t *testing.T) {
|
||||
domain := "f.com"
|
||||
|
||||
// Existing zone has external-dns managed record
|
||||
existing := models.Records{
|
||||
makeTestRecord("a-myapp", "TXT", "heritage=external-dns,external-dns/owner=k8s-cluster", domain),
|
||||
makeTestRecord("myapp", "A", "10.0.0.1", domain),
|
||||
}
|
||||
|
||||
// Desired ALSO has myapp - this is a conflict!
|
||||
desired := models.Records{
|
||||
makeTestRecord("myapp", "A", "192.168.1.1", domain), // Different IP
|
||||
}
|
||||
|
||||
// Call handsoff with IGNORE_EXTERNAL_DNS enabled
|
||||
result, msgs, err := handsoff(
|
||||
domain,
|
||||
existing,
|
||||
desired,
|
||||
nil, // absences
|
||||
nil, // unmanagedConfigs
|
||||
false, // unmanagedSafely
|
||||
false, // noPurge
|
||||
true, // ignoreExternalDNS
|
||||
"", // externalDNSPrefix
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Should get a warning about the conflict
|
||||
foundConflictWarning := false
|
||||
for _, msg := range msgs {
|
||||
if strings.Contains(msg, "WARNING") && strings.Contains(msg, "external-dns") {
|
||||
foundConflictWarning = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundConflictWarning {
|
||||
t.Error("Expected warning about conflict between desired and external-dns records")
|
||||
}
|
||||
|
||||
// The desired record should be in result (not duplicated)
|
||||
myappCount := 0
|
||||
for _, rec := range result {
|
||||
if rec.GetLabel() == "myapp" && rec.Type == "A" {
|
||||
myappCount++
|
||||
}
|
||||
}
|
||||
if myappCount != 1 {
|
||||
t.Errorf("Expected exactly 1 myapp A record in result, got %d", myappCount)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ package domaintags
|
|||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/idna"
|
||||
)
|
||||
|
||||
// DomainFixedForms stores the various fixed forms of a domain name and tag.
|
||||
type DomainFixedForms struct {
|
||||
NameRaw string // "originalinput.com" (name as input by the user, lowercased (no tag))
|
||||
NameIDN string // "punycode.com"
|
||||
NameASCII string // "punycode.com"
|
||||
NameUnicode string // "unicode.com" (converted to downcase BEFORE unicode conversion)
|
||||
UniqueName string // "punycode.com!tag"
|
||||
|
||||
|
|
@ -22,7 +24,8 @@ type DomainFixedForms struct {
|
|||
// * .NameUnicode: unicode version of the name, downcased.
|
||||
// * .UniqueName: "example.com!tag" unique across the entire config.
|
||||
func MakeDomainNameVarieties(n string) DomainFixedForms {
|
||||
var tag, nameRaw, nameIDN, nameUnicode, uniqueName string
|
||||
var err error
|
||||
var tag, nameRaw, nameASCII, nameUnicode, uniqueName string
|
||||
var hasBang bool
|
||||
|
||||
// Split tag from name.
|
||||
|
|
@ -35,25 +38,45 @@ func MakeDomainNameVarieties(n string) DomainFixedForms {
|
|||
hasBang = false
|
||||
}
|
||||
|
||||
nameRaw = strings.ToLower(p[0])
|
||||
nameRaw = p[0]
|
||||
if strings.HasPrefix(n, nameRaw) {
|
||||
// Avoid pointless duplication.
|
||||
nameRaw = n[0:len(nameRaw)]
|
||||
}
|
||||
|
||||
nameIDN = EfficientToASCII(nameRaw)
|
||||
nameUnicode = EfficientToUnicode(nameRaw)
|
||||
nameASCII, err = idna.ToASCII(nameRaw)
|
||||
if err != nil {
|
||||
nameASCII = nameRaw // Fallback to raw name on error.
|
||||
} else {
|
||||
nameASCII = strings.ToLower(nameASCII)
|
||||
// Avoid pointless duplication.
|
||||
if strings.HasPrefix(n, nameASCII) {
|
||||
// Avoid pointless duplication.
|
||||
nameASCII = n[0:len(nameASCII)]
|
||||
}
|
||||
}
|
||||
|
||||
nameUnicode, err = idna.ToUnicode(nameASCII) // We use nameASCII since it is already lowercased.
|
||||
if err != nil {
|
||||
nameUnicode = nameRaw // Fallback to raw name on error.
|
||||
} else {
|
||||
// Avoid pointless duplication.
|
||||
if strings.HasPrefix(n, nameUnicode) {
|
||||
// Avoid pointless duplication.
|
||||
nameUnicode = n[0:len(nameUnicode)]
|
||||
}
|
||||
}
|
||||
|
||||
if hasBang {
|
||||
uniqueName = nameIDN + "!" + tag
|
||||
uniqueName = nameASCII + "!" + tag
|
||||
} else {
|
||||
uniqueName = nameIDN
|
||||
uniqueName = nameASCII
|
||||
}
|
||||
|
||||
return DomainFixedForms{
|
||||
Tag: tag,
|
||||
NameRaw: nameRaw,
|
||||
NameIDN: nameIDN,
|
||||
NameASCII: nameASCII,
|
||||
NameUnicode: nameUnicode,
|
||||
UniqueName: uniqueName,
|
||||
HasBang: hasBang,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
|
|||
input string
|
||||
wantTag string
|
||||
wantNameRaw string
|
||||
wantNameIDN string
|
||||
wantNameASCII string
|
||||
wantNameUnicode string
|
||||
wantUniqueName string
|
||||
wantHasBang bool
|
||||
|
|
@ -20,7 +20,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
|
|||
input: "example.com",
|
||||
wantTag: "",
|
||||
wantNameRaw: "example.com",
|
||||
wantNameIDN: "example.com",
|
||||
wantNameASCII: "example.com",
|
||||
wantNameUnicode: "example.com",
|
||||
wantUniqueName: "example.com",
|
||||
wantHasBang: false,
|
||||
|
|
@ -30,7 +30,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
|
|||
input: "example.com!mytag",
|
||||
wantTag: "mytag",
|
||||
wantNameRaw: "example.com",
|
||||
wantNameIDN: "example.com",
|
||||
wantNameASCII: "example.com",
|
||||
wantNameUnicode: "example.com",
|
||||
wantUniqueName: "example.com!mytag",
|
||||
wantHasBang: true,
|
||||
|
|
@ -40,7 +40,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
|
|||
input: "example.com!",
|
||||
wantTag: "",
|
||||
wantNameRaw: "example.com",
|
||||
wantNameIDN: "example.com",
|
||||
wantNameASCII: "example.com",
|
||||
wantNameUnicode: "example.com",
|
||||
wantUniqueName: "example.com!",
|
||||
wantHasBang: true,
|
||||
|
|
@ -50,7 +50,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
|
|||
input: "उदाहरण.com",
|
||||
wantTag: "",
|
||||
wantNameRaw: "उदाहरण.com",
|
||||
wantNameIDN: "xn--p1b6ci4b4b3a.com",
|
||||
wantNameASCII: "xn--p1b6ci4b4b3a.com",
|
||||
wantNameUnicode: "उदाहरण.com",
|
||||
wantUniqueName: "xn--p1b6ci4b4b3a.com",
|
||||
wantHasBang: false,
|
||||
|
|
@ -60,7 +60,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
|
|||
input: "उदाहरण.com!mytag",
|
||||
wantTag: "mytag",
|
||||
wantNameRaw: "उदाहरण.com",
|
||||
wantNameIDN: "xn--p1b6ci4b4b3a.com",
|
||||
wantNameASCII: "xn--p1b6ci4b4b3a.com",
|
||||
wantNameUnicode: "उदाहरण.com",
|
||||
wantUniqueName: "xn--p1b6ci4b4b3a.com!mytag",
|
||||
wantHasBang: true,
|
||||
|
|
@ -70,17 +70,29 @@ func Test_MakeDomainFixForms(t *testing.T) {
|
|||
input: "xn--p1b6ci4b4b3a.com",
|
||||
wantTag: "",
|
||||
wantNameRaw: "xn--p1b6ci4b4b3a.com",
|
||||
wantNameIDN: "xn--p1b6ci4b4b3a.com",
|
||||
wantNameASCII: "xn--p1b6ci4b4b3a.com",
|
||||
wantNameUnicode: "उदाहरण.com",
|
||||
wantUniqueName: "xn--p1b6ci4b4b3a.com",
|
||||
wantHasBang: false,
|
||||
},
|
||||
{
|
||||
// Unicode chars should be left alone (as far as case folding goes)
|
||||
// Here are some Armenian characters https://tools.lgm.cl/lettercase.html
|
||||
name: "mixed case unicode",
|
||||
input: "fooԷէԸըԹ.com!myTag",
|
||||
wantTag: "myTag",
|
||||
wantNameRaw: "fooԷէԸըԹ.com",
|
||||
wantNameASCII: "xn--foo-b7dfg43aja.com",
|
||||
wantNameUnicode: "fooԷէԸըԹ.com",
|
||||
wantUniqueName: "xn--foo-b7dfg43aja.com!myTag",
|
||||
wantHasBang: true,
|
||||
},
|
||||
{
|
||||
name: "punycode domain with tag",
|
||||
input: "xn--p1b6ci4b4b3a.com!mytag",
|
||||
wantTag: "mytag",
|
||||
wantNameRaw: "xn--p1b6ci4b4b3a.com",
|
||||
wantNameIDN: "xn--p1b6ci4b4b3a.com",
|
||||
wantNameASCII: "xn--p1b6ci4b4b3a.com",
|
||||
wantNameUnicode: "उदाहरण.com",
|
||||
wantUniqueName: "xn--p1b6ci4b4b3a.com!mytag",
|
||||
wantHasBang: true,
|
||||
|
|
@ -89,8 +101,8 @@ func Test_MakeDomainFixForms(t *testing.T) {
|
|||
name: "mixed case domain",
|
||||
input: "Example.COM",
|
||||
wantTag: "",
|
||||
wantNameRaw: "example.com",
|
||||
wantNameIDN: "example.com",
|
||||
wantNameRaw: "Example.COM",
|
||||
wantNameASCII: "example.com",
|
||||
wantNameUnicode: "example.com",
|
||||
wantUniqueName: "example.com",
|
||||
wantHasBang: false,
|
||||
|
|
@ -99,12 +111,34 @@ func Test_MakeDomainFixForms(t *testing.T) {
|
|||
name: "mixed case domain with tag",
|
||||
input: "Example.COM!MyTag",
|
||||
wantTag: "MyTag",
|
||||
wantNameRaw: "example.com",
|
||||
wantNameIDN: "example.com",
|
||||
wantNameRaw: "Example.COM",
|
||||
wantNameASCII: "example.com",
|
||||
wantNameUnicode: "example.com",
|
||||
wantUniqueName: "example.com!MyTag",
|
||||
wantHasBang: true,
|
||||
},
|
||||
// This is used in the documentation for the BIND provider, thus we test
|
||||
// it to make sure we got it right.
|
||||
{
|
||||
name: "BIND example 1",
|
||||
input: "рф.com!myTag",
|
||||
wantTag: "myTag",
|
||||
wantNameRaw: "рф.com",
|
||||
wantNameASCII: "xn--p1ai.com",
|
||||
wantNameUnicode: "рф.com",
|
||||
wantUniqueName: "xn--p1ai.com!myTag",
|
||||
wantHasBang: true,
|
||||
},
|
||||
{
|
||||
name: "BIND example 2",
|
||||
input: "рф.com",
|
||||
wantTag: "",
|
||||
wantNameRaw: "рф.com",
|
||||
wantNameASCII: "xn--p1ai.com",
|
||||
wantNameUnicode: "рф.com",
|
||||
wantUniqueName: "xn--p1ai.com",
|
||||
wantHasBang: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -116,8 +150,8 @@ func Test_MakeDomainFixForms(t *testing.T) {
|
|||
if got.NameRaw != tt.wantNameRaw {
|
||||
t.Errorf("MakeDomainFixForms() gotNameRaw = %v, want %v", got.NameRaw, tt.wantNameRaw)
|
||||
}
|
||||
if got.NameIDN != tt.wantNameIDN {
|
||||
t.Errorf("MakeDomainFixForms() gotNameIDN = %v, want %v", got.NameIDN, tt.wantNameIDN)
|
||||
if got.NameASCII != tt.wantNameASCII {
|
||||
t.Errorf("MakeDomainFixForms() gotNameASCII = %v, want %v", got.NameASCII, tt.wantNameASCII)
|
||||
}
|
||||
if got.NameUnicode != tt.wantNameUnicode {
|
||||
t.Errorf("MakeDomainFixForms() gotNameUnicode = %v, want %v", got.NameUnicode, tt.wantNameUnicode)
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ func CompilePermitList(s string) PermitList {
|
|||
continue
|
||||
}
|
||||
ff := MakeDomainNameVarieties(l)
|
||||
if ff.HasBang && ff.NameIDN == "" { // Treat empty name as wildcard.
|
||||
ff.NameIDN = "*"
|
||||
if ff.HasBang && ff.NameASCII == "" { // Treat empty name as wildcard.
|
||||
ff.NameASCII = "*"
|
||||
}
|
||||
sl.items = append(sl.items, ff)
|
||||
}
|
||||
|
|
@ -65,24 +65,24 @@ func (pl *PermitList) Permitted(domToCheck string) bool {
|
|||
// Now that we know the tag matches, we can focus on the name.
|
||||
|
||||
// `*!tag` or `*` matches everything.
|
||||
if filterItem.NameIDN == "*" {
|
||||
if filterItem.NameASCII == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
// If the name starts with "*." then match the suffix.
|
||||
if strings.HasPrefix(filterItem.NameIDN, "*.") {
|
||||
if strings.HasPrefix(filterItem.NameASCII, "*.") {
|
||||
// example.com matches *.example.com
|
||||
if domToCheckFF.NameIDN == filterItem.NameIDN[2:] || domToCheckFF.NameUnicode == filterItem.NameUnicode[2:] {
|
||||
if domToCheckFF.NameASCII == filterItem.NameASCII[2:] || domToCheckFF.NameUnicode == filterItem.NameUnicode[2:] {
|
||||
return true
|
||||
}
|
||||
// foo.example.com matches *.example.com
|
||||
if strings.HasSuffix(domToCheckFF.NameIDN, filterItem.NameIDN[1:]) || strings.HasSuffix(domToCheckFF.NameUnicode, filterItem.NameUnicode[1:]) {
|
||||
if strings.HasSuffix(domToCheckFF.NameASCII, filterItem.NameASCII[1:]) || strings.HasSuffix(domToCheckFF.NameUnicode, filterItem.NameUnicode[1:]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// No wildcards? Exact match.
|
||||
if filterItem.NameIDN == domToCheckFF.NameIDN || filterItem.NameUnicode == domToCheckFF.NameUnicode {
|
||||
if filterItem.NameASCII == domToCheckFF.NameASCII || filterItem.NameUnicode == domToCheckFF.NameUnicode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -335,7 +335,15 @@ var AKAMAICDN = recordBuilder('AKAMAICDN');
|
|||
var AKAMAITLC = recordBuilder('AKAMAITLC', {
|
||||
args: [
|
||||
['name', _.isString],
|
||||
['answer_type', function (value) { return _.isString(value) && ['DUAL', 'A', 'AAAA'].indexOf(value) !== -1; }],
|
||||
[
|
||||
'answer_type',
|
||||
function (value) {
|
||||
return (
|
||||
_.isString(value) &&
|
||||
['DUAL', 'A', 'AAAA'].indexOf(value) !== -1
|
||||
);
|
||||
},
|
||||
],
|
||||
['target', _.isString],
|
||||
],
|
||||
transform: function (record, args, modifier) {
|
||||
|
|
@ -858,15 +866,15 @@ function locStringBuilder(record, args) {
|
|||
(args.alt < -100000
|
||||
? -100000
|
||||
: args.alt > 42849672.95
|
||||
? 42849672.95
|
||||
: args.alt.toString()) + 'm';
|
||||
? 42849672.95
|
||||
: args.alt.toString()) + 'm';
|
||||
precisionbuffer +=
|
||||
' ' +
|
||||
(args.siz > 90000000
|
||||
? 90000000
|
||||
: args.siz < 0
|
||||
? 0
|
||||
: args.siz.toString()) +
|
||||
? 0
|
||||
: args.siz.toString()) +
|
||||
'm';
|
||||
precisionbuffer +=
|
||||
' ' +
|
||||
|
|
@ -906,8 +914,8 @@ function locDMSBuilder(record, args) {
|
|||
record.localtitude > 4294967295
|
||||
? 4294967295
|
||||
: record.localtitude < 0
|
||||
? 0
|
||||
: record.localtitude;
|
||||
? 0
|
||||
: record.localtitude;
|
||||
// Size
|
||||
record.locsize = getENotationInt(args.siz);
|
||||
// Horizontal Precision
|
||||
|
|
@ -1163,6 +1171,29 @@ function NO_PURGE(d) {
|
|||
d.KeepUnknown = true;
|
||||
}
|
||||
|
||||
// IGNORE_EXTERNAL_DNS(prefix)
|
||||
// When enabled, DNSControl will automatically detect TXT records created by
|
||||
// Kubernetes external-dns and ignore both the TXT records and the corresponding
|
||||
// DNS records they manage. External-dns creates TXT records with content like:
|
||||
// "heritage=external-dns,external-dns/owner=<owner-id>,external-dns/resource=<resource>"
|
||||
// This allows DNSControl to coexist with external-dns in the same zone.
|
||||
//
|
||||
// Optional prefix parameter: If your external-dns is configured with a custom
|
||||
// --txt-prefix (e.g., "extdns-"), pass it here to detect those records.
|
||||
// Without a prefix, it detects the default format ("%{record_type}-" prefixes like "a-", "cname-").
|
||||
//
|
||||
// Usage:
|
||||
// IGNORE_EXTERNAL_DNS() // Use default detection (a-, cname-, etc.)
|
||||
// IGNORE_EXTERNAL_DNS("extdns-") // Custom prefix
|
||||
function IGNORE_EXTERNAL_DNS(prefix) {
|
||||
return function (d) {
|
||||
d.ignore_external_dns = true;
|
||||
if (prefix) {
|
||||
d.external_dns_prefix = prefix;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ENSURE_ABSENT_REC()
|
||||
// Usage: A("foo", "1.2.3.4", ENSURE_ABSENT_REC())
|
||||
function ENSURE_ABSENT_REC() {
|
||||
|
|
@ -1785,7 +1816,7 @@ function CAA_BUILDER(value) {
|
|||
throw 'CAA_BUILDER requires at least one entry at issue, issuewild, issuevmc or issuemail';
|
||||
}
|
||||
|
||||
var CAA_TTL = function () { };
|
||||
var CAA_TTL = function () {};
|
||||
if (value.ttl) {
|
||||
CAA_TTL = TTL(value.ttl);
|
||||
}
|
||||
|
|
@ -1802,7 +1833,7 @@ function CAA_BUILDER(value) {
|
|||
}
|
||||
|
||||
if (value.issue) {
|
||||
var flag = function () { };
|
||||
var flag = function () {};
|
||||
if (value.issue_critical) {
|
||||
flag = CAA_CRITICAL;
|
||||
}
|
||||
|
|
@ -1811,7 +1842,7 @@ function CAA_BUILDER(value) {
|
|||
}
|
||||
|
||||
if (value.issuewild) {
|
||||
var flag = function () { };
|
||||
var flag = function () {};
|
||||
if (value.issuewild_critical) {
|
||||
flag = CAA_CRITICAL;
|
||||
}
|
||||
|
|
@ -1822,7 +1853,7 @@ function CAA_BUILDER(value) {
|
|||
}
|
||||
|
||||
if (value.issuevmc) {
|
||||
var flag = function () { };
|
||||
var flag = function () {};
|
||||
if (value.issuevmc_critical) {
|
||||
flag = CAA_CRITICAL;
|
||||
}
|
||||
|
|
@ -1833,7 +1864,7 @@ function CAA_BUILDER(value) {
|
|||
}
|
||||
|
||||
if (value.issuemail) {
|
||||
var flag = function () { };
|
||||
var flag = function () {};
|
||||
if (value.issuemail_critical) {
|
||||
flag = CAA_CRITICAL;
|
||||
}
|
||||
|
|
@ -2052,7 +2083,7 @@ function DKIM_BUILDER(value) {
|
|||
}
|
||||
|
||||
// Handle TTL
|
||||
var DKIM_TTL = value.ttl ? TTL(value.ttl) : function () { };
|
||||
var DKIM_TTL = value.ttl ? TTL(value.ttl) : function () {};
|
||||
|
||||
return TXT(fullLabel, record.join('; '), DKIM_TTL);
|
||||
}
|
||||
|
|
@ -2284,20 +2315,20 @@ function M365_BUILDER(name, value) {
|
|||
CNAME(
|
||||
'selector1._domainkey',
|
||||
'selector1-' +
|
||||
value.domainGUID +
|
||||
'._domainkey.' +
|
||||
value.initialDomain +
|
||||
'.'
|
||||
value.domainGUID +
|
||||
'._domainkey.' +
|
||||
value.initialDomain +
|
||||
'.'
|
||||
)
|
||||
);
|
||||
r.push(
|
||||
CNAME(
|
||||
'selector2._domainkey',
|
||||
'selector2-' +
|
||||
value.domainGUID +
|
||||
'._domainkey.' +
|
||||
value.initialDomain +
|
||||
'.'
|
||||
value.domainGUID +
|
||||
'._domainkey.' +
|
||||
value.initialDomain +
|
||||
'.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
13
pkg/js/parse_tests/058-ignore-external-dns.js
Normal file
13
pkg/js/parse_tests/058-ignore-external-dns.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Test IGNORE_EXTERNAL_DNS domain modifier
|
||||
// Default usage (no prefix)
|
||||
D("extdns-default.com", "none", IGNORE_EXTERNAL_DNS());
|
||||
|
||||
// With custom prefix
|
||||
D("extdns-custom.com", "none", IGNORE_EXTERNAL_DNS("extdns-"));
|
||||
|
||||
// Combined with other records
|
||||
D("extdns-combined.com", "none",
|
||||
IGNORE_EXTERNAL_DNS(),
|
||||
A("www", "1.2.3.4"),
|
||||
CNAME("api", "www")
|
||||
);
|
||||
52
pkg/js/parse_tests/058-ignore-external-dns.json
Normal file
52
pkg/js/parse_tests/058-ignore-external-dns.json
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"dns_providers": [],
|
||||
"domains": [
|
||||
{
|
||||
"dnsProviders": {},
|
||||
"ignore_external_dns": true,
|
||||
"meta": {
|
||||
"dnscontrol_uniquename": "extdns-default.com"
|
||||
},
|
||||
"name": "extdns-default.com",
|
||||
"records": [],
|
||||
"registrar": "none"
|
||||
},
|
||||
{
|
||||
"dnsProviders": {},
|
||||
"external_dns_prefix": "extdns-",
|
||||
"ignore_external_dns": true,
|
||||
"meta": {
|
||||
"dnscontrol_uniquename": "extdns-custom.com"
|
||||
},
|
||||
"name": "extdns-custom.com",
|
||||
"records": [],
|
||||
"registrar": "none"
|
||||
},
|
||||
{
|
||||
"dnsProviders": {},
|
||||
"ignore_external_dns": true,
|
||||
"meta": {
|
||||
"dnscontrol_uniquename": "extdns-combined.com"
|
||||
},
|
||||
"name": "extdns-combined.com",
|
||||
"records": [
|
||||
{
|
||||
"filepos": "[line:12:5]",
|
||||
"name": "api",
|
||||
"target": "www.extdns-combined.com.",
|
||||
"ttl": 300,
|
||||
"type": "CNAME"
|
||||
},
|
||||
{
|
||||
"filepos": "[line:11:5]",
|
||||
"name": "www",
|
||||
"target": "1.2.3.4",
|
||||
"ttl": 300,
|
||||
"type": "A"
|
||||
}
|
||||
],
|
||||
"registrar": "none"
|
||||
}
|
||||
],
|
||||
"registrars": []
|
||||
}
|
||||
|
|
@ -71,7 +71,7 @@ func initBind(config map[string]string, providermeta json.RawMessage) (providers
|
|||
api.directory = "zones"
|
||||
}
|
||||
if api.filenameformat == "" {
|
||||
api.filenameformat = "%U.zone"
|
||||
api.filenameformat = "%c.zone"
|
||||
}
|
||||
if len(providermeta) != 0 {
|
||||
err := json.Unmarshal(providermeta, api)
|
||||
|
|
@ -173,7 +173,7 @@ func (c *bindProvider) GetZoneRecords(domain string, meta map[string]string) (mo
|
|||
ff := domaintags.DomainFixedForms{
|
||||
Tag: meta[models.DomainTag],
|
||||
NameRaw: meta[models.DomainNameRaw],
|
||||
NameIDN: domain,
|
||||
NameASCII: domain,
|
||||
NameUnicode: meta[models.DomainNameUnicode],
|
||||
UniqueName: meta[models.DomainUniqueName],
|
||||
}
|
||||
|
|
@ -305,7 +305,7 @@ func (c *bindProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundR
|
|||
domaintags.DomainFixedForms{
|
||||
Tag: dc.Tag,
|
||||
NameRaw: dc.NameRaw,
|
||||
NameIDN: dc.Name,
|
||||
NameASCII: dc.Name,
|
||||
NameUnicode: dc.NameUnicode,
|
||||
UniqueName: dc.UniqueName,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,11 +13,6 @@ import (
|
|||
// makeFileName uses format to generate a zone's filename. See the
|
||||
func makeFileName(format string, ff domaintags.DomainFixedForms) string {
|
||||
//fmt.Printf("DEBUG: makeFileName(%q, %+v)\n", format, ff)
|
||||
nameRaw := ff.NameRaw
|
||||
nameIDN := ff.NameIDN
|
||||
nameUnicode := ff.NameUnicode
|
||||
uniquename := ff.UniqueName
|
||||
tag := ff.Tag
|
||||
if format == "" {
|
||||
panic("BUG: makeFileName called with null format")
|
||||
}
|
||||
|
|
@ -40,16 +35,31 @@ func makeFileName(format string, ff domaintags.DomainFixedForms) string {
|
|||
pos++
|
||||
tok = tokens[pos]
|
||||
switch tok {
|
||||
case "D":
|
||||
b.WriteString(nameRaw)
|
||||
case "T":
|
||||
b.WriteString(tag)
|
||||
case "U":
|
||||
b.WriteString(uniquename)
|
||||
case "I":
|
||||
b.WriteString(nameIDN)
|
||||
case "N":
|
||||
b.WriteString(nameUnicode)
|
||||
|
||||
// v4.28 names
|
||||
case "r": // NameRaw "originalinput.com" (i for input)
|
||||
b.WriteString(ff.NameRaw)
|
||||
case "a": // NameASCII "punycode.com" (a for ascii)
|
||||
b.WriteString(ff.NameASCII)
|
||||
case "u": // NameUnicode "unicode.com" (u for unicode)
|
||||
b.WriteString(ff.NameUnicode)
|
||||
case "c": // UniqueName "punycode.com!tag" or "punycode.com" if no tag (c for canonical)
|
||||
b.WriteString(ff.UniqueName)
|
||||
case "f": //
|
||||
b.WriteString(ff.NameASCII)
|
||||
if ff.Tag != "" {
|
||||
b.WriteString("_")
|
||||
b.WriteString(ff.Tag)
|
||||
}
|
||||
case "F": //
|
||||
if ff.Tag != "" {
|
||||
b.WriteString(ff.Tag)
|
||||
b.WriteString("_")
|
||||
}
|
||||
b.WriteString(ff.NameASCII)
|
||||
case "T": // Tag The tag portion of `example.com!tag`
|
||||
b.WriteString(ff.Tag)
|
||||
|
||||
case "%":
|
||||
b.WriteString("%")
|
||||
case "?":
|
||||
|
|
@ -59,9 +69,20 @@ func makeFileName(format string, ff domaintags.DomainFixedForms) string {
|
|||
}
|
||||
pos++
|
||||
tok = tokens[pos]
|
||||
if tag != "" {
|
||||
if ff.Tag != "" {
|
||||
b.WriteString(tok)
|
||||
}
|
||||
|
||||
// Legacy names kept for compatibility
|
||||
case "U": // the domain name as specified in `D()`
|
||||
b.WriteString(strings.ToLower(ff.NameRaw))
|
||||
if ff.Tag != "" {
|
||||
b.WriteString("!")
|
||||
b.WriteString(ff.Tag)
|
||||
}
|
||||
case "D": // domain (without tag) as specified in D() (no IDN conversion, but downcased)
|
||||
b.WriteString(strings.ToLower(ff.NameRaw))
|
||||
|
||||
default:
|
||||
fmt.Fprintf(&b, "%%(unknown %%verb %%%s)", tok)
|
||||
}
|
||||
|
|
@ -156,14 +177,14 @@ func makeExtractor(format string) (string, error) {
|
|||
pos++
|
||||
tok = tokens[pos]
|
||||
switch tok {
|
||||
case "D":
|
||||
case "D", "a", "u", "r":
|
||||
b.WriteString(`(.*)`)
|
||||
case "T":
|
||||
if pass == 0 {
|
||||
// On the second pass, nothing is generated.
|
||||
b.WriteString(`.*`)
|
||||
}
|
||||
case "U":
|
||||
case "U", "c":
|
||||
if pass == 0 {
|
||||
b.WriteString(`(.*)!.+`)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -8,29 +8,28 @@ import (
|
|||
)
|
||||
|
||||
func Test_makeFileName(t *testing.T) {
|
||||
ff := domaintags.DomainFixedForms{
|
||||
NameRaw: "raw",
|
||||
NameIDN: "idn",
|
||||
NameUnicode: "unicode",
|
||||
UniqueName: "unique!taga",
|
||||
Tag: "tagb",
|
||||
}
|
||||
tagless := domaintags.DomainFixedForms{
|
||||
NameRaw: "raw",
|
||||
NameIDN: "idn",
|
||||
NameUnicode: "unicode",
|
||||
UniqueName: "unique",
|
||||
Tag: "",
|
||||
}
|
||||
fmtDefault := "%U.zone"
|
||||
fmtBasic := "%U - %T - %D"
|
||||
fmtBk1 := "db_%U" // Something I've seen in books on DNS
|
||||
fmtBk2 := "db_%T_%D" // Something I've seen in books on DNS
|
||||
fmtFancy := "%T%?_%D.zone" // Include the tag_ only if there is a tag
|
||||
fmtErrorPct := "literal%"
|
||||
fmtErrorOpt := "literal%?"
|
||||
fmtErrorUnk := "literal%o" // Unknown % verb
|
||||
|
||||
ff := domaintags.DomainFixedForms{
|
||||
NameRaw: "domy",
|
||||
NameASCII: "idn",
|
||||
NameUnicode: "uni",
|
||||
UniqueName: "unique!taga",
|
||||
Tag: "tagy",
|
||||
}
|
||||
tagless := domaintags.DomainFixedForms{
|
||||
NameRaw: "domy",
|
||||
NameASCII: "idn",
|
||||
NameUnicode: "uni",
|
||||
UniqueName: "unique",
|
||||
Tag: "",
|
||||
}
|
||||
|
||||
type args struct {
|
||||
format string
|
||||
ff domaintags.DomainFixedForms
|
||||
|
|
@ -40,25 +39,15 @@ func Test_makeFileName(t *testing.T) {
|
|||
args args
|
||||
want string
|
||||
}{
|
||||
// Test corner cases and common cases.
|
||||
{"literal", args{"literal", ff}, "literal"},
|
||||
{"middle", args{"mid%Dle", ff}, "midrawle"},
|
||||
{"D", args{"%D", ff}, "raw"},
|
||||
{"I", args{"%I", ff}, "idn"},
|
||||
{"N", args{"%N", ff}, "unicode"},
|
||||
{"T", args{"%T", ff}, "tagb"},
|
||||
{"x1", args{"XX%?xYY", ff}, "XXxYY"},
|
||||
{"x2", args{"AA%?xBB", tagless}, "AABB"},
|
||||
{"U", args{"%U", ff}, "unique!taga"},
|
||||
{"percent", args{"%%", ff}, "%"},
|
||||
//
|
||||
{"default", args{fmtDefault, ff}, "unique!taga.zone"},
|
||||
{"basic", args{fmtBasic, ff}, "unique!taga - tagb - raw"},
|
||||
{"front", args{"%Daaa", ff}, "rawaaa"},
|
||||
{"tail", args{"bbb%D", ff}, "bbbraw"},
|
||||
{"bk1", args{fmtBk1, ff}, "db_unique!taga"},
|
||||
{"bk2", args{fmtBk2, ff}, "db_tagb_raw"},
|
||||
{"fanWI", args{fmtFancy, ff}, "tagb_raw.zone"},
|
||||
{"fanWO", args{fmtFancy, tagless}, "raw.zone"},
|
||||
{"basic", args{fmtBasic, ff}, "domy!tagy - tagy - domy"},
|
||||
{"solo", args{"%D", ff}, "domy"},
|
||||
{"front", args{"%Daaa", ff}, "domyaaa"},
|
||||
{"tail", args{"bbb%D", ff}, "bbbdomy"},
|
||||
{"def", args{fmtDefault, ff}, "domy!tagy.zone"},
|
||||
{"fanWI", args{fmtFancy, ff}, "tagy_domy.zone"},
|
||||
{"fanWO", args{fmtFancy, tagless}, "domy.zone"},
|
||||
{"errP", args{fmtErrorPct, ff}, "literal%(format may not end in %)"},
|
||||
{"errQ", args{fmtErrorOpt, ff}, "literal%(format may not end in %?)"},
|
||||
{"errU", args{fmtErrorUnk, ff}, "literal%(unknown %verb %o)"},
|
||||
|
|
@ -73,6 +62,66 @@ func Test_makeFileName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_makeFileName_2(t *testing.T) {
|
||||
ff1 := domaintags.MakeDomainNameVarieties(`EXAMple.com`)
|
||||
ff2 := domaintags.MakeDomainNameVarieties(`EXAMple.com!myTag`)
|
||||
ff3 := domaintags.MakeDomainNameVarieties(`рф.com!myTag`)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
format string
|
||||
want1 string
|
||||
want2 string
|
||||
want3 string
|
||||
descr string // Not used in test, just for documentation generation
|
||||
}{
|
||||
// NOTE: "Domain" in these descriptions means the domain name without any split horizon tag. Technically the "Zone".
|
||||
{`T`, `%T`, ``, `myTag`, `myTag`, `the tag`},
|
||||
{`c`, `%c`, `example.com`, `example.com!myTag`, `xn--p1ai.com!myTag`, `canonical name, globally unique and comparable`},
|
||||
{`a`, `%a`, `example.com`, `example.com`, `xn--p1ai.com`, `ASCII domain (Punycode, downcased)`},
|
||||
{`u`, `%u`, `example.com`, `example.com`, `рф.com`, `Unicode domain (non-Unicode parts downcased)`},
|
||||
{`r`, `%r`, `EXAMple.com`, `EXAMple.com`, `рф.com`, "Raw (unmodified) Domain from `D()` (risky!)"},
|
||||
{`f`, `%f`, `example.com`, `example.com_myTag`, `xn--p1ai.com_myTag`, "like `%c` but better for filenames (`%a%?_%T`)"},
|
||||
{`F`, `%F`, `example.com`, `myTag_example.com`, `myTag_xn--p1ai.com`, "like `%f` but reversed order (`%T%?_%a`)"},
|
||||
{`%?x`, `%?x`, ``, `x`, `x`, "returns `x` if tag exists, otherwise \"\""},
|
||||
{`%`, `%%`, `%`, `%`, `%`, `a literal percent sign`},
|
||||
|
||||
// Pre-v4.28 names kept for compatibility (note: pre v4.28 did not permit mixed case domain names, we downcased them here for the tests)
|
||||
{`U`, `%U`, `example.com`, `example.com!myTag`, `рф.com!myTag`, "(deprecated, use `%c`) Same as `%D%?!%T` (risky!)"},
|
||||
{`D`, `%D`, `example.com`, `example.com`, `рф.com`, "(deprecated, use `%r`) mangles Unicode (risky!)"},
|
||||
{`%T%?_%D.zone`, `%T%?_%D.zone`, `example.com.zone`, `myTag_example.com.zone`, `myTag_рф.com.zone`, `mentioned in the docs`},
|
||||
{`db_%T%?_%D`, `db_%T%?_%D`, `db_example.com`, `db_myTag_example.com`, `db_myTag_рф.com`, `mentioned in the docs`},
|
||||
{`db_%D`, `db_%D`, `db_example.com`, `db_example.com`, `db_рф.com`, `mentioned in the docs`},
|
||||
|
||||
// Examples used in the documentation for the BIND provider
|
||||
{`%c.zone`, `%c.zone`, "example.com.zone", "example.com!myTag.zone", "xn--p1ai.com!myTag.zone", "Default format (v4.28 and later)"},
|
||||
{`%U.zone`, `%U.zone`, `example.com.zone`, `example.com!myTag.zone`, `рф.com!myTag.zone`, "Default format (pre-v4.28) (risky!)"},
|
||||
{`db_%f`, `db_%f`, `db_example.com`, `db_example.com_myTag`, `db_xn--p1ai.com_myTag`, "Recommended in a popular DNS book"},
|
||||
{`db_a%?_%T`, `db_%a%?_%T`, `db_example.com`, `db_example.com_myTag`, `db_xn--p1ai.com_myTag`, "same as above but using `%?_`"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got1 := makeFileName(tt.format, ff1)
|
||||
if got1 != tt.want1 {
|
||||
t.Errorf("makeFileName(%q) = ff1 %q, want %q", tt.format, got1, tt.want1)
|
||||
}
|
||||
got2 := makeFileName(tt.format, ff2)
|
||||
if got2 != tt.want2 {
|
||||
t.Errorf("makeFileName(%q) = ff2 %q, want %q", tt.format, got2, tt.want2)
|
||||
}
|
||||
got3 := makeFileName(tt.format, ff3)
|
||||
if got3 != tt.want3 {
|
||||
t.Errorf("makeFileName(%q) = ff3 %q, want %q", tt.format, got3, tt.want3)
|
||||
}
|
||||
//Uncomment to regenerate lines used in documentation/provider/bind.md 's table:
|
||||
// fmt.Print(strings.ReplaceAll(fmt.Sprintf("| `%s` | %s | `%s` | `%s` | `%s` |\n", tt.format, tt.descr, got1, got2, got3), "``", "`\"\"` (null)"))
|
||||
//Uncomment to regenerate the above test cases:
|
||||
// fmt.Printf("{`%s`, `%s`, `%s`, `%s`, `%s`, %q},\n", tt.name, tt.format, got1, got2, got3, tt.descr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_makeExtractor(t *testing.T) {
|
||||
type args struct {
|
||||
format string
|
||||
|
|
@ -83,6 +132,10 @@ func Test_makeExtractor(t *testing.T) {
|
|||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{"c", args{"%c.zone"}, `(.*)!.+\.zone|(.*)\.zone`, false},
|
||||
{"a", args{"%a.zone"}, `(.*)\.zone`, false},
|
||||
{"u", args{"%u.zone"}, `(.*)\.zone`, false},
|
||||
{"r", args{"%r.zone"}, `(.*)\.zone`, false},
|
||||
// TODO: Add test cases.
|
||||
{"u", args{"%U.zone"}, `(.*)!.+\.zone|(.*)\.zone`, false},
|
||||
{"d", args{"%D.zone"}, `(.*)\.zone`, false},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue