cmd/devp2p: submit Route53 changes in batches (#20524)
This change works around the 32k RDATA character limit per change request and fixes several issues in the deployer which prevented it from working for our production trees.
This commit is contained in:
@ -19,6 +19,7 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -31,6 +32,10 @@ import (
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
// The Route53 limits change sets to this size. DNS changes need to be split
|
||||
// up into multiple batches to work around the limit.
|
||||
const route53ChangeLimit = 30000
|
||||
|
||||
var (
|
||||
route53AccessKeyFlag = cli.StringFlag{
|
||||
Name: "access-key-id",
|
||||
@ -53,6 +58,11 @@ type route53Client struct {
|
||||
zoneID string
|
||||
}
|
||||
|
||||
type recordSet struct {
|
||||
values []string
|
||||
ttl int64
|
||||
}
|
||||
|
||||
// newRoute53Client sets up a Route53 API client from command line flags.
|
||||
func newRoute53Client(ctx *cli.Context) *route53Client {
|
||||
akey := ctx.String(route53AccessKeyFlag.Name)
|
||||
@ -78,31 +88,39 @@ func (c *route53Client) deploy(name string, t *dnsdisc.Tree) error {
|
||||
}
|
||||
|
||||
// Compute DNS changes.
|
||||
records := t.ToTXT(name)
|
||||
changes, err := c.computeChanges(name, records)
|
||||
existing, err := c.collectRecords(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info(fmt.Sprintf("Found %d TXT records", len(existing)))
|
||||
|
||||
records := t.ToTXT(name)
|
||||
changes := c.computeChanges(name, records, existing)
|
||||
if len(changes) == 0 {
|
||||
log.Info("No DNS changes needed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Submit change request.
|
||||
log.Info(fmt.Sprintf("Submitting %d changes to Route53", len(changes)))
|
||||
batch := new(route53.ChangeBatch)
|
||||
batch.SetChanges(changes)
|
||||
batch.SetComment(fmt.Sprintf("enrtree update of %s at seq %d", name, t.Seq()))
|
||||
req := &route53.ChangeResourceRecordSetsInput{HostedZoneId: &c.zoneID, ChangeBatch: batch}
|
||||
resp, err := c.api.ChangeResourceRecordSets(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Submit change batches.
|
||||
batches := splitChanges(changes, route53ChangeLimit)
|
||||
for i, changes := range batches {
|
||||
log.Info(fmt.Sprintf("Submitting %d changes to Route53", len(changes)))
|
||||
batch := new(route53.ChangeBatch)
|
||||
batch.SetChanges(changes)
|
||||
batch.SetComment(fmt.Sprintf("enrtree update %d/%d of %s at seq %d", i+1, len(batches), name, t.Seq()))
|
||||
req := &route53.ChangeResourceRecordSetsInput{HostedZoneId: &c.zoneID, ChangeBatch: batch}
|
||||
resp, err := c.api.ChangeResourceRecordSets(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for the change to be applied.
|
||||
log.Info(fmt.Sprintf("Waiting for change request %s", *resp.ChangeInfo.Id))
|
||||
wreq := &route53.GetChangeInput{Id: resp.ChangeInfo.Id}
|
||||
return c.api.WaitUntilResourceRecordSetsChanged(wreq)
|
||||
log.Info(fmt.Sprintf("Waiting for change request %s", *resp.ChangeInfo.Id))
|
||||
wreq := &route53.GetChangeInput{Id: resp.ChangeInfo.Id}
|
||||
if err := c.api.WaitUntilResourceRecordSetsChanged(wreq); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkZone verifies zone information for the given domain.
|
||||
@ -137,7 +155,7 @@ func (c *route53Client) findZoneID(name string) (string, error) {
|
||||
}
|
||||
|
||||
// computeChanges creates DNS changes for the given record.
|
||||
func (c *route53Client) computeChanges(name string, records map[string]string) ([]*route53.Change, error) {
|
||||
func (c *route53Client) computeChanges(name string, records map[string]string, existing map[string]recordSet) []*route53.Change {
|
||||
// Convert all names to lowercase.
|
||||
lrecords := make(map[string]string, len(records))
|
||||
for name, r := range records {
|
||||
@ -145,22 +163,15 @@ func (c *route53Client) computeChanges(name string, records map[string]string) (
|
||||
}
|
||||
records = lrecords
|
||||
|
||||
// Get existing records.
|
||||
existing, err := c.collectRecords(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Info(fmt.Sprintf("Found %d TXT records", len(existing)))
|
||||
|
||||
var changes []*route53.Change
|
||||
for path, val := range records {
|
||||
ttl := 1
|
||||
ttl := int64(rootTTL)
|
||||
if path != name {
|
||||
ttl = 2147483647
|
||||
ttl = int64(treeNodeTTL)
|
||||
}
|
||||
|
||||
prevRecords, exists := existing[path]
|
||||
prevValue := combineTXT(prevRecords)
|
||||
prevValue := combineTXT(prevRecords.values)
|
||||
if !exists {
|
||||
// Entry is unknown, push a new one
|
||||
log.Info(fmt.Sprintf("Creating %s = %q", path, val))
|
||||
@ -175,32 +186,76 @@ func (c *route53Client) computeChanges(name string, records map[string]string) (
|
||||
}
|
||||
|
||||
// Iterate over the old records and delete anything stale.
|
||||
for path, values := range existing {
|
||||
for path, set := range existing {
|
||||
if _, ok := records[path]; ok {
|
||||
continue
|
||||
}
|
||||
// Stale entry, nuke it.
|
||||
log.Info(fmt.Sprintf("Deleting %s = %q", path, combineTXT(values)))
|
||||
changes = append(changes, newTXTChange("DELETE", path, 1, values))
|
||||
log.Info(fmt.Sprintf("Deleting %s = %q", path, combineTXT(set.values)))
|
||||
changes = append(changes, newTXTChange("DELETE", path, set.ttl, set.values))
|
||||
}
|
||||
return changes, nil
|
||||
|
||||
sortChanges(changes)
|
||||
return changes
|
||||
}
|
||||
|
||||
// sortChanges ensures DNS changes are in leaf-added -> root-changed -> leaf-deleted order.
|
||||
func sortChanges(changes []*route53.Change) {
|
||||
score := map[string]int{"CREATE": 1, "UPSERT": 2, "DELETE": 3}
|
||||
sort.Slice(changes, func(i, j int) bool {
|
||||
if *changes[i].Action == *changes[j].Action {
|
||||
return *changes[i].ResourceRecordSet.Name < *changes[j].ResourceRecordSet.Name
|
||||
}
|
||||
return score[*changes[i].Action] < score[*changes[j].Action]
|
||||
})
|
||||
}
|
||||
|
||||
// splitChanges splits up DNS changes such that each change batch
|
||||
// is smaller than the given RDATA limit.
|
||||
func splitChanges(changes []*route53.Change, limit int) [][]*route53.Change {
|
||||
var batches [][]*route53.Change
|
||||
var batchSize int
|
||||
for _, ch := range changes {
|
||||
// Start new batch if this change pushes the current one over the limit.
|
||||
size := changeSize(ch)
|
||||
if len(batches) == 0 || batchSize+size > limit {
|
||||
batches = append(batches, nil)
|
||||
batchSize = 0
|
||||
}
|
||||
batches[len(batches)-1] = append(batches[len(batches)-1], ch)
|
||||
batchSize += size
|
||||
}
|
||||
return batches
|
||||
}
|
||||
|
||||
// changeSize returns the RDATA size of a DNS change.
|
||||
func changeSize(ch *route53.Change) int {
|
||||
size := 0
|
||||
for _, rr := range ch.ResourceRecordSet.ResourceRecords {
|
||||
if rr.Value != nil {
|
||||
size += len(*rr.Value)
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// collectRecords collects all TXT records below the given name.
|
||||
func (c *route53Client) collectRecords(name string) (map[string][]string, error) {
|
||||
func (c *route53Client) collectRecords(name string) (map[string]recordSet, error) {
|
||||
log.Info(fmt.Sprintf("Retrieving existing TXT records on %s (%s)", name, c.zoneID))
|
||||
var req route53.ListResourceRecordSetsInput
|
||||
req.SetHostedZoneId(c.zoneID)
|
||||
existing := make(map[string][]string)
|
||||
existing := make(map[string]recordSet)
|
||||
err := c.api.ListResourceRecordSetsPages(&req, func(resp *route53.ListResourceRecordSetsOutput, last bool) bool {
|
||||
for _, set := range resp.ResourceRecordSets {
|
||||
if !isSubdomain(*set.Name, name) || *set.Type != "TXT" {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSuffix(*set.Name, ".")
|
||||
s := recordSet{ttl: *set.TTL}
|
||||
for _, rec := range set.ResourceRecords {
|
||||
existing[name] = append(existing[name], *rec.Value)
|
||||
s.values = append(s.values, *rec.Value)
|
||||
}
|
||||
name := strings.TrimSuffix(*set.Name, ".")
|
||||
existing[name] = s
|
||||
}
|
||||
return true
|
||||
})
|
||||
@ -208,7 +263,7 @@ func (c *route53Client) collectRecords(name string) (map[string][]string, error)
|
||||
}
|
||||
|
||||
// newTXTChange creates a change to a TXT record.
|
||||
func newTXTChange(action, name string, ttl int, values []string) *route53.Change {
|
||||
func newTXTChange(action, name string, ttl int64, values []string) *route53.Change {
|
||||
var c route53.Change
|
||||
var r route53.ResourceRecordSet
|
||||
var rrs []*route53.ResourceRecord
|
||||
@ -219,7 +274,7 @@ func newTXTChange(action, name string, ttl int, values []string) *route53.Change
|
||||
}
|
||||
r.SetType("TXT")
|
||||
r.SetName(name)
|
||||
r.SetTTL(int64(ttl))
|
||||
r.SetTTL(ttl)
|
||||
r.SetResourceRecords(rrs)
|
||||
c.SetAction(action)
|
||||
c.SetResourceRecordSet(&r)
|
||||
|
Reference in New Issue
Block a user