feat(txt-registry): deprecate legacy txt-format (#5172)

* feat(txt-registry): only support single format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): only support single format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): only support single format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): only support single format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): only support single format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com>

* feat(txt-registry): deprecate legacy txt-format

Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): address review comments

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

* feat(txt-registry): deprecate legacy txt-format

Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(txt-registry): deprecate legacy txt-format

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

---------

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com>
This commit is contained in:
Ivan Ka 2025-06-25 08:16:29 +01:00 committed by GitHub
parent 3c8f774192
commit 9f16d835f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 361 additions and 173 deletions

View File

@ -20,3 +20,7 @@ insert_final_newline = true
[{Makefile,go.mod,go.sum,*.go}]
indent_style = tab
indent_size = 4
[*.py]
indent_style = space
indent_size = 4

View File

@ -23,5 +23,6 @@ repos:
rev: v0.44.0
hooks:
- id: markdownlint
args: ["--fix"]
minimum_pre_commit_version: !!str 3.2

View File

@ -395,7 +395,7 @@ func selectRegistry(cfg *externaldns.Config, p provider.Provider) (registry.Regi
case "noop":
r, err = registry.NewNoopRegistry(p)
case "txt":
r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval, cfg.TXTWildcardReplacement, cfg.ManagedDNSRecordTypes, cfg.ExcludeDNSRecordTypes, cfg.TXTEncryptEnabled, []byte(cfg.TXTEncryptAESKey), cfg.TXTNewFormatOnly)
r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval, cfg.TXTWildcardReplacement, cfg.ManagedDNSRecordTypes, cfg.ExcludeDNSRecordTypes, cfg.TXTEncryptEnabled, []byte(cfg.TXTEncryptAESKey))
case "aws-sd":
r, err = registry.NewAWSSDRegistry(p, cfg.TXTOwnerID)
default:

View File

@ -83,7 +83,6 @@ func TestSelectRegistry(t *testing.T) {
TXTWildcardReplacement: "wildcard",
ManagedDNSRecordTypes: []string{"A", "CNAME"},
ExcludeDNSRecordTypes: []string{"TXT"},
TXTNewFormatOnly: true,
},
provider: &MockProvider{},
wantErr: false,

View File

@ -165,7 +165,6 @@
| `--txt-wildcard-replacement=""` | When using the TXT registry, a custom string that's used instead of an asterisk for TXT records corresponding to wildcard DNS records (optional) |
| `--[no-]txt-encrypt-enabled` | When using the TXT registry, set if TXT records should be encrypted before stored (default: disabled) |
| `--txt-encrypt-aes-key=""` | When using the TXT registry, set TXT record decryption and encryption 32 byte aes key (required when --txt-encrypt=true) |
| `--[no-]txt-new-format-only` | When using the TXT registry, only use new format records which include record type information (e.g., prefix: 'a-'). Reduces number of TXT records (default: disabled) |
| `--dynamodb-region=""` | When using the DynamoDB registry, the AWS region of the DynamoDB table (optional) |
| `--dynamodb-table="external-dns"` | When using the DynamoDB registry, the name of the DynamoDB table (default: "external-dns") |
| `--txt-cache-interval=0s` | The interval between cache synchronizations in duration format (default: disabled) |

View File

@ -3,8 +3,51 @@
The TXT registry is the default registry.
It stores DNS record metadata in TXT records, using the same provider.
If you plan to manage apex domains with external-dns whilst using a txt registry, you should ensure when using --txt-prefix that you specify the record type substitution and that it ends in a period (**.**). The record should be created under the same domain as the apex record being managed, i.e. --txt-prefix=someprefix-%{record_type}.
> Note: `--txt-prefix` and `--txt-suffix` contribute to the 63-byte maximum record length. To avoid errors, use them only if absolutely required and keep them as short as possible.
## Record Format Options
### For version `v0.18+`
The TXT registry supports single format for storing DNS record metadata:
- Creates a TXT record with record type information (e.g., 'a-' prefix for A records)
The TXT registry would try to guarantee a consistency in between providers and sources, if provider supports the behaviour.
If you are dealing with APEX domains, like `example.com` and TXT records are failing to be created for managed record types specified by `--managed-record-types`, consider following options:
1. TXT record with prefix based on requirements. Example `--txt-prefix="%{record_type}-abc-"` or `--txt-prefix="%{record_type}.abc-"`
2. TXT record with suffix based on requirements. Example `--txt-suffix="-abc-%{record_type}"` or `--txt-suffix="-abc.%{record_type}."`
If configured `--txt-prefix="%{record_type}-abc-"` for apex domain `ex.com` the expected result is
| Name | TYPE |
|:------------------------------:|:-------:|
| `cname-a-abc-nginx-v2.ex.com.` | `TXT` |
| `nginx-v2.ex.com.` | `CNAME` |
If configured `--txt-suffix="-abc.%{record_type}"` for apex domain `ex.com` the expected result is
| Name | TYPE |
|:------------------------------:|:-------:|
| `cname-nginx-v2-abc.a.ex.com.` | `TXT` |
| `nginx-v3.ex.com.` | `CNAME` |
### Manually Cleanup Legacy TXT Records
> While deleting registry TXT records won't cause downtime, a well-thought-out migration and cleanup plan is crucial.
Occasionally, it may be necessary to remove outdated TXT records from your registry.
An example script for AWS can be found in [scripts/aws-cleanup-legacy-txt-records.py](../../scripts/aws-cleanup-legacy-txt-records.py) with instructions on how to run it.
The script performs targeted deletion of TXT records that include `ResourceRecords` matching the `heritage=external-dns,external-dns/owner=default` or similar pattern.
In the event of unintended deletion of all TXT records managed by `external-dns`, `external-dns` will initiate a full DNS record regeneration, along with`TXT` and `non-TXT` records. Just be aware, this operation's duration is directly proportional to the DNS estate size."
### For version `v0.16.0 & v0.16.1`
The TXT registry supports two formats for storing DNS record metadata:
- Legacy format: Creates a TXT record without record type information
@ -31,14 +74,14 @@ The `--txt-new-format-only` flag should be used in addition to your existing ext
### Migration to New Format Only
> Note: `external-dns` will not automatically remove legacy format records when switching to new-format-only mode. You'll need to clean up the old records manually if desired.
When transitioning from dual-format to new-format-only records:
- Ensure all your `external-dns` instances support the new format
- Enable the `--txt-new-format-only` flag on your external-dns instances
Manually clean up any existing legacy format TXT records from your DNS provider
Note: `external-dns` will not automatically remove legacy format records when switching to new-format-only mode. You'll need to clean up the old records manually if desired.
## Prefixes and Suffixes
In order to avoid having the registry TXT records collide with

