mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-11-15 04:36:19 +08:00
216 lines
6.6 KiB
Go
216 lines
6.6 KiB
Go
package diff2
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/StackExchange/dnscontrol/v3/models"
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/prettyzone"
|
|
)
|
|
|
|
type ComparableFunc func(*models.RecordConfig) string
|
|
|
|
type CompareConfig struct {
|
|
existing, desired models.Records
|
|
ldata []*labelConfig
|
|
//
|
|
origin string // Domain zone
|
|
compareableFunc ComparableFunc
|
|
//
|
|
labelMap map[string]bool
|
|
keyMap map[models.RecordKey]bool
|
|
}
|
|
|
|
type labelConfig struct {
|
|
label string
|
|
tdata []*rTypeConfig
|
|
}
|
|
|
|
type rTypeConfig struct {
|
|
rType string
|
|
existingTargets []targetConfig
|
|
desiredTargets []targetConfig
|
|
existingRecs []*models.RecordConfig
|
|
desiredRecs []*models.RecordConfig
|
|
}
|
|
|
|
type targetConfig struct {
|
|
compareable string
|
|
rec *models.RecordConfig
|
|
}
|
|
|
|
func NewCompareConfig(origin string, existing, desired models.Records, compFn ComparableFunc) *CompareConfig {
|
|
cc := &CompareConfig{
|
|
existing: existing,
|
|
desired: desired,
|
|
//
|
|
origin: origin,
|
|
compareableFunc: compFn,
|
|
//
|
|
labelMap: map[string]bool{},
|
|
keyMap: map[models.RecordKey]bool{},
|
|
}
|
|
cc.addRecords(existing, true)
|
|
cc.addRecords(desired, false)
|
|
cc.VerifyCNAMEAssertions()
|
|
sort.Slice(cc.ldata, func(i, j int) bool {
|
|
return prettyzone.LabelLess(cc.ldata[i].label, cc.ldata[j].label)
|
|
})
|
|
return cc
|
|
}
|
|
|
|
func (cc *CompareConfig) VerifyCNAMEAssertions() {
|
|
|
|
// NB(tlim): This can be deleted. This should be probably not possible.
|
|
// However, let's keep it around for a few iterations to be paranoid.
|
|
|
|
// According to the RFCs if a label has a CNAME, it can not have any other
|
|
// records at that label... even other CNAMEs. Therefore, we need to be
|
|
// careful with changes at a label that involve a CNAME.
|
|
// Example 1:
|
|
// OLD: a.example.com CNAME b
|
|
// NEW: a.example.com A 1.2.3.4
|
|
// We must delete the CNAME record THEN create the A record. If we
|
|
// blindly create the the A first, most APIs will reply with an error
|
|
// because there is already a CNAME at that label.
|
|
// Example 2:
|
|
// OLD: a.example.com A 1.2.3.4
|
|
// NEW: a.example.com CNAME b
|
|
// We must delete the A record THEN create the CNAME. If we blindly
|
|
// create the CNAME first, most APIs will reply with an error because
|
|
// there is already an A record at that label.
|
|
//
|
|
// To assure that DNS providers don't have to think about this, we order
|
|
// the tdata items so that we generate the instructions in the best order.
|
|
// In other words:
|
|
// If there is a CNAME in existing, it should be in front.
|
|
// If there is a CNAME in desired, it should be at the end.
|
|
|
|
// That said, since we addRecords existing first, and desired last, the data
|
|
// should already be in the right order.
|
|
|
|
for _, ld := range cc.ldata {
|
|
for j, td := range ld.tdata {
|
|
if td.rType == "CNAME" {
|
|
if len(td.existingTargets) != 0 {
|
|
//fmt.Printf("DEBUG: cname in existing: index=%d\n", j)
|
|
if j != 0 {
|
|
panic("should not happen: (CNAME not in first position)")
|
|
}
|
|
}
|
|
if len(td.desiredTargets) != 0 {
|
|
//fmt.Printf("DEBUG: cname in desired: index=%d\n", j)
|
|
if j != highest(ld.tdata) {
|
|
panic("should not happen: (CNAME not in last position)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func (cc *CompareConfig) String() string {
|
|
var buf bytes.Buffer
|
|
b := &buf
|
|
|
|
fmt.Fprintf(b, "ldata:\n")
|
|
for i, ld := range cc.ldata {
|
|
fmt.Fprintf(b, " ldata[%02d]: %s\n", i, ld.label)
|
|
for j, t := range ld.tdata {
|
|
fmt.Fprintf(b, " tdata[%d]: %q e(%d, %d) d(%d, %d)\n", j, t.rType,
|
|
len(t.existingTargets),
|
|
len(t.existingRecs),
|
|
len(t.desiredTargets),
|
|
len(t.desiredRecs),
|
|
)
|
|
}
|
|
}
|
|
fmt.Fprintf(b, "labelMap: len=%d %v\n", len(cc.labelMap), cc.labelMap)
|
|
fmt.Fprintf(b, "keyMap: len=%d %v\n", len(cc.keyMap), cc.keyMap)
|
|
fmt.Fprintf(b, "existing: %q\n", cc.existing)
|
|
fmt.Fprintf(b, "desired: %q\n", cc.desired)
|
|
fmt.Fprintf(b, "origin: %v\n", cc.origin)
|
|
fmt.Fprintf(b, "compFn: %v\n", cc.compareableFunc)
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func comparable(rc *models.RecordConfig, f func(*models.RecordConfig) string) string {
|
|
if f == nil {
|
|
return rc.ToDiffable()
|
|
}
|
|
return rc.ToDiffable() + " " + f(rc)
|
|
}
|
|
|
|
func (cc *CompareConfig) addRecords(recs models.Records, storeInExisting bool) {
|
|
|
|
// Sort, because sorted data is easier to work with.
|
|
// NB(tlim): The actual sort order doesn't matter as long as all the records
|
|
// of the same label+rtype are adjacent. We use PrettySort because it works,
|
|
// has been extensively tested, and assures that the ChangeList will be a
|
|
// pretty order.
|
|
//for _, rec := range recs {
|
|
z := prettyzone.PrettySort(recs, cc.origin, 0, nil)
|
|
for _, rec := range z.Records {
|
|
|
|
label := rec.NameFQDN
|
|
rtype := rec.Type
|
|
comp := comparable(rec, cc.compareableFunc)
|
|
//fmt.Printf("DEBUG addRecords rec=%v:%v es=%v comp=%v\n", label, rtype, storeInExisting, comp)
|
|
|
|
//fmt.Printf("BEFORE L: %v\n", len(cc.ldata))
|
|
// Are we seeing this label for the first time?
|
|
var labelIdx int
|
|
if _, ok := cc.labelMap[label]; !ok {
|
|
//fmt.Printf("DEBUG: I haven't see label=%v before. Adding.\n", label)
|
|
cc.labelMap[label] = true
|
|
cc.ldata = append(cc.ldata, &labelConfig{label: label})
|
|
labelIdx = highest(cc.ldata)
|
|
} else {
|
|
// find label in cc.ldata:
|
|
for k, v := range cc.ldata {
|
|
if v.label == label {
|
|
labelIdx = k
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Are we seeing this label+rtype for the first time?
|
|
key := rec.Key()
|
|
if _, ok := cc.keyMap[key]; !ok {
|
|
//fmt.Printf("DEBUG: I haven't see key=%v before. Adding.\n", key)
|
|
cc.keyMap[key] = true
|
|
x := cc.ldata[labelIdx]
|
|
//fmt.Printf("DEBUG: appending rtype=%v\n", rtype)
|
|
x.tdata = append(x.tdata, &rTypeConfig{rType: rtype})
|
|
}
|
|
var rtIdx int
|
|
// find rtype in tdata:
|
|
for k, v := range cc.ldata[labelIdx].tdata {
|
|
if v.rType == rtype {
|
|
rtIdx = k
|
|
break
|
|
}
|
|
}
|
|
//fmt.Printf("DEBUG: found rtype=%v at index %d\n", rtype, rtIdx)
|
|
|
|
// Now it is safe to add/modify the records.
|
|
|
|
//fmt.Printf("BEFORE E/D: %v/%v\n", len(td.existingRecs), len(td.desiredRecs))
|
|
if storeInExisting {
|
|
cc.ldata[labelIdx].tdata[rtIdx].existingRecs = append(cc.ldata[labelIdx].tdata[rtIdx].existingRecs, rec)
|
|
cc.ldata[labelIdx].tdata[rtIdx].existingTargets = append(cc.ldata[labelIdx].tdata[rtIdx].existingTargets,
|
|
targetConfig{compareable: comp, rec: rec})
|
|
} else {
|
|
cc.ldata[labelIdx].tdata[rtIdx].desiredRecs = append(cc.ldata[labelIdx].tdata[rtIdx].desiredRecs, rec)
|
|
cc.ldata[labelIdx].tdata[rtIdx].desiredTargets = append(cc.ldata[labelIdx].tdata[rtIdx].desiredTargets,
|
|
targetConfig{compareable: comp, rec: rec})
|
|
}
|
|
//fmt.Printf("AFTER L: %v\n", len(cc.ldata))
|
|
//fmt.Printf("AFTER E/D: %v/%v\n", len(td.existingRecs), len(td.desiredRecs))
|
|
//fmt.Printf("\n")
|
|
}
|
|
}
|