feat(txt-registry): add option to use only new format (#4946)

* feat: add option to use only new format TXT records

* add flag and docs

* refine documentation on how to use the flag

* add section regarding manual migration

* update documentation to be same as in types.go

* fix compile issue

* add tests for new flag

* update flags documentation correctly

* add new option to helm chart

* run helm-docs

* remove unessery newline

* add entry to unreleased chart items

* Revert "run helm-docs"

This reverts commit a1d64bd3e8.

* Revert "add new option to helm chart"

This reverts commit 299d087917.

* Revert "add entry to unreleased chart items"

This reverts commit 0bcd0e3612.

* fix test cases that have changed
This commit is contained in:
Malthe Poulsen 2025-01-28 12:21:23 +01:00 committed by GitHub
parent 30e912a5f3
commit cd624b6f55
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 211 additions and 46 deletions

View File

@ -152,6 +152,7 @@
| `--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) | | `--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) | | `--[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) | | `--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-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") | | `--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) | | `--txt-cache-interval=0s` | The interval between cache synchronizations in duration format (default: disabled) |

View File

@ -3,6 +3,35 @@
The TXT registry is the default registry. The TXT registry is the default registry.
It stores DNS record metadata in TXT records, using the same provider. It stores DNS record metadata in TXT records, using the same provider.
## Record Format Options
The TXT registry supports two formats for storing DNS record metadata:
- Legacy format: Creates a TXT record without record type information
- New format: Creates a TXT record with record type information (e.g., 'a-' prefix for A records)
By default, the TXT registry creates records in both formats for backwards compatibility. You can configure it to use only the new format by using the `--txt-new-format-only` flag. This reduces the number of TXT records created, which can be helpful when working with provider-specific record limits.
Note: The following record types always use only the new format regardless of this setting:
- AAAA records
- Encrypted TXT records (when using `--txt-encrypt-enabled`)
Example:
```sh
# Default behavior - creates both formats
external-dns --provider=aws --source=ingress --managed-record-types=A,TXT
# Only create new format records (alongside other required flags)
external-dns --provider=aws --source=ingress --managed-record-types=A,TXT --txt-new-format-only
```
The `--txt-new-format-only` flag should be used in addition to your existing external-dns configuration flags. It does not implicitly configure TXT record handling - you still need to specify `--managed-record-types=TXT` if you want external-dns to manage TXT records.
### Migration to New Format Only
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 ## Prefixes and Suffixes
In order to avoid having the registry TXT records collide with In order to avoid having the registry TXT records collide with

View File

@ -389,7 +389,7 @@ func main() {
case "noop": case "noop":
r, err = registry.NewNoopRegistry(p) r, err = registry.NewNoopRegistry(p)
case "txt": 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)) 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)
case "aws-sd": case "aws-sd":
r, err = registry.NewAWSSDRegistry(p, cfg.TXTOwnerID) r, err = registry.NewAWSSDRegistry(p, cfg.TXTOwnerID)
default: default:

View File

