mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-05-11 05:55:12 +00:00
ROUTE53: Adopt aws-sdk-go-v2 (#1321)
* Switch to aws-sdk-go-v2 AWS has released v2 of their SDK for Go. See: https://aws.github.io/aws-sdk-go-v2/ One big advantage of this is no longer needing to export the `AWS_SDK_LOAD_CONFIG=1` env var when using named profiles. * Update integration test README * Reenable pager601 and pager1201 integration tests for AWS Route53 * Implement intelligent batching for Route53 record changes The AWS Route53 API for batch record changes limits the request size to the smaller of: - 1000 records. - 32000 characters total for record values. Also UPSERTs count as double (a DELETE and then a CREATE). This commit changes how the record ChangeBatches are created to respect these limits. * Remove old comments Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package route53
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -8,12 +9,15 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"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"
|
||||
r53d "github.com/aws/aws-sdk-go/service/route53domains"
|
||||
"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"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
|
||||
@@ -22,12 +26,12 @@ import (
|
||||
)
|
||||
|
||||
type route53Provider struct {
|
||||
client *r53.Route53
|
||||
registrar *r53d.Route53Domains
|
||||
client *r53.Client
|
||||
registrar *r53d.Client
|
||||
delegationSet *string
|
||||
zonesById map[string]*r53.HostedZone
|
||||
zonesByDomain map[string]*r53.HostedZone
|
||||
originalRecords []*r53.ResourceRecordSet
|
||||
zonesById map[string]r53Types.HostedZone
|
||||
zonesByDomain map[string]r53Types.HostedZone
|
||||
originalRecords []r53Types.ResourceRecordSet
|
||||
}
|
||||
|
||||
func newRoute53Reg(conf map[string]string) (providers.Registrar, error) {
|
||||
@@ -39,28 +43,31 @@ func newRoute53Dsp(conf map[string]string, metadata json.RawMessage) (providers.
|
||||
}
|
||||
|
||||
func newRoute53(m map[string]string, metadata json.RawMessage) (*route53Provider, error) {
|
||||
keyID, secretKey, tokenID := m["KeyId"], m["SecretKey"], m["Token"]
|
||||
|
||||
// Route53 uses a global endpoint and route53domains
|
||||
// currently only has a single regional endpoint in us-east-1
|
||||
// http://docs.aws.amazon.com/general/latest/gr/rande.html#r53_region
|
||||
config := &aws.Config{
|
||||
Region: aws.String("us-east-1"),
|
||||
optFns := []func(*config.LoadOptions) error{
|
||||
// Route53 uses a global endpoint and route53domains
|
||||
// currently only has a single regional endpoint in us-east-1
|
||||
// http://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 != "" {
|
||||
config.Credentials = credentials.NewStaticCredentials(keyID, secretKey, tokenID)
|
||||
optFns = append(optFns, config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(keyID, secretKey, tokenID)))
|
||||
}
|
||||
|
||||
config, err := config.LoadDefaultConfig(context.Background(), optFns...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sess := session.Must(session.NewSession(config))
|
||||
|
||||
var dls *string
|
||||
if val, ok := m["DelegationSet"]; ok {
|
||||
fmt.Printf("ROUTE53 DelegationSet %s configured\n", val)
|
||||
dls = sPtr(val)
|
||||
dls = aws.String(val)
|
||||
}
|
||||
api := &route53Provider{client: r53.New(sess), registrar: r53d.New(sess), delegationSet: dls}
|
||||
err := api.getZones()
|
||||
api := &route53Provider{client: r53.NewFromConfig(config), registrar: r53d.NewFromConfig(config), delegationSet: dls}
|
||||
err = api.getZones()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -89,10 +96,6 @@ func init() {
|
||||
providers.RegisterCustomRecordType("R53_ALIAS", "ROUTE53", "")
|
||||
}
|
||||
|
||||
func sPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func withRetry(f func() error) {
|
||||
const maxRetries = 23
|
||||
// TODO: exponential backoff
|
||||
@@ -128,14 +131,14 @@ func (r *route53Provider) ListZones() ([]string, error) {
|
||||
|
||||
func (r *route53Provider) getZones() error {
|
||||
var nextMarker *string
|
||||
r.zonesByDomain = make(map[string]*r53.HostedZone)
|
||||
r.zonesById = make(map[string]*r53.HostedZone)
|
||||
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(inp)
|
||||
out, err = r.client.ListHostedZones(context.Background(), inp)
|
||||
return err
|
||||
})
|
||||
if err != nil && strings.Contains(err.Error(), "is not authorized") {
|
||||
@@ -144,9 +147,9 @@ func (r *route53Provider) getZones() error {
|
||||
return err
|
||||
}
|
||||
for _, z := range out.HostedZones {
|
||||
domain := strings.TrimSuffix(*z.Name, ".")
|
||||
domain := strings.TrimSuffix(aws.ToString(z.Name), ".")
|
||||
r.zonesByDomain[domain] = z
|
||||
r.zonesById[parseZoneId(*z.Id)] = z
|
||||
r.zonesById[parseZoneId(aws.ToString(z.Id))] = z
|
||||
}
|
||||
if out.NextMarker != nil {
|
||||
nextMarker = out.NextMarker
|
||||
@@ -182,7 +185,7 @@ func (r *route53Provider) GetNameservers(domain string) ([]*models.Nameserver, e
|
||||
var z *r53.GetHostedZoneOutput
|
||||
var err error
|
||||
withRetry(func() error {
|
||||
z, err = r.client.GetHostedZone(&r53.GetHostedZoneInput{Id: zone.Id})
|
||||
z, err = r.client.GetHostedZone(context.Background(), &r53.GetHostedZoneInput{Id: zone.Id})
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
@@ -191,9 +194,7 @@ func (r *route53Provider) GetNameservers(domain string) ([]*models.Nameserver, e
|
||||
|
||||
var nss []string
|
||||
if z.DelegationSet != nil {
|
||||
for _, nsPtr := range z.DelegationSet.NameServers {
|
||||
nss = append(nss, *nsPtr)
|
||||
}
|
||||
nss = z.DelegationSet.NameServers
|
||||
}
|
||||
return models.ToNameservers(nss)
|
||||
}
|
||||
@@ -206,11 +207,11 @@ func (r *route53Provider) GetZoneRecords(domain string) (models.Records, error)
|
||||
return nil, errDomainNoExist{domain}
|
||||
}
|
||||
|
||||
func (r *route53Provider) getZone(dc *models.DomainConfig) (*r53.HostedZone, error) {
|
||||
func (r *route53Provider) getZone(dc *models.DomainConfig) (r53Types.HostedZone, error) {
|
||||
if zoneId, ok := dc.Metadata["zone_id"]; ok {
|
||||
zone, ok := r.zonesById[zoneId]
|
||||
if !ok {
|
||||
return nil, errZoneNoExist{zoneId}
|
||||
return r53Types.HostedZone{}, errZoneNoExist{zoneId}
|
||||
}
|
||||
return zone, nil
|
||||
}
|
||||
@@ -219,10 +220,10 @@ func (r *route53Provider) getZone(dc *models.DomainConfig) (*r53.HostedZone, err
|
||||
return zone, nil
|
||||
}
|
||||
|
||||
return nil, errDomainNoExist{dc.Name}
|
||||
return r53Types.HostedZone{}, errDomainNoExist{dc.Name}
|
||||
}
|
||||
|
||||
func (r *route53Provider) getZoneRecords(zone *r53.HostedZone) (models.Records, error) {
|
||||
func (r *route53Provider) getZoneRecords(zone r53Types.HostedZone) (models.Records, error) {
|
||||
records, err := r.fetchRecordSets(zone.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -321,9 +322,9 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
|
||||
// we collect all changes into one of two categories now:
|
||||
// pure deletions where we delete an entire record set,
|
||||
// or changes where we upsert an entire record set.
|
||||
dels := []*r53.Change{}
|
||||
dels := []r53Types.Change{}
|
||||
delDesc := []string{}
|
||||
changes := []*r53.Change{}
|
||||
changes := []r53Types.Change{}
|
||||
changeDesc := []string{}
|
||||
|
||||
for _, k := range updateOrder {
|
||||
@@ -332,22 +333,26 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
|
||||
// indicates we should delete all records at that key.
|
||||
if len(recs) == 0 {
|
||||
// To delete, we submit the original resource set we got from r53.
|
||||
var rrset *r53.ResourceRecordSet
|
||||
var (
|
||||
rrset r53Types.ResourceRecordSet
|
||||
found bool
|
||||
)
|
||||
// Find the original resource set:
|
||||
for _, r := range r.originalRecords {
|
||||
if unescape(r.Name) == k.NameFQDN && (*r.Type == k.Type || k.Type == "R53_ALIAS_"+*r.Type) {
|
||||
if unescape(r.Name) == k.NameFQDN && (string(r.Type) == k.Type || k.Type == "R53_ALIAS_"+string(r.Type)) {
|
||||
rrset = r
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if rrset == nil {
|
||||
if !found {
|
||||
// This should not happen.
|
||||
return nil, fmt.Errorf("no record set found to delete. Name: '%s'. Type: '%s'", k.NameFQDN, k.Type)
|
||||
}
|
||||
// Assemble the change and add it to the list:
|
||||
chg := &r53.Change{
|
||||
Action: sPtr("DELETE"),
|
||||
ResourceRecordSet: rrset,
|
||||
chg := r53Types.Change{
|
||||
Action: r53Types.ChangeActionDelete,
|
||||
ResourceRecordSet: &rrset,
|
||||
}
|
||||
dels = append(dels, chg)
|
||||
delDesc = append(delDesc, strings.Join(namesToUpdate[k], "\n"))
|
||||
@@ -363,10 +368,10 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
|
||||
}
|
||||
for _, r := range recs {
|
||||
rrset := aliasToRRSet(zone, r)
|
||||
rrset.Name = sPtr(k.NameFQDN)
|
||||
rrset.Name = aws.String(k.NameFQDN)
|
||||
// Assemble the change and add it to the list:
|
||||
chg := &r53.Change{
|
||||
Action: sPtr("UPSERT"),
|
||||
chg := r53Types.Change{
|
||||
Action: r53Types.ChangeActionUpsert,
|
||||
ResourceRecordSet: rrset,
|
||||
}
|
||||
changes = append(changes, chg)
|
||||
@@ -374,22 +379,22 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
|
||||
}
|
||||
} else {
|
||||
// All other keys combine their updates into one rrset:
|
||||
rrset := &r53.ResourceRecordSet{
|
||||
Name: sPtr(k.NameFQDN),
|
||||
Type: sPtr(k.Type),
|
||||
rrset := &r53Types.ResourceRecordSet{
|
||||
Name: aws.String(k.NameFQDN),
|
||||
Type: r53Types.RRType(k.Type),
|
||||
}
|
||||
for _, r := range recs {
|
||||
val := r.GetTargetCombined()
|
||||
rr := &r53.ResourceRecord{
|
||||
Value: &val,
|
||||
rr := r53Types.ResourceRecord{
|
||||
Value: aws.String(val),
|
||||
}
|
||||
rrset.ResourceRecords = append(rrset.ResourceRecords, rr)
|
||||
i := int64(r.TTL)
|
||||
rrset.TTL = &i // TODO: make sure that ttls are consistent within a set
|
||||
}
|
||||
// Assemble the change and add it to the list:
|
||||
chg := &r53.Change{
|
||||
Action: sPtr("UPSERT"),
|
||||
chg := r53Types.Change{
|
||||
Action: r53Types.ChangeActionUpsert,
|
||||
ResourceRecordSet: rrset,
|
||||
}
|
||||
changes = append(changes, chg)
|
||||
@@ -407,7 +412,7 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
|
||||
var err error
|
||||
req.HostedZoneId = zone.Id
|
||||
withRetry(func() error {
|
||||
_, err = r.client.ChangeResourceRecordSets(req)
|
||||
_, err = r.client.ChangeResourceRecordSets(context.Background(), req)
|
||||
return err
|
||||
})
|
||||
return err
|
||||
@@ -415,75 +420,67 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
|
||||
})
|
||||
}
|
||||
|
||||
getBatchSize := func(size, max int) int {
|
||||
if size > max {
|
||||
return max
|
||||
batcher := newChangeBatcher(dels)
|
||||
for batcher.Next() {
|
||||
start, end := batcher.Batch()
|
||||
batch := dels[start:end]
|
||||
descBatchStr := "\n" + strings.Join(delDesc[start:end], "\n") + "\n"
|
||||
req := &r53.ChangeResourceRecordSetsInput{
|
||||
ChangeBatch: &r53Types.ChangeBatch{Changes: batch},
|
||||
}
|
||||
return size
|
||||
addCorrection(descBatchStr, req)
|
||||
}
|
||||
if err := batcher.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for len(dels) > 0 {
|
||||
batchSize := getBatchSize(len(dels), 1000)
|
||||
batch := dels[:batchSize]
|
||||
dels = dels[batchSize:]
|
||||
delDescBatch := delDesc[:batchSize]
|
||||
delDesc = delDesc[batchSize:]
|
||||
|
||||
delDescBatchStr := "\n" + strings.Join(delDescBatch, "\n") + "\n"
|
||||
|
||||
delReq := &r53.ChangeResourceRecordSetsInput{
|
||||
ChangeBatch: &r53.ChangeBatch{Changes: batch},
|
||||
batcher = newChangeBatcher(changes)
|
||||
for batcher.Next() {
|
||||
start, end := batcher.Batch()
|
||||
batch := changes[start:end]
|
||||
descBatchStr := "\n" + strings.Join(changeDesc[start:end], "\n") + "\n"
|
||||
req := &r53.ChangeResourceRecordSetsInput{
|
||||
ChangeBatch: &r53Types.ChangeBatch{Changes: batch},
|
||||
}
|
||||
addCorrection(delDescBatchStr, delReq)
|
||||
addCorrection(descBatchStr, req)
|
||||
}
|
||||
|
||||
for len(changes) > 0 {
|
||||
batchSize := getBatchSize(len(changes), 500)
|
||||
batch := changes[:batchSize]
|
||||
changes = changes[batchSize:]
|
||||
changeDescBatch := changeDesc[:batchSize]
|
||||
changeDesc = changeDesc[batchSize:]
|
||||
changeDescBatchStr := "\n" + strings.Join(changeDescBatch, "\n") + "\n"
|
||||
|
||||
changeReq := &r53.ChangeResourceRecordSetsInput{
|
||||
ChangeBatch: &r53.ChangeBatch{Changes: batch},
|
||||
}
|
||||
addCorrection(changeDescBatchStr, changeReq)
|
||||
if err := batcher.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return corrections, nil
|
||||
|
||||
}
|
||||
|
||||
func nativeToRecords(set *r53.ResourceRecordSet, origin string) ([]*models.RecordConfig, error) {
|
||||
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": *set.Type,
|
||||
"zone_id": *set.AliasTarget.HostedZoneId,
|
||||
"type": string(set.Type),
|
||||
"zone_id": aws.ToString(set.AliasTarget.HostedZoneId),
|
||||
},
|
||||
}
|
||||
rc.SetLabelFromFQDN(unescape(set.Name), origin)
|
||||
rc.SetTarget(aws.StringValue(set.AliasTarget.DNSName))
|
||||
rc.SetTarget(aws.ToString(set.AliasTarget.DNSName))
|
||||
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 "SOA":
|
||||
switch rtype := set.Type; rtype {
|
||||
case r53Types.RRTypeSoa:
|
||||
continue
|
||||
case "SPF":
|
||||
case r53Types.RRTypeSpf:
|
||||
// route53 uses a custom record type for SPF
|
||||
rtype = "TXT"
|
||||
fallthrough
|
||||
default:
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.TTL)}
|
||||
rc := &models.RecordConfig{TTL: uint32(aws.ToInt64(set.TTL))}
|
||||
rc.SetLabelFromFQDN(unescape(set.Name), origin)
|
||||
if err := rc.PopulateFromString(rtype, *rec.Value, origin); err != nil {
|
||||
if err := rc.PopulateFromString(string(rtype), *rec.Value, origin); err != nil {
|
||||
return nil, fmt.Errorf("unparsable record received from R53: %w", err)
|
||||
}
|
||||
results = append(results, rc)
|
||||
@@ -500,25 +497,24 @@ func getAliasMap(r *models.RecordConfig) map[string]string {
|
||||
return r.R53Alias
|
||||
}
|
||||
|
||||
func aliasToRRSet(zone *r53.HostedZone, r *models.RecordConfig) *r53.ResourceRecordSet {
|
||||
func aliasToRRSet(zone r53Types.HostedZone, r *models.RecordConfig) *r53Types.ResourceRecordSet {
|
||||
target := r.GetTargetField()
|
||||
zoneID := getZoneID(zone, r)
|
||||
targetHealth := false
|
||||
rrset := &r53.ResourceRecordSet{
|
||||
Type: sPtr(r.R53Alias["type"]),
|
||||
AliasTarget: &r53.AliasTarget{
|
||||
rrset := &r53Types.ResourceRecordSet{
|
||||
Type: r53Types.RRType(r.R53Alias["type"]),
|
||||
AliasTarget: &r53Types.AliasTarget{
|
||||
DNSName: &target,
|
||||
HostedZoneId: aws.String(zoneID),
|
||||
EvaluateTargetHealth: &targetHealth,
|
||||
EvaluateTargetHealth: false,
|
||||
},
|
||||
}
|
||||
return rrset
|
||||
}
|
||||
|
||||
func getZoneID(zone *r53.HostedZone, r *models.RecordConfig) string {
|
||||
func getZoneID(zone r53Types.HostedZone, r *models.RecordConfig) string {
|
||||
zoneID := r.R53Alias["zone_id"]
|
||||
if zoneID == "" {
|
||||
zoneID = aws.StringValue(zone.Id)
|
||||
zoneID = aws.ToString(zone.Id)
|
||||
}
|
||||
return parseZoneId(zoneID)
|
||||
}
|
||||
@@ -566,7 +562,7 @@ func (r *route53Provider) getRegistrarNameservers(domainName *string) ([]string,
|
||||
var domainDetail *r53d.GetDomainDetailOutput
|
||||
var err error
|
||||
withRetry(func() error {
|
||||
domainDetail, err = r.registrar.GetDomainDetail(&r53d.GetDomainDetailInput{DomainName: domainName})
|
||||
domainDetail, err = r.registrar.GetDomainDetail(context.Background(), &r53d.GetDomainDetailInput{DomainName: domainName})
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
@@ -575,22 +571,24 @@ func (r *route53Provider) getRegistrarNameservers(domainName *string) ([]string,
|
||||
|
||||
nameservers := []string{}
|
||||
for _, ns := range domainDetail.Nameservers {
|
||||
nameservers = append(nameservers, *ns.Name)
|
||||
nameservers = append(nameservers, aws.ToString(ns.Name))
|
||||
}
|
||||
|
||||
return nameservers, nil
|
||||
}
|
||||
|
||||
func (r *route53Provider) updateRegistrarNameservers(domainName string, nameservers []string) (*string, error) {
|
||||
servers := []*r53d.Nameserver{}
|
||||
servers := make([]r53dTypes.Nameserver, len(nameservers))
|
||||
for i := range nameservers {
|
||||
servers = append(servers, &r53d.Nameserver{Name: &nameservers[i]})
|
||||
servers[i] = r53dTypes.Nameserver{Name: aws.String(nameservers[i])}
|
||||
}
|
||||
var domainUpdate *r53d.UpdateDomainNameserversOutput
|
||||
var err error
|
||||
withRetry(func() error {
|
||||
domainUpdate, err = r.registrar.UpdateDomainNameservers(&r53d.UpdateDomainNameserversInput{
|
||||
DomainName: &domainName, Nameservers: servers})
|
||||
domainUpdate, err = r.registrar.UpdateDomainNameservers(context.Background(), &r53d.UpdateDomainNameserversInput{
|
||||
DomainName: aws.String(domainName),
|
||||
Nameservers: servers,
|
||||
})
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
@@ -600,24 +598,24 @@ func (r *route53Provider) updateRegistrarNameservers(domainName string, nameserv
|
||||
return domainUpdate.OperationId, nil
|
||||
}
|
||||
|
||||
func (r *route53Provider) fetchRecordSets(zoneID *string) ([]*r53.ResourceRecordSet, error) {
|
||||
func (r *route53Provider) fetchRecordSets(zoneID *string) ([]r53Types.ResourceRecordSet, error) {
|
||||
if zoneID == nil || *zoneID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var next *string
|
||||
var nextType *string
|
||||
var records []*r53.ResourceRecordSet
|
||||
var nextType r53Types.RRType
|
||||
var records []r53Types.ResourceRecordSet
|
||||
for {
|
||||
listInput := &r53.ListResourceRecordSetsInput{
|
||||
HostedZoneId: zoneID,
|
||||
StartRecordName: next,
|
||||
StartRecordType: nextType,
|
||||
MaxItems: sPtr("100"),
|
||||
MaxItems: aws.Int32(100),
|
||||
}
|
||||
var list *r53.ListResourceRecordSetsOutput
|
||||
var err error
|
||||
withRetry(func() error {
|
||||
list, err = r.client.ListResourceRecordSets(listInput)
|
||||
list, err = r.client.ListResourceRecordSets(context.Background(), listInput)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
@@ -657,12 +655,102 @@ func (r *route53Provider) EnsureDomainExists(domain string) error {
|
||||
in := &r53.CreateHostedZoneInput{
|
||||
Name: &domain,
|
||||
DelegationSetId: r.delegationSet,
|
||||
CallerReference: sPtr(fmt.Sprint(time.Now().UnixNano())),
|
||||
CallerReference: aws.String(fmt.Sprint(time.Now().UnixNano())),
|
||||
}
|
||||
var err error
|
||||
withRetry(func() error {
|
||||
_, err := r.client.CreateHostedZone(in)
|
||||
_, 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.
|
||||
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
|
||||
}
|
||||
|
Reference in New Issue
Block a user