Repository: StackExchange/dnscontrol
Pull request #885 (2020-10-07 16:18:31)

Ad8 bdl master

tlimoncelli: To expedite things, I'm going to merge this, but file https://github.com/StackExchange/dnscontrol/issues/886 to record the bug I found.  I'll block any releases until it is fixed.  (I might take a whack at it.)
tlimoncelli: To expedite things, I'm going to merge this, but file https://github.com/StackExchange/dnscontrol/issues/886 to record the bug I found.  I'll block any releases until it is fixed.  (I might take a whack at it.)

CC: @ad8-bdl 

Patch:
diff --git a/docs/_functions/global/D_EXTEND.md b/docs/_functions/global/D_EXTEND.md
index 85ae90c8..ba7f70d8 100644
--- a/docs/_functions/global/D_EXTEND.md
+++ b/docs/_functions/global/D_EXTEND.md
@@ -5,26 +5,83 @@ parameters:
   - modifiers...
 ---
 
-`D_EXTEND` adds records (and metadata) to a domain. The domain must have previously been defined by `D()`. `D_EXTEND()` behaves the same as `D()` in all other ways: The first argument is the domain name. See the documentation of `D` for further details.
+`D_EXTEND` adds records (and metadata) to a domain previously defined
+by `D()`. It can also be used to add subdomain records (and metadata)
+to a previously defined domain.
+
+The first argument is a domain name. If it exactly matches a
+previously defined domain, `D_EXTEND()` behaves the same as `D()`,
+simply adding records as if they had been specified in the original
+`D()`.
+
+If the domain name does not match an existing domain, but could be a
+(non-delegated) subdomain of an existing domain, the new records (and
+metadata) are added with the subdomain part appended to all record
+names (labels), and targets (as appropriate). See the examples below.
+
+Matching the domain name to previously-defined domains is done using a
+`longest match` algorithm.  If `domain.tld` and `sub.domain.tld` are
+defined as separate domains via separate `D()` statements, then
+`D_EXTEND('sub.sub.domain.tld', ...)` would match `sub.domain.tld`,
+not `domain.tld`.
+
+Some operators only act on an apex domain (e.g.
+`CF_REDIRECT` and `CF_TEMP_REDIRECT`). Using them 
+in a `D_EXTEND` subdomain may not be what you expect.
 
 Example:
+
 {% include startExample.html %}
 {% highlight js %}
-D('domain.tld', REG, DnsProvider(DNS),
-  A('@', "127.0.0.1")
-)
-D_EXTEND('domain.tld',
-  A('@', "127.0.0.2")
-)
+D("domain.tld", REG, DnsProvider(DNS),
+  A("@", "127.0.0.1"), // domain.tld
+  A("www", "127.0.0.2"), // www.domain.tld
+  CNAME("a", "b") // a.domain.tld -> b.domain.tld
+);
+D_EXTEND("domain.tld",
+  A("aaa", "127.0.0.3"), // aaa.domain.tld
+  CNAME("c", "d") // c.domain.tld -> d.domain.tld
+);
+D_EXTEND("sub.domain.tld",
+  A("bbb", "127.0.0.4"), // bbb.sub.domain.tld
+  A("ccc", "127.0.0.5"), // ccc.sub.domain.tld
+  CNAME("e", "f") // e.sub.domain.tld -> f.sub.domain.tld
+);
+D_EXTEND("sub.sub.domain.tld",
+  A("ddd", "127.0.0.6"), // ddd.sub.sub.domain.tld
+  CNAME("g", "h") // g.sub.sub.domain.tld -> h.sub.sub.domain.tld
+);
+D_EXTEND("sub.domain.tld",
+  A("@", "127.0.0.7"), // sub.domain.tld
+  CNAME("i", "j") // i.sub.domain.tld -> j.sub.domain.tld
+);
 {%endhighlight%}
 
-This will end up in following modifications:
+This will end up in the following modifications:
 ```
 ******************** Domain: domain.tld
------ Getting nameservers from: registrar
------ DNS Provider: registrar...3 corrections
-#1: CREATE A domain.tld 127.0.0.1 ttl=43200
-#2: CREATE A domain.tld 127.0.0.2 ttl=43200
-#3: REFRESH zone domain.tld
+----- Getting nameservers from: cloudflare
+----- DNS Provider: cloudflare...7 corrections
+#1: CREATE A aaa.domain.tld 127.0.0.3
+#2: CREATE A bbb.sub.domain.tld 127.0.0.4
+#3: CREATE A ccc.sub.domain.tld 127.0.0.5
+#4: CREATE A ddd.sub.sub.domain.tld 127.0.0.6
+#5: CREATE A sub.domain.tld 127.0.0.7
+#6: CREATE A www.domain.tld 127.0.0.2
+#7: CREATE A domain.tld 127.0.0.1
+#8: CREATE CNAME a.domain.tld b.domain.tld.
+#9: CREATE CNAME c.domain.tld d.domain.tld.
+#10: CREATE CNAME e.sub.domain.tld f.sub.domain.tld.
+#11: CREATE CNAME g.sub.sub.domain.tld h.sub.sub.domain.tld.
+#12: CREATE CNAME i.sub.domain.tld j.sub.domain.tld.
 ```
 {% include endExample.html %}