@ -138,6 +138,7 @@ type Config struct {
TXTSuffix string TXTSuffix string
TXTEncryptEnabled bool TXTEncryptEnabled bool
TXTEncryptAESKey string `secure:"yes"` TXTEncryptAESKey string `secure:"yes"`
TXTNewFormatOnly bool
Interval time.Duration Interval time.Duration
MinEventSyncInterval time.Duration MinEventSyncInterval time.Duration
Once bool Once bool
@ -299,6 +300,7 @@ var defaultConfig = &Config{
MinEventSyncInterval: 5 * time.Second, MinEventSyncInterval: 5 * time.Second,
TXTEncryptEnabled: false, TXTEncryptEnabled: false,
TXTEncryptAESKey: "", TXTEncryptAESKey: "",
TXTNewFormatOnly: false,
Interval: time.Minute, Interval: time.Minute,
Once: false, Once: false,
DryRun: false, DryRun: false,
@ -591,6 +593,7 @@ 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-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-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-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-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) 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

@ -75,7 +75,7 @@ var (
AzureSubscriptionID: "", AzureSubscriptionID: "",
CloudflareProxied: false, CloudflareProxied: false,
CloudflareDNSRecordsPerPage: 100, CloudflareDNSRecordsPerPage: 100,
CloudflareRegionKey: "", CloudflareRegionKey: "",
CoreDNSPrefix: "/skydns/", CoreDNSPrefix: "/skydns/",
AkamaiServiceConsumerDomain: "", AkamaiServiceConsumerDomain: "",
AkamaiClientToken: "", AkamaiClientToken: "",
@ -97,6 +97,7 @@ var (
TXTOwnerID: "default", TXTOwnerID: "default",
TXTPrefix: "", TXTPrefix: "",
TXTCacheInterval: 0, TXTCacheInterval: 0,
TXTNewFormatOnly: false,
Interval: time.Minute, Interval: time.Minute,
MinEventSyncInterval: 5 * time.Second, MinEventSyncInterval: 5 * time.Second,
Once: false, Once: false,
@ -176,7 +177,7 @@ var (
AzureSubscriptionID: "arg", AzureSubscriptionID: "arg",
CloudflareProxied: true, CloudflareProxied: true,
CloudflareDNSRecordsPerPage: 5000, CloudflareDNSRecordsPerPage: 5000,
CloudflareRegionKey: "us", CloudflareRegionKey: "us",
CoreDNSPrefix: "/coredns/", CoreDNSPrefix: "/coredns/",
AkamaiServiceConsumerDomain: "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", AkamaiServiceConsumerDomain: "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net",
AkamaiClientToken: "o184671d5307a388180fbf7f11dbdf46", AkamaiClientToken: "o184671d5307a388180fbf7f11dbdf46",
@ -202,6 +203,7 @@ var (
TXTOwnerID: "owner-1", TXTOwnerID: "owner-1",
TXTPrefix: "associated-txt-record", TXTPrefix: "associated-txt-record",
TXTCacheInterval: 12 * time.Hour, TXTCacheInterval: 12 * time.Hour,
TXTNewFormatOnly: true,
Interval: 10 * time.Minute, Interval: 10 * time.Minute,
MinEventSyncInterval: 50 * time.Second, MinEventSyncInterval: 50 * time.Second,
Once: true, Once: true,
@ -338,6 +340,7 @@ func TestParseFlags(t *testing.T) {
"--txt-owner-id=owner-1", "--txt-owner-id=owner-1",
"--txt-prefix=associated-txt-record", "--txt-prefix=associated-txt-record",
"--txt-cache-interval=12h", "--txt-cache-interval=12h",
"--txt-new-format-only",
"--dynamodb-table=custom-table", "--dynamodb-table=custom-table",
"--interval=10m", "--interval=10m",
"--min-event-sync-interval=50s", "--min-event-sync-interval=50s",
@ -399,7 +402,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_AZURE_SUBSCRIPTION_ID": "arg", "EXTERNAL_DNS_AZURE_SUBSCRIPTION_ID": "arg",
"EXTERNAL_DNS_CLOUDFLARE_PROXIED": "1", "EXTERNAL_DNS_CLOUDFLARE_PROXIED": "1",
"EXTERNAL_DNS_CLOUDFLARE_DNS_RECORDS_PER_PAGE": "5000", "EXTERNAL_DNS_CLOUDFLARE_DNS_RECORDS_PER_PAGE": "5000",
"EXTERNAL_DNS_CLOUDFLARE_REGION_KEY": "us", "EXTERNAL_DNS_CLOUDFLARE_REGION_KEY": "us",
"EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/", "EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/",
"EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN": "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", "EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN": "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net",
"EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN": "o184671d5307a388180fbf7f11dbdf46", "EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN": "o184671d5307a388180fbf7f11dbdf46",
@ -451,6 +454,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_TXT_OWNER_ID": "owner-1", "EXTERNAL_DNS_TXT_OWNER_ID": "owner-1",
"EXTERNAL_DNS_TXT_PREFIX": "associated-txt-record", "EXTERNAL_DNS_TXT_PREFIX": "associated-txt-record",
"EXTERNAL_DNS_TXT_CACHE_INTERVAL": "12h", "EXTERNAL_DNS_TXT_CACHE_INTERVAL": "12h",
"EXTERNAL_DNS_TXT_NEW_FORMAT_ONLY": "1",
"EXTERNAL_DNS_INTERVAL": "10m", "EXTERNAL_DNS_INTERVAL": "10m",
"EXTERNAL_DNS_MIN_EVENT_SYNC_INTERVAL": "50s", "EXTERNAL_DNS_MIN_EVENT_SYNC_INTERVAL": "50s",
"EXTERNAL_DNS_ONCE": "1", "EXTERNAL_DNS_ONCE": "1",

View File

@ -58,10 +58,18 @@ type TXTRegistry struct {
// encrypt text records // encrypt text records
txtEncryptEnabled bool txtEncryptEnabled bool
txtEncryptAESKey []byte txtEncryptAESKey []byte
newFormatOnly bool
} }
// NewTXTRegistry returns new TXTRegistry object // NewTXTRegistry returns a new TXTRegistry object. When newFormatOnly is true, it will only
func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration, txtWildcardReplacement string, managedRecordTypes, excludeRecordTypes []string, txtEncryptEnabled bool, txtEncryptAESKey []byte) (*TXTRegistry, error) { // generate new format TXT records, otherwise it generates both old and new formats for
// backwards compatibility.
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) {
if ownerID == "" { if ownerID == "" {
return nil, errors.New("owner id cannot be empty") return nil, errors.New("owner id cannot be empty")
} }
@ -95,6 +103,7 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st
excludeRecordTypes: excludeRecordTypes, excludeRecordTypes: excludeRecordTypes,
txtEncryptEnabled: txtEncryptEnabled, txtEncryptEnabled: txtEncryptEnabled,
txtEncryptAESKey: txtEncryptAESKey, txtEncryptAESKey: txtEncryptAESKey,
newFormatOnly: newFormatOnly,
}, nil }, nil
} }
@ -216,12 +225,14 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error
return endpoints, nil return endpoints, nil
} }
// generateTXTRecord generates both "old" and "new" TXT records. // generateTXTRecord generates TXT records in either both formats (old and new) or new format only,
// Once we decide to drop old format we need to drop toTXTName() and rename toNewTXTName // depending on the newFormatOnly configuration. The old format is maintained for backwards
// compatibility but can be disabled to reduce the number of DNS records.
func (im *TXTRegistry) generateTXTRecord(r *endpoint.Endpoint) []*endpoint.Endpoint { func (im *TXTRegistry) generateTXTRecord(r *endpoint.Endpoint) []*endpoint.Endpoint {
endpoints := make([]*endpoint.Endpoint, 0) endpoints := make([]*endpoint.Endpoint, 0)
if !im.txtEncryptEnabled && !im.mapper.recordTypeInAffix() && r.RecordType != endpoint.RecordTypeAAAA { // 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 // old TXT record format
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true, im.txtEncryptEnabled, im.txtEncryptAESKey)) txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true, im.txtEncryptEnabled, im.txtEncryptAESKey))
if txt != nil { if txt != nil {
@ -231,7 +242,8 @@ func (im *TXTRegistry) generateTXTRecord(r *endpoint.Endpoint) []*endpoint.Endpo
endpoints = append(endpoints, txt) endpoints = append(endpoints, txt)
} }
} }
// new TXT record format (containing record type)
// Always create new format record
recordType := r.RecordType recordType := r.RecordType
// AWS Alias records are encoded as type "cname" // AWS Alias records are encoded as type "cname"
if isAlias, found := r.GetProviderSpecificProperty("alias"); found && isAlias == "true" && recordType == endpoint.RecordTypeA { if isAlias, found := r.GetProviderSpecificProperty("alias"); found && isAlias == "true" && recordType == endpoint.RecordTypeA {

View File

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

View File

@ -46,20 +46,20 @@ func TestTXTRegistry(t *testing.T) {
func testTXTRegistryNew(t *testing.T) { func testTXTRegistryNew(t *testing.T) {
p := inmemory.NewInMemoryProvider() p := inmemory.NewInMemoryProvider()
_, err := NewTXTRegistry(p, "txt", "", "", time.Hour, "", []string{}, []string{}, false, nil) _, err := NewTXTRegistry(p, "txt", "", "", time.Hour, "", []string{}, []string{}, false, nil, false)
require.Error(t, err) require.Error(t, err)
_, err = NewTXTRegistry(p, "", "txt", "", time.Hour, "", []string{}, []string{}, false, nil) _, err = NewTXTRegistry(p, "", "txt", "", time.Hour, "", []string{}, []string{}, false, nil, false)
require.Error(t, err) require.Error(t, err)
r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, p, r.provider) assert.Equal(t, p, r.provider)
r, err = NewTXTRegistry(p, "", "txt", "owner", time.Hour, "", []string{}, []string{}, false, nil) r, err = NewTXTRegistry(p, "", "txt", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
require.NoError(t, err) require.NoError(t, err)
_, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour, "", []string{}, []string{}, false, nil) _, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
require.Error(t, err) require.Error(t, err)
_, ok := r.mapper.(affixNameMapper) _, ok := r.mapper.(affixNameMapper)
@ -68,16 +68,16 @@ func testTXTRegistryNew(t *testing.T) {
assert.Equal(t, p, r.provider) assert.Equal(t, p, r.provider)
aesKey := []byte(";k&l)nUC/33:{?d{3)54+,AD?]SX%yh^") aesKey := []byte(";k&l)nUC/33:{?d{3)54+,AD?]SX%yh^")
_, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) _, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
require.NoError(t, err) require.NoError(t, err)
_, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, aesKey) _, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, aesKey, false)
require.NoError(t, err) require.NoError(t, err)
_, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, nil) _, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, nil, false)
require.Error(t, err) require.Error(t, err)
r, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, aesKey) r, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, aesKey, false)
require.NoError(t, err) require.NoError(t, err)
_, ok = r.mapper.(affixNameMapper) _, ok = r.mapper.(affixNameMapper)
@ -215,13 +215,13 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
}, },
} }
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil) r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, false)
records, _ := r.Records(ctx) records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords)) assert.True(t, testutils.SameEndpoints(records, expectedRecords))
// Ensure prefix is case-insensitive // Ensure prefix is case-insensitive
r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil) r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, false)
records, _ = r.Records(ctx) records, _ = r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords)) assert.True(t, testutils.SameEndpoints(records, expectedRecords))
@ -340,13 +340,13 @@ func testTXTRegistryRecordsSuffixed(t *testing.T) {
}, },
} }
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "", []string{}, []string{}, false, nil) r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
records, _ := r.Records(ctx) records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords)) assert.True(t, testutils.SameEndpoints(records, expectedRecords))
// Ensure prefix is case-insensitive // Ensure prefix is case-insensitive
r, _ = NewTXTRegistry(p, "", "-TxT", "owner", time.Hour, "", []string{}, []string{}, false, nil) r, _ = NewTXTRegistry(p, "", "-TxT", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
records, _ = r.Records(ctx) records, _ = r.Records(ctx)
assert.True(t, testutils.SameEndpointLabels(records, expectedRecords)) assert.True(t, testutils.SameEndpointLabels(records, expectedRecords))
@ -457,7 +457,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
}, },
} }
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
records, _ := r.Records(ctx) records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords)) assert.True(t, testutils.SameEndpoints(records, expectedRecords))
@ -484,12 +484,12 @@ func testTXTRegistryRecordsPrefixedTemplated(t *testing.T) {
}, },
} }
r, _ := NewTXTRegistry(p, "txt-%{record_type}.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil) r, _ := NewTXTRegistry(p, "txt-%{record_type}.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, false)
records, _ := r.Records(ctx) records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords)) assert.True(t, testutils.SameEndpoints(records, expectedRecords))
r, _ = NewTXTRegistry(p, "TxT-%{record_type}.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil) r, _ = NewTXTRegistry(p, "TxT-%{record_type}.", "", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, false)
records, _ = r.Records(ctx) records, _ = r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords)) assert.True(t, testutils.SameEndpoints(records, expectedRecords))
@ -516,12 +516,12 @@ func testTXTRegistryRecordsSuffixedTemplated(t *testing.T) {
}, },
} }
r, _ := NewTXTRegistry(p, "", "txt%{record_type}", "owner", time.Hour, "wc", []string{}, []string{}, false, nil) r, _ := NewTXTRegistry(p, "", "txt%{record_type}", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, false)
records, _ := r.Records(ctx) records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords)) assert.True(t, testutils.SameEndpoints(records, expectedRecords))
r, _ = NewTXTRegistry(p, "", "TxT%{record_type}", "owner", time.Hour, "wc", []string{}, []string{}, false, nil) r, _ = NewTXTRegistry(p, "", "TxT%{record_type}", "owner", time.Hour, "wc", []string{}, []string{}, false, nil, false)
records, _ = r.Records(ctx) records, _ = r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords)) assert.True(t, testutils.SameEndpoints(records, expectedRecords))
@ -564,7 +564,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"), 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) r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
changes := &plan.Changes{ changes := &plan.Changes{
Create: []*endpoint.Endpoint{ Create: []*endpoint.Endpoint{
@ -653,7 +653,7 @@ func testTXTRegistryApplyChangesWithTemplatedPrefix(t *testing.T) {
p.ApplyChanges(ctx, &plan.Changes{ p.ApplyChanges(ctx, &plan.Changes{
Create: []*endpoint.Endpoint{}, Create: []*endpoint.Endpoint{},
}) })
r, _ := NewTXTRegistry(p, "prefix%{record_type}.", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) r, _ := NewTXTRegistry(p, "prefix%{record_type}.", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
changes := &plan.Changes{ changes := &plan.Changes{
Create: []*endpoint.Endpoint{ Create: []*endpoint.Endpoint{
newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress"), newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress"),
@ -696,7 +696,7 @@ func testTXTRegistryApplyChangesWithTemplatedSuffix(t *testing.T) {
p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) {
assert.Equal(t, ctxEndpoints, ctx.Value(provider.RecordsContextKey)) assert.Equal(t, ctxEndpoints, ctx.Value(provider.RecordsContextKey))
} }
r, _ := NewTXTRegistry(p, "", "-%{record_type}suffix", "owner", time.Hour, "", []string{}, []string{}, false, nil) r, _ := NewTXTRegistry(p, "", "-%{record_type}suffix", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
changes := &plan.Changes{ changes := &plan.Changes{
Create: []*endpoint.Endpoint{ Create: []*endpoint.Endpoint{
newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress"), newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "", "ingress/default/my-ingress"),
@ -761,7 +761,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"), 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) r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "wildcard", []string{}, []string{}, false, nil, false)
changes := &plan.Changes{ changes := &plan.Changes{
Create: []*endpoint.Endpoint{ Create: []*endpoint.Endpoint{
@ -865,7 +865,7 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) {
newEndpointWithOwner("cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), 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) r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
changes := &plan.Changes{ changes := &plan.Changes{
Create: []*endpoint.Endpoint{ Create: []*endpoint.Endpoint{
@ -1028,7 +1028,7 @@ func testTXTRegistryMissingRecordsNoPrefix(t *testing.T) {
}, },
} }
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS}, []string{}, false, nil) r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS}, []string{}, false, nil, false)
records, _ := r.Records(ctx) records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords)) assert.True(t, testutils.SameEndpoints(records, expectedRecords))
@ -1138,7 +1138,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) r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS, endpoint.RecordTypeTXT}, []string{}, false, nil, false)
records, _ := r.Records(ctx) records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords)) assert.True(t, testutils.SameEndpoints(records, expectedRecords))
@ -1433,7 +1433,7 @@ func TestNewTXTScheme(t *testing.T) {
newEndpointWithOwner("cname-foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), 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) r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
changes := &plan.Changes{ changes := &plan.Changes{
Create: []*endpoint.Endpoint{ Create: []*endpoint.Endpoint{
@ -1509,7 +1509,7 @@ func TestGenerateTXT(t *testing.T) {
} }
p := inmemory.NewInMemoryProvider() p := inmemory.NewInMemoryProvider()
p.CreateZone(testZone) p.CreateZone(testZone)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
gotTXT := r.generateTXTRecord(record) gotTXT := r.generateTXTRecord(record)
assert.Equal(t, expectedTXT, gotTXT) assert.Equal(t, expectedTXT, gotTXT)
} }
@ -1528,7 +1528,7 @@ func TestGenerateTXTForAAAA(t *testing.T) {
} }
p := inmemory.NewInMemoryProvider() p := inmemory.NewInMemoryProvider()
p.CreateZone(testZone) p.CreateZone(testZone)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
gotTXT := r.generateTXTRecord(record) gotTXT := r.generateTXTRecord(record)
assert.Equal(t, expectedTXT, gotTXT) assert.Equal(t, expectedTXT, gotTXT)
} }
@ -1545,7 +1545,7 @@ func TestFailGenerateTXT(t *testing.T) {
expectedTXT := []*endpoint.Endpoint{} expectedTXT := []*endpoint.Endpoint{}
p := inmemory.NewInMemoryProvider() p := inmemory.NewInMemoryProvider()
p.CreateZone(testZone) p.CreateZone(testZone)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil) r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, false)
gotTXT := r.generateTXTRecord(cnameRecord) gotTXT := r.generateTXTRecord(cnameRecord)
assert.Equal(t, expectedTXT, gotTXT) assert.Equal(t, expectedTXT, gotTXT)
} }
@ -1563,7 +1563,7 @@ func TestTXTRegistryApplyChangesEncrypt(t *testing.T) {
}, },
}) })
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte("12345678901234567890123456789012")) r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte("12345678901234567890123456789012"), false)
records, _ := r.Records(ctx) records, _ := r.Records(ctx)
changes := &plan.Changes{ changes := &plan.Changes{
Delete: records, Delete: records,
@ -1609,7 +1609,7 @@ func TestMultiClusterDifferentRecordTypeOwnership(t *testing.T) {
}, },
}) })
r, _ := NewTXTRegistry(p, "_owner.", "", "bar", time.Hour, "", []string{}, []string{}, false, nil) r, _ := NewTXTRegistry(p, "_owner.", "", "bar", time.Hour, "", []string{}, []string{}, false, nil, false)
records, _ := r.Records(ctx) records, _ := r.Records(ctx)
// new cluster has same ingress host as other cluster and uses CNAME ingress address // new cluster has same ingress host as other cluster and uses CNAME ingress address
@ -1682,3 +1682,119 @@ func newEndpointWithOwnerResource(dnsName, target, recordType, ownerID, resource
e.Labels[endpoint.ResourceLabelKey] = resource e.Labels[endpoint.ResourceLabelKey] = resource
return e 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
description string
}{
{
name: "legacy format enabled - standard record",
newFormatOnly: false,
endpoint: newEndpointWithOwner("foo.test-zone.example.org", "1.2.3.4", endpoint.RecordTypeA, "owner"),
expectedRecords: 2,
expectedPrefix: "a-",
description: "Should generate both old and 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-",
description: "Should only generate new format TXT record",
},
{
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-",
description: "Should only generate new format for AAAA records regardless of setting",
},
{
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-",
description: "Should only generate new format for AAAA records",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, tc.newFormatOnly)
records := r.generateTXTRecord(tc.endpoint)
assert.Equal(t, tc.expectedRecords, len(records), tc.description)
for _, record := range records {
assert.Equal(t, endpoint.RecordTypeTXT, record.RecordType)
}
if tc.newFormatOnly || tc.endpoint.RecordType == endpoint.RecordTypeAAAA {
hasNewFormat := false
for _, record := range records {
if strings.HasPrefix(record.DNSName, tc.expectedPrefix) {
hasNewFormat = true
break
}
}
assert.True(t, hasNewFormat,
"Should have at least one record with prefix %s when using new format", tc.expectedPrefix)
}
})
}
}
func TestApplyChangesWithNewFormatOnly(t *testing.T) {
p := inmemory.NewInMemoryProvider()
p.CreateZone(testZone)
ctx := context.Background()
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, false, nil, true)
changes := &plan.Changes{
Create: []*endpoint.Endpoint{
newEndpointWithOwner("new-record.test-zone.example.org", "1.2.3.4", endpoint.RecordTypeA, "owner"),
},
}
err := r.ApplyChanges(ctx, changes)
require.NoError(t, err)
records, err := p.Records(ctx)
require.NoError(t, err)
var txtRecords []*endpoint.Endpoint
for _, record := range records {
if record.RecordType == endpoint.RecordTypeTXT {
txtRecords = append(txtRecords, record)
}
}
assert.Equal(t, 1, len(txtRecords), "Should only create one TXT record in new format")
if len(txtRecords) > 0 {
assert.True(t, strings.HasPrefix(txtRecords[0].DNSName, "a-"),
"TXT record should have 'a-' prefix when using new format only")
}
}