From 68516025a507152630452f9a0e7bac3ead495c20 Mon Sep 17 00:00:00 2001 From: Dragos Harabor Date: Mon, 7 Nov 2022 08:27:04 -0800 Subject: [PATCH] FEATURE: Add rTypes restrictions to IGNORE_NAME (#1808) Co-authored-by: Tom Limoncelli --- docs/_functions/domain/IGNORE_NAME.md | 11 +++-- go.sum | 2 - integrationTest/integration_test.go | 6 +-- models/dns.go | 8 +++- models/domain.go | 2 +- pkg/diff/diff.go | 47 ++++++++++++++----- pkg/diff/diff_test.go | 34 +++++++++++--- pkg/js/helpers.js | 11 +++-- pkg/js/js.go | 2 +- pkg/js/parse_tests/005-ignored-records.js | 3 ++ pkg/js/parse_tests/005-ignored-records.json | 27 +++++++++-- .../parse_tests/023-ignored-glob-records.json | 7 ++- 12 files changed, 121 insertions(+), 39 deletions(-) diff --git a/docs/_functions/domain/IGNORE_NAME.md b/docs/_functions/domain/IGNORE_NAME.md index 664c77395..7470ed2f3 100644 --- a/docs/_functions/domain/IGNORE_NAME.md +++ b/docs/_functions/domain/IGNORE_NAME.md @@ -2,6 +2,7 @@ name: IGNORE_NAME parameters: - pattern + - rTypes --- WARNING: The `IGNORE_*` family of functions is risky to use. The code @@ -9,7 +10,7 @@ is brittle and has subtle bugs. Use at your own risk. Do not use these commands with `D_EXTEND()`. `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. +Records of that name will be completely ignored. An optional `rTypes` may be specified as a comma separated list to only ignore records of the given type, e.g. `"A"`, `"A,CNAME"`, `"A, MX, CNAME"`. If `rTypes` is omitted or is `"*"` all record types matching the name will be ignored. `IGNORE_NAME` is like `NO_PURGE` except it acts only on some specific records instead of the whole zone. @@ -17,7 +18,7 @@ Technically `IGNORE_NAME` is a promise that DNSControl will not add, change, or `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. +* 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 `A` record that is managed by a dynamic DNS client, or 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. @@ -25,8 +26,10 @@ In this example, DNSControl will insert/update the "baz.example.com" record but {% capture example %} ```js D("example.com", - `IGNORE_NAME`("foo"), - `IGNORE_NAME`("bar"), + IGNORE_NAME("foo"), // ignore all record types for name foo + IGNORE_NAME("baz", "*"), // ignore all record types for name baz + IGNORE_NAME("bar", "A,MX"), // ignore only A and MX records for name bar + CNAME("bar", "www"), // CNAME is not ignored A("baz", "1.2.3.4") ); ``` diff --git a/go.sum b/go.sum index f74eea8c9..b2b25c819 100644 --- a/go.sum +++ b/go.sum @@ -112,8 +112,6 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= github.com/deepmap/oapi-codegen v1.9.1 h1:yHmEnA7jSTUMQgV+uN02WpZtwHnz2CBW3mZRIxr1vtI= github.com/deepmap/oapi-codegen v1.9.1/go.mod h1:PLqNAhdedP8ttRpBBkzLKU3bp+Fpy+tTgeAMlztR2cw= -github.com/digitalocean/godo v1.87.0 h1:U6jyE7Ga+6NkAa8pnpgrKk0lEU1e3Fc/kWipC9tARds= -github.com/digitalocean/godo v1.87.0/go.mod h1:NRpFznZFvhHjBoqZAaOD3khVzsJ3EibzKqFL4R60dmA= github.com/digitalocean/godo v1.88.0 h1:SAEdw63xOMmzlwCeCWjLH1GcyDPUjbSAR1Bh7VELxzc= github.com/digitalocean/godo v1.88.0/go.mod h1:NRpFznZFvhHjBoqZAaOD3khVzsJ3EibzKqFL4R60dmA= github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c h1:+Zo5Ca9GH0RoeVZQKzFJcTLoAixx5s5Gq3pTIS+n354= diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 0d532a640..8998529f3 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -362,7 +362,7 @@ type TestGroup struct { type TestCase struct { Desc string Records []*models.RecordConfig - IgnoredNames []string + IgnoredNames []*models.IgnoreName IgnoredTargets []*models.IgnoreTarget } @@ -579,11 +579,11 @@ func testgroup(desc string, items ...interface{}) *TestGroup { func tc(desc string, recs ...*models.RecordConfig) *TestCase { var records []*models.RecordConfig - var ignoredNames []string + var ignoredNames []*models.IgnoreName var ignoredTargets []*models.IgnoreTarget for _, r := range recs { if r.Type == "IGNORE_NAME" { - ignoredNames = append(ignoredNames, r.GetLabel()) + ignoredNames = append(ignoredNames, &models.IgnoreName{Pattern: r.GetLabel(), Types: r.GetTargetField()}) } else if r.Type == "IGNORE_TARGET" { rec := &models.IgnoreTarget{ Pattern: r.GetLabel(), diff --git a/models/dns.go b/models/dns.go index 6fa4b5c62..a3dd1cf3a 100644 --- a/models/dns.go +++ b/models/dns.go @@ -119,9 +119,15 @@ func (config *DNSConfig) DomainContainingFQDN(fqdn string) *DomainConfig { return d } +// IgnoreName describes an IGNORE_NAME rule. +type IgnoreName struct { + Pattern string `json:"pattern"` // Glob pattern. + Types string `json:"types"` // All caps rtype names, comma separated. +} + // IgnoreTarget describes an IGNORE_TARGET rule. type IgnoreTarget struct { - Pattern string `json:"pattern"` // Glob pattern + Pattern string `json:"pattern"` // Glob pattern. Type string `json:"type"` // All caps rtype name. } diff --git a/models/domain.go b/models/domain.go index a05937bbc..e9f5d32b4 100644 --- a/models/domain.go +++ b/models/domain.go @@ -19,7 +19,7 @@ type DomainConfig struct { Records Records `json:"records"` Nameservers []*Nameserver `json:"nameservers,omitempty"` KeepUnknown bool `json:"keepunknown,omitempty"` - IgnoredNames []string `json:"ignored_names,omitempty"` + IgnoredNames []*IgnoreName `json:"ignored_names,omitempty"` IgnoredTargets []*IgnoreTarget `json:"ignored_targets,omitempty"` AutoDNSSEC string `json:"auto_dnssec,omitempty"` // "", "on", "off" //DNSSEC bool `json:"dnssec,omitempty"` diff --git a/pkg/diff/diff.go b/pkg/diff/diff.go index f2598d891..cedd10ac9 100644 --- a/pkg/diff/diff.go +++ b/pkg/diff/diff.go @@ -2,6 +2,7 @@ package diff import ( "fmt" + "regexp" "sort" "github.com/StackExchange/dnscontrol/v3/models" @@ -42,11 +43,18 @@ func New(dc *models.DomainConfig, extraValues ...func(*models.RecordConfig) map[ } } +// An ignoredName must match both the name glob and one of the recordTypes in rTypes. If rTypes is empty, any +// record type will match. +type ignoredName struct { + nameGlob glob.Glob + rTypes []string +} + type differ struct { dc *models.DomainConfig extraValues []func(*models.RecordConfig) map[string]string - compiledIgnoredNames []glob.Glob + compiledIgnoredNames []ignoredName compiledIgnoredTargets []glob.Glob } @@ -99,7 +107,7 @@ func (d *differ) IncrementalDiff(existing []*models.RecordConfig) (unchanged, cr // Gather the existing records. Skip over any that should be ignored. for _, e := range existing { //fmt.Printf("********** DEBUG: existing %v %v %v\n", e.GetLabel(), e.Type, e.GetTargetCombined()) - if d.matchIgnoredName(e.GetLabel()) { + if d.matchIgnoredName(e.GetLabel(), e.Type) { //fmt.Printf("Ignoring record %s %s due to IGNORE_NAME\n", e.GetLabel(), e.Type) printer.Debugf("Ignoring record %s %s due to IGNORE_NAME\n", e.GetLabel(), e.Type) } else if d.matchIgnoredTarget(e.GetTargetField(), e.Type) { @@ -115,7 +123,7 @@ func (d *differ) IncrementalDiff(existing []*models.RecordConfig) (unchanged, cr //fmt.Printf("********** DEBUG: desired list %+v\n", desired) for _, dr := range desired { //fmt.Printf("********** DEBUG: desired %v %v %v -- %v %v\n", dr.GetLabel(), dr.Type, dr.GetTargetCombined(), apexException(dr), d.matchIgnoredName(dr.GetLabel())) - if d.matchIgnoredName(dr.GetLabel()) { + if d.matchIgnoredName(dr.GetLabel(), dr.Type) { //if !apexException(dr) || !ignoreNameException(dr) { if (!ignoreNameException(dr)) && (!apexException(dr)) { return nil, nil, nil, nil, fmt.Errorf("trying to update/add IGNORE_NAMEd record: %s %s", dr.GetLabel(), dr.Type) @@ -345,16 +353,23 @@ func sortedKeys(m map[string]*models.RecordConfig) []string { return s } -func compileIgnoredNames(ignoredNames []string) []glob.Glob { - result := make([]glob.Glob, 0, len(ignoredNames)) +var spaceCommaTokenizerRegexp = regexp.MustCompile(`\s*,\s*`) + +func compileIgnoredNames(ignoredNames []*models.IgnoreName) []ignoredName { + result := make([]ignoredName, 0, len(ignoredNames)) for _, tst := range ignoredNames { - g, err := glob.Compile(tst, '.') + g, err := glob.Compile(tst.Pattern, '.') if err != nil { - panic(fmt.Sprintf("Failed to compile IGNORE_NAME pattern %q: %v", tst, err)) + panic(fmt.Sprintf("Failed to compile IGNORE_NAME pattern %q: %v", tst.Pattern, err)) } - result = append(result, g) + t := []string{} + if tst.Types != "" { + t = spaceCommaTokenizerRegexp.Split(tst.Types, -1) + } + + result = append(result, ignoredName{nameGlob: g, rTypes: t}) } return result @@ -379,11 +394,19 @@ func compileIgnoredTargets(ignoredTargets []*models.IgnoreTarget) []glob.Glob { return result } -func (d *differ) matchIgnoredName(name string) bool { +func (d *differ) matchIgnoredName(name string, rType string) bool { for _, tst := range d.compiledIgnoredNames { - //fmt.Printf("********** DEBUG: matchIgnoredName %q %q %v\n", name, tst, tst.Match(name)) - if tst.Match(name) { - return true + //fmt.Printf("********** DEBUG: matchIgnoredName %q %q %v %v\n", name, rType, tst, tst.nameGlob.Match(name)) + if tst.nameGlob.Match(name) { + if tst.rTypes == nil { + return true + } + + for _, rt := range tst.rTypes { + if rt == "*" || rt == rType { + return true + } + } } } return false diff --git a/pkg/diff/diff_test.go b/pkg/diff/diff_test.go index c29e2567f..eb2502c0c 100644 --- a/pkg/diff/diff_test.go +++ b/pkg/diff/diff_test.go @@ -158,10 +158,10 @@ 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{}, nil, valFuncs...) + return checkLengthsFull(t, existing, desired, unCount, createCount, delCount, modCount, keepUnknown, []*models.IgnoreName{}, nil, valFuncs...) } -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) { +func checkLengthsFull(t *testing.T, existing, desired []*models.RecordConfig, unCount, createCount, delCount, modCount int, keepUnknown bool, ignoredRecords []*models.IgnoreName, ignoredTargets []*models.IgnoreTarget, valFuncs ...func(*models.RecordConfig) map[string]string) (un, cre, del, mod Changeset) { dc := &models.DomainConfig{ Name: "example.com", Records: desired, @@ -206,14 +206,23 @@ func TestNoPurge(t *testing.T) { func TestIgnoredRecords(t *testing.T) { existing := []*models.RecordConfig{ + myRecord("www1 A 1 1.1.1.1"), myRecord("www1 MX 1 1.1.1.1"), + myRecord("www2 A 1 1.1.1.1"), + myRecord("www2 CNAME 1 www"), myRecord("www2 MX 1 1.1.1.1"), myRecord("www3 MX 1 1.1.1.1"), } desired := []*models.RecordConfig{ myRecord("www3 MX 1 2.2.2.2"), } - checkLengthsFull(t, existing, desired, 0, 0, 0, 1, false, []string{"www1", "www2"}, nil) + checkLengthsFull(t, existing, desired, 0, 0, 0, 1, false, + []*models.IgnoreName{ + {Pattern: "www1", Types: "*"}, + {Pattern: "www2", Types: "A,MX, CNAME"}, + }, + nil, + ) } func TestModifyingIgnoredRecords(t *testing.T) { @@ -232,7 +241,10 @@ func TestModifyingIgnoredRecords(t *testing.T) { } }() - checkLengthsFull(t, existing, desired, 0, 0, 0, 1, false, []string{"www1", "www2"}, nil) + checkLengthsFull(t, existing, desired, 0, 0, 0, 1, false, + []*models.IgnoreName{{Pattern: "www1", Types: "MX"}, {Pattern: "www2", Types: "*"}}, + nil, + ) } func TestGlobIgnoredName(t *testing.T) { @@ -245,7 +257,14 @@ func TestGlobIgnoredName(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"}, nil) + checkLengthsFull(t, existing, desired, 0, 0, 0, 1, false, + []*models.IgnoreName{ + {Pattern: "www1", Types: "*"}, + {Pattern: "*.www2", Types: "*"}, + {Pattern: "**.www3", Types: "*"}, + }, + nil, + ) } func TestInvalidGlobIgnoredName(t *testing.T) { @@ -264,7 +283,10 @@ func TestInvalidGlobIgnoredName(t *testing.T) { } }() - checkLengthsFull(t, existing, desired, 0, 1, 0, 0, false, []string{"www1", "www2", "[.www3"}, nil) + checkLengthsFull(t, existing, desired, 0, 1, 0, 0, false, + []*models.IgnoreName{{Pattern: "www1"}, {Pattern: "*.www2"}, {Pattern: "[.www3"}}, + nil, + ) } func TestGlobIgnoredTarget(t *testing.T) { diff --git a/pkg/js/helpers.js b/pkg/js/helpers.js index b33a29463..79854214f 100644 --- a/pkg/js/helpers.js +++ b/pkg/js/helpers.js @@ -584,10 +584,13 @@ function IGNORE(name) { return IGNORE_NAME(name); } -// IGNORE_NAME(name) -function IGNORE_NAME(name) { +// IGNORE_NAME(name, rTypes) +function IGNORE_NAME(name, rTypes) { + if (rTypes === undefined) { + rTypes = "*"; + } return function(d) { - d.ignored_names.push(name); + d.ignored_names.push({pattern: name, types: rTypes}); }; } @@ -600,7 +603,7 @@ var IGNORE_NAME_DISABLE_SAFETY_CHECK = { // See https://github.com/StackExchange/dnscontrol/issues/1106 }; -// IGNORE_TARGET(target) +// IGNORE_TARGET(target, rType) function IGNORE_TARGET(target, rType) { return function(d) { d.ignored_targets.push({pattern: target, type: rType}); diff --git a/pkg/js/js.go b/pkg/js/js.go index b0da224b3..676b111ad 100644 --- a/pkg/js/js.go +++ b/pkg/js/js.go @@ -35,7 +35,7 @@ var currentDirectory string // EnableFetch sets whether to enable fetch() in JS execution environment var EnableFetch bool = false -// ExecuteJavascript accepts a javascript string and runs it, returning the resulting dnsConfig. +// ExecuteJavascript accepts a javascript file and runs it, returning the resulting dnsConfig. func ExecuteJavascript(file string, devMode bool, variables map[string]string) (*models.DNSConfig, error) { script, err := os.ReadFile(file) if err != nil { diff --git a/pkg/js/parse_tests/005-ignored-records.js b/pkg/js/parse_tests/005-ignored-records.js index b35c2f810..cba00cc11 100644 --- a/pkg/js/parse_tests/005-ignored-records.js +++ b/pkg/js/parse_tests/005-ignored-records.js @@ -1,5 +1,8 @@ D("foo.com", "none" , IGNORE_NAME("testignore") + , IGNORE_NAME("testignore2", "A") + , IGNORE_NAME("testignore3", "A, CNAME, TXT") + , IGNORE_NAME("testignore4", "*") , IGNORE_TARGET("testtarget", "CNAME") , IGNORE("legacyignore") , IGNORE_NAME("@") diff --git a/pkg/js/parse_tests/005-ignored-records.json b/pkg/js/parse_tests/005-ignored-records.json index 528724b21..52d86f812 100644 --- a/pkg/js/parse_tests/005-ignored-records.json +++ b/pkg/js/parse_tests/005-ignored-records.json @@ -4,9 +4,30 @@ { "dnsProviders": {}, "ignored_names": [ - "testignore", - "legacyignore", - "@" + { + "pattern": "testignore", + "types": "*" + }, + { + "pattern": "testignore2", + "types": "A" + }, + { + "pattern": "testignore3", + "types": "A, CNAME, TXT" + }, + { + "pattern": "testignore4", + "types": "*" + }, + { + "pattern": "legacyignore", + "types": "*" + }, + { + "pattern": "@", + "types": "*" + } ], "ignored_targets": [ { diff --git a/pkg/js/parse_tests/023-ignored-glob-records.json b/pkg/js/parse_tests/023-ignored-glob-records.json index ab8972b4b..c3d62197a 100644 --- a/pkg/js/parse_tests/023-ignored-glob-records.json +++ b/pkg/js/parse_tests/023-ignored-glob-records.json @@ -8,8 +8,11 @@ "dnsProviders": {}, "records": [], "ignored_names": [ - "\\*.testignore" + { + "pattern": "\\*.testignore", + "types": "*" + } ] } ] -} \ No newline at end of file +}