Skip to content

Commit cd624b6

Browse files
authored
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 a1d64bd. * Revert "add new option to helm chart" This reverts commit 299d087. * Revert "add entry to unreleased chart items" This reverts commit 0bcd0e3. * fix test cases that have changed
1 parent 30e912a commit cd624b6

File tree

8 files changed

+211
-46
lines changed

8 files changed

+211
-46
lines changed

docs/flags.md

+1
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@
152152
| `--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) |
153153
| `--[no-]txt-encrypt-enabled` | When using the TXT registry, set if TXT records should be encrypted before stored (default: disabled) |
154154
| `--txt-encrypt-aes-key=""` | When using the TXT registry, set TXT record decryption and encryption 32 byte aes key (required when --txt-encrypt=true) |
155+
| `--[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) |
155156
| `--dynamodb-region=""` | When using the DynamoDB registry, the AWS region of the DynamoDB table (optional) |
156157
| `--dynamodb-table="external-dns"` | When using the DynamoDB registry, the name of the DynamoDB table (default: "external-dns") |
157158
| `--txt-cache-interval=0s` | The interval between cache synchronizations in duration format (default: disabled) |

docs/registry/txt.md

+29
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,35 @@
33
The TXT registry is the default registry.
44
It stores DNS record metadata in TXT records, using the same provider.
55

6+
## Record Format Options
7+
The TXT registry supports two formats for storing DNS record metadata:
8+
- Legacy format: Creates a TXT record without record type information
9+
- New format: Creates a TXT record with record type information (e.g., 'a-' prefix for A records)
10+
11+
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.
12+
13+
Note: The following record types always use only the new format regardless of this setting:
14+
- AAAA records
15+
- Encrypted TXT records (when using `--txt-encrypt-enabled`)
16+
17+
Example:
18+
```sh
19+
# Default behavior - creates both formats
20+
external-dns --provider=aws --source=ingress --managed-record-types=A,TXT
21+
22+
# Only create new format records (alongside other required flags)
23+
external-dns --provider=aws --source=ingress --managed-record-types=A,TXT --txt-new-format-only
24+
```
25+
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.
26+
27+
### Migration to New Format Only
28+
When transitioning from dual-format to new-format-only records:
29+
- Ensure all your `external-dns` instances support the new format
30+
- Enable the `--txt-new-format-only` flag on your external-dns instances
31+
Manually clean up any existing legacy format TXT records from your DNS provider
32+
33+
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.
34+
635
## Prefixes and Suffixes
736

837
In order to avoid having the registry TXT records collide with

main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ func main() {
389389
case "noop":
390390
r, err = registry.NewNoopRegistry(p)
391391
case "txt":
392-
r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval, cfg.TXTWildcardReplacement, cfg.ManagedDNSRecordTypes, cfg.ExcludeDNSRecordTypes, cfg.TXTEncryptEnabled, []byte(cfg.TXTEncryptAESKey))
392+
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)
393393
case "aws-sd":
394394
r, err = registry.NewAWSSDRegistry(p, cfg.TXTOwnerID)
395395
default:

pkg/apis/externaldns/types.go

+3
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ type Config struct {
138138
TXTSuffix string
139139
TXTEncryptEnabled bool
140140
TXTEncryptAESKey string `secure:"yes"`
141+
TXTNewFormatOnly bool
141142
Interval time.Duration
142143
MinEventSyncInterval time.Duration
143144
Once bool
@@ -299,6 +300,7 @@ var defaultConfig = &Config{
299300
MinEventSyncInterval: 5 * time.Second,
300301
TXTEncryptEnabled: false,
301302
TXTEncryptAESKey: "",
303+
TXTNewFormatOnly: false,
302304
Interval: time.Minute,
303305
Once: false,
304306
DryRun: false,
@@ -591,6 +593,7 @@ func App(cfg *Config) *kingpin.Application {
591593
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)
592594
app.Flag("txt-encrypt-enabled", "When using the TXT registry, set if TXT records should be encrypted before stored (default: disabled)").BoolVar(&cfg.TXTEncryptEnabled)
593595
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)
596+
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)
594597
app.Flag("dynamodb-region", "When using the DynamoDB registry, the AWS region of the DynamoDB table (optional)").Default(cfg.AWSDynamoDBRegion).StringVar(&cfg.AWSDynamoDBRegion)
595598
app.Flag("dynamodb-table", "When using the DynamoDB registry, the name of the DynamoDB table (default: \"external-dns\")").Default(defaultConfig.AWSDynamoDBTable).StringVar(&cfg.AWSDynamoDBTable)
596599

