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  }