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  }