Skip to content

Commit bb635fb

Browse files
authored
Merge pull request #2157 from k0da/registry_record_type
Registry record type
2 parents bf5c99d + 25c7cb2 commit bb635fb

File tree

5 files changed

+411
-42
lines changed

5 files changed

+411
-42
lines changed

docs/proposal/registry.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ The following presents two ways to implement the registry, and we are planning t
3232

3333
This implementation idea is borrowed from [Mate](https://github.com/linki/mate/)
3434

35-
Each record created by external-dns is accompanied by the TXT record, which internally stores the external-dns identifier. For example, if external dns with `owner-id="external-dns-1"` record to be created with dns name `foo.zone.org`, external-dns will create a TXT record with the same dns name `foo.zone.org` and injected value of `"external-dns-1"`. The transfer of ownership can be done by modifying the value of the TXT record. If no TXT record exists for the record or the value does not match its own `owner-id`, then external-dns will simply ignore it.
36-
35+
Each record created by external-dns is accompanied by the TXT record, which internally stores the external-dns identifier. For example, if external dns with `owner-id="external-dns-1"` record to be created with dns name `foo.zone.org`, external-dns will create a TXT record with the same dns name `<record_type>-foo.zone.org` and injected value of `"external-dns-1"`. The transfer of ownership can be done by modifying the value of the TXT record. If no TXT record exists for the record or the value does not match its own `owner-id`, then external-dns will simply ignore it.
3736

3837
#### Goods
3938
1. Easy to guarantee cross-cluster ownership safety

docs/registry.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
### TXT Registry migration to a new format ###
2+
3+
In order to support more record types and be able to track ownership without TXT record name clash, a new TXT record is introduced.
4+
It contains record type it manages, e.g.:
5+
* A record foo.example.com will be tracked with classic foo.example.com TXT record
6+
* At the same time a new TXT record will be created a-foo.example.com
7+
8+
Prefix and suffix are extended with %{record_type} template where the user can control how prefixed/suffixed records should look like.
9+
10+
In order to maintain compatibility, both records will be maintained for some time, in order to have downgrade possibility.
11+
12+
Later on, the old format will be dropped and only the new format will be kept (<record_type>-<endpoint_name>).
13+
14+
Cleanup will be done by controller itself.

pkg/apis/externaldns/types.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -512,8 +512,8 @@ func (cfg *Config) ParseFlags(args []string) error {
512512
// Flags related to the registry
513513
app.Flag("registry", "The registry implementation to use to keep track of DNS record ownership (default: txt, options: txt, noop, aws-sd)").Default(defaultConfig.Registry).EnumVar(&cfg.Registry, "txt", "noop", "aws-sd")
514514
app.Flag("txt-owner-id", "When using the TXT registry, a name that identifies this instance of ExternalDNS (default: default)").Default(defaultConfig.TXTOwnerID).StringVar(&cfg.TXTOwnerID)
515-
app.Flag("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional). Mutual exclusive with txt-suffix!").Default(defaultConfig.TXTPrefix).StringVar(&cfg.TXTPrefix)
516-
app.Flag("txt-suffix", "When using the TXT registry, a custom string that's suffixed to the host portion of each ownership DNS record (optional). Mutual exclusive with txt-prefix!").Default(defaultConfig.TXTSuffix).StringVar(&cfg.TXTSuffix)
515+
app.Flag("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional). Could contain record type template like '%{record_type}-prefix-'. Mutual exclusive with txt-suffix!").Default(defaultConfig.TXTPrefix).StringVar(&cfg.TXTPrefix)
516+
app.Flag("txt-suffix", "When using the TXT registry, a custom string that's suffixed to the host portion of each ownership DNS record (optional). Could contain record type template like '-%{record_type}-suffix'. Mutual exclusive with txt-prefix!").Default(defaultConfig.TXTSuffix).StringVar(&cfg.TXTSuffix)
517517
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)
518518

519519
// Flags related to the main control loop

registry/txt.go

+128-23
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import (
3030
"sigs.k8s.io/external-dns/provider"
3131
)
3232

33+
const recordTemplate = "%{record_type}"
34+
3335
// TXTRegistry implements registry interface with ownership implemented via associated TXT records
3436
type TXTRegistry struct {
3537
provider provider.Provider
@@ -68,6 +70,10 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st
6870
}, nil
6971
}
7072

73+
func getSupportedTypes() []string {
74+
return []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS}
75+
}
76+
7177
func (im *TXTRegistry) GetDomainFilter() endpoint.DomainFilterInterface {
7278
return im.provider.GetDomainFilter()
7379
}
@@ -140,6 +146,19 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error
140146
return endpoints, nil
141147
}
142148