+
+ProTips: `D_EXTEND()` permits you to create very complex and
+sophisticated configurations, but you shouldn't. Be nice to the next
+person that edits the file, who may not be as expert as yourself.
+Enhance readability by putting any `D_EXTEND()` statements immediately
+after the original `D()`, like in above example.  Avoid the temptation
+to obscure the addition of records to existing domains with randomly
+placed `D_EXTEND()` statements. Don't build up a domain using loops of
+`D_EXTEND()` statements. You'll be glad you didn't.
diff --git a/models/record.go b/models/record.go
index ed5d2be3..b7ab5455 100644
--- a/models/record.go
+++ b/models/record.go
@@ -49,7 +49,7 @@ import (
 //        the intended FQDN is "foo.com.foo.com." (which may look odd)
 // NameFQDN:
 //    This is the FQDN version of Name.
-//    It should never have a trailiing ".".
+//    It should never have a trailing ".".
 //    NOTE: Eventually we will unexport Name/NameFQDN. Please start using
 //      the setters (SetLabel/SetLabelFromFQDN) and getters (GetLabel/GetLabelFQDN).
 //      as they will always work.
@@ -59,13 +59,18 @@ import (
 //     fields.
 //   NOTE: Eventually we will unexport Target. Please start using the
 //     setters (SetTarget*) and getters (GetTarget*) as they will always work.
+// SubDomain:
+//    This is the subdomain path, if any, imported from the configuration. If
+//        present at the time of canonicalization it is inserted between the
+//        Name and origin when constructing a canonical (FQDN) target.
 //
 // Idioms:
 //  rec.Label() == "@"   // Is this record at the apex?
 //
 type RecordConfig struct {
-	Type             string            `json:"type"`   // All caps rtype name.
-	Name             string            `json:"name"`   // The short name. See above.
+	Type             string            `json:"type"` // All caps rtype name.
+	Name             string            `json:"name"` // The short name. See above.
+	SubDomain        string            `json:"subdomain,omitempty"`
 	NameFQDN         string            `json:"-"`      // Must end with ".$origin". See above.
 	Target           string            `json:"target"` // If a name, must end with "."
 	TTL              uint32            `json:"ttl,omitempty"`
diff --git a/pkg/js/030-dextenddoc.json b/pkg/js/030-dextenddoc.json
new file mode 100644
index 00000000..c505edf9
--- /dev/null
+++ b/pkg/js/030-dextenddoc.json
@@ -0,0 +1,92 @@
+{
+  "dns_providers": [
+    {
+      "name": "Cloudflare",
+      "type": "CLOUDFLAREAPI"
+    }
+  ],
+  "domains": [
+    {
+      "dnsProviders": {
+        "Cloudflare": -1
+      },
+      "name": "domain.tld",
+      "records": [
+        {
+          "name": "@",
+          "target": "127.0.0.1",
+          "type": "A"
+        },
+        {
+          "name": "www",
+          "target": "127.0.0.2",
+          "type": "A"
+        },
+        {
+          "name": "a",
+          "target": "b",
+          "type": "CNAME"
+        },
+        {
+          "name": "aaa",
+          "target": "127.0.0.3",
+          "type": "A"
+        },
+        {
+          "name": "c",
+          "target": "d",
+          "type": "CNAME"
+        },
+        {
+          "name": "bbb.sub",
+          "subdomain": "sub",
+          "target": "127.0.0.4",
+          "type": "A"
+        },
+        {
+          "name": "ccc.sub",
+          "subdomain": "sub",
+          "target": "127.0.0.5",
+          "type": "A"
+        },
+        {
+          "name": "e.sub",
+          "subdomain": "sub",
+          "target": "f",
+          "type": "CNAME"
+        },
+        {
+          "name": "ddd.sub.sub",
+          "subdomain": "sub.sub",
+          "target": "127.0.0.6",
+          "type": "A"
+        },
+        {
+          "name": "g.sub.sub",
+          "subdomain": "sub.sub",
+          "target": "h",
+          "type": "CNAME"
+        },
+        {
+          "name": "sub",
+          "subdomain": "sub",
+          "target": "127.0.0.7",
+          "type": "A"
+        },
+        {
+          "name": "i.sub",
+          "subdomain": "sub",
+          "target": "j",
+          "type": "CNAME"
+        }
+      ],
+      "registrar": "Third-Party"
+    }
+  ],
+  "registrars": [
+    {
+      "name": "Third-Party",
+      "type": "NONE"
+    }
+  ]
+}
diff --git a/pkg/js/helpers.js b/pkg/js/helpers.js
index 2053bb62..91cb98db 100644
--- a/pkg/js/helpers.js
+++ b/pkg/js/helpers.js
@@ -53,6 +53,7 @@ function NewDnsProvider(name, type, meta) {
 function newDomain(name, registrar) {
     return {
         name: name,
+        subdomain: '',
         registrar: registrar,
         meta: {},
         records: [],
@@ -102,12 +103,13 @@ function D(name, registrar) {
     conf.domain_names.push(name);
 }
 
-// DU(name): Update an already added DNS Domain with D().
+// D_EXTEND(name): Update a DNS Domain already added with D(), or subdomain thereof
 function D_EXTEND(name) {
     var domain = _getDomainObject(name);
     if (domain == null) {
         throw name + ' was not declared yet and therefore cannot be updated. Use D() before.';
     }
+    domain.obj.subdomain = name.substr(0, name.length-domain.obj.name.length - 1);
     for (var i = 0; i < defaultArgs.length; i++) {
         processDargs(defaultArgs[i], domain.obj);
     }
@@ -119,13 +121,19 @@ function D_EXTEND(name) {
 }
 
 // _getDomainObject(name): This is a small helper function to get the domain JS object returned.
+// returns the domain object defined for the given name or subdomain thereof
 function _getDomainObject(name) {
+    domain = null;
+    domain_len = 0;
     for(var i = 0; i < conf.domains.length; i++) {
-        if (conf.domains[i]['name'] == name) {
-            return {'id': i, 'obj': conf.domains[i]};
+        if (name.substr(-conf.domains[i]['name'].length) == conf.domains[i]['name']) {
+            if (conf.domains[i]['name'].length > domain_len) {
+                domain_len = conf.domains[i]['name'].length;
+                domain = {'id': i, 'obj': conf.domains[i]};
+            }
         }
     }
-    return null;
+    return domain;
 }
 
 // DEFAULTS provides a set of default arguments to apply to all future domains.
@@ -660,6 +668,17 @@ function recordBuilder(type, opts) {
             opts.applyModifier(record, modifiers);
             opts.transform(record, parsedArgs, modifiers);
 
+            // Handle D_EXTEND() with subdomains.
+            if (d.subdomain && record.type != 'CF_REDIRECT' &&
+                    record.type != 'CF_TEMP_REDIRECT') {
+                record.subdomain = d.subdomain;
+                if (record.name == '@') {
+                    record.name = d.subdomain;
+                } else {
+                    record.name += '.' + d.subdomain;
+                }
+            }
+
             d.records.push(record);
             return record;
         };
diff --git a/pkg/js/static.go b/pkg/js/static.go
index a099616a..6622ec48 100644
--- a/pkg/js/static.go
+++ b/pkg/js/static.go
@@ -212,124 +212,128 @@ var _escData = map[string]*_escFile{
 	"/helpers.js": {
 		name:    "helpers.js",
 		local:   "pkg/js/helpers.js",
-		size:    26563,
+		size:    27416,
 		modtime: 0,
 		compressed: `
-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
+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
+6diybudx5/8FAAD//7FJb88YawAA
 `,
 	},
 }
diff --git a/pkg/normalize/validate.go b/pkg/normalize/validate.go
index 0707d3e9..c1ad42a6 100644
--- a/pkg/normalize/validate.go
+++ b/pkg/normalize/validate.go
@@ -304,7 +304,11 @@ func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) {
 				// These record types have a target that is a hostname.
 				// We normalize them to a FQDN so there is less variation to handle.  If a
 				// provider API requires a shortname, the provider must do the shortening.
-				rec.SetTarget(dnsutil.AddOrigin(rec.GetTargetField(), domain.Name+"."))
+				origin := domain.Name + "."
+				if len(rec.SubDomain) > 0 {
+					origin = rec.SubDomain + "." + origin
+				}
+				rec.SetTarget(dnsutil.AddOrigin(rec.GetTargetField(), origin))
 			} else if rec.Type == "A" || rec.Type == "AAAA" {
 				rec.SetTarget(net.ParseIP(rec.GetTargetField()).String())
 			} else if rec.Type == "PTR" {
diff --git a/providers/cscglobal/api.go b/providers/cscglobal/api.go
index 9ffc243e..bb589383 100644
--- a/providers/cscglobal/api.go
+++ b/providers/cscglobal/api.go
@@ -158,7 +158,7 @@ func (c *api) get(endpoint string) ([]byte, error) {
 
 	if resp.StatusCode == 400 {
 		// 400, error message is in the body as plain text
-		return nil, fmt.Errorf("CSC Global API error: %w URL: %s%s",
+		return nil, fmt.Errorf("CSC Global API error: %s URL: %s%s",
 			bodyString,
 			req.Host, req.URL.RequestURI())
 	}


Test patch:
diff --git a/pkg/js/parse_tests/001-basic.js b/pkg/js/parse_tests/001-basic.js
index eb2576b2..fd143953 100644
--- a/pkg/js/parse_tests/001-basic.js
+++ b/pkg/js/parse_tests/001-basic.js
@@ -1,6 +1,7 @@
 
 var REG = NewRegistrar("Third-Party","NONE");
-var CF = NewDnsProvider("Cloudflare", "CLOUDFLAREAPI")
+var CF = NewDnsProvider("Cloudflare", "CLOUDFLAREAPI");
+
 D("foo.com",REG,DnsProvider(CF),
     A("@","1.2.3.4")
 );
\ No newline at end of file
diff --git a/pkg/js/parse_tests/001-basic.json b/pkg/js/parse_tests/001-basic.json
index 173814f9..eb09fe2e 100644
--- a/pkg/js/parse_tests/001-basic.json
+++ b/pkg/js/parse_tests/001-basic.json
@@ -27,4 +27,4 @@
             ]
         }
     ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/002-ttl.json b/pkg/js/parse_tests/002-ttl.json
index 709055ba..137e9a34 100644
--- a/pkg/js/parse_tests/002-ttl.json
+++ b/pkg/js/parse_tests/002-ttl.json
@@ -28,4 +28,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/003-meta.json b/pkg/js/parse_tests/003-meta.json
index 3dde27cb..2a36616c 100644
--- a/pkg/js/parse_tests/003-meta.json
+++ b/pkg/js/parse_tests/003-meta.json
@@ -23,4 +23,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/004-ips.json b/pkg/js/parse_tests/004-ips.json
index d61277db..1c43953a 100644
--- a/pkg/js/parse_tests/004-ips.json
+++ b/pkg/js/parse_tests/004-ips.json
@@ -37,4 +37,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/006-transforms.json b/pkg/js/parse_tests/006-transforms.json
index 7d95d009..47be4eb6 100644
--- a/pkg/js/parse_tests/006-transforms.json
+++ b/pkg/js/parse_tests/006-transforms.json
@@ -30,4 +30,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/007-importTransformTTL.json b/pkg/js/parse_tests/007-importTransformTTL.json
index fb04a385..150781e9 100644
--- a/pkg/js/parse_tests/007-importTransformTTL.json
+++ b/pkg/js/parse_tests/007-importTransformTTL.json
@@ -19,4 +19,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/008-import.json b/pkg/js/parse_tests/008-import.json
index 60266965..e743c851 100644
--- a/pkg/js/parse_tests/008-import.json
+++ b/pkg/js/parse_tests/008-import.json
@@ -15,4 +15,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/010-alias.json b/pkg/js/parse_tests/010-alias.json
index ee21ba09..47123e05 100644
--- a/pkg/js/parse_tests/010-alias.json
+++ b/pkg/js/parse_tests/010-alias.json
@@ -15,4 +15,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/011-cfRedirect.json b/pkg/js/parse_tests/011-cfRedirect.json
index 193c4507..b7b05b17 100644
--- a/pkg/js/parse_tests/011-cfRedirect.json
+++ b/pkg/js/parse_tests/011-cfRedirect.json
@@ -20,4 +20,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/012-duration.json b/pkg/js/parse_tests/012-duration.json
index a74ffc4e..5fe47fca 100644
--- a/pkg/js/parse_tests/012-duration.json
+++ b/pkg/js/parse_tests/012-duration.json
@@ -40,4 +40,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/013-mx.json b/pkg/js/parse_tests/013-mx.json
index cfae88d9..8935e93a 100644
--- a/pkg/js/parse_tests/013-mx.json
+++ b/pkg/js/parse_tests/013-mx.json
@@ -16,4 +16,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/014-caa.json b/pkg/js/parse_tests/014-caa.json
index 40800c1e..1559b0d3 100644
--- a/pkg/js/parse_tests/014-caa.json
+++ b/pkg/js/parse_tests/014-caa.json
@@ -42,4 +42,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/016-backslash.json b/pkg/js/parse_tests/016-backslash.json
index 9f3a972b..2b90a7ae 100644
--- a/pkg/js/parse_tests/016-backslash.json
+++ b/pkg/js/parse_tests/016-backslash.json
@@ -19,4 +19,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/018-dkim.json b/pkg/js/parse_tests/018-dkim.json
index 9c5563c8..cbadc47f 100644
--- a/pkg/js/parse_tests/018-dkim.json
+++ b/pkg/js/parse_tests/018-dkim.json
@@ -19,4 +19,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/019-r53-alias.json b/pkg/js/parse_tests/019-r53-alias.json
index a2dee407..8e91cec6 100644
--- a/pkg/js/parse_tests/019-r53-alias.json
+++ b/pkg/js/parse_tests/019-r53-alias.json
@@ -108,4 +108,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/020-complexRequire.json b/pkg/js/parse_tests/020-complexRequire.json
index 50a2a8ef..1e427d5b 100644
--- a/pkg/js/parse_tests/020-complexRequire.json
+++ b/pkg/js/parse_tests/020-complexRequire.json
@@ -35,4 +35,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/021-srv.json b/pkg/js/parse_tests/021-srv.json
index e1700641..3d7c3ffb 100644
--- a/pkg/js/parse_tests/021-srv.json
+++ b/pkg/js/parse_tests/021-srv.json
@@ -48,4 +48,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/024-json-import.json b/pkg/js/parse_tests/024-json-import.json
index 9c04cd72..9e9ea701 100644
--- a/pkg/js/parse_tests/024-json-import.json
+++ b/pkg/js/parse_tests/024-json-import.json
@@ -15,4 +15,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/026-azure-alias.json b/pkg/js/parse_tests/026-azure-alias.json
index 22bed19f..1a7caf8f 100644
--- a/pkg/js/parse_tests/026-azure-alias.json
+++ b/pkg/js/parse_tests/026-azure-alias.json
@@ -34,4 +34,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js/parse_tests/028-dextend.js b/pkg/js/parse_tests/028-dextend.js
new file mode 100644
index 00000000..7504ce12
--- /dev/null
+++ b/pkg/js/parse_tests/028-dextend.js
@@ -0,0 +1,22 @@
+var REG = NewRegistrar("Third-Party", "NONE");
+var CF = NewDnsProvider("Cloudflare", "CLOUDFLAREAPI");
+
+// Zone and subdomain Zone:
+D("foo.com", REG, DnsProvider(CF),
+  A("@", "10.1.1.1"),
+  A("www", "10.2.2.2")
+);
+D("bar.foo.com", REG, DnsProvider(CF),
+  A("@", "10.3.3.3"),
+  A("www", "10.4.4.4")
+);
+
+// Zone that gets extended
+D("foo.edu", REG, DnsProvider(CF),
+  A("@", "10.5.5.5"),
+  A("www", "10.6.6.6")
+);
+D_EXTEND("foo.edu",
+  A("more1", "10.7.7.7"),
+  A("more2", "10.8.8.8")
+);
diff --git a/pkg/js/parse_tests/028-dextend.json b/pkg/js/parse_tests/028-dextend.json
new file mode 100644
index 00000000..edbb43cc
--- /dev/null
+++ b/pkg/js/parse_tests/028-dextend.json
@@ -0,0 +1,83 @@
+{
+  "registrars": [
+    {
+      "name": "Third-Party",
+      "type": "NONE"
+    }
+  ],
+  "dns_providers": [
+    {
+      "name": "Cloudflare",
+      "type": "CLOUDFLAREAPI"
+    }
+  ],
+  "domains": [
+    {
+      "name": "foo.com",
+      "registrar": "Third-Party",
+      "dnsProviders": {
+        "Cloudflare": -1
+      },
+      "records": [
+        {
+          "type": "A",
+          "name": "@",
+          "target": "10.1.1.1"
+        },
+        {
+          "type": "A",
+          "name": "www",
+          "target": "10.2.2.2"
+        }
+      ]
+    },
+    {
+      "name": "bar.foo.com",
+      "registrar": "Third-Party",
+      "dnsProviders": {
+        "Cloudflare": -1
+      },
+      "records": [
+        {
+          "type": "A",
+          "name": "@",
+          "target": "10.3.3.3"
+        },
+        {
+          "type": "A",
+          "name": "www",
+          "target": "10.4.4.4"
+        }
+      ]
+    },
+    {
+      "name": "foo.edu",
+      "registrar": "Third-Party",
+      "dnsProviders": {
+        "Cloudflare": -1
+      },
+      "records": [
+        {
+          "type": "A",
+          "name": "@",
+          "target": "10.5.5.5"
+        },
+        {
+          "type": "A",
+          "name": "www",
+          "target": "10.6.6.6"
+        },
+        {
+          "type": "A",
+          "name": "more1",
+          "target": "10.7.7.7"
+        },
+        {
+          "type": "A",
+          "name": "more2",
+          "target": "10.8.8.8"
+        }
+      ]
+    }
+  ]
+}
diff --git a/pkg/js/parse_tests/029-dextendsub.js b/pkg/js/parse_tests/029-dextendsub.js
new file mode 100644
index 00000000..f4d05de4
--- /dev/null
+++ b/pkg/js/parse_tests/029-dextendsub.js
@@ -0,0 +1,84 @@
+var REG = NewRegistrar("Third-Party", "NONE");
+var CF = NewDnsProvider("Cloudflare", "CLOUDFLAREAPI");
+
+// Zone that gets extended by subdomain
+D("foo.net", REG, DnsProvider(CF),
+  A("@", "10.1.1.1"),
+  A("www", "10.2.2.2")
+);
+D_EXTEND("bar.foo.net",
+  A("@", "10.3.3.3"),
+  A("www", "10.4.4.4")
+);
+
+// Zone and subdomain zone, each get extended.
+D("foo.tld", REG, DnsProvider(CF),
+  A("@", "20.5.5.5"),
+  A("www", "20.6.6.6")
+);
+D("bar.foo.tld", REG, DnsProvider(CF),
+  A("@", "30.7.7.7"),
+  A("www", "30.8.8.8")
+);
+D_EXTEND("bar.foo.tld",
+  A("a", "30.9.9.9")
+);
+D_EXTEND("foo.tld",
+  A("a", "20.10.10.10")
+);
+
+// Zone and subdomain zone, each get extended by a subdomain.
+D("foo.help", REG, DnsProvider(CF),
+  A("@", "40.12.12.12"),
+  A("www", "40.12.12.12")
+);
+D("bar.foo.help", REG, DnsProvider(CF),
+  A("@", "50.13.13.13"),
+  A("www", "50.14.14.14")
+);
+D_EXTEND("zip.bar.foo.help",
+  A("@", "50.15.15.15"),
+  A("www", "50.16.16.16")
+);
+D_EXTEND("morty.foo.help",
+  A("@", "40.17.17.17"),
+  A("www", "40.18.18.18")
+);
+
+// Zone extended by a subdomain and sub-subdomain.
+D("foo.here", REG, DnsProvider(CF),
+  A("@", "60.19.19.19"),
+  A("www", "60.20.20.20")
+);
+D_EXTEND("bar.foo.here",
+  A("@", "60.21.21.21"),
+  A("www", "60.22.22.22")
+);
+D_EXTEND("baz.bar.foo.here",
+  A("@", "60.23.23.23"),
+  A("www", "60.24.24.24")
+);
+
+// Zone extended by a sub-subdomain.
+D_EXTEND("a.long.path.of.sub.domains.foo.net",
+  A("@", "10.25.25.25"),
+  A("www", "10.26.26.26")
+);
+
+// Zone extended by a subdomain, with absolute and relative CNAME targets
+D("example.tld", REG, DnsProvider(CF));
+D_EXTEND("sub.example.tld",
+    CNAME("a", "b"), // a.sub.example.tld -> b.sub.example.tld
+    CNAME("b", "@"), // a.sub.example.tld -> sub.example.tld
+    CNAME("c", "sub.example.tld."), // a.sub.example.tld -> sub.example.tld
+    //CNAME("d", "x.y"), // Error. Contains dot but doesn't end with dot.
+    CNAME("e", "otherdomain.tld.") // a.sub.example.tld -> otherdomain.tld
+    // Also test for MX, NS, ANAME, SRV.
+    // Not sure if PTR needs any special treatment. Haven't thought about it much.
+);
+
+D("foo.com", REG, DnsProvider(CF));
+D_EXTEND("sub.foo.com",
+    CF_REDIRECT("test.foo.com","https://goo.com/$1"),
+    CF_TEMP_REDIRECT("test.foo.com","https://goo.com/$1")
+);
diff --git a/pkg/js/parse_tests/029-dextendsub.json b/pkg/js/parse_tests/029-dextendsub.json
new file mode 100644
index 00000000..5c83649b
--- /dev/null
+++ b/pkg/js/parse_tests/029-dextendsub.json
@@ -0,0 +1,264 @@
+{
+  "registrars": [
+    {
+      "name": "Third-Party",
+      "type": "NONE"
+    }
+  ],
+  "dns_providers": [
+    {
+      "name": "Cloudflare",
+      "type": "CLOUDFLAREAPI"
+    }
+  ],
+  "domains": [
+    {
+      "name": "foo.net",
+      "registrar": "Third-Party",
+      "dnsProviders": {
+        "Cloudflare": -1
+      },
+      "records": [
+        {
+          "type": "A",
+          "name": "@",
+          "target": "10.1.1.1"
+        },
+        {
+          "type": "A",
+          "name": "www",
+          "target": "10.2.2.2"
+        },
+        {
+          "type": "A",
+          "name": "bar",
+          "subdomain": "bar",
+          "target": "10.3.3.3"
+        },
+        {
+          "type": "A",
+          "name": "www.bar",
+          "subdomain": "bar",
+          "target": "10.4.4.4"
+        },
+        {
+          "type": "A",
+          "name": "a.long.path.of.sub.domains",
+          "subdomain": "a.long.path.of.sub.domains",
+          "target": "10.25.25.25"
+        },
+        {
+          "type": "A",
+          "name": "www.a.long.path.of.sub.domains",
+          "subdomain": "a.long.path.of.sub.domains",
+          "target": "10.26.26.26"
+        }
+      ]
+    },
+    {
+      "name": "foo.tld",
+      "registrar": "Third-Party",
+      "dnsProviders": {
+        "Cloudflare": -1
+      },
+      "records": [
+        {
+          "type": "A",
+          "name": "@",
+          "target": "20.5.5.5"
+        },
+        {
+          "type": "A",
+          "name": "www",
+          "target": "20.6.6.6"
+        },
+        {
+          "type": "A",
+          "name": "a",
+          "target": "20.10.10.10"
+        }
+      ]
+    },
+    {
+      "name": "bar.foo.tld",
+      "registrar": "Third-Party",
+      "dnsProviders": {
+        "Cloudflare": -1
+      },
+      "records": [
+        {
+          "type": "A",
+          "name": "@",
+          "target": "30.7.7.7"
+        },
+        {
+          "type": "A",
+          "name": "www",
+          "target": "30.8.8.8"
+        },
+        {
+          "type": "A",
+          "name": "a",
+          "target": "30.9.9.9"
+        }
+      ]
+    },
+    {
+      "name": "foo.help",
+      "registrar": "Third-Party",
+      "dnsProviders": {
+        "Cloudflare": -1
+      },
+      "records": [
+        {
+          "type": "A",
+          "name": "@",
+          "target": "40.12.12.12"
+        },
+        {
+          "type": "A",
+          "name": "www",
+          "target": "40.12.12.12"
+        },
+        {
+          "type": "A",
+          "name": "morty",
+          "subdomain": "morty",
+          "target": "40.17.17.17"
+        },
+        {
+          "type": "A",
+          "name": "www.morty",
+          "subdomain": "morty",
+          "target": "40.18.18.18"
+        }
+      ]
+    },
+    {
+      "name": "bar.foo.help",
+      "registrar": "Third-Party",
+      "dnsProviders": {
+        "Cloudflare": -1
+      },
+      "records": [
+        {
+          "type": "A",
+          "name": "@",
+          "target": "50.13.13.13"
+        },
+        {
+          "type": "A",
+          "name": "www",
+          "target": "50.14.14.14"
+        },
+        {
+          "type": "A",
+          "name": "zip",
+          "subdomain": "zip",
+          "target": "50.15.15.15"
+        },
+        {
+          "type": "A",
+          "name": "www.zip",
+          "subdomain": "zip",
+          "target": "50.16.16.16"
+        }
+      ]
+    },
+    {
+      "name": "foo.here",
+      "registrar": "Third-Party",
+      "dnsProviders": {
+        "Cloudflare": -1
+      },
+      "records": [
+        {
+          "type": "A",
+          "name": "@",
+          "target": "60.19.19.19"
+        },
+        {
+          "type": "A",
+          "name": "www",
+          "target": "60.20.20.20"
+        },
+        {
+          "type": "A",
+          "name": "bar",
+          "subdomain": "bar",
+          "target": "60.21.21.21"
+        },
+        {
+          "type": "A",
+          "name": "www.bar",
+          "subdomain": "bar",
+          "target": "60.22.22.22"
+        },
+        {
+          "type": "A",
+          "name": "baz.bar",
+          "subdomain": "baz.bar",
+          "target": "60.23.23.23"
+        },
+        {
+          "type": "A",
+          "name": "www.baz.bar",
+          "subdomain": "baz.bar",
+          "target": "60.24.24.24"
+        }
+      ]
+    },
+    {
+      "name": "example.tld",
+      "registrar": "Third-Party",
+      "dnsProviders": {
+        "Cloudflare": -1
+      },
+      "records": [
+        {
+          "type": "CNAME",
+          "name": "a.sub",
+          "subdomain": "sub",
+          "target": "b"
+        },
+        {
+          "type": "CNAME",
+          "name": "b.sub",
+          "subdomain": "sub",
+          "target": "@"
+        },
+        {
+          "type": "CNAME",
+          "name": "c.sub",
+          "subdomain": "sub",
+          "target": "sub.example.tld."
+        },
+        {
+          "type": "CNAME",
+          "name": "e.sub",
+          "subdomain": "sub",
+          "target": "otherdomain.tld."
+        }
+      ]
+    },
+    {
+      "name": "foo.com",
+      "registrar": "Third-Party",
+      "dnsProviders": {
+        "Cloudflare": -1
+      },
+      "records": [
+        {
+          "type": "CF_REDIRECT",
+          "name": "@",
+          "target": "test.foo.com,https://goo.com/$1"
+        },
+        {
+          "type": "CF_TEMP_REDIRECT",
+          "name": "@",
+          "target": "test.foo.com,https://goo.com/$1"
+        }
+      ]
+    }
+  ]
+}
diff --git a/pkg/js/parse_tests/030-dextenddoc.js b/pkg/js/parse_tests/030-dextenddoc.js
new file mode 100644
index 00000000..0715a9ab
--- /dev/null
+++ b/pkg/js/parse_tests/030-dextenddoc.js
@@ -0,0 +1,27 @@
+var REG = NewRegistrar("Third-Party", "NONE");
+var DNS = NewDnsProvider("Cloudflare", "CLOUDFLAREAPI");
+
+// The example from docs/_functions/global/D_EXTEND.md
+
+D("domain.tld", REG, DnsProvider(DNS),
+  A("@", "127.0.0.1"), // domain.tld
+  A("www", "127.0.0.2"), // www.domain.tld
+  CNAME("a", "b") // a.domain.tld -> b.domain.tld
+);
+D_EXTEND("domain.tld",
+  A("aaa", "127.0.0.3"), // aaa.domain.tld
+  CNAME("c", "d") // c.domain.tld -> d.domain.tld
+);
+D_EXTEND("sub.domain.tld",
+  A("bbb", "127.0.0.4"), // bbb.sub.domain.tld
+  A("ccc", "127.0.0.5"), // ccc.sub.domain.tld
+  CNAME("e", "f") // e.sub.domain.tld -> f.sub.domain.tld
+);
+D_EXTEND("sub.sub.domain.tld",
+  A("ddd", "127.0.0.6"), // ddd.sub.sub.domain.tld
+  CNAME("g", "h") // g.sub.domain.tld -> h.sub.domain.tld
+);
+D_EXTEND("sub.domain.tld",
+  A("@", "127.0.0.7"), // sub.domain.tld
+  CNAME("i", "j") // i.sub.domain.tld -> j.sub.domain.tld
+);
diff --git a/pkg/js/parse_tests/030-dextenddoc.json b/pkg/js/parse_tests/030-dextenddoc.json
new file mode 100644
index 00000000..03f95924
--- /dev/null
+++ b/pkg/js/parse_tests/030-dextenddoc.json
@@ -0,0 +1,67 @@
+{
+  "registrars":
+  [
+    {"name":"Third-Party","type":"NONE"}
+  ],
+  "dns_providers":[{"name":"Cloudflare","type":"CLOUDFLAREAPI"}],
+  "domains":
+  [
+    {
+      "name":"domain.tld",
+      "registrar":"Third-Party",
+      "dnsProviders": {"Cloudflare":-1},
+      "records":
+      [
+        {
+          "type":"A",
+          "name":"@",
+          "target":"127.0.0.1"
+        },
+        {
+          "type":"A",
+          "name":"www",
+          "target":"127.0.0.2"
+        },
+        {
+          "type":"CNAME",
+          "name":"a",
+          "target":"b"
+        },
+        {
+          "type":"A",
+          "name":"aaa",
+          "target":"127.0.0.3"
+        },
+        {
+          "type":"CNAME",
+          "name":"c",
+          "target":"d"
+        },
+        {
+          "type":"A",
+          "name":"bbb.sub",
+          "subdomain":"sub",
+          "target":"127.0.0.4"
+        },
+        {
+          "type":"A","name":"ccc.sub","subdomain":"sub","target":"127.0.0.5"
+        },
+        {
+          "type":"CNAME","name":"e.sub","subdomain":"sub","target":"f"
+        },
+        {
+          "type":"A","name":"ddd.sub.sub","subdomain":"sub.sub","target":"127.0.0.6"
+        },
+        {
+          "type":"CNAME","name":"g.sub.sub","subdomain":"sub.sub","target":"h"
+        },
+        {
+          "type":"A","name":"sub","subdomain":"sub","target":"127.0.0.7"
+        },
+        {
+          "type":"CNAME","name":"i.sub","subdomain":"sub","target":"j"
+        }
+      ]
+    }
+  ]
+}
diff --git a/pkg/js/parse_tests/031-dextendnames.js b/pkg/js/parse_tests/031-dextendnames.js
new file mode 100644
index 00000000..731ab7ee
--- /dev/null
+++ b/pkg/js/parse_tests/031-dextendnames.js
@@ -0,0 +1,45 @@
+var REG = NewRegistrar("Third-Party", "NONE");
+var DNS = NewDnsProvider("Cloudflare", "CLOUDFLAREAPI");
+
+// Test the name matching algorithm
+
+D("domain.tld", REG, DnsProvider(DNS),
+  A("@", "127.0.0.1"),
+  A("a", "127.0.0.2"),
+  CNAME("b", "c")
+);
+
+D("sub.domain.tld", REG, DnsProvider(DNS),
+  A("@", "127.0.1.1"),
+  A("aa", "127.0.1.2"),
+  CNAME("bb", "cc")
+);
+
+
+// Should match domain.tld
+D_EXTEND("domain.tld",
+  A("@", "127.0.0.3"),
+  A("d", "127.0.0.4"), 
+  CNAME("e", "f") 
+);
+
+// Should match domain.tld
+D_EXTEND("ub.domain.tld",
+  A("@", "127.0.0.5"), 
+  A("g", "127.0.0.6"), 
+  CNAME("h", "i") 
+);
+
+// Should match sub.domain.tld
+D_EXTEND("sub.domain.tld",
+  A("@", "127.0.1.3"), 
+  A("dd", "127.0.1.4"), 
+  CNAME("ee", "ff") 
+);
+
+// Should match domain.tld
+D_EXTEND("ssub.domain.tld",
+  A("@", "127.0.0.7"), 
+  A("j", "127.0.0.8"), 
+  CNAME("k", "l") 
+);
diff --git a/pkg/js/parse_tests/031-dextendnames.json b/pkg/js/parse_tests/031-dextendnames.json
new file mode 100644
index 00000000..a4c90ceb
--- /dev/null
+++ b/pkg/js/parse_tests/031-dextendnames.json
@@ -0,0 +1,46 @@
+{
+  "registrars":
+  [
+    {"name":"Third-Party","type":"NONE"}
+  ],
+  "dns_providers":
+  [
+    {"name":"Cloudflare","type":"CLOUDFLAREAPI"}],
+    "domains":
+    [
+      {
+        "name":"domain.tld",
+        "registrar":"Third-Party",
+        "dnsProviders":{"Cloudflare":-1},
+        "records":
+        [
+          {"type":"A","name":"@","target":"127.0.0.1"},
+          {"type":"A","name":"a","target":"127.0.0.2"},
+          {"type":"CNAME","name":"b","target":"c"},
+          {"type":"A","name":"@","target":"127.0.0.3"},
+          {"type":"A","name":"d","target":"127.0.0.4"},
+          {"type":"CNAME","name":"e","target":"f"},
+          {"type":"A","name":"ub","subdomain":"ub","target":"127.0.0.5"},
+          {"type":"A","name":"g.ub","subdomain":"ub","target":"127.0.0.6"},
+          {"type":"CNAME","name":"h.ub","subdomain":"ub","target":"i"},
+          {"type":"A","name":"@","subdomain":"ssub","target":"127.0.0.7"},
+          {"type":"A","name":"j","subdomain":"ssub","target":"127.0.0.8"},
+          {"type":"CNAME","name":"k","subdomain":"ssub","target":"l"}
+        ]
+      },
+      {
+        "name":"sub.domain.tld",
+        "registrar":"Third-Party",
+        "dnsProviders":{"Cloudflare":-1},
+        "records":
+        [
+          {"type":"A","name":"@","target":"127.0.1.1"},
+          {"type":"A","name":"aa","target":"127.0.1.2"},
+          {"type":"CNAME","name":"bb","target":"cc"},
+          {"type":"A","name":"@","target":"127.0.1.3"},
+          {"type":"A","name":"dd","target":"127.0.1.4"},
+          {"type":"CNAME","name":"ee","target":"ff"}
+        ]
+      }
+    ]
+}