dnscontrol/pkg/diff2/analyze.go

373 lines
12 KiB
Go
Raw Normal View History

package diff2
import (
"fmt"
"sort"
"strings"
"github.com/StackExchange/dnscontrol/v3/models"
)
func analyzeByRecordSet(cc *CompareConfig) ChangeList {
var instructions ChangeList
// For each label...
for _, lc := range cc.ldata {
// for each type at that label...
for _, rt := range lc.tdata {
// ...if there are changes generate an instruction.
ets := rt.existingTargets
dts := rt.desiredTargets
msgs := genmsgs(ets, dts)
if len(msgs) == 0 { // No differences?
//fmt.Printf("DEBUG: done. Records are the same\n")
// The records at this rset are the same. No work to be done.
continue
}
if len(ets) == 0 { // Create a new label.
//fmt.Printf("DEBUG: add\n")
instructions = append(instructions, mkAdd(lc.label, rt.rType, msgs, rt.desiredRecs))
} else if len(dts) == 0 { // Delete that label and all its records.
//fmt.Printf("DEBUG: delete\n")
instructions = append(instructions, mkDelete(lc.label, rt.rType, rt.existingRecs, msgs))
} else { // Change the records at that label
//fmt.Printf("DEBUG: change\n")
instructions = append(instructions, mkChange(lc.label, rt.rType, msgs, rt.existingRecs, rt.desiredRecs))
}
}
}
return instructions
}
func analyzeByLabel(cc *CompareConfig) ChangeList {
var instructions ChangeList
//fmt.Printf("DEBUG: START: analyzeByLabel\n")
// Accumulate if there are any changes and collect the info needed to generate instructions.
for i, lc := range cc.ldata {
//fmt.Printf("DEBUG: START LABEL = %q\n", lc.label)
label := lc.label
var accMsgs []string
var accExisting models.Records
var accDesired models.Records
msgsByKey := map[models.RecordKey][]string{}
for _, rt := range lc.tdata {
//fmt.Printf("DEBUG: START RTYPE = %q\n", rt.rType)
ets := rt.existingTargets
dts := rt.desiredTargets
msgs := genmsgs(ets, dts)
msgsByKey[models.RecordKey{NameFQDN: label, Type: rt.rType}] = msgs
//fmt.Printf("DEBUG: appending msgs=%v\n", msgs)
accMsgs = append(accMsgs, msgs...) // Accumulate the messages
accExisting = append(accExisting, rt.existingRecs...) // Accumulate records existing at this label.
accDesired = append(accDesired, rt.desiredRecs...) // Accumulate records desired at this label.
}
// We now know what changed (accMsgs),
// what records USED TO EXIST at that label (accExisting),
// and what records SHOULD EXIST at that label (accDesired).
// Based on that info, we can generate the instructions.
if len(accMsgs) == 0 { // Nothing changed.
//fmt.Printf("DEBUG: analyzeByLabel: %02d: no change\n", i)
} else if len(accDesired) == 0 { // No new records at the label? This must be a delete.
//fmt.Printf("DEBUG: analyzeByLabel: %02d: delete\n", i)
instructions = append(instructions, mkDelete(label, "", accExisting, accMsgs))
} else if len(accExisting) == 0 { // No old records at the label? This must be a change.
//fmt.Printf("DEBUG: analyzeByLabel: %02d: create\n", i)
//fmt.Printf("DEBUG: analyzeByLabel mkAdd msgs=%d\n", len(accMsgs))
instructions = append(instructions, mkAddByLabel(label, "", accMsgs, accDesired))
} else { // If we get here, it must be a change.
_ = i
// fmt.Printf("DEBUG: analyzeByLabel: %02d: change %d{%v} %d{%v} msgs=%v\n", i,
// len(accExisting), accExisting,
// len(accDesired), accDesired,
// accMsgs,
// )
//fmt.Printf("DEBUG: analyzeByLabel mkchange msgs=%d\n", len(accMsgs))
instructions = append(instructions, mkChangeLabel(label, "", accMsgs, accExisting, accDesired, msgsByKey))
}
}
return instructions
}
func analyzeByRecord(cc *CompareConfig) ChangeList {
//fmt.Printf("DEBUG: analyzeByRecord: cc=%v\n", cc)
var instructions ChangeList
// For each label, for each type at that label, see if there are any changes.
for _, lc := range cc.ldata {
//fmt.Printf("DEBUG: analyzeByRecord: next lc=%v\n", lc)
for _, rt := range lc.tdata {
ets := rt.existingTargets
dts := rt.desiredTargets
cs := diffTargets(ets, dts)
//fmt.Printf("DEBUG: analyzeByRecord: cs=%v\n", cs)
instructions = append(instructions, cs...)
}
}
return instructions
}
// NB(tlim): there is no analyzeByZone. ByZone calls anayzeByRecords().
func mkAdd(l string, t string, msgs []string, recs models.Records) Change {
c := Change{Type: CREATE, Msgs: msgs}
c.Key.NameFQDN = l
c.Key.Type = t
c.New = recs
return c
}
// TODO(tlim): Clean these up. Some of them are exact duplicates!
func mkAddByLabel(l string, t string, msgs []string, newRecs models.Records) Change {
//fmt.Printf("DEBUG: mkAddByLabel: len(o)=%d len(m)=%d\n", len(newRecs), len(msgs))
//fmt.Printf("DEBUG: mkAddByLabel: msgs = %v\n", msgs)
c := Change{Type: CREATE, Msgs: msgs, MsgsJoined: strings.Join(msgs, "\n")}
c.Key.NameFQDN = l
c.Key.Type = t
c.New = newRecs
return c
}
func mkChange(l string, t string, msgs []string, oldRecs, newRecs models.Records) Change {
c := Change{Type: CHANGE, Msgs: msgs, MsgsJoined: strings.Join(msgs, "\n")}
c.Key.NameFQDN = l
c.Key.Type = t
c.Old = oldRecs
c.New = newRecs
return c
}
func mkChangeLabel(l string, t string, msgs []string, oldRecs, newRecs models.Records, msgsByKey map[models.RecordKey][]string) Change {
//fmt.Printf("DEBUG: mkChangeLabel: len(o)=%d\n", len(oldRecs))
c := Change{Type: CHANGE, Msgs: msgs, MsgsJoined: strings.Join(msgs, "\n")}
c.Key.NameFQDN = l
c.Key.Type = t
c.Old = oldRecs
c.New = newRecs
c.MsgsByKey = msgsByKey
return c
}
func mkDelete(l string, t string, oldRecs models.Records, msgs []string) Change {
c := Change{Type: DELETE, Msgs: msgs, MsgsJoined: strings.Join(msgs, "\n")}
c.Key.NameFQDN = l
c.Key.Type = t
c.Old = oldRecs
return c
}
func mkDeleteRec(l string, t string, msgs []string, rec *models.RecordConfig) Change {
c := Change{Type: DELETE, Msgs: msgs, MsgsJoined: strings.Join(msgs, "\n")}
c.Key.NameFQDN = l
c.Key.Type = t
c.Old = models.Records{rec}
return c
}
func removeCommon(existing, desired []targetConfig) ([]targetConfig, []targetConfig) {
// NB(tlim): We could probably make this faster. Some ideas:
// * pre-allocate newexisting/newdesired and assign to indexed elements instead of appending.
// * iterate backwards (len(d) to 0) and delete items that are the same.
// On the other hand, this function typically receives lists of 1-3 elements
// and any optimization is probably fruitless.
eKeys := map[string]*targetConfig{}
for _, v := range existing {
v := v
eKeys[v.compareable] = &v
}
dKeys := map[string]*targetConfig{}
for _, v := range desired {
v := v
dKeys[v.compareable] = &v
}
return filterBy(existing, dKeys), filterBy(desired, eKeys)
}
// Find the changes that are exclusively changes in TTL.
func splitTTLOnly(existing, desired []targetConfig) (
existDiff []targetConfig, desireDiff []targetConfig,
existTTL models.Records, desireTTL models.Records,
) {
ei := 0
di := 0
for (ei < len(existing)) && (di < len(desired)) {
er := existing[ei].rec
dr := desired[di].rec
ecomp := er.GetTargetCombined()
dcomp := dr.GetTargetCombined()
if ecomp == dcomp && er.TTL == dr.TTL {
panic("Should not happen. There should be some difference!")
}
//fmt.Printf("DEBUG ecomp=%q dcomp=%q ettl=%d dttl=%d\n", ecomp, dcomp, er.TTL, dr.TTL)
if ecomp == dcomp && er.TTL != dr.TTL {
//fmt.Printf("DEBUG: equal\n")
existTTL = append(existTTL, er)
desireTTL = append(desireTTL, dr)
ei++
di++
} else if ecomp < dcomp {
//fmt.Printf("DEBUG: less\n")
existDiff = append(existDiff, existing[ei])
ei++
} else if ecomp > dcomp {
//fmt.Printf("DEBUG: greater\n")
desireDiff = append(desireDiff, desired[di])
di++
} else {
panic("Should not happen. e and d can not be both equal, less and greater")
}
}
// Any remainder goes to the *Diff result:
if ei < len(existing) {
//fmt.Printf("DEBUG: append e len()=%d\n", ei)
existDiff = append(existDiff, existing[ei:]...)
}
if di < len(desired) {
//fmt.Printf("DEBUG: append d len()=%d\n", di)
desireDiff = append(desireDiff, desired[di:]...)
}
return
}
// Return s but remove any items that can be found in m.
func filterBy(s []targetConfig, m map[string]*targetConfig) []targetConfig {
// fmt.Printf("DEBUG: filterBy called with %v\n", s)
// for k := range m {
// fmt.Printf("DEBUG: map %q\n", k)
// }
i := 0 // output index
for _, x := range s {
if _, ok := m[x.compareable]; !ok {
//fmt.Printf("DEBUG: comp %q NO\n", x.compareable)
// copy and increment index
s[i] = x
i++
} else {
//fmt.Printf("DEBUG: comp %q YES\n", x.compareable)
}
}
// // Prevent memory leak by erasing truncated values
// // (not needed if values don't contain pointers, directly or indirectly)
// for j := i; j < len(s); j++ {
// s[j] = nil
// }
s = s[:i]
// fmt.Printf("DEBUG: filterBy returns %v\n", s)
return s
}
func humanDiff(a, b *models.RecordConfig) string {
acombined := a.GetTargetCombined()
bcombined := b.GetTargetCombined()
combinedDiff := acombined != bcombined
ttlDiff := a.TTL != b.TTL
// TODO(tlim): It would be nice if we included special cases for MX
// records and others.
if combinedDiff && ttlDiff {
return fmt.Sprintf("(%s ttl=%d) -> (%s ttl=%d)", acombined, a.TTL, bcombined, b.TTL)
}
if combinedDiff {
return fmt.Sprintf("(%s) -> (%s)", acombined, bcombined)
}
return fmt.Sprintf("%s (ttl %d->%d)", acombined, a.TTL, b.TTL)
}
func diffTargets(existing, desired []targetConfig) ChangeList {
//fmt.Printf("DEBUG: diffTargets called with len(e)=%d len(d)=%d\n", len(existing), len(desired))
// Nothing to do?
if len(existing) == 0 && len(desired) == 0 {
//fmt.Printf("DEBUG: diffTargets: nothing to do\n")
return nil
}
// Sort to make comparisons easier
sort.Slice(existing, func(i, j int) bool { return existing[i].compareable < existing[j].compareable })
sort.Slice(desired, func(i, j int) bool { return desired[i].compareable < desired[j].compareable })
var instructions ChangeList
// remove the exact matches.
existing, desired = removeCommon(existing, desired)
// At this point the exact matches are removed. However there may be
// records that have the same GetTargetCombined() but different
// TTLs.
// Find TTL changes:
existing, desired, existingTTL, desiredTTL := splitTTLOnly(existing, desired)
for i := range desiredTTL {
er := existingTTL[i]
dr := desiredTTL[i]
m := fmt.Sprintf("CHANGE %s %s ", dr.NameFQDN, dr.Type) + humanDiff(er, dr)
instructions = append(instructions, mkChange(dr.NameFQDN, dr.Type, []string{m},
models.Records{er},
models.Records{dr},
))
}
// the common chunk are changes (regardless of TTL)
mi := min(len(existing), len(desired))
//fmt.Printf("DEBUG: min=%d\n", mi)
for i := 0; i < mi; i++ {
//fmt.Println(i, "CHANGE")
er := existing[i].rec
dr := desired[i].rec
m := fmt.Sprintf("CHANGE %s %s ", dr.NameFQDN, dr.Type) + humanDiff(existing[i].rec, desired[i].rec)
instructions = append(instructions, mkChange(dr.NameFQDN, dr.Type, []string{m},
models.Records{er},
models.Records{dr},
))
}
// any left-over existing are deletes
for i := mi; i < len(existing); i++ {
//fmt.Println(i, "DEL")
er := existing[i].rec
m := fmt.Sprintf("DELETE %s %s %s", er.NameFQDN, er.Type, er.GetTargetCombined())
instructions = append(instructions, mkDeleteRec(er.NameFQDN, er.Type, []string{m}, er))
}
// any left-over desired are creates
for i := mi; i < len(desired); i++ {
//fmt.Println(i, "CREATE")
dr := desired[i].rec
m := fmt.Sprintf("CREATE %s %s %s", dr.NameFQDN, dr.Type, dr.GetTargetCombined())
instructions = append(instructions, mkAdd(dr.NameFQDN, dr.Type, []string{m}, models.Records{dr}))
}
return instructions
}
func genmsgs(existing, desired []targetConfig) []string {
cl := diffTargets(existing, desired)
return justMsgs(cl)
}
func justMsgs(cl ChangeList) []string {
var msgs []string
for _, c := range cl {
msgs = append(msgs, c.Msgs...)
}
return msgs
}
func justMsgString(cl ChangeList) string {
msgs := justMsgs(cl)
return strings.Join(msgs, "\n")
}