149+
// generateTXTRecord generates both "old" and "new" TXT records.
150+
// Once we decide to drop old format we need to drop toTXTName() and rename toNewTXTName
151+
func (im *TXTRegistry) generateTXTRecord(r *endpoint.Endpoint) []*endpoint.Endpoint {
152+
// old TXT record format
153+
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)).WithSetIdentifier(r.SetIdentifier)
154+
txt.ProviderSpecific = r.ProviderSpecific
155+
// new TXT record format (containing record type)
156+
txtNew := endpoint.NewEndpoint(im.mapper.toNewTXTName(r.DNSName, r.RecordType), endpoint.RecordTypeTXT, r.Labels.Serialize(true)).WithSetIdentifier(r.SetIdentifier)
157+
txtNew.ProviderSpecific = r.ProviderSpecific
158+
159+
return []*endpoint.Endpoint{txt, txtNew}
160+
}
161+
143162
// ApplyChanges updates dns provider with the changes
144163
// for each created/deleted record it will also take into account TXT records for creation/deletion
145164
func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
@@ -154,22 +173,19 @@ func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes)
154173
r.Labels = make(map[string]string)
155174
}
156175
r.Labels[endpoint.OwnerLabelKey] = im.ownerID
157-
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)).WithSetIdentifier(r.SetIdentifier)
158-
txt.ProviderSpecific = r.ProviderSpecific
159-
filteredChanges.Create = append(filteredChanges.Create, txt)
176+
177+
filteredChanges.Create = append(filteredChanges.Create, im.generateTXTRecord(r)...)
160178

161179
if im.cacheInterval > 0 {
162180
im.addToCache(r)
163181
}
164182
}
165183

166184
for _, r := range filteredChanges.Delete {
167-
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)).WithSetIdentifier(r.SetIdentifier)
168-
txt.ProviderSpecific = r.ProviderSpecific
169-
170185
// when we delete TXT records for which value has changed (due to new label) this would still work because
171186
// !!! TXT record value is uniquely generated from the Labels of the endpoint. Hence old TXT record can be uniquely reconstructed
172-
filteredChanges.Delete = append(filteredChanges.Delete, txt)
187+
// !!! After migration to the new TXT registry format we can drop records in old format here!!!
188+
filteredChanges.Delete = append(filteredChanges.Delete, im.generateTXTRecord(r)...)
173189

174190
if im.cacheInterval > 0 {
175191
im.removeFromCache(r)
@@ -178,11 +194,9 @@ func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes)
178194

179195
// make sure TXT records are consistently updated as well
180196
for _, r := range filteredChanges.UpdateOld {
181-
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)).WithSetIdentifier(r.SetIdentifier)
182-
txt.ProviderSpecific = r.ProviderSpecific
183197
// when we updateOld TXT records for which value has changed (due to new label) this would still work because
184198
// !!! TXT record value is uniquely generated from the Labels of the endpoint. Hence old TXT record can be uniquely reconstructed
185-
filteredChanges.UpdateOld = append(filteredChanges.UpdateOld, txt)
199+
filteredChanges.UpdateOld = append(filteredChanges.UpdateOld, im.generateTXTRecord(r)...)
186200
// remove old version of record from cache
187201
if im.cacheInterval > 0 {
188202
im.removeFromCache(r)
@@ -191,9 +205,7 @@ func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes)
191205

