diff --git a/build/generate/featureMatrix.go b/build/generate/featureMatrix.go
index 806195293..920ff2dda 100644
--- a/build/generate/featureMatrix.go
+++ b/build/generate/featureMatrix.go
@@ -42,6 +42,7 @@ func generateFeatureMatrix() error {
{"dual host", "This provider is recommended for use in 'dual hosting' scenarios. Usually this means the provider allows full control over the apex NS records"},
{"create-domains", "This means the provider can automatically create domains that do not currently exist on your account. The 'dnscontrol create-domains' command will initialize any missing domains"},
{"no_purge", "indicates you can use NO_PURGE macro to prevent deleting records not managed by dnscontrol. A few providers that generate the entire zone from scratch have a problem implementing this."},
+ {"get-zones", "indicates the dnscontrol get-zones subcommand is implemented."},
},
}
for _, p := range providerTypes {
@@ -81,6 +82,7 @@ func generateFeatureMatrix() error {
setCap("SSHFP", providers.CanUseSSHFP)
setCap("TLSA", providers.CanUseTLSA)
setCap("TXTMulti", providers.CanUseTXTMulti)
+ setCap("get-zones", providers.CanGetZones)
setDoc("dual host", providers.DocDualHost, false)
setDoc("create-domains", providers.DocCreateDomains, true)
diff --git a/cmd/convertzone/README.md b/cmd/convertzone/README.md
index d6f5b3664..1a945f800 100644
--- a/cmd/convertzone/README.md
+++ b/cmd/convertzone/README.md
@@ -1,3 +1,8 @@
+
+!!! NOTE: This command has been replaced by the "dnscontrol get-zones"
+!!! subcommand. It can do everything convertzone does and more, with
+!!! fewer bugs. This command will be removed from the distribution soon.
+
# convertzone -- Converts a standard DNS zonefile into tsv, pretty, or DSL
This is a crude hack we put together to read a couple common zonefile
@@ -98,4 +103,4 @@ Example: Generate statements for a dnsconfig.js file:
Note: The conversion is not perfect. You'll need to manually clean
it up and insert it into `dnsconfig.js`. More instructions in the
-DNSControl [migration doc]({site.github.url}}/migration).
\ No newline at end of file
+DNSControl [migration doc]({site.github.url}}/migration).
diff --git a/cmd/convertzone/main.go b/cmd/convertzone/main.go
index d2c5984c5..965a1a966 100644
--- a/cmd/convertzone/main.go
+++ b/cmd/convertzone/main.go
@@ -46,7 +46,7 @@ import (
"github.com/miekg/dns"
"github.com/miekg/dns/dnsutil"
- "github.com/StackExchange/dnscontrol/v2/providers/bind"
+ "github.com/StackExchange/dnscontrol/v2/pkg/prettyzone"
"github.com/StackExchange/dnscontrol/v2/providers/octodns/octoyaml"
)
@@ -83,7 +83,7 @@ func main() {
switch *flagOutfmt {
case "pretty":
- bind.WriteZoneFile(os.Stdout, recs, zonename)
+ prettyzone.WriteZoneFileRR(os.Stdout, recs, zonename, 0)
case "dsl":
fmt.Printf(`D("%s", %s, DnsProvider(%s)`, zonename, *flagRegText, *flagProviderText)
rrFormat(zonename, filename, recs, defTTL, true)
@@ -151,11 +151,6 @@ func readOctodns(zonename string, r io.Reader, filename string) []dns.RR {
return l
}
-// pretty outputs the zonefile using the prettyprinter.
-func writePretty(zonename string, recs []dns.RR, defaultTTL uint32) {
- bind.WriteZoneFile(os.Stdout, recs, zonename)
-}
-
// rrFormat outputs the zonefile in either DSL or TSV format.
func rrFormat(zonename string, filename string, recs []dns.RR, defaultTTL uint32, dsl bool) {
zonenamedot := zonename + "."
diff --git a/commands/getZones.go b/commands/getZones.go
new file mode 100644
index 000000000..37feb9764
--- /dev/null
+++ b/commands/getZones.go
@@ -0,0 +1,207 @@
+package commands
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/StackExchange/dnscontrol/v2/models"
+ "github.com/StackExchange/dnscontrol/v2/pkg/prettyzone"
+ "github.com/StackExchange/dnscontrol/v2/providers"
+ "github.com/StackExchange/dnscontrol/v2/providers/config"
+ "github.com/urfave/cli/v2"
+)
+
+var _ = cmd(catUtils, func() *cli.Command {
+ var args GetZoneArgs
+ return &cli.Command{
+ Name: "get-zones",
+ Aliases: []string{"get-zone"},
+ Usage: "gets a zone from a provider (stand-alone)",
+ Action: func(ctx *cli.Context) error {
+ if ctx.NArg() < 3 {
+ return cli.NewExitError("Arguments should be: credskey providername zone(s) (Ex: r53 ROUTE53 example.com)", 1)
+
+ }
+ args.CredName = ctx.Args().Get(0)
+ args.ProviderName = ctx.Args().Get(1)
+ args.ZoneNames = ctx.Args().Slice()[2:]
+ return exit(GetZone(args))
+ },
+ Flags: args.flags(),
+ UsageText: "dnscontrol get-zones [command options] credkey provider zone [...]",
+ Description: `Download a zone from a provider. This is a stand-alone utility.
+
+ARGUMENTS:
+ credkey: The name used in creds.json (first parameter to NewDnsProvider() in dnsconfig.js)
+ 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 (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)
+
+EXAMPLES:
+ dnscontrol get-zones myr53 ROUTE53 example.com
+ dnscontrol get-zones gmain GANDI_V5 example.comn other.com
+ dnscontrol get-zones cfmain CLOUDFLAREAPI all
+ dnscontrol get-zones -format=tsv bind BIND example.com
+ dnscontrol get-zones -format=dsl -out=draft.js glcoud GCLOUD example.com`,
+ }
+}())
+
+// GetZoneArgs args required for the create-domain subcommand.
+type GetZoneArgs struct {
+ GetCredentialsArgs // Args related to creds.json
+ CredName string // key in creds.json
+ ProviderName string // provider name: BIND, GANDI_V5, etc or "-"
+ ZoneNames []string // The zones to get
+ OutputFormat string // Output format
+ OutputFile string // Filename to send output ("" means stdout)
+ DefaultTTL int // default TTL for providers where it is unknown
+}
+
+func (args *GetZoneArgs) flags() []cli.Flag {
+ flags := args.GetCredentialsArgs.flags()
+ flags = append(flags, &cli.StringFlag{
+ Name: "format",
+ Destination: &args.OutputFormat,
+ Value: "pretty",
+ Usage: `Output format: dsl pretty tsv tsvfqdn`,
+ })
+ flags = append(flags, &cli.StringFlag{
+ Name: "out",
+ Destination: &args.OutputFile,
+ Usage: `Instead of stdout, write to this file`,
+ })
+ flags = append(flags, &cli.IntFlag{
+ Name: "ttl",
+ Destination: &args.DefaultTTL,
+ Usage: `Default TTL`,
+ Value: 300,
+ })
+ return flags
+}
+
+// GetZone contains all data/flags needed to run get-zones, independently of CLI.
+func GetZone(args GetZoneArgs) error {
+ var providerConfigs map[string]map[string]string
+ var err error
+
+ // Read it in:
+ providerConfigs, err = config.LoadProviderConfigs(args.CredsFile)
+ if err != nil {
+ return err
+ }
+ provider, err := providers.CreateDNSProvider(args.ProviderName, providerConfigs[args.CredName], nil)
+ if err != nil {
+ return err
+ }
+
+ // decide which zones we need to convert
+ zones := args.ZoneNames
+ if len(args.ZoneNames) == 1 && args.ZoneNames[0] == "all" {
+ lister, ok := provider.(providers.ZoneLister)
+ if !ok {
+ return fmt.Errorf("provider type %s cannot list zones to use the 'all' feature", args.ProviderName)
+ }
+ zones, err = lister.ListZones()
+ if err != nil {
+ return err
+ }
+ }
+
+ // actually fetch all of the records
+ zoneRecs := make([]models.Records, len(zones))
+ for i, zone := range zones {
+ recs, err := provider.GetZoneRecords(zone)
+ if err != nil {
+ return err
+ }
+ zoneRecs[i] = recs
+ }
+
+ // 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)
+ }
+
+ for i, recs := range zoneRecs {
+ zoneName := zones[i]
+ // now print all zones
+ z := prettyzone.PrettySort(recs, zoneName, 0)
+ switch args.OutputFormat {
+ case "pretty":
+ fmt.Fprintf(w, "$ORIGIN %s.\n", zoneName)
+ prettyzone.WriteZoneFileRC(w, z.Records, zoneName)
+ fmt.Fprintln(w)
+ case "dsl":
+
+ fmt.Fprintf(w, `D("%s", REG_CHANGEME,`+"\n", zoneName)
+ fmt.Fprintf(w, "\tDnsProvider(%s)", args.CredName)
+ for _, rec := range recs {
+ fmt.Fprint(w, formatDsl(zoneName, rec, uint32(args.DefaultTTL)))
+ }
+ fmt.Fprint(w, "\n)\n")
+ 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()))
+ }
+ default:
+ return fmt.Errorf("format %q unknown", args.OutputFile)
+ }
+ }
+ return nil
+}
+
+func formatDsl(zonename string, rec *models.RecordConfig, defaultTTL uint32) string {
+
+ target := rec.GetTargetCombined()
+
+ ttlop := ""
+ if rec.TTL != defaultTTL && rec.TTL != 0 {
+ ttlop = fmt.Sprintf(", TTL(%d)", rec.TTL)
+ }
+
+ switch rec.Type { // #rtype_variations
+ case "MX":
+ target = fmt.Sprintf("%d, '%s'", rec.MxPreference, rec.GetTargetField())
+ case "SOA":
+ case "TXT":
+ if len(rec.TxtStrings) == 1 {
+ target = `'` + rec.TxtStrings[0] + `'`
+ } else {
+ target = `['` + strings.Join(rec.TxtStrings, `', '`) + `']`
+ }
+ case "NS":
+ // NS records at the apex should be NAMESERVER() records.
+ if rec.Name == "@" {
+ return fmt.Sprintf(",\n\tNAMESERVER('%s'%s)", target, ttlop)
+ }
+ default:
+ target = "'" + target + "'"
+ }
+
+ return fmt.Sprintf(",\n\t%s('%s', %s%s)", rec.Type, rec.Name, target, ttlop)
+}
diff --git a/docs/_includes/matrix.html b/docs/_includes/matrix.html
index 7a49a6502..4e0f78bc5 100644
--- a/docs/_includes/matrix.html
+++ b/docs/_includes/matrix.html
@@ -902,5 +902,72 @@
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+ |
+
+
+ |
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
diff --git a/docs/adding-new-rtypes.md b/docs/adding-new-rtypes.md
index 559dc09e4..0b6a7f102 100644
--- a/docs/adding-new-rtypes.md
+++ b/docs/adding-new-rtypes.md
@@ -53,7 +53,7 @@ a minimum.
* Add the capability to the file `dnscontrol/providers/capabilities.go` (look for `CanUseAlias` and add
it to the end of the list.)
* Add this feature to the feature matrix in `dnscontrol/build/generate/featureMatrix.go` (Add it to the variable `matrix` then add it later in the file with a `setCap()` statement.
-* Mark the `bind` provider as supporting this record type by updating `dnscontrol/providers/bind/bindProvider.go` (look for `providers.CanUs` and you'll see what to do).
+* Mark the `bind` provider as supporting this record type by updating `dnscontrol/providers/bind/bindProvider.go` (look for `providers.CanUse` and you'll see what to do).
## Step 3: Add a helper function
diff --git a/docs/get-zones.md b/docs/get-zones.md
new file mode 100644
index 000000000..c7b4825e9
--- /dev/null
+++ b/docs/get-zones.md
@@ -0,0 +1,85 @@
+---
+layout: default
+title: Get-Zones subcommand
+---
+
+# get-zones (was "convertzone")
+
+DNSControl has a stand-alone utility that will contact a provider,
+download the records of one or more zones, and output them to a file
+in a variety of formats.
+
+The original purpose of this command is to help convert legacy domains
+to DNScontrol (bootstrapping). Since bootstrapping can not depend on
+`dnsconfig.js`, `get-zones` relies on command line parameters and
+`creds.json` exclusively.
+
+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")
+ --out value Instead of stdout, write to this file
+
+ARGUMENTS:
+ credkey: The name used in creds.json (first parameter to NewDnsProvider() in dnsconfig.js)
+ provider: The name of the provider (second parameter to NewDnsProvider() in dnsconfig.js)
+ zone: One or more zones (domains) to download; or "all".
+
+EXAMPLES:
+ dnscontrol get-zones myr53 ROUTE53 example.com
+ dnscontrol get-zones gmain GANDI_V5 example.comn other.com
+ dnscontrol get-zones cfmain CLOUDFLAREAPI all
+ dnscontrol get-zones -format=tsv bind BIND example.com
+ dnscontrol get-zones -format=dsl -out=draft.js glcoud GCLOUD example.com`,
+
+
+# Example commands
+
+dnscontrol get-zone
+
+# Developer Note
+
+This command is not implemented for all providers.
+
+To add this to a provider:
+
+1. Document the feature
+
+In the `*Provider.go` file, change the setting to implemented.
+
+* OLD: ` providers.CanGetZones: providers.Unimplemented(),`
+* NEW: ` providers.CanGetZones: providers.Can(),`
+
+2. Update the docs
+
+```
+go generate
+```
+
+3. Implement the `GetZoneRecords` function
+
+Find the `GetZoneRecords` function in the `*Provider.go` file.
+
+If currently returns `fmt.Errorf("not implemented")`.
+
+Instead, it should gather the records from the provider
+and return them as a list of RecordConfig structs.
+
+The code to do that already exists in `GetDomainCorrections`.
+You should extract it into its own function (`GetZoneRecords`), rather
+than having it be burried in the middle of `GetDomainCorrections`.
+`GetDomainCorrections` should call `GetZoneRecords`.
+
+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.
+
+(Technically what is happening is by implementing the `ListZones`
+function, you are completing the `ZoneLister` interface for that
+provider.)
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 475215f32..558582005 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -244,9 +244,10 @@ The [Migrating]({{site.github.url}}/migrating) doc has advice
about converting from other systems.
You can manually create the `D()` statements, or you can
generate them automatically using the
-[convertzone](https://github.com/StackExchange/dnscontrol/blob/master/cmd/convertzone/README.md)
-utility that is included in the DNSControl repo (it converts
-BIND-style zone files and OctoDNS-style YAML files to DNSControl's language).
+[dnscontrol get-zones]({{site.github.url}}/get-zones)
+command to import the zone from (most) providers and output it as code
+that can be added to `dnsconfig.js` and used with very little
+modification.
Now you can make change to the domain(s) and run `dnscontrol preview`
diff --git a/docs/migrating.md b/docs/migrating.md
index 934fde2cb..3ead689e6 100644
--- a/docs/migrating.md
+++ b/docs/migrating.md
@@ -42,34 +42,51 @@ For a small domain you can probably create the `D()` statements by
hand, possibly with your text editor's search and replace functions.
However, where's the fun in that?
-The `convertzone` tool can automate 90% of the conversion for you. It
-reads a BIND-style zone file or an OctoDNS-style YAML file and outputs a `D()` statement
-that is usually fairly complete. You may need to touch it up a bit.
+The `dnscontrol get-zones` subcommand
+[documented here]({{site.github.url}}/get-zones)
+can automate 90% of the conversion for you. It
+reads BIND-style zonefiles, or will use a providers API to gather the DNS records. It will then output the records in a variety of formats, including
+the as a `D()` statement
+that is usually fairly complete. You may need to touch it up a bit,
+especially if you use pseudo record types in one provider that are
+not supported by another.
-The convertzone command is in the `cmd/convertzone` subdirectory.
-Build instructions are
-[here](https://github.com/StackExchange/dnscontrol/blob/master/cmd/convertzone/README.md).
+Example 1: Read a BIND zonefile
-If you do not use BIND already, most DNS providers will export your
-existing zone data to a file called the BIND zone file format.
+Most DNS Service Providers have an 'export to zonefile' feature.
-For example, suppose you owned the `foo.com` domain and the zone file
-was in a file called `old/zone.foo.com`. This command will convert the file:
+```
+dnscontrol get-zones -format=dsl bind BIND example.com
+dnscontrol get-zones -format=dsl -out=draft.js bind BIND example.com
+```
- convertzone -out=dsl foo.com first-draft.js
+This will read the file `zones/example.com.zone`. The system is a bit
+inflexible and that must be the filename. You can copy the file to
+that name, or use a symlink.
-If you are converting an OctoDNS file, add the flag `-in=octodns`:
+Add the contents of `draft.js` to `dnsconfig.js` and edit it as needed.
- convertzone -in=octodns -out=dsl foo.com first-draft.js
+Example 2: Read from a provider
-Add the contents of `first-draft.js` to `dnsconfig.js`
+This requires creating a `creds.json` file as described in
+[Getting Started]({{site.github.url}}/getting-started).
+
+Suppose your `creds.json` file has the name `global_aws`
+for the provider `ROUTE53`. Your command would look like this:
+
+```
+dnscontrol get-zones -format=dsl global_aws ROUTE53 example.com
+dnscontrol get-zones -format=dsl -out=draft.js global_aws ROUTE53 example.com
+```
+
+Add the contents of `draft.js` to `dnsconfig.js` and edit it as needed.
Run `dnscontrol preview` and see if it finds any differences.
Edit dnsconfig.js until `dnscontrol preview` shows no errors and
no changes to be made. This means the conversion of your old DNS
data is correct.
-convertzone makes a guess at what to do with NS records.
+`dnscontrol get-zones` makes a guess at what to do with NS records.
An NS record at the apex is turned into a NAMESERVER() call, the
rest are left as NS(). You probably want to check each of them for
correctness.
@@ -81,7 +98,7 @@ Of course, once `dnscontrol preview` runs cleanly, you can do any
kind of cleanups you want. In fact, they should be easier to do
now that you are using DNSControl!
-If convertzone could have done a better job, please
+If `dnscontrol get-zones` could have done a better job, please
[let us know](https://github.com/StackExchange/dnscontrol/issues)!
## Example workflow
@@ -91,7 +108,8 @@ to convert a zone. Lines that start with `#` are comments.
# Note this command uses ">>" to append to dnsconfig.js. Do
# not use ">" as that will erase the existing file.
- convertzone -out=dsl foo.com >dnsconfig.js
+ dnscontrol get-zones -format=dsl -out=draft.js bind BIND foo.com
+ cat >>dnsconfig.js draft.js # Append to dnsconfig.js
#
dnscontrol preview
vim dnsconfig.js
@@ -105,4 +123,4 @@ to convert a zone. Lines that start with `#` are comments.
vim dnsconfig.js
dnscontrol preview
# (repeat until all warnings/errors are resolved)
- dnscontrol push
\ No newline at end of file
+ dnscontrol push
diff --git a/go.sum b/go.sum
index 5313032e5..81ebf5819 100644
--- a/go.sum
+++ b/go.sum
@@ -35,6 +35,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DisposaBoy/JsonConfigReader v0.0.0-20171218180944-5ea4d0ddac55 h1:jbGlDKdzAZ92NzK65hUP98ri0/r50vVVvmZsFP/nIqo=
github.com/DisposaBoy/JsonConfigReader v0.0.0-20171218180944-5ea4d0ddac55/go.mod h1:GCzqZQHydohgVLSIqRKZeTt8IGb1Y4NaFfim3H40uUI=
+github.com/StackExchange/dnscontrol v0.2.8 h1:7jviqDH9cIqRSRpH0UxgmpT7a8CwEhs9mLHBhoYhXo8=
github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d h1:WtAMR0fPCOfK7TPGZ8ZpLLY18HRvL7XJ3xcs0wnREgo=
github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d/go.mod h1:WML6KOYjeU8N6YyusMjj2qRvaPNUEvrQvaxuFcMRFJY=
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
diff --git a/models/dns_test.go b/models/dns_test.go
index 40cfb3d7c..b70c14a13 100644
--- a/models/dns_test.go
+++ b/models/dns_test.go
@@ -4,24 +4,6 @@ import (
"testing"
)
-func TestHasRecordTypeName(t *testing.T) {
- x := &RecordConfig{
- Type: "A",
- Name: "@",
- }
- dc := DomainConfig{}
- if dc.HasRecordTypeName("A", "@") {
- t.Errorf("%v: expected (%v) got (%v)\n", dc.Records, false, true)
- }
- dc.Records = append(dc.Records, x)
- if !dc.HasRecordTypeName("A", "@") {
- t.Errorf("%v: expected (%v) got (%v)\n", dc.Records, true, false)
- }
- if dc.HasRecordTypeName("AAAA", "@") {
- t.Errorf("%v: expected (%v) got (%v)\n", dc.Records, false, true)
- }
-}
-
func TestRR(t *testing.T) {
experiment := RecordConfig{
Type: "A",
@@ -75,10 +57,10 @@ func TestDowncase(t *testing.T) {
&RecordConfig{Type: "MX", Name: "UPPER", Target: "TARGETMX"},
}}
downcase(dc.Records)
- if !dc.HasRecordTypeName("MX", "lower") {
+ if !dc.Records.HasRecordTypeName("MX", "lower") {
t.Errorf("%v: expected (%v) got (%v)\n", dc.Records, false, true)
}
- if !dc.HasRecordTypeName("MX", "upper") {
+ if !dc.Records.HasRecordTypeName("MX", "upper") {
t.Errorf("%v: expected (%v) got (%v)\n", dc.Records, false, true)
}
if dc.Records[0].GetTargetField() != "targetmx" {
diff --git a/models/dnsrr.go b/models/dnsrr.go
new file mode 100644
index 000000000..b9a9e625a
--- /dev/null
+++ b/models/dnsrr.go
@@ -0,0 +1,123 @@
+package models
+
+// methods that make RecordConfig meet the dns.RR interface.
+
+import (
+ "fmt"
+ "log"
+ "strings"
+
+ "github.com/miekg/dns"
+)
+
+//// Header Header returns the header of an resource record.
+//func (rc *RecordConfig) Header() *dns.RR_Header {
+// log.Fatal("Header not implemented")
+// return nil
+//}
+
+// String returns the text representation of the resource record.
+func (rc *RecordConfig) String() string {
+ return rc.GetTargetCombined()
+}
+
+//// copy returns a copy of the RR
+//func (rc *RecordConfig) copy() dns.RR {
+// log.Fatal("Copy not implemented")
+// return dns.TypeToRR[dns.TypeA]()
+//}
+//
+//// len returns the length (in octets) of the uncompressed RR in wire format.
+//func (rc *RecordConfig) len() int {
+// log.Fatal("len not implemented")
+// return 0
+//}
+//
+//// pack packs an RR into wire format.
+//func (rc *RecordConfig) pack([]byte, int, map[string]int, bool) (int, error) {
+// log.Fatal("pack not implemented")
+// return 0, nil
+//}
+
+// Conversions
+
+// RRstoRCs converts []dns.RR to []RecordConfigs.
+func RRstoRCs(rrs []dns.RR, origin string, replaceSerial uint32) Records {
+ rcs := make(Records, 0, len(rrs))
+ var x uint32
+ for _, r := range rrs {
+ var rc RecordConfig
+ //fmt.Printf("CONVERT: %+v\n", r)
+ rc, x = RRtoRC(r, origin, replaceSerial)
+ replaceSerial = x
+ rcs = append(rcs, &rc)
+ }
+ return rcs
+}
+
+// RRtoRC converts dns.RR to RecordConfig
+func RRtoRC(rr dns.RR, origin string, replaceSerial uint32) (RecordConfig, uint32) {
+ // Convert's dns.RR into our native data type (RecordConfig).
+ // Records are translated directly with no changes.
+ // If it is an SOA for the apex domain and
+ // replaceSerial != 0, change the serial to replaceSerial.
+ // WARNING(tlim): This assumes SOAs do not have serial=0.
+ // If one is found, we replace it with serial=1.
+ var oldSerial, newSerial uint32
+ header := rr.Header()
+ rc := new(RecordConfig)
+ rc.Type = dns.TypeToString[header.Rrtype]
+ rc.TTL = header.Ttl
+ rc.Original = rr
+ rc.SetLabelFromFQDN(strings.TrimSuffix(header.Name, "."), origin)
+ switch v := rr.(type) { // #rtype_variations
+ case *dns.A:
+ panicInvalid(rc.SetTarget(v.A.String()))
+ case *dns.AAAA:
+ panicInvalid(rc.SetTarget(v.AAAA.String()))
+ case *dns.CAA:
+ panicInvalid(rc.SetTargetCAA(v.Flag, v.Tag, v.Value))
+ case *dns.CNAME:
+ panicInvalid(rc.SetTarget(v.Target))
+ case *dns.MX:
+ panicInvalid(rc.SetTargetMX(v.Preference, v.Mx))
+ case *dns.NS:
+ panicInvalid(rc.SetTarget(v.Ns))
+ case *dns.PTR:
+ panicInvalid(rc.SetTarget(v.Ptr))
+ case *dns.NAPTR:
+ panicInvalid(rc.SetTargetNAPTR(v.Order, v.Preference, v.Flags, v.Service, v.Regexp, v.Replacement))
+ case *dns.SOA:
+ oldSerial = v.Serial
+ if oldSerial == 0 {
+ // For SOA records, we never return a 0 serial number.
+ oldSerial = 1
+ }
+ newSerial = v.Serial
+ if rc.GetLabel() == "@" && replaceSerial != 0 {
+ newSerial = replaceSerial
+ }
+ panicInvalid(rc.SetTarget(
+ fmt.Sprintf("%v %v %v %v %v %v %v",
+ v.Ns, v.Mbox, newSerial, v.Refresh, v.Retry, v.Expire, v.Minttl),
+ ))
+ // FIXME(tlim): SOA should be handled by splitting out the fields.
+ case *dns.SRV:
+ panicInvalid(rc.SetTargetSRV(v.Priority, v.Weight, v.Port, v.Target))
+ case *dns.SSHFP:
+ panicInvalid(rc.SetTargetSSHFP(v.Algorithm, v.Type, v.FingerPrint))
+ case *dns.TLSA:
+ panicInvalid(rc.SetTargetTLSA(v.Usage, v.Selector, v.MatchingType, v.Certificate))
+ case *dns.TXT:
+ panicInvalid(rc.SetTargetTXTs(v.Txt))
+ default:
+ log.Fatalf("rrToRecord: Unimplemented zone record type=%s (%v)\n", rc.Type, rr)
+ }
+ return *rc, oldSerial
+}
+
+func panicInvalid(err error) {
+ if err != nil {
+ panic(fmt.Errorf("unparsable record received from BIND: %w", err))
+ }
+}
diff --git a/models/domain.go b/models/domain.go
index 633e7e47e..d25a93706 100644
--- a/models/domain.go
+++ b/models/domain.go
@@ -47,16 +47,6 @@ func (dc *DomainConfig) Copy() (*DomainConfig, error) {
return newDc, err
}
-// HasRecordTypeName returns True if there is a record with this rtype and name.
-func (dc *DomainConfig) HasRecordTypeName(rtype, name string) bool {
- for _, r := range dc.Records {
- if r.Type == rtype && r.GetLabel() == name {
- return true
- }
- }
- return false
-}
-
// Filter removes all records that don't match the filter f.
func (dc *DomainConfig) Filter(f func(r *RecordConfig) bool) {
recs := []*RecordConfig{}
diff --git a/models/provider.go b/models/provider.go
index 3a93a23c6..54c07f28e 100644
--- a/models/provider.go
+++ b/models/provider.go
@@ -4,20 +4,9 @@ package models
type DNSProvider interface {
GetNameservers(domain string) ([]*Nameserver, error)
GetDomainCorrections(dc *DomainConfig) ([]*Correction, error)
+ GetZoneRecords(domain string) (Records, error)
}
-// DNSProvider3 will replace DNSProvider in 3.0.
-// If you want to future-proof your code, implement these
-// functions and implement GetDomainCorrections() as in
-// providers/gandi_v5/gandi_v5Provider.go
-//type DNSProvider3 interface {
-// GetNameservers(domain string) ([]*Nameserver, error)
-// GetZoneRecords(domain string) (Records, error)
-// PrepFoundRecords(recs Records) Records
-// PrepDesiredRecords(dc *DomainConfig)
-// GenerateDomainCorrections(dc *DomainConfig, existing Records) ([]*Correction, error)
-//}
-
// Registrar is an interface for Registrar plug-ins.
type Registrar interface {
GetRegistrarCorrections(dc *DomainConfig) ([]*Correction, error)
diff --git a/models/record.go b/models/record.go
index a51e75d93..4b5c8c5f4 100644
--- a/models/record.go
+++ b/models/record.go
@@ -305,6 +305,16 @@ func (rc *RecordConfig) Key() RecordKey {
// Records is a list of *RecordConfig.
type Records []*RecordConfig
+// HasRecordTypeName returns True if there is a record with this rtype and name.
+func (recs Records) HasRecordTypeName(rtype, name string) bool {
+ for _, r := range recs {
+ if r.Type == rtype && r.Name == name {
+ return true
+ }
+ }
+ return false
+}
+
// FQDNMap returns a map of all LabelFQDNs. Useful for making a
// truthtable of labels that exist in Records.
func (r Records) FQDNMap() (m map[string]bool) {
diff --git a/models/record_test.go b/models/record_test.go
index be361e577..1d0fe28af 100644
--- a/models/record_test.go
+++ b/models/record_test.go
@@ -2,6 +2,24 @@ package models
import "testing"
+func TestHasRecordTypeName(t *testing.T) {
+ x := &RecordConfig{
+ Type: "A",
+ Name: "@",
+ }
+ recs := Records{}
+ if recs.HasRecordTypeName("A", "@") {
+ t.Errorf("%v: expected (%v) got (%v)\n", recs, false, true)
+ }
+ recs = append(recs, x)
+ if !recs.HasRecordTypeName("A", "@") {
+ t.Errorf("%v: expected (%v) got (%v)\n", recs, true, false)
+ }
+ if recs.HasRecordTypeName("AAAA", "@") {
+ t.Errorf("%v: expected (%v) got (%v)\n", recs, false, true)
+ }
+}
+
func TestKey(t *testing.T) {
var tests = []struct {
rc RecordConfig
diff --git a/pkg/normalize/validate.go b/pkg/normalize/validate.go
index 60a921c86..977f9a391 100644
--- a/pkg/normalize/validate.go
+++ b/pkg/normalize/validate.go
@@ -207,7 +207,7 @@ func importTransform(srcDomain, dstDomain *models.DomainConfig, transforms []tra
// 4. For As, change the target as described the transforms.
for _, rec := range srcDomain.Records {
- if dstDomain.HasRecordTypeName(rec.Type, rec.GetLabelFQDN()) {
+ if dstDomain.Records.HasRecordTypeName(rec.Type, rec.GetLabelFQDN()) {
continue
}
newRec := func() *models.RecordConfig {
diff --git a/pkg/prettyzone/prettyzone.go b/pkg/prettyzone/prettyzone.go
new file mode 100644
index 000000000..ba62b8093
--- /dev/null
+++ b/pkg/prettyzone/prettyzone.go
@@ -0,0 +1,142 @@
+package prettyzone
+
+// Generate zonefiles.
+// This generates a zonefile that prioritizes beauty over efficiency.
+
+import (
+ "fmt"
+ "io"
+ "sort"
+ "strings"
+
+ "github.com/StackExchange/dnscontrol/v2/models"
+ "github.com/miekg/dns"
+)
+
+// mostCommonTTL returns the most common TTL in a set of records. If there is
+// a tie, the highest TTL is selected. This makes the results consistent.
+// NS records are not included in the analysis because Tom said so.
+func mostCommonTTL(records models.Records) uint32 {
+ // Index the TTLs in use:
+ d := make(map[uint32]int)
+ for _, r := range records {
+ if r.Type != "NS" {
+ d[r.TTL]++
+ }
+ }
+ // Find the largest count:
+ var mc int
+ for _, value := range d {
+ if value > mc {
+ mc = value
+ }
+ }
+ // Find the largest key with that count:
+ var mk uint32
+ for key, value := range d {
+ if value == mc {
+ if key > mk {
+ mk = key
+ }
+ }
+ }
+ return mk
+}
+
+// WriteZoneFileRR is a helper for when you have []dns.RR instead of models.Records
+func WriteZoneFileRR(w io.Writer, records []dns.RR, origin string, serial uint32) error {
+ return WriteZoneFileRC(w, models.RRstoRCs(records, origin, serial), origin)
+}
+
+// WriteZoneFileRC writes a beautifully formatted zone file.
+func WriteZoneFileRC(w io.Writer, records models.Records, origin string) error {
+ // This function prioritizes beauty over output size.
+ // * The zone records are sorted by label, grouped by subzones to
+ // be easy to read and pleasant to the eye.
+ // * Within a label, SOA and NS records are listed first.
+ // * MX records are sorted numericly by preference value.
+ // * SRV records are sorted numericly by port, then priority, then weight.
+ // * A records are sorted by IP address, not lexicographically.
+ // * Repeated labels are removed.
+ // * $TTL is used to eliminate clutter. The most common TTL value is used.
+ // * "@" is used instead of the apex domain name.
+
+ z := PrettySort(records, origin, mostCommonTTL(records))
+
+ return z.generateZoneFileHelper(w)
+}
+
+func PrettySort(records models.Records, origin string, defaultTTL uint32) *zoneGenData {
+ if defaultTTL == 0 {
+ defaultTTL = mostCommonTTL(records)
+ }
+ z := &zoneGenData{
+ Origin: origin + ".",
+ DefaultTTL: defaultTTL,
+ }
+ z.Records = nil
+ for _, r := range records {
+ z.Records = append(z.Records, r)
+ }
+ return z
+}
+
+// generateZoneFileHelper creates a pretty zonefile.
+func (z *zoneGenData) generateZoneFileHelper(w io.Writer) error {
+
+ nameShortPrevious := ""
+
+ sort.Sort(z)
+ fmt.Fprintln(w, "$TTL", z.DefaultTTL)
+ for i, rr := range z.Records {
+
+ // Fake types are commented out.
+ prefix := ""
+ _, ok := dns.StringToType[rr.Type]
+ if !ok {
+ prefix = ";"
+ }
+
+ // name
+ nameShort := rr.Name
+ name := nameShort
+ if (prefix == "") && (i > 0 && nameShort == nameShortPrevious) {
+ name = ""
+ } else {
+ name = nameShort
+ }
+ nameShortPrevious = nameShort
+
+ // ttl
+ ttl := ""
+ if rr.TTL != z.DefaultTTL && rr.TTL != 0 {
+ ttl = fmt.Sprint(rr.TTL)
+ }
+
+ // type
+ typeStr := rr.Type
+
+ // the remaining line
+ target := rr.GetTargetCombined()
+
+ fmt.Fprintf(w, "%s%s\n",
+ prefix, formatLine([]int{10, 5, 2, 5, 0}, []string{name, ttl, "IN", typeStr, target}))
+ }
+ return nil
+}
+
+func formatLine(lengths []int, fields []string) string {
+ c := 0
+ result := ""
+ for i, length := range lengths {
+ item := fields[i]
+ for len(result) < c {
+ result += " "
+ }
+ if item != "" {
+ result += item + " "
+ }
+ c += length + 1
+ }
+ return strings.TrimRight(result, " ")
+}
diff --git a/providers/bind/prettyzone_test.go b/pkg/prettyzone/prettyzone_test.go
similarity index 77%
rename from providers/bind/prettyzone_test.go
rename to pkg/prettyzone/prettyzone_test.go
index fc7f1ec7c..1e576d685 100644
--- a/providers/bind/prettyzone_test.go
+++ b/pkg/prettyzone/prettyzone_test.go
@@ -1,4 +1,4 @@
-package bind
+package prettyzone
import (
"bytes"
@@ -7,6 +7,7 @@ import (
"math/rand"
"testing"
+ "github.com/StackExchange/dnscontrol/v2/models"
"github.com/miekg/dns"
"github.com/miekg/dns/dnsutil"
)
@@ -27,7 +28,7 @@ func parseAndRegen(t *testing.T, buf *bytes.Buffer, expected string) {
}
// Generate it back:
buf2 := &bytes.Buffer{}
- WriteZoneFile(buf2, parsed, "bosun.org.")
+ WriteZoneFileRR(buf2, parsed, "bosun.org", 99)
// Compare:
if buf2.String() != expected {
@@ -47,7 +48,8 @@ func TestMostCommonTtl(t *testing.T) {
// All records are TTL=100
records = nil
records, e = append(records, r1, r1, r1), 100
- g = mostCommonTTL(records)
+ x := models.RRstoRCs(records, "bosun.org", 99)
+ g = mostCommonTTL(x)
if e != g {
t.Fatalf("expected %d; got %d\n", e, g)
}
@@ -55,7 +57,7 @@ func TestMostCommonTtl(t *testing.T) {
// Mixture of TTLs with an obvious winner.
records = nil
records, e = append(records, r1, r2, r2), 200
- g = mostCommonTTL(records)
+ g = mostCommonTTL(models.RRstoRCs(records, "bosun.org", 99))
if e != g {
t.Fatalf("expected %d; got %d\n", e, g)
}
@@ -63,7 +65,7 @@ func TestMostCommonTtl(t *testing.T) {
// 3-way tie. Largest TTL should be used.
records = nil
records, e = append(records, r1, r2, r3), 300
- g = mostCommonTTL(records)
+ g = mostCommonTTL(models.RRstoRCs(records, "bosun.org", 99))
if e != g {
t.Fatalf("expected %d; got %d\n", e, g)
}
@@ -71,7 +73,7 @@ func TestMostCommonTtl(t *testing.T) {
// NS records are ignored.
records = nil
records, e = append(records, r1, r4, r5), 100
- g = mostCommonTTL(records)
+ g = mostCommonTTL(models.RRstoRCs(records, "bosun.org", 99))
if e != g {
t.Fatalf("expected %d; got %d\n", e, g)
}
@@ -85,7 +87,7 @@ func TestWriteZoneFileSimple(t *testing.T) {
r2, _ := dns.NewRR("bosun.org. 300 IN A 192.30.252.154")
r3, _ := dns.NewRR("www.bosun.org. 300 IN CNAME bosun.org.")
buf := &bytes.Buffer{}
- WriteZoneFile(buf, []dns.RR{r1, r2, r3}, "bosun.org.")
+ WriteZoneFileRR(buf, []dns.RR{r1, r2, r3}, "bosun.org", 99)
expected := `$TTL 300
@ IN A 192.30.252.153
IN A 192.30.252.154
@@ -106,7 +108,7 @@ func TestWriteZoneFileSimpleTtl(t *testing.T) {
r3, _ := dns.NewRR("bosun.org. 100 IN A 192.30.252.155")
r4, _ := dns.NewRR("www.bosun.org. 300 IN CNAME bosun.org.")
buf := &bytes.Buffer{}
- WriteZoneFile(buf, []dns.RR{r1, r2, r3, r4}, "bosun.org.")
+ WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4}, "bosun.org", 99)
expected := `$TTL 100
@ IN A 192.30.252.153
IN A 192.30.252.154
@@ -116,26 +118,27 @@ www 300 IN CNAME bosun.org.
if buf.String() != expected {
t.Log(buf.String())
t.Log(expected)
- t.Fatalf("Zone file does not match.")
+ t.Fatalf("Zone file does not match")
}
parseAndRegen(t, buf, expected)
}
func TestWriteZoneFileMx(t *testing.T) {
- // exhibits explicit ttls and long name
- r1, _ := dns.NewRR(`bosun.org. 300 IN TXT "aaa"`)
- r2, _ := dns.NewRR(`bosun.org. 300 IN TXT "bbb"`)
- r2.(*dns.TXT).Txt[0] = `b"bb`
- r3, _ := dns.NewRR("bosun.org. 300 IN MX 1 ASPMX.L.GOOGLE.COM.")
- r4, _ := dns.NewRR("bosun.org. 300 IN MX 5 ALT1.ASPMX.L.GOOGLE.COM.")
- r5, _ := dns.NewRR("bosun.org. 300 IN MX 10 ASPMX3.GOOGLEMAIL.COM.")
- r6, _ := dns.NewRR("bosun.org. 300 IN A 198.252.206.16")
- r7, _ := dns.NewRR("*.bosun.org. 600 IN A 198.252.206.16")
- r8, _ := dns.NewRR(`_domainkey.bosun.org. 300 IN TXT "vvvv"`)
- r9, _ := dns.NewRR(`google._domainkey.bosun.org. 300 IN TXT "\"foo\""`)
+ // sort by priority
+ r1, _ := dns.NewRR("aaa.bosun.org. IN MX 1 aaa.example.com.")
+ r2, _ := dns.NewRR("aaa.bosun.org. IN MX 5 aaa.example.com.")
+ r3, _ := dns.NewRR("aaa.bosun.org. IN MX 10 aaa.example.com.")
+ // same priority? sort by name
+ r4, _ := dns.NewRR("bbb.bosun.org. IN MX 10 ccc.example.com.")
+ r5, _ := dns.NewRR("bbb.bosun.org. IN MX 10 bbb.example.com.")
+ r6, _ := dns.NewRR("bbb.bosun.org. IN MX 10 aaa.example.com.")
+ // a mix
+ r7, _ := dns.NewRR("ccc.bosun.org. IN MX 40 zzz.example.com.")
+ r8, _ := dns.NewRR("ccc.bosun.org. IN MX 40 aaa.example.com.")
+ r9, _ := dns.NewRR("ccc.bosun.org. IN MX 1 ttt.example.com.")
buf := &bytes.Buffer{}
- WriteZoneFile(buf, []dns.RR{r1, r2, r3, r4, r5, r6, r7, r8, r9}, "bosun.org")
+ WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4, r5, r6, r7, r8, r9}, "bosun.org", 99)
if buf.String() != testdataZFMX {
t.Log(buf.String())
t.Log(testdataZFMX)
@@ -144,16 +147,16 @@ func TestWriteZoneFileMx(t *testing.T) {
parseAndRegen(t, buf, testdataZFMX)
}
-var testdataZFMX = `$TTL 300
-@ IN A 198.252.206.16
- IN MX 1 ASPMX.L.GOOGLE.COM.
- IN MX 5 ALT1.ASPMX.L.GOOGLE.COM.
- IN MX 10 ASPMX3.GOOGLEMAIL.COM.
- IN TXT "aaa"
- IN TXT "b\"bb"
-* 600 IN A 198.252.206.16
-_domainkey IN TXT "vvvv"
-google._domainkey IN TXT "\"foo\""
+var testdataZFMX = `$TTL 3600
+aaa IN MX 1 aaa.example.com.
+ IN MX 5 aaa.example.com.
+ IN MX 10 aaa.example.com.
+bbb IN MX 10 aaa.example.com.
+ IN MX 10 bbb.example.com.
+ IN MX 10 ccc.example.com.
+ccc IN MX 1 ttt.example.com.
+ IN MX 40 aaa.example.com.
+ IN MX 40 zzz.example.com.
`
func TestWriteZoneFileSrv(t *testing.T) {
@@ -164,7 +167,7 @@ func TestWriteZoneFileSrv(t *testing.T) {
r4, _ := dns.NewRR(`bosun.org. 300 IN SRV 20 10 5050 foo.com.`)
r5, _ := dns.NewRR(`bosun.org. 300 IN SRV 10 10 5050 foo.com.`)
buf := &bytes.Buffer{}
- WriteZoneFile(buf, []dns.RR{r1, r2, r3, r4, r5}, "bosun.org")
+ WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4, r5}, "bosun.org", 99)
if buf.String() != testdataZFSRV {
t.Log(buf.String())
t.Log(testdataZFSRV)
@@ -187,7 +190,7 @@ func TestWriteZoneFilePtr(t *testing.T) {
r2, _ := dns.NewRR(`bosun.org. 300 IN PTR barney.bosun.org.`)
r3, _ := dns.NewRR(`bosun.org. 300 IN PTR alex.bosun.org.`)
buf := &bytes.Buffer{}
- WriteZoneFile(buf, []dns.RR{r1, r2, r3}, "bosun.org")
+ WriteZoneFileRR(buf, []dns.RR{r1, r2, r3}, "bosun.org", 99)
if buf.String() != testdataZFPTR {
t.Log(buf.String())
t.Log(testdataZFPTR)
@@ -211,7 +214,7 @@ func TestWriteZoneFileCaa(t *testing.T) {
r5, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 iodef "https://example.net"`)
r6, _ := dns.NewRR(`bosun.org. 300 IN CAA 1 iodef "mailto:example.com"`)
buf := &bytes.Buffer{}
- WriteZoneFile(buf, []dns.RR{r1, r2, r3, r4, r5, r6}, "bosun.org")
+ WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4, r5, r6}, "bosun.org", 99)
if buf.String() != testdataZFCAA {
t.Log(buf.String())
t.Log(testdataZFCAA)
@@ -255,7 +258,7 @@ func TestWriteZoneFileEach(t *testing.T) {
d = append(d, mustNewRR(`sub.bosun.org. 300 IN NS bosun.org.`)) // Must be a label with no other records.
d = append(d, mustNewRR(`x.bosun.org. 300 IN CNAME bosun.org.`)) // Must be a label with no other records.
buf := &bytes.Buffer{}
- WriteZoneFile(buf, d, "bosun.org")
+ WriteZoneFileRR(buf, d, "bosun.org", 99)
if buf.String() != testdataZFEach {
t.Log(buf.String())
t.Log(testdataZFEach)
@@ -265,18 +268,49 @@ func TestWriteZoneFileEach(t *testing.T) {
}
var testdataZFEach = `$TTL 300
-4.5. IN PTR y.bosun.org.
@ IN A 1.2.3.4
- IN MX 1 bosun.org.
- IN TXT "my text"
IN AAAA 4500:fe::1
+ IN MX 1 bosun.org.
IN SRV 10 10 9999 foo.com.
+ IN TXT "my text"
IN CAA 0 issue "letsencrypt.org"
+4.5 IN PTR y.bosun.org.
_443._tcp IN TLSA 3 1 1 abcdef0
sub IN NS bosun.org.
x IN CNAME bosun.org.
`
+func TestWriteZoneFileSynth(t *testing.T) {
+ r1, _ := dns.NewRR("bosun.org. 300 IN A 192.30.252.153")
+ r2, _ := dns.NewRR("bosun.org. 300 IN A 192.30.252.154")
+ r3, _ := dns.NewRR("www.bosun.org. 300 IN CNAME bosun.org.")
+ rsynm := &models.RecordConfig{Type: "R53_ALIAS", TTL: 300}
+ rsynm.SetLabel("myalias", "bosun.org")
+ rsynz := &models.RecordConfig{Type: "R53_ALIAS", TTL: 300}
+ rsynz.SetLabel("zalias", "bosun.org")
+
+ recs := models.RRstoRCs([]dns.RR{r1, r2, r3}, "bosun.org", 99)
+ recs = append(recs, rsynm)
+ recs = append(recs, rsynm)
+ recs = append(recs, rsynz)
+
+ buf := &bytes.Buffer{}
+ WriteZoneFileRC(buf, recs, "bosun.org")
+ expected := `$TTL 300
+@ IN A 192.30.252.153
+ IN A 192.30.252.154
+;myalias IN R53_ALIAS type= zone_id=
+;myalias IN R53_ALIAS type= zone_id=
+www IN CNAME bosun.org.
+;zalias IN R53_ALIAS type= zone_id=
+`
+ if buf.String() != expected {
+ t.Log(buf.String())
+ t.Log(expected)
+ t.Fatalf("Zone file does not match.")
+ }
+}
+
// Test sorting
func TestWriteZoneFileOrder(t *testing.T) {
@@ -305,7 +339,7 @@ func TestWriteZoneFileOrder(t *testing.T) {
}
buf := &bytes.Buffer{}
- WriteZoneFile(buf, records, "stackoverflow.com.")
+ WriteZoneFileRR(buf, records, "stackoverflow.com", 99)
// Compare
if buf.String() != testdataOrder {
t.Log("Found:")
@@ -325,7 +359,7 @@ func TestWriteZoneFileOrder(t *testing.T) {
}
// Generate
buf := &bytes.Buffer{}
- WriteZoneFile(buf, records, "stackoverflow.com.")
+ WriteZoneFileRR(buf, records, "stackoverflow.com", 99)
// Compare
if buf.String() != testdataOrder {
t.Log(buf.String())
@@ -452,25 +486,25 @@ func TestZoneRrtypeLess(t *testing.T) {
*/
var tests = []struct {
- e1, e2 uint16
+ e1, e2 string
expected bool
}{
- {dns.TypeSOA, dns.TypeSOA, false},
- {dns.TypeSOA, dns.TypeA, true},
- {dns.TypeSOA, dns.TypeTXT, true},
- {dns.TypeSOA, dns.TypeNS, true},
- {dns.TypeNS, dns.TypeSOA, false},
- {dns.TypeNS, dns.TypeA, true},
- {dns.TypeNS, dns.TypeTXT, true},
- {dns.TypeNS, dns.TypeNS, false},
- {dns.TypeA, dns.TypeSOA, false},
- {dns.TypeA, dns.TypeA, false},
- {dns.TypeA, dns.TypeTXT, true},
- {dns.TypeA, dns.TypeNS, false},
- {dns.TypeMX, dns.TypeSOA, false},
- {dns.TypeMX, dns.TypeA, false},
- {dns.TypeMX, dns.TypeTXT, true},
- {dns.TypeMX, dns.TypeNS, false},
+ {"SOA", "SOA", false},
+ {"SOA", "A", true},
+ {"SOA", "TXT", true},
+ {"SOA", "NS", true},
+ {"NS", "SOA", false},
+ {"NS", "A", true},
+ {"NS", "TXT", true},
+ {"NS", "NS", false},
+ {"A", "SOA", false},
+ {"A", "A", false},
+ {"A", "TXT", true},
+ {"A", "NS", false},
+ {"MX", "SOA", false},
+ {"MX", "A", false},
+ {"MX", "TXT", true},
+ {"MX", "NS", false},
}
for _, test := range tests {
diff --git a/pkg/prettyzone/sorting.go b/pkg/prettyzone/sorting.go
new file mode 100644
index 000000000..03a78e420
--- /dev/null
+++ b/pkg/prettyzone/sorting.go
@@ -0,0 +1,194 @@
+package prettyzone
+
+// Generate zonefiles.
+// This generates a zonefile that prioritizes beauty over efficiency.
+
+import (
+ "bytes"
+ "log"
+ "strconv"
+ "strings"
+
+ "github.com/StackExchange/dnscontrol/v2/models"
+)
+
+type zoneGenData struct {
+ Origin string
+ DefaultTTL uint32
+ Records models.Records
+}
+
+func (z *zoneGenData) Len() int { return len(z.Records) }
+func (z *zoneGenData) Swap(i, j int) { z.Records[i], z.Records[j] = z.Records[j], z.Records[i] }
+func (z *zoneGenData) Less(i, j int) bool {
+ a, b := z.Records[i], z.Records[j]
+
+ // Sort by name.
+ compA, compB := a.NameFQDN, b.NameFQDN
+ if compA != compB {
+ if a.Name == "@" {
+ compA = "@"
+ }
+ if b.Name == "@" {
+ compB = "@"
+ }
+ return zoneLabelLess(compA, compB)
+ }
+
+ // sub-sort by type
+ if a.Type != b.Type {
+ return zoneRrtypeLess(a.Type, b.Type)
+ }
+
+ // sub-sort within type:
+ switch a.Type { // #rtype_variations
+ case "A":
+ ta2, tb2 := a.GetTargetIP(), b.GetTargetIP()
+ ipa, ipb := ta2.To4(), tb2.To4()
+ if ipa == nil || ipb == nil {
+ log.Fatalf("should not happen: IPs are not 4 bytes: %#v %#v", ta2, tb2)
+ }
+ return bytes.Compare(ipa, ipb) == -1
+ case "AAAA":
+ ta2, tb2 := a.GetTargetIP(), b.GetTargetIP()
+ ipa, ipb := ta2.To16(), tb2.To16()
+ if ipa == nil || ipb == nil {
+ log.Fatalf("should not happen: IPs are not 16 bytes: %#v %#v", ta2, tb2)
+ }
+ return bytes.Compare(ipa, ipb) == -1
+ case "MX":
+ // sort by priority. If they are equal, sort by Mx.
+ if a.MxPreference == b.MxPreference {
+ return a.GetTargetField() < b.GetTargetField()
+ }
+ return a.MxPreference < b.MxPreference
+ case "SRV":
+ //ta2, tb2 := a.(*dns.SRV), b.(*dns.SRV)
+ pa, pb := a.SrvPort, b.SrvPort
+ if pa != pb {
+ return pa < pb
+ }
+ pa, pb = a.SrvPriority, b.SrvPriority
+ if pa != pb {
+ return pa < pb
+ }
+ pa, pb = a.SrvWeight, b.SrvWeight
+ if pa != pb {
+ return pa < pb
+ }
+ case "PTR":
+ //ta2, tb2 := a.(*dns.PTR), b.(*dns.PTR)
+ pa, pb := a.GetTargetField(), b.GetTargetField()
+ if pa != pb {
+ return pa < pb
+ }
+ case "CAA":
+ //ta2, tb2 := a.(*dns.CAA), b.(*dns.CAA)
+ // sort by tag
+ pa, pb := a.CaaTag, b.CaaTag
+ if pa != pb {
+ return pa < pb
+ }
+ // then flag
+ fa, fb := a.CaaFlag, b.CaaFlag
+ if fa != fb {
+ // flag set goes before ones without flag set
+ return fa > fb
+ }
+ default:
+ // pass through. String comparison is sufficient.
+ }
+ return a.String() < b.String()
+}
+
+func zoneLabelLess(a, b string) bool {
+ // Compare two zone labels for the purpose of sorting the RRs in a Zone.
+
+ // If they are equal, we are done. All other code is simplified
+ // because we can assume a!=b.
+ if a == b {
+ return false
+ }
+
+ // Sort @ at the top, then *, then everything else lexigraphically.
+ // i.e. @ always is less. * is is less than everything but @.
+ if a == "@" {
+ return true
+ }
+ if b == "@" {
+ return false
+ }
+ if a == "*" {
+ return true
+ }
+ if b == "*" {
+ return false
+ }
+
+ // Split into elements and match up last elements to first. Compare the
+ // first non-equal elements.
+
+ as := strings.Split(a, ".")
+ bs := strings.Split(b, ".")
+ ia := len(as) - 1
+ ib := len(bs) - 1
+
+ var min int
+ if ia < ib {
+ min = len(as) - 1
+ } else {
+ min = len(bs) - 1
+ }
+
+ // Skip the matching highest elements, then compare the next item.
+ for i, j := ia, ib; min >= 0; i, j, min = i-1, j-1, min-1 {
+ // Compare as[i] < bs[j]
+ // Sort @ at the top, then *, then everything else.
+ // i.e. @ always is less. * is is less than everything but @.
+ // If both are numeric, compare as integers, otherwise as strings.
+
+ if as[i] != bs[j] {
+
+ // If the first element is *, it is always less.
+ if i == 0 && as[i] == "*" {
+ return true
+ }
+ if j == 0 && bs[j] == "*" {
+ return false
+ }
+
+ // If the elements are both numeric, compare as integers:
+ au, aerr := strconv.ParseUint(as[i], 10, 64)
+ bu, berr := strconv.ParseUint(bs[j], 10, 64)
+ if aerr == nil && berr == nil {
+ return au < bu
+ }
+ // otherwise, compare as strings:
+ return as[i] < bs[j]
+ }
+ }
+ // The min top elements were equal, so the shorter name is less.
+ return ia < ib
+}
+
+func zoneRrtypeLess(a, b string) bool {
+ // Compare two RR types for the purpose of sorting the RRs in a Zone.
+
+ if a == b {
+ return false
+ }
+
+ // List SOAs, NSs, etc. then all others alphabetically.
+
+ for _, t := range []string{"SOA", "NS", "CNAME",
+ "A", "AAAA", "MX", "SRV", "TXT",
+ } {
+ if a == t {
+ return true
+ }
+ if b == t {
+ return false
+ }
+ }
+ return a < b
+}
diff --git a/providers/activedir/activedirProvider.go b/providers/activedir/activedirProvider.go
index a9712bc09..0e501d0ac 100644
--- a/providers/activedir/activedirProvider.go
+++ b/providers/activedir/activedirProvider.go
@@ -24,6 +24,7 @@ var features = providers.DocumentationNotes{
providers.DocCreateDomains: providers.Cannot("AD depends on the zone already existing on the dns server"),
providers.DocDualHost: providers.Cannot("This driver does not manage NS records, so should not be used for dual-host scenarios"),
providers.DocOfficiallySupported: providers.Can(),
+ providers.CanGetZones: providers.Unimplemented(),
}
// Register with the dnscontrol system.
diff --git a/providers/activedir/domains.go b/providers/activedir/domains.go
index a865d1d4e..235795492 100644
--- a/providers/activedir/domains.go
+++ b/providers/activedir/domains.go
@@ -37,6 +37,14 @@ var supportedTypes = map[string]bool{
"NS": true,
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (client *adProvider) 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.
+}
+
// GetDomainCorrections gets existing records, diffs them against existing, and returns corrections.
func (c *adProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
diff --git a/providers/azuredns/azureDnsProvider.go b/providers/azuredns/azureDnsProvider.go
index 57b8052cf..2a55d4a5e 100644
--- a/providers/azuredns/azureDnsProvider.go
+++ b/providers/azuredns/azureDnsProvider.go
@@ -62,6 +62,7 @@ var features = providers.DocumentationNotes{
providers.CanUseNAPTR: providers.Cannot(),
providers.CanUseSSHFP: providers.Cannot(),
providers.CanUseTLSA: providers.Cannot(),
+ providers.CanGetZones: providers.Unimplemented(),
}
func init() {
@@ -110,6 +111,14 @@ func (a *azureDnsProvider) GetNameservers(domain string) ([]*models.Nameserver,
return ns, nil
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (client *azureDnsProvider) 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 (a *azureDnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
err := dc.Punycode()
diff --git a/providers/bind/bindProvider.go b/providers/bind/bindProvider.go
index 77d8f44be..4a56d079a 100644
--- a/providers/bind/bindProvider.go
+++ b/providers/bind/bindProvider.go
@@ -25,6 +25,7 @@ import (
"github.com/miekg/dns"
"github.com/StackExchange/dnscontrol/v2/models"
+ "github.com/StackExchange/dnscontrol/v2/pkg/prettyzone"
"github.com/StackExchange/dnscontrol/v2/providers"
"github.com/StackExchange/dnscontrol/v2/providers/diff"
)
@@ -41,6 +42,7 @@ var features = providers.DocumentationNotes{
providers.DocCreateDomains: providers.Can("Driver just maintains list of zone files. It should automatically add missing ones."),
providers.DocDualHost: providers.Can(),
providers.DocOfficiallySupported: providers.Can(),
+ providers.CanGetZones: providers.Can(),
}
func initBind(config map[string]string, providermeta json.RawMessage) (providers.DNSServiceProvider, error) {
@@ -83,79 +85,12 @@ func (s SoaInfo) String() string {
// Bind is the provider handle for the Bind driver.
type Bind struct {
- DefaultNS []string `json:"default_ns"`
- DefaultSoa SoaInfo `json:"default_soa"`
- nameservers []*models.Nameserver
- directory string
-}
-
-// var bindSkeletin = flag.String("bind_skeletin", "skeletin/master/var/named/chroot/var/named/master", "")
-
-func rrToRecord(rr dns.RR, origin string, replaceSerial uint32) (models.RecordConfig, uint32) {
- // Convert's dns.RR into our native data type (models.RecordConfig).
- // Records are translated directly with no changes.
- // If it is an SOA for the apex domain and
- // replaceSerial != 0, change the serial to replaceSerial.
- // WARNING(tlim): This assumes SOAs do not have serial=0.
- // If one is found, we replace it with serial=1.
- var oldSerial, newSerial uint32
- header := rr.Header()
- rc := models.RecordConfig{
- Type: dns.TypeToString[header.Rrtype],
- TTL: header.Ttl,
- }
- rc.SetLabelFromFQDN(strings.TrimSuffix(header.Name, "."), origin)
- switch v := rr.(type) { // #rtype_variations
- case *dns.A:
- panicInvalid(rc.SetTarget(v.A.String()))
- case *dns.AAAA:
- panicInvalid(rc.SetTarget(v.AAAA.String()))
- case *dns.CAA:
- panicInvalid(rc.SetTargetCAA(v.Flag, v.Tag, v.Value))
- case *dns.CNAME:
- panicInvalid(rc.SetTarget(v.Target))
- case *dns.MX:
- panicInvalid(rc.SetTargetMX(v.Preference, v.Mx))
- case *dns.NS:
- panicInvalid(rc.SetTarget(v.Ns))
- case *dns.PTR:
- panicInvalid(rc.SetTarget(v.Ptr))
- case *dns.NAPTR:
- panicInvalid(rc.SetTargetNAPTR(v.Order, v.Preference, v.Flags, v.Service, v.Regexp, v.Replacement))
- case *dns.SOA:
- oldSerial = v.Serial
- if oldSerial == 0 {
- // For SOA records, we never return a 0 serial number.
- oldSerial = 1
- }
- newSerial = v.Serial
- //if (dnsutil.TrimDomainName(rc.Name, origin+".") == "@") && replaceSerial != 0 {
- if rc.GetLabel() == "@" && replaceSerial != 0 {
- newSerial = replaceSerial
- }
- panicInvalid(rc.SetTarget(
- fmt.Sprintf("%v %v %v %v %v %v %v",
- v.Ns, v.Mbox, newSerial, v.Refresh, v.Retry, v.Expire, v.Minttl),
- ))
- // FIXME(tlim): SOA should be handled by splitting out the fields.
- case *dns.SRV:
- panicInvalid(rc.SetTargetSRV(v.Priority, v.Weight, v.Port, v.Target))
- case *dns.SSHFP:
- panicInvalid(rc.SetTargetSSHFP(v.Algorithm, v.Type, v.FingerPrint))
- case *dns.TLSA:
- panicInvalid(rc.SetTargetTLSA(v.Usage, v.Selector, v.MatchingType, v.Certificate))
- case *dns.TXT:
- panicInvalid(rc.SetTargetTXTs(v.Txt))
- default:
- log.Fatalf("rrToRecord: Unimplemented zone record type=%s (%v)\n", rc.Type, rr)
- }
- return rc, oldSerial
-}
-
-func panicInvalid(err error) {
- if err != nil {
- panic(fmt.Errorf("unparsable record received from BIND: %w", err))
- }
+ DefaultNS []string `json:"default_ns"`
+ DefaultSoa SoaInfo `json:"default_soa"`
+ nameservers []*models.Nameserver
+ directory string
+ zonefile string // Where the zone data is expected
+ zoneFileFound bool // Did the zonefile exist?
}
func makeDefaultSOA(info SoaInfo, origin string) *models.RecordConfig {
@@ -195,6 +130,56 @@ func (c *Bind) GetNameservers(string) ([]*models.Nameserver, error) {
return c.nameservers, nil
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (c *Bind) GetZoneRecords(domain string) (models.Records, error) {
+ // Default SOA record. If we see one in the zone, this will be replaced.
+
+ soaRec := makeDefaultSOA(c.DefaultSoa, domain)
+ foundRecords := models.Records{}
+ var oldSerial, newSerial uint32
+
+ if _, err := os.Stat(c.directory); os.IsNotExist(err) {
+ fmt.Printf("\nWARNING: BIND directory %q does not exist!\n", c.directory)
+ }
+
+ zonefile := filepath.Join(c.directory, strings.Replace(strings.ToLower(domain), "/", "_", -1)+".zone")
+ c.zonefile = zonefile
+ foundFH, err := os.Open(zonefile)
+ c.zoneFileFound = err == nil
+ if err != nil && !os.IsNotExist(os.ErrNotExist) {
+ // Don't whine if the file doesn't exist. However all other
+ // errors will be reported.
+ fmt.Printf("Could not read zonefile: %v\n", err)
+ } else {
+ for x := range dns.ParseZone(foundFH, domain, zonefile) {
+ if x.Error != nil {
+ log.Println("Error in zonefile:", x.Error)
+ } else {
+ rec, serial := models.RRtoRC(x.RR, domain, oldSerial)
+ if serial != 0 && oldSerial != 0 {
+ log.Fatalf("Multiple SOA records in zonefile: %v\n", zonefile)
+ }
+ if serial != 0 {
+ // This was an SOA record. Update the serial.
+ oldSerial = serial
+ newSerial = generateSerial(oldSerial)
+ // Regenerate with new serial:
+ *soaRec, _ = models.RRtoRC(x.RR, domain, newSerial)
+ rec = *soaRec
+ }
+ foundRecords = append(foundRecords, &rec)
+ }
+ }
+ }
+
+ // Add SOA record to expected set:
+ if !foundRecords.HasRecordTypeName("SOA", "@") {
+ //foundRecords = append(foundRecords, soaRec)
+ }
+
+ return foundRecords, nil
+}
+
// GetDomainCorrections returns a list of corrections to update a domain.
func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
@@ -211,49 +196,9 @@ func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correcti
// foundDiffRecords < foundRecords
// diff.Inc...(foundDiffRecords, expectedDiffRecords )
- // Default SOA record. If we see one in the zone, this will be replaced.
- soaRec := makeDefaultSOA(c.DefaultSoa, dc.Name)
-
- // Read foundRecords:
- foundRecords := make([]*models.RecordConfig, 0)
- var oldSerial, newSerial uint32
-
- if _, err := os.Stat(c.directory); os.IsNotExist(err) {
- fmt.Printf("\nWARNING: BIND directory %q does not exist!\n", c.directory)
- }
-
- zonefile := filepath.Join(c.directory, strings.Replace(strings.ToLower(dc.Name), "/", "_", -1)+".zone")
- foundFH, err := os.Open(zonefile)
- zoneFileFound := err == nil
- if err != nil && !os.IsNotExist(os.ErrNotExist) {
- // Don't whine if the file doesn't exist. However all other
- // errors will be reported.
- fmt.Printf("Could not read zonefile: %v\n", err)
- } else {
- for x := range dns.ParseZone(foundFH, dc.Name, zonefile) {
- if x.Error != nil {
- log.Println("Error in zonefile:", x.Error)
- } else {
- rec, serial := rrToRecord(x.RR, dc.Name, oldSerial)
- if serial != 0 && oldSerial != 0 {
- log.Fatalf("Multiple SOA records in zonefile: %v\n", zonefile)
- }
- if serial != 0 {
- // This was an SOA record. Update the serial.
- oldSerial = serial
- newSerial = generateSerial(oldSerial)
- // Regenerate with new serial:
- *soaRec, _ = rrToRecord(x.RR, dc.Name, newSerial)
- rec = *soaRec
- }
- foundRecords = append(foundRecords, &rec)
- }
- }
- }
-
- // Add SOA record to expected set:
- if !dc.HasRecordTypeName("SOA", "@") {
- dc.Records = append(dc.Records, soaRec)
+ foundRecords, err := c.GetZoneRecords(dc.Name)
+ if err != nil {
+ return nil, err
}
// Normalize
@@ -267,24 +212,24 @@ func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correcti
changes := false
for _, i := range create {
changes = true
- if zoneFileFound {
+ if c.zoneFileFound {
fmt.Fprintln(buf, i)
}
}
for _, i := range del {
changes = true
- if zoneFileFound {
+ if c.zoneFileFound {
fmt.Fprintln(buf, i)
}
}
for _, i := range mod {
changes = true
- if zoneFileFound {
+ if c.zoneFileFound {
fmt.Fprintln(buf, i)
}
}
msg := fmt.Sprintf("GENERATE_ZONEFILE: %s\n", dc.Name)
- if !zoneFileFound {
+ if !c.zoneFileFound {
msg = msg + fmt.Sprintf(" (%d records)\n", len(create))
}
msg += buf.String()
@@ -294,16 +239,12 @@ func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correcti
&models.Correction{
Msg: msg,
F: func() error {
- fmt.Printf("CREATING ZONEFILE: %v\n", zonefile)
- zf, err := os.Create(zonefile)
+ fmt.Printf("CREATING ZONEFILE: %v\n", c.zonefile)
+ zf, err := os.Create(c.zonefile)
if err != nil {
log.Fatalf("Could not create zonefile: %v", err)
}
- zonefilerecords := make([]dns.RR, 0, len(dc.Records))
- for _, r := range dc.Records {
- zonefilerecords = append(zonefilerecords, r.ToRR())
- }
- err = WriteZoneFile(zf, zonefilerecords, dc.Name)
+ err = prettyzone.WriteZoneFileRC(zf, dc.Records, dc.Name)
if err != nil {
log.Fatalf("WriteZoneFile error: %v\n", err)
diff --git a/providers/bind/prettyzone.go b/providers/bind/prettyzone.go
deleted file mode 100644
index 032220208..000000000
--- a/providers/bind/prettyzone.go
+++ /dev/null
@@ -1,325 +0,0 @@
-package bind
-
-// Generate zonefiles.
-// This generates a zonefile that prioritizes beauty over efficiency.
-
-import (
- "bytes"
- "fmt"
- "io"
- "log"
- "sort"
- "strconv"
- "strings"
-
- "github.com/miekg/dns"
- "github.com/miekg/dns/dnsutil"
-)
-
-type zoneGenData struct {
- Origin string
- DefaultTTL uint32
- Records []dns.RR
-}
-
-func (z *zoneGenData) Len() int { return len(z.Records) }
-func (z *zoneGenData) Swap(i, j int) { z.Records[i], z.Records[j] = z.Records[j], z.Records[i] }
-func (z *zoneGenData) Less(i, j int) bool {
- a, b := z.Records[i], z.Records[j]
- compA, compB := dnsutil.AddOrigin(a.Header().Name, z.Origin+"."), dnsutil.AddOrigin(b.Header().Name, z.Origin+".")
- if compA != compB {
- if compA == z.Origin+"." {
- compA = "@"
- }
- if compB == z.Origin+"." {
- compB = "@"
- }
- return zoneLabelLess(compA, compB)
- }
- rrtypeA, rrtypeB := a.Header().Rrtype, b.Header().Rrtype
- if rrtypeA != rrtypeB {
- return zoneRrtypeLess(rrtypeA, rrtypeB)
- }
- switch rrtypeA { // #rtype_variations
- case dns.TypeA:
- ta2, tb2 := a.(*dns.A), b.(*dns.A)
- ipa, ipb := ta2.A.To4(), tb2.A.To4()
- if ipa == nil || ipb == nil {
- log.Fatalf("should not happen: IPs are not 4 bytes: %#v %#v", ta2, tb2)
- }
- return bytes.Compare(ipa, ipb) == -1
- case dns.TypeAAAA:
- ta2, tb2 := a.(*dns.AAAA), b.(*dns.AAAA)
- ipa, ipb := ta2.AAAA.To16(), tb2.AAAA.To16()
- return bytes.Compare(ipa, ipb) == -1
- case dns.TypeMX:
- ta2, tb2 := a.(*dns.MX), b.(*dns.MX)
- pa, pb := ta2.Preference, tb2.Preference
- // sort by priority. If they are equal, sort by Mx.
- if pa != pb {
- return pa < pb
- }
- return ta2.Mx < tb2.Mx
- case dns.TypeSRV:
- ta2, tb2 := a.(*dns.SRV), b.(*dns.SRV)
- pa, pb := ta2.Port, tb2.Port
- if pa != pb {
- return pa < pb
- }
- pa, pb = ta2.Priority, tb2.Priority
- if pa != pb {
- return pa < pb
- }
- pa, pb = ta2.Weight, tb2.Weight
- if pa != pb {
- return pa < pb
- }
- case dns.TypePTR:
- ta2, tb2 := a.(*dns.PTR), b.(*dns.PTR)
- pa, pb := ta2.Ptr, tb2.Ptr
- if pa != pb {
- return pa < pb
- }
- case dns.TypeCAA:
- ta2, tb2 := a.(*dns.CAA), b.(*dns.CAA)
- // sort by tag
- pa, pb := ta2.Tag, tb2.Tag
- if pa != pb {
- return pa < pb
- }
- // then flag
- fa, fb := ta2.Flag, tb2.Flag
- if fa != fb {
- // flag set goes before ones without flag set
- return fa > fb
- }
- default:
- // pass through. String comparison is sufficient.
- }
- return a.String() < b.String()
-}
-
-// mostCommonTTL returns the most common TTL in a set of records. If there is
-// a tie, the highest TTL is selected. This makes the results consistent.
-// NS records are not included in the analysis because Tom said so.
-func mostCommonTTL(records []dns.RR) uint32 {
- // Index the TTLs in use:
- d := make(map[uint32]int)
- for _, r := range records {
- if r.Header().Rrtype != dns.TypeNS {
- d[r.Header().Ttl]++
- }
- }
- // Find the largest count:
- var mc int
- for _, value := range d {
- if value > mc {
- mc = value
- }
- }
- // Find the largest key with that count:
- var mk uint32
- for key, value := range d {
- if value == mc {
- if key > mk {
- mk = key
- }
- }
- }
- return mk
-}
-
-// WriteZoneFile writes a beautifully formatted zone file.
-func WriteZoneFile(w io.Writer, records []dns.RR, origin string) error {
- // This function prioritizes beauty over efficiency.
- // * The zone records are sorted by label, grouped by subzones to
- // be easy to read and pleasant to the eye.
- // * Within a label, SOA and NS records are listed first.
- // * MX records are sorted numericly by preference value.
- // * SRV records are sorted numericly by port, then priority, then weight.
- // * A records are sorted by IP address, not lexicographically.
- // * Repeated labels are removed.
- // * $TTL is used to eliminate clutter. The most common TTL value is used.
- // * "@" is used instead of the apex domain name.
-
- defaultTTL := mostCommonTTL(records)
-
- z := &zoneGenData{
- Origin: dnsutil.AddOrigin(origin, "."),
- DefaultTTL: defaultTTL,
- }
- z.Records = nil
- for _, r := range records {
- z.Records = append(z.Records, r)
- }
- return z.generateZoneFileHelper(w)
-}
-
-// generateZoneFileHelper creates a pretty zonefile.
-func (z *zoneGenData) generateZoneFileHelper(w io.Writer) error {
-
- nameShortPrevious := ""
-
- sort.Sort(z)
- fmt.Fprintln(w, "$TTL", z.DefaultTTL)
- for i, rr := range z.Records {
- line := rr.String()
- if line[0] == ';' {
- continue
- }
- hdr := rr.Header()
-
- items := strings.SplitN(line, "\t", 5)
- if len(items) < 5 {
- log.Fatalf("Too few items in: %v", line)
- }
-
- // items[0]: name
- nameFqdn := hdr.Name
- nameShort := dnsutil.TrimDomainName(nameFqdn, z.Origin)
- name := nameShort
- if i > 0 && nameShort == nameShortPrevious {
- name = ""
- } else {
- name = nameShort
- }
- nameShortPrevious = nameShort
-
- // items[1]: ttl
- ttl := ""
- if hdr.Ttl != z.DefaultTTL && hdr.Ttl != 0 {
- ttl = items[1]
- }
-
- // items[2]: class
- if hdr.Class != dns.ClassINET {
- log.Fatalf("generateZoneFileHelper: Unimplemented class=%v", items[2])
- }
-
- // items[3]: type
- typeStr := dns.TypeToString[hdr.Rrtype]
-
- // items[4]: the remaining line
- target := items[4]
-
- fmt.Fprintln(w, formatLine([]int{10, 5, 2, 5, 0}, []string{name, ttl, "IN", typeStr, target}))
- }
- return nil
-}
-
-func formatLine(lengths []int, fields []string) string {
- c := 0
- result := ""
- for i, length := range lengths {
- item := fields[i]
- for len(result) < c {
- result += " "
- }
- if item != "" {
- result += item + " "
- }
- c += length + 1
- }
- return strings.TrimRight(result, " ")
-}
-
-func isNumeric(s string) bool {
- _, err := strconv.ParseFloat(s, 64)
- return err == nil
-}
-
-func zoneLabelLess(a, b string) bool {
- // Compare two zone labels for the purpose of sorting the RRs in a Zone.
-
- // If they are equal, we are done. All other code is simplified
- // because we can assume a!=b.
- if a == b {
- return false
- }
-
- // Sort @ at the top, then *, then everything else lexigraphically.
- // i.e. @ always is less. * is is less than everything but @.
- if a == "@" {
- return true
- }
- if b == "@" {
- return false
- }
- if a == "*" {
- return true
- }
- if b == "*" {
- return false
- }
-
- // Split into elements and match up last elements to first. Compare the
- // first non-equal elements.
-
- as := strings.Split(a, ".")
- bs := strings.Split(b, ".")
- ia := len(as) - 1
- ib := len(bs) - 1
-
- var min int
- if ia < ib {
- min = len(as) - 1
- } else {
- min = len(bs) - 1
- }
-
- // Skip the matching highest elements, then compare the next item.
- for i, j := ia, ib; min >= 0; i, j, min = i-1, j-1, min-1 {
- // Compare as[i] < bs[j]
- // Sort @ at the top, then *, then everything else.
- // i.e. @ always is less. * is is less than everything but @.
- // If both are numeric, compare as integers, otherwise as strings.
-
- if as[i] != bs[j] {
-
- // If the first element is *, it is always less.
- if i == 0 && as[i] == "*" {
- return true
- }
- if j == 0 && bs[j] == "*" {
- return false
- }
-
- // If the elements are both numeric, compare as integers:
- au, aerr := strconv.ParseUint(as[i], 10, 64)
- bu, berr := strconv.ParseUint(bs[j], 10, 64)
- if aerr == nil && berr == nil {
- return au < bu
- }
- // otherwise, compare as strings:
- return as[i] < bs[j]
- }
- }
- // The min top elements were equal, so the shorter name is less.
- return ia < ib
-}
-
-func zoneRrtypeLess(a, b uint16) bool {
- // Compare two RR types for the purpose of sorting the RRs in a Zone.
-
- // If they are equal, we are done. All other code is simplified
- // because we can assume a!=b.
- if a == b {
- return false
- }
-
- // List SOAs, then NSs, then all others.
- // i.e. SOA is always less. NS is less than everything but SOA.
- if a == dns.TypeSOA {
- return true
- }
- if b == dns.TypeSOA {
- return false
- }
- if a == dns.TypeNS {
- return true
- }
- if b == dns.TypeNS {
- return false
- }
- return a < b
-}
diff --git a/providers/capabilities.go b/providers/capabilities.go
index dcc5699df..14b325d5b 100644
--- a/providers/capabilities.go
+++ b/providers/capabilities.go
@@ -49,6 +49,9 @@ const (
// CanUseRoute53Alias indicates the provider support the specific R53_ALIAS records that only the Route53 provider supports
CanUseRoute53Alias
+
+ // CanGetZoe indicates the provider supports the get-zones subcommand.
+ CanGetZones
)
var providerCapabilities = map[string]map[Capability]bool{}
diff --git a/providers/cloudflare/cloudflareProvider.go b/providers/cloudflare/cloudflareProvider.go
index 29d5de8fb..9bf6b3632 100644
--- a/providers/cloudflare/cloudflareProvider.go
+++ b/providers/cloudflare/cloudflareProvider.go
@@ -47,6 +47,7 @@ var features = providers.DocumentationNotes{
providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Cannot("Cloudflare will not work well in situations where it is not the only DNS server"),
providers.DocOfficiallySupported: providers.Can(),
+ providers.CanGetZones: providers.Can(),
}
func init() {
@@ -93,24 +94,60 @@ func (c *CloudflareApi) GetNameservers(domain string) ([]*models.Nameserver, err
return models.StringsToNameservers(ns), nil
}
-// GetDomainCorrections returns a list of corrections to update a domain.
-func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
- if c.domainIndex == nil {
- if err := c.fetchDomainList(); err != nil {
- return nil, err
+func (c *CloudflareApi) ListZones() ([]string, error) {
+ if err := c.fetchDomainList(); err != nil {
+ return nil, err
+ }
+ zones := make([]string, 0, len(c.domainIndex))
+ for d := range c.domainIndex {
+ zones = append(zones, d)
+ }
+ return zones, nil
+}
+
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (c *CloudflareApi) GetZoneRecords(domain string) (models.Records, error) {
+ id, err := c.getDomainID(domain)
+ if err != nil {
+ return nil, err
+ }
+ records, err := c.getRecordsForDomain(id, domain)
+ if err != nil {
+ return nil, err
+ }
+ for _, rec := range records {
+ if rec.TTL == 1 {
+ rec.TTL = 0
}
}
- id, ok := c.domainIndex[dc.Name]
- if !ok {
- return nil, fmt.Errorf("%s not listed in zones for cloudflare account", dc.Name)
- }
+ return records, nil
+}
- if err := c.preprocessConfig(dc); err != nil {
+func (c *CloudflareApi) getDomainID(name string) (string, error) {
+ if c.domainIndex == nil {
+ if err := c.fetchDomainList(); err != nil {
+ return "", err
+ }
+ }
+ id, ok := c.domainIndex[name]
+ if !ok {
+ return "", fmt.Errorf("'%s' not a zone in cloudflare account", name)
+ }
+ return id, nil
+}
+
+// GetDomainCorrections returns a list of corrections to update a domain.
+func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
+ id, err := c.getDomainID(dc.Name)
+ if err != nil {
+ return nil, err
+ }
+ records, err := c.getRecordsForDomain(id, dc.Name)
+ if err != nil {
return nil, err
}
- records, err := c.getRecordsForDomain(id, dc.Name)
- if err != nil {
+ if err := c.preprocessConfig(dc); err != nil {
return nil, err
}
for i := len(records) - 1; i >= 0; i-- {
diff --git a/providers/cloudns/cloudnsProvider.go b/providers/cloudns/cloudnsProvider.go
index 3086ce93d..aab892b2b 100644
--- a/providers/cloudns/cloudnsProvider.go
+++ b/providers/cloudns/cloudnsProvider.go
@@ -49,6 +49,7 @@ var features = providers.DocumentationNotes{
providers.CanUseCAA: providers.Can(),
providers.CanUseTLSA: providers.Can(),
providers.CanUsePTR: providers.Unimplemented(),
+ providers.CanGetZones: providers.Unimplemented(),
}
func init() {
@@ -79,7 +80,7 @@ func (c *api) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correctio
}
domainID, ok := c.domainIndex[dc.Name]
if !ok {
- return nil, fmt.Errorf("%s not listed in domains for ClouDNS account", dc.Name)
+ return nil, fmt.Errorf("'%s' not a zone in ClouDNS account", dc.Name)
}
records, err := c.getRecords(domainID)
@@ -150,6 +151,14 @@ func (c *api) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correctio
return corrections, nil
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (client *api) 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.
+}
+
// EnsureDomainExists returns an error if domain doesn't exist.
func (c *api) EnsureDomainExists(domain string) error {
if err := c.fetchDomainList(); err != nil {
diff --git a/providers/digitalocean/digitaloceanProvider.go b/providers/digitalocean/digitaloceanProvider.go
index bb8efb774..eafed6889 100644
--- a/providers/digitalocean/digitaloceanProvider.go
+++ b/providers/digitalocean/digitaloceanProvider.go
@@ -69,7 +69,8 @@ var features = providers.DocumentationNotes{
// Digitalocean support CAA records, except
// ";" value with issue/issuewild records:
// https://www.digitalocean.com/docs/networking/dns/how-to/create-caa-records/
- providers.CanUseCAA: providers.Can(),
+ providers.CanUseCAA: providers.Can(),
+ providers.CanGetZones: providers.Unimplemented(),
}
func init() {
@@ -95,6 +96,14 @@ func (api *DoApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
return models.StringsToNameservers(defaultNameServerNames), nil
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (client *DoApi) 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.
+}
+
// GetDomainCorrections returns a list of corretions for the domain.
func (api *DoApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
ctx := context.Background()
diff --git a/providers/dnsimple/dnsimpleProvider.go b/providers/dnsimple/dnsimpleProvider.go
index 526cdb3f0..272e0536f 100644
--- a/providers/dnsimple/dnsimpleProvider.go
+++ b/providers/dnsimple/dnsimpleProvider.go
@@ -25,6 +25,7 @@ var features = providers.DocumentationNotes{
providers.DocCreateDomains: providers.Cannot(),
providers.DocDualHost: providers.Cannot("DNSimple does not allow sufficient control over the apex NS records"),
providers.DocOfficiallySupported: providers.Cannot(),
+ providers.CanGetZones: providers.Unimplemented(),
}
func init() {
@@ -53,6 +54,14 @@ func (c *DnsimpleApi) GetNameservers(domainName string) ([]*models.Nameserver, e
return models.StringsToNameservers(defaultNameServerNames), nil
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (client *DnsimpleApi) 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.
+}
+
// GetDomainCorrections returns corrections that update a domain.
func (c *DnsimpleApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
corrections := []*models.Correction{}
diff --git a/providers/exoscale/exoscaleProvider.go b/providers/exoscale/exoscaleProvider.go
index db2afcdc0..a02927803 100644
--- a/providers/exoscale/exoscaleProvider.go
+++ b/providers/exoscale/exoscaleProvider.go
@@ -32,6 +32,7 @@ var features = providers.DocumentationNotes{
providers.DocCreateDomains: providers.Cannot(),
providers.DocDualHost: providers.Cannot("Exoscale does not allow sufficient control over the apex NS records"),
providers.DocOfficiallySupported: providers.Cannot(),
+ providers.CanGetZones: providers.Unimplemented(),
}
func init() {
@@ -55,6 +56,14 @@ func (c *exoscaleProvider) GetNameservers(domain string) ([]*models.Nameserver,
return nil, nil
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (client *exoscaleProvider) 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.
+}
+
// GetDomainCorrections returns a list of corretions for the domain.
func (c *exoscaleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
diff --git a/providers/gandi/gandiProvider.go b/providers/gandi/gandiProvider.go
index cd6265cc4..856a88edf 100644
--- a/providers/gandi/gandiProvider.go
+++ b/providers/gandi/gandiProvider.go
@@ -33,6 +33,7 @@ var features = providers.DocumentationNotes{
providers.CantUseNOPURGE: providers.Cannot(),
providers.DocCreateDomains: providers.Cannot("Can only manage domains registered through their service"),
providers.DocOfficiallySupported: providers.Cannot(),
+ providers.CanGetZones: providers.Unimplemented(),
}
func init() {
@@ -58,7 +59,7 @@ func (c *GandiApi) getDomainInfo(domain string) (*gandidomain.DomainInfo, error)
}
_, ok := c.domainIndex[domain]
if !ok {
- return nil, fmt.Errorf("%s not listed in zones for gandi account", domain)
+ return nil, fmt.Errorf("'%s' not a zone in gandi account", domain)
}
return c.fetchDomainInfo(domain)
}
@@ -76,6 +77,14 @@ func (c *GandiApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
return ns, nil
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (client *GandiApi) 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.
+}
+
// GetDomainCorrections returns a list of corrections recommended for this domain.
func (c *GandiApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
diff --git a/providers/gandi/livedns.go b/providers/gandi/livedns.go
index f984d0388..4b05808cd 100644
--- a/providers/gandi/livedns.go
+++ b/providers/gandi/livedns.go
@@ -85,6 +85,14 @@ func (c *liveClient) GetNameservers(domain string) ([]*models.Nameserver, error)
return ns, nil
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (client *liveClient) 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.
+}
+
// GetDomainCorrections returns a list of corrections recommended for this domain.
func (c *liveClient) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
diff --git a/providers/gandi_v5/gandi_v5Provider.go b/providers/gandi_v5/gandi_v5Provider.go
index e3993c92b..ad626eb98 100644
--- a/providers/gandi_v5/gandi_v5Provider.go
+++ b/providers/gandi_v5/gandi_v5Provider.go
@@ -46,6 +46,7 @@ var features = providers.DocumentationNotes{
providers.CantUseNOPURGE: providers.Cannot(),
providers.DocCreateDomains: providers.Cannot("Can only manage domains registered through their service"),
providers.DocOfficiallySupported: providers.Cannot(),
+ providers.CanGetZones: providers.Can(),
}
// Section 2: Define the API client.
diff --git a/providers/gcloud/gcloudProvider.go b/providers/gcloud/gcloudProvider.go
index 03770ed58..b3184e607 100644
--- a/providers/gcloud/gcloudProvider.go
+++ b/providers/gcloud/gcloudProvider.go
@@ -22,6 +22,7 @@ var features = providers.DocumentationNotes{
providers.CanUseSRV: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
+ providers.CanGetZones: providers.Unimplemented(),
}
func sPtr(s string) *string {
@@ -124,6 +125,14 @@ func keyForRec(r *models.RecordConfig) key {
return key{Type: r.Type, Name: r.GetLabelFQDN() + "."}
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (client *gcloud) 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 (g *gcloud) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
if err := dc.Punycode(); err != nil {
return nil, err
diff --git a/providers/hexonet/hexonetProvider.go b/providers/hexonet/hexonetProvider.go
index 894135aea..3cb530f5d 100644
--- a/providers/hexonet/hexonetProvider.go
+++ b/providers/hexonet/hexonetProvider.go
@@ -29,6 +29,7 @@ var features = providers.DocumentationNotes{
providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Can(),
providers.DocOfficiallySupported: providers.Cannot("Actively maintained provider module."),
+ providers.CanGetZones: providers.Unimplemented(),
}
func newProvider(conf map[string]string) (*HXClient, error) {
diff --git a/providers/hexonet/records.go b/providers/hexonet/records.go
index e318989ae..22f9e6045 100644
--- a/providers/hexonet/records.go
+++ b/providers/hexonet/records.go
@@ -35,6 +35,14 @@ type HXRecord struct {
Priority uint32
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (client *HXClient) 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.
+}
+
// GetDomainCorrections gathers correctios that would bring n to match dc.
func (n *HXClient) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
diff --git a/providers/linode/linodeProvider.go b/providers/linode/linodeProvider.go
index b3780d39d..a95bfac14 100644
--- a/providers/linode/linodeProvider.go
+++ b/providers/linode/linodeProvider.go
@@ -88,6 +88,7 @@ func NewLinode(m map[string]string, metadata json.RawMessage) (providers.DNSServ
var features = providers.DocumentationNotes{
providers.DocDualHost: providers.Cannot(),
providers.DocOfficiallySupported: providers.Cannot(),
+ providers.CanGetZones: providers.Unimplemented(),
}
func init() {
@@ -100,6 +101,14 @@ func (api *LinodeApi) GetNameservers(domain string) ([]*models.Nameserver, error
return models.StringsToNameservers(defaultNameServerNames), nil
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (client *LinodeApi) 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.
+}
+
// GetDomainCorrections returns the corrections for a domain.
func (api *LinodeApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc, err := dc.Copy()
@@ -116,7 +125,7 @@ func (api *LinodeApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.C
}
domainID, ok := api.domainIndex[dc.Name]
if !ok {
- return nil, fmt.Errorf("%s not listed in domains for Linode account", dc.Name)
+ return nil, fmt.Errorf("'%s' not a zone in Linode account", dc.Name)
}
records, err := api.getRecords(domainID)
diff --git a/providers/namecheap/namecheapProvider.go b/providers/namecheap/namecheapProvider.go
index 1648ab6f3..b6020d2c2 100644
--- a/providers/namecheap/namecheapProvider.go
+++ b/providers/namecheap/namecheapProvider.go
@@ -36,6 +36,7 @@ var features = providers.DocumentationNotes{
providers.DocCreateDomains: providers.Cannot("Requires domain registered through their service"),
providers.DocDualHost: providers.Cannot("Doesn't allow control of apex NS records"),
providers.DocOfficiallySupported: providers.Cannot(),
+ providers.CanGetZones: providers.Unimplemented(),
}
func init() {
@@ -105,6 +106,14 @@ func doWithRetry(f func() error) {
}
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (client *Namecheap) 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.
+}
+
// GetDomainCorrections returns the corrections for the domain.
func (n *Namecheap) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
diff --git a/providers/namedotcom/namedotcomProvider.go b/providers/namedotcom/namedotcomProvider.go
index 83f36913d..93ff3efad 100644
--- a/providers/namedotcom/namedotcomProvider.go
+++ b/providers/namedotcom/namedotcomProvider.go
@@ -28,6 +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(),
}
func newReg(conf map[string]string) (providers.Registrar, error) {
diff --git a/providers/namedotcom/records.go b/providers/namedotcom/records.go
index 99237e4d6..ac3528b98 100644
--- a/providers/namedotcom/records.go
+++ b/providers/namedotcom/records.go
@@ -19,6 +19,14 @@ var defaultNameservers = []*models.Nameserver{
{Name: "ns4.name.com"},
}
+// 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.
+}
+
// GetDomainCorrections gathers correctios that would bring n to match dc.
func (n *NameCom) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
diff --git a/providers/ns1/ns1provider.go b/providers/ns1/ns1provider.go
index 25c0a75d0..ae5650909 100644
--- a/providers/ns1/ns1provider.go
+++ b/providers/ns1/ns1provider.go
@@ -43,6 +43,14 @@ func (n *nsone) GetNameservers(domain string) ([]*models.Nameserver, error) {
return models.StringsToNameservers(z.DNSServers), nil
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (client *nsone) 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 *nsone) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
//dc.CombineMXs()
diff --git a/providers/octodns/octodnsProvider.go b/providers/octodns/octodnsProvider.go
index 1c6354d70..91a52075e 100644
--- a/providers/octodns/octodnsProvider.go
+++ b/providers/octodns/octodnsProvider.go
@@ -40,6 +40,7 @@ var features = providers.DocumentationNotes{
//providers.CanUseTXTMulti: providers.Can(),
providers.DocCreateDomains: providers.Cannot("Driver just maintains list of OctoDNS config files. You must manually create the master config files that refer these."),
providers.DocDualHost: providers.Cannot("Research is needed."),
+ providers.CanGetZones: providers.Unimplemented(),
}
func initProvider(config map[string]string, providermeta json.RawMessage) (providers.DNSServiceProvider, error) {
@@ -78,6 +79,14 @@ func (c *Provider) GetNameservers(string) ([]*models.Nameserver, error) {
return nil, nil
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (client *Provider) 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.
+}
+
// GetDomainCorrections returns a list of corrections to update a domain.
func (c *Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
diff --git a/providers/opensrs/opensrsProvider.go b/providers/opensrs/opensrsProvider.go
index e16a00cfe..5956421e2 100644
--- a/providers/opensrs/opensrsProvider.go
+++ b/providers/opensrs/opensrsProvider.go
@@ -17,6 +17,7 @@ var docNotes = providers.DocumentationNotes{
providers.DocCreateDomains: providers.Cannot(),
providers.DocOfficiallySupported: providers.Cannot(),
providers.CanUseTLSA: providers.Cannot(),
+ providers.CanGetZones: providers.Unimplemented(),
}
func init() {
diff --git a/providers/ovh/ovhProvider.go b/providers/ovh/ovhProvider.go
index 9e544b94c..17483a388 100644
--- a/providers/ovh/ovhProvider.go
+++ b/providers/ovh/ovhProvider.go
@@ -27,6 +27,7 @@ var features = providers.DocumentationNotes{
providers.DocCreateDomains: providers.Cannot("New domains require registration"),
providers.DocDualHost: providers.Can(),
providers.DocOfficiallySupported: providers.Cannot(),
+ providers.CanGetZones: providers.Unimplemented(),
}
func newOVH(m map[string]string, metadata json.RawMessage) (*ovhProvider, error) {
@@ -60,7 +61,7 @@ func init() {
func (c *ovhProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
_, ok := c.zones[domain]
if !ok {
- return nil, fmt.Errorf("%s not listed in zones for ovh account", domain)
+ return nil, fmt.Errorf("'%s' not a zone in ovh account", domain)
}
ns, err := c.fetchRegistrarNS(domain)
@@ -79,6 +80,14 @@ func (e errNoExist) Error() string {
return fmt.Sprintf("Domain %s not found in your ovh account", e.domain)
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (client *ovhProvider) 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 (c *ovhProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
//dc.CombineMXs()
diff --git a/providers/providers.go b/providers/providers.go
index 3a35aaa65..7b83a9a6a 100644
--- a/providers/providers.go
+++ b/providers/providers.go
@@ -24,6 +24,13 @@ type DomainCreator interface {
EnsureDomainExists(domain string) error
}
+// DomainLister should be implemented by providers that have the
+// ability to list the zones they manage. This facilitates using the
+// "get-zones" command for "all" zones.
+type ZoneLister interface {
+ ListZones() ([]string, error)
+}
+
// RegistrarInitializer is a function to create a registrar. Function will be passed the unprocessed json payload from the configuration file for the given provider.
type RegistrarInitializer func(map[string]string) (Registrar, error)
@@ -85,6 +92,14 @@ func (n None) GetNameservers(string) ([]*models.Nameserver, error) {
return nil, nil
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (client None) 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.
+}
+
// GetDomainCorrections returns corrections to update a domain.
func (n None) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
return nil, nil
diff --git a/providers/route53/route53Provider.go b/providers/route53/route53Provider.go
index f0a60355c..66ace81b7 100644
--- a/providers/route53/route53Provider.go
+++ b/providers/route53/route53Provider.go
@@ -20,10 +20,11 @@ import (
)
type route53Provider struct {
- client *r53.Route53
- registrar *r53d.Route53Domains
- delegationSet *string
- zones map[string]*r53.HostedZone
+ client *r53.Route53
+ registrar *r53d.Route53Domains
+ delegationSet *string
+ zones map[string]*r53.HostedZone
+ originalRecords []*r53.ResourceRecordSet
}
func newRoute53Reg(conf map[string]string) (providers.Registrar, error) {
@@ -73,6 +74,7 @@ var features = providers.DocumentationNotes{
providers.CanUseTXTMulti: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseRoute53Alias: providers.Can(),
+ providers.CanGetZones: providers.Can(),
}
func init() {
@@ -169,25 +171,42 @@ func (r *route53Provider) GetNameservers(domain string) ([]*models.Nameserver, e
return ns, nil
}
-func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
- dc.Punycode()
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (r *route53Provider) GetZoneRecords(domain string) (models.Records, error) {
- var corrections = []*models.Correction{}
- zone, ok := r.zones[dc.Name]
- // add zone if it doesn't exist
+ zone, ok := r.zones[domain]
if !ok {
- return nil, errNoExist{dc.Name}
+ return nil, errNoExist{domain}
}
records, err := r.fetchRecordSets(zone.Id)
if err != nil {
return nil, err
}
+ r.originalRecords = records
var existingRecords = []*models.RecordConfig{}
for _, set := range records {
- existingRecords = append(existingRecords, nativeToRecords(set, dc.Name)...)
+ existingRecords = append(existingRecords, nativeToRecords(set, domain)...)
}
+ return existingRecords, nil
+}
+
+func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
+ dc.Punycode()
+
+ var corrections = []*models.Correction{}
+
+ existingRecords, err := r.GetZoneRecords(dc.Name)
+ if err != nil {
+ return nil, err
+ }
+
+ zone, ok := r.zones[dc.Name]
+ if !ok {
+ return nil, errNoExist{dc.Name}
+ }
+
for _, want := range dc.Records {
// update zone_id to current zone.id if not specified by the user
if want.Type == "R53_ALIAS" && want.R53Alias["zone_id"] == "" {
@@ -235,7 +254,7 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
chg.Action = sPtr("DELETE")
delDesc = append(delDesc, strings.Join(namesToUpdate[k], "\n"))
// on delete just submit the original resource set we got from r53.
- for _, r := range records {
+ for _, r := range r.originalRecords {
if unescape(r.Name) == k.NameFQDN && (*r.Type == k.Type || k.Type == "R53_ALIAS_"+*r.Type) {
rrset = r
break
diff --git a/providers/softlayer/softlayerProvider.go b/providers/softlayer/softlayerProvider.go
index 37eda34d1..24a1b02d3 100644
--- a/providers/softlayer/softlayerProvider.go
+++ b/providers/softlayer/softlayerProvider.go
@@ -22,7 +22,8 @@ type SoftLayer struct {
}
var features = providers.DocumentationNotes{
- providers.CanUseSRV: providers.Can(),
+ providers.CanUseSRV: providers.Can(),
+ providers.CanGetZones: providers.Unimplemented(),
}
func init() {
@@ -52,6 +53,14 @@ func (s *SoftLayer) GetNameservers(domain string) ([]*models.Nameserver, error)
return models.StringsToNameservers(nservers), nil
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (client *SoftLayer) 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.
+}
+
// GetDomainCorrections returns corrections to update a domain.
func (s *SoftLayer) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
corrections := []*models.Correction{}
diff --git a/providers/vultr/vultrProvider.go b/providers/vultr/vultrProvider.go
index ccea43c5f..0a7a4ab83 100644
--- a/providers/vultr/vultrProvider.go
+++ b/providers/vultr/vultrProvider.go
@@ -34,6 +34,7 @@ var features = providers.DocumentationNotes{
providers.CanUseSSHFP: providers.Can(),
providers.DocCreateDomains: providers.Can(),
providers.DocOfficiallySupported: providers.Cannot(),
+ providers.CanGetZones: providers.Unimplemented(),
}
func init() {
@@ -66,6 +67,14 @@ func NewProvider(m map[string]string, metadata json.RawMessage) (providers.DNSSe
return &Provider{client, token}, err
}
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (client *Provider) 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.
+}
+
// GetDomainCorrections gets the corrections for a DomainConfig.
func (api *Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()