github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/route53/route53Provider.go (about) 1 package route53 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "sort" 8 "strings" 9 "time" 10 11 "github.com/aws/aws-sdk-go/aws" 12 "github.com/aws/aws-sdk-go/aws/credentials" 13 "github.com/aws/aws-sdk-go/aws/session" 14 r53 "github.com/aws/aws-sdk-go/service/route53" 15 r53d "github.com/aws/aws-sdk-go/service/route53domains" 16 17 "github.com/StackExchange/dnscontrol/v2/models" 18 "github.com/StackExchange/dnscontrol/v2/providers" 19 "github.com/StackExchange/dnscontrol/v2/providers/diff" 20 ) 21 22 type route53Provider struct { 23 client *r53.Route53 24 registrar *r53d.Route53Domains 25 delegationSet *string 26 zones map[string]*r53.HostedZone 27 originalRecords []*r53.ResourceRecordSet 28 } 29 30 func newRoute53Reg(conf map[string]string) (providers.Registrar, error) { 31 return newRoute53(conf, nil) 32 } 33 34 func newRoute53Dsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { 35 return newRoute53(conf, metadata) 36 } 37 38 func newRoute53(m map[string]string, metadata json.RawMessage) (*route53Provider, error) { 39 keyID, secretKey, tokenID := m["KeyId"], m["SecretKey"], m["Token"] 40 41 // Route53 uses a global endpoint and route53domains 42 // currently only has a single regional endpoint in us-east-1 43 // http://docs.aws.amazon.com/general/latest/gr/rande.html#r53_region 44 config := &aws.Config{ 45 Region: aws.String("us-east-1"), 46 } 47 48 // Token is optional and left empty unless required 49 if keyID != "" || secretKey != "" { 50 config.Credentials = credentials.NewStaticCredentials(keyID, secretKey, tokenID) 51 } 52 sess := session.Must(session.NewSession(config)) 53 54 var dls *string = nil 55 if val, ok := m["DelegationSet"]; ok { 56 fmt.Printf("ROUTE53 DelegationSet %s configured\n", val) 57 dls = sPtr(val) 58 } 59 api := &route53Provider{client: r53.New(sess), registrar: r53d.New(sess), delegationSet: dls} 60 err := api.getZones() 61 if err != nil { 62 return nil, err 63 } 64 return api, nil 65 } 66 67 var features = providers.DocumentationNotes{ 68 providers.CanUseAlias: providers.Cannot("R53 does not provide a generic ALIAS functionality. Use R53_ALIAS instead."), 69 providers.DocCreateDomains: providers.Can(), 70 providers.DocDualHost: providers.Can(), 71 providers.DocOfficiallySupported: providers.Can(), 72 providers.CanUsePTR: providers.Can(), 73 providers.CanUseSRV: providers.Can(), 74 providers.CanUseTXTMulti: providers.Can(), 75 providers.CanUseCAA: providers.Can(), 76 providers.CanUseRoute53Alias: providers.Can(), 77 providers.CanGetZones: providers.Can(), 78 } 79 80 func init() { 81 providers.RegisterDomainServiceProviderType("ROUTE53", newRoute53Dsp, features) 82 providers.RegisterRegistrarType("ROUTE53", newRoute53Reg) 83 providers.RegisterCustomRecordType("R53_ALIAS", "ROUTE53", "") 84 } 85 86 func sPtr(s string) *string { 87 return &s 88 } 89 90 func withRetry(f func() error) { 91 const maxRetries = 23 92 // TODO: exponential backoff 93 const sleepTime = 5 * time.Second 94 var currentRetry int = 0 95 for { 96 err := f() 97 if err == nil { 98 return 99 } 100 if strings.Contains(err.Error(), "Rate exceeded") { 101 currentRetry++ 102 if currentRetry >= maxRetries { 103 return 104 } 105 fmt.Printf("============ Route53 rate limit exceeded. Waiting %s to retry.\n", sleepTime) 106 time.Sleep(sleepTime) 107 } else { 108 return 109 } 110 } 111 } 112 113 // ListZones lists the zones on this account. 114 func (r *route53Provider) ListZones() ([]string, error) { 115 var zones []string 116 // Assumes r.zones was filled already by newRoute53(). 117 for i := range r.zones { 118 zones = append(zones, i) 119 } 120 return zones, nil 121 } 122 123 func (r *route53Provider) getZones() error { 124 var nextMarker *string 125 r.zones = make(map[string]*r53.HostedZone) 126 for { 127 var out *r53.ListHostedZonesOutput 128 var err error 129 withRetry(func() error { 130 inp := &r53.ListHostedZonesInput{Marker: nextMarker} 131 out, err = r.client.ListHostedZones(inp) 132 return err 133 }) 134 if err != nil && strings.Contains(err.Error(), "is not authorized") { 135 return errors.New("Check your credentials, your not authorized to perform actions on Route 53 AWS Service") 136 } else if err != nil { 137 return err 138 } 139 for _, z := range out.HostedZones { 140 domain := strings.TrimSuffix(*z.Name, ".") 141 r.zones[domain] = z 142 } 143 if out.NextMarker != nil { 144 nextMarker = out.NextMarker 145 } else { 146 break 147 } 148 } 149 return nil 150 } 151 152 type errNoExist struct { 153 domain string 154 } 155 156 func (e errNoExist) Error() string { 157 return fmt.Sprintf("Domain %s not found in your route 53 account", e.domain) 158 } 159 160 func (r *route53Provider) GetNameservers(domain string) ([]*models.Nameserver, error) { 161 162 zone, ok := r.zones[domain] 163 if !ok { 164 return nil, errNoExist{domain} 165 } 166 var z *r53.GetHostedZoneOutput 167 var err error 168 withRetry(func() error { 169 z, err = r.client.GetHostedZone(&r53.GetHostedZoneInput{Id: zone.Id}) 170 return err 171 }) 172 if err != nil { 173 return nil, err 174 } 175 ns := []*models.Nameserver{} 176 if z.DelegationSet != nil { 177 for _, nsPtr := range z.DelegationSet.NameServers { 178 ns = append(ns, &models.Nameserver{Name: *nsPtr}) 179 } 180 } 181 return ns, nil 182 } 183 184 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format. 185 func (r *route53Provider) GetZoneRecords(domain string) (models.Records, error) { 186 187 zone, ok := r.zones[domain] 188 if !ok { 189 return nil, errNoExist{domain} 190 } 191 192 records, err := r.fetchRecordSets(zone.Id) 193 if err != nil { 194 return nil, err 195 } 196 r.originalRecords = records 197 198 var existingRecords = []*models.RecordConfig{} 199 for _, set := range records { 200 existingRecords = append(existingRecords, nativeToRecords(set, domain)...) 201 } 202 return existingRecords, nil 203 } 204 205 func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 206 dc.Punycode() 207 208 var corrections = []*models.Correction{} 209 210 existingRecords, err := r.GetZoneRecords(dc.Name) 211 if err != nil { 212 return nil, err 213 } 214 215 zone, ok := r.zones[dc.Name] 216 if !ok { 217 return nil, errNoExist{dc.Name} 218 } 219 220 for _, want := range dc.Records { 221 // update zone_id to current zone.id if not specified by the user 222 if want.Type == "R53_ALIAS" && want.R53Alias["zone_id"] == "" { 223 want.R53Alias["zone_id"] = getZoneID(zone, want) 224 } 225 } 226 227 // Normalize 228 models.PostProcessRecords(existingRecords) 229 230 // diff 231 differ := diff.New(dc, getAliasMap) 232 namesToUpdate := differ.ChangedGroups(existingRecords) 233 234 if len(namesToUpdate) == 0 { 235 return nil, nil 236 } 237 238 updates := map[models.RecordKey][]*models.RecordConfig{} 239 240 // for each name we need to update, collect relevant records from our desired domain state 241 for k := range namesToUpdate { 242 updates[k] = nil 243 for _, rc := range dc.Records { 244 if rc.Key() == k { 245 updates[k] = append(updates[k], rc) 246 } 247 } 248 } 249 250 // we collect all changes into one of two categories now: 251 // pure deletions where we delete an entire record set, 252 // or changes where we upsert an entire record set. 253 dels := []*r53.Change{} 254 changes := []*r53.Change{} 255 changeDesc := []string{} 256 delDesc := []string{} 257 258 for k, recs := range updates { 259 chg := &r53.Change{} 260 var rrset *r53.ResourceRecordSet 261 // if there are no records in our desired state for a key, then we just delete it from r53 262 if len(recs) == 0 { 263 dels = append(dels, chg) 264 chg.Action = sPtr("DELETE") 265 delDesc = append(delDesc, strings.Join(namesToUpdate[k], "\n")) 266 // on delete just submit the original resource set we got from r53. 267 for _, r := range r.originalRecords { 268 if unescape(r.Name) == k.NameFQDN && (*r.Type == k.Type || k.Type == "R53_ALIAS_"+*r.Type) { 269 rrset = r 270 break 271 } 272 } 273 if rrset == nil { 274 return nil, fmt.Errorf("No record set found to delete. Name: '%s'. Type: '%s'", k.NameFQDN, k.Type) 275 } 276 } else { 277 changes = append(changes, chg) 278 changeDesc = append(changeDesc, strings.Join(namesToUpdate[k], "\n")) 279 // on change or create, just build a new record set from our desired state 280 chg.Action = sPtr("UPSERT") 281 rrset = &r53.ResourceRecordSet{ 282 Name: sPtr(k.NameFQDN), 283 Type: sPtr(k.Type), 284 } 285 for _, r := range recs { 286 val := r.GetTargetCombined() 287 if r.Type != "R53_ALIAS" { 288 rr := &r53.ResourceRecord{ 289 Value: &val, 290 } 291 rrset.ResourceRecords = append(rrset.ResourceRecords, rr) 292 i := int64(r.TTL) 293 rrset.TTL = &i // TODO: make sure that ttls are consistent within a set 294 } else { 295 rrset = aliasToRRSet(zone, r) 296 } 297 } 298 } 299 chg.ResourceRecordSet = rrset 300 } 301 302 addCorrection := func(msg string, req *r53.ChangeResourceRecordSetsInput) { 303 corrections = append(corrections, 304 &models.Correction{ 305 Msg: msg, 306 F: func() error { 307 var err error 308 req.HostedZoneId = zone.Id 309 withRetry(func() error { 310 _, err = r.client.ChangeResourceRecordSets(req) 311 return err 312 }) 313 return err 314 }, 315 }) 316 } 317 318 getBatchSize := func(size, max int) int { 319 if size > max { 320 return max 321 } 322 return size 323 } 324 325 for len(dels) > 0 { 326 batchSize := getBatchSize(len(dels), 1000) 327 batch := dels[:batchSize] 328 dels = dels[batchSize:] 329 delDescBatch := delDesc[:batchSize] 330 delDesc = delDesc[batchSize:] 331 332 delDescBatchStr := "\n" + strings.Join(delDescBatch, "\n") + "\n" 333 334 delReq := &r53.ChangeResourceRecordSetsInput{ 335 ChangeBatch: &r53.ChangeBatch{Changes: batch}, 336 } 337 addCorrection(delDescBatchStr, delReq) 338 } 339 340 for len(changes) > 0 { 341 batchSize := getBatchSize(len(changes), 500) 342 batch := changes[:batchSize] 343 changes = changes[batchSize:] 344 changeDescBatch := changeDesc[:batchSize] 345 changeDesc = changeDesc[batchSize:] 346 changeDescBatchStr := "\n" + strings.Join(changeDescBatch, "\n") + "\n" 347 348 changeReq := &r53.ChangeResourceRecordSetsInput{ 349 ChangeBatch: &r53.ChangeBatch{Changes: batch}, 350 } 351 addCorrection(changeDescBatchStr, changeReq) 352 } 353 354 return corrections, nil 355 356 } 357 358 func nativeToRecords(set *r53.ResourceRecordSet, origin string) []*models.RecordConfig { 359 results := []*models.RecordConfig{} 360 if set.AliasTarget != nil { 361 rc := &models.RecordConfig{ 362 Type: "R53_ALIAS", 363 TTL: 300, 364 R53Alias: map[string]string{ 365 "type": *set.Type, 366 "zone_id": *set.AliasTarget.HostedZoneId, 367 }, 368 } 369 rc.SetLabelFromFQDN(unescape(set.Name), origin) 370 rc.SetTarget(aws.StringValue(set.AliasTarget.DNSName)) 371 results = append(results, rc) 372 } else if set.TrafficPolicyInstanceId != nil { 373 // skip traffic policy records 374 } else { 375 for _, rec := range set.ResourceRecords { 376 switch rtype := *set.Type; rtype { 377 case "SOA": 378 continue 379 default: 380 rc := &models.RecordConfig{TTL: uint32(*set.TTL)} 381 rc.SetLabelFromFQDN(unescape(set.Name), origin) 382 if err := rc.PopulateFromString(*set.Type, *rec.Value, origin); err != nil { 383 panic(fmt.Errorf("unparsable record received from R53: %w", err)) 384 } 385 results = append(results, rc) 386 } 387 } 388 } 389 return results 390 } 391 392 func getAliasMap(r *models.RecordConfig) map[string]string { 393 if r.Type != "R53_ALIAS" { 394 return nil 395 } 396 return r.R53Alias 397 } 398 399 func aliasToRRSet(zone *r53.HostedZone, r *models.RecordConfig) *r53.ResourceRecordSet { 400 rrset := &r53.ResourceRecordSet{ 401 Name: sPtr(r.GetLabelFQDN()), 402 Type: sPtr(r.R53Alias["type"]), 403 } 404 zoneID := getZoneID(zone, r) 405 targetHealth := false 406 target := r.GetTargetField() 407 rrset.AliasTarget = &r53.AliasTarget{ 408 DNSName: &target, 409 HostedZoneId: aws.String(zoneID), 410 EvaluateTargetHealth: &targetHealth, 411 } 412 return rrset 413 } 414 415 func getZoneID(zone *r53.HostedZone, r *models.RecordConfig) string { 416 zoneID := r.R53Alias["zone_id"] 417 if zoneID == "" { 418 zoneID = aws.StringValue(zone.Id) 419 } 420 if strings.HasPrefix(zoneID, "/hostedzone/") { 421 zoneID = strings.TrimPrefix(zoneID, "/hostedzone/") 422 } 423 return zoneID 424 } 425 426 func (r *route53Provider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 427 corrections := []*models.Correction{} 428 actualSet, err := r.getRegistrarNameservers(&dc.Name) 429 if err != nil { 430 return nil, err 431 } 432 sort.Strings(actualSet) 433 actual := strings.Join(actualSet, ",") 434 435 expectedSet := []string{} 436 for _, ns := range dc.Nameservers { 437 expectedSet = append(expectedSet, ns.Name) 438 } 439 sort.Strings(expectedSet) 440 expected := strings.Join(expectedSet, ",") 441 442 if actual != expected { 443 return []*models.Correction{ 444 { 445 Msg: fmt.Sprintf("Update nameservers %s -> %s", actual, expected), 446 F: func() error { 447 _, err := r.updateRegistrarNameservers(dc.Name, expectedSet) 448 return err 449 }, 450 }, 451 }, nil 452 } 453 454 return corrections, nil 455 } 456 457 func (r *route53Provider) getRegistrarNameservers(domainName *string) ([]string, error) { 458 var domainDetail *r53d.GetDomainDetailOutput 459 var err error 460 withRetry(func() error { 461 domainDetail, err = r.registrar.GetDomainDetail(&r53d.GetDomainDetailInput{DomainName: domainName}) 462 return err 463 }) 464 if err != nil { 465 return nil, err 466 } 467 468 nameservers := []string{} 469 for _, ns := range domainDetail.Nameservers { 470 nameservers = append(nameservers, *ns.Name) 471 } 472 473 return nameservers, nil 474 } 475 476 func (r *route53Provider) updateRegistrarNameservers(domainName string, nameservers []string) (*string, error) { 477 servers := []*r53d.Nameserver{} 478 for i := range nameservers { 479 servers = append(servers, &r53d.Nameserver{Name: &nameservers[i]}) 480 } 481 var domainUpdate *r53d.UpdateDomainNameserversOutput 482 var err error 483 withRetry(func() error { 484 domainUpdate, err = r.registrar.UpdateDomainNameservers(&r53d.UpdateDomainNameserversInput{ 485 DomainName: &domainName, Nameservers: servers}) 486 return err 487 }) 488 if err != nil { 489 return nil, err 490 } 491 492 return domainUpdate.OperationId, nil 493 } 494 495 func (r *route53Provider) fetchRecordSets(zoneID *string) ([]*r53.ResourceRecordSet, error) { 496 if zoneID == nil || *zoneID == "" { 497 return nil, nil 498 } 499 var next *string 500 var nextType *string 501 var records []*r53.ResourceRecordSet 502 for { 503 listInput := &r53.ListResourceRecordSetsInput{ 504 HostedZoneId: zoneID, 505 StartRecordName: next, 506 StartRecordType: nextType, 507 MaxItems: sPtr("100"), 508 } 509 var list *r53.ListResourceRecordSetsOutput 510 var err error 511 withRetry(func() error { 512 list, err = r.client.ListResourceRecordSets(listInput) 513 return err 514 }) 515 if err != nil { 516 return nil, err 517 } 518 519 records = append(records, list.ResourceRecordSets...) 520 if list.NextRecordName != nil { 521 next = list.NextRecordName 522 nextType = list.NextRecordType 523 } else { 524 break 525 } 526 } 527 return records, nil 528 } 529 530 // we have to process names from route53 to match what we expect and to remove their odd octal encoding 531 func unescape(s *string) string { 532 if s == nil { 533 return "" 534 } 535 name := strings.TrimSuffix(*s, ".") 536 name = strings.Replace(name, `\052`, "*", -1) // TODO: escape all octal sequences 537 return name 538 } 539 540 func (r *route53Provider) EnsureDomainExists(domain string) error { 541 if _, ok := r.zones[domain]; ok { 542 return nil 543 } 544 if r.delegationSet != nil { 545 fmt.Printf("Adding zone for %s to route 53 account with delegationSet %s\n", domain, *r.delegationSet) 546 } else { 547 fmt.Printf("Adding zone for %s to route 53 account\n", domain) 548 } 549 in := &r53.CreateHostedZoneInput{ 550 Name: &domain, 551 DelegationSetId: r.delegationSet, 552 CallerReference: sPtr(fmt.Sprint(time.Now().UnixNano())), 553 } 554 var err error 555 withRetry(func() error { 556 _, err := r.client.CreateHostedZone(in) 557 return err 558 }) 559 return err 560 }