mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-02-23 07:03:01 +08:00
779 lines
23 KiB
Go
779 lines
23 KiB
Go
package route53
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/StackExchange/dnscontrol/v4/models"
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
|
|
"github.com/StackExchange/dnscontrol/v4/providers"
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/config"
|
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
|
r53 "github.com/aws/aws-sdk-go-v2/service/route53"
|
|
r53Types "github.com/aws/aws-sdk-go-v2/service/route53/types"
|
|
r53d "github.com/aws/aws-sdk-go-v2/service/route53domains"
|
|
r53dTypes "github.com/aws/aws-sdk-go-v2/service/route53domains/types"
|
|
)
|
|
|
|
type route53Provider struct {
|
|
client *r53.Client
|
|
registrar *r53d.Client
|
|
delegationSet *string
|
|
zonesByID map[string]r53Types.HostedZone
|
|
zonesByDomain map[string]r53Types.HostedZone
|
|
}
|
|
|
|
func newRoute53Reg(conf map[string]string) (providers.Registrar, error) {
|
|
return newRoute53(conf, nil)
|
|
}
|
|
|
|
func newRoute53Dsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
|
|
return newRoute53(conf, metadata)
|
|
}
|
|
|
|
func newRoute53(m map[string]string, _ json.RawMessage) (*route53Provider, error) {
|
|
optFns := []func(*config.LoadOptions) error{
|
|
// Route53 uses a global endpoint and route53domains
|
|
// currently only has a single regional endpoint in us-east-1
|
|
// https://docs.aws.amazon.com/general/latest/gr/rande.html#r53_region
|
|
config.WithRegion("us-east-1"),
|
|
}
|
|
|
|
keyID, secretKey, tokenID := m["KeyId"], m["SecretKey"], m["Token"]
|
|
// Token is optional and left empty unless required
|
|
if keyID != "" || secretKey != "" {
|
|
optFns = append(optFns, config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(keyID, secretKey, tokenID)))
|
|
}
|
|
|
|
config, err := config.LoadDefaultConfig(context.Background(), optFns...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var dls *string
|
|
if val, ok := m["DelegationSet"]; ok {
|
|
printer.Printf("ROUTE53 DelegationSet %s configured\n", val)
|
|
dls = aws.String(val)
|
|
}
|
|
api := &route53Provider{client: r53.NewFromConfig(config), registrar: r53d.NewFromConfig(config), delegationSet: dls}
|
|
err = api.getZones()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return api, nil
|
|
}
|
|
|
|
var features = providers.DocumentationNotes{
|
|
// The default for unlisted capabilities is 'Cannot'.
|
|
// See providers/capabilities.go for the entire list of capabilities.
|
|
providers.CanGetZones: providers.Can(),
|
|
providers.CanConcur: providers.Can(),
|
|
providers.CanUseAlias: providers.Cannot("R53 does not provide a generic ALIAS functionality. Use R53_ALIAS instead."),
|
|
providers.CanUseCAA: providers.Can(),
|
|
providers.CanUseHTTPS: providers.Can(),
|
|
providers.CanUseLOC: providers.Cannot(),
|
|
providers.CanUsePTR: providers.Can(),
|
|
providers.CanUseRoute53Alias: providers.Can(),
|
|
providers.CanUseSRV: providers.Can(),
|
|
providers.CanUseSSHFP: providers.Can(),
|
|
providers.CanUseSVCB: providers.Can(),
|
|
providers.CanUseTLSA: providers.Can(),
|
|
providers.DocCreateDomains: providers.Can(),
|
|
providers.DocDualHost: providers.Can(),
|
|
providers.DocOfficiallySupported: providers.Can(),
|
|
}
|
|
|
|
func init() {
|
|
const providerName = "ROUTE53"
|
|
const providerMaintainer = "@tresni"
|
|
fns := providers.DspFuncs{
|
|
Initializer: newRoute53Dsp,
|
|
RecordAuditor: AuditRecords,
|
|
}
|
|
providers.RegisterDomainServiceProviderType(providerName, fns, features)
|
|
providers.RegisterRegistrarType(providerName, newRoute53Reg)
|
|
providers.RegisterCustomRecordType("R53_ALIAS", providerName, "")
|
|
providers.RegisterMaintainer(providerName, providerMaintainer)
|
|
}
|
|
|
|
func withRetry(f func() error) {
|
|
const maxRetries = 23
|
|
// TODO: exponential backoff
|
|
const sleepTime = 5 * time.Second
|
|
var currentRetry int
|
|
for {
|
|
err := f()
|
|
if err == nil {
|
|
return
|
|
}
|
|
if strings.Contains(err.Error(), "Rate exceeded") {
|
|
currentRetry++
|
|
if currentRetry >= maxRetries {
|
|
return
|
|
}
|
|
printer.Printf("============ Route53 rate limit exceeded. Waiting %s to retry.\n", sleepTime)
|
|
time.Sleep(sleepTime)
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// ListZones lists the zones on this account.
|
|
func (r *route53Provider) ListZones() ([]string, error) {
|
|
if err := r.getZones(); err != nil {
|
|
return nil, err
|
|
}
|
|
var zones []string
|
|
for i := range r.zonesByDomain {
|
|
zones = append(zones, i)
|
|
}
|
|
return zones, nil
|
|
}
|
|
|
|
func (r *route53Provider) getZones() error {
|
|
if r.zonesByDomain != nil {
|
|
return nil
|
|
}
|
|
|
|
var nextMarker *string
|
|
r.zonesByDomain = make(map[string]r53Types.HostedZone)
|
|
r.zonesByID = make(map[string]r53Types.HostedZone)
|
|
for {
|
|
var out *r53.ListHostedZonesOutput
|
|
var err error
|
|
withRetry(func() error {
|
|
inp := &r53.ListHostedZonesInput{Marker: nextMarker}
|
|
out, err = r.client.ListHostedZones(context.Background(), inp)
|
|
return err
|
|
})
|
|
if err != nil && strings.Contains(err.Error(), "is not authorized") {
|
|
return errors.New("check your credentials, you're not authorized to perform actions on Route 53 AWS Service")
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
for _, z := range out.HostedZones {
|
|
domain := strings.TrimSuffix(aws.ToString(z.Name), ".")
|
|
r.zonesByDomain[domain] = z
|
|
r.zonesByID[parseZoneID(aws.ToString(z.Id))] = z
|
|
}
|
|
if out.NextMarker != nil {
|
|
nextMarker = out.NextMarker
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type errDomainNoExist struct {
|
|
domain string
|
|
}
|
|
|
|
type errZoneNoExist struct {
|
|
zoneID string
|
|
}
|
|
|
|
func (e errDomainNoExist) Error() string {
|
|
return fmt.Sprintf("Domain %s not found in your route 53 account", e.domain)
|
|
}
|
|
|
|
func (e errZoneNoExist) Error() string {
|
|
return fmt.Sprintf("Zone with id %s not found in your route 53 account", e.zoneID)
|
|
}
|
|
|
|
func (r *route53Provider) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
|
if err := r.getZones(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
zone, ok := r.zonesByDomain[domain]
|
|
if !ok {
|
|
return nil, errDomainNoExist{domain}
|
|
}
|
|
var z *r53.GetHostedZoneOutput
|
|
var err error
|
|
withRetry(func() error {
|
|
z, err = r.client.GetHostedZone(context.Background(), &r53.GetHostedZoneInput{Id: zone.Id})
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var nss []string
|
|
if z.DelegationSet != nil {
|
|
nss = z.DelegationSet.NameServers
|
|
}
|
|
return models.ToNameservers(nss)
|
|
}
|
|
|
|
func (r *route53Provider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
|
|
if err := r.getZones(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var zone r53Types.HostedZone
|
|
|
|
// If the zone_id is specified in meta, use it.
|
|
if zoneID, ok := meta["zone_id"]; ok {
|
|
zone = r.zonesByID[zoneID]
|
|
return r.getZoneRecords(zone)
|
|
}
|
|
|
|
// fmt.Printf("DEBUG: ROUTE53 zones:\n")
|
|
// for i, j := range r.zonesByDomain {
|
|
// fmt.Printf(" %s: %v\n", i, aws.ToString(j.Id))
|
|
// }
|
|
|
|
// Otherwise, use the domain name to look up the zone.
|
|
if zone, ok := r.zonesByDomain[domain]; ok {
|
|
return r.getZoneRecords(zone)
|
|
}
|
|
|
|
// Not found there? Error.
|
|
return nil, errDomainNoExist{domain}
|
|
}
|
|
|
|
func (r *route53Provider) getZone(dc *models.DomainConfig) (r53Types.HostedZone, error) {
|
|
if err := r.getZones(); err != nil {
|
|
return r53Types.HostedZone{}, err
|
|
}
|
|
|
|
if zoneID, ok := dc.Metadata["zone_id"]; ok {
|
|
zone, ok := r.zonesByID[zoneID]
|
|
if !ok {
|
|
return r53Types.HostedZone{}, errZoneNoExist{zoneID}
|
|
}
|
|
return zone, nil
|
|
}
|
|
|
|
if zone, ok := r.zonesByDomain[dc.Name]; ok {
|
|
return zone, nil
|
|
}
|
|
|
|
return r53Types.HostedZone{}, errDomainNoExist{dc.Name}
|
|
}
|
|
|
|
func (r *route53Provider) getZoneRecords(zone r53Types.HostedZone) (models.Records, error) {
|
|
records, err := r.fetchRecordSets(zone.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
existingRecords := []*models.RecordConfig{}
|
|
for _, set := range records {
|
|
rts, err := nativeToRecords(set, unescape(zone.Name))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
existingRecords = append(existingRecords, rts...)
|
|
}
|
|
return existingRecords, nil
|
|
}
|
|
|
|
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
|
|
func (r *route53Provider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
|
|
zone, err := r.getZone(dc)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// update zone_id to current zone.id if not specified by the user
|
|
for _, want := range dc.Records {
|
|
if want.Type == "R53_ALIAS" && want.R53Alias["zone_id"] == "" {
|
|
want.R53Alias["zone_id"] = getZoneID(zone, want)
|
|
}
|
|
}
|
|
|
|
var corrections []*models.Correction
|
|
changes := []r53Types.Change{}
|
|
changeDesc := []string{} // TODO(tlim): This should be a [][]string so that we aren't joining strings until the last moment.
|
|
|
|
// Amazon Route53 is a "ByRecordSet" API.
|
|
// At each label:rtype pair, we either delete all records or UPSERT the desired records.
|
|
instructions, actualChangeCount, err := diff2.ByRecordSet(existingRecords, dc, nil)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
instructions = reorderInstructions(instructions)
|
|
var reports []*models.Correction
|
|
|
|
// wasReport := false
|
|
for _, inst := range instructions {
|
|
instNameFQDN := inst.Key.NameFQDN
|
|
instType := inst.Key.Type
|
|
var chg r53Types.Change
|
|
|
|
switch inst.Type {
|
|
case diff2.REPORT:
|
|
// REPORTs are held in a separate list so that they aren't part of the batching process.
|
|
reports = append(reports,
|
|
&models.Correction{
|
|
Msg: inst.MsgsJoined,
|
|
})
|
|
continue
|
|
|
|
case diff2.CREATE:
|
|
fallthrough
|
|
case diff2.CHANGE:
|
|
// To CREATE/CHANGE, build a new record set from the desired state and UPSERT it.
|
|
|
|
// Make the rrset to be UPSERTed:
|
|
var rrset *r53Types.ResourceRecordSet
|
|
if instType == "R53_ALIAS" || strings.HasPrefix(instType, "R53_ALIAS_") {
|
|
// A R53_ALIAS_* requires ResourceRecordSet to a single item, not a list.
|
|
if len(inst.New) != 1 {
|
|
log.Fatal("Only one R53_ALIAS_ permitted on a label")
|
|
}
|
|
rrset = aliasToRRSet(zone, inst.New[0])
|
|
rrset.Name = aws.String(instNameFQDN)
|
|
} else {
|
|
// Make a list of all the records to be installed at label:rtype
|
|
rrset = &r53Types.ResourceRecordSet{
|
|
Name: aws.String(instNameFQDN),
|
|
Type: r53Types.RRType(instType),
|
|
}
|
|
|
|
for _, r := range inst.New {
|
|
rr := r53Types.ResourceRecord{
|
|
Value: aws.String(r.GetTargetCombinedFunc(txtutil.EncodeQuoted)),
|
|
}
|
|
rrset.ResourceRecords = append(rrset.ResourceRecords, rr)
|
|
i := int64(r.TTL)
|
|
rrset.TTL = &i
|
|
}
|
|
}
|
|
chg = r53Types.Change{
|
|
Action: r53Types.ChangeActionUpsert,
|
|
ResourceRecordSet: rrset,
|
|
}
|
|
|
|
case diff2.DELETE:
|
|
rrset := inst.Old[0].Original.(r53Types.ResourceRecordSet) // The native record as downloaded via the API
|
|
chg = r53Types.Change{
|
|
Action: r53Types.ChangeActionDelete,
|
|
ResourceRecordSet: &rrset,
|
|
}
|
|
|
|
default:
|
|
panic(fmt.Sprintf("unhandled inst.Type %s", inst.Type))
|
|
}
|
|
|
|
changes = append(changes, chg)
|
|
changeDesc = append(changeDesc, inst.MsgsJoined)
|
|
}
|
|
|
|
addCorrection := func(msg string, req *r53.ChangeResourceRecordSetsInput) {
|
|
corrections = append(corrections,
|
|
&models.Correction{
|
|
Msg: msg,
|
|
F: func() error {
|
|
var err error
|
|
req.HostedZoneId = zone.Id
|
|
withRetry(func() error {
|
|
_, err = r.client.ChangeResourceRecordSets(context.Background(), req)
|
|
return err
|
|
})
|
|
return err
|
|
},
|
|
})
|
|
}
|
|
|
|
// Send the changes in as few API calls as possible.
|
|
batcher := newChangeBatcher(changes)
|
|
for batcher.Next() {
|
|
start, end := batcher.Batch()
|
|
batch := changes[start:end]
|
|
descBatchStr := strings.Join(changeDesc[start:end], "\n")
|
|
req := &r53.ChangeResourceRecordSetsInput{
|
|
ChangeBatch: &r53Types.ChangeBatch{Changes: batch},
|
|
}
|
|
addCorrection(descBatchStr, req)
|
|
}
|
|
if err := batcher.Err(); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return append(reports, corrections...), actualChangeCount, nil
|
|
}
|
|
|
|
// reorderInstructions returns changes reordered to comply with AWS's requirements:
|
|
// - The R43_ALIAS updates must come after records they refer to. To handle
|
|
// this, we simply move all R53_ALIAS instructions to the end of the list, thus
|
|
// guaranteeing they will happen after the records they refer to have been
|
|
// reated.
|
|
func reorderInstructions(changes diff2.ChangeList) diff2.ChangeList {
|
|
var main, tail diff2.ChangeList
|
|
for _, change := range changes {
|
|
// Reports should be early in the list.
|
|
// R53_ALIAS_ records should go to the tail.
|
|
if change.Type != diff2.REPORT && strings.HasPrefix(change.Key.Type, "R53_ALIAS_") {
|
|
tail = append(tail, change)
|
|
} else {
|
|
main = append(main, change)
|
|
}
|
|
}
|
|
return append(main, tail...)
|
|
// NB(tlim): This algorithm is O(n*2) but it is simple and usually only
|
|
// operates on very small lists.
|
|
}
|
|
|
|
func nativeToRecords(set r53Types.ResourceRecordSet, origin string) ([]*models.RecordConfig, error) {
|
|
results := []*models.RecordConfig{}
|
|
if set.AliasTarget != nil {
|
|
rc := &models.RecordConfig{
|
|
Type: "R53_ALIAS",
|
|
TTL: 300,
|
|
R53Alias: map[string]string{
|
|
"type": string(set.Type),
|
|
"zone_id": aws.ToString(set.AliasTarget.HostedZoneId),
|
|
"evaluate_target_health": strconv.FormatBool(set.AliasTarget.EvaluateTargetHealth),
|
|
},
|
|
}
|
|
rc.SetLabelFromFQDN(unescape(set.Name), origin)
|
|
if err := rc.SetTarget(aws.ToString(set.AliasTarget.DNSName)); err != nil {
|
|
return nil, err
|
|
}
|
|
// rc.Original stores a pointer to the original set for use by
|
|
// r53Types.ChangeActionDelete and anything else that needs the
|
|
// native record verbatim.
|
|
rc.Original = set
|
|
results = append(results, rc)
|
|
} else if set.TrafficPolicyInstanceId != nil {
|
|
// skip traffic policy records
|
|
} else {
|
|
for _, rec := range set.ResourceRecords {
|
|
switch rtype := set.Type; rtype {
|
|
case r53Types.RRTypeSoa:
|
|
continue
|
|
case r53Types.RRTypeSpf:
|
|
// route53 uses a custom record type for SPF
|
|
rtype = "TXT"
|
|
fallthrough
|
|
default:
|
|
rtypeString := string(rtype)
|
|
val := *rec.Value
|
|
|
|
// AWS Route53 has a bug. Sometimes it returns a target
|
|
// without a trailing dot. In this case we add the dot. It is
|
|
// not risky to "just add the dot" because this field never
|
|
// includes shortnames. That said, we only do it for certain
|
|
// record types where we can show the problem exists.
|
|
// 2022-02-23: NS records do NOT have this bug.
|
|
//
|
|
// NOTE: The dot is missing when the record is added via the
|
|
// AWS web console manually.
|
|
//
|
|
// The next "dnscontrol push" will update the record, even
|
|
// though it doesn't seem to be broken. This only happens once
|
|
// per record. Sadly the updates only fix the first record.
|
|
// So, if n records are affected by this bug, the next n
|
|
// pushes will be required to clean up all the records.
|
|
// Someone converting a new zone will see this issue for the
|
|
// first n pushes. It will seem odd but this is AWS's bug.
|
|
// The UPSERT command only fixes the first record, even if
|
|
// the UPSET received a list of corrections.
|
|
if rtypeString == "CNAME" || rtypeString == "MX" {
|
|
if !strings.HasSuffix(val, ".") {
|
|
val = val + "."
|
|
}
|
|
}
|
|
|
|
rc := &models.RecordConfig{TTL: uint32(aws.ToInt64(set.TTL))}
|
|
rc.SetLabelFromFQDN(unescape(set.Name), origin)
|
|
rc.Original = set
|
|
if err := rc.PopulateFromStringFunc(rtypeString, val, origin, txtutil.ParseQuoted); err != nil {
|
|
return nil, fmt.Errorf("unparsable record type=%q received from ROUTE53: %w", rtypeString, err)
|
|
}
|
|
|
|
results = append(results, rc)
|
|
}
|
|
}
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func aliasToRRSet(zone r53Types.HostedZone, r *models.RecordConfig) *r53Types.ResourceRecordSet {
|
|
target := r.GetTargetField()
|
|
zoneID := getZoneID(zone, r)
|
|
evalTargetHealth, err := strconv.ParseBool(r.R53Alias["evaluate_target_health"])
|
|
if err != nil {
|
|
evalTargetHealth = false
|
|
}
|
|
rrset := &r53Types.ResourceRecordSet{
|
|
Type: r53Types.RRType(r.R53Alias["type"]),
|
|
AliasTarget: &r53Types.AliasTarget{
|
|
DNSName: &target,
|
|
HostedZoneId: aws.String(zoneID),
|
|
EvaluateTargetHealth: evalTargetHealth,
|
|
},
|
|
}
|
|
return rrset
|
|
}
|
|
|
|
func getZoneID(zone r53Types.HostedZone, r *models.RecordConfig) string {
|
|
zoneID := r.R53Alias["zone_id"]
|
|
if zoneID == "" {
|
|
zoneID = aws.ToString(zone.Id)
|
|
}
|
|
return parseZoneID(zoneID)
|
|
}
|
|
|
|
/** Removes "/hostedzone/"" prefix from AWS ZoneId */
|
|
func parseZoneID(zoneID string) string {
|
|
return strings.TrimPrefix(zoneID, "/hostedzone/")
|
|
}
|
|
|
|
func (r *route53Provider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
|
corrections := []*models.Correction{}
|
|
actualSet, err := r.getRegistrarNameservers(&dc.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sort.Strings(actualSet)
|
|
actual := strings.Join(actualSet, ",")
|
|
|
|
expectedSet := []string{}
|
|
for _, ns := range dc.Nameservers {
|
|
expectedSet = append(expectedSet, ns.Name)
|
|
}
|
|
sort.Strings(expectedSet)
|
|
expected := strings.Join(expectedSet, ",")
|
|
|
|
if actual != expected {
|
|
return []*models.Correction{
|
|
{
|
|
Msg: fmt.Sprintf("Update nameservers %s -> %s", actual, expected),
|
|
F: func() error {
|
|
_, err := r.updateRegistrarNameservers(dc.Name, expectedSet)
|
|
return err
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
return corrections, nil
|
|
}
|
|
|
|
func (r *route53Provider) getRegistrarNameservers(domainName *string) ([]string, error) {
|
|
var domainDetail *r53d.GetDomainDetailOutput
|
|
var err error
|
|
withRetry(func() error {
|
|
domainDetail, err = r.registrar.GetDomainDetail(context.Background(), &r53d.GetDomainDetailInput{DomainName: domainName})
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nameservers := []string{}
|
|
for _, ns := range domainDetail.Nameservers {
|
|
nameservers = append(nameservers, aws.ToString(ns.Name))
|
|
}
|
|
|
|
return nameservers, nil
|
|
}
|
|
|
|
func (r *route53Provider) updateRegistrarNameservers(domainName string, nameservers []string) (*string, error) {
|
|
servers := make([]r53dTypes.Nameserver, len(nameservers))
|
|
for i := range nameservers {
|
|
servers[i] = r53dTypes.Nameserver{Name: aws.String(nameservers[i])}
|
|
}
|
|
var domainUpdate *r53d.UpdateDomainNameserversOutput
|
|
var err error
|
|
withRetry(func() error {
|
|
domainUpdate, err = r.registrar.UpdateDomainNameservers(context.Background(), &r53d.UpdateDomainNameserversInput{
|
|
DomainName: aws.String(domainName),
|
|
Nameservers: servers,
|
|
})
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return domainUpdate.OperationId, nil
|
|
}
|
|
|
|
func (r *route53Provider) fetchRecordSets(zoneID *string) ([]r53Types.ResourceRecordSet, error) {
|
|
if zoneID == nil || *zoneID == "" {
|
|
return nil, nil
|
|
}
|
|
var next *string
|
|
var nextType r53Types.RRType
|
|
var records []r53Types.ResourceRecordSet
|
|
for {
|
|
listInput := &r53.ListResourceRecordSetsInput{
|
|
HostedZoneId: zoneID,
|
|
StartRecordName: next,
|
|
StartRecordType: nextType,
|
|
MaxItems: aws.Int32(100),
|
|
}
|
|
var list *r53.ListResourceRecordSetsOutput
|
|
var err error
|
|
withRetry(func() error {
|
|
list, err = r.client.ListResourceRecordSets(context.Background(), listInput)
|
|
return err
|
|
})
|
|
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) EnsureZoneExists(domain string) error {
|
|
if err := r.getZones(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, ok := r.zonesByDomain[domain]; ok {
|
|
return nil
|
|
}
|
|
if r.delegationSet != nil {
|
|
printer.Printf("Adding zone for %s to route 53 account with delegationSet %s\n", domain, *r.delegationSet)
|
|
} else {
|
|
printer.Printf("Adding zone for %s to route 53 account\n", domain)
|
|
}
|
|
in := &r53.CreateHostedZoneInput{
|
|
Name: &domain,
|
|
DelegationSetId: r.delegationSet,
|
|
CallerReference: aws.String(strconv.FormatInt(time.Now().UnixNano(), 10)),
|
|
}
|
|
|
|
// reset zone cache
|
|
r.zonesByDomain = nil
|
|
r.zonesByID = nil
|
|
|
|
var err error
|
|
withRetry(func() error {
|
|
_, err := r.client.CreateHostedZone(context.Background(), in)
|
|
return err
|
|
})
|
|
return err
|
|
}
|
|
|
|
// changeBatcher takes a set of r53Types.Changes and turns them into a series of
|
|
// batches that meet the limits of the ChangeResourceRecordSets API.
|
|
//
|
|
// See also: https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-requests-changeresourcerecordsets
|
|
type changeBatcher struct {
|
|
changes []r53Types.Change
|
|
|
|
maxSize int // Max records per request.
|
|
maxChars int // Max record value characters per request.
|
|
|
|
start, end int // Cursors into changes.
|
|
err error // Populated by Next.
|
|
}
|
|
|
|
// newChangeBatcher returns a new changeBatcher.
|
|
func newChangeBatcher(changes []r53Types.Change) *changeBatcher {
|
|
return &changeBatcher{
|
|
changes: changes,
|
|
maxSize: 1000, // "A request cannot contain more than 1,000 ResourceRecord elements."
|
|
maxChars: 32000, // "The sum of the number of characters (including spaces) in all Value elements in a request cannot exceed 32,000 characters."
|
|
}
|
|
}
|
|
|
|
// Next returns true if there is another batch of Changes.
|
|
// It returns false if there are no more batches or an error occurred.
|
|
func (b *changeBatcher) Next() bool {
|
|
if b.end >= len(b.changes) || b.err != nil {
|
|
return false
|
|
}
|
|
|
|
start, end := b.end, b.end
|
|
var (
|
|
reqSize int
|
|
reqChars int
|
|
)
|
|
for end < len(b.changes) {
|
|
c := &b.changes[end]
|
|
|
|
// Check that we won't exceed 1000 ResourceRecords in the request.
|
|
if c.ResourceRecordSet == nil {
|
|
end++
|
|
continue
|
|
}
|
|
|
|
rrsetSize := len(c.ResourceRecordSet.ResourceRecords)
|
|
if c.Action == r53Types.ChangeActionUpsert {
|
|
// "When the value of the Action element is UPSERT, each ResourceRecord element is counted twice."
|
|
rrsetSize *= 2
|
|
}
|
|
if newReqSize := reqSize + rrsetSize; newReqSize > b.maxSize {
|
|
break
|
|
} else {
|
|
reqSize = newReqSize
|
|
}
|
|
|
|
// Check that we won't exceed 32000 Value characters in the request.
|
|
var rrsetChars int
|
|
for _, rr := range c.ResourceRecordSet.ResourceRecords {
|
|
rrsetChars += utf8.RuneCountInString(aws.ToString(rr.Value))
|
|
}
|
|
if c.Action == r53Types.ChangeActionUpsert {
|
|
// "When the value of the Action element is UPSERT, each character in a Value element is counted twice."
|
|
rrsetChars *= 2
|
|
}
|
|
if newReqChars := reqChars + rrsetChars; newReqChars > b.maxChars {
|
|
break
|
|
} else {
|
|
reqChars = newReqChars
|
|
}
|
|
|
|
end++
|
|
}
|
|
|
|
if start == end {
|
|
b.err = errors.New("could not create ChangeResourceRecordSets request within AWS API limits")
|
|
return false
|
|
}
|
|
|
|
b.start = start
|
|
b.end = end
|
|
|
|
return true
|
|
}
|
|
|
|
// Batch returns the current batch. It should only be called
|
|
// after Next returns true.
|
|
func (b *changeBatcher) Batch() (start, end int) {
|
|
return b.start, b.end
|
|
}
|
|
|
|
// Err returns the error encountered during the previous call to Next.
|
|
func (b *changeBatcher) Err() error {
|
|
return b.err
|
|
}
|