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  }