Repository: StackExchange/dnscontrol
Pull request #640 (2020-02-21 07:36:29)

Add AUTODNSSEC, implement for DNSimple
There are two models for DNSSEC in DNS management: either dnscontrol manages all the DNSSEC records and has to be invoked regularly for re-signing, or the provider manages DNSSEC automatically and dnscontrol is not involved beyond saying "yeah, do that".

This implements the latter, as a model, and for one provider.

Potentially we could tune/configure options for DNSSEC such as algorithm, but DNSimple don't expose that API so I haven't implemented it.

This minimal model should be something which maps into other providers cleanly.


philpennock: NB: this is cumulative on top of #639 ... review that first, then only the last commit in this PR matters.

This addresses issue #31 with one model of implementing DNSSEC.  It works for me, I have this used for `synadia.net` and `synadia.cloud` via DNSimple now.

@aeden for the DNSimple side of things.

PS: I spent the longest time trying to figure out why I wasn't seeing the DomainConfig field being set, before realizing that I was using the wrong example to crib from, and the helpers.js code needed to reference the JSON name to be able to set the field.  I haven't touched it here because it's not part of my work, but I think that `PURGE` and `NO_PURGE` might be broken: they're accessing `d.KeepUnknown` and should probably be accessing `d.keepunknown` instead, to hit the JSON field?  Unless I missed some case insensitivity somewhere.
philpennock: Hrm, I'm getting a build failure because the validate tool insists that `gofmt` needs to be run on a file which I didn't touch, and where `gofmt` makes no changes.

