mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-12-07 04:41:00 +01:00
* adds tests for shouldUpdateProviderSpecific Signed-off-by: Raffaele Di Fazio <difazio.raffaele@gmail.com> * move AWS health to where it belongs Signed-off-by: Raffaele Di Fazio <difazio.raffaele@gmail.com> * add test that breaks things Signed-off-by: Raffaele Di Fazio <difazio.raffaele@gmail.com> * adds adjustendpoints method Signed-off-by: Raffaele Di Fazio <raffo@github.com> * fix controller Signed-off-by: Raffaele Di Fazio <raffo@github.com> * actually pass the provider where needed Signed-off-by: Raffaele Di Fazio <raffo@github.com> * OMG goland do your go fmt thing Signed-off-by: Raffaele Di Fazio <raffo@github.com> * use registry as proxy Signed-off-by: Raffaele Di Fazio <raffo@github.com> * make linter happy Signed-off-by: Raffaele Di Fazio <raffo@github.com> * change AdjustEndpoints signature Signed-off-by: Raffaele Di Fazio <raffo@github.com> * fix typo Signed-off-by: Raffaele Di Fazio <raffo@github.com> * actually use adjusted endpoints Signed-off-by: Raffaele Di Fazio <raffo@github.com> * revert cloudflare change Signed-off-by: Raffaele Di Fazio <raffo@github.com> * Update provider/cloudflare/cloudflare.go Co-authored-by: Nick Jüttner <nick@juni.io> Co-authored-by: Nick Jüttner <nick@juni.io>
291 lines
9.9 KiB
Go
291 lines
9.9 KiB
Go
/*
|
|
Copyright 2017 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 (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"sigs.k8s.io/external-dns/endpoint"
|
|
"sigs.k8s.io/external-dns/plan"
|
|
"sigs.k8s.io/external-dns/provider"
|
|
)
|
|
|
|
// TXTRegistry implements registry interface with ownership implemented via associated TXT records
|
|
type TXTRegistry struct {
|
|
provider provider.Provider
|
|
ownerID string //refers to the owner id of the current instance
|
|
mapper nameMapper
|
|
|
|
// cache the records in memory and update on an interval instead.
|
|
recordsCache []*endpoint.Endpoint
|
|
recordsCacheRefreshTime time.Time
|
|
cacheInterval time.Duration
|
|
|
|
// optional string to use to replace the asterisk in wildcard entries - without using this,
|
|
// registry TXT records corresponding to wildcard records will be invalid (and rejected by most providers), due to
|
|
// having a '*' appear (not as the first character) - see https://tools.ietf.org/html/rfc1034#section-4.3.3
|
|
wildcardReplacement string
|
|
}
|
|
|
|
// NewTXTRegistry returns new TXTRegistry object
|
|
func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration, txtWildcardReplacement string) (*TXTRegistry, error) {
|
|
if ownerID == "" {
|
|
return nil, errors.New("owner id cannot be empty")
|
|
}
|
|
|
|
if len(txtPrefix) > 0 && len(txtSuffix) > 0 {
|
|
return nil, errors.New("txt-prefix and txt-suffix are mutual exclusive")
|
|
}
|
|
|
|
mapper := newaffixNameMapper(txtPrefix, txtSuffix, txtWildcardReplacement)
|
|
|
|
return &TXTRegistry{
|
|
provider: provider,
|
|
ownerID: ownerID,
|
|
mapper: mapper,
|
|
cacheInterval: cacheInterval,
|
|
wildcardReplacement: txtWildcardReplacement,
|
|
}, nil
|
|
}
|
|
|
|
// Records returns the current records from the registry excluding TXT Records
|
|
// If TXT records was created previously to indicate ownership its corresponding value
|
|
// will be added to the endpoints Labels map
|
|
func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
|
// If we have the zones cached AND we have refreshed the cache since the
|
|
// last given interval, then just use the cached results.
|
|
if im.recordsCache != nil && time.Since(im.recordsCacheRefreshTime) < im.cacheInterval {
|
|
log.Debug("Using cached records.")
|
|
return im.recordsCache, nil
|
|
}
|
|
|
|
records, err := im.provider.Records(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
endpoints := []*endpoint.Endpoint{}
|
|
|
|
labelMap := map[string]endpoint.Labels{}
|
|
|
|
for _, record := range records {
|
|
if record.RecordType != endpoint.RecordTypeTXT {
|
|
endpoints = append(endpoints, record)
|
|
continue
|
|
}
|
|
// We simply assume that TXT records for the registry will always have only one target.
|
|
labels, err := endpoint.NewLabelsFromString(record.Targets[0])
|
|
if err == endpoint.ErrInvalidHeritage {
|
|
//if no heritage is found or it is invalid
|
|
//case when value of txt record cannot be identified
|
|
//record will not be removed as it will have empty owner
|
|
endpoints = append(endpoints, record)
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
key := fmt.Sprintf("%s::%s", im.mapper.toEndpointName(record.DNSName), record.SetIdentifier)
|
|
labelMap[key] = labels
|
|
}
|
|
|
|
for _, ep := range endpoints {
|
|
if ep.Labels == nil {
|
|
ep.Labels = endpoint.NewLabels()
|
|
}
|
|
dnsNameSplit := strings.Split(ep.DNSName, ".")
|
|
// If specified, replace a leading asterisk in the generated txt record name with some other string
|
|
if im.wildcardReplacement != "" && dnsNameSplit[0] == "*" {
|
|
dnsNameSplit[0] = im.wildcardReplacement
|
|
}
|
|
dnsName := strings.Join(dnsNameSplit, ".")
|
|
key := fmt.Sprintf("%s::%s", dnsName, ep.SetIdentifier)
|
|
if labels, ok := labelMap[key]; ok {
|
|
for k, v := range labels {
|
|
ep.Labels[k] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update the cache.
|
|
if im.cacheInterval > 0 {
|
|
im.recordsCache = endpoints
|
|
im.recordsCacheRefreshTime = time.Now()
|
|
}
|
|
|
|
return endpoints, nil
|
|
}
|
|
|
|
// ApplyChanges updates dns provider with the changes
|
|
// for each created/deleted record it will also take into account TXT records for creation/deletion
|
|
func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
|
filteredChanges := &plan.Changes{
|
|
Create: changes.Create,
|
|
UpdateNew: filterOwnedRecords(im.ownerID, changes.UpdateNew),
|
|
UpdateOld: filterOwnedRecords(im.ownerID, changes.UpdateOld),
|
|
Delete: filterOwnedRecords(im.ownerID, changes.Delete),
|
|
}
|
|
for _, r := range filteredChanges.Create {
|
|
if r.Labels == nil {
|
|
r.Labels = make(map[string]string)
|
|
}
|
|
r.Labels[endpoint.OwnerLabelKey] = im.ownerID
|
|
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)).WithSetIdentifier(r.SetIdentifier)
|
|
txt.ProviderSpecific = r.ProviderSpecific
|
|
filteredChanges.Create = append(filteredChanges.Create, txt)
|
|
|
|
if im.cacheInterval > 0 {
|
|
im.addToCache(r)
|
|
}
|
|
}
|
|
|
|
for _, r := range filteredChanges.Delete {
|
|
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)).WithSetIdentifier(r.SetIdentifier)
|
|
txt.ProviderSpecific = r.ProviderSpecific
|
|
|
|
// when we delete TXT records for which value has changed (due to new label) this would still work because
|
|
// !!! TXT record value is uniquely generated from the Labels of the endpoint. Hence old TXT record can be uniquely reconstructed
|
|
filteredChanges.Delete = append(filteredChanges.Delete, txt)
|
|
|
|
if im.cacheInterval > 0 {
|
|
im.removeFromCache(r)
|
|
}
|
|
}
|
|
|
|
// make sure TXT records are consistently updated as well
|
|
for _, r := range filteredChanges.UpdateOld {
|
|
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)).WithSetIdentifier(r.SetIdentifier)
|
|
txt.ProviderSpecific = r.ProviderSpecific
|
|
// when we updateOld TXT records for which value has changed (due to new label) this would still work because
|
|
// !!! TXT record value is uniquely generated from the Labels of the endpoint. Hence old TXT record can be uniquely reconstructed
|
|
filteredChanges.UpdateOld = append(filteredChanges.UpdateOld, txt)
|
|
// remove old version of record from cache
|
|
if im.cacheInterval > 0 {
|
|
im.removeFromCache(r)
|
|
}
|
|
}
|
|
|
|
// make sure TXT records are consistently updated as well
|
|
for _, r := range filteredChanges.UpdateNew {
|
|
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), endpoint.RecordTypeTXT, r.Labels.Serialize(true)).WithSetIdentifier(r.SetIdentifier)
|
|
txt.ProviderSpecific = r.ProviderSpecific
|
|
filteredChanges.UpdateNew = append(filteredChanges.UpdateNew, txt)
|
|
// add new version of record to cache
|
|
if im.cacheInterval > 0 {
|
|
im.addToCache(r)
|
|
}
|
|
}
|
|
|
|
// when caching is enabled, disable the provider from using the cache
|
|
if im.cacheInterval > 0 {
|
|
ctx = context.WithValue(ctx, provider.RecordsContextKey, nil)
|
|
}
|
|
return im.provider.ApplyChanges(ctx, filteredChanges)
|
|
}
|
|
|
|
// PropertyValuesEqual compares two attribute values for equality
|
|
func (im *TXTRegistry) PropertyValuesEqual(name string, previous string, current string) bool {
|
|
return im.provider.PropertyValuesEqual(name, previous, current)
|
|
}
|
|
|
|
// AdjustEndpoints modifies the endpoints as needed by the specific provider
|
|
func (im *TXTRegistry) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
|
|
return im.provider.AdjustEndpoints(endpoints)
|
|
}
|
|
|
|
/**
|
|
TXT registry specific private methods
|
|
*/
|
|
|
|
/**
|
|
nameMapper defines interface which maps the dns name defined for the source
|
|
to the dns name which TXT record will be created with
|
|
*/
|
|
|
|
type nameMapper interface {
|
|
toEndpointName(string) string
|
|
toTXTName(string) string
|
|
}
|
|
|
|
type affixNameMapper struct {
|
|
prefix string
|
|
suffix string
|
|
wildcardReplacement string
|
|
}
|
|
|
|
var _ nameMapper = affixNameMapper{}
|
|
|
|
func newaffixNameMapper(prefix string, suffix string, wildcardReplacement string) affixNameMapper {
|
|
return affixNameMapper{prefix: strings.ToLower(prefix), suffix: strings.ToLower(suffix), wildcardReplacement: strings.ToLower(wildcardReplacement)}
|
|
}
|
|
|
|
func (pr affixNameMapper) toEndpointName(txtDNSName string) string {
|
|
lowerDNSName := strings.ToLower(txtDNSName)
|
|
if strings.HasPrefix(lowerDNSName, pr.prefix) && len(pr.suffix) == 0 {
|
|
return strings.TrimPrefix(lowerDNSName, pr.prefix)
|
|
}
|
|
|
|
if len(pr.suffix) > 0 {
|
|
DNSName := strings.SplitN(lowerDNSName, ".", 2)
|
|
if strings.HasSuffix(DNSName[0], pr.suffix) {
|
|
return strings.TrimSuffix(DNSName[0], pr.suffix) + "." + DNSName[1]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (pr affixNameMapper) toTXTName(endpointDNSName string) string {
|
|
DNSName := strings.SplitN(endpointDNSName, ".", 2)
|
|
|
|
// 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 pr.prefix + DNSName[0] + pr.suffix
|
|
}
|
|
return pr.prefix + DNSName[0] + pr.suffix + "." + DNSName[1]
|
|
}
|
|
|
|
func (im *TXTRegistry) addToCache(ep *endpoint.Endpoint) {
|
|
if im.recordsCache != nil {
|
|
im.recordsCache = append(im.recordsCache, ep)
|
|
}
|
|
}
|
|
|
|
func (im *TXTRegistry) removeFromCache(ep *endpoint.Endpoint) {
|
|
if im.recordsCache == nil || ep == nil {
|
|
// return early.
|
|
return
|
|
}
|
|
|
|
for i, e := range im.recordsCache {
|
|
if e.DNSName == ep.DNSName && e.RecordType == ep.RecordType && e.SetIdentifier == ep.SetIdentifier && e.Targets.Same(ep.Targets) {
|
|
// We found a match delete the endpoint from the cache.
|
|
im.recordsCache = append(im.recordsCache[:i], im.recordsCache[i+1:]...)
|
|
return
|
|
}
|
|
}
|
|
}
|