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