sigs.k8s.io/external-dns@v0.14.1/plan/plan.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package plan
    18  
    19  import (
    20  	"fmt"
    21  	"strings"
    22  
    23  	"github.com/google/go-cmp/cmp"
    24  	log "github.com/sirupsen/logrus"
    25  
    26  	"sigs.k8s.io/external-dns/endpoint"
    27  )
    28  
    29  // PropertyComparator is used in Plan for comparing the previous and current custom annotations.
    30  type PropertyComparator func(name string, previous string, current string) bool
    31  
    32  // Plan can convert a list of desired and current records to a series of create,
    33  // update and delete actions.
    34  type Plan struct {
    35  	// List of current records
    36  	Current []*endpoint.Endpoint
    37  	// List of desired records
    38  	Desired []*endpoint.Endpoint
    39  	// Policies under which the desired changes are calculated
    40  	Policies []Policy
    41  	// List of changes necessary to move towards desired state
    42  	// Populated after calling Calculate()
    43  	Changes *Changes
    44  	// DomainFilter matches DNS names
    45  	DomainFilter endpoint.MatchAllDomainFilters
    46  	// ManagedRecords are DNS record types that will be considered for management.
    47  	ManagedRecords []string
    48  	// ExcludeRecords are DNS record types that will be excluded from management.
    49  	ExcludeRecords []string
    50  	// OwnerID of records to manage
    51  	OwnerID string
    52  }
    53  
    54  // Changes holds lists of actions to be executed by dns providers
    55  type Changes struct {
    56  	// Records that need to be created
    57  	Create []*endpoint.Endpoint
    58  	// Records that need to be updated (current data)
    59  	UpdateOld []*endpoint.Endpoint
    60  	// Records that need to be updated (desired data)
    61  	UpdateNew []*endpoint.Endpoint
    62  	// Records that need to be deleted
    63  	Delete []*endpoint.Endpoint
    64  }
    65  
    66  // planKey is a key for a row in `planTable`.
    67  type planKey struct {
    68  	dnsName       string
    69  	setIdentifier string
    70  }
    71  
    72  // planTable is a supplementary struct for Plan
    73  // each row correspond to a planKey -> (current records + all desired records)
    74  //
    75  //	planTable (-> = target)
    76  //	--------------------------------------------------------------
    77  //	DNSName | Current record       | Desired Records             |
    78  //	--------------------------------------------------------------
    79  //	foo.com | [->1.1.1.1 ]         | [->1.1.1.1]                 |  = no action
    80  //	--------------------------------------------------------------
    81  //	bar.com |                      | [->191.1.1.1, ->190.1.1.1]  |  = create (bar.com [-> 190.1.1.1])
    82  //	--------------------------------------------------------------
    83  //	dog.com | [->1.1.1.2]          |                             |  = delete (dog.com [-> 1.1.1.2])
    84  //	--------------------------------------------------------------
    85  //	cat.com | [->::1, ->1.1.1.3]   | [->1.1.1.3]                 |  = update old (cat.com [-> ::1, -> 1.1.1.3]) new (cat.com [-> 1.1.1.3])
    86  //	--------------------------------------------------------------
    87  //	big.com | [->1.1.1.4]          | [->ing.elb.com]             |  = update old (big.com [-> 1.1.1.4]) new (big.com [-> ing.elb.com])
    88  //	--------------------------------------------------------------
    89  //	"=", i.e. result of calculation relies on supplied ConflictResolver
    90  type planTable struct {
    91  	rows     map[planKey]*planTableRow
    92  	resolver ConflictResolver
    93  }
    94  
    95  func newPlanTable() planTable { // TODO: make resolver configurable
    96  	return planTable{map[planKey]*planTableRow{}, PerResource{}}
    97  }
    98  
    99  // planTableRow represents a set of current and desired domain resource records.
   100  type planTableRow struct {
   101  	// current corresponds to the records currently occupying dns name on the dns provider. More than one record may
   102  	// be represented here: for example A and AAAA. If the current domain record is a CNAME, no other record types
   103  	// are allowed per [RFC 1034 3.6.2]
   104  	//
   105  	// [RFC 1034 3.6.2]: https://datatracker.ietf.org/doc/html/rfc1034#autoid-15
   106  	current []*endpoint.Endpoint
   107  	// candidates corresponds to the list of records which would like to have this dnsName.
   108  	candidates []*endpoint.Endpoint
   109  	// records is a grouping of current and candidates by record type, for example A, AAAA, CNAME.
   110  	records map[string]*domainEndpoints
   111  }
   112  
   113  // domainEndpoints is a grouping of current, which are existing records from the registry, and candidates,
   114  // which are desired records from the source. All records in this grouping have the same record type.
   115  type domainEndpoints struct {
   116  	// current corresponds to existing record from the registry. Maybe nil if no current record of the type exists.
   117  	current *endpoint.Endpoint
   118  	// candidates corresponds to the list of records which would like to have this dnsName.
   119  	candidates []*endpoint.Endpoint
   120  }
   121  
   122  func (t planTableRow) String() string {
   123  	return fmt.Sprintf("planTableRow{current=%v, candidates=%v}", t.current, t.candidates)
   124  }
   125  
   126  func (t planTable) addCurrent(e *endpoint.Endpoint) {
   127  	key := t.newPlanKey(e)
   128  	t.rows[key].current = append(t.rows[key].current, e)
   129  	t.rows[key].records[e.RecordType].current = e
   130  }
   131  
   132  func (t planTable) addCandidate(e *endpoint.Endpoint) {
   133  	key := t.newPlanKey(e)
   134  	t.rows[key].candidates = append(t.rows[key].candidates, e)
   135  	t.rows[key].records[e.RecordType].candidates = append(t.rows[key].records[e.RecordType].candidates, e)
   136  }
   137  
   138  func (t *planTable) newPlanKey(e *endpoint.Endpoint) planKey {
   139  	key := planKey{
   140  		dnsName:       normalizeDNSName(e.DNSName),
   141  		setIdentifier: e.SetIdentifier,
   142  	}
   143  
   144  	if _, ok := t.rows[key]; !ok {
   145  		t.rows[key] = &planTableRow{
   146  			records: make(map[string]*domainEndpoints),
   147  		}
   148  	}
   149  
   150  	if _, ok := t.rows[key].records[e.RecordType]; !ok {
   151  		t.rows[key].records[e.RecordType] = &domainEndpoints{}
   152  	}
   153  
   154  	return key
   155  }
   156  
   157  func (c *Changes) HasChanges() bool {
   158  	if len(c.Create) > 0 || len(c.Delete) > 0 {
   159  		return true
   160  	}
   161  	return !cmp.Equal(c.UpdateNew, c.UpdateOld)
   162  }
   163  
   164  // Calculate computes the actions needed to move current state towards desired
   165  // state. It then passes those changes to the current policy for further
   166  // processing. It returns a copy of Plan with the changes populated.
   167  func (p *Plan) Calculate() *Plan {
   168  	t := newPlanTable()
   169  
   170  	if p.DomainFilter == nil {
   171  		p.DomainFilter = endpoint.MatchAllDomainFilters(nil)
   172  	}
   173  
   174  	for _, current := range filterRecordsForPlan(p.Current, p.DomainFilter, p.ManagedRecords, p.ExcludeRecords) {
   175  		t.addCurrent(current)
   176  	}
   177  	for _, desired := range filterRecordsForPlan(p.Desired, p.DomainFilter, p.ManagedRecords, p.ExcludeRecords) {
   178  		t.addCandidate(desired)
   179  	}
   180  
   181  	changes := &Changes{}
   182  
   183  	for key, row := range t.rows {
   184  		// dns name not taken
   185  		if len(row.current) == 0 {
   186  			recordsByType := t.resolver.ResolveRecordTypes(key, row)
   187  			for _, records := range recordsByType {
   188  				if len(records.candidates) > 0 {
   189  					changes.Create = append(changes.Create, t.resolver.ResolveCreate(records.candidates))
   190  				}
   191  			}
   192  		}
   193  
   194  		// dns name released or possibly owned by a different external dns
   195  		if len(row.current) > 0 && len(row.candidates) == 0 {
   196  			changes.Delete = append(changes.Delete, row.current...)
   197  		}
   198  
   199  		// dns name is taken
   200  		if len(row.current) > 0 && len(row.candidates) > 0 {
   201  			creates := []*endpoint.Endpoint{}
   202  
   203  			// apply changes for each record type
   204  			recordsByType := t.resolver.ResolveRecordTypes(key, row)
   205  			for _, records := range recordsByType {
   206  				// record type not desired
   207  				if records.current != nil && len(records.candidates) == 0 {
   208  					changes.Delete = append(changes.Delete, records.current)
   209  				}
   210  
   211  				// new record type desired
   212  				if records.current == nil && len(records.candidates) > 0 {
   213  					update := t.resolver.ResolveCreate(records.candidates)
   214  					// creates are evaluated after all domain records have been processed to
   215  					// validate that this external dns has ownership claim on the domain before
   216  					// adding the records to planned changes.
   217  					creates = append(creates, update)
   218  				}
   219  
   220  				// update existing record
   221  				if records.current != nil && len(records.candidates) > 0 {
   222  					update := t.resolver.ResolveUpdate(records.current, records.candidates)
   223  
   224  					if shouldUpdateTTL(update, records.current) || targetChanged(update, records.current) || p.shouldUpdateProviderSpecific(update, records.current) {
   225  						inheritOwner(records.current, update)
   226  						changes.UpdateNew = append(changes.UpdateNew, update)
   227  						changes.UpdateOld = append(changes.UpdateOld, records.current)
   228  					}
   229  				}
   230  			}
   231  
   232  			if len(creates) > 0 {
   233  				// only add creates if the external dns has ownership claim on the domain
   234  				ownersMatch := true
   235  				for _, current := range row.current {
   236  					if p.OwnerID != "" && !current.IsOwnedBy(p.OwnerID) {
   237  						ownersMatch = false
   238  					}
   239  				}
   240  
   241  				if ownersMatch {
   242  					changes.Create = append(changes.Create, creates...)
   243  				}
   244  			}
   245  		}
   246  	}
   247  
   248  	for _, pol := range p.Policies {
   249  		changes = pol.Apply(changes)
   250  	}
   251  
   252  	// filter out updates this external dns does not have ownership claim over
   253  	if p.OwnerID != "" {
   254  		changes.Delete = endpoint.FilterEndpointsByOwnerID(p.OwnerID, changes.Delete)
   255  		changes.UpdateOld = endpoint.FilterEndpointsByOwnerID(p.OwnerID, changes.UpdateOld)
   256  		changes.UpdateNew = endpoint.FilterEndpointsByOwnerID(p.OwnerID, changes.UpdateNew)
   257  	}
   258  
   259  	plan := &Plan{
   260  		Current:        p.Current,
   261  		Desired:        p.Desired,
   262  		Changes:        changes,
   263  		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME},
   264  	}
   265  
   266  	return plan
   267  }
   268  
   269  func inheritOwner(from, to *endpoint.Endpoint) {
   270  	if to.Labels == nil {
   271  		to.Labels = map[string]string{}
   272  	}
   273  	if from.Labels == nil {
   274  		from.Labels = map[string]string{}
   275  	}
   276  	to.Labels[endpoint.OwnerLabelKey] = from.Labels[endpoint.OwnerLabelKey]
   277  }
   278  
   279  func targetChanged(desired, current *endpoint.Endpoint) bool {
   280  	return !desired.Targets.Same(current.Targets)
   281  }
   282  
   283  func shouldUpdateTTL(desired, current *endpoint.Endpoint) bool {
   284  	if !desired.RecordTTL.IsConfigured() {
   285  		return false
   286  	}
   287  	return desired.RecordTTL != current.RecordTTL
   288  }
   289  
   290  func (p *Plan) shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint) bool {
   291  	desiredProperties := map[string]endpoint.ProviderSpecificProperty{}
   292  
   293  	for _, d := range desired.ProviderSpecific {
   294  		desiredProperties[d.Name] = d
   295  	}
   296  	for _, c := range current.ProviderSpecific {
   297  		if d, ok := desiredProperties[c.Name]; ok {
   298  			if c.Value != d.Value {
   299  				return true
   300  			}
   301  			delete(desiredProperties, c.Name)
   302  		} else {
   303  			return true
   304  		}
   305  	}
   306  
   307  	return len(desiredProperties) > 0
   308  }
   309  
   310  // filterRecordsForPlan removes records that are not relevant to the planner.
   311  // Currently this just removes TXT records to prevent them from being
   312  // deleted erroneously by the planner (only the TXT registry should do this.)
   313  //
   314  // Per RFC 1034, CNAME records conflict with all other records - it is the
   315  // only record with this property. The behavior of the planner may need to be
   316  // made more sophisticated to codify this.
   317  func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.MatchAllDomainFilters, managedRecords, excludeRecords []string) []*endpoint.Endpoint {
   318  	filtered := []*endpoint.Endpoint{}
   319  
   320  	for _, record := range records {
   321  		// Ignore records that do not match the domain filter provided
   322  		if !domainFilter.Match(record.DNSName) {
   323  			log.Debugf("ignoring record %s that does not match domain filter", record.DNSName)
   324  			continue
   325  		}
   326  		if IsManagedRecord(record.RecordType, managedRecords, excludeRecords) {
   327  			filtered = append(filtered, record)
   328  		}
   329  	}
   330  
   331  	return filtered
   332  }
   333  
   334  // normalizeDNSName converts a DNS name to a canonical form, so that we can use string equality
   335  // it: removes space, converts to lower case, ensures there is a trailing dot
   336  func normalizeDNSName(dnsName string) string {
   337  	s := strings.TrimSpace(strings.ToLower(dnsName))
   338  	if !strings.HasSuffix(s, ".") {
   339  		s += "."
   340  	}
   341  	return s
   342  }
   343  
   344  func IsManagedRecord(record string, managedRecords, excludeRecords []string) bool {
   345  	for _, r := range excludeRecords {
   346  		if record == r {
   347  			return false
   348  		}
   349  	}
   350  	for _, r := range managedRecords {
   351  		if record == r {
   352  			return true
   353  		}
   354  	}
   355  	return false
   356  }