View File

@ -146,7 +146,6 @@ type Config struct {
TXTSuffix string
TXTEncryptEnabled bool
TXTEncryptAESKey string `secure:"yes"`
TXTNewFormatOnly bool
Interval time.Duration
MinEventSyncInterval time.Duration
Once bool
@ -367,7 +366,6 @@ var defaultConfig = &Config{
TXTCacheInterval: 0,
TXTEncryptAESKey: "",
TXTEncryptEnabled: false,
TXTNewFormatOnly: false,
TXTOwnerID: "default",
TXTPrefix: "",
TXTSuffix: "",
@ -625,7 +623,6 @@ func App(cfg *Config) *kingpin.Application {
app.Flag("txt-wildcard-replacement", "When using the TXT registry, a custom string that's used instead of an asterisk for TXT records corresponding to wildcard DNS records (optional)").Default(defaultConfig.TXTWildcardReplacement).StringVar(&cfg.TXTWildcardReplacement)
app.Flag("txt-encrypt-enabled", "When using the TXT registry, set if TXT records should be encrypted before stored (default: disabled)").BoolVar(&cfg.TXTEncryptEnabled)
app.Flag("txt-encrypt-aes-key", "When using the TXT registry, set TXT record decryption and encryption 32 byte aes key (required when --txt-encrypt=true)").Default(defaultConfig.TXTEncryptAESKey).StringVar(&cfg.TXTEncryptAESKey)
app.Flag("txt-new-format-only", "When using the TXT registry, only use new format records which include record type information (e.g., prefix: 'a-'). Reduces number of TXT records (default: disabled)").BoolVar(&cfg.TXTNewFormatOnly)
app.Flag("dynamodb-region", "When using the DynamoDB registry, the AWS region of the DynamoDB table (optional)").Default(cfg.AWSDynamoDBRegion).StringVar(&cfg.AWSDynamoDBRegion)
app.Flag("dynamodb-table", "When using the DynamoDB registry, the name of the DynamoDB table (default: \"external-dns\")").Default(defaultConfig.AWSDynamoDBTable).StringVar(&cfg.AWSDynamoDBTable)

View File

@ -101,7 +101,6 @@ var (
TXTOwnerID: "default",
TXTPrefix: "",
TXTCacheInterval: 0,
TXTNewFormatOnly: false,
Interval: time.Minute,
MinEventSyncInterval: 5 * time.Second,
Once: false,
@ -214,7 +213,6 @@ var (
TXTOwnerID: "owner-1",
TXTPrefix: "associated-txt-record",
TXTCacheInterval: 12 * time.Hour,
TXTNewFormatOnly: true,
Interval: 10 * time.Minute,
MinEventSyncInterval: 50 * time.Second,
Once: true,
@ -359,7 +357,6 @@ func TestParseFlags(t *testing.T) {
"--txt-owner-id=owner-1",
"--txt-prefix=associated-txt-record",
"--txt-cache-interval=12h",
"--txt-new-format-only",
"--dynamodb-table=custom-table",
"--interval=10m",
"--min-event-sync-interval=50s",

View File

@ -58,8 +58,6 @@ type TXTRegistry struct {
// encrypt text records
txtEncryptEnabled bool
txtEncryptAESKey []byte
newFormatOnly bool
}
// NewTXTRegistry returns a new TXTRegistry object. When newFormatOnly is true, it will only
@ -68,8 +66,7 @@ type TXTRegistry struct {
func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string,
cacheInterval time.Duration, txtWildcardReplacement string,
managedRecordTypes, excludeRecordTypes []string,
txtEncryptEnabled bool, txtEncryptAESKey []byte,
newFormatOnly bool) (*TXTRegistry, error) {
txtEncryptEnabled bool, txtEncryptAESKey []byte) (*TXTRegistry, error) {
if ownerID == "" {
return nil, errors.New("owner id cannot be empty")
}
@ -103,7 +100,6 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st
excludeRecordTypes: excludeRecordTypes,
txtEncryptEnabled: txtEncryptEnabled,
txtEncryptAESKey: txtEncryptAESKey,
newFormatOnly: newFormatOnly,
}, nil
}
@ -236,25 +232,13 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error
func (im *TXTRegistry) generateTXTRecord(r *endpoint.Endpoint) []*endpoint.Endpoint {
endpoints := make([]*endpoint.Endpoint, 0)
// Create legacy format record by default unless newFormatOnly is true
if !im.newFormatOnly && !im.txtEncryptEnabled && !im.mapper.recordTypeInAffix() && r.RecordType != endpoint.RecordTypeAAAA {
// old TXT record format
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true, im.txtEncryptEnabled, im.txtEncryptAESKey))
if txt != nil {
txt.WithSetIdentifier(r.SetIdentifier)
txt.Labels[endpoint.OwnedRecordLabelKey] = r.DNSName
txt.ProviderSpecific = r.ProviderSpecific
endpoints = append(endpoints, txt)
}
}
// Always create new format record
recordType := r.RecordType
// AWS Alias records are encoded as type "cname"
if isAlias, found := r.GetProviderSpecificProperty("alias"); found && isAlias == "true" && recordType == endpoint.RecordTypeA {
recordType = endpoint.RecordTypeCNAME
}
txtNew := endpoint.NewEndpoint(im.mapper.toNewTXTName(r.DNSName, recordType), endpoint.RecordTypeTXT, r.Labels.Serialize(true, im.txtEncryptEnabled, im.txtEncryptAESKey))
txtNew := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName, recordType), endpoint.RecordTypeTXT, r.Labels.Serialize(true, im.txtEncryptEnabled, im.txtEncryptAESKey))
if txtNew != nil {
txtNew.WithSetIdentifier(r.SetIdentifier)
txtNew.Labels[endpoint.OwnedRecordLabelKey] = r.DNSName
@ -336,8 +320,7 @@ func (im *TXTRegistry) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpo
type nameMapper interface {
toEndpointName(string) (endpointName string, recordType string)
toTXTName(string) string
toNewTXTName(string, string) string
toTXTName(string, string) string
recordTypeInAffix() bool
}
@ -437,22 +420,6 @@ func (pr affixNameMapper) toEndpointName(txtDNSName string) (endpointName string
return "", ""
}
func (pr affixNameMapper) toTXTName(endpointDNSName string) string {
DNSName := strings.SplitN(endpointDNSName, ".", 2)
prefix := pr.dropAffixTemplate(pr.prefix)
suffix := pr.dropAffixTemplate(pr.suffix)
// If specified, replace a leading asterisk in the generated txt record name with some other string
if pr.wildcardReplacement != "" && DNSName[0] == "*" {
DNSName[0] = pr.wildcardReplacement
}
if len(DNSName) < 2 {
return prefix + DNSName[0] + suffix
}
return prefix + DNSName[0] + suffix + "." + DNSName[1]
}
func (pr affixNameMapper) recordTypeInAffix() bool {
if strings.Contains(pr.prefix, recordTemplate) {
return true
@ -470,7 +437,7 @@ func (pr affixNameMapper) normalizeAffixTemplate(afix, recordType string) string
return afix
}
func (pr affixNameMapper) toNewTXTName(endpointDNSName, recordType string) string {
func (pr affixNameMapper) toTXTName(endpointDNSName, recordType string) string {
DNSName := strings.SplitN(endpointDNSName, ".", 2)
recordType = strings.ToLower(recordType)
recordT := recordType + "-"

View File

@ -61,7 +61,7 @@ func TestNewTXTRegistryEncryptionConfig(t *testing.T) {
},
}
for _, test := range tests {
actual, err := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, []string{}, test.encEnabled, test.aesKeyRaw, false)
actual, err := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, []string{}, test.encEnabled, test.aesKeyRaw)
if test.errorExpected {
require.Error(t, err)
} else {
@ -107,7 +107,7 @@ func TestGenerateTXTGenerateTextRecordEncryptionWihDecryption(t *testing.T) {
for _, k := range withEncryptionKeys {
t.Run(fmt.Sprintf("key '%s' with decrypted result '%s'", k, test.decrypted), func(t *testing.T) {
key := []byte(k)
r, err := NewTXTRegistry(p, "", "", "owner", time.Minute, "", []string{}, []string{}, true, key, false)
r, err := NewTXTRegistry(p, "", "", "owner", time.Minute, "", []string{}, []string{}, true, key)
assert.NoError(t, err, "Error creating TXT registry")
txtRecords := r.generateTXTRecord(test.record)
assert.Len(t, txtRecords, len(test.record.Targets))
@ -144,7 +144,7 @@ func TestApplyRecordsWithEncryption(t *testing.T) {
key := []byte("ZPitL0NGVQBZbTD6DwXJzD8RiStSazzYXQsdUowLURY=")
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, key, false)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, key)
_ = r.ApplyChanges(ctx, &plan.Changes{
Create: []*endpoint.Endpoint{
@ -202,7 +202,7 @@ func TestApplyRecordsWithEncryptionKeyChanged(t *testing.T) {
}
for _, key := range withEncryptionKeys {
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte(key), false)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte(key))
_ = r.ApplyChanges(ctx, &plan.Changes{
Create: []*endpoint.Endpoint{
newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner"),
@ -232,7 +232,7 @@ func TestApplyRecordsOnEncryptionKeyChangeWithKeyIdLabel(t *testing.T) {
}
for i, key := range withEncryptionKeys {
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte(key), false)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte(key))
keyId := fmt.Sprintf("key-id-%d", i)
changes := []*endpoint.Endpoint{
newEndpointWithOwnerAndOwnedRecordWithKeyIDLabel("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "", keyId),

View File

@ -18,6 +18,7 @@ package registry
import (
"context"
"fmt"
"reflect"
"strings"
"testing"
@ -48,20 +49,20 @@ func TestTXTRegistry(t *testing.T) {
func testTXTRegistryNew(t *testing.T) {
p := inmemory.NewInMemoryProvider()
_, err := NewTXTRegistry(p, "txt", "", "", time.Hour, "", []string{}, []string{}, false, nil, false)
_, err := NewTXTRegistry(p, "txt", "", "", time.Hour, "", []string{}, []string{}, false, nil)
require.Error(t, err)
_, err = NewTXTRegistry(p, "", "txt", "", time.Hour, "", []string{}, []string{}, false, nil, false)
_, err = NewTXTRegistry(p, "", "txt", "", time.Hour, "", []string{}, []string{}, false, nil)
require.Error(t, err)
r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour, "", []string{}, []string{}, false, nil)
require.NoError(t, err)
assert.Equal(t, p, r.provider)
r, err = NewTXTRegistry(p, "", "txt", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
r, err = NewTXTRegistry(p, "", "txt", "owner", time.Hour, "", []string{}, []string{}, false, nil)
require.NoError(t, err)
_, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
_, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour, "", []string{}, []string{}, false, nil)
require.Error(t, err)
_, ok := r.mapper.(affixNameMapper)
@ -70,16 +71,16 @@ func testTXTRegistryNew(t *testing.T) {
assert.Equal(t, p, r.provider)
aesKey := []byte(";k&l)nUC/33:{?d{3)54+,AD?]SX%yh^")
_, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
_, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil)
require.NoError(t, err)
_, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, aesKey, false)
_, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, aesKey)
require.NoError(t, err)
_, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, nil, false)
_, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, nil)
require.Error(t, err)
r, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, aesKey, false)
r, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, aesKey)
require.NoError(t, err)
_, ok = r.mapper.(affixNameMapper)
@ -227,13 +228,13 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
},
}
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, false)
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil)
records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
// Ensure prefix is case-insensitive
r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, false)
r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil)
records, _ = r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
@ -362,13 +363,13 @@ func testTXTRegistryRecordsSuffixed(t *testing.T) {
},
}
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "", []string{}, []string{}, false, nil)
records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
// Ensure prefix is case-insensitive
r, _ = NewTXTRegistry(p, "", "-TxT", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
r, _ = NewTXTRegistry(p, "", "-TxT", "owner", time.Hour, "", []string{}, []string{}, false, nil)
records, _ = r.Records(ctx)
assert.True(t, testutils.SameEndpointLabels(records, expectedRecords))
@ -489,7 +490,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
},
}
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil)
records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
@ -526,12 +527,12 @@ func testTXTRegistryRecordsPrefixedTemplated(t *testing.T) {
},
}
r, _ := NewTXTRegistry(p, "txt-%{record_type}.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, false)
r, _ := NewTXTRegistry(p, "txt-%{record_type}.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil)
records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
r, _ = NewTXTRegistry(p, "TxT-%{record_type}.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, false)
r, _ = NewTXTRegistry(p, "TxT-%{record_type}.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil)
records, _ = r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
@ -568,12 +569,12 @@ func testTXTRegistryRecordsSuffixedTemplated(t *testing.T) {
},
}
r, _ := NewTXTRegistry(p, "", "txt%{record_type}", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, false)
r, _ := NewTXTRegistry(p, "", "txt%{record_type}", "owner", time.Hour, "wc", []string{}, []string{}, false, nil)
records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
r, _ = NewTXTRegistry(p, "", "TxT%{record_type}", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, false)
r, _ = NewTXTRegistry(p, "", "TxT%{record_type}", "owner", time.Hour, "wc", []string{}, []string{}, false, nil)
records, _ = r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
@ -616,7 +617,7 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) {
newEndpointWithOwner("txt.cname-multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
},
})
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, []string{}, false, nil)
changes := &plan.Changes{
Create: []*endpoint.Endpoint{
@ -640,37 +641,29 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) {
expected := &plan.Changes{
Create: []*endpoint.Endpoint{
newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress"),
newEndpointWithOwnerAndOwnedRecord("txt.new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "new-record-1.test-zone.example.org"),
newEndpointWithOwnerAndOwnedRecord("txt.cname-new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "new-record-1.test-zone.example.org"),
newEndpointWithOwnerResource("multiple.test-zone.example.org", "lb3.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"),
newEndpointWithOwnerAndOwnedRecord("txt.multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-3"),
newEndpointWithOwnerAndOwnedRecord("txt.cname-multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-3"),
newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress"),
newEndpointWithOwnerAndOwnedRecord("txt.example", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "example"),
newEndpointWithOwnerAndOwnedRecord("txt.cname-example", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "example"),
},
Delete: []*endpoint.Endpoint{
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwnerAndOwnedRecord("txt.foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "foobar.test-zone.example.org"),
newEndpointWithOwnerAndOwnedRecord("txt.cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "foobar.test-zone.example.org"),
newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-1"),
newEndpointWithOwnerAndOwnedRecord("txt.multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-1"),
newEndpointWithOwnerAndOwnedRecord("txt.cname-multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-1"),
},
UpdateNew: []*endpoint.Endpoint{
newEndpointWithOwnerResource("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2"),
newEndpointWithOwnerAndOwnedRecord("txt.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, "", "tar.test-zone.example.org"),
newEndpointWithOwnerAndOwnedRecord("txt.cname-tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, "", "tar.test-zone.example.org"),
newEndpointWithOwnerResource("multiple.test-zone.example.org", "new.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2").WithSetIdentifier("test-set-2"),
newEndpointWithOwnerAndOwnedRecord("txt.multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-2"),
newEndpointWithOwnerAndOwnedRecord("txt.cname-multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-2"),
},
UpdateOld: []*endpoint.Endpoint{
newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwnerAndOwnedRecord("txt.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "tar.test-zone.example.org"),
newEndpointWithOwnerAndOwnedRecord("txt.cname-tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "tar.test-zone.example.org"),
newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-2"),
newEndpointWithOwnerAndOwnedRecord("txt.multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-2"),
newEndpointWithOwnerAndOwnedRecord("txt.cname-multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-2"),
},
}
@ -705,7 +698,7 @@ func testTXTRegistryApplyChangesWithTemplatedPrefix(t *testing.T) {
p.ApplyChanges(ctx, &plan.Changes{
Create: []*endpoint.Endpoint{},
})
r, _ := NewTXTRegistry(p, "prefix%{record_type}.", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
r, _ := NewTXTRegistry(p, "prefix%{record_type}.", "", "owner", time.Hour, "", []string{}, []string{}, false, nil)
changes := &plan.Changes{
Create: []*endpoint.Endpoint{
newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress"),
@ -748,7 +741,7 @@ func testTXTRegistryApplyChangesWithTemplatedSuffix(t *testing.T) {
p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) {
assert.Equal(t, ctxEndpoints, ctx.Value(provider.RecordsContextKey))
}
r, _ := NewTXTRegistry(p, "", "-%{record_type}suffix", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
r, _ := NewTXTRegistry(p, "", "-%{record_type}suffix", "owner", time.Hour, "", []string{}, []string{}, false, nil)
changes := &plan.Changes{
Create: []*endpoint.Endpoint{
newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress"),
@ -813,7 +806,7 @@ func testTXTRegistryApplyChangesWithSuffix(t *testing.T) {
newEndpointWithOwner("cname-multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
},
})
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "wildcard", []string{}, []string{}, false, nil, false)
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "wildcard", []string{}, []string{}, false, nil)
changes := &plan.Changes{
Create: []*endpoint.Endpoint{
@ -838,40 +831,30 @@ func testTXTRegistryApplyChangesWithSuffix(t *testing.T) {
expected := &plan.Changes{
Create: []*endpoint.Endpoint{
newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress"),
newEndpointWithOwnerAndOwnedRecord("new-record-1-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "new-record-1.test-zone.example.org"),
newEndpointWithOwnerAndOwnedRecord("cname-new-record-1-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "new-record-1.test-zone.example.org"),
newEndpointWithOwnerResource("multiple.test-zone.example.org", "lb3.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"),
newEndpointWithOwnerAndOwnedRecord("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-3"),
newEndpointWithOwnerAndOwnedRecord("cname-multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-3"),
newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress"),
newEndpointWithOwnerAndOwnedRecord("example-txt", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "example"),
newEndpointWithOwnerAndOwnedRecord("cname-example-txt", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "example"),
newEndpointWithOwnerResource("*.wildcard.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress"),
newEndpointWithOwnerAndOwnedRecord("wildcard-txt.wildcard.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "*.wildcard.test-zone.example.org"),
newEndpointWithOwnerAndOwnedRecord("cname-wildcard-txt.wildcard.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "", "*.wildcard.test-zone.example.org"),
},
Delete: []*endpoint.Endpoint{
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwnerAndOwnedRecord("foobar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "foobar.test-zone.example.org"),
newEndpointWithOwnerAndOwnedRecord("cname-foobar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "foobar.test-zone.example.org"),
newEndpointWithOwner("multiple.test-zone.example.org", "lb1.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-1"),
newEndpointWithOwnerAndOwnedRecord("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-1"),
newEndpointWithOwnerAndOwnedRecord("cname-multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-1"),
},
UpdateNew: []*endpoint.Endpoint{
newEndpointWithOwnerResource("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2"),
newEndpointWithOwnerAndOwnedRecord("tar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, "", "tar.test-zone.example.org"),
newEndpointWithOwnerAndOwnedRecord("cname-tar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, "", "tar.test-zone.example.org"),
newEndpointWithOwnerResource("multiple.test-zone.example.org", "new.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2").WithSetIdentifier("test-set-2"),
newEndpointWithOwnerAndOwnedRecord("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-2"),
newEndpointWithOwnerAndOwnedRecord("cname-multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-2"),
},
UpdateOld: []*endpoint.Endpoint{
newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwnerAndOwnedRecord("tar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "tar.test-zone.example.org"),
newEndpointWithOwnerAndOwnedRecord("cname-tar-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "tar.test-zone.example.org"),
newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "owner").WithSetIdentifier("test-set-2"),
newEndpointWithOwnerAndOwnedRecord("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-2"),
newEndpointWithOwnerAndOwnedRecord("cname-multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "multiple.test-zone.example.org").WithSetIdentifier("test-set-2"),
},
}
@ -917,7 +900,7 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) {
newEndpointWithOwner("cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
},
})
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil)
changes := &plan.Changes{
Create: []*endpoint.Endpoint{
@ -938,19 +921,14 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) {
expected := &plan.Changes{
Create: []*endpoint.Endpoint{
newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwnerAndOwnedRecord("new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "new-record-1.test-zone.example.org"),
newEndpointWithOwnerAndOwnedRecord("cname-new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "new-record-1.test-zone.example.org"),
newEndpointWithOwner("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwnerAndOwnedRecord("example", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "example"),
newEndpointWithOwnerAndOwnedRecord("cname-example", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "example"),
newEndpointWithOwner("new-alias.test-zone.example.org", "my-domain.com", endpoint.RecordTypeA, "owner").WithProviderSpecific("alias", "true"),
// TODO: It's not clear why the TXT registry copies ProviderSpecificProperties to ownership records; that doesn't seem correct.
newEndpointWithOwnerAndOwnedRecord("new-alias.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "new-alias.test-zone.example.org").WithProviderSpecific("alias", "true"),
newEndpointWithOwnerAndOwnedRecord("cname-new-alias.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "new-alias.test-zone.example.org").WithProviderSpecific("alias", "true"),
},
Delete: []*endpoint.Endpoint{
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwnerAndOwnedRecord("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "foobar.test-zone.example.org"),
newEndpointWithOwnerAndOwnedRecord("cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "foobar.test-zone.example.org"),
},
UpdateNew: []*endpoint.Endpoint{},
@ -1080,7 +1058,7 @@ func testTXTRegistryMissingRecordsNoPrefix(t *testing.T) {
},
}
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS}, []string{}, false, nil, false)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS}, []string{}, false, nil)
records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
@ -1190,7 +1168,7 @@ func testTXTRegistryMissingRecordsWithPrefix(t *testing.T) {
},
}
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS, endpoint.RecordTypeTXT}, []string{}, false, nil, false)
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS, endpoint.RecordTypeTXT}, []string{}, false, nil)
records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
@ -1454,7 +1432,7 @@ func TestToEndpointNameNewTXT(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
txtDomain := tc.mapper.toNewTXTName(tc.domain, tc.recordType)
txtDomain := tc.mapper.toTXTName(tc.domain, tc.recordType)
assert.Equal(t, tc.txtDomain, txtDomain)
domain, _ := tc.mapper.toEndpointName(txtDomain)
@ -1485,7 +1463,7 @@ func TestNewTXTScheme(t *testing.T) {
newEndpointWithOwner("cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
},
})
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil)
changes := &plan.Changes{
Create: []*endpoint.Endpoint{
@ -1505,15 +1483,12 @@ func TestNewTXTScheme(t *testing.T) {
expected := &plan.Changes{
Create: []*endpoint.Endpoint{
newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwnerAndOwnedRecord("new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "new-record-1.test-zone.example.org"),
newEndpointWithOwnerAndOwnedRecord("cname-new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "new-record-1.test-zone.example.org"),
newEndpointWithOwner("example", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwnerAndOwnedRecord("example", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "example"),
newEndpointWithOwnerAndOwnedRecord("cname-example", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "example"),
},
Delete: []*endpoint.Endpoint{
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwnerAndOwnedRecord("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "foobar.test-zone.example.org"),
newEndpointWithOwnerAndOwnedRecord("cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "foobar.test-zone.example.org"),
},
UpdateNew: []*endpoint.Endpoint{},
@ -1536,20 +1511,13 @@ func TestNewTXTScheme(t *testing.T) {
assert.Nil(t, ctx.Value(provider.RecordsContextKey))
}
err := r.ApplyChanges(ctx, changes)
fmt.Println(err)
require.NoError(t, err)
}
func TestGenerateTXT(t *testing.T) {
record := newEndpointWithOwner("foo.test-zone.example.org", "new-foo.loadbalancer.com", endpoint.RecordTypeCNAME, "owner")
expectedTXT := []*endpoint.Endpoint{
{
DNSName: "foo.test-zone.example.org",
Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""},
RecordType: endpoint.RecordTypeTXT,
Labels: map[string]string{
endpoint.OwnedRecordLabelKey: "foo.test-zone.example.org",
},
},
{
DNSName: "cname-foo.test-zone.example.org",
Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""},
@ -1561,7 +1529,7 @@ func TestGenerateTXT(t *testing.T) {
}
p := inmemory.NewInMemoryProvider()
p.CreateZone(testZone)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil)
gotTXT := r.generateTXTRecord(record)
assert.Equal(t, expectedTXT, gotTXT)
}
@ -1580,7 +1548,7 @@ func TestGenerateTXTForAAAA(t *testing.T) {
}
p := inmemory.NewInMemoryProvider()
p.CreateZone(testZone)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil)
gotTXT := r.generateTXTRecord(record)
assert.Equal(t, expectedTXT, gotTXT)
}
@ -1597,7 +1565,7 @@ func TestFailGenerateTXT(t *testing.T) {
expectedTXT := []*endpoint.Endpoint{}
p := inmemory.NewInMemoryProvider()
p.CreateZone(testZone)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil)
gotTXT := r.generateTXTRecord(cnameRecord)
assert.Equal(t, expectedTXT, gotTXT)
}
@ -1615,7 +1583,7 @@ func TestTXTRegistryApplyChangesEncrypt(t *testing.T) {
},
})
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte("12345678901234567890123456789012"), false)
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte("12345678901234567890123456789012"))
records, _ := r.Records(ctx)
changes := &plan.Changes{
Delete: records,
@ -1661,7 +1629,7 @@ func TestMultiClusterDifferentRecordTypeOwnership(t *testing.T) {
},
})
r, _ := NewTXTRegistry(p, "_owner.", "", "bar", time.Hour, "", []string{}, []string{}, false, nil, false)
r, _ := NewTXTRegistry(p, "_owner.", "", "bar", time.Hour, "", []string{}, []string{}, false, nil)
records, _ := r.Records(ctx)
// new cluster has same ingress host as other cluster and uses CNAME ingress address
@ -1705,53 +1673,10 @@ func TestMultiClusterDifferentRecordTypeOwnership(t *testing.T) {
}
}
/**
helper methods
*/
func newEndpointWithOwner(dnsName, target, recordType, ownerID string) *endpoint.Endpoint {
return newEndpointWithOwnerAndLabels(dnsName, target, recordType, ownerID, nil)
}
func newEndpointWithOwnerAndOwnedRecord(dnsName, target, recordType, ownerID, ownedRecord string) *endpoint.Endpoint {
return newEndpointWithOwnerAndLabels(dnsName, target, recordType, ownerID, endpoint.Labels{endpoint.OwnedRecordLabelKey: ownedRecord})
}
func newEndpointWithOwnerAndLabels(dnsName, target, recordType, ownerID string, labels endpoint.Labels) *endpoint.Endpoint {
e := endpoint.NewEndpoint(dnsName, recordType, target)
e.Labels[endpoint.OwnerLabelKey] = ownerID
for k, v := range labels {
e.Labels[k] = v
}
return e
}
func newEndpointWithOwnerResource(dnsName, target, recordType, ownerID, resource string) *endpoint.Endpoint {
e := endpoint.NewEndpoint(dnsName, recordType, target)
e.Labels[endpoint.OwnerLabelKey] = ownerID
e.Labels[endpoint.ResourceLabelKey] = resource
return e
}
func TestNewTXTRegistryWithNewFormatOnly(t *testing.T) {
p := inmemory.NewInMemoryProvider()
r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
require.NoError(t, err)
assert.False(t, r.newFormatOnly)
r, err = NewTXTRegistry(p, "txt", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, true)
require.NoError(t, err)
assert.True(t, r.newFormatOnly)
}
func TestGenerateTXTRecordWithNewFormatOnly(t *testing.T) {
p := inmemory.NewInMemoryProvider()
testCases := []struct {
name string
newFormatOnly bool
endpoint *endpoint.Endpoint
expectedRecords int
expectedPrefix string
@ -1759,15 +1684,13 @@ func TestGenerateTXTRecordWithNewFormatOnly(t *testing.T) {
}{
{
name: "legacy format enabled - standard record",
newFormatOnly: false,
endpoint: newEndpointWithOwner("foo.test-zone.example.org", "1.2.3.4", endpoint.RecordTypeA, "owner"),
expectedRecords: 2,
expectedRecords: 1,
expectedPrefix: "a-",
description: "Should generate both old and new format TXT records",
description: "Should generate only new format TXT records",
},
{
name: "new format only - standard record",
newFormatOnly: true,
endpoint: newEndpointWithOwner("foo.test-zone.example.org", "1.2.3.4", endpoint.RecordTypeA, "owner"),
expectedRecords: 1,
expectedPrefix: "a-",
@ -1775,7 +1698,6 @@ func TestGenerateTXTRecordWithNewFormatOnly(t *testing.T) {
},
{
name: "legacy format enabled - AAAA record",
newFormatOnly: false,
endpoint: newEndpointWithOwner("foo.test-zone.example.org", "2001:db8::1", endpoint.RecordTypeAAAA, "owner"),
expectedRecords: 1,
expectedPrefix: "aaaa-",
@ -1783,7 +1705,6 @@ func TestGenerateTXTRecordWithNewFormatOnly(t *testing.T) {
},
{
name: "new format only - AAAA record",
newFormatOnly: true,
endpoint: newEndpointWithOwner("foo.test-zone.example.org", "2001:db8::1", endpoint.RecordTypeAAAA, "owner"),
expectedRecords: 1,
expectedPrefix: "aaaa-",
@ -1793,7 +1714,7 @@ func TestGenerateTXTRecordWithNewFormatOnly(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, tc.newFormatOnly)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil)
records := r.generateTXTRecord(tc.endpoint)
assert.Len(t, records, tc.expectedRecords, tc.description)
@ -1802,7 +1723,7 @@ func TestGenerateTXTRecordWithNewFormatOnly(t *testing.T) {
assert.Equal(t, endpoint.RecordTypeTXT, record.RecordType)
}
if tc.newFormatOnly || tc.endpoint.RecordType == endpoint.RecordTypeAAAA {
if tc.endpoint.RecordType == endpoint.RecordTypeAAAA {
hasNewFormat := false
for _, record := range records {
if strings.HasPrefix(record.DNSName, tc.expectedPrefix) {
@ -1822,7 +1743,7 @@ func TestApplyChangesWithNewFormatOnly(t *testing.T) {
p.CreateZone(testZone)
ctx := context.Background()
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, true)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil)
changes := &plan.Changes{
Create: []*endpoint.Endpoint{
@ -1870,7 +1791,7 @@ func TestTXTRegistryRecordsWithEmptyTargets(t *testing.T) {
},
})
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil)
hook := testutils.LogsUnderTestWithLogLevel(log.ErrorLevel, t)
records, err := r.Records(ctx)
require.NoError(t, err)

View File

@ -0,0 +1,45 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry
import (
"sigs.k8s.io/external-dns/endpoint"
)
func newEndpointWithOwner(dnsName, target, recordType, ownerID string) *endpoint.Endpoint {
return newEndpointWithOwnerAndLabels(dnsName, target, recordType, ownerID, nil)
}
func newEndpointWithOwnerAndOwnedRecord(dnsName, target, recordType, ownerID, ownedRecord string) *endpoint.Endpoint {
return newEndpointWithOwnerAndLabels(dnsName, target, recordType, ownerID, endpoint.Labels{endpoint.OwnedRecordLabelKey: ownedRecord})
}
func newEndpointWithOwnerAndLabels(dnsName, target, recordType, ownerID string, labels endpoint.Labels) *endpoint.Endpoint {
e := endpoint.NewEndpoint(dnsName, recordType, target)
e.Labels[endpoint.OwnerLabelKey] = ownerID
for k, v := range labels {
e.Labels[k] = v
}
return e
}
func newEndpointWithOwnerResource(dnsName, target, recordType, ownerID, resource string) *endpoint.Endpoint {
e := endpoint.NewEndpoint(dnsName, recordType, target)
e.Labels[endpoint.OwnerLabelKey] = ownerID
e.Labels[endpoint.ResourceLabelKey] = resource
return e
}

View File

@ -0,0 +1,215 @@
#!/usr/bin/env python
# Copyright 2025 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Warning: The script deletes all records that match certain values. It could delete both legacy and new records if there is no way to differentiate them.
# This Python script is designed to help migrate DNS management to `external-dns` by cleaning up legacy TXT records in AWS Route 53.
# It identifies and deletes TXT records that match a specified pattern, ensuring that `external-dns` can take over managing these resources.
# The script performs the following steps:
#
# 1. **Setup and Configuration**:
# - Imports necessary libraries (`boto3`, `argparse`, etc.).
# - Defines constants and utility functions.
# - Parses command-line arguments for configuration.
#
# 2. **Record Class**:
# - Represents a DNS record with methods to check if it should be deleted.
#
# 3. **Main Functionality**:
# - Connects to AWS Route 53 using `boto3`.
# - Support single zone cleanup at a time.
# - Lists and filters TXT records based on the specified pattern.
# - Deletes the filtered records in batches, with an option for a dry run or actual deletion.
#
# 4. **Execution**:
# - The script is executed with command-line arguments specifying the hosted zone ID, record pattern, total items to delete, batch size, and whether to perform a dry run or actual deletion.
# - Check 'To Run script' section for more details
# WARNING: run this script at your own RISK. This will delete all the TXT records that do contain certain string.
# To Run script
# 1. Python, pip and pipenv installed https://pipenv.pypa.io/en/latest/
# 2. AWS Access https://docs.aws.amazon.com/signin/latest/userguide/command-line-sign-in.html
# 3. pipenv shell
# 4. pip install boto3
# 5. python scripts/aws-cleanup-legacy-txt-records.py --help
# 6. DRY RUN python scripts/aws-cleanup-legacy-txt-records.py --zone-id ASDFQEQREWRQADF --record-match text
# 6.1 Before execution consider to stop `external-dns`
# 7. Execute Deletion. First few times with reduced number of items
# - python scripts/aws-cleanup-legacy-txt-records.py --zone-id ASDFQEQREWRQADF --total-items 3 --batch-delete-count 1 --record-match 'external-dns'
# - python scripts/aws-cleanup-legacy-txt-records.py --zone-id ASDFQEQREWRQADF --total-items 10000 --batch-delete-count 50 --run --record-match "external-dns/owner=default"
# python scripts/aws-cleanup-legacy-txt-records.py --help
# python scripts/aws-cleanup-legacy-txt-records.py --zone-id Z06155043AVN8RVC88TYY --total-items 300 --batch-delete-count 20 --record-match "external-dns/owner=default" --run
import boto3
from botocore.config import Config as AwsConfig
import json, argparse, os, uuid, time
MAX_ITEMS=300 # max is 300 https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/route53/client/list_resource_record_sets.html
SLEEP=1 # in seconds, required to make sure Route53 API is not throttled
SESSION_ID=uuid.uuid4()
def json_prettify(data):
return json.dumps(data, indent=4, default=str)
class Record:
def __init__(self, record):
# static
self.type = 'TXT'
self.record = record
self.name = record['Name']
self.resource_records = record['ResourceRecords']
resource_record = ''
for r in self.resource_records:
resource_record += r['Value']
self.resource_record = resource_record
def is_for_deletion(self, contains):
if contains in self.resource_record:
return True
return False
def __str__(self):
return f'record: name: {self.name}, type: {self.type}, records: {self.resource_record}'
class Config:
def __init__(self, zone_id, contain, total_items, batch, run):
self.zone_id = zone_id
self.record_contain = contain
self.total_items = total_items
self.batch_size = batch
self.run = run
self.contain = contain
def records(config: Config) -> None:
print(f"calculate TXT records to cleanup for 'zone:{config.zone_id}' and 'max records:{config.total_items}'")
# https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html
cfg = AwsConfig(
user_agent=f"ExternalDNS/boto3-{SESSION_ID}",
)
r53client = boto3.client('route53', config=cfg)
dns_records_to_cleanup = []
items = 0
try:
params = {
'HostedZoneId': config.zone_id,
'MaxItems': str(MAX_ITEMS),
}
dns_in_iteration = r53client.list_resource_record_sets(**params)
elements = dns_in_iteration['ResourceRecordSets']
for el in elements:
if el['Type'] == 'TXT':
record = Record(el)
if record.is_for_deletion(config.contain):
dns_records_to_cleanup.append(record)
print("to cleanup >>", record)
items += 1
if items >= config.total_items:
break
while len(elements) > 0 and 'NextRecordName' in dns_in_iteration.keys() and items < config.total_items:
dns_in_iteration = r53client.list_resource_record_sets(
HostedZoneId= config.zone_id,
StartRecordName= dns_in_iteration['NextRecordName'],
MaxItems= str(MAX_ITEMS),
)
elements = dns_in_iteration['ResourceRecordSets']
for el in elements:
if el['Type'] == 'TXT':
record = Record(el)
if record.is_for_deletion(config.contain):
dns_records_to_cleanup.append(record)
print("to cleanup >>", record)
items += 1
if items >= config.total_items:
break
if len(dns_records_to_cleanup) > 0:
delete_records(r53client, config, dns_records_to_cleanup)
else:
print("No 'TXT' records found to cleanup....")
except Exception as e:
print(f"An error occurred: {e}")
os._exit(os.EX_OSERR)
def delete_records(client: boto3.client, config: Config, records: list[Record]) -> None:
total=len(records)
print(f"will cleanup '{total}' records with batch '{config.batch_size}' at a time")
count = 0
if config.run:
print("deletion of records!!")
else:
print("dry run execution")
for i in range(0, total, config.batch_size):
if config.batch_size <= 0:
break
batch = records[i:min(i + config.batch_size, total)]
count += config.batch_size
if count >= total:
count = total
changes = []
for el in batch:
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/route53/client/change_resource_record_sets.html
changes.append({
'Action': 'DELETE',
'ResourceRecordSet': el.record
})
print(f"BATCH deletion(start). {len(changes)} records > {changes}")
if config.run:
client.change_resource_record_sets(
HostedZoneId=config.zone_id,
ChangeBatch={
"Comment": "external-dns legacy record cleanup. batch of ",
"Changes": changes,
}
)
time.sleep(SLEEP)
print(f"BATCH deletion(success). {count}/{total}(deleted/total)")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Cleanup legacy TXT records")
parser.add_argument("--zone-id", type=str, required=True, help="Hosted Zone ID for which to run a cleanup.")
parser.add_argument("--record-match", type=str, required=True, help="Record to match specific value. Example 'external-dns/owner=default'")
parser.add_argument("--total-items", type=int, required=False, default=10, help="Number of items to delete. Default to 10")
parser.add_argument("--batch-delete-count", type=int, required=False, default=2, help="Number of items to delete in single DELETE batch. Default to 2")
parser.add_argument("--run", action="store_true", help="Execute the cleanup. The tool will do a dry-run if --run is not specified.")
answer = input("Run this script at your own RISKS!!! Please enter 'yes' or 'no': ")
if answer != 'yes':
os._exit(0)
print(f"Session ID '{SESSION_ID}'")
args = parser.parse_args()
print("arguments:",args)
cfg = Config(
zone_id=args.zone_id,
contain=args.record_match,
total_items=args.total_items,
batch=args.batch_delete_count,
run=args.run,
)
records(cfg)