mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-09-20 06:46:19 +08:00
NEW FEATURE: Order changes based on the record dependencies (#2419)
Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com> Co-authored-by: Tom Limoncelli <tal@whatexit.org>
This commit is contained in:
parent
e26afaf7e0
commit
e32bdc053f
|
@ -70,6 +70,11 @@ func Run(v string) int {
|
|||
Destination: &diff2.EnableDiff2,
|
||||
Value: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "disableordering",
|
||||
Usage: "Disables the dns ordering part of the diff2 package",
|
||||
Destination: &diff2.DisableOrdering,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-colors",
|
||||
Usage: "Disable colors",
|
||||
|
|
|
@ -206,6 +206,9 @@ title: DNSControl
|
|||
<li>
|
||||
<a href="https://docs.dnscontrol.org/developer-info/adding-new-rtypes">Step-by-Step Guide: Adding new DNS rtypes</a>: How to add a new DNS record type
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://docs.dnscontrol.org/developer-info/ordering">DNS reordering</a>: How DNSControl determines the order of the changes
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -169,6 +169,7 @@
|
|||
* [Debugging with dlv](debugging-with-dlv.md)
|
||||
* [ALIAS Records](alias.md)
|
||||
* [TXT record testing](testing-txt-records.md)
|
||||
* [DNS records ordering](ordering.md)
|
||||
|
||||
## Release
|
||||
|
||||
|
|
39
documentation/ordering.md
Normal file
39
documentation/ordering.md
Normal file
|
@ -0,0 +1,39 @@
|
|||
# Ordering of DNS records
|
||||
|
||||
DNSControl tries to automatically reorder the pending changes based on the dependencies of the records.
|
||||
For example, if an A record and a CNAME that points to the A record are created at the same time, some providers require the A record to be created before the CNAME.
|
||||
|
||||
Some providers explicitly require the targets of certain records like CNAMEs to exist, and source records to be valid. This makes it not always possible to "just" apply the pending changes in any order. This is why reordering the records based on the dependencies and the type of operation is required.
|
||||
|
||||
## Practical example
|
||||
|
||||
```js
|
||||
D('example.com', REG_NONE, DnsProvider(DNS_BIND),
|
||||
CNAME('foo', 'bar')
|
||||
A('bar', '1.2.3.4'),
|
||||
);
|
||||
```
|
||||
|
||||
`foo` requires `bar` to exist. Thus `bar` needs to exist before `foo`. But when deleting these records, `foo` needs to be deleted before `bar`.
|
||||
|
||||
## Unresolved records
|
||||
|
||||
DNSControl can produce a warning stating it found `unresolved records` this is most likely because of a cycle in the targets of your records. For instance in the code sample below both `foo` and `bar` depend on each other and thus will produce the warning.
|
||||
|
||||
Such updates will be done after all other updates to that domain.
|
||||
|
||||
In this (contrived) example, it is impossible to know which CNAME should be created first. Therefore they will be done in a non-deterministic order after all other updates to that domain:
|
||||
|
||||
```js
|
||||
D('example.com', REG_NONE, DnsProvider(DNS_BIND),
|
||||
CNAME('foo', 'bar')
|
||||
CNAME('bar', 'foo'),
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
## Disabling ordering
|
||||
|
||||
The re-ordering feature can be disabled using the `--disableordering` global flag (it goes before `preview` or `push`). While the code has been extensively tested, it is new and you may still find a bug. This flag leaves the updates unordered and may require multiple `push` runs to complete the update.
|
||||
|
||||
If you encounter any issues with the reordering please [open an issue](https://github.com/StackExchange/dnscontrol/issues).
|
|
@ -1986,15 +1986,15 @@ func makeTests(t *testing.T) []*TestGroup {
|
|||
testgroup("IGNORE_TARGET function CNAME",
|
||||
tc("Create some records",
|
||||
cname("foo", "test.foo.com."),
|
||||
cname("keep", "keep.example.com."),
|
||||
cname("keep", "keeper.example.com."),
|
||||
),
|
||||
tc("ignoring CNAME=test.foo.com.",
|
||||
ignoreTarget("test.foo.com.", "CNAME"),
|
||||
cname("keep", "keep.example.com."),
|
||||
cname("keep", "keeper.example.com."),
|
||||
).ExpectNoChanges(),
|
||||
tc("ignoring CNAME=test.foo.com. and add",
|
||||
ignoreTarget("test.foo.com.", "CNAME"),
|
||||
cname("keep", "keep.example.com."),
|
||||
cname("keep", "keeper.example.com."),
|
||||
a("adding", "1.2.3.4"),
|
||||
cname("another", "www.example.com."),
|
||||
),
|
||||
|
|
|
@ -455,6 +455,19 @@ func (rc *RecordConfig) ToRR() dns.RR {
|
|||
return rr
|
||||
}
|
||||
|
||||
// GetDependencies returns the FQDNs on which this record dependents
|
||||
func (rc *RecordConfig) GetDependencies() []string {
|
||||
|
||||
switch rc.Type {
|
||||
case "NS", "SRV", "CNAME", "MX", "ALIAS", "AZURE_ALIAS", "R53_ALIAS":
|
||||
return []string{
|
||||
rc.target,
|
||||
}
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// RecordKey represents a resource record in a format used by some systems.
|
||||
type RecordKey struct {
|
||||
NameFQDN string
|
||||
|
@ -554,6 +567,16 @@ func (recs Records) GroupedByFQDN() ([]string, map[string]Records) {
|
|||
return order, groups
|
||||
}
|
||||
|
||||
// GetAllDependencies concatinates all dependencies of all records
|
||||
func (recs Records) GetAllDependencies() []string {
|
||||
var dependencies []string
|
||||
for _, rec := range recs {
|
||||
dependencies = append(dependencies, rec.GetDependencies()...)
|
||||
}
|
||||
|
||||
return dependencies
|
||||
}
|
||||
|
||||
// PostProcessRecords does any post-processing of the downloaded DNS records.
|
||||
// Deprecated. zonerecords.CorrectZoneRecords() calls Downcase directly.
|
||||
func PostProcessRecords(recs []*RecordConfig) {
|
||||
|
@ -587,19 +610,21 @@ func Downcase(recs []*RecordConfig) {
|
|||
|
||||
// CanonicalizeTargets turns Targets into FQDNs
|
||||
func CanonicalizeTargets(recs []*RecordConfig, origin string) {
|
||||
originFQDN := origin + "."
|
||||
|
||||
for _, r := range recs {
|
||||
switch r.Type { // #rtype_variations
|
||||
case "AKAMAICDN", "ANAME", "CNAME", "DS", "MX", "NS", "NAPTR", "PTR", "SRV":
|
||||
// Target is a hostname that might be a shortname. Turn it into a FQDN.
|
||||
r.target = dnsutil.AddOrigin(r.target, origin)
|
||||
r.target = dnsutil.AddOrigin(r.target, originFQDN)
|
||||
case "A", "ALIAS", "CAA", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE", "IMPORT_TRANSFORM", "LOC", "SSHFP", "TLSA", "TXT":
|
||||
// Do nothing.
|
||||
case "SOA":
|
||||
if r.target != "DEFAULT_NOT_SET." {
|
||||
r.target = dnsutil.AddOrigin(r.target, origin) // .target stores the Ns
|
||||
r.target = dnsutil.AddOrigin(r.target, originFQDN) // .target stores the Ns
|
||||
}
|
||||
if r.SoaMbox != "DEFAULT_NOT_SET." {
|
||||
r.SoaMbox = dnsutil.AddOrigin(r.SoaMbox, origin)
|
||||
r.SoaMbox = dnsutil.AddOrigin(r.SoaMbox, originFQDN)
|
||||
}
|
||||
default:
|
||||
// TODO: we'd like to panic here, but custom record types complicate things.
|
||||
|
|
|
@ -32,6 +32,9 @@ func analyzeByRecordSet(cc *CompareConfig) ChangeList {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
instructions = orderByDependencies(instructions)
|
||||
|
||||
return instructions
|
||||
}
|
||||
|
||||
|
@ -74,6 +77,8 @@ func analyzeByLabel(cc *CompareConfig) ChangeList {
|
|||
}
|
||||
}
|
||||
|
||||
instructions = orderByDependencies(instructions)
|
||||
|
||||
return instructions
|
||||
}
|
||||
|
||||
|
@ -89,6 +94,9 @@ func analyzeByRecord(cc *CompareConfig) ChangeList {
|
|||
instructions = append(instructions, cs...)
|
||||
}
|
||||
}
|
||||
|
||||
instructions = orderByDependencies(instructions)
|
||||
|
||||
return instructions
|
||||
}
|
||||
|
||||
|
|
|
@ -18,40 +18,42 @@ func init() {
|
|||
color.NoColor = true
|
||||
}
|
||||
|
||||
var testDataAA1234 = makeRec("laba", "A", "1.2.3.4") // [0]
|
||||
var testDataAA1234 = makeRec("laba", "A", "1.2.3.4") // [ 0]
|
||||
var testDataAA5678 = makeRec("laba", "A", "5.6.7.8") //
|
||||
var testDataAA1234ttl700 = makeRecTTL("laba", "A", "1.2.3.4", 700) //
|
||||
var testDataAA5678ttl700 = makeRecTTL("laba", "A", "5.6.7.8", 700) //
|
||||
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 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", "laba") // [ 7]
|
||||
var e8 = makeRec("labg", "NS", "labb") // [ 8]
|
||||
var e9 = makeRec("labg", "NS", "labc") // [ 9]
|
||||
var e10 = makeRec("labg", "NS", "labe") // [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 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", "labf") // [ 8']
|
||||
var d9 = makeRec("labg", "NS", "laba") // [ 9']
|
||||
var d10 = makeRec("labg", "NS", "labe") // [10']
|
||||
var d11 = makeRec("labg", "NS", "labb") // [11']
|
||||
var d12 = makeRec("labh", "A", "1.2.3.4") // [12']
|
||||
var d13 = makeRec("labc", "CNAME", "labe") // [13']
|
||||
var testDataApexMX22bbb = makeRec("", "MX", "22 bbb")
|
||||
|
||||
func compareMsgs(t *testing.T, fnname, testname, testpart string, gotcc ChangeList, wantstring string) {
|
||||
func compareMsgs(t *testing.T, fnname, testname, testpart string, gotcc ChangeList, wantstring string, wantstringdefault string) {
|
||||
wantstring = coalesce(wantstring, wantstringdefault)
|
||||
t.Helper()
|
||||
gs := strings.TrimSpace(justMsgString(gotcc))
|
||||
ws := strings.TrimSpace(wantstring)
|
||||
|
@ -80,16 +82,17 @@ func Test_analyzeByRecordSet(t *testing.T) {
|
|||
}
|
||||
|
||||
origin := "f.com"
|
||||
existingSample := models.Records{testDataAA1234, testDataAMX10a, testDataCCa, testDataEA15, e4, e5, e6, e7, e8, e9, e10, e11}
|
||||
desiredSample := 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
|
||||
wantMsgsRSet string
|
||||
wantChangeLabel string
|
||||
wantMsgsLabel string
|
||||
wantChangeRec string
|
||||
wantMsgsRec string
|
||||
wantChangeZone string
|
||||
}{
|
||||
|
||||
|
@ -113,30 +116,30 @@ func Test_analyzeByRecordSet(t *testing.T) {
|
|||
existing: models.Records{testDataAA1234, testDataAMX10a},
|
||||
desired: models.Records{testDataAA1234clone, testDataAMX20b},
|
||||
},
|
||||
wantMsgs: "± MODIFY laba.f.com MX (10 laba ttl=300) -> (20 labb ttl=300)",
|
||||
wantMsgs: "± MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)",
|
||||
wantChangeRSet: `
|
||||
ChangeList: len=1
|
||||
00: Change: verb=CHANGE
|
||||
key={laba.f.com MX}
|
||||
old=[10 laba]
|
||||
new=[20 labb]
|
||||
msg=["± MODIFY laba.f.com MX (10 laba ttl=300) -> (20 labb ttl=300)"]
|
||||
old=[10 laba.f.com.]
|
||||
new=[20 labb.f.com.]
|
||||
msg=["± MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)"]
|
||||
`,
|
||||
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=["± MODIFY laba.f.com MX (10 laba ttl=300) -> (20 labb ttl=300)"]
|
||||
old=[1.2.3.4 10 laba.f.com.]
|
||||
new=[1.2.3.4 20 labb.f.com.]
|
||||
msg=["± MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)"]
|
||||
`,
|
||||
wantChangeRec: `
|
||||
ChangeList: len=1
|
||||
00: Change: verb=CHANGE
|
||||
key={laba.f.com MX}
|
||||
old=[10 laba]
|
||||
new=[20 labb]
|
||||
msg=["± MODIFY laba.f.com MX (10 laba ttl=300) -> (20 labb ttl=300)"]
|
||||
old=[10 laba.f.com.]
|
||||
new=[20 labb.f.com.]
|
||||
msg=["± MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)"]
|
||||
`,
|
||||
},
|
||||
|
||||
|
@ -147,30 +150,30 @@ ChangeList: len=1
|
|||
existing: models.Records{testDataAA1234, testDataApexMX1aaa},
|
||||
desired: models.Records{testDataAA1234clone, testDataApexMX22bbb},
|
||||
},
|
||||
wantMsgs: "± MODIFY f.com MX (1 aaa ttl=300) -> (22 bbb ttl=300)",
|
||||
wantMsgs: "± MODIFY f.com MX (1 aaa.f.com. ttl=300) -> (22 bbb.f.com. ttl=300)",
|
||||
wantChangeRSet: `
|
||||
ChangeList: len=1
|
||||
00: Change: verb=CHANGE
|
||||
key={f.com MX}
|
||||
old=[1 aaa]
|
||||
new=[22 bbb]
|
||||
msg=["± MODIFY f.com MX (1 aaa ttl=300) -> (22 bbb ttl=300)"]
|
||||
old=[1 aaa.f.com.]
|
||||
new=[22 bbb.f.com.]
|
||||
msg=["± MODIFY f.com MX (1 aaa.f.com. ttl=300) -> (22 bbb.f.com. ttl=300)"]
|
||||
`,
|
||||
wantChangeLabel: `
|
||||
ChangeList: len=1
|
||||
00: Change: verb=CHANGE
|
||||
key={f.com }
|
||||
old=[1 aaa]
|
||||
new=[22 bbb]
|
||||
msg=["± MODIFY f.com MX (1 aaa ttl=300) -> (22 bbb ttl=300)"]
|
||||
old=[1 aaa.f.com.]
|
||||
new=[22 bbb.f.com.]
|
||||
msg=["± MODIFY f.com MX (1 aaa.f.com. ttl=300) -> (22 bbb.f.com. ttl=300)"]
|
||||
`,
|
||||
wantChangeRec: `
|
||||
ChangeList: len=1
|
||||
00: Change: verb=CHANGE
|
||||
key={f.com MX}
|
||||
old=[1 aaa]
|
||||
new=[22 bbb]
|
||||
msg=["± MODIFY f.com MX (1 aaa ttl=300) -> (22 bbb ttl=300)"]
|
||||
old=[1 aaa.f.com.]
|
||||
new=[22 bbb.f.com.]
|
||||
msg=["± MODIFY f.com MX (1 aaa.f.com. ttl=300) -> (22 bbb.f.com. ttl=300)"]
|
||||
`,
|
||||
},
|
||||
|
||||
|
@ -182,41 +185,107 @@ ChangeList: len=1
|
|||
desired: models.Records{testDataAA1234clone, testDataAA12345, testDataAMX20b},
|
||||
},
|
||||
wantMsgs: `
|
||||
± MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)
|
||||
+ CREATE laba.f.com A 1.2.3.5 ttl=300
|
||||
± MODIFY laba.f.com MX (10 laba ttl=300) -> (20 labb ttl=300)
|
||||
`,
|
||||
`,
|
||||
wantMsgsLabel: `
|
||||
+ CREATE laba.f.com A 1.2.3.5 ttl=300
|
||||
± MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)
|
||||
`,
|
||||
wantChangeRSet: `
|
||||
ChangeList: len=2
|
||||
00: Change: verb=CHANGE
|
||||
key={laba.f.com MX}
|
||||
old=[10 laba.f.com.]
|
||||
new=[20 labb.f.com.]
|
||||
msg=["± MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)"]
|
||||
01: 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 ttl=300"]
|
||||
01: Change: verb=CHANGE
|
||||
key={laba.f.com MX}
|
||||
old=[10 laba]
|
||||
new=[20 labb]
|
||||
msg=["± MODIFY laba.f.com MX (10 laba ttl=300) -> (20 labb ttl=300)"]
|
||||
`,
|
||||
`,
|
||||
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 ttl=300" "± MODIFY laba.f.com MX (10 laba ttl=300) -> (20 labb ttl=300)"]
|
||||
old=[1.2.3.4 10 laba.f.com.]
|
||||
new=[1.2.3.4 1.2.3.5 20 labb.f.com.]
|
||||
msg=["+ CREATE laba.f.com A 1.2.3.5 ttl=300" "± MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)"]
|
||||
`,
|
||||
wantChangeRec: `
|
||||
ChangeList: len=2
|
||||
00: Change: verb=CREATE
|
||||
00: Change: verb=CHANGE
|
||||
key={laba.f.com MX}
|
||||
old=[10 laba.f.com.]
|
||||
new=[20 labb.f.com.]
|
||||
msg=["± MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)"]
|
||||
01: Change: verb=CREATE
|
||||
key={laba.f.com A}
|
||||
new=[1.2.3.5]
|
||||
msg=["+ CREATE laba.f.com A 1.2.3.5 ttl=300"]
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
name: "order forward and backward dependent records",
|
||||
args: args{
|
||||
origin: origin,
|
||||
existing: models.Records{testDataAA1234, testDataCCa},
|
||||
desired: models.Records{d13, d3},
|
||||
},
|
||||
wantMsgs: `
|
||||
+ CREATE labe.f.com A 10.10.10.95 ttl=300
|
||||
± MODIFY labc.f.com CNAME (laba.f.com. ttl=300) -> (labe.f.com. ttl=300)
|
||||
- DELETE laba.f.com A 1.2.3.4 ttl=300
|
||||
`,
|
||||
wantChangeRSet: `
|
||||
ChangeList: len=3
|
||||
00: Change: verb=CREATE
|
||||
key={labe.f.com A}
|
||||
new=[10.10.10.95]
|
||||
msg=["+ CREATE labe.f.com A 10.10.10.95 ttl=300"]
|
||||
01: Change: verb=CHANGE
|
||||
key={laba.f.com MX}
|
||||
old=[10 laba]
|
||||
new=[20 labb]
|
||||
msg=["± MODIFY laba.f.com MX (10 laba ttl=300) -> (20 labb ttl=300)"]
|
||||
key={labc.f.com CNAME}
|
||||
old=[laba.f.com.]
|
||||
new=[labe.f.com.]
|
||||
msg=["± MODIFY labc.f.com CNAME (laba.f.com. ttl=300) -> (labe.f.com. ttl=300)"]
|
||||
02: Change: verb=DELETE
|
||||
key={laba.f.com A}
|
||||
old=[1.2.3.4]
|
||||
msg=["- DELETE laba.f.com A 1.2.3.4 ttl=300"]
|
||||
`,
|
||||
wantChangeLabel: `
|
||||
ChangeList: len=3
|
||||
00: Change: verb=CREATE
|
||||
key={labe.f.com }
|
||||
new=[10.10.10.95]
|
||||
msg=["+ CREATE labe.f.com A 10.10.10.95 ttl=300"]
|
||||
01: Change: verb=CHANGE
|
||||
key={labc.f.com }
|
||||
old=[laba.f.com.]
|
||||
new=[labe.f.com.]
|
||||
msg=["± MODIFY labc.f.com CNAME (laba.f.com. ttl=300) -> (labe.f.com. ttl=300)"]
|
||||
02: Change: verb=DELETE
|
||||
key={laba.f.com }
|
||||
old=[1.2.3.4]
|
||||
msg=["- DELETE laba.f.com A 1.2.3.4 ttl=300"]
|
||||
`,
|
||||
wantChangeRec: `
|
||||
ChangeList: len=3
|
||||
00: Change: verb=CREATE
|
||||
key={labe.f.com A}
|
||||
new=[10.10.10.95]
|
||||
msg=["+ CREATE labe.f.com A 10.10.10.95 ttl=300"]
|
||||
01: Change: verb=CHANGE
|
||||
key={labc.f.com CNAME}
|
||||
old=[laba.f.com.]
|
||||
new=[labe.f.com.]
|
||||
msg=["± MODIFY labc.f.com CNAME (laba.f.com. ttl=300) -> (labe.f.com. ttl=300)"]
|
||||
02: Change: verb=DELETE
|
||||
key={laba.f.com A}
|
||||
old=[1.2.3.4]
|
||||
msg=["- DELETE laba.f.com A 1.2.3.4 ttl=300"]
|
||||
`,
|
||||
},
|
||||
|
||||
|
@ -224,154 +293,176 @@ ChangeList: len=2
|
|||
name: "big",
|
||||
args: args{
|
||||
origin: origin,
|
||||
existing: existingSample,
|
||||
desired: desiredSample,
|
||||
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},
|
||||
},
|
||||
wantMsgs: `
|
||||
+ CREATE labf.f.com TXT "foo" ttl=300
|
||||
± MODIFY labg.f.com NS (labc.f.com. ttl=300) -> (labf.f.com. ttl=300)
|
||||
- DELETE labh.f.com CNAME labd.f.com. ttl=300
|
||||
+ CREATE labh.f.com A 1.2.3.4 ttl=300
|
||||
- DELETE labc.f.com CNAME laba.f.com. ttl=300
|
||||
± MODIFY labe.f.com A (10.10.10.15 ttl=300) -> (10.10.10.95 ttl=300)
|
||||
± MODIFY labe.f.com A (10.10.10.16 ttl=300) -> (10.10.10.96 ttl=300)
|
||||
± MODIFY labe.f.com A (10.10.10.17 ttl=300) -> (10.10.10.97 ttl=300)
|
||||
± MODIFY labe.f.com A (10.10.10.18 ttl=300) -> (10.10.10.98 ttl=300)
|
||||
± MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)
|
||||
+ CREATE laba.f.com A 1.2.3.5 ttl=300
|
||||
± MODIFY laba.f.com MX (10 laba ttl=300) -> (20 labb ttl=300)
|
||||
- DELETE labc.f.com CNAME laba ttl=300
|
||||
`,
|
||||
wantChangeRSet: `
|
||||
ChangeList: len=8
|
||||
00: Change: verb=CREATE
|
||||
key={labf.f.com TXT}
|
||||
new=["foo"]
|
||||
msg=["+ CREATE labf.f.com TXT \"foo\" ttl=300"]
|
||||
01: Change: verb=CHANGE
|
||||
key={labg.f.com NS}
|
||||
old=[laba.f.com. labb.f.com. labc.f.com. labe.f.com.]
|
||||
new=[laba.f.com. labb.f.com. labe.f.com. labf.f.com.]
|
||||
msg=["± MODIFY labg.f.com NS (labc.f.com. ttl=300) -> (labf.f.com. ttl=300)"]
|
||||
02: Change: verb=DELETE
|
||||
key={labh.f.com CNAME}
|
||||
old=[labd.f.com.]
|
||||
msg=["- DELETE labh.f.com CNAME labd.f.com. ttl=300"]
|
||||
03: Change: verb=CREATE
|
||||
key={labh.f.com A}
|
||||
new=[1.2.3.4]
|
||||
msg=["+ CREATE labh.f.com A 1.2.3.4 ttl=300"]
|
||||
04: Change: verb=DELETE
|
||||
key={labc.f.com CNAME}
|
||||
old=[laba.f.com.]
|
||||
msg=["- DELETE labc.f.com CNAME laba.f.com. ttl=300"]
|
||||
05: 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=["± MODIFY labe.f.com A (10.10.10.15 ttl=300) -> (10.10.10.95 ttl=300)" "± MODIFY labe.f.com A (10.10.10.16 ttl=300) -> (10.10.10.96 ttl=300)" "± MODIFY labe.f.com A (10.10.10.17 ttl=300) -> (10.10.10.97 ttl=300)" "± MODIFY labe.f.com A (10.10.10.18 ttl=300) -> (10.10.10.98 ttl=300)"]
|
||||
06: Change: verb=CHANGE
|
||||
key={laba.f.com MX}
|
||||
old=[10 laba.f.com.]
|
||||
new=[20 labb.f.com.]
|
||||
msg=["± MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)"]
|
||||
07: 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 ttl=300"]
|
||||
`,
|
||||
wantMsgsLabel: `
|
||||
+ CREATE labf.f.com TXT "foo" ttl=300
|
||||
± MODIFY labg.f.com NS (labc.f.com. ttl=300) -> (labf.f.com. ttl=300)
|
||||
- DELETE labh.f.com CNAME labd.f.com. ttl=300
|
||||
+ CREATE labh.f.com A 1.2.3.4 ttl=300
|
||||
- DELETE labc.f.com CNAME laba.f.com. ttl=300
|
||||
± MODIFY labe.f.com A (10.10.10.15 ttl=300) -> (10.10.10.95 ttl=300)
|
||||
± MODIFY labe.f.com A (10.10.10.16 ttl=300) -> (10.10.10.96 ttl=300)
|
||||
± MODIFY labe.f.com A (10.10.10.17 ttl=300) -> (10.10.10.97 ttl=300)
|
||||
± MODIFY labe.f.com A (10.10.10.18 ttl=300) -> (10.10.10.98 ttl=300)
|
||||
+ CREATE laba.f.com A 1.2.3.5 ttl=300
|
||||
± MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)
|
||||
`,
|
||||
wantChangeLabel: `
|
||||
ChangeList: len=6
|
||||
00: Change: verb=CREATE
|
||||
key={labf.f.com }
|
||||
new=["foo"]
|
||||
msg=["+ CREATE labf.f.com TXT \"foo\" ttl=300"]
|
||||
01: Change: verb=CHANGE
|
||||
key={labg.f.com }
|
||||
old=[laba.f.com. labb.f.com. labc.f.com. labe.f.com.]
|
||||
new=[laba.f.com. labb.f.com. labe.f.com. labf.f.com.]
|
||||
msg=["± MODIFY labg.f.com NS (labc.f.com. ttl=300) -> (labf.f.com. ttl=300)"]
|
||||
02: Change: verb=CHANGE
|
||||
key={labh.f.com }
|
||||
old=[labd.f.com.]
|
||||
new=[1.2.3.4]
|
||||
msg=["- DELETE labh.f.com CNAME labd.f.com. ttl=300" "+ CREATE labh.f.com A 1.2.3.4 ttl=300"]
|
||||
03: Change: verb=DELETE
|
||||
key={labc.f.com }
|
||||
old=[laba.f.com.]
|
||||
msg=["- DELETE labc.f.com CNAME laba.f.com. ttl=300"]
|
||||
04: 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=["± MODIFY labe.f.com A (10.10.10.15 ttl=300) -> (10.10.10.95 ttl=300)" "± MODIFY labe.f.com A (10.10.10.16 ttl=300) -> (10.10.10.96 ttl=300)" "± MODIFY labe.f.com A (10.10.10.17 ttl=300) -> (10.10.10.97 ttl=300)" "± MODIFY labe.f.com A (10.10.10.18 ttl=300) -> (10.10.10.98 ttl=300)"]
|
||||
05: Change: verb=CHANGE
|
||||
key={laba.f.com }
|
||||
old=[1.2.3.4 10 laba.f.com.]
|
||||
new=[1.2.3.4 1.2.3.5 20 labb.f.com.]
|
||||
msg=["+ CREATE laba.f.com A 1.2.3.5 ttl=300" "± MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)"]
|
||||
`,
|
||||
wantMsgsRec: `
|
||||
± MODIFY labe.f.com A (10.10.10.15 ttl=300) -> (10.10.10.95 ttl=300)
|
||||
± MODIFY labe.f.com A (10.10.10.16 ttl=300) -> (10.10.10.96 ttl=300)
|
||||
± MODIFY labe.f.com A (10.10.10.17 ttl=300) -> (10.10.10.97 ttl=300)
|
||||
± MODIFY labe.f.com A (10.10.10.18 ttl=300) -> (10.10.10.98 ttl=300)
|
||||
+ CREATE labf.f.com TXT "foo" ttl=300
|
||||
± MODIFY labg.f.com NS (10.10.10.17 ttl=300) -> (10.10.10.10 ttl=300)
|
||||
± MODIFY labg.f.com NS (10.10.10.18 ttl=300) -> (10.10.10.97 ttl=300)
|
||||
- DELETE labh.f.com CNAME labd ttl=300
|
||||
± MODIFY labg.f.com NS (labc.f.com. ttl=300) -> (labf.f.com. ttl=300)
|
||||
- DELETE labh.f.com CNAME labd.f.com. ttl=300
|
||||
+ CREATE labh.f.com A 1.2.3.4 ttl=300
|
||||
- DELETE labc.f.com CNAME laba.f.com. ttl=300
|
||||
± MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)
|
||||
+ CREATE laba.f.com A 1.2.3.5 ttl=300
|
||||
`,
|
||||
wantChangeRSet: `
|
||||
ChangeList: len=8
|
||||
wantChangeRec: `
|
||||
ChangeList: len=11
|
||||
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 ttl=300"]
|
||||
key={labe.f.com A}
|
||||
old=[10.10.10.15]
|
||||
new=[10.10.10.95]
|
||||
msg=["± MODIFY labe.f.com A (10.10.10.15 ttl=300) -> (10.10.10.95 ttl=300)"]
|
||||
01: Change: verb=CHANGE
|
||||
key={laba.f.com MX}
|
||||
old=[10 laba]
|
||||
new=[20 labb]
|
||||
msg=["± MODIFY laba.f.com MX (10 laba ttl=300) -> (20 labb ttl=300)"]
|
||||
02: Change: verb=DELETE
|
||||
key={labc.f.com CNAME}
|
||||
old=[laba]
|
||||
msg=["- DELETE labc.f.com CNAME laba ttl=300"]
|
||||
key={labe.f.com A}
|
||||
old=[10.10.10.16]
|
||||
new=[10.10.10.96]
|
||||
msg=["± MODIFY labe.f.com A (10.10.10.16 ttl=300) -> (10.10.10.96 ttl=300)"]
|
||||
02: Change: verb=CHANGE
|
||||
key={labe.f.com A}
|
||||
old=[10.10.10.17]
|
||||
new=[10.10.10.97]
|
||||
msg=["± MODIFY labe.f.com A (10.10.10.17 ttl=300) -> (10.10.10.97 ttl=300)"]
|
||||
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=["± MODIFY labe.f.com A (10.10.10.15 ttl=300) -> (10.10.10.95 ttl=300)" "± MODIFY labe.f.com A (10.10.10.16 ttl=300) -> (10.10.10.96 ttl=300)" "± MODIFY labe.f.com A (10.10.10.17 ttl=300) -> (10.10.10.97 ttl=300)" "± MODIFY labe.f.com A (10.10.10.18 ttl=300) -> (10.10.10.98 ttl=300)"]
|
||||
old=[10.10.10.18]
|
||||
new=[10.10.10.98]
|
||||
msg=["± MODIFY labe.f.com A (10.10.10.18 ttl=300) -> (10.10.10.98 ttl=300)"]
|
||||
04: Change: verb=CREATE
|
||||
key={labf.f.com TXT}
|
||||
new=["foo"]
|
||||
msg=["+ CREATE labf.f.com TXT \"foo\" ttl=300"]
|
||||
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=["± MODIFY labg.f.com NS (10.10.10.17 ttl=300) -> (10.10.10.10 ttl=300)" "± MODIFY labg.f.com NS (10.10.10.18 ttl=300) -> (10.10.10.97 ttl=300)"]
|
||||
old=[labc.f.com.]
|
||||
new=[labf.f.com.]
|
||||
msg=["± MODIFY labg.f.com NS (labc.f.com. ttl=300) -> (labf.f.com. ttl=300)"]
|
||||
06: Change: verb=DELETE
|
||||
key={labh.f.com CNAME}
|
||||
old=[labd]
|
||||
msg=["- DELETE labh.f.com CNAME labd ttl=300"]
|
||||
old=[labd.f.com.]
|
||||
msg=["- DELETE labh.f.com CNAME labd.f.com. ttl=300"]
|
||||
07: Change: verb=CREATE
|
||||
key={labh.f.com A}
|
||||
new=[1.2.3.4]
|
||||
msg=["+ CREATE labh.f.com A 1.2.3.4 ttl=300"]
|
||||
`,
|
||||
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 ttl=300" "± MODIFY laba.f.com MX (10 laba ttl=300) -> (20 labb ttl=300)"]
|
||||
01: Change: verb=DELETE
|
||||
key={labc.f.com }
|
||||
old=[laba]
|
||||
msg=["- DELETE labc.f.com CNAME laba ttl=300"]
|
||||
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=["± MODIFY labe.f.com A (10.10.10.15 ttl=300) -> (10.10.10.95 ttl=300)" "± MODIFY labe.f.com A (10.10.10.16 ttl=300) -> (10.10.10.96 ttl=300)" "± MODIFY labe.f.com A (10.10.10.17 ttl=300) -> (10.10.10.97 ttl=300)" "± MODIFY labe.f.com A (10.10.10.18 ttl=300) -> (10.10.10.98 ttl=300)"]
|
||||
03: Change: verb=CREATE
|
||||
key={labf.f.com }
|
||||
new=["foo"]
|
||||
msg=["+ CREATE labf.f.com TXT \"foo\" ttl=300"]
|
||||
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=["± MODIFY labg.f.com NS (10.10.10.17 ttl=300) -> (10.10.10.10 ttl=300)" "± MODIFY labg.f.com NS (10.10.10.18 ttl=300) -> (10.10.10.97 ttl=300)"]
|
||||
05: Change: verb=CHANGE
|
||||
key={labh.f.com }
|
||||
old=[labd]
|
||||
new=[1.2.3.4]
|
||||
msg=["- DELETE labh.f.com CNAME labd ttl=300" "+ CREATE labh.f.com A 1.2.3.4 ttl=300"]
|
||||
`,
|
||||
wantChangeRec: `
|
||||
ChangeList: len=12
|
||||
00: Change: verb=CREATE
|
||||
08: Change: verb=DELETE
|
||||
key={labc.f.com CNAME}
|
||||
old=[laba.f.com.]
|
||||
msg=["- DELETE labc.f.com CNAME laba.f.com. ttl=300"]
|
||||
09: Change: verb=CHANGE
|
||||
key={laba.f.com MX}
|
||||
old=[10 laba.f.com.]
|
||||
new=[20 labb.f.com.]
|
||||
msg=["± MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)"]
|
||||
10: Change: verb=CREATE
|
||||
key={laba.f.com A}
|
||||
new=[1.2.3.5]
|
||||
msg=["+ CREATE laba.f.com A 1.2.3.5 ttl=300"]
|
||||
01: Change: verb=CHANGE
|
||||
key={laba.f.com MX}
|
||||
old=[10 laba]
|
||||
new=[20 labb]
|
||||
msg=["± MODIFY laba.f.com MX (10 laba ttl=300) -> (20 labb ttl=300)"]
|
||||
02: Change: verb=DELETE
|
||||
key={labc.f.com CNAME}
|
||||
old=[laba]
|
||||
msg=["- DELETE labc.f.com CNAME laba ttl=300"]
|
||||
03: Change: verb=CHANGE
|
||||
key={labe.f.com A}
|
||||
old=[10.10.10.15]
|
||||
new=[10.10.10.95]
|
||||
msg=["± MODIFY labe.f.com A (10.10.10.15 ttl=300) -> (10.10.10.95 ttl=300)"]
|
||||
04: Change: verb=CHANGE
|
||||
key={labe.f.com A}
|
||||
old=[10.10.10.16]
|
||||
new=[10.10.10.96]
|
||||
msg=["± MODIFY labe.f.com A (10.10.10.16 ttl=300) -> (10.10.10.96 ttl=300)"]
|
||||
05: Change: verb=CHANGE
|
||||
key={labe.f.com A}
|
||||
old=[10.10.10.17]
|
||||
new=[10.10.10.97]
|
||||
msg=["± MODIFY labe.f.com A (10.10.10.17 ttl=300) -> (10.10.10.97 ttl=300)"]
|
||||
06: Change: verb=CHANGE
|
||||
key={labe.f.com A}
|
||||
old=[10.10.10.18]
|
||||
new=[10.10.10.98]
|
||||
msg=["± MODIFY labe.f.com A (10.10.10.18 ttl=300) -> (10.10.10.98 ttl=300)"]
|
||||
07: Change: verb=CREATE
|
||||
key={labf.f.com TXT}
|
||||
new=["foo"]
|
||||
msg=["+ CREATE labf.f.com TXT \"foo\" ttl=300"]
|
||||
08: Change: verb=CHANGE
|
||||
key={labg.f.com NS}
|
||||
old=[10.10.10.17]
|
||||
new=[10.10.10.10]
|
||||
msg=["± MODIFY labg.f.com NS (10.10.10.17 ttl=300) -> (10.10.10.10 ttl=300)"]
|
||||
09: Change: verb=CHANGE
|
||||
key={labg.f.com NS}
|
||||
old=[10.10.10.18]
|
||||
new=[10.10.10.97]
|
||||
msg=["± MODIFY labg.f.com NS (10.10.10.18 ttl=300) -> (10.10.10.97 ttl=300)"]
|
||||
10: Change: verb=DELETE
|
||||
key={labh.f.com CNAME}
|
||||
old=[labd]
|
||||
msg=["- DELETE labh.f.com CNAME labd ttl=300"]
|
||||
11: Change: verb=CREATE
|
||||
key={labh.f.com A}
|
||||
new=[1.2.3.4]
|
||||
msg=["+ CREATE labh.f.com A 1.2.3.4 ttl=300"]
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
models.CanonicalizeTargets(tt.args.existing, tt.args.origin)
|
||||
models.CanonicalizeTargets(tt.args.desired, tt.args.origin)
|
||||
|
||||
// Each "analyze*()" should return the same msgs, but a different ChangeList.
|
||||
// Sadly the analyze*() functions are destructive to the CompareConfig struct.
|
||||
|
@ -379,19 +470,19 @@ ChangeList: len=12
|
|||
|
||||
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)
|
||||
compareMsgs(t, "analyzeByRecordSet", tt.name, "RSet", cl, tt.wantMsgsRSet, 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)
|
||||
compareMsgs(t, "analyzeByLabel", tt.name, "Label", cl, tt.wantMsgsLabel, 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)
|
||||
compareMsgs(t, "analyzeByRecord", tt.name, "Rec", cl, tt.wantMsgsRec, tt.wantMsgs)
|
||||
compareCL(t, "analyzeByRecord", tt.name, "Rec", cl, tt.wantChangeRec)
|
||||
})
|
||||
|
||||
|
@ -400,8 +491,17 @@ ChangeList: len=12
|
|||
}
|
||||
}
|
||||
|
||||
func coalesce(a string, b string) string {
|
||||
if a != "" {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func mkTargetConfig(x ...*models.RecordConfig) []targetConfig {
|
||||
var tc []targetConfig
|
||||
|
||||
models.CanonicalizeTargets(x, "f.com")
|
||||
for _, r := range x {
|
||||
ct, cf := mkCompareBlobs(r, nil)
|
||||
tc = append(tc, targetConfig{
|
||||
|
@ -469,8 +569,8 @@ func Test_diffTargets(t *testing.T) {
|
|||
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 ttl=300"},
|
||||
New: models.Records{makeRec("laba", "MX", "10 laba.f.com.")},
|
||||
Msgs: []string{"+ CREATE laba.f.com MX 10 laba.f.com. ttl=300"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -484,8 +584,8 @@ func Test_diffTargets(t *testing.T) {
|
|||
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 ttl=300"},
|
||||
Old: models.Records{makeRec("laba", "MX", "10 laba.f.com.")},
|
||||
Msgs: []string{"- DELETE laba.f.com MX 10 laba.f.com. ttl=300"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -501,7 +601,7 @@ func Test_diffTargets(t *testing.T) {
|
|||
Key: models.RecordKey{NameFQDN: "laba.f.com", Type: "MX"},
|
||||
Old: models.Records{testDataAMX10a},
|
||||
New: models.Records{testDataAMX20b},
|
||||
Msgs: []string{"± MODIFY laba.f.com MX (10 laba ttl=300) -> (20 labb ttl=300)"},
|
||||
Msgs: []string{"± MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -58,8 +58,8 @@ ldata:
|
|||
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"]
|
||||
existing: ["22 ttt.f.com." "1.2.3.4"]
|
||||
desired: ["labd.f.com."]
|
||||
origin: f.com
|
||||
compFn: <nil>
|
||||
`,
|
||||
|
@ -81,8 +81,8 @@ ldata:
|
|||
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"]
|
||||
existing: ["labd.f.com."]
|
||||
desired: ["1.2.3.4" "22 ttt.f.com."]
|
||||
origin: f.com
|
||||
compFn: <nil>
|
||||
`,
|
||||
|
@ -104,7 +104,7 @@ ldata:
|
|||
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"]
|
||||
existing: ["1.2.3.4" "laba.f.com."]
|
||||
desired: ["1.2.3.4"]
|
||||
origin: f.com
|
||||
compFn: <nil>
|
||||
|
@ -127,8 +127,8 @@ ldata:
|
|||
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"]
|
||||
existing: ["1.2.3.4" "1 aaa.f.com."]
|
||||
desired: ["1.2.3.4" "22 bbb.f.com."]
|
||||
origin: f.com
|
||||
compFn: <nil>
|
||||
`,
|
||||
|
@ -153,8 +153,8 @@ ldata:
|
|||
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"]
|
||||
existing: ["1.2.3.4" "10 laba.f.com." "laba.f.com." "10.10.10.15"]
|
||||
desired: ["1.2.3.4" "1.2.3.5" "20 labb.f.com." "10.10.10.95" "10.10.10.96"]
|
||||
origin: f.com
|
||||
compFn: <nil>
|
||||
`,
|
||||
|
@ -186,8 +186,8 @@ ldata:
|
|||
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"]
|
||||
existing: ["1.2.3.4" "10 laba.f.com." "laba.f.com." "10.10.10.15" "10.10.10.16" "10.10.10.17" "10.10.10.18" "laba.f.com." "labb.f.com." "labc.f.com." "labe.f.com." "labd.f.com."]
|
||||
desired: ["1.2.3.4" "1.2.3.5" "20 labb.f.com." "10.10.10.95" "10.10.10.96" "10.10.10.97" "10.10.10.98" "\"foo\"" "labf.f.com." "laba.f.com." "labe.f.com." "labb.f.com." "1.2.3.4"]
|
||||
origin: f.com
|
||||
compFn: <nil>
|
||||
`,
|
||||
|
@ -196,6 +196,10 @@ compFn: <nil>
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
models.CanonicalizeTargets(tt.args.desired, "f.com")
|
||||
models.CanonicalizeTargets(tt.args.existing, "f.com")
|
||||
|
||||
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)
|
||||
|
@ -229,6 +233,7 @@ func Test_mkCompareBlobs(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
got, got1 := mkCompareBlobs(tt.args.rc, tt.args.f)
|
||||
if got != tt.want {
|
||||
t.Errorf("mkCompareBlobs() got = %q, want %q", got, tt.want)
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/dnsgraph"
|
||||
)
|
||||
|
||||
// Verb indicates the Change's type (create, delete, etc.)
|
||||
|
@ -52,6 +53,31 @@ type Change struct {
|
|||
HintRecordSetLen1 bool
|
||||
}
|
||||
|
||||
func (c Change) GetType() dnsgraph.NodeType {
|
||||
if c.Type == REPORT {
|
||||
return dnsgraph.Report
|
||||
}
|
||||
|
||||
return dnsgraph.Change
|
||||
}
|
||||
|
||||
func (c Change) GetName() string {
|
||||
return c.Key.NameFQDN
|
||||
}
|
||||
|
||||
func (c Change) GetDependencies() []dnsgraph.Dependency {
|
||||
var dependencies []dnsgraph.Dependency
|
||||
|
||||
if c.Type == CHANGE || c.Type == DELETE {
|
||||
dependencies = append(dependencies, dnsgraph.CreateDependencies(c.Old.GetAllDependencies(), dnsgraph.BackwardDependency)...)
|
||||
}
|
||||
if c.Type == CHANGE || c.Type == CREATE {
|
||||
dependencies = append(dependencies, dnsgraph.CreateDependencies(c.New.GetAllDependencies(), dnsgraph.ForwardDependency)...)
|
||||
}
|
||||
|
||||
return dependencies
|
||||
}
|
||||
|
||||
/*
|
||||
General instructions:
|
||||
|
||||
|
|
|
@ -2,3 +2,6 @@ package diff2
|
|||
|
||||
// EnableDiff2 is true to activate the experimental diff2 algorithm.
|
||||
var EnableDiff2 bool
|
||||
|
||||
// DisableOrdering can be set to true to disable the reordering of the changes
|
||||
var DisableOrdering bool
|
||||
|
|
28
pkg/diff2/ordering.go
Normal file
28
pkg/diff2/ordering.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package diff2
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/dnsgraph"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/dnssort"
|
||||
)
|
||||
|
||||
func orderByDependencies(changes ChangeList) ChangeList {
|
||||
if DisableOrdering {
|
||||
log.Println("[Info: ordering of the changes has been disabled.]")
|
||||
return changes
|
||||
}
|
||||
|
||||
a := dnssort.SortUsingGraph(changes)
|
||||
|
||||
if len(a.UnresolvedRecords) > 0 {
|
||||
log.Printf("Warning: Found unresolved records %v.\n"+
|
||||
"This can indicate a circular dependency, please ensure all targets from given records exist and no circular dependencies exist in the changeset. "+
|
||||
"These unresolved records are still added as changes and pushed to the provider, but will cause issues if and when the provider checks the changes.\n"+
|
||||
"For more information and how to disable the reordering please consolidate our documentation at https://docs.dnscontrol.org/developer-info/ordering\n",
|
||||
dnsgraph.GetRecordsNamesForGraphables(a.UnresolvedRecords),
|
||||
)
|
||||
}
|
||||
|
||||
return a.SortedRecords
|
||||
}
|
11
pkg/dnsgraph/dependencies.go
Normal file
11
pkg/dnsgraph/dependencies.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package dnsgraph
|
||||
|
||||
func CreateDependencies(dependencyFQDNs []string, dependencyType DependencyType) []Dependency {
|
||||
var dependencies []Dependency
|
||||
|
||||
for _, dependency := range dependencyFQDNs {
|
||||
dependencies = append(dependencies, Dependency{NameFQDN: dependency, Type: dependencyType})
|
||||
}
|
||||
|
||||
return dependencies
|
||||
}
|
143
pkg/dnsgraph/dnsgraph.go
Normal file
143
pkg/dnsgraph/dnsgraph.go
Normal file
|
@ -0,0 +1,143 @@
|
|||
package dnsgraph
|
||||
|
||||
import "github.com/StackExchange/dnscontrol/v4/pkg/dnstree"
|
||||
|
||||
type edgeDirection uint8
|
||||
|
||||
const (
|
||||
IncomingEdge edgeDirection = iota
|
||||
OutgoingEdge
|
||||
)
|
||||
|
||||
type DNSGraphEdge[T Graphable] struct {
|
||||
Dependency Dependency
|
||||
Node *DNSGraphNode[T]
|
||||
Direction edgeDirection
|
||||
}
|
||||
|
||||
type DNSGraphEdges[T Graphable] []DNSGraphEdge[T]
|
||||
|
||||
type DNSGraphNode[T Graphable] struct {
|
||||
Data T
|
||||
Edges DNSGraphEdges[T]
|
||||
}
|
||||
|
||||
type dnsGraphNodes[T Graphable] []*DNSGraphNode[T]
|
||||
|
||||
type DNSGraph[T Graphable] struct {
|
||||
All dnsGraphNodes[T]
|
||||
Tree *dnstree.DomainTree[dnsGraphNodes[T]]
|
||||
}
|
||||
|
||||
func CreateGraph[T Graphable](entries []T) *DNSGraph[T] {
|
||||
graph := &DNSGraph[T]{
|
||||
All: dnsGraphNodes[T]{},
|
||||
Tree: dnstree.Create[dnsGraphNodes[T]](),
|
||||
}
|
||||
|
||||
for _, data := range entries {
|
||||
graph.AddNode(data)
|
||||
}
|
||||
|
||||
for _, sourceNode := range graph.All {
|
||||
for _, dependency := range sourceNode.Data.GetDependencies() {
|
||||
graph.AddEdge(sourceNode, dependency)
|
||||
}
|
||||
}
|
||||
|
||||
return graph
|
||||
}
|
||||
|
||||
func (graph *DNSGraph[T]) RemoveNode(toRemove *DNSGraphNode[T]) {
|
||||
for _, edge := range toRemove.Edges {
|
||||
edge.Node.Edges = edge.Node.Edges.RemoveNode(toRemove)
|
||||
}
|
||||
|
||||
graph.All = graph.All.RemoveNode(toRemove)
|
||||
|
||||
nodes := graph.Tree.Get(toRemove.Data.GetName())
|
||||
if nodes != nil {
|
||||
nodes = nodes.RemoveNode(toRemove)
|
||||
graph.Tree.Set(toRemove.Data.GetName(), nodes)
|
||||
}
|
||||
}
|
||||
|
||||
func (graph *DNSGraph[T]) AddNode(data T) {
|
||||
nodes := graph.Tree.Get(data.GetName())
|
||||
node := &DNSGraphNode[T]{
|
||||
Data: data,
|
||||
Edges: DNSGraphEdges[T]{},
|
||||
}
|
||||
if nodes == nil {
|
||||
nodes = dnsGraphNodes[T]{}
|
||||
}
|
||||
nodes = append(nodes, node)
|
||||
|
||||
graph.All = append(graph.All, node)
|
||||
graph.Tree.Set(data.GetName(), nodes)
|
||||
}
|
||||
|
||||
func (graph *DNSGraph[T]) AddEdge(sourceNode *DNSGraphNode[T], dependency Dependency) {
|
||||
destinationNodes := graph.Tree.Get(dependency.NameFQDN)
|
||||
|
||||
if destinationNodes == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, destinationNode := range destinationNodes {
|
||||
if sourceNode == destinationNode {
|
||||
continue
|
||||
}
|
||||
|
||||
if sourceNode.Edges.Contains(destinationNode, OutgoingEdge) {
|
||||
continue
|
||||
}
|
||||
|
||||
sourceNode.Edges = append(sourceNode.Edges, DNSGraphEdge[T]{
|
||||
Dependency: dependency,
|
||||
Node: destinationNode,
|
||||
Direction: OutgoingEdge,
|
||||
})
|
||||
|
||||
destinationNode.Edges = append(destinationNode.Edges, DNSGraphEdge[T]{
|
||||
Dependency: dependency,
|
||||
Node: sourceNode,
|
||||
Direction: IncomingEdge,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (nodes dnsGraphNodes[T]) RemoveNode(toRemove *DNSGraphNode[T]) dnsGraphNodes[T] {
|
||||
var newNodes dnsGraphNodes[T]
|
||||
|
||||
for _, node := range nodes {
|
||||
if node != toRemove {
|
||||
newNodes = append(newNodes, node)
|
||||
}
|
||||
}
|
||||
|
||||
return newNodes
|
||||
}
|
||||
|
||||
func (edges DNSGraphEdges[T]) RemoveNode(toRemove *DNSGraphNode[T]) DNSGraphEdges[T] {
|
||||
var newEdges DNSGraphEdges[T]
|
||||
|
||||
for _, edge := range edges {
|
||||
if edge.Node != toRemove {
|
||||
newEdges = append(newEdges, edge)
|
||||
}
|
||||
}
|
||||
|
||||
return newEdges
|
||||
}
|
||||
|
||||
func (edges DNSGraphEdges[T]) Contains(toFind *DNSGraphNode[T], direction edgeDirection) bool {
|
||||
|
||||
for _, edge := range edges {
|
||||
if edge.Node == toFind && edge.Direction == direction {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
68
pkg/dnsgraph/dnsgraph_test.go
Normal file
68
pkg/dnsgraph/dnsgraph_test.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package dnsgraph_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/dnsgraph"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/dnsgraph/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_CreateGraph(t *testing.T) {
|
||||
changes := []testutils.StubRecord{
|
||||
{NameFQDN: "example.com", Dependencies: []dnsgraph.Dependency{}},
|
||||
{NameFQDN: "mail.example.com", Dependencies: []dnsgraph.Dependency{{Type: dnsgraph.ForwardDependency, NameFQDN: "example.com"}, {Type: dnsgraph.ForwardDependency, NameFQDN: "someserver.example.com"}}},
|
||||
{NameFQDN: "*.hq.example.com", Dependencies: []dnsgraph.Dependency{{Type: dnsgraph.ForwardDependency, NameFQDN: "example.com"}}},
|
||||
{NameFQDN: "someserver.example.com", Dependencies: []dnsgraph.Dependency{{Type: dnsgraph.ForwardDependency, NameFQDN: "a.hq.example.com"}, {Type: dnsgraph.ForwardDependency, NameFQDN: "b.hq.example.com"}}},
|
||||
}
|
||||
|
||||
graph := dnsgraph.CreateGraph(testutils.StubRecordsAsGraphable(changes))
|
||||
|
||||
nodes := graph.Tree.Get("example.com")
|
||||
assert.Len(t, nodes, 1)
|
||||
assert.Len(t, nodes[0].Edges, 2)
|
||||
assert.Equal(t, "mail.example.com", nodes[0].Edges[0].Node.Data.GetName())
|
||||
assert.Equal(t, dnsgraph.IncomingEdge, nodes[0].Edges[0].Direction)
|
||||
assert.Equal(t, "*.hq.example.com", nodes[0].Edges[1].Node.Data.GetName())
|
||||
|
||||
nodes = graph.Tree.Get("someserver.example.com")
|
||||
assert.Len(t, nodes, 1)
|
||||
// *.hq.example.com is added once
|
||||
assert.Len(t, nodes[0].Edges, 2)
|
||||
assert.Equal(t, "mail.example.com", nodes[0].Edges[0].Node.Data.GetName())
|
||||
assert.Equal(t, dnsgraph.IncomingEdge, nodes[0].Edges[0].Direction)
|
||||
assert.Equal(t, "*.hq.example.com", nodes[0].Edges[1].Node.Data.GetName())
|
||||
assert.Equal(t, dnsgraph.OutgoingEdge, nodes[0].Edges[1].Direction)
|
||||
|
||||
nodes = graph.Tree.Get("a.hq.example.com")
|
||||
assert.Len(t, nodes, 1)
|
||||
// someserver.example.com is added once
|
||||
assert.Len(t, nodes[0].Edges, 2)
|
||||
assert.Equal(t, "example.com", nodes[0].Edges[0].Node.Data.GetName())
|
||||
assert.Equal(t, dnsgraph.OutgoingEdge, nodes[0].Edges[0].Direction)
|
||||
assert.Equal(t, "someserver.example.com", nodes[0].Edges[1].Node.Data.GetName())
|
||||
assert.Equal(t, dnsgraph.IncomingEdge, nodes[0].Edges[1].Direction)
|
||||
}
|
||||
|
||||
func Test_RemoveNode(t *testing.T) {
|
||||
changes := []testutils.StubRecord{
|
||||
{NameFQDN: "example.com", Dependencies: []dnsgraph.Dependency{}},
|
||||
{NameFQDN: "mail.example.com", Dependencies: []dnsgraph.Dependency{{Type: dnsgraph.ForwardDependency, NameFQDN: "example.com"}, {Type: dnsgraph.ForwardDependency, NameFQDN: "someserver.example.com"}}},
|
||||
{NameFQDN: "*.hq.example.com", Dependencies: []dnsgraph.Dependency{{Type: dnsgraph.ForwardDependency, NameFQDN: "example.com"}}},
|
||||
{NameFQDN: "someserver.example.com", Dependencies: []dnsgraph.Dependency{{Type: dnsgraph.ForwardDependency, NameFQDN: "a.hq.example.com"}, {Type: dnsgraph.ForwardDependency, NameFQDN: "b.hq.example.com"}}},
|
||||
}
|
||||
|
||||
graph := dnsgraph.CreateGraph(testutils.StubRecordsAsGraphable(changes))
|
||||
|
||||
graph.RemoveNode(graph.Tree.Get("example.com")[0])
|
||||
|
||||
// example.com change has been removed
|
||||
nodes := graph.Tree.Get("example.com")
|
||||
assert.Len(t, nodes, 0)
|
||||
|
||||
nodes = graph.Tree.Get("a.hq.example.com")
|
||||
assert.Len(t, nodes, 1)
|
||||
|
||||
assert.Len(t, nodes[0].Edges, 1)
|
||||
assert.Equal(t, "someserver.example.com", nodes[0].Edges[0].Node.Data.GetName())
|
||||
}
|
36
pkg/dnsgraph/graphable.go
Normal file
36
pkg/dnsgraph/graphable.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package dnsgraph
|
||||
|
||||
type NodeType uint8
|
||||
|
||||
const (
|
||||
Change NodeType = iota
|
||||
Report
|
||||
)
|
||||
|
||||
type DependencyType uint8
|
||||
|
||||
const (
|
||||
ForwardDependency DependencyType = iota
|
||||
BackwardDependency
|
||||
)
|
||||
|
||||
type Dependency struct {
|
||||
NameFQDN string
|
||||
Type DependencyType
|
||||
}
|
||||
|
||||
type Graphable interface {
|
||||
GetType() NodeType
|
||||
GetName() string
|
||||
GetDependencies() []Dependency
|
||||
}
|
||||
|
||||
func GetRecordsNamesForGraphables[T Graphable](graphables []T) []string {
|
||||
var names []string
|
||||
|
||||
for _, graphable := range graphables {
|
||||
names = append(names, graphable.GetName())
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
31
pkg/dnsgraph/testutils/stubrecords.go
Normal file
31
pkg/dnsgraph/testutils/stubrecords.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package testutils
|
||||
|
||||
import "github.com/StackExchange/dnscontrol/v4/pkg/dnsgraph"
|
||||
|
||||
type StubRecord struct {
|
||||
NameFQDN string
|
||||
Dependencies []dnsgraph.Dependency
|
||||
Type dnsgraph.NodeType
|
||||
}
|
||||
|
||||
func (record StubRecord) GetType() dnsgraph.NodeType {
|
||||
return record.Type
|
||||
}
|
||||
|
||||
func (record StubRecord) GetName() string {
|
||||
return record.NameFQDN
|
||||
}
|
||||
|
||||
func (record StubRecord) GetDependencies() []dnsgraph.Dependency {
|
||||
return record.Dependencies
|
||||
}
|
||||
|
||||
func StubRecordsAsGraphable(records []StubRecord) []dnsgraph.Graphable {
|
||||
sortableRecords := make([]dnsgraph.Graphable, len(records))
|
||||
|
||||
for iX := range records {
|
||||
sortableRecords[iX] = records[iX]
|
||||
}
|
||||
|
||||
return sortableRecords
|
||||
}
|
121
pkg/dnssort/graphsort.go
Normal file
121
pkg/dnssort/graphsort.go
Normal file
|
@ -0,0 +1,121 @@
|
|||
package dnssort
|
||||
|
||||
import "github.com/StackExchange/dnscontrol/v4/pkg/dnsgraph"
|
||||
|
||||
// SortUsingGraph sorts changes based on their dependencies using a directed graph.
|
||||
// Most changes have dependencies on other changes.
|
||||
// Either they are depend on already (a backwards dependency) or their new state depend on another record (a forwards dependency) which can be in our change set or an existing record.
|
||||
// A rough sketch of our sorting algorithm is as follows
|
||||
//
|
||||
// while graph has nodes:
|
||||
//
|
||||
// foreach node in graph:
|
||||
//
|
||||
// if node has no dependencies:
|
||||
// add node to sortedNodes
|
||||
// remove node from graph
|
||||
//
|
||||
// return sortedNodes
|
||||
//
|
||||
// The code below also accounts for the existence of cycles by tracking if any nodes were added to the sorted set in the last round.
|
||||
func SortUsingGraph[T dnsgraph.Graphable](records []T) SortResult[T] {
|
||||
sortState := createDirectedSortState(records)
|
||||
|
||||
for sortState.hasWork() {
|
||||
|
||||
for _, node := range sortState.graph.All {
|
||||
sortState.hasResolvedLastRound = false
|
||||
|
||||
if hasUnmetDependencies(node) {
|
||||
continue
|
||||
}
|
||||
|
||||
sortState.hasResolvedLastRound = true
|
||||
sortState.addSortedRecord(node.Data)
|
||||
sortState.graph.RemoveNode(node)
|
||||
}
|
||||
|
||||
if sortState.hasStalled() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
sortState.finalize()
|
||||
|
||||
return SortResult[T]{
|
||||
SortedRecords: sortState.sortedRecords,
|
||||
UnresolvedRecords: sortState.unresolvedRecords,
|
||||
}
|
||||
}
|
||||
|
||||
type directedSortState[T dnsgraph.Graphable] struct {
|
||||
graph *dnsgraph.DNSGraph[T]
|
||||
sortedRecords []T
|
||||
unresolvedRecords []T
|
||||
hasResolvedLastRound bool
|
||||
}
|
||||
|
||||
func createDirectedSortState[T dnsgraph.Graphable](records []T) directedSortState[T] {
|
||||
changes, reportChanges := splitRecordsByType(records)
|
||||
|
||||
graph := dnsgraph.CreateGraph(changes)
|
||||
|
||||
return directedSortState[T]{
|
||||
graph: graph,
|
||||
unresolvedRecords: []T{},
|
||||
sortedRecords: reportChanges,
|
||||
hasResolvedLastRound: false,
|
||||
}
|
||||
}
|
||||
|
||||
func splitRecordsByType[T dnsgraph.Graphable](records []T) ([]T, []T) {
|
||||
var changes []T
|
||||
var reports []T
|
||||
|
||||
for _, record := range records {
|
||||
switch record.GetType() {
|
||||
case dnsgraph.Report:
|
||||
reports = append(reports, record)
|
||||
case dnsgraph.Change:
|
||||
changes = append(changes, record)
|
||||
}
|
||||
}
|
||||
|
||||
return changes, reports
|
||||
}
|
||||
|
||||
func (sortState *directedSortState[T]) hasWork() bool {
|
||||
return len(sortState.graph.All) > 0
|
||||
}
|
||||
|
||||
func (sortState *directedSortState[T]) hasStalled() bool {
|
||||
return !sortState.hasResolvedLastRound
|
||||
}
|
||||
|
||||
func (sortState *directedSortState[T]) addSortedRecord(node T) {
|
||||
sortState.sortedRecords = append(sortState.sortedRecords, node)
|
||||
}
|
||||
|
||||
func (sortState *directedSortState[T]) finalize() {
|
||||
// Add all of the changes remaining in the graph as unresolved and add them at the end of the sorted result to at least include everything
|
||||
if len(sortState.graph.All) > 0 {
|
||||
for _, unresolved := range sortState.graph.All {
|
||||
sortState.addSortedRecord(unresolved.Data)
|
||||
sortState.unresolvedRecords = append(sortState.unresolvedRecords, unresolved.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hasUnmetDependencies[T dnsgraph.Graphable](node *dnsgraph.DNSGraphNode[T]) bool {
|
||||
for _, edge := range node.Edges {
|
||||
if edge.Dependency.Type == dnsgraph.BackwardDependency && edge.Direction == dnsgraph.IncomingEdge {
|
||||
return true
|
||||
}
|
||||
|
||||
if edge.Dependency.Type == dnsgraph.ForwardDependency && edge.Direction == dnsgraph.OutgoingEdge {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
191
pkg/dnssort/graphsort_test.go
Normal file
191
pkg/dnssort/graphsort_test.go
Normal file
|
@ -0,0 +1,191 @@
|
|||
package dnssort_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/dnsgraph"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/dnsgraph/testutils"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/dnssort"
|
||||
)
|
||||
|
||||
func Test_graphsort(t *testing.T) {
|
||||
|
||||
t.Run("Direct dependency",
|
||||
executeGraphSort(
|
||||
[]testutils.StubRecord{
|
||||
{NameFQDN: "www.example.com", Dependencies: []dnsgraph.Dependency{{Type: dnsgraph.ForwardDependency, NameFQDN: "example.com"}}},
|
||||
{NameFQDN: "example.com", Dependencies: []dnsgraph.Dependency{}},
|
||||
},
|
||||
[]string{
|
||||
"example.com",
|
||||
"www.example.com",
|
||||
},
|
||||
[]string{},
|
||||
),
|
||||
)
|
||||
|
||||
t.Run("Already in correct order",
|
||||
executeGraphSort(
|
||||
[]testutils.StubRecord{
|
||||
{NameFQDN: "example.com", Dependencies: []dnsgraph.Dependency{}},
|
||||
{NameFQDN: "www.example.com", Dependencies: []dnsgraph.Dependency{{Type: dnsgraph.ForwardDependency, NameFQDN: "example.com"}}},
|
||||
},
|
||||
[]string{
|
||||
"example.com",
|
||||
"www.example.com",
|
||||
},
|
||||
[]string{},
|
||||
),
|
||||
)
|
||||
|
||||
t.Run("Use wildcards",
|
||||
executeGraphSort(
|
||||
[]testutils.StubRecord{
|
||||
{NameFQDN: "www.example.com", Dependencies: []dnsgraph.Dependency{{Type: dnsgraph.ForwardDependency, NameFQDN: "a.test.example.com"}}},
|
||||
{NameFQDN: "*.test.example.com", Dependencies: []dnsgraph.Dependency{}},
|
||||
},
|
||||
[]string{
|
||||
"*.test.example.com",
|
||||
"www.example.com",
|
||||
},
|
||||
[]string{},
|
||||
),
|
||||
)
|
||||
|
||||
t.Run("Cyclic dependency added on the end",
|
||||
executeGraphSort(
|
||||
[]testutils.StubRecord{
|
||||
{NameFQDN: "a.example.com", Dependencies: []dnsgraph.Dependency{{Type: dnsgraph.ForwardDependency, NameFQDN: "b.example.com"}}},
|
||||
{NameFQDN: "b.example.com", Dependencies: []dnsgraph.Dependency{{Type: dnsgraph.ForwardDependency, NameFQDN: "a.example.com"}}},
|
||||
{NameFQDN: "www.example.com", Dependencies: []dnsgraph.Dependency{{Type: dnsgraph.ForwardDependency, NameFQDN: "example.com"}}},
|
||||
{NameFQDN: "example.com", Dependencies: []dnsgraph.Dependency{}},
|
||||
},
|
||||
[]string{
|
||||
"example.com",
|
||||
"www.example.com",
|
||||
"a.example.com",
|
||||
"b.example.com",
|
||||
},
|
||||
[]string{
|
||||
"a.example.com",
|
||||
"b.example.com",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
t.Run("Dependency on the same named record resolves in correct order",
|
||||
executeGraphSort(
|
||||
[]testutils.StubRecord{
|
||||
{NameFQDN: "a.example.com", Dependencies: []dnsgraph.Dependency{{Type: dnsgraph.ForwardDependency, NameFQDN: "a.example.com"}}},
|
||||
{NameFQDN: "www.example.com", Dependencies: []dnsgraph.Dependency{{Type: dnsgraph.ForwardDependency, NameFQDN: "example.com"}}},
|
||||
{NameFQDN: "example.com", Dependencies: []dnsgraph.Dependency{{}}},
|
||||
{NameFQDN: "a.example.com", Dependencies: []dnsgraph.Dependency{}},
|
||||
},
|
||||
[]string{
|
||||
"example.com",
|
||||
"a.example.com",
|
||||
"a.example.com",
|
||||
"www.example.com",
|
||||
},
|
||||
[]string{},
|
||||
),
|
||||
)
|
||||
|
||||
t.Run("Ignores external dependency",
|
||||
executeGraphSort(
|
||||
[]testutils.StubRecord{
|
||||
{NameFQDN: "mail.example.com", Dependencies: []dnsgraph.Dependency{{Type: dnsgraph.ForwardDependency, NameFQDN: "mail.external.tld"}}},
|
||||
},
|
||||
[]string{
|
||||
"mail.example.com",
|
||||
},
|
||||
[]string{},
|
||||
),
|
||||
)
|
||||
|
||||
t.Run("Deletions in correct order",
|
||||
executeGraphSort(
|
||||
[]testutils.StubRecord{
|
||||
{NameFQDN: "mail.example.com", Dependencies: []dnsgraph.Dependency{}},
|
||||
{NameFQDN: "example.com", Dependencies: []dnsgraph.Dependency{{NameFQDN: "mail.example.com", Type: dnsgraph.BackwardDependency}}},
|
||||
},
|
||||
[]string{
|
||||
"example.com",
|
||||
"mail.example.com",
|
||||
},
|
||||
[]string{},
|
||||
),
|
||||
)
|
||||
|
||||
t.Run("A Change with dependency on old and new state",
|
||||
executeGraphSort(
|
||||
[]testutils.StubRecord{
|
||||
{NameFQDN: "bar2.example.com", Dependencies: []dnsgraph.Dependency{{}}},
|
||||
{NameFQDN: "foo.example.com", Dependencies: []dnsgraph.Dependency{{NameFQDN: "bar2.example.com", Type: dnsgraph.BackwardDependency}, {NameFQDN: "new2.example.com", Type: dnsgraph.ForwardDependency}}},
|
||||
{NameFQDN: "new2.example.com", Dependencies: []dnsgraph.Dependency{{}}},
|
||||
},
|
||||
[]string{
|
||||
"new2.example.com",
|
||||
"foo.example.com",
|
||||
"bar2.example.com",
|
||||
},
|
||||
[]string{},
|
||||
),
|
||||
)
|
||||
|
||||
t.Run("Reports first and ignore dependencies",
|
||||
executeGraphSort(
|
||||
[]testutils.StubRecord{
|
||||
{NameFQDN: "example.com", Dependencies: []dnsgraph.Dependency{}},
|
||||
{NameFQDN: "test.example.com", Dependencies: []dnsgraph.Dependency{{Type: dnsgraph.ForwardDependency, NameFQDN: "example.com"}}, Type: dnsgraph.Report},
|
||||
},
|
||||
[]string{
|
||||
"test.example.com",
|
||||
"example.com",
|
||||
},
|
||||
[]string{},
|
||||
),
|
||||
)
|
||||
|
||||
t.Run("Reports are not resolvable as dependencies",
|
||||
executeGraphSort(
|
||||
[]testutils.StubRecord{
|
||||
{NameFQDN: "test2.example.com", Dependencies: []dnsgraph.Dependency{{Type: dnsgraph.ForwardDependency, NameFQDN: "test.example.com"}}},
|
||||
{NameFQDN: "test.example.com", Dependencies: []dnsgraph.Dependency{}, Type: dnsgraph.Report},
|
||||
},
|
||||
[]string{
|
||||
"test.example.com",
|
||||
"test2.example.com",
|
||||
},
|
||||
[]string{},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func executeGraphSort(inputOrder []testutils.StubRecord, expectedOutputOrder []string, expectedUnresolved []string) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
inputSortableRecords := testutils.StubRecordsAsGraphable(inputOrder)
|
||||
t.Helper()
|
||||
result := dnssort.SortUsingGraph(inputSortableRecords)
|
||||
|
||||
if len(expectedOutputOrder) != len(result.SortedRecords) {
|
||||
t.Errorf("Missing records after sort. Expected order: %v. Got order: %v\n", expectedOutputOrder, dnsgraph.GetRecordsNamesForGraphables(result.SortedRecords))
|
||||
} else {
|
||||
for iX := range expectedOutputOrder {
|
||||
if result.SortedRecords[iX].GetName() != expectedOutputOrder[iX] {
|
||||
t.Errorf("Invalid order on index %d after sort. Expected order: %v. Got order: %v\n", iX, expectedOutputOrder, dnsgraph.GetRecordsNamesForGraphables(result.SortedRecords))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(expectedUnresolved) != len(result.UnresolvedRecords) {
|
||||
t.Errorf("Missing unresolved records. Expected unresolved: %v. Got: %v\n", expectedUnresolved, dnsgraph.GetRecordsNamesForGraphables(result.UnresolvedRecords))
|
||||
} else {
|
||||
for iX := range expectedUnresolved {
|
||||
if result.UnresolvedRecords[iX].GetName() != expectedUnresolved[iX] {
|
||||
t.Errorf("Invalid unresolved records after sort. Expected: %v. Got: %v\n", expectedOutputOrder, dnsgraph.GetRecordsNamesForGraphables(result.UnresolvedRecords))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
8
pkg/dnssort/result.go
Normal file
8
pkg/dnssort/result.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package dnssort
|
||||
|
||||
import "github.com/StackExchange/dnscontrol/v4/pkg/dnsgraph"
|
||||
|
||||
type SortResult[T dnsgraph.Graphable] struct {
|
||||
SortedRecords []T
|
||||
UnresolvedRecords []T
|
||||
}
|
144
pkg/dnstree/dnstree.go
Normal file
144
pkg/dnstree/dnstree.go
Normal file
|
@ -0,0 +1,144 @@
|
|||
package dnstree
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Create creates a tree like structure to add arbitrary data to DNS names.
|
||||
// The DomainTree splits the domain name based on the dot (.), reverses the resulting list and add all strings the tree in order.
|
||||
// It has support for wildcard domain names the tree nodes (`Set`), but not during retrieval (Get and Has).
|
||||
// Get always returns the most specific node; it doesn't immediately return the node upon finding a wildcard node.
|
||||
func Create[T any]() *DomainTree[T] {
|
||||
return &DomainTree[T]{
|
||||
IsLeaf: false,
|
||||
IsWildcard: false,
|
||||
Name: "",
|
||||
Children: map[string]*domainNode[T]{},
|
||||
}
|
||||
}
|
||||
|
||||
type DomainTree[T any] domainNode[T]
|
||||
|
||||
type domainNode[T any] struct {
|
||||
IsLeaf bool
|
||||
IsWildcard bool
|
||||
Name string
|
||||
Children map[string]*domainNode[T]
|
||||
data T
|
||||
}
|
||||
|
||||
func createNode[T any](name string) *domainNode[T] {
|
||||
return &domainNode[T]{
|
||||
IsLeaf: false,
|
||||
Name: name,
|
||||
Children: map[string]*domainNode[T]{},
|
||||
}
|
||||
}
|
||||
|
||||
// Set adds given data to the given fqdn.
|
||||
// The FQDN can contain a wildcard on the start.
|
||||
// example fqdn: *.example.com
|
||||
func (tree *DomainTree[T]) Set(fqdn string, data T) {
|
||||
domainParts := splitFQDN(fqdn)
|
||||
|
||||
isWildcard := domainParts[0] == "*"
|
||||
if isWildcard {
|
||||
domainParts = domainParts[1:]
|
||||
}
|
||||
|
||||
ptr := (*domainNode[T])(tree)
|
||||
for iX := len(domainParts) - 1; iX > 0; iX -= 1 {
|
||||
ptr = ptr.addIntermediate(domainParts[iX])
|
||||
}
|
||||
|
||||
ptr.addLeaf(domainParts[0], isWildcard, data)
|
||||
}
|
||||
|
||||
// Retrieves the attached data from a given FQDN.
|
||||
// The tree will return the data entry for the most specific FQDN entry.
|
||||
// If no entry is found Get will return the default value for the specific type.
|
||||
//
|
||||
// tree.Set("*.example.com", 1)
|
||||
// tree.Set("a.example.com", 2)
|
||||
// tree.Get("a.example.com") // 2
|
||||
// tree.Get("a.a.example.com") // 1
|
||||
// tree.Get("other.com") // 0
|
||||
func (tree *DomainTree[T]) Get(fqdn string) T {
|
||||
domainParts := splitFQDN(fqdn)
|
||||
|
||||
var mostSpecificNode *domainNode[T]
|
||||
ptr := (*domainNode[T])(tree)
|
||||
|
||||
for iX := len(domainParts) - 1; iX >= 0; iX -= 1 {
|
||||
node, ok := ptr.Children[domainParts[iX]]
|
||||
if !ok {
|
||||
if mostSpecificNode != nil {
|
||||
return mostSpecificNode.data
|
||||
}
|
||||
return *new(T)
|
||||
}
|
||||
|
||||
if node.IsWildcard {
|
||||
mostSpecificNode = node
|
||||
}
|
||||
|
||||
ptr = node
|
||||
}
|
||||
|
||||
if ptr.IsLeaf || ptr.IsWildcard {
|
||||
return ptr.data
|
||||
}
|
||||
|
||||
if mostSpecificNode != nil {
|
||||
return mostSpecificNode.data
|
||||
}
|
||||
|
||||
return *new(T)
|
||||
}
|
||||
|
||||
// Has returns if the tree contains data for given FQDN.
|
||||
func (tree *DomainTree[T]) Has(fqdn string) bool {
|
||||
domainParts := splitFQDN(fqdn)
|
||||
|
||||
var mostSpecificNode *domainNode[T]
|
||||
ptr := (*domainNode[T])(tree)
|
||||
|
||||
for iX := len(domainParts) - 1; iX >= 0; iX -= 1 {
|
||||
node, ok := ptr.Children[domainParts[iX]]
|
||||
if !ok {
|
||||
return mostSpecificNode != nil
|
||||
}
|
||||
|
||||
if node.IsWildcard {
|
||||
mostSpecificNode = node
|
||||
}
|
||||
|
||||
ptr = node
|
||||
}
|
||||
|
||||
return ptr.IsLeaf || ptr.IsWildcard || mostSpecificNode != nil
|
||||
}
|
||||
|
||||
func splitFQDN(fqdn string) []string {
|
||||
normalizedFQDN := strings.TrimSuffix(fqdn, ".")
|
||||
|
||||
return strings.Split(normalizedFQDN, ".")
|
||||
}
|
||||
|
||||
func (tree *domainNode[T]) addIntermediate(name string) *domainNode[T] {
|
||||
if _, ok := tree.Children[name]; !ok {
|
||||
tree.Children[name] = createNode[T](name)
|
||||
}
|
||||
|
||||
return tree.Children[name]
|
||||
}
|
||||
|
||||
func (tree *domainNode[T]) addLeaf(name string, isWildcard bool, data T) *domainNode[T] {
|
||||
node := tree.addIntermediate(name)
|
||||
|
||||
node.data = data
|
||||
node.IsLeaf = true
|
||||
node.IsWildcard = node.IsWildcard || isWildcard
|
||||
|
||||
return node
|
||||
}
|
64
pkg/dnstree/dnstree_test.go
Normal file
64
pkg/dnstree/dnstree_test.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package dnstree_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/dnstree"
|
||||
)
|
||||
|
||||
func Test_domaintree(t *testing.T) {
|
||||
|
||||
t.Run("Single FQDN",
|
||||
executeTreeTest(
|
||||
[]string{
|
||||
"other.example.com",
|
||||
},
|
||||
[]string{"other.example.com"},
|
||||
[]string{"com", "x.example.com", "x.www.example.com", "example.com"},
|
||||
),
|
||||
)
|
||||
|
||||
t.Run("Wildcard",
|
||||
executeTreeTest(
|
||||
[]string{
|
||||
"*.example.com",
|
||||
},
|
||||
[]string{"example.com", "other.example.com"},
|
||||
[]string{"com", "example.nl", "*.com"},
|
||||
),
|
||||
)
|
||||
|
||||
t.Run("Combined domains",
|
||||
executeTreeTest(
|
||||
[]string{
|
||||
"*.other.example.com",
|
||||
"specific.example.com",
|
||||
"specific.example.nl",
|
||||
},
|
||||
[]string{"any.other.example.com", "specific.example.com", "specific.example.nl"},
|
||||
[]string{"com", "nl", "", "example.nl", "other.nl"},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func executeTreeTest(inputs []string, founds []string, missings []string) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
t.Helper()
|
||||
tree := dnstree.Create[interface{}]()
|
||||
for _, input := range inputs {
|
||||
tree.Set(input, struct{}{})
|
||||
}
|
||||
|
||||
for _, found := range founds {
|
||||
if tree.Has(found) == false {
|
||||
t.Errorf("Expected %s to be found in tree, but is missing", found)
|
||||
}
|
||||
}
|
||||
|
||||
for _, missing := range missings {
|
||||
if tree.Has(missing) == true {
|
||||
t.Errorf("Expected %s to be missing in tree, but is found", missing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue