mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-09-20 14:56:20 +08:00
95705e7f2d
* 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
305 lines
7.5 KiB
Go
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
|
|
|
|
}
|