Also: I found out how to render the docs locally so investigated, and the new `AUTODNSSEC` row in the feature matrix is missing a check for the platform which actually has it, so I need to investigate why that failed (unless you know and it's an easy fix?)
philpennock: Okay, found the problem: missing `CanAutoDNSSEC: providers.Can()` in the provider for dnsimple.  I remember writing that line, must've deleted it by accident.

So does this mean that "missing Can" is not the same as "Cannot"?  How do we mark this feature as "must be explicitly enabled in a given provider, there's no default assumption that the provider probably can" ?

Patch:
diff --git a/build/generate/featureMatrix.go b/build/generate/featureMatrix.go
index 920ff2dd..a34b597a 100644
--- a/build/generate/featureMatrix.go
+++ b/build/generate/featureMatrix.go
@@ -30,6 +30,7 @@ func generateFeatureMatrix() error {
 			{"DNS Provider", "Can manage and serve DNS zones"},
 			{"Registrar", "The provider has registrar capabilities to set nameservers for zones"},
 			{"ALIAS", "Provider supports some kind of ALIAS, ANAME or flattened CNAME record type"},
+			{"AUTODNSSEC", "Provider can automatically manage DNSSEC"},
 			{"CAA", "Provider can manage CAA records"},
 			{"PTR", "Provider supports adding PTR records for reverse lookup zones"},
 			{"NAPTR", "Provider can manage NAPTR records"},
@@ -74,6 +75,7 @@ func generateFeatureMatrix() error {
 		fm.SetSimple("DNS Provider", false, func() bool { return providers.DNSProviderTypes[p] != nil })
 		fm.SetSimple("Registrar", false, func() bool { return providers.RegistrarTypes[p] != nil })
 		setCap("ALIAS", providers.CanUseAlias)
+		setCap("AUTODNSSEC", providers.CanAutoDNSSEC)
 		setCap("CAA", providers.CanUseCAA)
 		setCap("NAPTR", providers.CanUseNAPTR)
 		setCap("PTR", providers.CanUsePTR)
diff --git a/commands/getZones.go b/commands/getZones.go
index 37feb976..90ec0438 100644
--- a/commands/getZones.go
+++ b/commands/getZones.go
@@ -38,10 +38,10 @@ ARGUMENTS:
    zone:     One or more zones (domains) to download; or "all".
 
 FORMATS:
-   --format=dsl      dnsconfig.js format (a decent first draft)
-	 --format=pretty   BIND Zonefile format
-	 --format=tsv      TAB separated value (useful for AWK)
-	 --format=tsvfqdn  tsv with FQDNs (useful for multiple zones)
+   --format=dsl      dnsconfig.js format (not perfect, but a decent first draft)
+   --format=nameonly Just print the zone names
+   --format=pretty   BIND Zonefile format
+   --format=tsv      TAB separated value (useful for AWK)
 
 EXAMPLES:
    dnscontrol get-zones myr53 ROUTE53 example.com
@@ -69,7 +69,7 @@ func (args *GetZoneArgs) flags() []cli.Flag {
 		Name:        "format",
 		Destination: &args.OutputFormat,
 		Value:       "pretty",
-		Usage:       `Output format: dsl pretty tsv tsvfqdn`,
+		Usage:       `Output format: dsl pretty tsv nameonly`,
 	})
 	flags = append(flags, &cli.StringFlag{
 		Name:        "out",
@@ -113,6 +113,23 @@ func GetZone(args GetZoneArgs) error {
 		}
 	}
 
+	// first open output stream and print initial header (if applicable)
+	w := os.Stdout
+	if args.OutputFile != "" {
+		w, err = os.Create(args.OutputFile)
+	}
+	if err != nil {
+		return err
+	}
+	defer w.Close()
+
+	if args.OutputFormat == "nameonly" {
+		for _, zone := range zones {
+			fmt.Fprintln(w, zone)
+		}
+		return nil
+	}
+
 	// actually fetch all of the records
 	zoneRecs := make([]models.Records, len(zones))
 	for i, zone := range zones {
@@ -125,23 +142,15 @@ func GetZone(args GetZoneArgs) error {
 
 	// Write it out:
 
-	// first open output stream and print initial header (if applicable)
-	w := os.Stdout
-	if args.OutputFile != "" {
-		w, err = os.Create(args.OutputFile)
-	}
-	if err != nil {
-		return err
-	}
-	defer w.Close()
 	if args.OutputFormat == "dsl" {
 		fmt.Fprintf(w, `var %s = NewDnsProvider("%s", "%s");`+"\n",
 			args.CredName, args.CredName, args.ProviderName)
 	}
 
+	// now print all zones
 	for i, recs := range zoneRecs {
 		zoneName := zones[i]
-		// now print all zones
+
 		z := prettyzone.PrettySort(recs, zoneName, 0)
 		switch args.OutputFormat {
 		case "pretty":
@@ -159,14 +168,8 @@ func GetZone(args GetZoneArgs) error {
 		case "tsv":
 			for _, rec := range recs {
 				fmt.Fprintf(w,
-					fmt.Sprintf("%s\t%d\tIN\t%s\t%s\n",
-						rec.Name, rec.TTL, rec.Type, rec.GetTargetCombined()))
-			}
-		case "tsvfqdn":
-			for _, rec := range recs {
-				fmt.Fprintf(w,
-					fmt.Sprintf("%s\t%d\tIN\t%s\t%s\n",
-						rec.NameFQDN, rec.TTL, rec.Type, rec.GetTargetCombined()))
+					fmt.Sprintf("%s\t%s\t%d\tIN\t%s\t%s\n",
+						rec.NameFQDN, rec.Name, rec.TTL, rec.Type, rec.GetTargetCombined()))
 			}
 		default:
 			return fmt.Errorf("format %q unknown", args.OutputFile)
diff --git a/docs/_functions/domain/AUTODNSSEC.md b/docs/_functions/domain/AUTODNSSEC.md
new file mode 100644
index 00000000..152f8830
--- /dev/null
+++ b/docs/_functions/domain/AUTODNSSEC.md
@@ -0,0 +1,19 @@
+---
+name: AUTODNSSEC
+---
+
+AUTODNSSEC indicates that the DNS provider can automatically manage
+DNSSEC for a domain and we should ask it to do so.
+
+At this time, AUTODNSSEC takes no parameters.
+There is no ability to tune what the DNS provider sets, no algorithm choice.
+We simply ask that they follow their defaults when enabling a no-fuss DNSSEC
+data model.
+
+{% include startExample.html %}
+{% highlight js %}
+D("example.com", .... ,
+  AUTODNSSEC,
+);
+{%endhighlight%}
+{% include endExample.html %}
diff --git a/docs/_includes/matrix.html b/docs/_includes/matrix.html
index c49f33cb..608ae46f 100644
--- a/docs/_includes/matrix.html
+++ b/docs/_includes/matrix.html
@@ -309,6 +309,35 @@
 			<i class="fa fa-times text-danger" aria-hidden="true"></i>
 		</td>
 		</tr>
+	<tr>
+		<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="Provider can automatically manage DNSSEC">AUTODNSSEC</th>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td class="success">
+			<i class="fa fa-check text-success" aria-hidden="true"></i>
+		</td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		</tr>
 	<tr>
 		<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="Provider can manage CAA records">CAA</th>
 		<td class="danger">
@@ -939,8 +968,8 @@
 		<td class="success">
 			<i class="fa fa-check text-success" aria-hidden="true"></i>
 		</td>
-		<td class="info">
-			<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
+		<td class="success">
+			<i class="fa fa-check text-success" aria-hidden="true"></i>
 		</td>
 		<td class="info">
 			<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
diff --git a/docs/get-zones.md b/docs/get-zones.md
index c7b4825e..42411292 100644
--- a/docs/get-zones.md
+++ b/docs/get-zones.md
@@ -19,7 +19,7 @@ Syntax:
    dnscontrol get-zones [command options] credkey provider zone [...]
 
    --creds value   Provider credentials JSON file (default: "creds.json")
-   --format value  Output format: dsl tsv pretty (default: "pretty")
+   --format value  Output format: pretty dsl tsv nameonly (default: "pretty")
    --out value     Instead of stdout, write to this file
 
 ARGUMENTS:
@@ -27,6 +27,19 @@ ARGUMENTS:
    provider: The name of the provider (second parameter to NewDnsProvider() in dnsconfig.js)
    zone:     One or more zones (domains) to download; or "all".
 
+FORMATS:
+   --format=dsl      dnsconfig.js format (not perfect, but a decent first draft)
+   --format=nameonly Just print the zone names
+   --format=pretty   BIND Zonefile format
+   --format=tsv      TAB separated value (useful for AWK)
+
+When using `tsv`, the columns are:
+   FQDN (the label with the domain)
+   ShortName (just the label, "@" if it is the naked domain)
+   TTL
+   Record Type (A, AAAA, CNAME, etc.)
+   Target and arguments (quoted like in a zonefile)
+
 EXAMPLES:
    dnscontrol get-zones myr53 ROUTE53 example.com
    dnscontrol get-zones gmain GANDI_V5 example.comn other.com
@@ -76,9 +89,9 @@ Once that is done the `get-zone` subcommand should work.
 
 4. Optionally implemement the `ListZones` function
 
-If the `ListZones` function is implemented, the command will activate
-the ability to specify `all` as the zone, at which point all zones
-will be downloaded.
+If the `ListZones` function is implemented, the "all" special case
+will be activated.  In this case, listing a single zone named `all`
+will query the provider for the list of zones.
 
 (Technically what is happening is by implementing the `ListZones`
 function, you are completing the `ZoneLister` interface for that
diff --git a/models/domain.go b/models/domain.go
index d25a9370..d4e3244b 100644
--- a/models/domain.go
+++ b/models/domain.go
@@ -17,6 +17,7 @@ type DomainConfig struct {
 	Nameservers   []*Nameserver     `json:"nameservers,omitempty"`
 	KeepUnknown   bool              `json:"keepunknown,omitempty"`
 	IgnoredLabels []string          `json:"ignored_labels,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/models/record.go b/models/record.go
index 4b5c8c5f..dbfd488a 100644
--- a/models/record.go
+++ b/models/record.go
@@ -28,6 +28,7 @@ import (
 //     TXT
 //   Pseudo-Types:
 //     ALIAS
+//     AUTODNSSEC
 //     CF_REDIRECT
 //     CF_TEMP_REDIRECT
 //     FRAME
diff --git a/pkg/js/helpers.js b/pkg/js/helpers.js
index 6ad8f681..a5c6f139 100644
--- a/pkg/js/helpers.js
+++ b/pkg/js/helpers.js
@@ -413,6 +413,11 @@ function NO_PURGE(d) {
     d.KeepUnknown = true;
 }
 
+// AUTODNSSEC()
+function AUTODNSSEC(d) {
+    d.auto_dnssec = true;
+}
+
 /**
  * @deprecated
  */
diff --git a/pkg/js/static.go b/pkg/js/static.go
index 0a545ec1..2e1fd9b3 100644
--- a/pkg/js/static.go
+++ b/pkg/js/static.go
@@ -212,103 +212,104 @@ var _escData = map[string]*_escFile{
 	"/helpers.js": {
 		name:    "helpers.js",
 		local:   "pkg/js/helpers.js",
-		size:    21786,
+		size:    21856,
 		modtime: 0,
 		compressed: `
-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
+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
+AA==
 `,
 	},
 }
diff --git a/pkg/normalize/validate.go b/pkg/normalize/validate.go
index 977f9a39..b999c37f 100644
--- a/pkg/normalize/validate.go
+++ b/pkg/normalize/validate.go
@@ -436,23 +436,34 @@ func checkProviderCapabilities(dc *models.DomainConfig) error {
 		cap   providers.Capability
 	}{
 		{"ALIAS", providers.CanUseAlias},
+		{"AUTODNSSEC", providers.CanAutoDNSSEC},
+		{"CAA", providers.CanUseCAA},
 		{"PTR", providers.CanUsePTR},
+		{"SSHFP", providers.CanUseSSHFP},
 		{"SRV", providers.CanUseSRV},
-		{"CAA", providers.CanUseCAA},
 		{"TLSA", providers.CanUseTLSA},
 	}
 	for _, ty := range types {
 		hasAny := false
-		for _, r := range dc.Records {
-			if r.Type == ty.rType {
+		switch ty.rType {
+		case "AUTODNSSEC":
+			if dc.AutoDNSSEC {
 				hasAny = true
-				break
 			}
+		default:
+			for _, r := range dc.Records {
+				if r.Type == ty.rType {
+					hasAny = true
+					break
+				}
+			}
+
 		}
 		if !hasAny {
 			continue
 		}
 		for _, provider := range dc.DNSProviderInstances {
+			// fmt.Printf("  (checking if %q can %q for domain %q)\n", provider.ProviderType, ty.rType, dc.Name)
 			if !providers.ProviderHasCapability(provider.ProviderType, ty.cap) {
 				return fmt.Errorf("Domain %s uses %s records, but DNS provider type %s does not support them", dc.Name, ty.rType, provider.ProviderType)
 			}
diff --git a/providers/capabilities.go b/providers/capabilities.go
index 14b325d5..1bb52f62 100644
--- a/providers/capabilities.go
+++ b/providers/capabilities.go
@@ -34,6 +34,10 @@ const (
 	// CanUseTXTMulti indicates the provider can handle TXT records with multiple strings
 	CanUseTXTMulti
 
+	// CanAutoDNSSEC indicates that the provider can automatically handle DNSSEC,
+	// so folks can ask for that.
+	CanAutoDNSSEC
+
 	// CantUseNOPURGE indicates NO_PURGE is broken for this provider. To make it
 	// work would require complex emulation of an incremental update mechanism,
 	// so it is easier to simply mark this feature as not working for this
diff --git a/providers/dnsimple/dnsimpleProvider.go b/providers/dnsimple/dnsimpleProvider.go
index 74cc8c6e..bdec1d8a 100644
--- a/providers/dnsimple/dnsimpleProvider.go
+++ b/providers/dnsimple/dnsimpleProvider.go
@@ -23,6 +23,7 @@ var features = providers.DocumentationNotes{
 	providers.CanUseSSHFP:            providers.Can(),
 	providers.CanUseSRV:              providers.Can(),
 	providers.CanUseTXTMulti:         providers.Can(),
+	providers.CanAutoDNSSEC:          providers.Can(),
 	providers.CanUseTLSA:             providers.Cannot(),
 	providers.DocCreateDomains:       providers.Cannot(),
 	providers.DocDualHost:            providers.Cannot("DNSimple does not allow sufficient control over the apex NS records"),
@@ -85,6 +86,8 @@ func (client *DnsimpleApi) GetZoneRecords(domain string) (models.Records, error)
 		}
 		rec.SetLabel(r.Name, domain)
 		switch rtype := r.Type; rtype {
+		case "DNSKEY", "CDNSKEY", "CDS":
+			continue
 		case "ALIAS", "URL":
 			rec.Type = r.Type
 			rec.SetTarget(r.Content)
@@ -119,6 +122,12 @@ func (c *DnsimpleApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.C
 		return nil, err
 	}
 
+	dnssecFixes, err := c.getDNSSECCorrections(dc)
+	if err != nil {
+		return nil, err
+	}
+	corrections = append(corrections, dnssecFixes...)
+
 	records, err := c.GetZoneRecords(dc.Name)
 	if err != nil {
 		return nil, err
@@ -201,6 +210,34 @@ func (c *DnsimpleApi) GetRegistrarCorrections(dc *models.DomainConfig) ([]*model
 	return corrections, nil
 }
 
+// getDNSSECCorrections returns corrections that update a domain's DNSSEC state.
+func (c *DnsimpleApi) getDNSSECCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
+	enabled, err := c.getDnssec(dc.Name)
+	if err != nil {
+		return nil, err
+	}
+
+	if enabled && !dc.AutoDNSSEC {
+		return []*models.Correction{
+			{
+				Msg: "Disable DNSSEC",
+				F:   func() error { _, err := c.disableDnssec(dc.Name); return err },
+			},
+		}, nil
+	}
+
+	if !enabled && dc.AutoDNSSEC {
+		return []*models.Correction{
+			{
+				Msg: "Enable DNSSEC",
+				F:   func() error { _, err := c.enableDnssec(dc.Name); return err },
+			},
+		}, nil
+	}
+
+	return []*models.Correction{}, nil
+}
+
 // DNSimple calls
 
 func (c *DnsimpleApi) getClient() *dnsimpleapi.Client {
@@ -258,6 +295,69 @@ func (c *DnsimpleApi) getRecords(domainName string) ([]dnsimpleapi.ZoneRecord, e
 	return recs, nil
 }
 
+func (c *DnsimpleApi) getDnssec(domainName string) (bool, error) {
+	var (
+		client    *dnsimpleapi.Client
+		accountID string
+		err       error
+	)
+	client = c.getClient()
+	if accountID, err = c.getAccountID(); err != nil {
+		return false, err
+	}
+
+	dnssecResponse, err := client.Domains.GetDnssec(accountID, domainName)
+	if err != nil {
+		return false, err
+	}
+	if dnssecResponse.Data == nil {
+		return false, nil
+	}
+	return dnssecResponse.Data.Enabled, nil
+}
+
+func (c *DnsimpleApi) enableDnssec(domainName string) (bool, error) {
+	var (
+		client    *dnsimpleapi.Client
+		accountID string
+		err       error
+	)
+	client = c.getClient()
+	if accountID, err = c.getAccountID(); err != nil {
+		return false, err
+	}
+
+	dnssecResponse, err := client.Domains.EnableDnssec(accountID, domainName)
+	if err != nil {
+		return false, err
+	}
+	if dnssecResponse.Data == nil {
+		return false, nil
+	}
+	return dnssecResponse.Data.Enabled, nil
+}
+
+func (c *DnsimpleApi) disableDnssec(domainName string) (bool, error) {
+	var (
+		client    *dnsimpleapi.Client
+		accountID string
+		err       error
+	)
+	client = c.getClient()
+	if accountID, err = c.getAccountID(); err != nil {
+		return false, err
+	}
+
+	dnssecResponse, err := client.Domains.DisableDnssec(accountID, domainName)
+	if err != nil {
+		return false, err
+	}
+	if dnssecResponse.Data == nil {
+		return false, nil
+	}
+	return dnssecResponse.Data.Enabled, nil
+}
+
 // Returns the name server names that should be used. If the domain is registered
 // then this method will return the delegation name servers. If this domain
 // is hosted only, then it will return the default DNSimple name servers.
diff --git a/providers/gcloud/gcloudProvider.go b/providers/gcloud/gcloudProvider.go
index 5520c4c6..769703b7 100644
--- a/providers/gcloud/gcloudProvider.go
+++ b/providers/gcloud/gcloudProvider.go
@@ -108,7 +108,7 @@ func (g *gcloud) loadZoneInfo() error {
 // ListZones returns the list of zones (domains) in this account.
 func (g *gcloud) ListZones() ([]string, error) {
 	var zones []string
-	for i, _ := range g.zones {
+	for i := range g.zones {
 		zones = append(zones, strings.TrimSuffix(i, "."))
 	}
 	return zones, nil
diff --git a/providers/namedotcom/namedotcomProvider.go b/providers/namedotcom/namedotcomProvider.go
index 93ff3efa..960a7a19 100644
--- a/providers/namedotcom/namedotcomProvider.go
+++ b/providers/namedotcom/namedotcomProvider.go
@@ -28,7 +28,7 @@ var features = providers.DocumentationNotes{
 	providers.DocCreateDomains:       providers.Cannot("New domains require registration"),
 	providers.DocDualHost:            providers.Cannot("Apex NS records not editable"),
 	providers.DocOfficiallySupported: providers.Can(),
-	providers.CanGetZones:            providers.Unimplemented(),
+	providers.CanGetZones:            providers.Can(),
 }
 
 func newReg(conf map[string]string) (providers.Registrar, error) {
diff --git a/providers/namedotcom/records.go b/providers/namedotcom/records.go
index ac3528b9..7337d8c6 100644
--- a/providers/namedotcom/records.go
+++ b/providers/namedotcom/records.go
@@ -20,24 +20,28 @@ var defaultNameservers = []*models.Nameserver{
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (client *NameCom) GetZoneRecords(domain string) (models.Records, error) {
-	return nil, fmt.Errorf("not implemented")
-	// This enables the get-zones subcommand.
-	// Implement this by extracting the code from GetDomainCorrections into
-	// a single function.  For most providers this should be relatively easy.
+func (n *NameCom) GetZoneRecords(domain string) (models.Records, error) {
+	records, err := n.getRecords(domain)
+	if err != nil {
+		return nil, err
+	}
+
+	actual := make([]*models.RecordConfig, len(records))
+	for i, r := range records {
+		actual[i] = toRecord(r, domain)
+	}
+
+	return actual, nil
 }
 
 // GetDomainCorrections gathers correctios that would bring n to match dc.
 func (n *NameCom) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
 	dc.Punycode()
-	records, err := n.getRecords(dc.Name)
+
+	actual, err := n.GetZoneRecords(dc.Name)
 	if err != nil {
 		return nil, err
 	}
-	actual := make([]*models.RecordConfig, len(records))
-	for i, r := range records {
-		actual[i] = toRecord(r, dc.Name)
-	}
 
 	for _, rec := range dc.Records {
 		if rec.Type == "ALIAS" {
diff --git a/providers/namedotcom/zones.go b/providers/namedotcom/zones.go
new file mode 100644
index 00000000..22c73465
--- /dev/null
+++ b/providers/namedotcom/zones.go
@@ -0,0 +1,29 @@
+package namedotcom
+
+import (
+	"github.com/namedotcom/go/namecom"
+)
+
+// ListZones returns all the zones in an account
+func (c *NameCom) ListZones() ([]string, error) {
+	var names []string
+	var page int32
+
+	for true {
+		n, err := c.client.ListDomains(&namecom.ListDomainsRequest{Page: page})
+		if err != nil {
+			return nil, err
+		}
+		page = n.NextPage
+
+		for _, j := range n.Domains {
+			names = append(names, j.DomainName)
+		}
+
+		if page == 0 {
+			break
+		}
+	}
+
+	return names, nil
+}


Test patch:
diff --git a/pkg/js/parse_tests/025-autodnssec.js b/pkg/js/parse_tests/025-autodnssec.js
new file mode 100644
index 00000000..3b2cb528
--- /dev/null
+++ b/pkg/js/parse_tests/025-autodnssec.js
@@ -0,0 +1,3 @@
+D("foo.com","none",
+  AUTODNSSEC
+);
diff --git a/pkg/js/parse_tests/025-autodnssec.json b/pkg/js/parse_tests/025-autodnssec.json
new file mode 100644
index 00000000..dd8f5a2d
--- /dev/null
+++ b/pkg/js/parse_tests/025-autodnssec.json
@@ -0,0 +1,13 @@
+{
+  "registrars": [],
+  "dns_providers": [],
+  "domains": [
+    {
+      "name": "foo.com",
+      "registrar": "none",
+      "dnsProviders": {},
+      "records":[],
+      "auto_dnssec": true
+    }
+  ]
+}