Repository: StackExchange/dnscontrol
Pull request #806 (2020-08-08 11:43:44)

New feature: Add IGNORE_TARGET
Our zone files have a lot of CNAMEs used for DNS validation with AWS Certificate Manager (https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html). These records are managed by other processes and we would want these left untouched by dnscontrol.

It's not possible to match these by record name without risk of false positives. It is however possible to positively match them based on the target of the record.

I'm proposing an IGNORE_TARGET function to handly this scenario.

tlimoncelli: Hi Mike!  Certainly an interesting feature!   We are getting many requests for ways to deal with specific records being managed by a 3rd party.

I'm not clear how this is different from IGNORE()?  Is it that it accepts globs? 
tlimoncelli: (Ignore my previous reply. I misunderstood the feature.)

Hi Mike! Certainly an interesting feature! We are getting many requests for ways to deal with specific records being managed by a 3rd party.

I'm concerned about adding such a broad feature which could be used in unexpected ways that we'd have to support in the future.

When AWS Certificate Manager looks for a validation record, is it always a CNAME?  Should we restrict this feature to be just for CNAMEs?  Maybe take a parameter indicating which rtype it applies to (and have it be an error to be anything other than CNAME for now)?

I'm concerned that IGNORE() becomes ambiguous if there is also a IGNORE_somethingelse().  We should add IGNORE_LABEL() to be an alias for IGNORE(), and have IGNORE() be deprecated. (or something like that)

Thoughts?
mikenz: I'd certainly support renaming IGNORE to IGNORE_NAME ('name' is the term used in both Cloudflare and R53's interface so I would favor that over 'label'). 
Initially I assumed I would be able to use use IGNORE to match all the ACM validation CNAMES. It wasn't until I tried using it that I realised it only matched on the name. Renaming that function to be more specific seems like a good idea to me.

Passing in the rType and only supporting CNAME, I'll add that to this PR. Although I don't think it'd be too long before someone also wants A records.
mikenz: FYI I'm not a Go dev, I'm expecting a bunch of code changes out of code review
mikenz: I think that's everything addressed from round 1 :-)
tlimoncelli: Looks ready for merge to me.  Let me know if you agree.
mikenz: @tlimoncelli I believe this is ready to merge now. Thanks
tlimoncelli: Thanks!  A lot of AWS users are going to be very happy!

Patch:
diff --git a/docs/_functions/domain/IGNORE.md b/docs/_functions/domain/IGNORE.md
index 864097ea..f0667157 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 00000000..d04f6bed
--- /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 00000000..92070b03
--- /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/models/dns.go b/models/dns.go
index 09fc0728..77df62b6 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 4273cc1e..772fc4fc 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 fc6e29e0..c9f68b48 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/js/helpers.js b/pkg/js/helpers.js
index 25e34bf5..c497c50d 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/static.go b/pkg/js/static.go
index 56e04359..dd8e5d63 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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
 `,
 	},
 }


Test patch:
diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go
index a0f76c86..7cd9a109 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/pkg/diff/diff_test.go b/pkg/diff/diff_test.go
index 28e4a9b4..1a714426 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/parse_tests/019-ignored-records.js b/pkg/js/parse_tests/019-ignored-records.js
index 727bc99d..ad2a0acb 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 a198a89b..0a341127 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 dde70a06..ab8972b4 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"
       ]
     }