192206
// make sure TXT records are consistently updated as well
193207
for _, r := range filteredChanges.UpdateNew {
194-
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)).WithSetIdentifier(r.SetIdentifier)
195-
txt.ProviderSpecific = r.ProviderSpecific
196-
filteredChanges.UpdateNew = append(filteredChanges.UpdateNew, txt)
208+
filteredChanges.UpdateNew = append(filteredChanges.UpdateNew, im.generateTXTRecord(r)...)
197209
// add new version of record to cache
198210
if im.cacheInterval > 0 {
199211
im.addToCache(r)
@@ -229,6 +241,7 @@ func (im *TXTRegistry) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoi
229241
type nameMapper interface {
230242
toEndpointName(string) string
231243
toTXTName(string) string
244+
toNewTXTName(string, string) string
232245
}
233246

234247
type affixNameMapper struct {
@@ -239,37 +252,129 @@ type affixNameMapper struct {
239252

240253
var _ nameMapper = affixNameMapper{}
241254

242-
func newaffixNameMapper(prefix string, suffix string, wildcardReplacement string) affixNameMapper {
255+
func newaffixNameMapper(prefix, suffix, wildcardReplacement string) affixNameMapper {
243256
return affixNameMapper{prefix: strings.ToLower(prefix), suffix: strings.ToLower(suffix), wildcardReplacement: strings.ToLower(wildcardReplacement)}
244257
}
245258

259+
func dropRecordType(name string) string {
260+
nameS := strings.Split(name, "-")
261+
for _, t := range getSupportedTypes() {
262+
if nameS[0] == strings.ToLower(t) {
263+
return strings.TrimPrefix(name, nameS[0]+"-")
264+
}
265+
}
266+
return name
267+
}
268+
269+
// dropAffix strips TXT record to find an endpoint name it manages
270+
// It takes into consideration a fact that it could contain record type
271+
// So it gets stripped first
272+
func (pr affixNameMapper) dropAffix(name string) string {
273+
if pr.recordTypeInAffix() {
274+
for _, t := range getSupportedTypes() {
275+
t = strings.ToLower(t)
276+
iPrefix := strings.ReplaceAll(pr.prefix, recordTemplate, t)
277+
iSuffix := strings.ReplaceAll(pr.suffix, recordTemplate, t)
278+
if pr.isPrefix() && strings.HasPrefix(name, iPrefix) {
279+
return strings.TrimPrefix(name, iPrefix)
280+
}
281+
282+
if pr.isSuffix() && strings.HasSuffix(name, iSuffix) {
283+
return strings.TrimSuffix(name, iSuffix)
284+
}
285+
}
286+
}
287+
if strings.HasPrefix(name, pr.prefix) && pr.isPrefix() {
288+
return strings.TrimPrefix(name, pr.prefix)
289+
}
290+
291+
if strings.HasSuffix(name, pr.suffix) && pr.isSuffix() {
292+
return strings.TrimSuffix(name, pr.suffix)
293+
}
294+
return ""
295+
}
296+
297+
func (pr affixNameMapper) dropAffixTemplate(name string) string {
298+
return strings.ReplaceAll(name, recordTemplate, "")
299+
}
300+
301+
func (pr affixNameMapper) isPrefix() bool {
302+
return len(pr.suffix) == 0
303+
}
304+
func (pr affixNameMapper) isSuffix() bool {
305+
return len(pr.prefix) == 0 && len(pr.suffix) > 0
306+
}
307+
246308
func (pr affixNameMapper) toEndpointName(txtDNSName string) string {
247-
lowerDNSName := strings.ToLower(txtDNSName)
248-
if strings.HasPrefix(lowerDNSName, pr.prefix) && len(pr.suffix) == 0 {
249-
return strings.TrimPrefix(lowerDNSName, pr.prefix)
309+
lowerDNSName := dropRecordType(strings.ToLower(txtDNSName))
310+
311+
// drop prefix
312+
if strings.HasPrefix(lowerDNSName, pr.prefix) && pr.isPrefix() {
313+
return pr.dropAffix(lowerDNSName)
250314
}
251315

252-
if len(pr.suffix) > 0 {
316+
// drop suffix
317+
if pr.isSuffix() {
253318
DNSName := strings.SplitN(lowerDNSName, ".", 2)
254-
if strings.HasSuffix(DNSName[0], pr.suffix) {
255-
return strings.TrimSuffix(DNSName[0], pr.suffix) + "." + DNSName[1]
256-
}
319+
return pr.dropAffix(DNSName[0]) + "." + DNSName[1]
257320
}
258321
return ""
259322
}
260323

261324
func (pr affixNameMapper) toTXTName(endpointDNSName string) string {
262325
DNSName := strings.SplitN(endpointDNSName, ".", 2)
263326

327+
prefix := pr.dropAffixTemplate(pr.prefix)
328+
suffix := pr.dropAffixTemplate(pr.suffix)
264329
// If specified, replace a leading asterisk in the generated txt record name with some other string
265330
if pr.wildcardReplacement != "" && DNSName[0] == "*" {
266331
DNSName[0] = pr.wildcardReplacement
267332
}
268333

269334
if len(DNSName) < 2 {
270-
return pr.prefix + DNSName[0] + pr.suffix
335+
return prefix + DNSName[0] + suffix
336+
}
337+
return prefix + DNSName[0] + suffix + "." + DNSName[1]
338+
}
339+
340+
func (pr affixNameMapper) recordTypeInAffix() bool {
341+
if strings.Contains(pr.prefix, recordTemplate) {
342+
return true
271343
}
272-
return pr.prefix + DNSName[0] + pr.suffix + "." + DNSName[1]
344+
if strings.Contains(pr.suffix, recordTemplate) {
345+
return true
346+
}
347+
return false
348+
}
349+
350+
func (pr affixNameMapper) normalizeAffixTemplate(afix, recordType string) string {
351+
if strings.Contains(afix, recordTemplate) {
352+
return strings.ReplaceAll(afix, recordTemplate, recordType)
353+
}
354+
return afix
355+
}
356+
func (pr affixNameMapper) toNewTXTName(endpointDNSName, recordType string) string {
357+
DNSName := strings.SplitN(endpointDNSName, ".", 2)
358+
recordType = strings.ToLower(recordType)
359+
recordT := recordType + "-"
360+
361+
prefix := pr.normalizeAffixTemplate(pr.prefix, recordType)
362+
suffix := pr.normalizeAffixTemplate(pr.suffix, recordType)
363+
364+
// If specified, replace a leading asterisk in the generated txt record name with some other string
365+
if pr.wildcardReplacement != "" && DNSName[0] == "*" {
366+
DNSName[0] = pr.wildcardReplacement
367+
}
368+
369+
if !pr.recordTypeInAffix() {
370+
DNSName[0] = recordT + DNSName[0]
371+
}
372+
373+
if len(DNSName) < 2 {
374+
return prefix + DNSName[0] + suffix
375+
}
376+
377+
return prefix + DNSName[0] + suffix + "." + DNSName[1]
273378
}
274379

275380
func (im *TXTRegistry) addToCache(ep *endpoint.Endpoint) {

0 commit comments

Comments
 (0)