sigs.k8s.io/external-dns@v0.14.1/plan/conflict.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  	"sort"
    21  
    22  	log "github.com/sirupsen/logrus"
    23  
    24  	"sigs.k8s.io/external-dns/endpoint"
    25  )
    26  
    27  // ConflictResolver is used to make a decision in case of two or more different kubernetes resources
    28  // are trying to acquire same DNS name
    29  type ConflictResolver interface {
    30  	ResolveCreate(candidates []*endpoint.Endpoint) *endpoint.Endpoint
    31  	ResolveUpdate(current *endpoint.Endpoint, candidates []*endpoint.Endpoint) *endpoint.Endpoint
    32  	ResolveRecordTypes(key planKey, row *planTableRow) map[string]*domainEndpoints
    33  }
    34  
    35  // PerResource allows only one resource to own a given dns name
    36  type PerResource struct{}
    37  
    38  // ResolveCreate is invoked when dns name is not owned by any resource
    39  // ResolveCreate takes "minimal" (string comparison of Target) endpoint to acquire the DNS record
    40  func (s PerResource) ResolveCreate(candidates []*endpoint.Endpoint) *endpoint.Endpoint {
    41  	var min *endpoint.Endpoint
    42  	for _, ep := range candidates {
    43  		if min == nil || s.less(ep, min) {
    44  			min = ep
    45  		}
    46  	}
    47  	return min
    48  }
    49  
    50  // ResolveUpdate is invoked when dns name is already owned by "current" endpoint
    51  // ResolveUpdate uses "current" record as base and updates it accordingly with new version of same resource
    52  // if it doesn't exist then pick min
    53  func (s PerResource) ResolveUpdate(current *endpoint.Endpoint, candidates []*endpoint.Endpoint) *endpoint.Endpoint {
    54  	currentResource := current.Labels[endpoint.ResourceLabelKey] // resource which has already acquired the DNS
    55  	// TODO: sort candidates only needed because we can still have two endpoints from same resource here. We sort for consistency
    56  	// TODO: remove once single endpoint can have multiple targets
    57  	sort.SliceStable(candidates, func(i, j int) bool {
    58  		return s.less(candidates[i], candidates[j])
    59  	})
    60  	for _, ep := range candidates {
    61  		if ep.Labels[endpoint.ResourceLabelKey] == currentResource {
    62  			return ep
    63  		}
    64  	}
    65  	return s.ResolveCreate(candidates)
    66  }
    67  
    68  // ResolveRecordTypes attempts to detect and resolve record type conflicts in desired
    69  // endpoints for a domain. For eample if the there is more than 1 candidate and at lease one
    70  // of them is a CNAME. Per [RFC 1034 3.6.2] domains that contain a CNAME can not contain any
    71  // other record types. The default policy will prefer A and AAAA record types when a conflict is
    72  // detected (consistent with [endpoint.Targets.Less]).
    73  //
    74  // [RFC 1034 3.6.2]: https://datatracker.ietf.org/doc/html/rfc1034#autoid-15
    75  func (s PerResource) ResolveRecordTypes(key planKey, row *planTableRow) map[string]*domainEndpoints {
    76  	// no conflicts if only a single desired record type for the domain
    77  	if len(row.candidates) <= 1 {
    78  		return row.records
    79  	}
    80  
    81  	cname := false
    82  	other := false
    83  	for _, c := range row.candidates {
    84  		if c.RecordType == endpoint.RecordTypeCNAME {
    85  			cname = true
    86  		} else {
    87  			other = true
    88  		}
    89  
    90  		if cname && other {
    91  			break
    92  		}
    93  	}
    94  
    95  	// conflict was found, remove candiates of non-preferred record types
    96  	if cname && other {
    97  		log.Infof("Domain %s contains conflicting record type candidates; discarding CNAME record", key.dnsName)
    98  		records := map[string]*domainEndpoints{}
    99  		for recordType, recs := range row.records {
   100  			// policy is to prefer the non-CNAME record types when a conflict is found
   101  			if recordType == endpoint.RecordTypeCNAME {
   102  				// discard candidates of conflicting records
   103  				// keep currect so they can be deleted
   104  				records[recordType] = &domainEndpoints{
   105  					current:    recs.current,
   106  					candidates: []*endpoint.Endpoint{},
   107  				}
   108  			} else {
   109  				records[recordType] = recs
   110  			}
   111  		}
   112  
   113  		return records
   114  	}
   115  
   116  	// no conflict, return all records types
   117  	return row.records
   118  }
   119  
   120  // less returns true if endpoint x is less than y
   121  func (s PerResource) less(x, y *endpoint.Endpoint) bool {
   122  	return x.Targets.IsLess(y.Targets)
   123  }
   124  
   125  // TODO: with cross-resource/cross-cluster setup alternative variations of ConflictResolver can be used