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:
Vincent Hagen 2023-08-29 20:00:09 +02:00 committed by GitHub
parent e26afaf7e0
commit e32bdc053f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1265 additions and 205 deletions

View file

@ -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",

View file

@ -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>

View file

@ -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
View 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).

View file

@ -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."),
),

View file

@ -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.

View file

@ -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
}

View file

@ -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)"},
},
},
},

View file

@ -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)

View file

@ -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:

View file

@ -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
View 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
}

View 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
View 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
}

View 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
View 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
}

View 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
View 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
}

View 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
View 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
View 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
}

View 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)
}
}
}
}