FEATURE: Add rTypes restrictions to IGNORE_NAME (#1808)

Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
Dragos Harabor 2022-11-07 08:27:04 -08:00 committed by GitHub
parent 4b3d8f724e
commit 68516025a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 121 additions and 39 deletions

View file

@ -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")
);
```

2
go.sum
View file

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

View file

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

View file

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

View file

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

View file

@ -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,12 +394,20 @@ 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) {
//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
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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": [
{

View file

@ -8,7 +8,10 @@
"dnsProviders": {},
"records": [],
"ignored_names": [
"\\*.testignore"
{
"pattern": "\\*.testignore",
"types": "*"
}
]
}
]