1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

New feature: IGNORE_TARGET. Rename INGORE to IGNORE_NAME (#806)

This commit is contained in:
Mike Cochrane
2020-08-19 03:14:34 +12:00
committed by GitHub
parent f88c60a8f3
commit 960dc66bd2
13 changed files with 397 additions and 202 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
D("foo.com", "none"
, IGNORE("testignore")
, IGNORE_NAME("testignore")
, IGNORE_TARGET("testtarget", "CNAME")
, IGNORE("legacyignore")
);

View File

@ -7,9 +7,13 @@
"registrar": "none",
"dnsProviders": {},
"records": [],
"ignored_labels": [
"testignore"
]
"ignored_names": [
"testignore",
"legacyignore"
],
"ignored_targets": [
{"pattern":"testtarget","type":"CNAME"}
],
}
]
}

View File

@ -7,7 +7,7 @@
"registrar": "none",
"dnsProviders": {},
"records": [],
"ignored_labels": [
"ignored_names": [
"\\*.testignore"
]
}

View File

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