From 960dc66bd2d65cbd4d495a244713bb32b8b36731 Mon Sep 17 00:00:00 2001 From: Mike Cochrane Date: Wed, 19 Aug 2020 03:14:34 +1200 Subject: [PATCH] New feature: IGNORE_TARGET. Rename INGORE to IGNORE_NAME (#806) --- docs/_functions/domain/IGNORE.md | 37 +-- docs/_functions/domain/IGNORE_NAME.md | 40 ++++ docs/_functions/domain/IGNORE_TARGET.md | 34 +++ integrationTest/integration_test.go | 52 +++-- models/dns.go | 10 + models/domain.go | 19 +- pkg/diff/diff.go | 69 ++++-- pkg/diff/diff_test.go | 84 +++++-- pkg/js/helpers.js | 19 +- pkg/js/parse_tests/019-ignored-records.js | 4 +- pkg/js/parse_tests/019-ignored-records.json | 10 +- .../parse_tests/023-ignored-glob-records.json | 2 +- pkg/js/static.go | 219 +++++++++--------- 13 files changed, 397 insertions(+), 202 deletions(-) create mode 100644 docs/_functions/domain/IGNORE_NAME.md create mode 100644 docs/_functions/domain/IGNORE_TARGET.md diff --git a/docs/_functions/domain/IGNORE.md b/docs/_functions/domain/IGNORE.md index 864097ea8..f0667157a 100644 --- a/docs/_functions/domain/IGNORE.md +++ b/docs/_functions/domain/IGNORE.md @@ -2,39 +2,4 @@ name: IGNORE --- -IGNORE can be used to ignore some records presents in zone. -All records (independently of their type) of that name will be completely ignored. - -IGNORE is like NO_PURGE except it acts only on some specific records instead of the whole zone. - -IGNORE is generally used in very specific situations: - -* Some records are managed by some other system and DNSControl is only used to manage some records and/or keep them updated. For example a DNS record that is managed by Kubernetes External DNS, but DNSControl is used to manage the rest of the zone. In this case we don't want dnscontrol to try to delete the externally managed record. -* To work-around a pseudo record type that is not supported by DNSControl. For example some providers have a fake DNS record type called "URL" which creates a redirect. DNSControl normally deletes these records because it doesn't understand them. IGNORE will leave those records alone. - -In this example, dnscontrol will insert/update the "baz.example.com" record but will leave unchanged the "foo.example.com" and "bar.example.com" ones. - -{% include startExample.html %} -{% highlight js %} -D("example.com", - IGNORE("foo"), - IGNORE("bar"), - A("baz", "1.2.3.4") -); -{%endhighlight%} -{% include endExample.html %} - -IGNORE also supports glob patterns in the style of the [gobwas/glob](https://github.com/gobwas/glob) library. All of -the following patterns will work: - -* `IGNORE("*.foo")` will ignore all records in the style of `bar.foo`, but will not ignore records using a double -subdomain, such as `foo.bar.foo`. -* `IGNORE("**.foo")` will ignore all subdomains of `foo`, including double subdomains. -* `IGNORE("?oo")` will ignore all records of three symbols ending in `oo`, for example `foo` and `zoo`. It will -not match `.` -* `IGNORE("[abc]oo")` will ignore records `aoo`, `boo` and `coo`. `IGNORE("[a-c]oo")` is equivalent. -* `IGNORE("[!abc]oo")` will ignore all three symbol records ending in `oo`, except for `aoo`, `boo`, `coo`. `IGNORE("[!a-c]oo")` is equivalent. -* `IGNORE("{bar,[fz]oo}` will ignore `bar`, `foo` and `zoo`. -* `IGNORE("\\*.foo")` will ignore the literal record `*.foo`. - -It is considered as an error to try to manage an ignored record. \ No newline at end of file +IGNORE has been renamed to `IGNORE_NAME`. IGNORE will continue to function, but its use is deprecated. Please update your configuration files to use `IGNORE_NAME`. diff --git a/docs/_functions/domain/IGNORE_NAME.md b/docs/_functions/domain/IGNORE_NAME.md new file mode 100644 index 000000000..d04f6bede --- /dev/null +++ b/docs/_functions/domain/IGNORE_NAME.md @@ -0,0 +1,40 @@ +--- +name: IGNORE_NAME +--- + +IGNORE_NAME can be used to ignore some records present in zone. +All records (independently of their type) of that name will be completely ignored. + +IGNORE_NAME is like NO_PURGE except it acts only on some specific records instead of the whole zone. + +IGNORE_NAME is generally used in very specific situations: + +* Some records are managed by some other system and DNSControl is only used to manage some records and/or keep them updated. For example a DNS record that is managed by Kubernetes External DNS, but DNSControl is used to manage the rest of the zone. In this case we don't want DNSControl to try to delete the externally managed record. +* To work-around a pseudo record type that is not supported by DNSControl. For example some providers have a fake DNS record type called "URL" which creates a redirect. DNSControl normally deletes these records because it doesn't understand them. IGNORE_NAME will leave those records alone. + +In this example, DNSControl will insert/update the "baz.example.com" record but will leave unchanged the "foo.example.com" and "bar.example.com" ones. + +{% include startExample.html %} +{% highlight js %} +D("example.com", + IGNORE_NAME("foo"), + IGNORE_NAME("bar"), + A("baz", "1.2.3.4") +); +{%endhighlight%} +{% include endExample.html %} + +IGNORE_NAME also supports glob patterns in the style of the [gobwas/glob](https://github.com/gobwas/glob) library. All of +the following patterns will work: + +* `IGNORE_NAME("*.foo")` will ignore all records in the style of `bar.foo`, but will not ignore records using a double +subdomain, such as `foo.bar.foo`. +* `IGNORE_NAME("**.foo")` will ignore all subdomains of `foo`, including double subdomains. +* `IGNORE_NAME("?oo")` will ignore all records of three symbols ending in `oo`, for example `foo` and `zoo`. It will +not match `.` +* `IGNORE_NAME("[abc]oo")` will ignore records `aoo`, `boo` and `coo`. `IGNORE_NAME("[a-c]oo")` is equivalent. +* `IGNORE_NAME("[!abc]oo")` will ignore all three symbol records ending in `oo`, except for `aoo`, `boo`, `coo`. `IGNORE_NAME("[!a-c]oo")` is equivalent. +* `IGNORE_NAME("{bar,[fz]oo}")` will ignore `bar`, `foo` and `zoo`. +* `IGNORE_NAME("\\*.foo")` will ignore the literal record `*.foo`. + +It is considered as an error to try to manage an ignored record. \ No newline at end of file diff --git a/docs/_functions/domain/IGNORE_TARGET.md b/docs/_functions/domain/IGNORE_TARGET.md new file mode 100644 index 000000000..92070b03e --- /dev/null +++ b/docs/_functions/domain/IGNORE_TARGET.md @@ -0,0 +1,34 @@ +--- +name: IGNORE_TARGET +parameters: + - pattern + - rType +--- + +IGNORE_TARGET can be used to ignore some records present in zone based on the record's target and type. IGNORE_TARGET currently only supports CNAME record types. + +IGNORE_TARGET is like NO_PURGE except it acts only on some specific records instead of the whole zone. + +IGNORE_TARGET is generally used in very specific situations: + +* Some records are managed by some other system and DNSControl is only used to manage some records and/or keep them updated. For example a DNS record that is created by AWS Certificate Manager for validation, but DNSControl is used to manage the rest of the zone. In this case we don't want DNSControl to try to delete the externally managed record. + +In this example, DNSControl will insert/update the "baz.example.com" record but will leave unchanged a CNAME to "foo.acm-validations.aws" record. + +{% include startExample.html %} +{% highlight js %} +D("example.com", + IGNORE_TARGET('**.acm-validations.aws.', 'CNAME'), + A("baz", "1.2.3.4") +); +{%endhighlight%} +{% include endExample.html %} + +IGNORE_TARGET also supports glob patterns in the style of the [gobwas/glob](https://github.com/gobwas/glob#example) library. Some example patterns: + +* `IGNORE_TARGET("example.com", "CNAME")` will ignore all CNAME records with targets of exactly `example.com`. +* `IGNORE_TARGET("*.foo", "CNAME")` will ignore all CNAME records with targets in the style of `bar.foo`, but will not ignore records with targets using a double subdomain, such as `foo.bar.foo`. +* `IGNORE_TARGET("**.bar", "CNAME")` will ignore all CNAME records with target subdomains of `bar`, including double subdomains such as `www.foo.bar`. +* `IGNORE_TARGET("dev.*.foo", "CNAME")` will ignore all CNAME records with targets in the style of `dev.bar.foo`, but will not ignore records with targets using a double subdomain, such as `dev.foo.bar.foo`. + +It is considered as an error to try to manage an ignored record. \ No newline at end of file diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index a0f76c863..7cd9a1099 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -162,7 +162,8 @@ func makeChanges(t *testing.T, prv providers.DNSServiceProvider, dc *models.Doma //} dom.Records = append(dom.Records, &rc) } - dom.IgnoredLabels = tst.IgnoredLabels + dom.IgnoredNames = tst.IgnoredNames + dom.IgnoredTargets = tst.IgnoredTargets models.PostProcessRecords(dom.Records) dom2, _ := dom.Copy() @@ -301,9 +302,10 @@ type TestGroup struct { } type TestCase struct { - Desc string - Records []*rec - IgnoredLabels []string + Desc string + Records []*rec + IgnoredNames []string + IgnoredTargets []*models.IgnoreTarget } type rec models.RecordConfig @@ -426,9 +428,18 @@ func tlsa(name string, usage, selector, matchingtype uint8, target string) *rec return r } -func ignore(name string) *rec { +func ignoreName(name string) *rec { r := &rec{ - Type: "IGNORE", + Type: "IGNORE_NAME", + } + r.SetLabel(name, "**current-domain**") + return r +} + +func ignoreTarget(name string, typ string) *rec { + r := &rec{ + Type: "IGNORE_TARGET", + Target: typ, } r.SetLabel(name, "**current-domain**") return r @@ -490,10 +501,16 @@ func testgroup(desc string, items ...interface{}) *TestGroup { func tc(desc string, recs ...*rec) *TestCase { var records []*rec - var ignored []string + var ignoredNames []string + var ignoredTargets []*models.IgnoreTarget for _, r := range recs { - if r.Type == "IGNORE" { - ignored = append(ignored, r.GetLabel()) + if r.Type == "IGNORE_NAME" { + ignoredNames = append(ignoredNames, r.GetLabel()) + } else if r.Type == "IGNORE_TARGET" { + ignoredTargets = append(ignoredTargets, &models.IgnoreTarget{ + Pattern: r.GetLabel(), + Type: r.Target, + }) } else { records = append(records, r) } @@ -501,7 +518,8 @@ func tc(desc string, recs ...*rec) *TestCase { return &TestCase{ Desc: desc, Records: records, - IgnoredLabels: ignored, + IgnoredNames: ignoredNames, + IgnoredTargets: ignoredTargets, } } @@ -637,12 +655,20 @@ func makeTests(t *testing.T) []*TestGroup { tc("NS Record pointing to @", ns("foo", "**current-domain**")), ), - testgroup("IGNORE function", + testgroup("IGNORE_NAME function", tc("Create some records", txt("foo", "simple"), a("foo", "1.2.3.4")), - tc("Add a new record - ignoring foo", a("bar", "1.2.3.4"), ignore("foo")), + tc("Add a new record - ignoring foo", a("bar", "1.2.3.4"), ignoreName("foo")), clear(), tc("Create some records", txt("bar.foo", "simple"), a("bar.foo", "1.2.3.4")), - tc("Add a new record - ignoring *.foo", a("bar", "1.2.3.4"), ignore("*.foo")), + tc("Add a new record - ignoring *.foo", a("bar", "1.2.3.4"), ignoreName("*.foo")), + ), + + testgroup("IGNORE_TARGET function", + tc("Create some records", cname("foo", "test.foo.com."), cname("bar", "test.foo.com.")), + tc("Add a new record - ignoring foo", cname("bar", "bar.foo.com."), ignoreTarget("test.foo.com.", "CNAME")), + clear(), + tc("Create some records", cname("bar.foo", "a.b.foo.com."), a("bar.foo", "1.2.3.4")), + tc("Add a new record - ignoring **.foo.com targets", a("bar", "1.2.3.4"), ignoreTarget("**.foo.com.", "CNAME")), ), testgroup("single TXT", diff --git a/models/dns.go b/models/dns.go index 09fc07288..77df62b60 100644 --- a/models/dns.go +++ b/models/dns.go @@ -126,3 +126,13 @@ func (config *DNSConfig) DomainContainingFQDN(fqdn string) *DomainConfig { } return d } + +// IgnoreTarget describes an IGNORE_TARGET rule. +type IgnoreTarget struct { + Pattern string `json:"pattern"` // Glob pattern + Type string `json:"type"` // All caps rtype name. +} + +func (i *IgnoreTarget) String() string { + return i.Pattern +} diff --git a/models/domain.go b/models/domain.go index 4273cc1e9..772fc4fca 100644 --- a/models/domain.go +++ b/models/domain.go @@ -8,16 +8,17 @@ import ( // DomainConfig describes a DNS domain (tecnically a DNS zone). type DomainConfig struct { - Name string `json:"name"` // NO trailing "." - RegistrarName string `json:"registrar"` - DNSProviderNames map[string]int `json:"dnsProviders"` + Name string `json:"name"` // NO trailing "." + RegistrarName string `json:"registrar"` + DNSProviderNames map[string]int `json:"dnsProviders"` - Metadata map[string]string `json:"meta,omitempty"` - Records Records `json:"records"` - Nameservers []*Nameserver `json:"nameservers,omitempty"` - KeepUnknown bool `json:"keepunknown,omitempty"` - IgnoredLabels []string `json:"ignored_labels,omitempty"` - AutoDNSSEC bool `json:"auto_dnssec,omitempty"` + Metadata map[string]string `json:"meta,omitempty"` + Records Records `json:"records"` + Nameservers []*Nameserver `json:"nameservers,omitempty"` + KeepUnknown bool `json:"keepunknown,omitempty"` + IgnoredNames []string `json:"ignored_names,omitempty"` + IgnoredTargets []*IgnoreTarget `json:"ignored_targets,omitempty"` + AutoDNSSEC bool `json:"auto_dnssec,omitempty"` //DNSSEC bool `json:"dnssec,omitempty"` // These fields contain instantiated provider instances once everything is linked up. diff --git a/pkg/diff/diff.go b/pkg/diff/diff.go index fc6e29e03..c9f68b480 100644 --- a/pkg/diff/diff.go +++ b/pkg/diff/diff.go @@ -35,8 +35,11 @@ func New(dc *models.DomainConfig, extraValues ...func(*models.RecordConfig) map[ dc: dc, extraValues: extraValues, - // compile IGNORE glob patterns - compiledIgnoredLabels: compileIgnoredLabels(dc.IgnoredLabels), + // compile IGNORE_NAME glob patterns + compiledIgnoredNames: compileIgnoredNames(dc.IgnoredNames), + + // compile IGNORE_TARGET glob patterns + compiledIgnoredTargets: compileIgnoredTargets(dc.IgnoredTargets), } } @@ -44,7 +47,8 @@ type differ struct { dc *models.DomainConfig extraValues []func(*models.RecordConfig) map[string]string - compiledIgnoredLabels []glob.Glob + compiledIgnoredNames []glob.Glob + compiledIgnoredTargets []glob.Glob } // get normalized content for record. target, ttl, mxprio, and specified metadata @@ -93,16 +97,20 @@ func (d *differ) IncrementalDiff(existing []*models.RecordConfig) (unchanged, cr existingByNameAndType := map[models.RecordKey][]*models.RecordConfig{} desiredByNameAndType := map[models.RecordKey][]*models.RecordConfig{} for _, e := range existing { - if d.matchIgnored(e.GetLabel()) { - printer.Debugf("Ignoring record %s %s due to IGNORE\n", e.GetLabel(), e.Type) + if d.matchIgnoredName(e.GetLabel()) { + printer.Debugf("Ignoring record %s %s due to IGNORE_NAME\n", e.GetLabel(), e.Type) + } else if d.matchIgnoredTarget(e.GetTargetField(), e.Type) { + printer.Debugf("Ignoring record %s %s due to IGNORE_TARGET\n", e.GetLabel(), e.Type) } else { k := e.Key() existingByNameAndType[k] = append(existingByNameAndType[k], e) } } for _, dr := range desired { - if d.matchIgnored(dr.GetLabel()) { - panic(fmt.Sprintf("Trying to update/add IGNOREd record: %s %s", dr.GetLabel(), dr.Type)) + if d.matchIgnoredName(dr.GetLabel()) { + panic(fmt.Sprintf("Trying to update/add IGNORE_NAMEd record: %s %s", dr.GetLabel(), dr.Type)) + } else if d.matchIgnoredTarget(dr.GetTargetField(), dr.Type) { + panic(fmt.Sprintf("Trying to update/add IGNORE_TARGETd record: %s %s", dr.GetLabel(), dr.Type)) } else { k := dr.Key() desiredByNameAndType[k] = append(desiredByNameAndType[k], dr) @@ -312,13 +320,13 @@ func sortedKeys(m map[string]*models.RecordConfig) []string { return s } -func compileIgnoredLabels(ignoredLabels []string) []glob.Glob { - result := make([]glob.Glob, 0, len(ignoredLabels)) +func compileIgnoredNames(ignoredNames []string) []glob.Glob { + result := make([]glob.Glob, 0, len(ignoredNames)) - for _, tst := range ignoredLabels { + for _, tst := range ignoredNames { g, err := glob.Compile(tst, '.') if err != nil { - panic(fmt.Sprintf("Failed to compile IGNORE pattern %q: %v", tst, err)) + panic(fmt.Sprintf("Failed to compile IGNORE_NAME pattern %q: %v", tst, err)) } result = append(result, g) @@ -327,11 +335,46 @@ func compileIgnoredLabels(ignoredLabels []string) []glob.Glob { return result } -func (d *differ) matchIgnored(name string) bool { - for _, tst := range d.compiledIgnoredLabels { +func compileIgnoredTargets(ignoredTargets []*models.IgnoreTarget) []glob.Glob { + result := make([]glob.Glob, 0, len(ignoredTargets)) + + for _, tst := range ignoredTargets { + fmt.Sprintf("rType for IGNORE_TARGET %v", tst.Type) + + if tst.Type != "CNAME" { + panic(fmt.Sprintf("Invalid rType for IGNORE_TARGET %v", tst.Type)) + } + + g, err := glob.Compile(tst.Pattern, '.') + if err != nil { + panic(fmt.Sprintf("Failed to compile IGNORE_TARGET pattern %q: %v", tst, err)) + } + + result = append(result, g) + } + + return result +} + +func (d *differ) matchIgnoredName(name string) bool { + for _, tst := range d.compiledIgnoredNames { if tst.Match(name) { return true } } return false } + +func (d *differ) matchIgnoredTarget(target string, rType string) bool { + if rType != "CNAME" { + return false + } + + for _, tst := range d.compiledIgnoredTargets { + if tst.Match(target) { + return true + } + } + + return false +} diff --git a/pkg/diff/diff_test.go b/pkg/diff/diff_test.go index 28e4a9b41..1a714426c 100644 --- a/pkg/diff/diff_test.go +++ b/pkg/diff/diff_test.go @@ -158,15 +158,16 @@ func checkLengths(t *testing.T, existing, desired []*models.RecordConfig, unCoun } func checkLengthsWithKeepUnknown(t *testing.T, existing, desired []*models.RecordConfig, unCount, createCount, delCount, modCount int, keepUnknown bool, valFuncs ...func(*models.RecordConfig) map[string]string) (un, cre, del, mod Changeset) { - return checkLengthsFull(t, existing, desired, unCount, createCount, delCount, modCount, keepUnknown, []string{}, valFuncs...) + return checkLengthsFull(t, existing, desired, unCount, createCount, delCount, modCount, keepUnknown, []string{}, nil, valFuncs...) } -func checkLengthsFull(t *testing.T, existing, desired []*models.RecordConfig, unCount, createCount, delCount, modCount int, keepUnknown bool, ignoredRecords []string, valFuncs ...func(*models.RecordConfig) map[string]string) (un, cre, del, mod Changeset) { +func checkLengthsFull(t *testing.T, existing, desired []*models.RecordConfig, unCount, createCount, delCount, modCount int, keepUnknown bool, ignoredRecords []string, ignoredTargets []*models.IgnoreTarget, valFuncs ...func(*models.RecordConfig) map[string]string) (un, cre, del, mod Changeset) { dc := &models.DomainConfig{ - Name: "example.com", - Records: desired, - KeepUnknown: keepUnknown, - IgnoredLabels: ignoredRecords, + Name: "example.com", + Records: desired, + KeepUnknown: keepUnknown, + IgnoredNames: ignoredRecords, + IgnoredTargets: ignoredTargets, } d := New(dc, valFuncs...) un, cre, del, mod = d.IncrementalDiff(existing) @@ -209,7 +210,7 @@ func TestIgnoredRecords(t *testing.T) { desired := []*models.RecordConfig{ myRecord("www3 MX 1 2.2.2.2"), } - checkLengthsFull(t, existing, desired, 0, 0, 0, 1, false, []string{"www1", "www2"}) + checkLengthsFull(t, existing, desired, 0, 0, 0, 1, false, []string{"www1", "www2"}, nil) } func TestModifyingIgnoredRecords(t *testing.T) { @@ -228,10 +229,10 @@ func TestModifyingIgnoredRecords(t *testing.T) { } }() - checkLengthsFull(t, existing, desired, 0, 0, 0, 1, false, []string{"www1", "www2"}) + checkLengthsFull(t, existing, desired, 0, 0, 0, 1, false, []string{"www1", "www2"}, nil) } -func TestGlobIgnoredRecords(t *testing.T) { +func TestGlobIgnoredName(t *testing.T) { existing := []*models.RecordConfig{ myRecord("www1 MX 1 1.1.1.1"), myRecord("foo.www2 MX 1 1.1.1.1"), @@ -241,10 +242,10 @@ func TestGlobIgnoredRecords(t *testing.T) { desired := []*models.RecordConfig{ myRecord("www4 MX 1 2.2.2.2"), } - checkLengthsFull(t, existing, desired, 0, 0, 0, 1, false, []string{"www1", "*.www2", "**.www3"}) + checkLengthsFull(t, existing, desired, 0, 0, 0, 1, false, []string{"www1", "*.www2", "**.www3"}, nil) } -func TestInvalidGlobIgnoredRecord(t *testing.T) { +func TestInvalidGlobIgnoredName(t *testing.T) { existing := []*models.RecordConfig{ myRecord("www1 MX 1 1.1.1.1"), myRecord("www2 MX 1 1.1.1.1"), @@ -256,11 +257,64 @@ func TestInvalidGlobIgnoredRecord(t *testing.T) { defer func() { if r := recover(); r == nil { - t.Errorf("should panic: invalid glob pattern for IGNORE") + t.Errorf("should panic: invalid glob pattern for IGNORE_NAME") } }() - checkLengthsFull(t, existing, desired, 0, 1, 0, 0, false, []string{"www1", "www2", "[.www3"}) + checkLengthsFull(t, existing, desired, 0, 1, 0, 0, false, []string{"www1", "www2", "[.www3"}, nil) +} + +func TestGlobIgnoredTarget(t *testing.T) { + existing := []*models.RecordConfig{ + myRecord("www1 CNAME 1 ignoreme.com"), + myRecord("foo.www2 MX 1 1.1.1.2"), + myRecord("foo.bar.www3 MX 1 1.1.1.1"), + myRecord("www4 MX 1 1.1.1.1"), + } + desired := []*models.RecordConfig{ + myRecord("foo.www2 MX 1 1.1.1.2"), + myRecord("foo.bar.www3 MX 1 1.1.1.1"), + myRecord("www4 MX 1 2.2.2.2"), + } + checkLengthsFull(t, existing, desired, 2, 0, 0, 1, false, nil, []*models.IgnoreTarget{{Pattern: "ignoreme.com", Type: "CNAME"}}) +} + +func TestInvalidGlobIgnoredTarget(t *testing.T) { + existing := []*models.RecordConfig{ + myRecord("www1 MX 1 1.1.1.1"), + myRecord("www2 MX 1 1.1.1.1"), + myRecord("www3 MX 1 1.1.1.1"), + } + desired := []*models.RecordConfig{ + myRecord("www4 MX 1 2.2.2.2"), + } + + defer func() { + if r := recover(); r == nil { + t.Errorf("should panic: invalid glob pattern for IGNORE_TARGET") + } + }() + + checkLengthsFull(t, existing, desired, 0, 1, 0, 0, false, nil, []*models.IgnoreTarget{{Pattern: "[.www3", Type: "CNAME"}} ) +} + +func TestInvalidTypeIgnoredTarget(t *testing.T) { + existing := []*models.RecordConfig{ + myRecord("www1 MX 1 1.1.1.1"), + myRecord("www2 MX 1 1.1.1.1"), + myRecord("www3 MX 1 1.1.1.1"), + } + desired := []*models.RecordConfig{ + myRecord("www4 MX 1 2.2.2.2"), + } + + defer func() { + if r := recover(); r == nil { + t.Errorf("should panic: Invalid rType for IGNORE_TARGET A") + } + }() + + checkLengthsFull(t, existing, desired, 0, 1, 0, 0, false, nil, []*models.IgnoreTarget{{Pattern: "1.1.1.1", Type: "A"}} ) } // from https://github.com/StackExchange/dnscontrol/issues/552 @@ -284,12 +338,12 @@ func TestCaas(t *testing.T) { desired[1].SetTargetCAA(3, "issue", "amazon.com.") desired[2].SetTargetCAA(3, "issuewild", "letsencrypt.org.") - checkLengthsFull(t, existing, desired, 3, 0, 0, 0, false, nil) + checkLengthsFull(t, existing, desired, 3, 0, 0, 0, false, nil, nil) // Make sure it passes with a different ordering. Not ok. desired[2].SetTargetCAA(3, "issue", "letsencrypt.org.") desired[1].SetTargetCAA(3, "issue", "amazon.com.") desired[0].SetTargetCAA(3, "issuewild", "letsencrypt.org.") - checkLengthsFull(t, existing, desired, 3, 0, 0, 0, false, nil) + checkLengthsFull(t, existing, desired, 3, 0, 0, 0, false, nil, nil) } diff --git a/pkg/js/helpers.js b/pkg/js/helpers.js index 25e34bf55..c497c50d3 100644 --- a/pkg/js/helpers.js +++ b/pkg/js/helpers.js @@ -58,7 +58,8 @@ function newDomain(name, registrar) { dnsProviders: {}, defaultTTL: 0, nameservers: [], - ignored_labels: [], + ignored_names: [], + ignored_targets: [], }; } @@ -494,11 +495,25 @@ function format_tt(transform_table) { // IGNORE(name) function IGNORE(name) { + // deprecated, use IGNORE_NAME + return IGNORE_NAME(name); +} + +// IGNORE_NAME(name) +function IGNORE_NAME(name) { return function(d) { - d.ignored_labels.push(name); + d.ignored_names.push(name); }; } +// IGNORE_TARGET(target) +function IGNORE_TARGET(target, rType) { + return function(d) { + d.ignored_targets.push({pattern: target, type: rType}); + }; +} + + // IMPORT_TRANSFORM(translation_table, domain) var IMPORT_TRANSFORM = recordBuilder('IMPORT_TRANSFORM', { args: [['translation_table'], ['domain'], ['ttl', _.isNumber]], diff --git a/pkg/js/parse_tests/019-ignored-records.js b/pkg/js/parse_tests/019-ignored-records.js index 727bc99d8..ad2a0acbe 100644 --- a/pkg/js/parse_tests/019-ignored-records.js +++ b/pkg/js/parse_tests/019-ignored-records.js @@ -1,3 +1,5 @@ D("foo.com", "none" - , IGNORE("testignore") + , IGNORE_NAME("testignore") + , IGNORE_TARGET("testtarget", "CNAME") + , IGNORE("legacyignore") ); diff --git a/pkg/js/parse_tests/019-ignored-records.json b/pkg/js/parse_tests/019-ignored-records.json index a198a89b1..0a3411272 100644 --- a/pkg/js/parse_tests/019-ignored-records.json +++ b/pkg/js/parse_tests/019-ignored-records.json @@ -7,9 +7,13 @@ "registrar": "none", "dnsProviders": {}, "records": [], - "ignored_labels": [ - "testignore" - ] + "ignored_names": [ + "testignore", + "legacyignore" + ], + "ignored_targets": [ + {"pattern":"testtarget","type":"CNAME"} + ], } ] } \ No newline at end of file diff --git a/pkg/js/parse_tests/023-ignored-glob-records.json b/pkg/js/parse_tests/023-ignored-glob-records.json index dde70a06e..ab8972b4b 100644 --- a/pkg/js/parse_tests/023-ignored-glob-records.json +++ b/pkg/js/parse_tests/023-ignored-glob-records.json @@ -7,7 +7,7 @@ "registrar": "none", "dnsProviders": {}, "records": [], - "ignored_labels": [ + "ignored_names": [ "\\*.testignore" ] } diff --git a/pkg/js/static.go b/pkg/js/static.go index 56e04359f..dd8e5d637 100644 --- a/pkg/js/static.go +++ b/pkg/js/static.go @@ -212,117 +212,118 @@ var _escData = map[string]*_escFile{ "/helpers.js": { name: "helpers.js", local: "pkg/js/helpers.js", - size: 25306, + size: 25616, modtime: 0, compressed: ` -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 -hjskFLAWqHsOMBIs/l8AAAD//+WKkZfaYgAA +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 `, }, }