|
|
@@ -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)
|