sigs.k8s.io/external-dns@v0.14.1/provider/godaddy/godaddy.go (about) 1 /* 2 Copyright 2020 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 godaddy 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "strings" 25 26 log "github.com/sirupsen/logrus" 27 "golang.org/x/sync/errgroup" 28 29 "sigs.k8s.io/external-dns/endpoint" 30 "sigs.k8s.io/external-dns/plan" 31 "sigs.k8s.io/external-dns/provider" 32 ) 33 34 const ( 35 gdMinimalTTL = 600 36 gdCreate = 0 37 gdReplace = 1 38 gdDelete = 2 39 ) 40 41 var actionNames = []string{ 42 "create", 43 "replace", 44 "delete", 45 } 46 47 const domainsURI = "/v1/domains?statuses=ACTIVE,PENDING_DNS_ACTIVE" 48 49 // ErrRecordToMutateNotFound when ApplyChange has to update/delete and didn't found the record in the existing zone (Change with no record ID) 50 var ErrRecordToMutateNotFound = errors.New("record to mutate not found in current zone") 51 52 type gdClient interface { 53 Patch(string, interface{}, interface{}) error 54 Post(string, interface{}, interface{}) error 55 Put(string, interface{}, interface{}) error 56 Get(string, interface{}) error 57 Delete(string, interface{}) error 58 } 59 60 // GDProvider declare GoDaddy provider 61 type GDProvider struct { 62 provider.BaseProvider 63 64 domainFilter endpoint.DomainFilter 65 client gdClient 66 ttl int64 67 DryRun bool 68 } 69 70 type gdEndpoint struct { 71 endpoint *endpoint.Endpoint 72 action int 73 } 74 75 type gdRecordField struct { 76 Data string `json:"data"` 77 Name string `json:"name"` 78 TTL int64 `json:"ttl"` 79 Type string `json:"type"` 80 Port *int `json:"port,omitempty"` 81 Priority *int `json:"priority,omitempty"` 82 Weight *int64 `json:"weight,omitempty"` 83 Protocol *string `json:"protocol,omitempty"` 84 Service *string `json:"service,omitempty"` 85 } 86 87 type gdReplaceRecordField struct { 88 Data string `json:"data"` 89 TTL int64 `json:"ttl"` 90 Port *int `json:"port,omitempty"` 91 Priority *int `json:"priority,omitempty"` 92 Weight *int64 `json:"weight,omitempty"` 93 Protocol *string `json:"protocol,omitempty"` 94 Service *string `json:"service,omitempty"` 95 } 96 97 type gdRecords struct { 98 records []gdRecordField 99 changed bool 100 zone string 101 } 102 103 type gdZone struct { 104 CreatedAt string 105 Domain string 106 DomainID int64 107 ExpirationProtected bool 108 Expires string 109 ExposeWhois bool 110 HoldRegistrar bool 111 Locked bool 112 NameServers *[]string 113 Privacy bool 114 RenewAuto bool 115 RenewDeadline string 116 Renewable bool 117 Status string 118 TransferProtected bool 119 } 120 121 type gdZoneIDName map[string]*gdRecords 122 123 func (z gdZoneIDName) add(zoneID string, zoneRecord *gdRecords) { 124 z[zoneID] = zoneRecord 125 } 126 127 func (z gdZoneIDName) findZoneRecord(hostname string) (suitableZoneID string, suitableZoneRecord *gdRecords) { 128 for zoneID, zoneRecord := range z { 129 if hostname == zoneRecord.zone || strings.HasSuffix(hostname, "."+zoneRecord.zone) { 130 if suitableZoneRecord == nil || len(zoneRecord.zone) > len(suitableZoneRecord.zone) { 131 suitableZoneID = zoneID 132 suitableZoneRecord = zoneRecord 133 } 134 } 135 } 136 137 return 138 } 139 140 // NewGoDaddyProvider initializes a new GoDaddy DNS based Provider. 141 func NewGoDaddyProvider(ctx context.Context, domainFilter endpoint.DomainFilter, ttl int64, apiKey, apiSecret string, useOTE, dryRun bool) (*GDProvider, error) { 142 client, err := NewClient(useOTE, apiKey, apiSecret) 143 if err != nil { 144 return nil, err 145 } 146 147 return &GDProvider{ 148 client: client, 149 domainFilter: domainFilter, 150 ttl: maxOf(gdMinimalTTL, ttl), 151 DryRun: dryRun, 152 }, nil 153 } 154 155 func (p *GDProvider) zones() ([]string, error) { 156 zones := []gdZone{} 157 filteredZones := []string{} 158 159 if err := p.client.Get(domainsURI, &zones); err != nil { 160 return nil, err 161 } 162 163 for _, zone := range zones { 164 if p.domainFilter.Match(zone.Domain) { 165 filteredZones = append(filteredZones, zone.Domain) 166 log.Debugf("GoDaddy: %s zone found", zone.Domain) 167 } 168 } 169 170 log.Infof("GoDaddy: %d zones found", len(filteredZones)) 171 172 return filteredZones, nil 173 } 174 175 func (p *GDProvider) zonesRecords(ctx context.Context, all bool) ([]string, []gdRecords, error) { 176 var allRecords []gdRecords 177 zones, err := p.zones() 178 if err != nil { 179 return nil, nil, err 180 } 181 182 if len(zones) == 0 { 183 allRecords = []gdRecords{} 184 } else if len(zones) == 1 { 185 record, err := p.records(&ctx, zones[0], all) 186 if err != nil { 187 return nil, nil, err 188 } 189 190 allRecords = append(allRecords, *record) 191 } else { 192 chRecords := make(chan gdRecords, len(zones)) 193 194 eg, ctx := errgroup.WithContext(ctx) 195 196 for _, zoneName := range zones { 197 zone := zoneName 198 eg.Go(func() error { 199 record, err := p.records(&ctx, zone, all) 200 if err != nil { 201 return err 202 } 203 204 chRecords <- *record 205 206 return nil 207 }) 208 } 209 210 if err := eg.Wait(); err != nil { 211 return nil, nil, err 212 } 213 214 close(chRecords) 215 216 for records := range chRecords { 217 allRecords = append(allRecords, records) 218 } 219 } 220 221 return zones, allRecords, nil 222 } 223 224 func (p *GDProvider) records(ctx *context.Context, zone string, all bool) (*gdRecords, error) { 225 var recordsIds []gdRecordField 226 227 log.Debugf("GoDaddy: Getting records for %s", zone) 228 229 if err := p.client.Get(fmt.Sprintf("/v1/domains/%s/records", zone), &recordsIds); err != nil { 230 return nil, err 231 } 232 233 if all { 234 return &gdRecords{ 235 zone: zone, 236 records: recordsIds, 237 }, nil 238 } 239 240 results := &gdRecords{ 241 zone: zone, 242 records: make([]gdRecordField, 0, len(recordsIds)), 243 } 244 245 for _, rec := range recordsIds { 246 if provider.SupportedRecordType(rec.Type) { 247 log.Debugf("GoDaddy: Record %s for %s is %+v", rec.Name, zone, rec) 248 249 results.records = append(results.records, rec) 250 } else { 251 log.Infof("GoDaddy: Ignore record %s for %s is %+v", rec.Name, zone, rec) 252 } 253 } 254 255 return results, nil 256 } 257 258 func (p *GDProvider) groupByNameAndType(zoneRecords []gdRecords) []*endpoint.Endpoint { 259 endpoints := []*endpoint.Endpoint{} 260 261 // group supported records by name and type 262 groupsByZone := map[string]map[string][]gdRecordField{} 263 264 for _, zone := range zoneRecords { 265 groups := map[string][]gdRecordField{} 266 267 groupsByZone[zone.zone] = groups 268 269 for _, r := range zone.records { 270 groupBy := fmt.Sprintf("%s - %s", r.Type, r.Name) 271 272 if _, ok := groups[groupBy]; !ok { 273 groups[groupBy] = []gdRecordField{} 274 } 275 276 groups[groupBy] = append(groups[groupBy], r) 277 } 278 } 279 280 // create single endpoint with all the targets for each name/type 281 for zoneName, groups := range groupsByZone { 282 for _, records := range groups { 283 targets := []string{} 284 285 for _, record := range records { 286 targets = append(targets, record.Data) 287 } 288 289 var recordName string 290 291 if records[0].Name == "@" { 292 recordName = strings.TrimPrefix(zoneName, ".") 293 } else { 294 recordName = strings.TrimPrefix(fmt.Sprintf("%s.%s", records[0].Name, zoneName), ".") 295 } 296 297 endpoint := endpoint.NewEndpointWithTTL( 298 recordName, 299 records[0].Type, 300 endpoint.TTL(records[0].TTL), 301 targets..., 302 ) 303 304 endpoints = append(endpoints, endpoint) 305 } 306 } 307 308 return endpoints 309 } 310 311 // Records returns the list of records in all relevant zones. 312 func (p *GDProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { 313 _, records, err := p.zonesRecords(ctx, false) 314 if err != nil { 315 return nil, err 316 } 317 318 endpoints := p.groupByNameAndType(records) 319 320 log.Infof("GoDaddy: %d endpoints have been found", len(endpoints)) 321 322 return endpoints, nil 323 } 324 325 func (p *GDProvider) appendChange(action int, endpoints []*endpoint.Endpoint, allChanges []gdEndpoint) []gdEndpoint { 326 for _, e := range endpoints { 327 allChanges = append(allChanges, gdEndpoint{ 328 action: action, 329 endpoint: e, 330 }) 331 } 332 333 return allChanges 334 } 335 336 func (p *GDProvider) changeAllRecords(endpoints []gdEndpoint, zoneRecords []*gdRecords) error { 337 zoneNameIDMapper := gdZoneIDName{} 338 339 for _, zoneRecord := range zoneRecords { 340 zoneNameIDMapper.add(zoneRecord.zone, zoneRecord) 341 } 342 343 for _, e := range endpoints { 344 dnsName := e.endpoint.DNSName 345 zone, zoneRecord := zoneNameIDMapper.findZoneRecord(dnsName) 346 347 if zone == "" { 348 log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", dnsName) 349 } else { 350 dnsName = strings.TrimSuffix(dnsName, "."+zone) 351 if dnsName == zone { 352 dnsName = "" 353 } 354 355 if e.endpoint.RecordType == endpoint.RecordTypeA && (len(dnsName) == 0) { 356 dnsName = "@" 357 } 358 359 e.endpoint.RecordTTL = endpoint.TTL(maxOf(gdMinimalTTL, int64(e.endpoint.RecordTTL))) 360 361 if err := zoneRecord.applyEndpoint(e.action, p.client, *e.endpoint, dnsName, p.DryRun); err != nil { 362 log.Errorf("Unable to apply change %s on record %s type %s, %v", actionNames[e.action], dnsName, e.endpoint.RecordType, err) 363 364 return err 365 } 366 } 367 } 368 369 return nil 370 } 371 372 // ApplyChanges applies a given set of changes in a given zone. 373 func (p *GDProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { 374 if countTargets(changes) == 0 { 375 return nil 376 } 377 378 _, records, err := p.zonesRecords(ctx, true) 379 if err != nil { 380 return err 381 } 382 383 changedZoneRecords := make([]*gdRecords, len(records)) 384 385 for i := range records { 386 changedZoneRecords[i] = &records[i] 387 } 388 389 var allChanges []gdEndpoint 390 391 allChanges = p.appendChange(gdDelete, changes.Delete, allChanges) 392 393 iOldSkip := make(map[int]bool) 394 iNewSkip := make(map[int]bool) 395 396 for iOld, recOld := range changes.UpdateOld { 397 for iNew, recNew := range changes.UpdateNew { 398 if recOld.DNSName == recNew.DNSName && recOld.RecordType == recNew.RecordType { 399 ReplaceEndpoints := []*endpoint.Endpoint{recNew} 400 allChanges = p.appendChange(gdReplace, ReplaceEndpoints, allChanges) 401 iOldSkip[iOld] = true 402 iNewSkip[iNew] = true 403 break 404 } 405 } 406 } 407 408 for iOld, recOld := range changes.UpdateOld { 409 _, found := iOldSkip[iOld] 410 if found { 411 continue 412 } 413 for iNew, recNew := range changes.UpdateNew { 414 _, found := iNewSkip[iNew] 415 if found { 416 continue 417 } 418 419 if recOld.DNSName != recNew.DNSName { 420 continue 421 } 422 423 DeleteEndpoints := []*endpoint.Endpoint{recOld} 424 CreateEndpoints := []*endpoint.Endpoint{recNew} 425 allChanges = p.appendChange(gdDelete, DeleteEndpoints, allChanges) 426 allChanges = p.appendChange(gdCreate, CreateEndpoints, allChanges) 427 428 break 429 } 430 } 431 432 allChanges = p.appendChange(gdCreate, changes.Create, allChanges) 433 434 log.Infof("GoDaddy: %d changes will be done", len(allChanges)) 435 436 if err = p.changeAllRecords(allChanges, changedZoneRecords); err != nil { 437 return err 438 } 439 440 return nil 441 } 442 443 func (p *gdRecords) addRecord(client gdClient, endpoint endpoint.Endpoint, dnsName string, dryRun bool) error { 444 var response GDErrorResponse 445 for _, target := range endpoint.Targets { 446 change := gdRecordField{ 447 Type: endpoint.RecordType, 448 Name: dnsName, 449 TTL: int64(endpoint.RecordTTL), 450 Data: target, 451 } 452 453 p.records = append(p.records, change) 454 p.changed = true 455 456 log.Debugf("GoDaddy: Add an entry %s to zone %s", change.String(), p.zone) 457 if dryRun { 458 log.Infof("[DryRun] - Add record %s.%s of type %s %s", change.Name, p.zone, change.Type, toString(change)) 459 } else if err := client.Patch(fmt.Sprintf("/v1/domains/%s/records", p.zone), []gdRecordField{change}, &response); err != nil { 460 log.Errorf("Add record %s.%s of type %s failed: %s", change.Name, p.zone, change.Type, response) 461 462 return err 463 } 464 } 465 466 return nil 467 } 468 469 func (p *gdRecords) replaceRecord(client gdClient, endpoint endpoint.Endpoint, dnsName string, dryRun bool) error { 470 changed := []gdReplaceRecordField{} 471 records := []string{} 472 473 for _, target := range endpoint.Targets { 474 change := gdRecordField{ 475 Type: endpoint.RecordType, 476 Name: dnsName, 477 TTL: int64(endpoint.RecordTTL), 478 Data: target, 479 } 480 481 for index, record := range p.records { 482 if record.Type == change.Type && record.Name == change.Name { 483 p.records[index] = change 484 p.changed = true 485 } 486 } 487 records = append(records, target) 488 changed = append(changed, gdReplaceRecordField{ 489 Data: change.Data, 490 TTL: change.TTL, 491 Port: change.Port, 492 Priority: change.Priority, 493 Weight: change.Weight, 494 Protocol: change.Protocol, 495 Service: change.Service, 496 }) 497 } 498 499 var response GDErrorResponse 500 501 if dryRun { 502 log.Infof("[DryRun] - Replace record %s.%s of type %s %s", dnsName, p.zone, endpoint.RecordType, records) 503 504 return nil 505 } 506 507 log.Debugf("Replace record %s.%s of type %s %s", dnsName, p.zone, endpoint.RecordType, records) 508 if err := client.Put(fmt.Sprintf("/v1/domains/%s/records/%s/%s", p.zone, endpoint.RecordType, dnsName), changed, &response); err != nil { 509 log.Errorf("Replace record %s.%s of type %s failed: %v", dnsName, p.zone, endpoint.RecordType, response) 510 511 return err 512 } 513 514 return nil 515 } 516 517 // Remove one record from the record list 518 func (p *gdRecords) deleteRecord(client gdClient, endpoint endpoint.Endpoint, dnsName string, dryRun bool) error { 519 records := []string{} 520 521 for _, target := range endpoint.Targets { 522 change := gdRecordField{ 523 Type: endpoint.RecordType, 524 Name: dnsName, 525 TTL: int64(endpoint.RecordTTL), 526 Data: target, 527 } 528 records = append(records, target) 529 530 log.Debugf("GoDaddy: Delete an entry %s from zone %s", change.String(), p.zone) 531 532 deleteIndex := -1 533 534 for index, record := range p.records { 535 if record.Type == change.Type && record.Name == change.Name && record.Data == change.Data { 536 deleteIndex = index 537 break 538 } 539 } 540 541 if deleteIndex >= 0 { 542 p.records[deleteIndex] = p.records[len(p.records)-1] 543 544 p.records = p.records[:len(p.records)-1] 545 p.changed = true 546 } 547 } 548 549 if dryRun { 550 log.Infof("[DryRun] - Delete record %s.%s of type %s %s", dnsName, p.zone, endpoint.RecordType, records) 551 552 return nil 553 } 554 555 var response GDErrorResponse 556 if err := client.Delete(fmt.Sprintf("/v1/domains/%s/records/%s/%s", p.zone, endpoint.RecordType, dnsName), &response); err != nil { 557 log.Errorf("Delete record %s.%s of type %s failed: %v", dnsName, p.zone, endpoint.RecordType, response) 558 559 return err 560 } 561 562 return nil 563 } 564 565 func (p *gdRecords) applyEndpoint(action int, client gdClient, endpoint endpoint.Endpoint, dnsName string, dryRun bool) error { 566 switch action { 567 case gdCreate: 568 return p.addRecord(client, endpoint, dnsName, dryRun) 569 case gdReplace: 570 return p.replaceRecord(client, endpoint, dnsName, dryRun) 571 case gdDelete: 572 return p.deleteRecord(client, endpoint, dnsName, dryRun) 573 } 574 575 return nil 576 } 577 578 func (c gdRecordField) String() string { 579 return fmt.Sprintf("%s %d IN %s %s", c.Name, c.TTL, c.Type, c.Data) 580 } 581 582 func countTargets(p *plan.Changes) int { 583 changes := [][]*endpoint.Endpoint{p.Create, p.UpdateNew, p.UpdateOld, p.Delete} 584 count := 0 585 586 for _, endpoints := range changes { 587 for _, endpoint := range endpoints { 588 count += len(endpoint.Targets) 589 } 590 } 591 592 return count 593 } 594 595 func maxOf(vars ...int64) int64 { 596 max := vars[0] 597 598 for _, i := range vars { 599 if max < i { 600 max = i 601 } 602 } 603 604 return max 605 } 606 607 func toString(obj interface{}) string { 608 b, err := json.MarshalIndent(obj, "", " ") 609 if err != nil { 610 return fmt.Sprintf("<%v>", err) 611 } 612 613 return string(b) 614 }