sigs.k8s.io/external-dns@v0.14.1/provider/linode/linode.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 linode 18 19 import ( 20 "context" 21 "fmt" 22 "net/http" 23 "os" 24 "strconv" 25 "strings" 26 27 "github.com/linode/linodego" 28 log "github.com/sirupsen/logrus" 29 "golang.org/x/oauth2" 30 31 "sigs.k8s.io/external-dns/endpoint" 32 "sigs.k8s.io/external-dns/plan" 33 "sigs.k8s.io/external-dns/provider" 34 ) 35 36 // LinodeDomainClient interface to ease testing 37 type LinodeDomainClient interface { 38 ListDomainRecords(ctx context.Context, domainID int, opts *linodego.ListOptions) ([]linodego.DomainRecord, error) 39 ListDomains(ctx context.Context, opts *linodego.ListOptions) ([]linodego.Domain, error) 40 CreateDomainRecord(ctx context.Context, domainID int, domainrecord linodego.DomainRecordCreateOptions) (*linodego.DomainRecord, error) 41 DeleteDomainRecord(ctx context.Context, domainID int, id int) error 42 UpdateDomainRecord(ctx context.Context, domainID int, id int, domainrecord linodego.DomainRecordUpdateOptions) (*linodego.DomainRecord, error) 43 } 44 45 // LinodeProvider is an implementation of Provider for Digital Ocean's DNS. 46 type LinodeProvider struct { 47 provider.BaseProvider 48 Client LinodeDomainClient 49 domainFilter endpoint.DomainFilter 50 DryRun bool 51 } 52 53 // LinodeChanges All API calls calculated from the plan 54 type LinodeChanges struct { 55 Creates []LinodeChangeCreate 56 Deletes []LinodeChangeDelete 57 Updates []LinodeChangeUpdate 58 } 59 60 // LinodeChangeCreate Linode Domain Record Creates 61 type LinodeChangeCreate struct { 62 Domain linodego.Domain 63 Options linodego.DomainRecordCreateOptions 64 } 65 66 // LinodeChangeUpdate Linode Domain Record Updates 67 type LinodeChangeUpdate struct { 68 Domain linodego.Domain 69 DomainRecord linodego.DomainRecord 70 Options linodego.DomainRecordUpdateOptions 71 } 72 73 // LinodeChangeDelete Linode Domain Record Deletes 74 type LinodeChangeDelete struct { 75 Domain linodego.Domain 76 DomainRecord linodego.DomainRecord 77 } 78 79 // NewLinodeProvider initializes a new Linode DNS based Provider. 80 func NewLinodeProvider(domainFilter endpoint.DomainFilter, dryRun bool, appVersion string) (*LinodeProvider, error) { 81 token, ok := os.LookupEnv("LINODE_TOKEN") 82 if !ok { 83 return nil, fmt.Errorf("no token found") 84 } 85 86 tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) 87 88 oauth2Client := &http.Client{ 89 Transport: &oauth2.Transport{ 90 Source: tokenSource, 91 }, 92 } 93 94 linodeClient := linodego.NewClient(oauth2Client) 95 linodeClient.SetUserAgent(fmt.Sprintf("ExternalDNS/%s linodego/%s", appVersion, linodego.Version)) 96 97 provider := &LinodeProvider{ 98 Client: &linodeClient, 99 domainFilter: domainFilter, 100 DryRun: dryRun, 101 } 102 return provider, nil 103 } 104 105 // Zones returns the list of hosted zones. 106 func (p *LinodeProvider) Zones(ctx context.Context) ([]linodego.Domain, error) { 107 zones, err := p.fetchZones(ctx) 108 if err != nil { 109 return nil, err 110 } 111 112 return zones, nil 113 } 114 115 // Records returns the list of records in a given zone. 116 func (p *LinodeProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { 117 zones, err := p.Zones(ctx) 118 if err != nil { 119 return nil, err 120 } 121 122 var endpoints []*endpoint.Endpoint 123 124 for _, zone := range zones { 125 records, err := p.fetchRecords(ctx, zone.ID) 126 if err != nil { 127 return nil, err 128 } 129 130 for _, r := range records { 131 if provider.SupportedRecordType(string(r.Type)) { 132 name := fmt.Sprintf("%s.%s", r.Name, zone.Domain) 133 134 // root name is identified by the empty string and should be 135 // translated to zone name for the endpoint entry. 136 if r.Name == "" { 137 name = zone.Domain 138 } 139 140 endpoints = append(endpoints, endpoint.NewEndpointWithTTL(name, string(r.Type), endpoint.TTL(r.TTLSec), r.Target)) 141 } 142 } 143 } 144 145 return endpoints, nil 146 } 147 148 func (p *LinodeProvider) fetchRecords(ctx context.Context, domainID int) ([]linodego.DomainRecord, error) { 149 records, err := p.Client.ListDomainRecords(ctx, domainID, nil) 150 if err != nil { 151 return nil, err 152 } 153 154 return records, nil 155 } 156 157 func (p *LinodeProvider) fetchZones(ctx context.Context) ([]linodego.Domain, error) { 158 var zones []linodego.Domain 159 160 allZones, err := p.Client.ListDomains(ctx, linodego.NewListOptions(0, "")) 161 if err != nil { 162 return nil, err 163 } 164 165 for _, zone := range allZones { 166 if !p.domainFilter.Match(zone.Domain) { 167 continue 168 } 169 170 zones = append(zones, zone) 171 } 172 173 return zones, nil 174 } 175 176 // submitChanges takes a zone and a collection of Changes and sends them as a single transaction. 177 func (p *LinodeProvider) submitChanges(ctx context.Context, changes LinodeChanges) error { 178 for _, change := range changes.Creates { 179 logFields := log.Fields{ 180 "record": change.Options.Name, 181 "type": change.Options.Type, 182 "action": "Create", 183 "zoneName": change.Domain.Domain, 184 "zoneID": change.Domain.ID, 185 } 186 187 log.WithFields(logFields).Info("Creating record.") 188 189 if p.DryRun { 190 log.WithFields(logFields).Info("Would create record.") 191 } else if _, err := p.Client.CreateDomainRecord(ctx, change.Domain.ID, change.Options); err != nil { 192 log.WithFields(logFields).Errorf( 193 "Failed to Create record: %v", 194 err, 195 ) 196 } 197 } 198 199 for _, change := range changes.Deletes { 200 logFields := log.Fields{ 201 "record": change.DomainRecord.Name, 202 "type": change.DomainRecord.Type, 203 "action": "Delete", 204 "zoneName": change.Domain.Domain, 205 "zoneID": change.Domain.ID, 206 } 207 208 log.WithFields(logFields).Info("Deleting record.") 209 210 if p.DryRun { 211 log.WithFields(logFields).Info("Would delete record.") 212 } else if err := p.Client.DeleteDomainRecord(ctx, change.Domain.ID, change.DomainRecord.ID); err != nil { 213 log.WithFields(logFields).Errorf( 214 "Failed to Delete record: %v", 215 err, 216 ) 217 } 218 } 219 220 for _, change := range changes.Updates { 221 logFields := log.Fields{ 222 "record": change.Options.Name, 223 "type": change.Options.Type, 224 "action": "Update", 225 "zoneName": change.Domain.Domain, 226 "zoneID": change.Domain.ID, 227 } 228 229 log.WithFields(logFields).Info("Updating record.") 230 231 if p.DryRun { 232 log.WithFields(logFields).Info("Would update record.") 233 } else if _, err := p.Client.UpdateDomainRecord(ctx, change.Domain.ID, change.DomainRecord.ID, change.Options); err != nil { 234 log.WithFields(logFields).Errorf( 235 "Failed to Update record: %v", 236 err, 237 ) 238 } 239 } 240 241 return nil 242 } 243 244 func getWeight(recordType linodego.DomainRecordType) *int { 245 weight := 1 246 247 // NS records do not support having weight 248 if recordType == linodego.RecordTypeNS { 249 weight = 0 250 } 251 return &weight 252 } 253 254 func getPort() *int { 255 port := 0 256 return &port 257 } 258 259 func getPriority() *int { 260 priority := 0 261 return &priority 262 } 263 264 // ApplyChanges applies a given set of changes in a given zone. 265 func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { 266 recordsByZoneID := make(map[string][]linodego.DomainRecord) 267 268 zones, err := p.fetchZones(ctx) 269 if err != nil { 270 return err 271 } 272 273 zonesByID := make(map[string]linodego.Domain) 274 275 zoneNameIDMapper := provider.ZoneIDName{} 276 277 for _, z := range zones { 278 zoneNameIDMapper.Add(strconv.Itoa(z.ID), z.Domain) 279 zonesByID[strconv.Itoa(z.ID)] = z 280 } 281 282 // Fetch records for each zone 283 for _, zone := range zones { 284 records, err := p.fetchRecords(ctx, zone.ID) 285 if err != nil { 286 return err 287 } 288 289 recordsByZoneID[strconv.Itoa(zone.ID)] = append(recordsByZoneID[strconv.Itoa(zone.ID)], records...) 290 } 291 292 createsByZone := endpointsByZone(zoneNameIDMapper, changes.Create) 293 updatesByZone := endpointsByZone(zoneNameIDMapper, changes.UpdateNew) 294 deletesByZone := endpointsByZone(zoneNameIDMapper, changes.Delete) 295 296 var linodeCreates []LinodeChangeCreate 297 var linodeUpdates []LinodeChangeUpdate 298 var linodeDeletes []LinodeChangeDelete 299 300 // Generate Creates 301 for zoneID, creates := range createsByZone { 302 zone := zonesByID[zoneID] 303 304 if len(creates) == 0 { 305 log.WithFields(log.Fields{ 306 "zoneID": zoneID, 307 "zoneName": zone.Domain, 308 }).Debug("Skipping Zone, no creates found.") 309 continue 310 } 311 312 records := recordsByZoneID[zoneID] 313 314 for _, ep := range creates { 315 matchedRecords := getRecordID(records, zone, ep) 316 317 if len(matchedRecords) != 0 { 318 log.WithFields(log.Fields{ 319 "zoneID": zoneID, 320 "zoneName": zone.Domain, 321 "dnsName": ep.DNSName, 322 "recordType": ep.RecordType, 323 }).Warn("Records found which should not exist. Not touching it.") 324 continue 325 } 326 327 recordType, err := convertRecordType(ep.RecordType) 328 if err != nil { 329 return err 330 } 331 332 for _, target := range ep.Targets { 333 linodeCreates = append(linodeCreates, LinodeChangeCreate{ 334 Domain: zone, 335 Options: linodego.DomainRecordCreateOptions{ 336 Target: target, 337 Name: getStrippedRecordName(zone, ep), 338 Type: recordType, 339 Weight: getWeight(recordType), 340 Port: getPort(), 341 Priority: getPriority(), 342 TTLSec: int(ep.RecordTTL), 343 }, 344 }) 345 } 346 } 347 } 348 349 // Generate Updates 350 for zoneID, updates := range updatesByZone { 351 zone := zonesByID[zoneID] 352 353 if len(updates) == 0 { 354 log.WithFields(log.Fields{ 355 "zoneID": zoneID, 356 "zoneName": zone.Domain, 357 }).Debug("Skipping Zone, no updates found.") 358 continue 359 } 360 361 records := recordsByZoneID[zoneID] 362 363 for _, ep := range updates { 364 matchedRecords := getRecordID(records, zone, ep) 365 366 if len(matchedRecords) == 0 { 367 log.WithFields(log.Fields{ 368 "zoneID": zoneID, 369 "dnsName": ep.DNSName, 370 "zoneName": zone.Domain, 371 "recordType": ep.RecordType, 372 }).Warn("Update Records not found.") 373 } 374 375 recordType, err := convertRecordType(ep.RecordType) 376 if err != nil { 377 return err 378 } 379 380 matchedRecordsByTarget := make(map[string]linodego.DomainRecord) 381 382 for _, record := range matchedRecords { 383 matchedRecordsByTarget[record.Target] = record 384 } 385 386 for _, target := range ep.Targets { 387 if record, ok := matchedRecordsByTarget[target]; ok { 388 log.WithFields(log.Fields{ 389 "zoneID": zoneID, 390 "dnsName": ep.DNSName, 391 "zoneName": zone.Domain, 392 "recordType": ep.RecordType, 393 "target": target, 394 }).Warn("Updating Existing Target") 395 396 linodeUpdates = append(linodeUpdates, LinodeChangeUpdate{ 397 Domain: zone, 398 DomainRecord: record, 399 Options: linodego.DomainRecordUpdateOptions{ 400 Target: target, 401 Name: getStrippedRecordName(zone, ep), 402 Type: recordType, 403 Weight: getWeight(recordType), 404 Port: getPort(), 405 Priority: getPriority(), 406 TTLSec: int(ep.RecordTTL), 407 }, 408 }) 409 410 delete(matchedRecordsByTarget, target) 411 } else { 412 // Record did not previously exist, create new 'target' 413 log.WithFields(log.Fields{ 414 "zoneID": zoneID, 415 "dnsName": ep.DNSName, 416 "zoneName": zone.Domain, 417 "recordType": ep.RecordType, 418 "target": target, 419 }).Warn("Creating New Target") 420 421 linodeCreates = append(linodeCreates, LinodeChangeCreate{ 422 Domain: zone, 423 Options: linodego.DomainRecordCreateOptions{ 424 Target: target, 425 Name: getStrippedRecordName(zone, ep), 426 Type: recordType, 427 Weight: getWeight(recordType), 428 Port: getPort(), 429 Priority: getPriority(), 430 TTLSec: int(ep.RecordTTL), 431 }, 432 }) 433 } 434 } 435 436 // Any remaining records have been removed, delete them 437 for _, record := range matchedRecordsByTarget { 438 log.WithFields(log.Fields{ 439 "zoneID": zoneID, 440 "dnsName": ep.DNSName, 441 "zoneName": zone.Domain, 442 "recordType": ep.RecordType, 443 "target": record.Target, 444 }).Warn("Deleting Target") 445 446 linodeDeletes = append(linodeDeletes, LinodeChangeDelete{ 447 Domain: zone, 448 DomainRecord: record, 449 }) 450 } 451 } 452 } 453 454 // Generate Deletes 455 for zoneID, deletes := range deletesByZone { 456 zone := zonesByID[zoneID] 457 458 if len(deletes) == 0 { 459 log.WithFields(log.Fields{ 460 "zoneID": zoneID, 461 "zoneName": zone.Domain, 462 }).Debug("Skipping Zone, no deletes found.") 463 continue 464 } 465 466 records := recordsByZoneID[zoneID] 467 468 for _, ep := range deletes { 469 matchedRecords := getRecordID(records, zone, ep) 470 471 if len(matchedRecords) == 0 { 472 log.WithFields(log.Fields{ 473 "zoneID": zoneID, 474 "dnsName": ep.DNSName, 475 "zoneName": zone.Domain, 476 "recordType": ep.RecordType, 477 }).Warn("Records to Delete not found.") 478 } 479 480 for _, record := range matchedRecords { 481 linodeDeletes = append(linodeDeletes, LinodeChangeDelete{ 482 Domain: zone, 483 DomainRecord: record, 484 }) 485 } 486 } 487 } 488 489 return p.submitChanges(ctx, LinodeChanges{ 490 Creates: linodeCreates, 491 Deletes: linodeDeletes, 492 Updates: linodeUpdates, 493 }) 494 } 495 496 func endpointsByZone(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) map[string][]endpoint.Endpoint { 497 endpointsByZone := make(map[string][]endpoint.Endpoint) 498 499 for _, ep := range endpoints { 500 zoneID, _ := zoneNameIDMapper.FindZone(ep.DNSName) 501 if zoneID == "" { 502 log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", ep.DNSName) 503 continue 504 } 505 endpointsByZone[zoneID] = append(endpointsByZone[zoneID], *ep) 506 } 507 508 return endpointsByZone 509 } 510 511 func convertRecordType(recordType string) (linodego.DomainRecordType, error) { 512 switch recordType { 513 case "A": 514 return linodego.RecordTypeA, nil 515 case "AAAA": 516 return linodego.RecordTypeAAAA, nil 517 case "CNAME": 518 return linodego.RecordTypeCNAME, nil 519 case "TXT": 520 return linodego.RecordTypeTXT, nil 521 case "SRV": 522 return linodego.RecordTypeSRV, nil 523 case "NS": 524 return linodego.RecordTypeNS, nil 525 default: 526 return "", fmt.Errorf("invalid Record Type: %s", recordType) 527 } 528 } 529 530 func getStrippedRecordName(zone linodego.Domain, ep endpoint.Endpoint) string { 531 // Handle root 532 if ep.DNSName == zone.Domain { 533 return "" 534 } 535 536 return strings.TrimSuffix(ep.DNSName, "."+zone.Domain) 537 } 538 539 func getRecordID(records []linodego.DomainRecord, zone linodego.Domain, ep endpoint.Endpoint) []linodego.DomainRecord { 540 var matchedRecords []linodego.DomainRecord 541 542 for _, record := range records { 543 if record.Name == getStrippedRecordName(zone, ep) && string(record.Type) == ep.RecordType { 544 matchedRecords = append(matchedRecords, record) 545 } 546 } 547 548 return matchedRecords 549 }