mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-09-20 06:46:19 +08:00
NEW FEATURE: diff2: A better "diff" mechanism (#1852)
This commit is contained in:
parent
b0f2945510
commit
54fc2e9ce3
2
go.mod
2
go.mod
|
@ -62,6 +62,7 @@ require (
|
|||
require (
|
||||
github.com/G-Core/gcore-dns-sdk-go v0.2.3
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/kylelemons/godebug v1.1.0
|
||||
github.com/mattn/go-isatty v0.0.16
|
||||
github.com/vultr/govultr/v2 v2.17.2
|
||||
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9
|
||||
|
@ -129,7 +130,6 @@ require (
|
|||
github.com/juju/errors v0.0.0-20200330140219-3fe23663418f // indirect
|
||||
github.com/juju/testing v0.0.0-20210324180055-18c50b0c2098 // indirect
|
||||
github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
|
|
|
@ -15,13 +15,17 @@ type DomainConfig struct {
|
|||
RegistrarName string `json:"registrar"`
|
||||
DNSProviderNames map[string]int `json:"dnsProviders"`
|
||||
|
||||
Metadata map[string]string `json:"meta,omitempty"`
|
||||
Records Records `json:"records"`
|
||||
Nameservers []*Nameserver `json:"nameservers,omitempty"`
|
||||
KeepUnknown bool `json:"keepunknown,omitempty"`
|
||||
IgnoredNames []*IgnoreName `json:"ignored_names,omitempty"`
|
||||
IgnoredTargets []*IgnoreTarget `json:"ignored_targets,omitempty"`
|
||||
AutoDNSSEC string `json:"auto_dnssec,omitempty"` // "", "on", "off"
|
||||
Metadata map[string]string `json:"meta,omitempty"`
|
||||
Records Records `json:"records"`
|
||||
Nameservers []*Nameserver `json:"nameservers,omitempty"`
|
||||
|
||||
KeepUnknown bool `json:"keepunknown,omitempty"`
|
||||
IgnoredNames []*IgnoreName `json:"ignored_names,omitempty"`
|
||||
IgnoredTargets []*IgnoreTarget `json:"ignored_targets,omitempty"`
|
||||
Unmanaged []*UnmanagedConfig `json:"unmanaged,omitempty"`
|
||||
UnmanagedUnsafe bool `json:"unmanaged_disable_safety_check,omitempty"`
|
||||
|
||||
AutoDNSSEC string `json:"auto_dnssec,omitempty"` // "", "on", "off"
|
||||
//DNSSEC bool `json:"dnssec,omitempty"`
|
||||
|
||||
// These fields contain instantiated provider instances once everything is linked up.
|
||||
|
@ -32,6 +36,14 @@ type DomainConfig struct {
|
|||
DNSProviderInstances []*DNSProviderInstance `json:"-"`
|
||||
}
|
||||
|
||||
// UnmanagedConfig describes an UNMANAGED() rule.
|
||||
type UnmanagedConfig struct {
|
||||
Label string `json:"label_pattern"` // Glob pattern for matching labels.
|
||||
RType string `json:"rType_pattern"` // Comma-separated list of DNS Resource Types.
|
||||
typeMap map[string]bool // map of RTypes or len()=0 for all
|
||||
Target string `json:"target_pattern"` // Glob pattern for matching targets.
|
||||
}
|
||||
|
||||
// Copy returns a deep copy of the DomainConfig.
|
||||
func (dc *DomainConfig) Copy() (*DomainConfig, error) {
|
||||
newDc := &DomainConfig{}
|
||||
|
|
|
@ -305,10 +305,13 @@ func (rc *RecordConfig) GetLabelFQDN() string {
|
|||
// ToDiffable returns a string that is comparable by a differ.
|
||||
// extraMaps: a list of maps that should be included in the comparison.
|
||||
func (rc *RecordConfig) ToDiffable(extraMaps ...map[string]string) string {
|
||||
content := fmt.Sprintf("%v ttl=%d", rc.GetTargetCombined(), rc.TTL)
|
||||
if rc.Type == "SOA" {
|
||||
var content string
|
||||
switch rc.Type {
|
||||
case "SOA":
|
||||
content = fmt.Sprintf("%s %v %d %d %d %d ttl=%d", rc.target, rc.SoaMbox, rc.SoaRefresh, rc.SoaRetry, rc.SoaExpire, rc.SoaMinttl, rc.TTL)
|
||||
// SoaSerial is not used in comparison
|
||||
default:
|
||||
content = fmt.Sprintf("%v ttl=%d", rc.GetTargetCombined(), rc.TTL)
|
||||
}
|
||||
for _, valueMap := range extraMaps {
|
||||
// sort the extra values map keys to perform a deterministic
|
||||
|
@ -424,6 +427,10 @@ type RecordKey struct {
|
|||
Type string
|
||||
}
|
||||
|
||||
func (rk *RecordKey) String() string {
|
||||
return rk.NameFQDN + ":" + rk.Type
|
||||
}
|
||||
|
||||
// Key converts a RecordConfig into a RecordKey.
|
||||
func (rc *RecordConfig) Key() RecordKey {
|
||||
t := rc.Type
|
||||
|
|
30
package-lock.json
generated
30
package-lock.json
generated
|
@ -1,11 +1,33 @@
|
|||
{
|
||||
"name": "dnscontrol",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"prettier": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz",
|
||||
"integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==",
|
||||
"bin": {
|
||||
"prettier": "bin-prettier.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"prettier": {
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
|
||||
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew=="
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz",
|
||||
"integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/gobwas/glob"
|
||||
)
|
||||
|
||||
// Correlation stores a difference between two domains.
|
||||
// Correlation stores a difference between two records.
|
||||
type Correlation struct {
|
||||
d *differ
|
||||
Existing *models.RecordConfig
|
||||
|
|
275
pkg/diff2/analyze.go
Normal file
275
pkg/diff2/analyze.go
Normal file
|
@ -0,0 +1,275 @@
|
|||
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}
|
||||
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}
|
||||
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}
|
||||
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}
|
||||
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}
|
||||
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 {
|
||||
eKeys[v.compareable] = &v
|
||||
}
|
||||
dKeys := map[string]*targetConfig{}
|
||||
for _, v := range desired {
|
||||
dKeys[v.compareable] = &v
|
||||
}
|
||||
|
||||
return filterBy(existing, dKeys), filterBy(desired, eKeys)
|
||||
}
|
||||
|
||||
func filterBy(s []targetConfig, m map[string]*targetConfig) []targetConfig {
|
||||
i := 0 // output index
|
||||
for _, x := range s {
|
||||
if _, ok := m[x.compareable]; !ok {
|
||||
// copy and increment index
|
||||
s[i] = x
|
||||
i++
|
||||
}
|
||||
}
|
||||
// // 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]
|
||||
return s
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// the common chunk are changes
|
||||
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 (%s) -> (%s)", dr.NameFQDN, dr.Type, er.GetTargetCombined(), dr.GetTargetCombined())
|
||||
instructions = append(instructions, mkChange(dr.NameFQDN, dr.Type, []string{m},
|
||||
//models.Records{existing[i].rec},
|
||||
//models.Records{desired[i].rec},
|
||||
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")
|
||||
}
|
635
pkg/diff2/analyze_test.go
Normal file
635
pkg/diff2/analyze_test.go
Normal file
|
@ -0,0 +1,635 @@
|
|||
package diff2
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"github.com/kylelemons/godebug/diff"
|
||||
)
|
||||
|
||||
var testDataAA1234 = makeRec("laba", "A", "1.2.3.4") // [0]
|
||||
var testDataAA5678 = makeRec("laba", "A", "5.6.7.8") // [0]
|
||||
var testDataAMX10a = makeRec("laba", "MX", "10 laba") // [1]
|
||||
var testDataCCa = makeRec("labc", "CNAME", "laba") // [2]
|
||||
var testDataEA15 = makeRec("labe", "A", "10.10.10.15") // [3]
|
||||
var e4 = makeRec("labe", "A", "10.10.10.16") // [4]
|
||||
var e5 = makeRec("labe", "A", "10.10.10.17") // [5]
|
||||
var e6 = makeRec("labe", "A", "10.10.10.18") // [6]
|
||||
var e7 = makeRec("labg", "NS", "10.10.10.15") // [7]
|
||||
var e8 = makeRec("labg", "NS", "10.10.10.16") // [8]
|
||||
var e9 = makeRec("labg", "NS", "10.10.10.17") // [9]
|
||||
var e10 = makeRec("labg", "NS", "10.10.10.18") // [10]
|
||||
var e11mx = makeRec("labh", "MX", "22 ttt") // [11]
|
||||
var e11 = makeRec("labh", "CNAME", "labd") // [11]
|
||||
var testDataApexMX1aaa = makeRec("", "MX", "1 aaa")
|
||||
|
||||
var testDataAA1234clone = makeRec("laba", "A", "1.2.3.4") // [0']
|
||||
var testDataAA12345 = makeRec("laba", "A", "1.2.3.5") // [1']
|
||||
var testDataAMX20b = makeRec("laba", "MX", "20 labb") // [2']
|
||||
var d3 = makeRec("labe", "A", "10.10.10.95") // [3']
|
||||
var d4 = makeRec("labe", "A", "10.10.10.96") // [4']
|
||||
var d5 = makeRec("labe", "A", "10.10.10.97") // [5']
|
||||
var d6 = makeRec("labe", "A", "10.10.10.98") // [6']
|
||||
var d7 = makeRec("labf", "TXT", "foo") // [7']
|
||||
var d8 = makeRec("labg", "NS", "10.10.10.10") // [8']
|
||||
var d9 = makeRec("labg", "NS", "10.10.10.15") // [9']
|
||||
var d10 = makeRec("labg", "NS", "10.10.10.16") // [10']
|
||||
var d11 = makeRec("labg", "NS", "10.10.10.97") // [11']
|
||||
var d12 = makeRec("labh", "A", "1.2.3.4") // [12']
|
||||
var testDataApexMX22bbb = makeRec("", "MX", "22 bbb")
|
||||
|
||||
var d0tc = targetConfig{compareable: "1.2.3.4 ttl=0", rec: testDataAA1234clone}
|
||||
|
||||
func makeChange(v Verb, l, t string, old, new models.Records, msgs []string) Change {
|
||||
c := Change{
|
||||
Type: v,
|
||||
Old: old,
|
||||
New: new,
|
||||
Msgs: msgs,
|
||||
}
|
||||
c.Key.NameFQDN = l
|
||||
c.Key.Type = t
|
||||
return c
|
||||
}
|
||||
|
||||
func compareMsgs(t *testing.T, fnname, testname, testpart string, gotcc ChangeList, wantstring string) {
|
||||
t.Helper()
|
||||
gs := strings.TrimSpace(justMsgString(gotcc))
|
||||
ws := strings.TrimSpace(wantstring)
|
||||
d := diff.Diff(gs, ws)
|
||||
if d != "" {
|
||||
t.Errorf("%s()/%s (wantMsgs:%s):\n===got===\n%s\n===want===\n%s\n===diff===\n%s\n===", fnname, testname, testpart, gs, ws, d)
|
||||
}
|
||||
}
|
||||
|
||||
func compareCL(t *testing.T, fnname, testname, testpart string, gotcl ChangeList, wantstring string) {
|
||||
t.Helper()
|
||||
gs := strings.TrimSpace(gotcl.String())
|
||||
ws := strings.TrimSpace(wantstring)
|
||||
d := diff.Diff(gs, ws)
|
||||
if d != "" {
|
||||
t.Errorf("%s()/%s (wantChange%s):\n===got===\n%s\n===want===\n%s\n===diff===\n%s\n===", fnname, testname, testpart, gs, ws, d)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_analyzeByRecordSet(t *testing.T) {
|
||||
type args struct {
|
||||
origin string
|
||||
existing, desired models.Records
|
||||
compFn ComparableFunc
|
||||
}
|
||||
|
||||
origin := "f.com"
|
||||
existing := models.Records{testDataAA1234, testDataAMX10a, testDataCCa, testDataEA15, e4, e5, e6, e7, e8, e9, e10, e11}
|
||||
desired := models.Records{testDataAA1234clone, testDataAA12345, testDataAMX20b, d3, d4, d5, d6, d7, d8, d9, d10, d11, d12}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantMsgs string
|
||||
wantChangeRSet string
|
||||
wantChangeLabel string
|
||||
wantChangeRec string
|
||||
wantChangeZone string
|
||||
}{
|
||||
|
||||
{
|
||||
name: "oneequal",
|
||||
args: args{
|
||||
origin: origin,
|
||||
existing: models.Records{testDataAA1234},
|
||||
desired: models.Records{testDataAA1234clone},
|
||||
},
|
||||
wantMsgs: "", // Empty
|
||||
wantChangeRSet: "ChangeList: len=0",
|
||||
wantChangeLabel: "ChangeList: len=0",
|
||||
wantChangeRec: "ChangeList: len=0",
|
||||
},
|
||||
|
||||
{
|
||||
name: "onediff",
|
||||
args: args{
|
||||
origin: origin,
|
||||
existing: models.Records{testDataAA1234, testDataAMX10a},
|
||||
desired: models.Records{testDataAA1234clone, testDataAMX20b},
|
||||
},
|
||||
wantMsgs: "CHANGE laba.f.com MX (10 laba) -> (20 labb)",
|
||||
wantChangeRSet: `
|
||||
ChangeList: len=1
|
||||
00: Change: verb=CHANGE
|
||||
key={laba.f.com MX}
|
||||
old=[10 laba]
|
||||
new=[20 labb]
|
||||
msg=["CHANGE laba.f.com MX (10 laba) -> (20 labb)"]
|
||||
`,
|
||||
wantChangeLabel: `
|
||||
ChangeList: len=1
|
||||
00: Change: verb=CHANGE
|
||||
key={laba.f.com }
|
||||
old=[1.2.3.4 10 laba]
|
||||
new=[1.2.3.4 20 labb]
|
||||
msg=["CHANGE laba.f.com MX (10 laba) -> (20 labb)"]
|
||||
`,
|
||||
wantChangeRec: `
|
||||
ChangeList: len=1
|
||||
00: Change: verb=CHANGE
|
||||
key={laba.f.com MX}
|
||||
old=[10 laba]
|
||||
new=[20 labb]
|
||||
msg=["CHANGE laba.f.com MX (10 laba) -> (20 labb)"]
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
name: "apex",
|
||||
args: args{
|
||||
origin: origin,
|
||||
existing: models.Records{testDataAA1234, testDataApexMX1aaa},
|
||||
desired: models.Records{testDataAA1234clone, testDataApexMX22bbb},
|
||||
},
|
||||
wantMsgs: "CHANGE f.com MX (1 aaa) -> (22 bbb)",
|
||||
wantChangeRSet: `
|
||||
ChangeList: len=1
|
||||
00: Change: verb=CHANGE
|
||||
key={f.com MX}
|
||||
old=[1 aaa]
|
||||
new=[22 bbb]
|
||||
msg=["CHANGE f.com MX (1 aaa) -> (22 bbb)"]
|
||||
`,
|
||||
wantChangeLabel: `
|
||||
ChangeList: len=1
|
||||
00: Change: verb=CHANGE
|
||||
key={f.com }
|
||||
old=[1 aaa]
|
||||
new=[22 bbb]
|
||||
msg=["CHANGE f.com MX (1 aaa) -> (22 bbb)"]
|
||||
`,
|
||||
wantChangeRec: `
|
||||
ChangeList: len=1
|
||||
00: Change: verb=CHANGE
|
||||
key={f.com MX}
|
||||
old=[1 aaa]
|
||||
new=[22 bbb]
|
||||
msg=["CHANGE f.com MX (1 aaa) -> (22 bbb)"]
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
name: "firsta",
|
||||
args: args{
|
||||
origin: origin,
|
||||
existing: models.Records{testDataAA1234, testDataAMX10a},
|
||||
desired: models.Records{testDataAA1234clone, testDataAA12345, testDataAMX20b},
|
||||
},
|
||||
wantMsgs: `
|
||||
CREATE laba.f.com A 1.2.3.5
|
||||
CHANGE laba.f.com MX (10 laba) -> (20 labb)
|
||||
`,
|
||||
wantChangeRSet: `
|
||||
ChangeList: len=2
|
||||
00: Change: verb=CHANGE
|
||||
key={laba.f.com A}
|
||||
old=[1.2.3.4]
|
||||
new=[1.2.3.4 1.2.3.5]
|
||||
msg=["CREATE laba.f.com A 1.2.3.5"]
|
||||
01: Change: verb=CHANGE
|
||||
key={laba.f.com MX}
|
||||
old=[10 laba]
|
||||
new=[20 labb]
|
||||
msg=["CHANGE laba.f.com MX (10 laba) -> (20 labb)"]
|
||||
`,
|
||||
wantChangeLabel: `
|
||||
ChangeList: len=1
|
||||
00: Change: verb=CHANGE
|
||||
key={laba.f.com }
|
||||
old=[1.2.3.4 10 laba]
|
||||
new=[1.2.3.4 1.2.3.5 20 labb]
|
||||
msg=["CREATE laba.f.com A 1.2.3.5" "CHANGE laba.f.com MX (10 laba) -> (20 labb)"]
|
||||
`,
|
||||
wantChangeRec: `
|
||||
ChangeList: len=2
|
||||
00: Change: verb=CREATE
|
||||
key={laba.f.com A}
|
||||
new=[1.2.3.5]
|
||||
msg=["CREATE laba.f.com A 1.2.3.5"]
|
||||
01: Change: verb=CHANGE
|
||||
key={laba.f.com MX}
|
||||
old=[10 laba]
|
||||
new=[20 labb]
|
||||
msg=["CHANGE laba.f.com MX (10 laba) -> (20 labb)"]
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
name: "big",
|
||||
args: args{
|
||||
origin: origin,
|
||||
existing: existing,
|
||||
desired: desired,
|
||||
},
|
||||
wantMsgs: `
|
||||
CREATE laba.f.com A 1.2.3.5
|
||||
CHANGE laba.f.com MX (10 laba) -> (20 labb)
|
||||
DELETE labc.f.com CNAME laba
|
||||
CHANGE labe.f.com A (10.10.10.15) -> (10.10.10.95)
|
||||
CHANGE labe.f.com A (10.10.10.16) -> (10.10.10.96)
|
||||
CHANGE labe.f.com A (10.10.10.17) -> (10.10.10.97)
|
||||
CHANGE labe.f.com A (10.10.10.18) -> (10.10.10.98)
|
||||
CREATE labf.f.com TXT "foo"
|
||||
CHANGE labg.f.com NS (10.10.10.17) -> (10.10.10.10)
|
||||
CHANGE labg.f.com NS (10.10.10.18) -> (10.10.10.97)
|
||||
DELETE labh.f.com CNAME labd
|
||||
CREATE labh.f.com A 1.2.3.4
|
||||
`,
|
||||
wantChangeRSet: `
|
||||
ChangeList: len=8
|
||||
00: Change: verb=CHANGE
|
||||
key={laba.f.com A}
|
||||
old=[1.2.3.4]
|
||||
new=[1.2.3.4 1.2.3.5]
|
||||
msg=["CREATE laba.f.com A 1.2.3.5"]
|
||||
01: Change: verb=CHANGE
|
||||
key={laba.f.com MX}
|
||||
old=[10 laba]
|
||||
new=[20 labb]
|
||||
msg=["CHANGE laba.f.com MX (10 laba) -> (20 labb)"]
|
||||
02: Change: verb=DELETE
|
||||
key={labc.f.com CNAME}
|
||||
old=[laba]
|
||||
msg=["DELETE labc.f.com CNAME laba"]
|
||||
03: Change: verb=CHANGE
|
||||
key={labe.f.com A}
|
||||
old=[10.10.10.15 10.10.10.16 10.10.10.17 10.10.10.18]
|
||||
new=[10.10.10.95 10.10.10.96 10.10.10.97 10.10.10.98]
|
||||
msg=["CHANGE labe.f.com A (10.10.10.15) -> (10.10.10.95)" "CHANGE labe.f.com A (10.10.10.16) -> (10.10.10.96)" "CHANGE labe.f.com A (10.10.10.17) -> (10.10.10.97)" "CHANGE labe.f.com A (10.10.10.18) -> (10.10.10.98)"]
|
||||
04: Change: verb=CREATE
|
||||
key={labf.f.com TXT}
|
||||
new=["foo"]
|
||||
msg=["CREATE labf.f.com TXT \"foo\""]
|
||||
05: Change: verb=CHANGE
|
||||
key={labg.f.com NS}
|
||||
old=[10.10.10.15 10.10.10.16 10.10.10.17 10.10.10.18]
|
||||
new=[10.10.10.10 10.10.10.15 10.10.10.16 10.10.10.97]
|
||||
msg=["CHANGE labg.f.com NS (10.10.10.17) -> (10.10.10.10)" "CHANGE labg.f.com NS (10.10.10.18) -> (10.10.10.97)"]
|
||||
06: Change: verb=DELETE
|
||||
key={labh.f.com CNAME}
|
||||
old=[labd]
|
||||
msg=["DELETE labh.f.com CNAME labd"]
|
||||
07: Change: verb=CREATE
|
||||
key={labh.f.com A}
|
||||
new=[1.2.3.4]
|
||||
msg=["CREATE labh.f.com A 1.2.3.4"]
|
||||
`,
|
||||
wantChangeLabel: `
|
||||
ChangeList: len=6
|
||||
00: Change: verb=CHANGE
|
||||
key={laba.f.com }
|
||||
old=[1.2.3.4 10 laba]
|
||||
new=[1.2.3.4 1.2.3.5 20 labb]
|
||||
msg=["CREATE laba.f.com A 1.2.3.5" "CHANGE laba.f.com MX (10 laba) -> (20 labb)"]
|
||||
01: Change: verb=DELETE
|
||||
key={labc.f.com }
|
||||
old=[laba]
|
||||
msg=["DELETE labc.f.com CNAME laba"]
|
||||
02: Change: verb=CHANGE
|
||||
key={labe.f.com }
|
||||
old=[10.10.10.15 10.10.10.16 10.10.10.17 10.10.10.18]
|
||||
new=[10.10.10.95 10.10.10.96 10.10.10.97 10.10.10.98]
|
||||
msg=["CHANGE labe.f.com A (10.10.10.15) -> (10.10.10.95)" "CHANGE labe.f.com A (10.10.10.16) -> (10.10.10.96)" "CHANGE labe.f.com A (10.10.10.17) -> (10.10.10.97)" "CHANGE labe.f.com A (10.10.10.18) -> (10.10.10.98)"]
|
||||
03: Change: verb=CREATE
|
||||
key={labf.f.com }
|
||||
new=["foo"]
|
||||
msg=["CREATE labf.f.com TXT \"foo\""]
|
||||
04: Change: verb=CHANGE
|
||||
key={labg.f.com }
|
||||
old=[10.10.10.15 10.10.10.16 10.10.10.17 10.10.10.18]
|
||||
new=[10.10.10.10 10.10.10.15 10.10.10.16 10.10.10.97]
|
||||
msg=["CHANGE labg.f.com NS (10.10.10.17) -> (10.10.10.10)" "CHANGE labg.f.com NS (10.10.10.18) -> (10.10.10.97)"]
|
||||
05: Change: verb=CHANGE
|
||||
key={labh.f.com }
|
||||
old=[labd]
|
||||
new=[1.2.3.4]
|
||||
msg=["DELETE labh.f.com CNAME labd" "CREATE labh.f.com A 1.2.3.4"]
|
||||
`,
|
||||
wantChangeRec: `
|
||||
ChangeList: len=12
|
||||
00: Change: verb=CREATE
|
||||
key={laba.f.com A}
|
||||
new=[1.2.3.5]
|
||||
msg=["CREATE laba.f.com A 1.2.3.5"]
|
||||
01: Change: verb=CHANGE
|
||||
key={laba.f.com MX}
|
||||
old=[10 laba]
|
||||
new=[20 labb]
|
||||
msg=["CHANGE laba.f.com MX (10 laba) -> (20 labb)"]
|
||||
02: Change: verb=DELETE
|
||||
key={labc.f.com CNAME}
|
||||
old=[laba]
|
||||
msg=["DELETE labc.f.com CNAME laba"]
|
||||
03: Change: verb=CHANGE
|
||||
key={labe.f.com A}
|
||||
old=[10.10.10.15]
|
||||
new=[10.10.10.95]
|
||||
msg=["CHANGE labe.f.com A (10.10.10.15) -> (10.10.10.95)"]
|
||||
04: Change: verb=CHANGE
|
||||
key={labe.f.com A}
|
||||
old=[10.10.10.16]
|
||||
new=[10.10.10.96]
|
||||
msg=["CHANGE labe.f.com A (10.10.10.16) -> (10.10.10.96)"]
|
||||
05: Change: verb=CHANGE
|
||||
key={labe.f.com A}
|
||||
old=[10.10.10.17]
|
||||
new=[10.10.10.97]
|
||||
msg=["CHANGE labe.f.com A (10.10.10.17) -> (10.10.10.97)"]
|
||||
06: Change: verb=CHANGE
|
||||
key={labe.f.com A}
|
||||
old=[10.10.10.18]
|
||||
new=[10.10.10.98]
|
||||
msg=["CHANGE labe.f.com A (10.10.10.18) -> (10.10.10.98)"]
|
||||
07: Change: verb=CREATE
|
||||
key={labf.f.com TXT}
|
||||
new=["foo"]
|
||||
msg=["CREATE labf.f.com TXT \"foo\""]
|
||||
08: Change: verb=CHANGE
|
||||
key={labg.f.com NS}
|
||||
old=[10.10.10.17]
|
||||
new=[10.10.10.10]
|
||||
msg=["CHANGE labg.f.com NS (10.10.10.17) -> (10.10.10.10)"]
|
||||
09: Change: verb=CHANGE
|
||||
key={labg.f.com NS}
|
||||
old=[10.10.10.18]
|
||||
new=[10.10.10.97]
|
||||
msg=["CHANGE labg.f.com NS (10.10.10.18) -> (10.10.10.97)"]
|
||||
10: Change: verb=DELETE
|
||||
key={labh.f.com CNAME}
|
||||
old=[labd]
|
||||
msg=["DELETE labh.f.com CNAME labd"]
|
||||
11: Change: verb=CREATE
|
||||
key={labh.f.com A}
|
||||
new=[1.2.3.4]
|
||||
msg=["CREATE labh.f.com A 1.2.3.4"]
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
||||
// Each "analyze*()" should return the same msgs, but a different ChangeList.
|
||||
// Sadly the analyze*() functions are destructive to the CompareConfig struct.
|
||||
// Therefore we have to run NewCompareConfig() each time.
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cl := analyzeByRecordSet(NewCompareConfig(tt.args.origin, tt.args.existing, tt.args.desired, tt.args.compFn))
|
||||
compareMsgs(t, "analyzeByRecordSet", tt.name, "RSet", cl, tt.wantMsgs)
|
||||
compareCL(t, "analyzeByRecordSet", tt.name, "RSet", cl, tt.wantChangeRSet)
|
||||
})
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cl := analyzeByLabel(NewCompareConfig(tt.args.origin, tt.args.existing, tt.args.desired, tt.args.compFn))
|
||||
compareMsgs(t, "analyzeByLabel", tt.name, "Label", cl, tt.wantMsgs)
|
||||
compareCL(t, "analyzeByLabel", tt.name, "Label", cl, tt.wantChangeLabel)
|
||||
})
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cl := analyzeByRecord(NewCompareConfig(tt.args.origin, tt.args.existing, tt.args.desired, tt.args.compFn))
|
||||
compareMsgs(t, "analyzeByRecord", tt.name, "Rec", cl, tt.wantMsgs)
|
||||
compareCL(t, "analyzeByRecord", tt.name, "Rec", cl, tt.wantChangeRec)
|
||||
})
|
||||
|
||||
// NB(tlim): There is no analyzeByZone(). ByZone() uses analyzeByRecord().
|
||||
|
||||
}
|
||||
}
|
||||
func Test_diffTargets(t *testing.T) {
|
||||
type args struct {
|
||||
existing []targetConfig
|
||||
desired []targetConfig
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want ChangeList
|
||||
}{
|
||||
|
||||
{
|
||||
name: "single",
|
||||
args: args{
|
||||
existing: []targetConfig{
|
||||
{compareable: "1.2.3.4", rec: testDataAA1234},
|
||||
},
|
||||
desired: []targetConfig{
|
||||
{compareable: "1.2.3.4", rec: testDataAA1234},
|
||||
},
|
||||
},
|
||||
//want: ,
|
||||
},
|
||||
|
||||
{
|
||||
name: "add1",
|
||||
args: args{
|
||||
existing: []targetConfig{
|
||||
{compareable: "1.2.3.4", rec: testDataAA1234},
|
||||
},
|
||||
desired: []targetConfig{
|
||||
{compareable: "1.2.3.4", rec: testDataAA1234},
|
||||
{compareable: "10 laba", rec: testDataAMX10a},
|
||||
},
|
||||
},
|
||||
want: ChangeList{
|
||||
Change{Type: CREATE,
|
||||
Key: models.RecordKey{NameFQDN: "laba.f.com", Type: "MX"},
|
||||
New: models.Records{testDataAMX10a},
|
||||
Msgs: []string{"CREATE laba.f.com MX 10 laba"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "del1",
|
||||
args: args{
|
||||
existing: []targetConfig{
|
||||
{compareable: "1.2.3.4", rec: testDataAA1234},
|
||||
{compareable: "10 laba", rec: testDataAMX10a},
|
||||
},
|
||||
desired: []targetConfig{
|
||||
{compareable: "1.2.3.4", rec: testDataAA1234},
|
||||
},
|
||||
},
|
||||
want: ChangeList{
|
||||
Change{Type: DELETE,
|
||||
Key: models.RecordKey{NameFQDN: "laba.f.com", Type: "MX"},
|
||||
Old: models.Records{testDataAMX10a},
|
||||
Msgs: []string{"DELETE laba.f.com MX 10 laba"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "change2nd",
|
||||
args: args{
|
||||
existing: []targetConfig{
|
||||
{compareable: "1.2.3.4", rec: testDataAA1234},
|
||||
{compareable: "10 laba", rec: testDataAMX10a},
|
||||
},
|
||||
desired: []targetConfig{
|
||||
{compareable: "1.2.3.4", rec: testDataAA1234},
|
||||
{compareable: "20 laba", rec: testDataAMX20b},
|
||||
},
|
||||
},
|
||||
want: ChangeList{
|
||||
Change{Type: CHANGE,
|
||||
Key: models.RecordKey{NameFQDN: "laba.f.com", Type: "MX"},
|
||||
Old: models.Records{testDataAMX10a},
|
||||
New: models.Records{testDataAMX20b},
|
||||
Msgs: []string{"CHANGE laba.f.com MX (10 laba) -> (20 labb)"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "del2nd",
|
||||
args: args{
|
||||
existing: []targetConfig{
|
||||
{compareable: "1.2.3.4", rec: testDataAA1234},
|
||||
{compareable: "5.6.7.8", rec: testDataAA5678},
|
||||
},
|
||||
desired: []targetConfig{
|
||||
{compareable: "1.2.3.4", rec: testDataAA1234},
|
||||
},
|
||||
},
|
||||
want: ChangeList{
|
||||
Change{Type: CHANGE,
|
||||
Key: models.RecordKey{NameFQDN: "laba.f.com", Type: "A"},
|
||||
Old: models.Records{testDataAA1234, testDataAA5678},
|
||||
New: models.Records{testDataAA1234},
|
||||
Msgs: []string{"DELETE laba.f.com A 5.6.7.8"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
//fmt.Printf("DEBUG: Test %02d\n", i)
|
||||
got := diffTargets(tt.args.existing, tt.args.desired)
|
||||
d := diff.Diff(strings.TrimSpace(justMsgString(got)), strings.TrimSpace(justMsgString(tt.want)))
|
||||
if d != "" {
|
||||
//fmt.Printf("DEBUG: %d %d\n", len(got), len(tt.want))
|
||||
t.Errorf("diffTargets()\n diff=%s", d)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_removeCommon(t *testing.T) {
|
||||
type args struct {
|
||||
existing []targetConfig
|
||||
desired []targetConfig
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []targetConfig
|
||||
want1 []targetConfig
|
||||
}{
|
||||
{
|
||||
name: "same",
|
||||
args: args{
|
||||
existing: []targetConfig{d0tc},
|
||||
desired: []targetConfig{d0tc},
|
||||
},
|
||||
want: []targetConfig{},
|
||||
want1: []targetConfig{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1 := removeCommon(tt.args.existing, tt.args.desired)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("removeCommon() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if (!(got1 == nil && tt.want1 == nil)) && !reflect.DeepEqual(got1, tt.want1) {
|
||||
t.Errorf("removeCommon() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func comparables(s []targetConfig) []string {
|
||||
var r []string
|
||||
for _, j := range s {
|
||||
r = append(r, j.compareable)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func Test_filterBy(t *testing.T) {
|
||||
type args struct {
|
||||
s []targetConfig
|
||||
m map[string]*targetConfig
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []targetConfig
|
||||
}{
|
||||
|
||||
{
|
||||
name: "removeall",
|
||||
args: args{
|
||||
s: []targetConfig{
|
||||
{compareable: "1.2.3.4", rec: testDataAA1234},
|
||||
{compareable: "10 laba", rec: testDataAMX10a},
|
||||
},
|
||||
m: map[string]*targetConfig{
|
||||
"1.2.3.4": {compareable: "1.2.3.4", rec: testDataAA1234},
|
||||
"10 laba": {compareable: "10 laba", rec: testDataAMX10a},
|
||||
},
|
||||
},
|
||||
want: []targetConfig{},
|
||||
},
|
||||
|
||||
{
|
||||
name: "keepall",
|
||||
args: args{
|
||||
s: []targetConfig{
|
||||
{compareable: "1.2.3.4", rec: testDataAA1234},
|
||||
{compareable: "10 laba", rec: testDataAMX10a},
|
||||
},
|
||||
m: map[string]*targetConfig{
|
||||
"nothing": {compareable: "1.2.3.4", rec: testDataAA1234},
|
||||
"matches": {compareable: "10 laba", rec: testDataAMX10a},
|
||||
},
|
||||
},
|
||||
want: []targetConfig{
|
||||
{compareable: "1.2.3.4", rec: testDataAA1234},
|
||||
{compareable: "10 laba", rec: testDataAMX10a},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "keepsome",
|
||||
args: args{
|
||||
s: []targetConfig{
|
||||
{compareable: "1.2.3.4", rec: testDataAA1234},
|
||||
{compareable: "10 laba", rec: testDataAMX10a},
|
||||
},
|
||||
m: map[string]*targetConfig{
|
||||
"nothing": {compareable: "1.2.3.4", rec: testDataAA1234},
|
||||
"10 laba": {compareable: "10 laba", rec: testDataAMX10a},
|
||||
},
|
||||
},
|
||||
want: []targetConfig{
|
||||
{compareable: "1.2.3.4", rec: testDataAA1234},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := filterBy(tt.args.s, tt.args.m); !reflect.DeepEqual(comparables(got), comparables(tt.want)) {
|
||||
t.Errorf("filterBy() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
214
pkg/diff2/compareconfig.go
Normal file
214
pkg/diff2/compareconfig.go
Normal file
|
@ -0,0 +1,214 @@
|
|||
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")
|
||||
}
|
||||
}
|
214
pkg/diff2/compareconfig_test.go
Normal file
214
pkg/diff2/compareconfig_test.go
Normal file
|
@ -0,0 +1,214 @@
|
|||
package diff2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"github.com/kylelemons/godebug/diff"
|
||||
)
|
||||
|
||||
func prettyPrint(i interface{}) string {
|
||||
s, _ := json.MarshalIndent(i, "", "\t")
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func TestNewCompareConfig(t *testing.T) {
|
||||
type args struct {
|
||||
origin string
|
||||
existing models.Records
|
||||
desired models.Records
|
||||
compFn ComparableFunc
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
|
||||
{
|
||||
name: "one",
|
||||
args: args{
|
||||
origin: "f.com",
|
||||
existing: models.Records{testDataAA1234},
|
||||
desired: models.Records{testDataAA1234clone},
|
||||
compFn: nil,
|
||||
},
|
||||
want: `
|
||||
ldata:
|
||||
ldata[00]: laba.f.com
|
||||
tdata[0]: "A" e(1, 1) d(1, 1)
|
||||
labelMap: len=1 map[laba.f.com:true]
|
||||
keyMap: len=1 map[{laba.f.com A}:true]
|
||||
existing: ["1.2.3.4"]
|
||||
desired: ["1.2.3.4"]
|
||||
origin: f.com
|
||||
compFn: <nil>
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
name: "cnameAdd",
|
||||
args: args{
|
||||
origin: "f.com",
|
||||
existing: models.Records{e11mx, d12},
|
||||
desired: models.Records{e11},
|
||||
compFn: nil,
|
||||
},
|
||||
want: `
|
||||
ldata:
|
||||
ldata[00]: labh.f.com
|
||||
tdata[0]: "A" e(1, 1) d(0, 0)
|
||||
tdata[1]: "MX" e(1, 1) d(0, 0)
|
||||
tdata[2]: "CNAME" e(0, 0) d(1, 1)
|
||||
labelMap: len=1 map[labh.f.com:true]
|
||||
keyMap: len=3 map[{labh.f.com A}:true {labh.f.com CNAME}:true {labh.f.com MX}:true]
|
||||
existing: ["22 ttt" "1.2.3.4"]
|
||||
desired: ["labd"]
|
||||
origin: f.com
|
||||
compFn: <nil>
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
name: "cnameDel",
|
||||
args: args{
|
||||
origin: "f.com",
|
||||
existing: models.Records{e11},
|
||||
desired: models.Records{d12, e11mx},
|
||||
compFn: nil,
|
||||
},
|
||||
want: `
|
||||
ldata:
|
||||
ldata[00]: labh.f.com
|
||||
tdata[0]: "CNAME" e(1, 1) d(0, 0)
|
||||
tdata[1]: "A" e(0, 0) d(1, 1)
|
||||
tdata[2]: "MX" e(0, 0) d(1, 1)
|
||||
labelMap: len=1 map[labh.f.com:true]
|
||||
keyMap: len=3 map[{labh.f.com A}:true {labh.f.com CNAME}:true {labh.f.com MX}:true]
|
||||
existing: ["labd"]
|
||||
desired: ["1.2.3.4" "22 ttt"]
|
||||
origin: f.com
|
||||
compFn: <nil>
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
name: "two",
|
||||
args: args{
|
||||
origin: "f.com",
|
||||
existing: models.Records{testDataAA1234, testDataCCa},
|
||||
desired: models.Records{testDataAA1234clone},
|
||||
compFn: nil,
|
||||
},
|
||||
want: `
|
||||
ldata:
|
||||
ldata[00]: laba.f.com
|
||||
tdata[0]: "A" e(1, 1) d(1, 1)
|
||||
ldata[01]: labc.f.com
|
||||
tdata[0]: "CNAME" e(1, 1) d(0, 0)
|
||||
labelMap: len=2 map[laba.f.com:true labc.f.com:true]
|
||||
keyMap: len=2 map[{laba.f.com A}:true {labc.f.com CNAME}:true]
|
||||
existing: ["1.2.3.4" "laba"]
|
||||
desired: ["1.2.3.4"]
|
||||
origin: f.com
|
||||
compFn: <nil>
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
name: "apex",
|
||||
args: args{
|
||||
origin: "f.com",
|
||||
existing: models.Records{testDataAA1234, testDataApexMX1aaa},
|
||||
desired: models.Records{testDataAA1234clone, testDataApexMX22bbb},
|
||||
compFn: nil,
|
||||
},
|
||||
want: `
|
||||
ldata:
|
||||
ldata[00]: f.com
|
||||
tdata[0]: "MX" e(1, 1) d(1, 1)
|
||||
ldata[01]: laba.f.com
|
||||
tdata[0]: "A" e(1, 1) d(1, 1)
|
||||
labelMap: len=2 map[f.com:true laba.f.com:true]
|
||||
keyMap: len=2 map[{f.com MX}:true {laba.f.com A}:true]
|
||||
existing: ["1.2.3.4" "1 aaa"]
|
||||
desired: ["1.2.3.4" "22 bbb"]
|
||||
origin: f.com
|
||||
compFn: <nil>
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
name: "many",
|
||||
args: args{
|
||||
origin: "f.com",
|
||||
existing: models.Records{testDataAA1234, testDataAMX10a, testDataCCa, testDataEA15},
|
||||
desired: models.Records{testDataAA1234clone, testDataAA12345, testDataAMX20b, d3, d4},
|
||||
compFn: nil,
|
||||
},
|
||||
want: `
|
||||
ldata:
|
||||
ldata[00]: laba.f.com
|
||||
tdata[0]: "A" e(1, 1) d(2, 2)
|
||||
tdata[1]: "MX" e(1, 1) d(1, 1)
|
||||
ldata[01]: labc.f.com
|
||||
tdata[0]: "CNAME" e(1, 1) d(0, 0)
|
||||
ldata[02]: labe.f.com
|
||||
tdata[0]: "A" e(1, 1) d(2, 2)
|
||||
labelMap: len=3 map[laba.f.com:true labc.f.com:true labe.f.com:true]
|
||||
keyMap: len=4 map[{laba.f.com A}:true {laba.f.com MX}:true {labc.f.com CNAME}:true {labe.f.com A}:true]
|
||||
existing: ["1.2.3.4" "10 laba" "laba" "10.10.10.15"]
|
||||
desired: ["1.2.3.4" "1.2.3.5" "20 labb" "10.10.10.95" "10.10.10.96"]
|
||||
origin: f.com
|
||||
compFn: <nil>
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
name: "all",
|
||||
args: args{
|
||||
origin: "f.com",
|
||||
existing: models.Records{testDataAA1234, testDataAMX10a, testDataCCa, testDataEA15, e4, e5, e6, e7, e8, e9, e10, e11},
|
||||
desired: models.Records{testDataAA1234clone, testDataAA12345, testDataAMX20b, d3, d4, d5, d6, d7, d8, d9, d10, d11, d12},
|
||||
compFn: nil,
|
||||
},
|
||||
want: `
|
||||
ldata:
|
||||
ldata[00]: laba.f.com
|
||||
tdata[0]: "A" e(1, 1) d(2, 2)
|
||||
tdata[1]: "MX" e(1, 1) d(1, 1)
|
||||
ldata[01]: labc.f.com
|
||||
tdata[0]: "CNAME" e(1, 1) d(0, 0)
|
||||
ldata[02]: labe.f.com
|
||||
tdata[0]: "A" e(4, 4) d(4, 4)
|
||||
ldata[03]: labf.f.com
|
||||
tdata[0]: "TXT" e(0, 0) d(1, 1)
|
||||
ldata[04]: labg.f.com
|
||||
tdata[0]: "NS" e(4, 4) d(4, 4)
|
||||
ldata[05]: labh.f.com
|
||||
tdata[0]: "CNAME" e(1, 1) d(0, 0)
|
||||
tdata[1]: "A" e(0, 0) d(1, 1)
|
||||
labelMap: len=6 map[laba.f.com:true labc.f.com:true labe.f.com:true labf.f.com:true labg.f.com:true labh.f.com:true]
|
||||
keyMap: len=8 map[{laba.f.com A}:true {laba.f.com MX}:true {labc.f.com CNAME}:true {labe.f.com A}:true {labf.f.com TXT}:true {labg.f.com NS}:true {labh.f.com A}:true {labh.f.com CNAME}:true]
|
||||
existing: ["1.2.3.4" "10 laba" "laba" "10.10.10.15" "10.10.10.16" "10.10.10.17" "10.10.10.18" "10.10.10.15" "10.10.10.16" "10.10.10.17" "10.10.10.18" "labd"]
|
||||
desired: ["1.2.3.4" "1.2.3.5" "20 labb" "10.10.10.95" "10.10.10.96" "10.10.10.97" "10.10.10.98" "\"foo\"" "10.10.10.10" "10.10.10.15" "10.10.10.16" "10.10.10.97" "1.2.3.4"]
|
||||
origin: f.com
|
||||
compFn: <nil>
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cc := NewCompareConfig(tt.args.origin, tt.args.existing, tt.args.desired, tt.args.compFn)
|
||||
got := strings.TrimSpace(cc.String())
|
||||
tt.want = strings.TrimSpace(tt.want)
|
||||
if got != tt.want {
|
||||
d := diff.Diff(got, tt.want)
|
||||
t.Errorf("NewCompareConfig() = \n%s\n", d)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
185
pkg/diff2/diff2.go
Normal file
185
pkg/diff2/diff2.go
Normal file
|
@ -0,0 +1,185 @@
|
|||
package diff2
|
||||
|
||||
//go:generate stringer -type=Verb
|
||||
|
||||
// This module provides functions that "diff" the existing records
|
||||
// against the desired records.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
)
|
||||
|
||||
type Verb int
|
||||
|
||||
const (
|
||||
_ Verb = iota // Skip the first value of 0
|
||||
CREATE // Create a record/recordset/label where none existed before.
|
||||
CHANGE // Change existing record/recordset/label
|
||||
DELETE // Delete existing record/recordset/label
|
||||
)
|
||||
|
||||
type ChangeList []Change
|
||||
|
||||
type Change struct {
|
||||
Type Verb // Add, Change, Delete
|
||||
|
||||
Key models.RecordKey // .Key.Type is "" unless using ByRecordSet
|
||||
Old models.Records
|
||||
New models.Records // any changed or added records at Key.
|
||||
Msgs []string // Human-friendly explanation of what changed
|
||||
MsgsByKey map[models.RecordKey][]string // Messages for a given key
|
||||
}
|
||||
|
||||
// ByRecordSet takes two lists of records (existing and desired) and
|
||||
// returns instructions for turning existing into desired.
|
||||
//
|
||||
// Use this with DNS providers whose API updates one recordset at a
|
||||
// time. A recordset is all the records of a particular type at a
|
||||
// label. For example, if www.example.com has 3 A records and a TXT
|
||||
// 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.
|
||||
//
|
||||
// Examples include:
|
||||
func ByRecordSet(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
|
||||
// dc stores the desired state.
|
||||
|
||||
desired := dc.Records
|
||||
var err error
|
||||
desired, err = handsoff(existing, desired, dc.Unmanaged, dc.UnmanagedUnsafe) // Handle UNMANAGED()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cc := NewCompareConfig(dc.Name, existing, desired, compFunc)
|
||||
instructions := analyzeByRecordSet(cc)
|
||||
return processPurge(instructions, !dc.KeepUnknown), nil
|
||||
}
|
||||
|
||||
// ByLabel takes two lists of records (existing and desired) and
|
||||
// returns instructions for turning existing into desired.
|
||||
//
|
||||
// Use this with DNS providers whose API updates one label at a
|
||||
// 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.
|
||||
//
|
||||
// Examples include:
|
||||
func ByLabel(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
|
||||
// dc stores the desired state.
|
||||
|
||||
desired := dc.Records
|
||||
var err error
|
||||
desired, err = handsoff(existing, desired, dc.Unmanaged, dc.UnmanagedUnsafe) // Handle UNMANAGED()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cc := NewCompareConfig(dc.Name, existing, desired, compFunc)
|
||||
instructions := analyzeByLabel(cc)
|
||||
return processPurge(instructions, !dc.KeepUnknown), nil
|
||||
}
|
||||
|
||||
// ByRecord takes two lists of records (existing and desired) and
|
||||
// returns instructions for turning existing into desired.
|
||||
//
|
||||
// Use this with DNS providers whose API updates one record at a time.
|
||||
//
|
||||
// Examples include: INWX
|
||||
func ByRecord(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
|
||||
// dc stores the desired state.
|
||||
|
||||
desired := dc.Records
|
||||
var err error
|
||||
desired, err = handsoff(existing, desired, dc.Unmanaged, dc.UnmanagedUnsafe) // Handle UNMANAGED()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cc := NewCompareConfig(dc.Name, existing, desired, compFunc)
|
||||
instructions := analyzeByRecord(cc)
|
||||
return processPurge(instructions, !dc.KeepUnknown), nil
|
||||
}
|
||||
|
||||
// ByZone takes two lists of records (existing and desired) and
|
||||
// returns text one would output to users describing the change.
|
||||
//
|
||||
// Use this with DNS providers whose API updates the entire zone at a
|
||||
// time. That is, to make any change (1 record or many) the entire DNS
|
||||
// zone is uploaded.
|
||||
//
|
||||
// The user should see a list of changes as if individual records were
|
||||
// updated.
|
||||
//
|
||||
// The caller of this function should:
|
||||
//
|
||||
// changed, msgs := diff2.ByZone(existing, desired, origin, nil
|
||||
// fmt.Sprintf("CREATING ZONEFILE FOR THE FIRST TIME: dir/example.com.zone"))
|
||||
// if changed {
|
||||
// // output msgs
|
||||
// // generate the zone using the "desired" records
|
||||
// }
|
||||
//
|
||||
// Example providers include: BIND
|
||||
func ByZone(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) ([]string, bool, error) {
|
||||
// dc stores the desired state.
|
||||
|
||||
if len(existing) == 0 {
|
||||
// Nothing previously existed. No need to output a list of individual changes.
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
desired := dc.Records
|
||||
var err error
|
||||
desired, err = handsoff(existing, desired, dc.Unmanaged, dc.UnmanagedUnsafe) // Handle UNMANAGED()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
cc := NewCompareConfig(dc.Name, existing, desired, compFunc)
|
||||
instructions := analyzeByRecord(cc)
|
||||
instructions = processPurge(instructions, !dc.KeepUnknown)
|
||||
return justMsgs(instructions), len(instructions) != 0, nil
|
||||
}
|
||||
|
||||
/*
|
||||
nil, nil :
|
||||
nil, nonzero : nil, true, nil
|
||||
nonzero, nil : msgs, true, nil
|
||||
nonzero, nonzero :
|
||||
|
||||
existing: changes : return msgs, true, nil
|
||||
existing: no changes : return nil, false, nil
|
||||
not existing: no changes: return nil, false, nil
|
||||
not existing: changes : return nil, true, nil
|
||||
*/
|
||||
|
||||
func (c Change) String() string {
|
||||
var buf bytes.Buffer
|
||||
b := &buf
|
||||
|
||||
fmt.Fprintf(b, "Change: verb=%v\n", c.Type)
|
||||
fmt.Fprintf(b, " key=%v\n", c.Key)
|
||||
if len(c.Old) != 0 {
|
||||
fmt.Fprintf(b, " old=%v\n", c.Old)
|
||||
}
|
||||
if len(c.New) != 0 {
|
||||
fmt.Fprintf(b, " new=%v\n", c.New)
|
||||
}
|
||||
fmt.Fprintf(b, " msg=%q\n", c.Msgs)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (cl ChangeList) String() string {
|
||||
var buf bytes.Buffer
|
||||
b := &buf
|
||||
|
||||
fmt.Fprintf(b, "ChangeList: len=%d\n", len(cl))
|
||||
for i, j := range cl {
|
||||
fmt.Fprintf(b, "%02d: %s", i, j)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
45
pkg/diff2/groupsort.go
Normal file
45
pkg/diff2/groupsort.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package diff2
|
||||
|
||||
import (
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"github.com/StackExchange/dnscontrol/v3/pkg/prettyzone"
|
||||
)
|
||||
|
||||
type recset struct {
|
||||
Key models.RecordKey
|
||||
Recs []*models.RecordConfig
|
||||
}
|
||||
|
||||
func groupbyRSet(recs models.Records, origin string) []recset {
|
||||
|
||||
if len(recs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sort the NameFQDN to a consistent order. The actual sort methodology
|
||||
// doesn't matter as long as equal values are adjacent.
|
||||
// Use the PrettySort ordering so that the records are in a nice order.
|
||||
pretty := prettyzone.PrettySort(recs, origin, 0, nil)
|
||||
recs = pretty.Records
|
||||
|
||||
var result []recset
|
||||
var acc []*models.RecordConfig
|
||||
|
||||
// Do the first element
|
||||
prevkey := recs[0].Key()
|
||||
acc = append(acc, recs[0])
|
||||
|
||||
for i := 1; i < len(recs); i++ {
|
||||
curkey := recs[i].Key()
|
||||
if prevkey == curkey { // A run of equal keys.
|
||||
acc = append(acc, recs[i])
|
||||
} else { // New key. Append old data to result and start new acc.
|
||||
result = append(result, recset{Key: prevkey, Recs: acc})
|
||||
acc = []*models.RecordConfig{recs[i]}
|
||||
}
|
||||
prevkey = curkey
|
||||
}
|
||||
result = append(result, recset{Key: prevkey, Recs: acc}) // The remainder
|
||||
|
||||
return result
|
||||
}
|
45
pkg/diff2/groupsort_test.go
Normal file
45
pkg/diff2/groupsort_test.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package diff2
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
)
|
||||
|
||||
func makeRec(label, rtype, content string) *models.RecordConfig {
|
||||
origin := "f.com"
|
||||
r := models.RecordConfig{}
|
||||
r.SetLabel(label, origin)
|
||||
r.PopulateFromString(rtype, content, origin)
|
||||
return &r
|
||||
}
|
||||
func makeRecSet(recs ...*models.RecordConfig) *recset {
|
||||
result := recset{}
|
||||
result.Key = recs[0].Key()
|
||||
result.Recs = append(result.Recs, recs...)
|
||||
return &result
|
||||
}
|
||||
func Test_groupbyRSet(t *testing.T) {
|
||||
|
||||
wwwa1 := makeRec("www", "A", "1.1.1.1")
|
||||
wwwa2 := makeRec("www", "A", "2.2.2.2")
|
||||
zzza1 := makeRec("zzz", "A", "1.1.0.0")
|
||||
zzza2 := makeRec("zzz", "A", "2.2.0.0")
|
||||
wwwmx1 := makeRec("www", "MX", "1 mx1.foo.com.")
|
||||
wwwmx2 := makeRec("www", "MX", "2 mx2.foo.com.")
|
||||
zzzmx1 := makeRec("zzz", "MX", "1 mx.foo.com.")
|
||||
orig := models.Records{wwwa1, wwwa2, zzza1, zzza2, wwwmx1, wwwmx2, zzzmx1}
|
||||
wantResult := []recset{
|
||||
*makeRecSet(wwwa1, wwwa2),
|
||||
*makeRecSet(wwwmx1, wwwmx2),
|
||||
*makeRecSet(zzza1, zzza2),
|
||||
*makeRecSet(zzzmx1),
|
||||
}
|
||||
|
||||
t.Run("afew", func(t *testing.T) {
|
||||
if gotResult := groupbyRSet(orig, "f.com"); !reflect.DeepEqual(gotResult, wantResult) {
|
||||
t.Errorf("groupbyRSet() = %v, want %v", gotResult, wantResult)
|
||||
}
|
||||
})
|
||||
}
|
7
pkg/diff2/highest.go
Normal file
7
pkg/diff2/highest.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package diff2
|
||||
|
||||
// Return the highest valid index for an array. The equiv of len(s)-1, but with
|
||||
// less likelihood that you'll commit an off-by-one error.
|
||||
func highest[S ~[]T, T any](s S) int {
|
||||
return len(s) - 1
|
||||
}
|
10
pkg/diff2/min.go
Normal file
10
pkg/diff2/min.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package diff2
|
||||
|
||||
import "golang.org/x/exp/constraints"
|
||||
|
||||
func min[T constraints.Ordered](a, b T) T {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
22
pkg/diff2/nopurge.go
Normal file
22
pkg/diff2/nopurge.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package diff2
|
||||
|
||||
func processPurge(instructions ChangeList, nopurge bool) ChangeList {
|
||||
|
||||
if nopurge {
|
||||
return instructions
|
||||
}
|
||||
|
||||
// TODO(tlim): This can probably be done without allocations but it
|
||||
// works and I won't want to prematurely optimize.
|
||||
|
||||
newinstructions := make(ChangeList, 0, len(instructions))
|
||||
for _, j := range instructions {
|
||||
if j.Type == DELETE {
|
||||
continue
|
||||
}
|
||||
newinstructions = append(newinstructions, j)
|
||||
}
|
||||
|
||||
return newinstructions
|
||||
|
||||
}
|
198
pkg/diff2/notes.txt
Normal file
198
pkg/diff2/notes.txt
Normal file
|
@ -0,0 +1,198 @@
|
|||
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
|
||||
}
|
||||
}
|
129
pkg/diff2/unmanaged.go
Normal file
129
pkg/diff2/unmanaged.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package diff2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"github.com/StackExchange/dnscontrol/v3/pkg/printer"
|
||||
)
|
||||
|
||||
func handsoff(
|
||||
existing, desired models.Records,
|
||||
unmanaged []*models.UnmanagedConfig,
|
||||
beSafe bool,
|
||||
) (models.Records, error) {
|
||||
|
||||
// What foreign items should we ignore?
|
||||
foreign, err := manyQueries(existing, unmanaged)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(foreign) != 0 {
|
||||
printer.Printf("INFO: Foreign records being ignored: (%d)\n", len(foreign))
|
||||
for i, r := range foreign {
|
||||
printer.Printf("- % 4d: %s %s %s\n", i, r.GetLabelFQDN(), r.Type, r.GetTargetRFC1035Quoted())
|
||||
}
|
||||
}
|
||||
|
||||
// What desired items might conflict?
|
||||
conflicts, err := manyQueries(desired, unmanaged)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(conflicts) != 0 {
|
||||
level := "WARN"
|
||||
if beSafe {
|
||||
level = "ERROR"
|
||||
}
|
||||
printer.Printf("%s: dnsconfig.js records that overlap MANAGED: (%d)\n", level, len(conflicts))
|
||||
for i, r := range conflicts {
|
||||
printer.Printf("- % 4d: %s %s %s\n", i, r.GetLabelFQDN(), r.Type, r.GetTargetRFC1035Quoted())
|
||||
}
|
||||
if beSafe {
|
||||
return nil, fmt.Errorf("ERROR: Unsafe to continue. Add DISABLE_UNMANAGED_SAFETY_CHECK to D() to override")
|
||||
}
|
||||
}
|
||||
|
||||
// Add the foreign items to the desired list.
|
||||
// (Rather than literally ignoring them, we just add them to the desired list
|
||||
// and all the diffing algorithms become more simple.)
|
||||
desired = append(desired, foreign...)
|
||||
|
||||
return desired, nil
|
||||
}
|
||||
|
||||
func manyQueries(rcs models.Records, queries []*models.UnmanagedConfig) (result models.Records, err error) {
|
||||
|
||||
for _, q := range queries {
|
||||
|
||||
lab := q.Label
|
||||
if lab == "" {
|
||||
lab = "*"
|
||||
}
|
||||
glabel, err := glob.Compile(lab)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
targ := q.Target
|
||||
if targ == "" {
|
||||
targ = "*"
|
||||
}
|
||||
gtarget, err := glob.Compile(targ)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hasRType := compileTypeGlob(q.RType)
|
||||
|
||||
for _, rc := range rcs {
|
||||
if match(rc, glabel, gtarget, hasRType) {
|
||||
result = append(result, rc)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func compileTypeGlob(g string) map[string]bool {
|
||||
m := map[string]bool{}
|
||||
for _, j := range strings.Split(g, ",") {
|
||||
m[strings.TrimSpace(j)] = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func match(rc *models.RecordConfig, glabel, gtarget glob.Glob, hasRType map[string]bool) bool {
|
||||
printer.Printf("DEBUG: match(%v, %v, %v, %v)\n", rc.NameFQDN, glabel, gtarget, hasRType)
|
||||
|
||||
// _ = glabel.Match(rc.NameFQDN)
|
||||
// _ = matchType(rc.Type, hasRType)
|
||||
// x := rc.GetTargetField()
|
||||
// _ = gtarget.Match(x)
|
||||
|
||||
if !glabel.Match(rc.NameFQDN) {
|
||||
printer.Printf("DEBUG: REJECTED LABEL: %s:%v\n", rc.NameFQDN, glabel)
|
||||
return false
|
||||
} else if !matchType(rc.Type, hasRType) {
|
||||
printer.Printf("DEBUG: REJECTED TYPE: %s:%v\n", rc.Type, hasRType)
|
||||
return false
|
||||
} else if gtarget == nil {
|
||||
return true
|
||||
} else if !gtarget.Match(rc.GetTargetField()) {
|
||||
printer.Printf("DEBUG: REJECTED TARGET: %v:%v\n", rc.GetTargetField(), gtarget)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func matchType(s string, hasRType map[string]bool) bool {
|
||||
printer.Printf("DEBUG: matchType map=%v\n", hasRType)
|
||||
if len(hasRType) == 0 {
|
||||
return true
|
||||
}
|
||||
_, ok := hasRType[s]
|
||||
return ok
|
||||
}
|
147
pkg/diff2/unmanaged_test.go
Normal file
147
pkg/diff2/unmanaged_test.go
Normal file
|
@ -0,0 +1,147 @@
|
|||
package diff2
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"github.com/gobwas/glob"
|
||||
)
|
||||
|
||||
var rmapNil map[string]bool
|
||||
var rmapAMX = map[string]bool{
|
||||
"A": true,
|
||||
"MX": true,
|
||||
}
|
||||
var rmapCNAME = map[string]bool{
|
||||
"CNAME": true,
|
||||
}
|
||||
|
||||
func Test_match(t *testing.T) {
|
||||
|
||||
testRecLammaA1234 := makeRec("lamma", "A", "1.2.3.4")
|
||||
|
||||
type args struct {
|
||||
rc *models.RecordConfig
|
||||
glabel glob.Glob
|
||||
gtarget glob.Glob
|
||||
hasRType map[string]bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
|
||||
{
|
||||
name: "match3",
|
||||
args: args{
|
||||
rc: testRecLammaA1234,
|
||||
glabel: glob.MustCompile("lam*"),
|
||||
hasRType: rmapAMX,
|
||||
gtarget: glob.MustCompile("1.2.3.*"),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: "match2",
|
||||
args: args{
|
||||
rc: testRecLammaA1234,
|
||||
glabel: glob.MustCompile("lam*"),
|
||||
hasRType: rmapAMX,
|
||||
gtarget: nil,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: "match1",
|
||||
args: args{
|
||||
rc: testRecLammaA1234,
|
||||
glabel: glob.MustCompile("lam*"),
|
||||
hasRType: rmapNil,
|
||||
gtarget: nil,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: "reject1",
|
||||
args: args{
|
||||
rc: testRecLammaA1234,
|
||||
glabel: glob.MustCompile("yyyy"),
|
||||
hasRType: rmapAMX,
|
||||
gtarget: glob.MustCompile("1.2.3.*"),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: "reject2",
|
||||
args: args{
|
||||
rc: testRecLammaA1234,
|
||||
glabel: glob.MustCompile("lam*"),
|
||||
hasRType: rmapCNAME,
|
||||
gtarget: glob.MustCompile("1.2.3.*"),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: "reject3",
|
||||
args: args{
|
||||
rc: testRecLammaA1234,
|
||||
glabel: glob.MustCompile("lam*"),
|
||||
hasRType: rmapAMX,
|
||||
gtarget: glob.MustCompile("zzzzz"),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := match(tt.args.rc, tt.args.glabel, tt.args.gtarget, tt.args.hasRType); got != tt.want {
|
||||
t.Errorf("match() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_matchType(t *testing.T) {
|
||||
type args struct {
|
||||
s string
|
||||
hasRType map[string]bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
|
||||
{
|
||||
name: "matchCNAME",
|
||||
args: args{"CNAME", rmapCNAME},
|
||||
want: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: "rejectCNAME",
|
||||
args: args{"MX", rmapCNAME},
|
||||
want: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: "matchNIL",
|
||||
args: args{"CNAME", rmapNil},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := matchType(tt.args.s, tt.args.hasRType); got != tt.want {
|
||||
t.Errorf("matchType() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
26
pkg/diff2/verb_string.go
Normal file
26
pkg/diff2/verb_string.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Code generated by "stringer -type=Verb"; DO NOT EDIT.
|
||||
|
||||
package diff2
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[CREATE-1]
|
||||
_ = x[CHANGE-2]
|
||||
_ = x[DELETE-3]
|
||||
}
|
||||
|
||||
const _Verb_name = "CREATECHANGEDELETE"
|
||||
|
||||
var _Verb_index = [...]uint8{0, 6, 12, 18}
|
||||
|
||||
func (i Verb) String() string {
|
||||
i -= 1
|
||||
if i < 0 || i >= Verb(len(_Verb_index)-1) {
|
||||
return "Verb(" + strconv.FormatInt(int64(i+1), 10) + ")"
|
||||
}
|
||||
return _Verb_name[_Verb_index[i]:_Verb_index[i+1]]
|
||||
}
|
|
@ -109,6 +109,7 @@ function newDomain(name, registrar) {
|
|||
nameservers: [],
|
||||
ignored_names: [],
|
||||
ignored_targets: [],
|
||||
unmanaged: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -610,6 +611,11 @@ function IGNORE_NAME(name, rTypes) {
|
|||
}
|
||||
return function (d) {
|
||||
d.ignored_names.push({ pattern: name, types: rTypes });
|
||||
d.unmanaged.push({
|
||||
label_pattern: name,
|
||||
rType_pattern: rTypes,
|
||||
target_pattern: '*',
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -622,10 +628,14 @@ var IGNORE_NAME_DISABLE_SAFETY_CHECK = {
|
|||
// See https://github.com/StackExchange/dnscontrol/issues/1106
|
||||
};
|
||||
|
||||
// IGNORE_TARGET(target, rType)
|
||||
function IGNORE_TARGET(target, rType) {
|
||||
return function (d) {
|
||||
d.ignored_targets.push({ pattern: target, type: rType });
|
||||
d.unmanaged.push({
|
||||
label_pattern: '*',
|
||||
rType_pattern: rType,
|
||||
target_pattern: target,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -667,6 +677,34 @@ function AUTODNSSEC(d) {
|
|||
);
|
||||
}
|
||||
|
||||
function UNMANAGED(label_pattern, rType_pattern, target_pattern) {
|
||||
if (rType_pattern === undefined) {
|
||||
rType_pattern = '*';
|
||||
}
|
||||
if (rType_pattern === "") {
|
||||
rType_pattern = '*';
|
||||
}
|
||||
if (target_pattern === undefined) {
|
||||
target_pattern = '*';
|
||||
}
|
||||
return function (d) {
|
||||
d.unmanaged.push({
|
||||
label_pattern: label_pattern,
|
||||
rType_pattern: rType_pattern,
|
||||
target_pattern: target_pattern,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function DISABLE_UNMANAGED_SAFETY_CHECK(d) {
|
||||
// This disables a safety check intended to prevent DNSControl and
|
||||
// another system getting into a battle as they both try to update
|
||||
// the same record over and over, back and forth. However, people
|
||||
// kept asking for it so... caveat emptor!
|
||||
// It only affects the current domain.
|
||||
d.unmanaged_disable_safety_check = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
|
|
|
@ -86,7 +86,7 @@ func TestParsedFiles(t *testing.T) {
|
|||
as := string(actualJSON)
|
||||
_, _ = es, as
|
||||
// When debugging, leave behind the actual result:
|
||||
// os.WriteFile(expectedFile+".ACTUAL", []byte(es), 0644)
|
||||
//os.WriteFile(expectedFile+".ACTUAL", []byte(as), 0644)
|
||||
testifyrequire.JSONEqf(t, es, as, "EXPECTING %q = \n```\n%s\n```", expectedFile, as)
|
||||
|
||||
// For each domain, if there is a zone file, test against it:
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
{
|
||||
"registrars": [],
|
||||
"dns_providers": [],
|
||||
"domains": [
|
||||
{
|
||||
"name": "foo.com",
|
||||
"registrar": "none",
|
||||
"dnsProviders": {},
|
||||
"records": [],
|
||||
"ignored_names": [
|
||||
{
|
||||
"pattern": "testignore",
|
||||
|
@ -39,10 +43,48 @@
|
|||
"type": "CNAME"
|
||||
}
|
||||
],
|
||||
"name": "foo.com",
|
||||
"records": [],
|
||||
"registrar": "none"
|
||||
"unmanaged": [
|
||||
{
|
||||
"label_pattern": "testignore",
|
||||
"rType_pattern": "*",
|
||||
"target_pattern": "*"
|
||||
},
|
||||
{
|
||||
"label_pattern": "testignore2",
|
||||
"rType_pattern": "A",
|
||||
"target_pattern": "*"
|
||||
},
|
||||
{
|
||||
"label_pattern": "testignore3",
|
||||
"rType_pattern": "A, CNAME, TXT",
|
||||
"target_pattern": "*"
|
||||
},
|
||||
{
|
||||
"label_pattern": "testignore4",
|
||||
"rType_pattern": "*",
|
||||
"target_pattern": "*"
|
||||
},
|
||||
{
|
||||
"label_pattern": "*",
|
||||
"rType_pattern": "CNAME",
|
||||
"target_pattern": "testtarget"
|
||||
},
|
||||
{
|
||||
"label_pattern": "legacyignore",
|
||||
"rType_pattern": "*",
|
||||
"target_pattern": "*"
|
||||
},
|
||||
{
|
||||
"label_pattern": "@",
|
||||
"rType_pattern": "*",
|
||||
"target_pattern": "*"
|
||||
},
|
||||
{
|
||||
"label_pattern": "*",
|
||||
"rType_pattern": "CNAME",
|
||||
"target_pattern": "@"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"registrars": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -12,7 +12,14 @@
|
|||
"pattern": "\\*.testignore",
|
||||
"types": "*"
|
||||
}
|
||||
],
|
||||
"unmanaged": [
|
||||
{
|
||||
"label_pattern": "\\*.testignore",
|
||||
"rType_pattern": "*",
|
||||
"target_pattern": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
6
pkg/js/parse_tests/042-unmanaged.js
Normal file
6
pkg/js/parse_tests/042-unmanaged.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
D("foo.com", "none"
|
||||
, UNMANAGED("one")
|
||||
, UNMANAGED("two", "A, CNAME")
|
||||
, UNMANAGED("three", "TXT", "findme")
|
||||
, UNMANAGED("notype", "", "targglob")
|
||||
);
|
34
pkg/js/parse_tests/042-unmanaged.json
Normal file
34
pkg/js/parse_tests/042-unmanaged.json
Normal file
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"registrars": [],
|
||||
"dns_providers": [],
|
||||
"domains": [
|
||||
{
|
||||
"name": "foo.com",
|
||||
"registrar": "none",
|
||||
"dnsProviders": {},
|
||||
"records": [],
|
||||
"unmanaged": [
|
||||
{
|
||||
"label_pattern": "one",
|
||||
"rType_pattern": "*",
|
||||
"target_pattern": "*"
|
||||
},
|
||||
{
|
||||
"label_pattern": "two",
|
||||
"rType_pattern": "A, CNAME",
|
||||
"target_pattern": "*"
|
||||
},
|
||||
{
|
||||
"label_pattern": "three",
|
||||
"rType_pattern": "TXT",
|
||||
"target_pattern": "findme"
|
||||
},
|
||||
{
|
||||
"label_pattern": "notype",
|
||||
"rType_pattern": "*",
|
||||
"target_pattern": "targglob"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
5
pkg/js/parse_tests/043-safety.js
Normal file
5
pkg/js/parse_tests/043-safety.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
D("unsafe.com", "none"
|
||||
, DISABLE_UNMANAGED_SAFETY_CHECK
|
||||
);
|
||||
D("safe.com", "none"
|
||||
);
|
19
pkg/js/parse_tests/043-safety.json
Normal file
19
pkg/js/parse_tests/043-safety.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"registrars": [],
|
||||
"dns_providers": [],
|
||||
"domains": [
|
||||
{
|
||||
"name": "unsafe.com",
|
||||
"registrar": "none",
|
||||
"dnsProviders": {},
|
||||
"records": [],
|
||||
"unmanaged_disable_safety_check": true
|
||||
},
|
||||
{
|
||||
"name": "safe.com",
|
||||
"registrar": "none",
|
||||
"dnsProviders": {},
|
||||
"records": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -484,11 +484,11 @@ func TestZoneLabelLess(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, test := range tests {
|
||||
actual := zoneLabelLess(test.e1, test.e2)
|
||||
actual := LabelLess(test.e1, test.e2)
|
||||
if test.expected != actual {
|
||||
t.Errorf("%v: expected (%v) got (%v)\n", test.e1, test.e2, actual)
|
||||
}
|
||||
actual = zoneLabelLess(test.e2, test.e1)
|
||||
actual = LabelLess(test.e2, test.e1)
|
||||
// The reverse should work too:
|
||||
var expected bool
|
||||
if test.e1 == test.e2 {
|
||||
|
|
|
@ -26,6 +26,8 @@ func (z *ZoneGenData) Less(i, j int) bool {
|
|||
a, b := z.Records[i], z.Records[j]
|
||||
|
||||
// Sort by name.
|
||||
|
||||
// If we are at the apex, use "@" in the sorting.
|
||||
compA, compB := a.NameFQDN, b.NameFQDN
|
||||
if compA != compB {
|
||||
if a.Name == "@" {
|
||||
|
@ -34,7 +36,7 @@ func (z *ZoneGenData) Less(i, j int) bool {
|
|||
if b.Name == "@" {
|
||||
compB = "@"
|
||||
}
|
||||
return zoneLabelLess(compA, compB)
|
||||
return LabelLess(compA, compB)
|
||||
}
|
||||
|
||||
// sub-sort by type
|
||||
|
@ -103,7 +105,7 @@ func (z *ZoneGenData) Less(i, j int) bool {
|
|||
return a.String() < b.String()
|
||||
}
|
||||
|
||||
func zoneLabelLess(a, b string) bool {
|
||||
func LabelLess(a, b string) bool {
|
||||
// Compare two zone labels for the purpose of sorting the RRs in a Zone.
|
||||
|
||||
// If they are equal, we are done. All other code is simplified
|
||||
|
|
|
@ -185,6 +185,7 @@ func (api *autoDNSProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mo
|
|||
// Insert Future diff2 version here.
|
||||
|
||||
return corrections, nil
|
||||
|
||||
}
|
||||
|
||||
// GetNameservers returns the nameservers for a domain.
|
||||
|
|
|
@ -450,4 +450,5 @@ func (c *axfrddnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
|
|||
// Insert Future diff2 version here.
|
||||
|
||||
return corrections, nil
|
||||
|
||||
}
|
||||
|
|
|
@ -197,7 +197,7 @@ func (a *azurednsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
|
|||
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
|
||||
|
||||
var corrections []*models.Correction
|
||||
if !diff2.EnableDiff2 || true { // Remove "|| true" when diff2 version arrives
|
||||
if !diff2.EnableDiff2 {
|
||||
|
||||
differ := diff.New(dc)
|
||||
namesToUpdate, err := differ.ChangedGroups(existingRecords)
|
||||
|
@ -330,7 +330,69 @@ func (a *azurednsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
|
|||
return corrections, nil
|
||||
}
|
||||
|
||||
// Insert Future diff2 version here.
|
||||
// Azure is a "ByRSet" API.
|
||||
|
||||
instructions, err := diff2.ByLabel(existingRecords, dc, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, inst := range instructions {
|
||||
switch inst.Type {
|
||||
|
||||
case diff2.CHANGE, diff2.CREATE:
|
||||
var rrset *adns.RecordSet
|
||||
var recordName string
|
||||
var recordType adns.RecordType
|
||||
if len(inst.Old) == 0 { // Create
|
||||
rrset = &adns.RecordSet{Type: to.StringPtr(inst.Key.Type), Properties: &adns.RecordSetProperties{}}
|
||||
recordType, _ = nativeToRecordType(to.StringPtr(inst.Key.Type))
|
||||
recordName = inst.Key.NameFQDN
|
||||
} else { // Change
|
||||
rrset = inst.Old[0].Original.(*adns.RecordSet)
|
||||
recordType, _ = nativeToRecordType(to.StringPtr(*rrset.Type))
|
||||
recordName = *rrset.Name
|
||||
}
|
||||
// ^^^ this is broken and can probably be cleaned up significantly by
|
||||
// someone that understands Azure's API.
|
||||
|
||||
corrections = append(corrections,
|
||||
&models.Correction{
|
||||
Msg: strings.Join(inst.Msgs, "\n"),
|
||||
F: func() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 6000*time.Second)
|
||||
defer cancel()
|
||||
_, err := a.recordsClient.CreateOrUpdate(ctx, *a.resourceGroup, zoneName, recordName, recordType, *rrset, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
case diff2.DELETE:
|
||||
fmt.Printf("DEBUG: azure inst=%s\n", inst)
|
||||
rrset := inst.Old[0].Original.(*adns.RecordSet)
|
||||
corrections = append(corrections,
|
||||
&models.Correction{
|
||||
Msg: strings.Join(inst.Msgs, "\n"),
|
||||
F: func() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 6000*time.Second)
|
||||
defer cancel()
|
||||
rt, err := nativeToRecordType(rrset.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = a.recordsClient.Delete(ctx, *a.resourceGroup, zoneName, *rrset.Name, rt, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return corrections, nil
|
||||
}
|
||||
|
@ -370,7 +432,7 @@ func nativeToRecords(set *adns.RecordSet, origin string) []*models.RecordConfig
|
|||
case "Microsoft.Network/dnszones/A":
|
||||
if set.Properties.ARecords != nil {
|
||||
for _, rec := range set.Properties.ARecords {
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL)}
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set}
|
||||
rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
|
||||
rc.Type = "A"
|
||||
_ = rc.SetTarget(*rec.IPv4Address)
|
||||
|
@ -383,6 +445,7 @@ func nativeToRecords(set *adns.RecordSet, origin string) []*models.RecordConfig
|
|||
AzureAlias: map[string]string{
|
||||
"type": "A",
|
||||
},
|
||||
Original: set,
|
||||
}
|
||||
rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
|
||||
_ = rc.SetTarget(*set.Properties.TargetResource.ID)
|
||||
|
@ -391,7 +454,7 @@ func nativeToRecords(set *adns.RecordSet, origin string) []*models.RecordConfig
|
|||
case "Microsoft.Network/dnszones/AAAA":
|
||||
if set.Properties.AaaaRecords != nil {
|
||||
for _, rec := range set.Properties.AaaaRecords {
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL)}
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set}
|
||||
rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
|
||||
rc.Type = "AAAA"
|
||||
_ = rc.SetTarget(*rec.IPv6Address)
|
||||
|
@ -404,6 +467,7 @@ func nativeToRecords(set *adns.RecordSet, origin string) []*models.RecordConfig
|
|||
AzureAlias: map[string]string{
|
||||
"type": "AAAA",
|
||||
},
|
||||
Original: set,
|
||||
}
|
||||
rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
|
||||
_ = rc.SetTarget(*set.Properties.TargetResource.ID)
|
||||
|
@ -411,7 +475,7 @@ func nativeToRecords(set *adns.RecordSet, origin string) []*models.RecordConfig
|
|||
}
|
||||
case "Microsoft.Network/dnszones/CNAME":
|
||||
if set.Properties.CnameRecord != nil {
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL)}
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set}
|
||||
rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
|
||||
rc.Type = "CNAME"
|
||||
_ = rc.SetTarget(*set.Properties.CnameRecord.Cname)
|
||||
|
@ -423,6 +487,7 @@ func nativeToRecords(set *adns.RecordSet, origin string) []*models.RecordConfig
|
|||
AzureAlias: map[string]string{
|
||||
"type": "CNAME",
|
||||
},
|
||||
Original: set,
|
||||
}
|
||||
rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
|
||||
_ = rc.SetTarget(*set.Properties.TargetResource.ID)
|
||||
|
@ -430,7 +495,7 @@ func nativeToRecords(set *adns.RecordSet, origin string) []*models.RecordConfig
|
|||
}
|
||||
case "Microsoft.Network/dnszones/NS":
|
||||
for _, rec := range set.Properties.NsRecords {
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL)}
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set}
|
||||
rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
|
||||
rc.Type = "NS"
|
||||
_ = rc.SetTarget(*rec.Nsdname)
|
||||
|
@ -438,7 +503,7 @@ func nativeToRecords(set *adns.RecordSet, origin string) []*models.RecordConfig
|
|||
}
|
||||
case "Microsoft.Network/dnszones/PTR":
|
||||
for _, rec := range set.Properties.PtrRecords {
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL)}
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set}
|
||||
rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
|
||||
rc.Type = "PTR"
|
||||
_ = rc.SetTarget(*rec.Ptrdname)
|
||||
|
@ -446,14 +511,14 @@ func nativeToRecords(set *adns.RecordSet, origin string) []*models.RecordConfig
|
|||
}
|
||||
case "Microsoft.Network/dnszones/TXT":
|
||||
if len(set.Properties.TxtRecords) == 0 { // Empty String Record Parsing
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL)}
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set}
|
||||
rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
|
||||
rc.Type = "TXT"
|
||||
_ = rc.SetTargetTXT("")
|
||||
results = append(results, rc)
|
||||
} else {
|
||||
for _, rec := range set.Properties.TxtRecords {
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL)}
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set}
|
||||
rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
|
||||
rc.Type = "TXT"
|
||||
var txts []string
|
||||
|
@ -466,7 +531,7 @@ func nativeToRecords(set *adns.RecordSet, origin string) []*models.RecordConfig
|
|||
}
|
||||
case "Microsoft.Network/dnszones/MX":
|
||||
for _, rec := range set.Properties.MxRecords {
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL)}
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set}
|
||||
rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
|
||||
rc.Type = "MX"
|
||||
_ = rc.SetTargetMX(uint16(*rec.Preference), *rec.Exchange)
|
||||
|
@ -474,7 +539,7 @@ func nativeToRecords(set *adns.RecordSet, origin string) []*models.RecordConfig
|
|||
}
|
||||
case "Microsoft.Network/dnszones/SRV":
|
||||
for _, rec := range set.Properties.SrvRecords {
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL)}
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set}
|
||||
rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
|
||||
rc.Type = "SRV"
|
||||
_ = rc.SetTargetSRV(uint16(*rec.Priority), uint16(*rec.Weight), uint16(*rec.Port), *rec.Target)
|
||||
|
@ -482,7 +547,7 @@ func nativeToRecords(set *adns.RecordSet, origin string) []*models.RecordConfig
|
|||
}
|
||||
case "Microsoft.Network/dnszones/CAA":
|
||||
for _, rec := range set.Properties.CaaRecords {
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL)}
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set}
|
||||
rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
|
||||
rc.Type = "CAA"
|
||||
_ = rc.SetTargetCAA(uint8(*rec.Flags), *rec.Tag, *rec.Value)
|
||||
|
|
|
@ -169,7 +169,7 @@ func (c *bindProvider) GetZoneRecords(domain string) (models.Records, error) {
|
|||
if os.IsNotExist(err) {
|
||||
// If the file doesn't exist, that's not an error. Just informational.
|
||||
c.zoneFileFound = false
|
||||
fmt.Fprintf(os.Stderr, "File does not yet exist: %q\n", c.zonefile)
|
||||
fmt.Fprintf(os.Stderr, "File does not yet exist: %q (will create)\n", c.zonefile)
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -244,8 +244,10 @@ func (c *bindProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.
|
|||
models.PostProcessRecords(foundRecords)
|
||||
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
|
||||
|
||||
var corrections []*models.Correction
|
||||
if !diff2.EnableDiff2 || true { // Remove "|| true" when diff2 version arrives
|
||||
changes := false
|
||||
var msg string
|
||||
|
||||
if !diff2.EnableDiff2 {
|
||||
|
||||
differ := diff.New(dc)
|
||||
_, create, del, mod, err := differ.IncrementalDiff(foundRecords)
|
||||
|
@ -255,7 +257,7 @@ func (c *bindProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.
|
|||
|
||||
buf := &bytes.Buffer{}
|
||||
// Print a list of changes. Generate an actual change that is the zone
|
||||
changes := false
|
||||
|
||||
for _, i := range create {
|
||||
changes = true
|
||||
if c.zoneFileFound {
|
||||
|
@ -275,48 +277,56 @@ func (c *bindProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.
|
|||
}
|
||||
}
|
||||
|
||||
var msg string
|
||||
if c.zoneFileFound {
|
||||
msg = fmt.Sprintf("GENERATE_ZONEFILE: '%s'. Changes:\n%s", dc.Name, buf)
|
||||
} else {
|
||||
msg = fmt.Sprintf("GENERATE_ZONEFILE: '%s' (new file with %d records)\n", dc.Name, len(create))
|
||||
}
|
||||
|
||||
if changes {
|
||||
} else {
|
||||
|
||||
// We only change the serial number if there is a change.
|
||||
desiredSoa.SoaSerial = nextSerial
|
||||
|
||||
corrections = append(corrections,
|
||||
&models.Correction{
|
||||
Msg: msg,
|
||||
F: func() error {
|
||||
printer.Printf("WRITING ZONEFILE: %v\n", c.zonefile)
|
||||
zf, err := os.Create(c.zonefile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create zonefile: %w", err)
|
||||
}
|
||||
// Beware that if there are any fake types, then they will
|
||||
// be commented out on write, but we don't reverse that when
|
||||
// reading, so there will be a diff on every invocation.
|
||||
err = prettyzone.WriteZoneFileRC(zf, dc.Records, dc.Name, 0, comments)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed WriteZoneFile: %w", err)
|
||||
}
|
||||
err = zf.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
var msgs []string
|
||||
msgs, changes, err = diff2.ByZone(foundRecords, dc, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//fmt.Printf("DEBUG: BIND changes=%v\n", changes)
|
||||
msg = strings.Join(msgs, "\n")
|
||||
|
||||
return corrections, nil
|
||||
}
|
||||
|
||||
// Insert Future diff2 version here.
|
||||
var corrections []*models.Correction
|
||||
//fmt.Printf("DEBUG: BIND changes=%v\n", changes)
|
||||
if changes {
|
||||
|
||||
// We only change the serial number if there is a change.
|
||||
desiredSoa.SoaSerial = nextSerial
|
||||
|
||||
corrections = append(corrections,
|
||||
&models.Correction{
|
||||
Msg: msg,
|
||||
F: func() error {
|
||||
printer.Printf("WRITING ZONEFILE: %v\n", c.zonefile)
|
||||
zf, err := os.Create(c.zonefile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create zonefile: %w", err)
|
||||
}
|
||||
// Beware that if there are any fake types, then they will
|
||||
// be commented out on write, but we don't reverse that when
|
||||
// reading, so there will be a diff on every invocation.
|
||||
err = prettyzone.WriteZoneFileRC(zf, dc.Records, dc.Name, 0, comments)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed WriteZoneFile: %w", err)
|
||||
}
|
||||
err = zf.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return corrections, nil
|
||||
}
|
||||
|
|
|
@ -326,6 +326,7 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
|
|||
// Insert Future diff2 version here.
|
||||
|
||||
return corrections, nil
|
||||
|
||||
}
|
||||
|
||||
func checkNSModifications(dc *models.DomainConfig) {
|
||||
|
|
|
@ -190,6 +190,7 @@ func (c *cloudnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
|
|||
// Insert Future diff2 version here.
|
||||
|
||||
return corrections, nil
|
||||
|
||||
}
|
||||
|
||||
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
||||
|
|
|
@ -194,6 +194,7 @@ func (client *providerClient) GenerateDomainCorrections(dc *models.DomainConfig,
|
|||
// Insert Future diff2 version here.
|
||||
|
||||
return corrections, nil
|
||||
|
||||
}
|
||||
|
||||
func makePurge(domainname string, cor diff.Correlation) zoneResourceRecordEdit {
|
||||
|
|
|
@ -248,5 +248,6 @@ func (c *desecProvider) GenerateDomainCorrections(dc *models.DomainConfig, exist
|
|||
|
||||
// Insert Future diff2 version here.
|
||||
|
||||
// Insert Future diff2 version here.
|
||||
return corrections, nil
|
||||
}
|
||||
|
|
|
@ -230,7 +230,7 @@ func (client *gandiv5Provider) GenerateDomainCorrections(dc *models.DomainConfig
|
|||
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
|
||||
|
||||
var corrections []*models.Correction
|
||||
if !diff2.EnableDiff2 || true { // Remove "|| true" when diff2 version arrives
|
||||
if !diff2.EnableDiff2 {
|
||||
|
||||
// diff existing vs. current.
|
||||
differ := diff.New(dc)
|
||||
|
@ -305,6 +305,7 @@ func (client *gandiv5Provider) GenerateDomainCorrections(dc *models.DomainConfig
|
|||
// First time putting data on this label. Create it.
|
||||
|
||||
// We have to create the label one rtype at a time.
|
||||
ns := recordsToNative(desiredRecords[label], dc.Name)
|
||||
for _, n := range ns {
|
||||
msg := strings.Join(msgsForLabel[label], "\n")
|
||||
domain := dc.Name
|
||||
|
@ -339,7 +340,83 @@ func (client *gandiv5Provider) GenerateDomainCorrections(dc *models.DomainConfig
|
|||
return corrections, nil
|
||||
}
|
||||
|
||||
// Insert Future diff2 version here.
|
||||
g := gandi.NewLiveDNSClient(config.Config{
|
||||
APIKey: client.apikey,
|
||||
SharingID: client.sharingid,
|
||||
Debug: client.debug,
|
||||
})
|
||||
|
||||
// Gandi is a "ByLabel" API with the odd exception that changes must be
|
||||
// done one label:rtype at a time.
|
||||
instructions, err := diff2.ByLabel(existing, dc, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, inst := range instructions {
|
||||
switch inst.Type {
|
||||
|
||||
case diff2.CREATE:
|
||||
// We have to create the label one rtype at a time.
|
||||
natives := recordsToNative(inst.New, dc.Name)
|
||||
for _, n := range natives {
|
||||
label := inst.Key.NameFQDN
|
||||
rtype := n.RrsetType
|
||||
domain := dc.Name
|
||||
shortname := dnsutil.TrimDomainName(label, dc.Name)
|
||||
ttl := n.RrsetTTL
|
||||
values := n.RrsetValues
|
||||
key := models.RecordKey{NameFQDN: label, Type: rtype}
|
||||
msg := strings.Join(inst.MsgsByKey[key], "\n")
|
||||
corrections = append(corrections,
|
||||
&models.Correction{
|
||||
Msg: msg,
|
||||
F: func() error {
|
||||
res, err := g.CreateDomainRecord(domain, shortname, rtype, ttl, values)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%+v: %w", res, err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case diff2.CHANGE:
|
||||
msgs := strings.Join(inst.Msgs, "\n")
|
||||
domain := dc.Name
|
||||
label := inst.Key.NameFQDN
|
||||
shortname := dnsutil.TrimDomainName(label, dc.Name)
|
||||
ns := recordsToNative(inst.New, dc.Name)
|
||||
corrections = append(corrections,
|
||||
&models.Correction{
|
||||
Msg: msgs,
|
||||
F: func() error {
|
||||
res, err := g.UpdateDomainRecordsByName(domain, shortname, ns)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%+v: %w", res, err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
case diff2.DELETE:
|
||||
msgs := strings.Join(inst.Msgs, "\n")
|
||||
domain := dc.Name
|
||||
label := inst.Key.NameFQDN
|
||||
shortname := dnsutil.TrimDomainName(label, dc.Name)
|
||||
corrections = append(corrections,
|
||||
&models.Correction{
|
||||
Msg: msgs,
|
||||
F: func() error {
|
||||
err := g.DeleteDomainRecordsByName(domain, shortname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return corrections, nil
|
||||
}
|
||||
|
|
|
@ -257,8 +257,6 @@ func (o *oracleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*model
|
|||
for any size zone.
|
||||
*/
|
||||
|
||||
corrections := []*models.Correction{}
|
||||
|
||||
if len(create) > 0 {
|
||||
createRecords := models.Records{}
|
||||
desc := ""
|
||||
|
|
|
@ -138,8 +138,6 @@ func (c *ovhProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.C
|
|||
return nil, err
|
||||
}
|
||||
|
||||
corrections := []*models.Correction{}
|
||||
|
||||
for _, del := range delete {
|
||||
rec := del.Existing.Original.(*Record)
|
||||
corrections = append(corrections, &models.Correction{
|
||||
|
|
Loading…
Reference in a new issue