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