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

Add "get-zone" command (#613)

* Add GetZoneRecords to DNSProvider interface
* dnscontrol now uses ufave/cli/v2
* NEW: get-zones.md
* HasRecordTypeName should be a method on models.Records not models.DomainConfig
* Implement BIND's GetZoneRecords
* new WriteZoneFile implemented
* go mod vendor
* Update docs to use get-zone instead of convertzone
* Add CanGetZone capability and update all providers.
* Get all zones for a provider at once (#626)
* implement GetZoneRecords for cloudflare
* munge cloudflare ttls
* Implement GetZoneRecords for cloudflare (#625)

Co-authored-by: Craig Peterson <192540+captncraig@users.noreply.github.com>
This commit is contained in:
Tom Limoncelli
2020-02-18 08:59:18 -05:00
committed by GitHub
parent cd680cc738
commit 87ad01d194
49 changed files with 1327 additions and 612 deletions

View File

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

View File

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

View File

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

207
commands/getZones.go Normal file
View File

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

View File

@ -902,5 +902,72 @@
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
</tr>
<tr>
<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="indicates the dnscontrol get-zones subcommand is implemented.">get-zones</th>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
</tr>
</tbody>
</table>

View File

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

85
docs/get-zones.md Normal file
View File

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

View File

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

View File

@ -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 <old/zone.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 <config/foo.com.yaml >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 <old/zone.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

1
go.sum
View File

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

View File

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

123
models/dnsrr.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

194
pkg/prettyzone/sorting.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -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) {
@ -87,75 +89,8 @@ type Bind struct {
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))
}
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)

View File

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

View File

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

View File

@ -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 {
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)
}
id, ok := c.domainIndex[dc.Name]
if !ok {
return nil, fmt.Errorf("%s not listed in zones for cloudflare account", dc.Name)
}
return zones, nil
}
if err := c.preprocessConfig(dc); err != 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
}
}
return records, 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-- {

View File

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

View File

@ -70,6 +70,7 @@ var features = providers.DocumentationNotes{
// ";" value with issue/issuewild records:
// https://www.digitalocean.com/docs/networking/dns/how-to/create-caa-records/
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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

@ -24,6 +24,7 @@ type route53Provider struct {
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

View File

@ -23,6 +23,7 @@ type SoftLayer struct {
var features = providers.DocumentationNotes{
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{}

View File

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