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:
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
207
commands/getZones.go
Normal 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)
|
||||
}
|
@ -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>
|
||||
|
@ -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
85
docs/get-zones.md
Normal 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.)
|
@ -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`
|
||||
|
||||
|
@ -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
1
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=
|
||||
|
@ -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
123
models/dnsrr.go
Normal 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))
|
||||
}
|
||||
}
|
@ -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{}
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
142
pkg/prettyzone/prettyzone.go
Normal file
142
pkg/prettyzone/prettyzone.go
Normal 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, " ")
|
||||
}
|
@ -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
194
pkg/prettyzone/sorting.go
Normal 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
|
||||
}
|
@ -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.
|
||||
|
@ -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) {
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
@ -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{}
|
||||
|
@ -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-- {
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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{}
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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() {
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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{}
|
||||
|
@ -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()
|
||||
|
Reference in New Issue
Block a user