diff --git a/.editorconfig b/.editorconfig index 4b89edcf9b..b5f1f0448f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 953261ab60..d27eddee6d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,5 +23,6 @@ repos: rev: v0.44.0 hooks: - id: markdownlint + args: ["--fix"] minimum_pre_commit_version: !!str 3.2 diff --git a/controller/execute.go b/controller/execute.go index 851bccf286..6a3ae5b4d3 100644 --- a/controller/execute.go +++ b/controller/execute.go @@ -392,7 +392,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: diff --git a/controller/execute_test.go b/controller/execute_test.go index c7cb3ce9b8..924c4c809c 100644 --- a/controller/execute_test.go +++ b/controller/execute_test.go @@ -83,7 +83,6 @@ func TestSelectRegistry(t *testing.T) { TXTWildcardReplacement: "wildcard", ManagedDNSRecordTypes: []string{"A", "CNAME"}, ExcludeDNSRecordTypes: []string{"TXT"}, - TXTNewFormatOnly: true, }, provider: &MockProvider{}, wantErr: false, diff --git a/docs/flags.md b/docs/flags.md index 3825292fd7..441e46f4fa 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -164,7 +164,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) | diff --git a/docs/registry/txt.md b/docs/registry/txt.md index f7d0718452..e3ae66d655 100644 --- a/docs/registry/txt.md +++ b/docs/registry/txt.md @@ -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 diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 6ea0b4a2ae..8f6e0d01dc 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -145,7 +145,6 @@ type Config struct { TXTSuffix string TXTEncryptEnabled bool TXTEncryptAESKey string `secure:"yes"` - TXTNewFormatOnly bool Interval time.Duration MinEventSyncInterval time.Duration Once bool @@ -365,7 +364,6 @@ var defaultConfig = &Config{ TXTCacheInterval: 0, TXTEncryptAESKey: "", TXTEncryptEnabled: false, - TXTNewFormatOnly: false, TXTOwnerID: "default", TXTPrefix: "", TXTSuffix: "", @@ -622,7 +620,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) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 357672dc34..ed16c267f1 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -101,7 +101,6 @@ var ( TXTOwnerID: "default", TXTPrefix: "", TXTCacheInterval: 0, - TXTNewFormatOnly: false, Interval: time.Minute, MinEventSyncInterval: 5 * time.Second, Once: false, @@ -213,7 +212,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, @@ -357,7 +355,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", diff --git a/registry/txt.go b/registry/txt.go index 06a8314a78..5ea0ad2845 100644 --- a/registry/txt.go +++ b/registry/txt.go @@ -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 + "-" diff --git a/registry/txt_encryption_test.go b/registry/txt_encryption_test.go index 4b3d4023ee..1b7f9f5c9e 100644 --- a/registry/txt_encryption_test.go +++ b/registry/txt_encryption_test.go @@ -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), diff --git a/registry/txt_test.go b/registry/txt_test.go index bafacce542..a9e7d5196a 100644 --- a/registry/txt_test.go +++ b/registry/txt_test.go @@ -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) @@ -217,13 +218,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)) @@ -342,13 +343,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)) @@ -459,7 +460,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)) @@ -486,12 +487,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)) @@ -518,12 +519,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)) @@ -566,7 +567,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{ @@ -590,37 +591,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"), }, } @@ -655,7 +648,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"), @@ -698,7 +691,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"), @@ -763,7 +756,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{ @@ -788,40 +781,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"), }, } @@ -867,7 +850,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{ @@ -888,19 +871,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{}, @@ -1030,7 +1008,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)) @@ -1140,7 +1118,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)) @@ -1404,7 +1382,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) @@ -1435,7 +1413,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{ @@ -1455,15 +1433,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{}, @@ -1486,20 +1461,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\""}, @@ -1511,7 +1479,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) } @@ -1530,7 +1498,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) } @@ -1547,7 +1515,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) } @@ -1565,7 +1533,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, @@ -1611,7 +1579,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 @@ -1655,53 +1623,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 @@ -1709,15 +1634,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-", @@ -1725,7 +1648,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-", @@ -1733,7 +1655,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-", @@ -1743,7 +1664,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) @@ -1752,7 +1673,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) { @@ -1772,7 +1693,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{ @@ -1820,7 +1741,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) diff --git a/registry/txt_utils_test.go b/registry/txt_utils_test.go new file mode 100644 index 0000000000..5ff663c54b --- /dev/null +++ b/registry/txt_utils_test.go @@ -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 +} diff --git a/scripts/aws-cleanup-legacy-txt-records.py b/scripts/aws-cleanup-legacy-txt-records.py new file mode 100755 index 0000000000..768c8b0e9f --- /dev/null +++ b/scripts/aws-cleanup-legacy-txt-records.py @@ -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)