dnscontrol/providers/route53/route53Provider.go
Mathieu Herbert 95705e7f2d Allow more flexibility in configuring Route53 (#93)
* providers/route53/route53Provider.go

* fix fmt

* check credentials, add Route53 fetch zones in init section and add check if delegation set is present or nil
2017-05-03 11:30:05 -06:00

305 lines
7.5 KiB
Go

package route53
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/providers"
"github.com/StackExchange/dnscontrol/providers/diff"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
r53 "github.com/aws/aws-sdk-go/service/route53"
"github.com/pkg/errors"
)
type route53Provider struct {
client *r53.Route53
zones map[string]*r53.HostedZone
}
func newRoute53(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
keyId, secretKey := m["KeyId"], m["SecretKey"]
config := &aws.Config{
Region: aws.String("us-west-2"),
}
if keyId != "" || secretKey != "" {
config.Credentials = credentials.NewStaticCredentials(keyId, secretKey, "")
}
sess := session.New(config)
api := &route53Provider{client: r53.New(sess)}
err := api.getZones()
if err != nil {
return nil, err
}
return api, nil
}
func init() {
providers.RegisterDomainServiceProviderType("ROUTE53", newRoute53)
}
func sPtr(s string) *string {
return &s
}
func (r *route53Provider) getZones() error {
var nextMarker *string
r.zones = make(map[string]*r53.HostedZone)
for {
if nextMarker != nil {
fmt.Println(*nextMarker)
}
inp := &r53.ListHostedZonesInput{Marker: nextMarker}
out, err := r.client.ListHostedZones(inp)
if err != nil && strings.Contains(err.Error(), "is not authorized") {
return errors.New("Check your credentials, your not authorized to perform actions on Route 53 AWS Service")
} else if err != nil {
return err
}
for _, z := range out.HostedZones {
domain := strings.TrimSuffix(*z.Name, ".")
r.zones[domain] = z
}
if out.NextMarker != nil {
nextMarker = out.NextMarker
} else {
break
}
}
return nil
}
//map key for grouping records
type key struct {
Name, Type string
}
func getKey(r *models.RecordConfig) key {
return key{r.NameFQDN, r.Type}
}
type errNoExist struct {
domain string
}
func (e errNoExist) Error() string {
return fmt.Sprintf("Domain %s not found in your route 53 account", e.domain)
}
func (r *route53Provider) GetNameservers(domain string) ([]*models.Nameserver, error) {
zone, ok := r.zones[domain]
if !ok {
return nil, errNoExist{domain}
}
z, err := r.client.GetHostedZone(&r53.GetHostedZoneInput{Id: zone.Id})
if err != nil {
return nil, err
}
ns := []*models.Nameserver{}
if z.DelegationSet != nil {
for _, nsPtr := range z.DelegationSet.NameServers {
ns = append(ns, &models.Nameserver{Name: *nsPtr})
}
}
return ns, nil
}
func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
var corrections = []*models.Correction{}
zone, ok := r.zones[dc.Name]
// add zone if it doesn't exist
if !ok {
return nil, errNoExist{dc.Name}
}
records, err := r.fetchRecordSets(zone.Id)
if err != nil {
return nil, err
}
var existingRecords = []*models.RecordConfig{}
for _, set := range records {
for _, rec := range set.ResourceRecords {
if *set.Type == "SOA" {
continue
}
r := &models.RecordConfig{
NameFQDN: unescape(set.Name),
Type: *set.Type,
Target: *rec.Value,
TTL: uint32(*set.TTL),
}
existingRecords = append(existingRecords, r)
}
}
for _, want := range dc.Records {
if want.Type == "MX" {
want.Target = fmt.Sprintf("%d %s", want.Priority, want.Target)
want.Priority = 0
} else if want.Type == "TXT" {
want.Target = fmt.Sprintf(`"%s"`, want.Target) //FIXME: better escaping/quoting
}
}
//diff
differ := diff.New(dc)
_, create, delete, modify := differ.IncrementalDiff(existingRecords)
namesToUpdate := map[key][]string{}
for _, c := range create {
namesToUpdate[getKey(c.Desired)] = append(namesToUpdate[getKey(c.Desired)], c.String())
}
for _, d := range delete {
namesToUpdate[getKey(d.Existing)] = append(namesToUpdate[getKey(d.Existing)], d.String())
}
for _, m := range modify {
namesToUpdate[getKey(m.Desired)] = append(namesToUpdate[getKey(m.Desired)], m.String())
}
if len(namesToUpdate) == 0 {
return nil, nil
}
updates := map[key][]*models.RecordConfig{}
//for each name we need to update, collect relevant records from dc
for k := range namesToUpdate {
updates[k] = nil
for _, rc := range dc.Records {
if getKey(rc) == k {
updates[k] = append(updates[k], rc)
}
}
}
dels := []*r53.Change{}
changes := []*r53.Change{}
changeDesc := ""
delDesc := ""
for k, recs := range updates {
chg := &r53.Change{}
var rrset *r53.ResourceRecordSet
if len(recs) == 0 {
dels = append(dels, chg)
chg.Action = sPtr("DELETE")
delDesc += strings.Join(namesToUpdate[k], "\n") + "\n"
// on delete just submit the original resource set we got from r53.
for _, r := range records {
if *r.Name == k.Name+"." && *r.Type == k.Type {
rrset = r
break
}
}
} else {
changes = append(changes, chg)
changeDesc += strings.Join(namesToUpdate[k], "\n") + "\n"
//on change or create, just build a new record set from our desired state
chg.Action = sPtr("UPSERT")
rrset = &r53.ResourceRecordSet{
Name: sPtr(k.Name),
Type: sPtr(k.Type),
ResourceRecords: []*r53.ResourceRecord{},
}
for _, r := range recs {
val := r.Target
rr := &r53.ResourceRecord{
Value: &val,
}
rrset.ResourceRecords = append(rrset.ResourceRecords, rr)
i := int64(r.TTL)
rrset.TTL = &i //TODO: make sure that ttls are consistent within a set
}
}
chg.ResourceRecordSet = rrset
}
changeReq := &r53.ChangeResourceRecordSetsInput{
ChangeBatch: &r53.ChangeBatch{Changes: changes},
}
delReq := &r53.ChangeResourceRecordSetsInput{
ChangeBatch: &r53.ChangeBatch{Changes: dels},
}
addCorrection := func(req *r53.ChangeResourceRecordSetsInput) {
corrections = append(corrections,
&models.Correction{
Msg: changeDesc,
F: func() error {
req.HostedZoneId = zone.Id
_, err := r.client.ChangeResourceRecordSets(req)
return err
},
})
}
if len(dels) > 0 {
addCorrection(delReq)
}
if len(changes) > 0 {
addCorrection(changeReq)
}
return corrections, nil
}
func (r *route53Provider) fetchRecordSets(zoneID *string) ([]*r53.ResourceRecordSet, error) {
if zoneID == nil || *zoneID == "" {
return nil, nil
}
var next *string
var nextType *string
var records []*r53.ResourceRecordSet
for {
listInput := &r53.ListResourceRecordSetsInput{
HostedZoneId: zoneID,
StartRecordName: next,
StartRecordType: nextType,
MaxItems: sPtr("100"),
}
list, err := r.client.ListResourceRecordSets(listInput)
if err != nil {
return nil, err
}
records = append(records, list.ResourceRecordSets...)
if list.NextRecordName != nil {
next = list.NextRecordName
nextType = list.NextRecordType
} else {
break
}
}
return records, nil
}
//we have to process names from route53 to match what we expect and to remove their odd octal encoding
func unescape(s *string) string {
if s == nil {
return ""
}
name := strings.TrimSuffix(*s, ".")
name = strings.Replace(name, `\052`, "*", -1) //TODO: escape all octal sequences
return name
}
func (r *route53Provider) EnsureDomainExists(domain string) error {
if _, ok := r.zones[domain]; ok {
return nil
}
fmt.Printf("Adding zone for %s to route 53 account\n", domain)
in := &r53.CreateHostedZoneInput{
Name: &domain,
CallerReference: sPtr(fmt.Sprint(time.Now().UnixNano())),
}
_, err := r.client.CreateHostedZone(in)
return err
}