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