CORE: Clean up diff2 code in prep for production ()

This commit is contained in:
Tom Limoncelli 2023-02-28 01:25:09 -05:00 committed by GitHub
parent e129e40313
commit 2586e2b611
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 93 additions and 412 deletions

View file

@ -326,7 +326,7 @@ func TestDualProviders(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
for i, c := range cs { for i, c := range cs {
t.Logf("#%d: %s", i+1, c.Msg) t.Logf("#%d:\n%s", i+1, c.Msg)
if err = c.F(); err != nil { if err = c.F(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -913,6 +913,7 @@ func makeTests(t *testing.T) []*TestGroup {
not( not(
"AZURE_DNS", // Removed because it is too slow "AZURE_DNS", // Removed because it is too slow
"CLOUDFLAREAPI", // Infinite pagesize but due to slow speed, skipping. "CLOUDFLAREAPI", // Infinite pagesize but due to slow speed, skipping.
"DIGITALOCEAN", // No paging. Why bother?
"CSCGLOBAL", // Doesn't page. Works fine. Due to the slow API we skip. "CSCGLOBAL", // Doesn't page. Works fine. Due to the slow API we skip.
"GANDI_V5", // Their API is so damn slow. We'll add it back as needed. "GANDI_V5", // Their API is so damn slow. We'll add it back as needed.
"MSDNS", // No paging done. No need to test. "MSDNS", // No paging done. No need to test.

View file

@ -50,12 +50,7 @@ type differCompat struct {
// - The NewCompat() feature `extraValues` is not supported. That // - The NewCompat() feature `extraValues` is not supported. That
// parameter must be set to nil. If you use that feature, consider // parameter must be set to nil. If you use that feature, consider
// one of the pkg/diff2/By*() functions. // one of the pkg/diff2/By*() functions.
func (d *differCompat) IncrementalDiff(existing []*models.RecordConfig) (unchanged, create, toDelete, modify Changeset, err error) { func (d *differCompat) IncrementalDiff(existing []*models.RecordConfig) (unchanged, toCreate, toDelete, toModify Changeset, err error) {
unchanged = Changeset{}
create = Changeset{}
toDelete = Changeset{}
modify = Changeset{}
instructions, err := diff2.ByRecord(existing, d.dc, nil) instructions, err := diff2.ByRecord(existing, d.dc, nil)
if err != nil { if err != nil {
return nil, nil, nil, nil, err return nil, nil, nil, nil, err
@ -70,11 +65,11 @@ func (d *differCompat) IncrementalDiff(existing []*models.RecordConfig) (unchang
fmt.Println(inst.MsgsJoined) fmt.Println(inst.MsgsJoined)
case diff2.CREATE: case diff2.CREATE:
cor.Desired = inst.New[0] cor.Desired = inst.New[0]
create = append(create, cor) toCreate = append(toCreate, cor)
case diff2.CHANGE: case diff2.CHANGE:
cor.Existing = inst.Old[0] cor.Existing = inst.Old[0]
cor.Desired = inst.New[0] cor.Desired = inst.New[0]
modify = append(modify, cor) toModify = append(toModify, cor)
case diff2.DELETE: case diff2.DELETE:
cor.Existing = inst.Old[0] cor.Existing = inst.Old[0]
toDelete = append(toDelete, cor) toDelete = append(toDelete, cor)
@ -89,17 +84,17 @@ func (d *differCompat) IncrementalDiff(existing []*models.RecordConfig) (unchang
// ChangedGroups provides the same results as IncrementalDiff but grouped by key. // ChangedGroups provides the same results as IncrementalDiff but grouped by key.
func (d *differCompat) ChangedGroups(existing []*models.RecordConfig) (map[models.RecordKey][]string, error) { func (d *differCompat) ChangedGroups(existing []*models.RecordConfig) (map[models.RecordKey][]string, error) {
changedKeys := map[models.RecordKey][]string{} changedKeys := map[models.RecordKey][]string{}
_, create, toDelete, modify, err := d.IncrementalDiff(existing) _, toCreate, toDelete, toModify, err := d.IncrementalDiff(existing)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, c := range create { for _, c := range toCreate {
changedKeys[c.Desired.Key()] = append(changedKeys[c.Desired.Key()], c.String()) changedKeys[c.Desired.Key()] = append(changedKeys[c.Desired.Key()], c.String())
} }
for _, d := range toDelete { for _, d := range toDelete {
changedKeys[d.Existing.Key()] = append(changedKeys[d.Existing.Key()], d.String()) changedKeys[d.Existing.Key()] = append(changedKeys[d.Existing.Key()], d.String())
} }
for _, m := range modify { for _, m := range toModify {
changedKeys[m.Desired.Key()] = append(changedKeys[m.Desired.Key()], m.String()) changedKeys[m.Desired.Key()] = append(changedKeys[m.Desired.Key()], m.String())
} }
return changedKeys, nil return changedKeys, nil

View file

@ -20,18 +20,14 @@ func analyzeByRecordSet(cc *CompareConfig) ChangeList {
dts := rt.desiredTargets dts := rt.desiredTargets
msgs := genmsgs(ets, dts) msgs := genmsgs(ets, dts)
if len(msgs) == 0 { // No differences? 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. // The records at this rset are the same. No work to be done.
continue continue
} }
if len(ets) == 0 { // Create a new label. if len(ets) == 0 { // Create a new label.
//fmt.Printf("DEBUG: add\n")
instructions = append(instructions, mkAdd(lc.label, rt.rType, msgs, rt.desiredRecs)) instructions = append(instructions, mkAdd(lc.label, rt.rType, msgs, rt.desiredRecs))
} else if len(dts) == 0 { // Delete that label and all its records. } 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, msgs, rt.existingRecs)) instructions = append(instructions, mkDelete(lc.label, rt.rType, msgs, rt.existingRecs))
} else { // Change the records at that label } 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)) instructions = append(instructions, mkChange(lc.label, rt.rType, msgs, rt.existingRecs, rt.desiredRecs))
} }
} }
@ -41,22 +37,21 @@ func analyzeByRecordSet(cc *CompareConfig) ChangeList {
func analyzeByLabel(cc *CompareConfig) ChangeList { func analyzeByLabel(cc *CompareConfig) ChangeList {
var instructions ChangeList var instructions ChangeList
//fmt.Printf("DEBUG: START: analyzeByLabel\n") // Accumulate any changes and collect the info needed to generate instructions.
// Accumulate if there are any changes and collect the info needed to generate instructions. for _, lc := range cc.ldata {
for i, lc := range cc.ldata { // for each type at that label...
//fmt.Printf("DEBUG: START LABEL = %q\n", lc.label)
label := lc.label label := lc.label
var accMsgs []string var accMsgs []string
var accExisting models.Records var accExisting models.Records
var accDesired models.Records var accDesired models.Records
msgsByKey := map[models.RecordKey][]string{} msgsByKey := map[models.RecordKey][]string{}
for _, rt := range lc.tdata { for _, rt := range lc.tdata {
//fmt.Printf("DEBUG: START RTYPE = %q\n", rt.rType) // for each type at that label...
ets := rt.existingTargets ets := rt.existingTargets
dts := rt.desiredTargets dts := rt.desiredTargets
msgs := genmsgs(ets, dts) msgs := genmsgs(ets, dts)
msgsByKey[models.RecordKey{NameFQDN: label, Type: rt.rType}] = msgs k := models.RecordKey{NameFQDN: label, Type: rt.rType}
//fmt.Printf("DEBUG: appending msgs=%v\n", msgs) msgsByKey[k] = msgs
accMsgs = append(accMsgs, msgs...) // Accumulate the messages accMsgs = append(accMsgs, msgs...) // Accumulate the messages
accExisting = append(accExisting, rt.existingRecs...) // Accumulate records existing at this label. accExisting = append(accExisting, rt.existingRecs...) // Accumulate records existing at this label.
accDesired = append(accDesired, rt.desiredRecs...) // Accumulate records desired at this label. accDesired = append(accDesired, rt.desiredRecs...) // Accumulate records desired at this label.
@ -68,23 +63,14 @@ func analyzeByLabel(cc *CompareConfig) ChangeList {
// Based on that info, we can generate the instructions. // Based on that info, we can generate the instructions.
if len(accMsgs) == 0 { // Nothing changed. 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. } 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, "", accMsgs, accExisting)) instructions = append(instructions, mkDelete(label, "", accMsgs, accExisting))
} else if len(accExisting) == 0 { // No old records at the label? This must be a change. } else if len(accExisting) == 0 { // No old records at the label? This must be a change.
//fmt.Printf("DEBUG: analyzeByLabel: %02d: create\n", i) c := mkAdd(label, "", accMsgs, accDesired)
//fmt.Printf("DEBUG: analyzeByLabel mkAdd msgs=%d\n", len(accMsgs)) c.MsgsByKey = msgsByKey
instructions = append(instructions, mkAddByLabel(label, "", accMsgs, accDesired)) instructions = append(instructions, c)
} else { // If we get here, it must be a change. } else { // If we get here, it must be a change.
_ = i instructions = append(instructions, mkChange(label, "", accMsgs, accExisting, accDesired))
// 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, mkChangeByLabel(label, "", accMsgs, accExisting, accDesired, msgsByKey))
} }
} }
@ -92,38 +78,23 @@ func analyzeByLabel(cc *CompareConfig) ChangeList {
} }
func analyzeByRecord(cc *CompareConfig) ChangeList { func analyzeByRecord(cc *CompareConfig) ChangeList {
//fmt.Printf("DEBUG: analyzeByRecord: cc=%v\n", cc)
var instructions ChangeList var instructions ChangeList
// For each label, for each type at that label, see if there are any changes. // For each label, for each type at that label, see if there are any changes.
for _, lc := range cc.ldata { for _, lc := range cc.ldata {
//fmt.Printf("DEBUG: analyzeByRecord: next lc=%v\n", lc)
for _, rt := range lc.tdata { for _, rt := range lc.tdata {
ets := rt.existingTargets ets := rt.existingTargets
dts := rt.desiredTargets dts := rt.desiredTargets
cs := diffTargets(ets, dts) cs := diffTargets(ets, dts)
//fmt.Printf("DEBUG: analyzeByRecord: cs=%v\n", cs)
instructions = append(instructions, cs...) instructions = append(instructions, cs...)
} }
} }
return instructions return instructions
} }
// NB(tlim): there is no analyzeByZone. ByZone calls anayzeByRecords(). // FYI: there is no analyzeByZone. diff2.ByZone() calls analyzeByRecords().
func mkAdd(l string, t string, msgs []string, recs models.Records) Change { func mkAdd(l string, t string, msgs []string, newRecs models.Records) Change {
//fmt.Printf("DEBUG mkAdd called: (%v, %v, %v, %v)\n", l, t, msgs, recs)
c := Change{Type: CREATE, Msgs: msgs, MsgsJoined: strings.Join(msgs, "\n")}
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 called: (%v, %v, %v, %v)\n", l, t, msgs, newRecs)
c := Change{Type: CREATE, Msgs: msgs, MsgsJoined: strings.Join(msgs, "\n")} c := Change{Type: CREATE, Msgs: msgs, MsgsJoined: strings.Join(msgs, "\n")}
c.Key.NameFQDN = l c.Key.NameFQDN = l
c.Key.Type = t c.Key.Type = t
@ -132,7 +103,6 @@ func mkAddByLabel(l string, t string, msgs []string, newRecs models.Records) Cha
} }
func mkChange(l string, t string, msgs []string, oldRecs, newRecs models.Records) Change { func mkChange(l string, t string, msgs []string, oldRecs, newRecs models.Records) Change {
//fmt.Printf("DEBUG mkChange called: (%v, %v, %v, %v, %v)\n", l, t, msgs, oldRecs, newRecs)
c := Change{Type: CHANGE, Msgs: msgs, MsgsJoined: strings.Join(msgs, "\n")} c := Change{Type: CHANGE, Msgs: msgs, MsgsJoined: strings.Join(msgs, "\n")}
c.Key.NameFQDN = l c.Key.NameFQDN = l
c.Key.Type = t c.Key.Type = t
@ -141,49 +111,21 @@ func mkChange(l string, t string, msgs []string, oldRecs, newRecs models.Records
return c return c
} }
func mkChangeByLabel(l string, t string, msgs []string, oldRecs, newRecs models.Records, msgsByKey map[models.RecordKey][]string) Change {
//fmt.Printf("DEBUG mkChangeLabel called: (%v, %v, %v, %v, %v, %v)\n", l, t, msgs, oldRecs, newRecs, msgsByKey)
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, msgs []string, oldRecs models.Records) Change { func mkDelete(l string, t string, msgs []string, oldRecs models.Records) Change {
//fmt.Printf("DEBUG mkDelete called: (%v, %v, %v, %v)\n", l, t, msgs, oldRecs)
if len(msgs) == 0 {
panic("mkDelete with no msg")
}
c := Change{Type: DELETE, Msgs: msgs, MsgsJoined: strings.Join(msgs, "\n")} c := Change{Type: DELETE, Msgs: msgs, MsgsJoined: strings.Join(msgs, "\n")}
c.Key.NameFQDN = l c.Key.NameFQDN = l
c.Key.Type = t c.Key.Type = t
c.Old = oldRecs c.Old = oldRecs
return c return c
} }
func mkDeleteByRecord(l string, t string, msgs []string, rec *models.RecordConfig) Change {
//fmt.Printf("DEBUG mkDeleteREc called: (%v, %v, %v, %v)\n", l, t, msgs, rec)
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) { func removeCommon(existing, desired []targetConfig) ([]targetConfig, []targetConfig) {
// NB(tlim): We could probably make this faster. Some ideas: // Sort by comparableFull.
// * 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.
// Sort to make comparisons easier
sort.Slice(existing, func(i, j int) bool { return existing[i].comparableFull < existing[j].comparableFull }) sort.Slice(existing, func(i, j int) bool { return existing[i].comparableFull < existing[j].comparableFull })
sort.Slice(desired, func(i, j int) bool { return desired[i].comparableFull < desired[j].comparableFull }) sort.Slice(desired, func(i, j int) bool { return desired[i].comparableFull < desired[j].comparableFull })
// Build maps required by filterBy
eKeys := map[string]*targetConfig{} eKeys := map[string]*targetConfig{}
for _, v := range existing { for _, v := range existing {
v := v v := v
@ -201,13 +143,12 @@ func removeCommon(existing, desired []targetConfig) ([]targetConfig, []targetCon
// findTTLChanges finds the records that ONLY change their TTL. For those, generate a Change. // findTTLChanges finds the records that ONLY change their TTL. For those, generate a Change.
// Remove such items from the list. // Remove such items from the list.
func findTTLChanges(existing, desired []targetConfig) ([]targetConfig, []targetConfig, ChangeList) { func findTTLChanges(existing, desired []targetConfig) ([]targetConfig, []targetConfig, ChangeList) {
//fmt.Printf("DEBUG: findTTLChanges(%v,\n%v)\n", existing, desired)
if (len(existing) == 0) || (len(desired) == 0) { if (len(existing) == 0) || (len(desired) == 0) {
return existing, desired, nil return existing, desired, nil
} }
// Sort to make comparisons easier // Sort by comparableNoTTL
sort.Slice(existing, func(i, j int) bool { return existing[i].comparableNoTTL < existing[j].comparableNoTTL }) sort.Slice(existing, func(i, j int) bool { return existing[i].comparableNoTTL < existing[j].comparableNoTTL })
sort.Slice(desired, func(i, j int) bool { return desired[i].comparableNoTTL < desired[j].comparableNoTTL }) sort.Slice(desired, func(i, j int) bool { return desired[i].comparableNoTTL < desired[j].comparableNoTTL })
@ -222,8 +163,9 @@ func findTTLChanges(existing, desired []targetConfig) ([]targetConfig, []targetC
dcomp := desired[di].comparableNoTTL dcomp := desired[di].comparableNoTTL
if ecomp == dcomp && er.TTL == dr.TTL { if ecomp == dcomp && er.TTL == dr.TTL {
fmt.Printf("DEBUG: ecomp=%q dcomp=%q er.TTL=%v dr.TTL=%v\n", ecomp, dcomp, er.TTL, dr.TTL) panic(fmt.Sprintf(
panic("Should not happen. There should be some difference!") "Should not happen. There should be some difference! ecomp=%q dcomp=%q er.TTL=%v dr.TTL=%v\n",
ecomp, dcomp, er.TTL, dr.TTL))
} }
if ecomp == dcomp && er.TTL != dr.TTL { if ecomp == dcomp && er.TTL != dr.TTL {
@ -247,7 +189,6 @@ func findTTLChanges(existing, desired []targetConfig) ([]targetConfig, []targetC
// Any remainder goes to the *Diff result: // Any remainder goes to the *Diff result:
if ei < len(existing) { if ei < len(existing) {
//fmt.Printf("DEBUG: append e len()=%d\n", ei)
existDiff = append(existDiff, existing[ei:]...) existDiff = append(existDiff, existing[ei:]...)
} }
if di < len(desired) { if di < len(desired) {
@ -259,14 +200,9 @@ func findTTLChanges(existing, desired []targetConfig) ([]targetConfig, []targetC
// Return s but remove any items that can be found in m. // Return s but remove any items that can be found in m.
func filterBy(s []targetConfig, m map[string]*targetConfig) []targetConfig { 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 i := 0 // output index
for _, x := range s { for _, x := range s {
if _, ok := m[x.comparableFull]; !ok { if _, ok := m[x.comparableFull]; !ok {
//fmt.Printf("DEBUG: comp %q NO\n", x.comparable)
// copy and increment index // copy and increment index
s[i] = x s[i] = x
i++ i++
@ -278,7 +214,6 @@ func filterBy(s []targetConfig, m map[string]*targetConfig) []targetConfig {
// s[j] = nil // s[j] = nil
// } // }
s = s[:i] s = s[:i]
// fmt.Printf("DEBUG: filterBy returns %v\n", s)
return s return s
} }
@ -298,22 +233,16 @@ func humanDiff(a, b targetConfig) string {
} }
func diffTargets(existing, desired []targetConfig) ChangeList { 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? // Nothing to do?
if len(existing) == 0 && len(desired) == 0 { if len(existing) == 0 && len(desired) == 0 {
//fmt.Printf("DEBUG: diffTargets: nothing to do\n")
return nil return nil
} }
var instructions ChangeList var instructions ChangeList
//fmt.Printf("DEBUG: diffTargets BEFORE existing=%+v\n", existing)
//fmt.Printf("DEBUG: diffTargets BEFORE desired=%+v\n", desired)
// remove the exact matches. // remove the exact matches.
existing, desired = removeCommon(existing, desired) existing, desired = removeCommon(existing, desired)
//fmt.Printf("DEBUG: diffTargets AFTER existing=%+v\n", existing)
//fmt.Printf("DEBUG: diffTargets AFTER desired=%+v\n", desired)
// At this point the exact matches are removed. However there may be // At this point the exact matches are removed. However there may be
// records that have the same GetTargetCombined() but different // records that have the same GetTargetCombined() but different
@ -322,37 +251,32 @@ func diffTargets(existing, desired []targetConfig) ChangeList {
existing, desired, newChanges := findTTLChanges(existing, desired) existing, desired, newChanges := findTTLChanges(existing, desired)
instructions = append(instructions, newChanges...) instructions = append(instructions, newChanges...)
// Sort to make comparisons easier // Sort by comparableFull
sort.Slice(existing, func(i, j int) bool { return existing[i].comparableFull < existing[j].comparableFull }) sort.Slice(existing, func(i, j int) bool { return existing[i].comparableFull < existing[j].comparableFull })
sort.Slice(desired, func(i, j int) bool { return desired[i].comparableFull < desired[j].comparableFull }) sort.Slice(desired, func(i, j int) bool { return desired[i].comparableFull < desired[j].comparableFull })
// the remaining chunks are changes (regardless of TTL) // the remaining chunks are changes (regardless of TTL)
mi := min(len(existing), len(desired)) mi := min(len(existing), len(desired))
for i := 0; i < mi; i++ { for i := 0; i < mi; i++ {
//fmt.Println(i, "CHANGE")
er := existing[i].rec er := existing[i].rec
dr := desired[i].rec dr := desired[i].rec
m := color.YellowString("± MODIFY %s %s %s", dr.NameFQDN, dr.Type, humanDiff(existing[i], desired[i])) m := color.YellowString("± MODIFY %s %s %s", dr.NameFQDN, dr.Type, humanDiff(existing[i], desired[i]))
instructions = append(instructions, mkChange(dr.NameFQDN, dr.Type, []string{m}, instructions = append(instructions,
models.Records{er}, mkChange(dr.NameFQDN, dr.Type, []string{m}, models.Records{er}, models.Records{dr}),
models.Records{dr}, )
))
} }
// any left-over existing are deletes // any left-over existing are deletes
for i := mi; i < len(existing); i++ { for i := mi; i < len(existing); i++ {
//fmt.Println(i, "DEL")
er := existing[i].rec er := existing[i].rec
m := color.RedString("- DELETE %s %s %s", er.NameFQDN, er.Type, existing[i].comparableFull) m := color.RedString("- DELETE %s %s %s", er.NameFQDN, er.Type, existing[i].comparableFull)
instructions = append(instructions, mkDeleteByRecord(er.NameFQDN, er.Type, []string{m}, er)) instructions = append(instructions, mkDelete(er.NameFQDN, er.Type, []string{m}, models.Records{er}))
} }
// any left-over desired are creates // any left-over desired are creates
for i := mi; i < len(desired); i++ { for i := mi; i < len(desired); i++ {
//fmt.Println(i, "CREATE")
dr := desired[i].rec dr := desired[i].rec
m := color.GreenString("+ CREATE %s %s %s", dr.NameFQDN, dr.Type, desired[i].comparableFull) m := color.GreenString("+ CREATE %s %s %s", dr.NameFQDN, dr.Type, desired[i].comparableFull)
instructions = append(instructions, mkAdd(dr.NameFQDN, dr.Type, []string{m}, models.Records{dr})) instructions = append(instructions, mkAdd(dr.NameFQDN, dr.Type, []string{m}, models.Records{dr}))
@ -362,8 +286,7 @@ func diffTargets(existing, desired []targetConfig) ChangeList {
} }
func genmsgs(existing, desired []targetConfig) []string { func genmsgs(existing, desired []targetConfig) []string {
cl := diffTargets(existing, desired) return justMsgs(diffTargets(existing, desired))
return justMsgs(cl)
} }
func justMsgs(cl ChangeList) []string { func justMsgs(cl ChangeList) []string {

View file

@ -398,7 +398,7 @@ ChangeList: len=12
compareCL(t, "analyzeByRecord", tt.name, "Rec", cl, tt.wantChangeRec) compareCL(t, "analyzeByRecord", tt.name, "Rec", cl, tt.wantChangeRec)
}) })
// NB(tlim): There is no analyzeByZone(). ByZone() uses analyzeByRecord(). // NB(tlim): There is no analyzeByZone(). diff2.ByZone() uses analyzeByRecord().
} }
} }

View file

@ -82,7 +82,7 @@ type rTypeConfig struct {
} }
type targetConfig struct { type targetConfig struct {
comparableFull string // A string that can be used to compare two rec's for equality. comparableFull string // A string that can be used to compare two rec's for exact equality.
comparableNoTTL string // A string that can be used to compare two rec's for equality, ignoring the TTL comparableNoTTL string // A string that can be used to compare two rec's for equality, ignoring the TTL
rec *models.RecordConfig // The RecordConfig itself. rec *models.RecordConfig // The RecordConfig itself.
@ -110,10 +110,6 @@ func NewCompareConfig(origin string, existing, desired models.Records, compFn Co
func (cc *CompareConfig) VerifyCNAMEAssertions() { func (cc *CompareConfig) VerifyCNAMEAssertions() {
// In theory these assertions do not need to be tested as they test
// something that can not happen. In my I've proved this to be
// true. That said, a little paranoia is healthy.
// According to the RFCs if a label has a CNAME, it can not have any other // 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 // records at that label... even other CNAMEs. Therefore, we need to be
// careful with changes at a label that involve a CNAME. // careful with changes at a label that involve a CNAME.
@ -131,7 +127,7 @@ func (cc *CompareConfig) VerifyCNAMEAssertions() {
// there is already an A record at that label. // there is already an A record at that label.
// //
// To assure that DNS providers don't have to think about this, we order // 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. // the tdata items so that we generate the instructions in the correct order.
// In other words: // In other words:
// If there is a CNAME in existing, it should be in front. // 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. // If there is a CNAME in desired, it should be at the end.
@ -154,15 +150,12 @@ func (cc *CompareConfig) VerifyCNAMEAssertions() {
} }
if len(td.existingTargets) != 0 { if len(td.existingTargets) != 0 {
//fmt.Printf("DEBUG: cname in existing: index=%d\n", j)
if j != 0 { if j != 0 {
panic("should not happen: (CNAME not in first position)") panic("should not happen: (CNAME not in first position)")
} }
} }
if len(td.desiredTargets) != 0 { if len(td.desiredTargets) != 0 {
//fmt.Printf("DEBUG: cname in desired: index=%d\n", j)
//fmt.Printf("DEBUG: highest: index=%d\n", j)
if j != highest(ld.tdata) { if j != highest(ld.tdata) {
panic("should not happen: (CNAME not in last position)") panic("should not happen: (CNAME not in last position)")
} }
@ -273,7 +266,6 @@ func (cc *CompareConfig) addRecords(recs models.Records, storeInExisting bool) {
// Now it is safe to add/modify the records. // 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 { if storeInExisting {
cc.ldata[labelIdx].tdata[rtIdx].existingRecs = append(cc.ldata[labelIdx].tdata[rtIdx].existingRecs, rec) 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, cc.ldata[labelIdx].tdata[rtIdx].existingTargets = append(cc.ldata[labelIdx].tdata[rtIdx].existingTargets,
@ -283,8 +275,5 @@ func (cc *CompareConfig) addRecords(recs models.Records, storeInExisting bool) {
cc.ldata[labelIdx].tdata[rtIdx].desiredTargets = append(cc.ldata[labelIdx].tdata[rtIdx].desiredTargets, cc.ldata[labelIdx].tdata[rtIdx].desiredTargets = append(cc.ldata[labelIdx].tdata[rtIdx].desiredTargets,
targetConfig{comparableNoTTL: compNoTTL, comparableFull: compFull, rec: rec}) targetConfig{comparableNoTTL: compNoTTL, comparableFull: compFull, 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")
} }
} }

View file

@ -20,7 +20,7 @@ const (
CREATE // Create a record/recordset/label where none existed before. CREATE // Create a record/recordset/label where none existed before.
CHANGE // Change existing record/recordset/label CHANGE // Change existing record/recordset/label
DELETE // Delete existing record/recordset/label DELETE // Delete existing record/recordset/label
REPORT // No change, but boy do I have something to say! REPORT // No change, but I have something to say!
) )
type ChangeList []Change type ChangeList []Change
@ -83,7 +83,6 @@ General instructions:
return corrections, nil return corrections, nil
} }
*/ */
// ByRecordSet takes two lists of records (existing and desired) and // ByRecordSet takes two lists of records (existing and desired) and
@ -95,7 +94,7 @@ General instructions:
// record, if A records are added, changed, or removed, the API takes // record, if A records are added, changed, or removed, the API takes
// www.example.com, A, and a list of all the desired IP addresses. // www.example.com, A, and a list of all the desired IP addresses.
// //
// Examples include: // Examples include: AZURE_DNS, GCORE, NS1, ROUTE53
func ByRecordSet(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) { func ByRecordSet(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
return byHelper(analyzeByRecordSet, existing, dc, compFunc) return byHelper(analyzeByRecordSet, existing, dc, compFunc)
} }
@ -107,7 +106,7 @@ func ByRecordSet(existing models.Records, dc *models.DomainConfig, compFunc Comp
// time. That is, updates are done by sending a list of DNS records // time. That is, updates are done by sending a list of DNS records
// to be served at a particular label, or the label itself is deleted. // to be served at a particular label, or the label itself is deleted.
// //
// Examples include: // Examples include: GANDI_V5
func ByLabel(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) { func ByLabel(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
return byHelper(analyzeByLabel, existing, dc, compFunc) return byHelper(analyzeByLabel, existing, dc, compFunc)
} }
@ -123,7 +122,7 @@ func ByLabel(existing models.Records, dc *models.DomainConfig, compFunc Comparab
// A change always has exactly 1 old and 1 new: .Old[0] and .New[0] // A change always has exactly 1 old and 1 new: .Old[0] and .New[0]
// A delete always has exactly 1 old: .Old[0] // A delete always has exactly 1 old: .Old[0]
// //
// Examples include: INWX // Examples include: CLOUDFLAREAPI, HEDNS, INWX, MSDNS, OVH, PORKBUN, VULTR
func ByRecord(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) { func ByRecord(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
return byHelper(analyzeByRecord, existing, dc, compFunc) return byHelper(analyzeByRecord, existing, dc, compFunc)
} }
@ -149,7 +148,7 @@ func ByRecord(existing models.Records, dc *models.DomainConfig, compFunc Compara
// // (dc.Records are the new records for the zone). // // (dc.Records are the new records for the zone).
// } // }
// //
// Example providers include: BIND // Example providers include: BIND, AUTODNS
func ByZone(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) ([]string, bool, error) { func ByZone(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) ([]string, bool, error) {
if len(existing) == 0 { if len(existing) == 0 {
@ -157,7 +156,7 @@ func ByZone(existing models.Records, dc *models.DomainConfig, compFunc Comparabl
return nil, true, nil return nil, true, nil
} }
// Only return the messages. // Only return the messages. The caller has the list of records needed to build the new zone.
instructions, err := byHelper(analyzeByRecord, existing, dc, compFunc) instructions, err := byHelper(analyzeByRecord, existing, dc, compFunc)
return justMsgs(instructions), len(instructions) != 0, err return justMsgs(instructions), len(instructions) != 0, err
} }
@ -199,7 +198,7 @@ func byHelper(fn func(cc *CompareConfig) ChangeList, existing models.Records, dc
return instructions, nil return instructions, nil
} }
// Stringify the datastructures for easier debugging // Stringify the datastructures (for debugging)
func (c Change) String() string { func (c Change) String() string {
var buf bytes.Buffer var buf bytes.Buffer

View file

@ -25,6 +25,7 @@ func makeRecSet(recs ...*models.RecordConfig) *recset {
result.Recs = append(result.Recs, recs...) result.Recs = append(result.Recs, recs...)
return &result return &result
} }
func Test_groupbyRSet(t *testing.T) { func Test_groupbyRSet(t *testing.T) {
wwwa1 := makeRec("www", "A", "1.1.1.1") wwwa1 := makeRec("www", "A", "1.1.1.1")

View file

@ -16,12 +16,11 @@ import (
# How do NO_PURGE, IGNORE_*, ENSURE_ABSENT and friends work? # How do NO_PURGE, IGNORE_*, ENSURE_ABSENT and friends work?
## Terminology: ## Terminology:
* "existing" refers to the records downloaded from the provider via the API. * "existing" refers to the records downloaded from the provider via the API.
* "desired" refers to the records generated from dnsconfig.js. * "desired" refers to the records generated from dnsconfig.js.
* "absences" refers to a list of records tagged with ASSURE_ABSENT. * "absences" refers to a list of records tagged with ENSURE_ABSENT.
## What are the features? ## What are the features?
@ -39,9 +38,9 @@ and 1 way to make exceptions.
* IGNORE_TARGET(foo) is the same as UNMANAGED("*", "*", foo) * IGNORE_TARGET(foo) is the same as UNMANAGED("*", "*", foo)
* FYI: You CAN have a label with two A records, one controlled by * FYI: You CAN have a label with two A records, one controlled by
DNSControl and one controlled by an external system. DNSControl would DNSControl and one controlled by an external system. DNSControl would
need to have an UNMANAGED() statement with a targetglob that matches need to have an UNMANAGED() statement with a targetglob that matches
the external system's target values. the external system's target values.
* ASSURE_ABSENT: Override NO_PURGE for specific records. i.e. delete them even * ENSURE_ABSENT: Override NO_PURGE for specific records. i.e. delete them even
though NO_PURGE is enabled. though NO_PURGE is enabled.
* If any of these records are in desired (matched on * If any of these records are in desired (matched on
label:rtype:target), remove them. This takes priority over label:rtype:target), remove them. This takes priority over
@ -53,8 +52,9 @@ The fundamental premise is "if you don't want it deleted, copy it to the
'desired' list." So, for example, if you want to IGNORE_NAME("www"), then you 'desired' list." So, for example, if you want to IGNORE_NAME("www"), then you
find any records with the label "www" in "existing" and copy them to "desired". find any records with the label "www" in "existing" and copy them to "desired".
As a result, the diff2 algorithm won't delete them because they are desired! As a result, the diff2 algorithm won't delete them because they are desired!
(Of course "desired" can't have duplicate records. Check before you add.)
This is different than in the old system (pkg/diff) which would generate the This is different than in the old implementation (pkg/diff) which would generate the
diff but but then do a bunch of checking to see if the record was one that diff but but then do a bunch of checking to see if the record was one that
shouldn't be deleted. Or, in the case of NO_PURGE, would simply not do the shouldn't be deleted. Or, in the case of NO_PURGE, would simply not do the
deletions. This was complex because there were many edge cases to deal with. deletions. This was complex because there were many edge cases to deal with.
@ -75,7 +75,7 @@ Here is how we intend to implement these features:
NO_PURGE + ENSURE_ABSENT is implemented as: NO_PURGE + ENSURE_ABSENT is implemented as:
* Take the list of existing records. If any do not appear in desired, add them * Take the list of existing records. If any do not appear in desired, add them
to desired UNLESS they appear in absences. to desired UNLESS they appear in absences. (Yes, that's complex!)
* "appear in desired" is done by matching on label:type. * "appear in desired" is done by matching on label:type.
* "appear in absences" is done by matching on label:type:target. * "appear in absences" is done by matching on label:type:target.
@ -154,7 +154,7 @@ func processIgnoreAndNoPurge(domain string, existing, desired, absences models.R
absentDB := models.NewRecordDBFromRecords(absences, domain) absentDB := models.NewRecordDBFromRecords(absences, domain)
compileUnmanagedConfigs(unmanagedConfigs) compileUnmanagedConfigs(unmanagedConfigs)
for _, rec := range existing { for _, rec := range existing {
if matchAll(unmanagedConfigs, rec) { if matchAny(unmanagedConfigs, rec) {
ignorable = append(ignorable, rec) ignorable = append(ignorable, rec)
} else { } else {
if noPurge { if noPurge {
@ -176,7 +176,7 @@ func processIgnoreAndNoPurge(domain string, existing, desired, absences models.R
func findConflicts(uconfigs []*models.UnmanagedConfig, recs models.Records) models.Records { func findConflicts(uconfigs []*models.UnmanagedConfig, recs models.Records) models.Records {
var conflicts models.Records var conflicts models.Records
for _, rec := range recs { for _, rec := range recs {
if matchAll(uconfigs, rec) { if matchAny(uconfigs, rec) {
conflicts = append(conflicts, rec) conflicts = append(conflicts, rec)
} }
} }
@ -219,8 +219,8 @@ func compileUnmanagedConfigs(configs []*models.UnmanagedConfig) error {
return nil return nil
} }
// matchAll returns true if rec matches any of the uconfigs. // matchAny returns true if rec matches any of the uconfigs.
func matchAll(uconfigs []*models.UnmanagedConfig, rec *models.RecordConfig) bool { func matchAny(uconfigs []*models.UnmanagedConfig, rec *models.RecordConfig) bool {
for _, uc := range uconfigs { for _, uc := range uconfigs {
if matchLabel(uc.LabelGlob, rec.GetLabel()) && if matchLabel(uc.LabelGlob, rec.GetLabel()) &&
matchType(uc.RTypeMap, rec.Type) && matchType(uc.RTypeMap, rec.Type) &&

View file

@ -49,7 +49,6 @@ func handsoffHelper(t *testing.T, existingZone, desiredJs string, noPurge bool,
if err != nil { if err != nil {
panic(err) panic(err)
} }
//fmt.Printf("DEBUG: existing=%s\n", showRecs(existing))
dnsconfig, err := js.ExecuteJavascriptString([]byte(desiredJs), false, nil) dnsconfig, err := js.ExecuteJavascriptString([]byte(desiredJs), false, nil)
if err != nil { if err != nil {

View file

@ -1,198 +0,0 @@
EXISTING:
laba A 1.2.3.4 [0]
laba MX 10 laba [1]
labc CNAME laba [2]
labe A 10.10.10.15 [3]
labe A 10.10.10.16 [4]
labe A 10.10.10.17 [5]
labe A 10.10.10.18 [6]
labg NS 10.10.10.15 [7]
labg NS 10.10.10.16 [8]
labg NS 10.10.10.17 [9]
labg NS 10.10.10.18 [10]
labh CNAME labd [11]
DESIRED:
laba A 1.2.3.4 [0']
laba A 1.2.3.5 [1']
laba MX 20 labb [2']
labe A 10.10.10.95 [3']
labe A 10.10.10.96 [4']
labe A 10.10.10.97 [5']
labe A 10.10.10.98 [6']
labf TXT "foo" [7']
labg NS 10.10.10.10 [8']
labg NS 10.10.10.15 [9']
labg NS 10.10.10.16 [10']
labg NS 10.10.10.97 [11']
labh A 1.2.3.4 [12']
ByRRSet:
[] laba:A CHANGE NewSet: { [0], [1'] } (ByRecords needs: Old [0] )
[] laba:MX CHANGE NewSet: { [2'] } (ByLabel needs: Old: [2])
[] labc:CNAME DELETE Old: { [2 ] }
[] labe:A CHANGE NewSet: { [3'], [4'], [5'], [6'] }
[] labf:TXT CHANGE NewSet: { [7'] }
[] labg:NS CHANGE NewSet: { [7] [8] [8'] [11'] }
[] labh:CNAME DELETE Old { [11] }
[] labh:A CREATE NewSet: { [12'] }
ByRecord:
CREATE [1']
CHANGE [1] [2']
DELETE [2]
CHANGE [3] [3']
CHANGE [4] [4']
CHANGE [5] [5']
CHANGE [6] [6']
CREATE [7']
CREATE [8']
CHANGE [10] [11']
DELETE [11]
CREATE [12']
ByLabel: (take ByRRSet gather all CHANGES)
laba CHANGE NewSet: { [0'], [1'], [2'] }
labc DELETE Old: { [2] }
labe CHANGE New: { [3'], [4'], [5'], [6'] }
labf CREATE New: { [7'] }
labg CHANGE NewSet: { [7] [8] [8'] [11'] }
labh DELETE Old { [11] }
labh CREATE NewSet: { [12'] }
By Record:
rewrite as triples: FQDN+TYPE, TARGET, RC
byRecord:
group-by key=FQDN+TYPE, use targets to make add/change/delete for each record.
byRSet:
group-by key=FQDN+TYPE, use targets to make add/change/delete for each record.
for each key:
if both have this key:
IF targets are the same, skip.
Else generate CHANGE for KEY:
New = Recs from desired.
Msgs = The msgs from targetdiff(e.Recs, d.Recs)
byLabel:
group-by key=FQDN, use type+targets to make add/change/delete for each record.
rewrite as triples: FQDN {, TYPE, TARGET, RC
type CompareConfig struct {
existing, desired models.Records
ldata: []LabelConfig
}
type ByLabelConfig struct {
label string
tdata: []ByRTypeConfig
}
type ByRTypeConfig struct {
rtype string
existing: []TargetConfig
desired: []TargetConfig
existingRecs: []*models.RecordConfig
desiredRecs: []*models.RecordConfig
}
type TargetConfig struct {
compareable string
rec *model.RecordConfig
}
func highest[S ~[]T, T any](s S) int {
return len(s) - 1
}
populate CompareConfig.
for rec := range desired {
label = FILL
rtype = FILL
comp = FILL
cc.labelMap[label] = &rc
cc.keyMap[key] = &rc
if not seen label:
append cc.ldata ByLabelConfig{}
labelIdx = last(cc.ldata)
if not seen key:
append cc.ldata[labelIdx].tdata ByRTypeConfig{}
rtIdx = last(cc.ldata[labelIdx].tdata)
cc.ldata[labelIdx].label = label
cc.ldata[labelIdx].tdata[rtIdx].rtype = rtype
cc.ldata[labelIdx].tdata[rtIdx].existing[append].comparable = comp
cc.ldata[labelIdx].tdata[rtIdx].existing[append].rec = &rc
}
ByRSet:
func traverse(cc CompareConfig) {
for label := range cc.data {
for rtype := range label.data {
Msgs := genmsgs(rtype.existing, rtype.desired)
if no Msgs, continue
if len(rtype.existing) = 0 {
yield create(label, rtype, rtype.desiredRecs, Msgs)
} else if len(rtype.desired) = 0 {
yield delete(label, rtype, rtype.existingRecs, Msgs)
} else { // equal
yield change(label, rtype, rtype.desiredRecs, Msgs)
}
}
}
byLabel:
func traverse(cc CompareConfig) {
for label := range cc.data {
initialize msgs, desiredRecords
anyExisting = false
for rtype := range label.data {
accumulate Msgs := genmsgs(rtype.existing, rtype.desired)
if Msgs (i.e. there were changes) {
accumulate AllDesired := rtype.desiredRecs
if len(rtype.existing) != 0 {
anyExisting = true
}
}
}
if there are Msgs:
if len(AllDesired) = 0 {
yield delete(label)
} else if countAllExisting == 0 {
yield create(label, AllDesired)
} else {
yield change(label, AllDesired)
}
}
}
ByRecord:
func traverse(cc CompareConfig) {
for label := range cc.data {
for rtype := range label.data {
create, change, delete := difftargets(rtype.existing, rtype.desired)
yield creates, changes, deletes
}
}
}
Byzone:
func traverse(cc CompareConfig) {
for label := range cc.data {
for rtype := range label.data {
Msgs := genmsgs(rtype.existing, rtype.desired)
accumulate Msgs
}
}
if len(accumMsgs) == 0 {
return nil, FirstMsg
} else {
return desired, Msgs
}
}

View file

@ -128,7 +128,6 @@ func (a *edgeDNSProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
keysToUpdate, err = (diff.New(dc)).ChangedGroups(existingRecords) keysToUpdate, err = (diff.New(dc)).ChangedGroups(existingRecords)
} else { } else {
keysToUpdate, err = (diff.NewCompat(dc)).ChangedGroups(existingRecords) keysToUpdate, err = (diff.NewCompat(dc)).ChangedGroups(existingRecords)
// TODO(tlim): In the future please adopt `pkg/diff2/By*()`
} }
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -14,6 +14,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/pkg/transform" "github.com/StackExchange/dnscontrol/v3/pkg/transform"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
"github.com/cloudflare/cloudflare-go" "github.com/cloudflare/cloudflare-go"
"github.com/fatih/color"
"github.com/miekg/dns/dnsutil" "github.com/miekg/dns/dnsutil"
) )
@ -415,6 +416,17 @@ func (c *cloudflareProvider) mkCreateCorrection(newrec *models.RecordConfig, dom
} }
func (c *cloudflareProvider) mkChangeCorrection(oldrec, newrec *models.RecordConfig, domainID string, msg string) []*models.Correction { func (c *cloudflareProvider) mkChangeCorrection(oldrec, newrec *models.RecordConfig, domainID string, msg string) []*models.Correction {
var idTxt string
switch oldrec.Type {
case "PAGE_RULE":
idTxt = oldrec.Original.(cloudflare.PageRule).ID
case "WORKER_ROUTE":
idTxt = oldrec.Original.(cloudflare.WorkerRoute).ID
default:
idTxt = oldrec.Original.(cloudflare.DNSRecord).ID
}
msg = msg + color.YellowString(" id=%v", idTxt)
switch newrec.Type { switch newrec.Type {
case "PAGE_RULE": case "PAGE_RULE":
return []*models.Correction{{ return []*models.Correction{{
@ -441,6 +453,7 @@ func (c *cloudflareProvider) mkChangeCorrection(oldrec, newrec *models.RecordCon
} }
func (c *cloudflareProvider) mkDeleteCorrection(recType string, origRec any, domainID string, msg string) []*models.Correction { func (c *cloudflareProvider) mkDeleteCorrection(recType string, origRec any, domainID string, msg string) []*models.Correction {
var idTxt string var idTxt string
switch recType { switch recType {
case "PAGE_RULE": case "PAGE_RULE":
@ -450,7 +463,7 @@ func (c *cloudflareProvider) mkDeleteCorrection(recType string, origRec any, dom
default: default:
idTxt = origRec.(cloudflare.DNSRecord).ID idTxt = origRec.(cloudflare.DNSRecord).ID
} }
msg = msg + fmt.Sprintf(" id=%v", idTxt) msg = msg + color.RedString(" id=%v", idTxt)
correction := &models.Correction{ correction := &models.Correction{
Msg: msg, Msg: msg,

View file

@ -118,42 +118,18 @@ func (client *providerClient) GenerateDomainCorrections(dc *models.DomainConfig,
//txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records //txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
var corrections []*models.Correction var corrections []*models.Correction
var creates, dels, modifications diff.Changeset
var err error var err error
var differ diff.Differ
if !diff2.EnableDiff2 { if !diff2.EnableDiff2 {
differ := diff.New(dc) differ = diff.New(dc)
_, creates, dels, modifications, err = differ.IncrementalDiff(foundRecords)
} else { } else {
differ := diff.NewCompat(dc) differ = diff.NewCompat(dc)
_, creates, dels, modifications, err = differ.IncrementalDiff(foundRecords)
} }
_, creates, dels, modifications, err := differ.IncrementalDiff(foundRecords)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// How to generate corrections?
// (1) Most providers take individual deletes, creates, and
// modifications:
// // Generate changes.
// corrections := []*models.Correction{}
// for _, del := range dels {
// corrections = append(corrections, client.deleteRec(client.dnsserver, dc.Name, del))
// }
// for _, cre := range creates {
// corrections = append(corrections, client.createRec(client.dnsserver, dc.Name, cre)...)
// }
// for _, m := range modifications {
// corrections = append(corrections, client.modifyRec(client.dnsserver, dc.Name, m))
// }
// return corrections, nil
// (2) Some providers upload the entire zone every time. Look at
// GetDomainCorrections for BIND and NAMECHEAP for inspiration.
// (3) Others do something entirely different. Like CSCGlobal:
// CSCGlobal has a unique API. A list of edits is sent in one API // CSCGlobal has a unique API. A list of edits is sent in one API
// call. Edits aren't permitted if an existing edit is being // call. Edits aren't permitted if an existing edit is being
// processed. Therefore, before we do an edit we block until the // processed. Therefore, before we do an edit we block until the

View file

@ -147,20 +147,19 @@ func (api *digitaloceanProvider) GetDomainCorrections(dc *models.DomainConfig) (
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
var corrections []*models.Correction var corrections []*models.Correction
var create, delete, modify diff.Changeset var differ diff.Differ
if !diff2.EnableDiff2 { if !diff2.EnableDiff2 {
differ := diff.New(dc) differ = diff.New(dc)
_, create, delete, modify, err = differ.IncrementalDiff(existingRecords)
} else { } else {
differ := diff.NewCompat(dc) differ = diff.NewCompat(dc)
_, create, delete, modify, err = differ.IncrementalDiff(existingRecords)
} }
_, toCreate, toDelete, toModify, err := differ.IncrementalDiff(existingRecords)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Deletes first so changing type works etc. // Deletes first so changing type works etc.
for _, m := range delete { for _, m := range toDelete {
id := m.Existing.Original.(*godo.DomainRecord).ID id := m.Existing.Original.(*godo.DomainRecord).ID
corr := &models.Correction{ corr := &models.Correction{
Msg: fmt.Sprintf("%s, DO ID: %d", m.String(), id), Msg: fmt.Sprintf("%s, DO ID: %d", m.String(), id),
@ -177,7 +176,7 @@ func (api *digitaloceanProvider) GetDomainCorrections(dc *models.DomainConfig) (
} }
corrections = append(corrections, corr) corrections = append(corrections, corr)
} }
for _, m := range create { for _, m := range toCreate {
req := toReq(dc, m.Desired) req := toReq(dc, m.Desired)
corr := &models.Correction{ corr := &models.Correction{
Msg: m.String(), Msg: m.String(),
@ -194,7 +193,7 @@ func (api *digitaloceanProvider) GetDomainCorrections(dc *models.DomainConfig) (
} }
corrections = append(corrections, corr) corrections = append(corrections, corr)
} }
for _, m := range modify { for _, m := range toModify {
id := m.Existing.Original.(*godo.DomainRecord).ID id := m.Existing.Original.(*godo.DomainRecord).ID
req := toReq(dc, m.Desired) req := toReq(dc, m.Desired)
corr := &models.Correction{ corr := &models.Correction{
@ -281,7 +280,7 @@ func toRc(domain string, r *godo.DomainRecord) *models.RecordConfig {
t.SetTarget(target) t.SetTarget(target)
switch rtype := r.Type; rtype { switch rtype := r.Type; rtype {
case "TXT": case "TXT":
t.SetTargetTXTString(target) t.SetTargetTXTfromRFC1035Quoted(target)
default: default:
// nothing additional required // nothing additional required
} }

View file

@ -177,10 +177,6 @@ func (c *dnsimpleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
return nil, err return nil, err
} }
if err != nil {
return nil, err
}
for _, del := range del { for _, del := range del {
rec := del.Existing.Original.(dnsimpleapi.ZoneRecord) rec := del.Existing.Original.(dnsimpleapi.ZoneRecord)
corrections = append(corrections, &models.Correction{ corrections = append(corrections, &models.Correction{

View file

@ -366,6 +366,8 @@ func (client *gandiv5Provider) GenerateDomainCorrections(dc *models.DomainConfig
case diff2.CREATE: case diff2.CREATE:
// We have to create the label one rtype at a time. // We have to create the label one rtype at a time.
// In other words, this is a ByRecordSet API for creation, even though
// this is a ByLabel() API for everything else.
natives := recordsToNative(inst.New, dc.Name) natives := recordsToNative(inst.New, dc.Name)
for _, n := range natives { for _, n := range natives {
label := inst.Key.NameFQDN label := inst.Key.NameFQDN

View file

@ -213,26 +213,22 @@ func (g *gcloudProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*model
differ = diff.NewCompat(dc) differ = diff.NewCompat(dc)
} }
_, create, delete, modify, err := differ.IncrementalDiff(existingRecords) _, create, delete, modify, err := differ.IncrementalDiff(existingRecords)
if err != nil {
return nil, err
}
if err != nil { if err != nil {
return nil, fmt.Errorf("incdiff error: %w", err) return nil, fmt.Errorf("incdiff error: %w", err)
} }
changedKeys := map[key]bool{} changedKeys := map[key]bool{}
desc := "" var msgs []string
for _, c := range create { for _, c := range create {
desc += fmt.Sprintln(c) msgs = append(msgs, fmt.Sprint(c))
changedKeys[keyForRec(c.Desired)] = true changedKeys[keyForRec(c.Desired)] = true
} }
for _, d := range delete { for _, d := range delete {
desc += fmt.Sprintln(d) msgs = append(msgs, fmt.Sprint(d))
changedKeys[keyForRec(d.Existing)] = true changedKeys[keyForRec(d.Existing)] = true
} }
for _, m := range modify { for _, m := range modify {
desc += fmt.Sprintln(m) msgs = append(msgs, fmt.Sprint(m))
changedKeys[keyForRec(m.Existing)] = true changedKeys[keyForRec(m.Existing)] = true
} }
if len(changedKeys) == 0 { if len(changedKeys) == 0 {
@ -282,7 +278,7 @@ func (g *gcloudProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*model
} }
return []*models.Correction{{ return []*models.Correction{{
Msg: desc, Msg: strings.Join(msgs, "\n"),
F: runChange, F: runChange,
}}, nil }}, nil

View file

@ -203,9 +203,9 @@ func (n *namecheapProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mo
return nil, err return nil, err
} }
// // because namecheap doesn't have selective create, delete, modify, // because namecheap doesn't have selective create, delete, modify,
// // we bundle them all up to send at once. We *do* want to see the // we bundle them all up to send at once. We *do* want to see the
// // changes though // changes though
var desc []string var desc []string
for _, i := range create { for _, i := range create {

View file

@ -55,14 +55,13 @@ func (n *namedotcomProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
models.PostProcessRecords(actual) models.PostProcessRecords(actual)
var corrections []*models.Correction var corrections []*models.Correction
var create, del, mod diff.Changeset var differ diff.Differ
if !diff2.EnableDiff2 { if !diff2.EnableDiff2 {
differ := diff.New(dc) differ = diff.New(dc)
_, create, del, mod, err = differ.IncrementalDiff(actual)
} else { } else {
differ := diff.NewCompat(dc) differ = diff.NewCompat(dc)
_, create, del, mod, err = differ.IncrementalDiff(actual)
} }
_, create, del, mod, err := differ.IncrementalDiff(actual)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -137,10 +137,6 @@ func (api *packetframeProvider) GetDomainCorrections(dc *models.DomainConfig) ([
return nil, err return nil, err
} }
if err != nil {
return nil, err
}
for _, m := range create { for _, m := range create {
req, err := toReq(zone.ID, dc, m.Desired) req, err := toReq(zone.ID, dc, m.Desired)
if err != nil { if err != nil {

View file

@ -65,10 +65,6 @@ func (api *rwthProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*model
return nil, err return nil, err
} }
if err != nil {
return nil, err
}
for _, d := range create { for _, d := range create {
des := d.Desired des := d.Desired
corrections = append(corrections, &models.Correction{ corrections = append(corrections, &models.Correction{