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