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 }