github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/route53/route53Provider.go (about)

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