pkg/apis/externaldns/types_test.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ var (
7575
AzureSubscriptionID: "",
7676
CloudflareProxied: false,
7777
CloudflareDNSRecordsPerPage: 100,
78-
CloudflareRegionKey: "",
78+
CloudflareRegionKey: "",
7979
CoreDNSPrefix: "/skydns/",
8080
AkamaiServiceConsumerDomain: "",
8181
AkamaiClientToken: "",
@@ -97,6 +97,7 @@ var (
9797
TXTOwnerID: "default",
9898
TXTPrefix: "",
9999
TXTCacheInterval: 0,
100+
TXTNewFormatOnly: false,
100101
Interval: time.Minute,
101102
MinEventSyncInterval: 5 * time.Second,
102103
Once: false,
@@ -176,7 +177,7 @@ var (
176177
AzureSubscriptionID: "arg",
177178
CloudflareProxied: true,
178179
CloudflareDNSRecordsPerPage: 5000,
179-
CloudflareRegionKey: "us",
180+
CloudflareRegionKey: "us",
180181
CoreDNSPrefix: "/coredns/",
181182
AkamaiServiceConsumerDomain: "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net",
182183
AkamaiClientToken: "o184671d5307a388180fbf7f11dbdf46",
@@ -202,6 +203,7 @@ var (
202203
TXTOwnerID: "owner-1",
203204
TXTPrefix: "associated-txt-record",
204205
TXTCacheInterval: 12 * time.Hour,
206+
TXTNewFormatOnly: true,
205207
Interval: 10 * time.Minute,
206208
MinEventSyncInterval: 50 * time.Second,
207209
Once: true,
@@ -338,6 +340,7 @@ func TestParseFlags(t *testing.T) {
338340
"--txt-owner-id=owner-1",
339341
"--txt-prefix=associated-txt-record",
340342
"--txt-cache-interval=12h",
343+
"--txt-new-format-only",
341344
"--dynamodb-table=custom-table",
342345
"--interval=10m",
343346
"--min-event-sync-interval=50s",
@@ -399,7 +402,7 @@ func TestParseFlags(t *testing.T) {
399402
"EXTERNAL_DNS_AZURE_SUBSCRIPTION_ID": "arg",
400403
"EXTERNAL_DNS_CLOUDFLARE_PROXIED": "1",
401404
"EXTERNAL_DNS_CLOUDFLARE_DNS_RECORDS_PER_PAGE": "5000",
402-
"EXTERNAL_DNS_CLOUDFLARE_REGION_KEY": "us",
405+
"EXTERNAL_DNS_CLOUDFLARE_REGION_KEY": "us",
403406
"EXTERNAL_DNS_COREDNS_PREFIX": "/coredns/",
404407
"EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN": "oooo-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net",
405408
"EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN": "o184671d5307a388180fbf7f11dbdf46",
@@ -451,6 +454,7 @@ func TestParseFlags(t *testing.T) {
451454
"EXTERNAL_DNS_TXT_OWNER_ID": "owner-1",
452455
"EXTERNAL_DNS_TXT_PREFIX": "associated-txt-record",
453456
"EXTERNAL_DNS_TXT_CACHE_INTERVAL": "12h",
457+
"EXTERNAL_DNS_TXT_NEW_FORMAT_ONLY": "1",
454458
"EXTERNAL_DNS_INTERVAL": "10m",
455459
"EXTERNAL_DNS_MIN_EVENT_SYNC_INTERVAL": "50s",
456460
"EXTERNAL_DNS_ONCE": "1",

registry/txt.go

+18-6
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,18 @@ type TXTRegistry struct {
5858
// encrypt text records
5959
txtEncryptEnabled bool
6060
txtEncryptAESKey []byte
61+
62+
newFormatOnly bool
6163
}
6264

63-
// NewTXTRegistry returns new TXTRegistry object
64-
func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration, txtWildcardReplacement string, managedRecordTypes, excludeRecordTypes []string, txtEncryptEnabled bool, txtEncryptAESKey []byte) (*TXTRegistry, error) {
65+
// NewTXTRegistry returns a new TXTRegistry object. When newFormatOnly is true, it will only
66+
// generate new format TXT records, otherwise it generates both old and new formats for
67+
// backwards compatibility.
68+
func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string,
69+
cacheInterval time.Duration, txtWildcardReplacement string,
70+
managedRecordTypes, excludeRecordTypes []string,
71+
txtEncryptEnabled bool, txtEncryptAESKey []byte,
72+
newFormatOnly bool) (*TXTRegistry, error) {
6573
if ownerID == "" {
6674
return nil, errors.New("owner id cannot be empty")
6775
}
@@ -95,6 +103,7 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st
95103
excludeRecordTypes: excludeRecordTypes,
96104
txtEncryptEnabled: txtEncryptEnabled,
97105
txtEncryptAESKey: txtEncryptAESKey,
106+
newFormatOnly: newFormatOnly,
98107
}, nil
99108
}
100109

@@ -216,12 +225,14 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error
216225
return endpoints, nil
217226
}
218227

219-
// generateTXTRecord generates both "old" and "new" TXT records.
220-
// Once we decide to drop old format we need to drop toTXTName() and rename toNewTXTName
228+
// generateTXTRecord generates TXT records in either both formats (old and new) or new format only,
229+
// depending on the newFormatOnly configuration. The old format is maintained for backwards
230+
// compatibility but can be disabled to reduce the number of DNS records.
221231
func (im *TXTRegistry) generateTXTRecord(r *endpoint.Endpoint) []*endpoint.Endpoint {
222232
endpoints := make([]*endpoint.Endpoint, 0)
223233

224-
if !im.txtEncryptEnabled && !im.mapper.recordTypeInAffix() && r.RecordType != endpoint.RecordTypeAAAA {
234+
// Create legacy format record by default unless newFormatOnly is true
235+
if !im.newFormatOnly && !im.txtEncryptEnabled && !im.mapper.recordTypeInAffix() && r.RecordType != endpoint.RecordTypeAAAA {
225236
// old TXT record format
226237
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true, im.txtEncryptEnabled, im.txtEncryptAESKey))
227238
if txt != nil {
@@ -231,7 +242,8 @@ func (im *TXTRegistry) generateTXTRecord(r *endpoint.Endpoint) []*endpoint.Endpo
231242
endpoints = append(endpoints, txt)
232243
}
233244
}
234-
// new TXT record format (containing record type)
245+
246+
// Always create new format record
235247
recordType := r.RecordType
236248
// AWS Alias records are encoded as type "cname"
237249
if isAlias, found := r.GetProviderSpecificProperty("alias"); found && isAlias == "true" && recordType == endpoint.RecordTypeA {

registry/txt_encryption_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func TestNewTXTRegistryEncryptionConfig(t *testing.T) {
6060
},
6161
}
6262
for _, test := range tests {
63-
actual, err := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, []string{}, test.encEnabled, test.aesKeyRaw)
63+
actual, err := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, []string{}, test.encEnabled, test.aesKeyRaw, false)
6464
if test.errorExpected {
6565
require.Error(t, err)
6666
} else {
@@ -106,7 +106,7 @@ func TestGenerateTXTGenerateTextRecordEncryptionWihDecryption(t *testing.T) {
106106
for _, k := range withEncryptionKeys {
107107
t.Run(fmt.Sprintf("key '%s' with decrypted result '%s'", k, test.decrypted), func(t *testing.T) {
108108
key := []byte(k)
109-
r, err := NewTXTRegistry(p, "", "", "owner", time.Minute, "", []string{}, []string{}, true, key)
109+
r, err := NewTXTRegistry(p, "", "", "owner", time.Minute, "", []string{}, []string{}, true, key, false)
110110
assert.NoError(t, err, "Error creating TXT registry")
111111
txtRecords := r.generateTXTRecord(test.record)
112112
assert.Len(t, txtRecords, len(test.record.Targets))
@@ -143,7 +143,7 @@ func TestApplyRecordsWithEncryption(t *testing.T) {
143143

144144
key := []byte("ZPitL0NGVQBZbTD6DwXJzD8RiStSazzYXQsdUowLURY=")
145145

146-
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, key)
146+
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, key, false)
147147

148148
_ = r.ApplyChanges(ctx, &plan.Changes{
149149
Create: []*endpoint.Endpoint{
@@ -201,7 +201,7 @@ func TestApplyRecordsWithEncryptionKeyChanged(t *testing.T) {
201201
}
202202

203203
for _, key := range withEncryptionKeys {
204-
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte(key))
204+
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte(key), false)
205205
_ = r.ApplyChanges(ctx, &plan.Changes{
206206
Create: []*endpoint.Endpoint{
207207
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) {
231231
}
232232

233233
for i, key := range withEncryptionKeys {
234-
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte(key))
234+
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte(key), false)
235235
keyId := fmt.Sprintf("key-id-%d", i)
236236
changes := []*endpoint.Endpoint{
237237
newEndpointWithOwnerAndOwnedRecordWithKeyIDLabel("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "", keyId),

0 commit comments

Comments
 (0)