mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-09-20 14:56:20 +08:00
OCTODNS: Remove octodns provider (#1910)
This commit is contained in:
parent
5c0801f4a8
commit
bec7e1e865
|
@ -35,7 +35,6 @@ import (
|
|||
_ "github.com/StackExchange/dnscontrol/v3/providers/netcup"
|
||||
_ "github.com/StackExchange/dnscontrol/v3/providers/netlify"
|
||||
_ "github.com/StackExchange/dnscontrol/v3/providers/ns1"
|
||||
_ "github.com/StackExchange/dnscontrol/v3/providers/octodns"
|
||||
_ "github.com/StackExchange/dnscontrol/v3/providers/opensrs"
|
||||
_ "github.com/StackExchange/dnscontrol/v3/providers/oracle"
|
||||
_ "github.com/StackExchange/dnscontrol/v3/providers/ovh"
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
# Testing:
|
||||
|
||||
## Create the environment
|
||||
|
||||
These variables are used in all other sections of this doc.
|
||||
|
||||
```bash
|
||||
export DNSCONFIGDIR=~/gitwork/fakeroot/ExternalDNS
|
||||
export OCTCONFIGDIR=~/gitwork/octodns/dns
|
||||
export SRCDIR=~/src/github.com/StackExchange/dnscontrol
|
||||
```
|
||||
|
||||
## Code tests
|
||||
|
||||
Unit tests:
|
||||
|
||||
```bash
|
||||
cd $SRCDIR/providers/octodns/octoyaml
|
||||
go test -v
|
||||
```
|
||||
|
||||
Integration tests:
|
||||
|
||||
```bash
|
||||
cd $SRCDIR/integrationTest
|
||||
go test -v -verbose -provider OCTODNS
|
||||
```
|
||||
|
||||
## Test against OctoDNS-Validate
|
||||
|
||||
### Download OctoDNS:
|
||||
|
||||
```bash
|
||||
cd $DNSCONFIGDIR
|
||||
mkdir dns
|
||||
cd dns
|
||||
virtualenv env
|
||||
source env/bin/activate
|
||||
pip install octodns
|
||||
ln -s ~/gitwork/fakeroot/ExternalDNS/config config
|
||||
```
|
||||
|
||||
### Modify dnsconfig.js
|
||||
|
||||
Make a copy of dnsconfig.js and modify it to use OCTODNS as a provider. We did it this way:
|
||||
|
||||
```bash
|
||||
cd $DNSCONFIGDIR/dns
|
||||
cp ../dnsconfig.js .
|
||||
cp ../creds.json .
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```js
|
||||
var OCT = NewDnsProvider("octodns", "OCTODNS");
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```diff
|
||||
DEFAULTS(
|
||||
DnsProvider(SERVERFAULT, 0),
|
||||
+ DnsProvider(OCT, 0),
|
||||
NAMESERVER_TTL("2d"),
|
||||
END);
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```diff
|
||||
var NO_BIND = function(d) {
|
||||
delete d.dnsProviders[SERVERFAULT];
|
||||
+ delete d.dnsProviders[OCT];
|
||||
};
|
||||
```
|
||||
|
||||
## Run the tests:
|
||||
|
||||
### Step 1: Generate the files
|
||||
|
||||
This builds the software then generates the yaml files in the config directory:
|
||||
|
||||
```bash
|
||||
(cd $SRCDIR && go install ) && cd $DNSCONFIGDIR/dns && rm -f config/*.yaml && dnscontrol push -providers=octodns
|
||||
```
|
||||
|
||||
### Step 2: Run the validator:
|
||||
|
||||
This runs octodns-validate against the YAMl files we generated. production.yaml should
|
||||
list each domain.
|
||||
|
||||
We create production.yaml like this:
|
||||
|
||||
```bash
|
||||
cd $DNSCONFIGDIR/dns && $SRCDIR/providers/octodns/mkprodyaml.sh
|
||||
```
|
||||
|
||||
Now we can run the validation:
|
||||
|
||||
```bash
|
||||
cd $DNSCONFIGDIR/dns
|
||||
cp $SRCDIR/providers/octodns/testdata/production.yaml config/. && env/bin/octodns-validate --log-stream-stdout
|
||||
```
|
|
@ -1,10 +0,0 @@
|
|||
package octodns
|
||||
|
||||
import "github.com/StackExchange/dnscontrol/v3/models"
|
||||
|
||||
// AuditRecords returns a list of errors corresponding to the records
|
||||
// that aren't supported by this provider. If all records are
|
||||
// supported, an empty list is returned.
|
||||
func AuditRecords(records []*models.RecordConfig) []error {
|
||||
return nil
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Read the YAML files in the config directory and generate a production.yaml file.
|
||||
|
||||
cat <<HERE >production.yaml
|
||||
---
|
||||
providers:
|
||||
config:
|
||||
class: octodns.provider.yaml.YamlProvider
|
||||
directory: ./config
|
||||
zones:
|
||||
HERE
|
||||
|
||||
( cd config && ls *.yaml ) |
|
||||
grep -v '^provider.yaml$' |
|
||||
sed 's/.\{4\}$//' |
|
||||
awk '{ print " " $1 ":" ; print " sources:" ; print " - config" }' >>production.yaml
|
|
@ -1,186 +0,0 @@
|
|||
package octodns
|
||||
|
||||
/*
|
||||
|
||||
octodns -
|
||||
Generate zonefiles suitiable for OctoDNS.
|
||||
|
||||
The zonefiles are read and written to the directory octoconfig
|
||||
|
||||
If the old octoconfig files are readable, we read them to determine
|
||||
if an update is actually needed.
|
||||
|
||||
The YAML input and output code is extremely complicated because
|
||||
the format does not fit well with a statically typed language.
|
||||
The YAML format changes drastically if the label has single
|
||||
or multiple rtypes associated with it, and if there is a single
|
||||
or multiple rtype data.
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
|
||||
"github.com/StackExchange/dnscontrol/v3/pkg/printer"
|
||||
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
|
||||
"github.com/StackExchange/dnscontrol/v3/providers"
|
||||
"github.com/StackExchange/dnscontrol/v3/providers/octodns/octoyaml"
|
||||
)
|
||||
|
||||
var features = providers.DocumentationNotes{
|
||||
providers.CanGetZones: providers.Unimplemented(),
|
||||
providers.CanUsePTR: providers.Can(),
|
||||
providers.CanUseSRV: 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."),
|
||||
}
|
||||
|
||||
func initProvider(config map[string]string, providermeta json.RawMessage) (providers.DNSServiceProvider, error) {
|
||||
// config -- the key/values from creds.json
|
||||
// meta -- the json blob from NewReq('name', 'TYPE', meta)
|
||||
api := &octodnsProvider{
|
||||
directory: config["directory"],
|
||||
}
|
||||
if api.directory == "" {
|
||||
api.directory = "config"
|
||||
}
|
||||
// Commented out because at this time api has no exported fields.
|
||||
// if len(providermeta) != 0 {
|
||||
// err := json.Unmarshal(providermeta, api)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// }
|
||||
return api, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
fns := providers.DspFuncs{
|
||||
Initializer: initProvider,
|
||||
RecordAuditor: AuditRecords,
|
||||
}
|
||||
providers.RegisterDomainServiceProviderType("OCTODNS", fns, features)
|
||||
}
|
||||
|
||||
// octodnsProvider is the provider handle for the OctoDNS driver.
|
||||
type octodnsProvider struct {
|
||||
//DefaultNS []string `json:"default_ns"`
|
||||
//DefaultSoa SoaInfo `json:"default_soa"`
|
||||
//nameservers []*models.Nameserver
|
||||
directory string
|
||||
}
|
||||
|
||||
// GetNameservers returns the nameservers for a domain.
|
||||
func (c *octodnsProvider) GetNameservers(string) ([]*models.Nameserver, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
||||
func (c *octodnsProvider) 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 *octodnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
dc.Punycode()
|
||||
// Phase 1: Copy everything to []*models.RecordConfig:
|
||||
// expectedRecords < dc.Records[i]
|
||||
// foundRecords < zonefile
|
||||
//
|
||||
// Phase 2: Do any manipulations:
|
||||
// add NS
|
||||
// manipulate SOA
|
||||
//
|
||||
// Phase 3: Convert to []diff.Records and compare:
|
||||
// expectedDiffRecords < expectedRecords
|
||||
// foundDiffRecords < foundRecords
|
||||
// diff.Inc...(foundDiffRecords, expectedDiffRecords )
|
||||
|
||||
// Read foundRecords:
|
||||
var foundRecords models.Records
|
||||
zoneFileFound := true
|
||||
zoneFileName := filepath.Join(c.directory, strings.Replace(strings.ToLower(dc.Name), "/", "_", -1)+".yaml")
|
||||
foundFH, err := os.Open(zoneFileName)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
zoneFileFound = false
|
||||
} else {
|
||||
return nil, fmt.Errorf("can't open %s: %w", zoneFileName, err)
|
||||
}
|
||||
} else {
|
||||
foundRecords, err = octoyaml.ReadYaml(foundFH, dc.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can not get corrections: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize
|
||||
models.PostProcessRecords(foundRecords)
|
||||
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
|
||||
|
||||
differ := diff.New(dc)
|
||||
_, create, del, mod, err := differ.IncrementalDiff(foundRecords)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
// Print a list of changes. Generate an actual change that is the zone
|
||||
changes := false
|
||||
for _, i := range create {
|
||||
changes = true
|
||||
fmt.Fprintln(buf, i)
|
||||
}
|
||||
for _, i := range del {
|
||||
changes = true
|
||||
fmt.Fprintln(buf, i)
|
||||
}
|
||||
for _, i := range mod {
|
||||
changes = true
|
||||
fmt.Fprintln(buf, i)
|
||||
}
|
||||
msg := fmt.Sprintf("GENERATE_CONFIGFILE: %s", dc.Name)
|
||||
if zoneFileFound {
|
||||
msg += "\n"
|
||||
msg += buf.String()
|
||||
} else {
|
||||
msg += fmt.Sprintf(" (%d records)\n", len(create))
|
||||
}
|
||||
corrections := []*models.Correction{}
|
||||
if changes {
|
||||
corrections = append(corrections,
|
||||
&models.Correction{
|
||||
Msg: msg,
|
||||
F: func() error {
|
||||
printer.Printf("CREATING CONFIGFILE: %v\n", zoneFileName)
|
||||
zf, err := os.Create(zoneFileName)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not create zonefile: %v", err)
|
||||
}
|
||||
//err = WriteZoneFile(zf, dc.Records, dc.Name)
|
||||
err = octoyaml.WriteYaml(zf, dc.Records, dc.Name)
|
||||
if err != nil {
|
||||
log.Fatalf("WriteZoneFile error: %v\n", err)
|
||||
}
|
||||
err = zf.Close()
|
||||
if err != nil {
|
||||
log.Fatalf("Closing: %v", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return corrections, nil
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
package octoyaml
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"github.com/StackExchange/dnscontrol/v3/pkg/printer"
|
||||
"github.com/StackExchange/dnscontrol/v3/pkg/transform"
|
||||
"github.com/robertkrimen/otto"
|
||||
_ "github.com/robertkrimen/otto/underscore" // required by otto
|
||||
)
|
||||
|
||||
// ExecuteJavascript accepts a javascript string and runs it, returning the resulting dnsConfig.
|
||||
func ExecuteJavascript(script string, devMode bool) (*models.DNSConfig, error) {
|
||||
vm := otto.New()
|
||||
|
||||
vm.Set("require", require)
|
||||
vm.Set("REV", reverse)
|
||||
|
||||
helperJs := GetHelpers(true)
|
||||
// run helper script to prime vm and initialize variables
|
||||
if _, err := vm.Run(helperJs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// run user script
|
||||
if _, err := vm.Run(script); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// export conf as string and unmarshal
|
||||
value, err := vm.Run(`JSON.stringify(conf)`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
str, err := value.ToString()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conf := &models.DNSConfig{}
|
||||
if err = json.Unmarshal([]byte(str), conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// GetHelpers returns the filename of helpers.js, or the esc'ed version.
|
||||
func GetHelpers(devMode bool) string {
|
||||
d, err := os.ReadFile("../pkg/js/helpers.js")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(d)
|
||||
}
|
||||
|
||||
func require(call otto.FunctionCall) otto.Value {
|
||||
if len(call.ArgumentList) != 1 {
|
||||
throw(call.Otto, "require takes exactly one argument")
|
||||
}
|
||||
file := call.Argument(0).String()
|
||||
printer.Printf("requiring: %s\n", file)
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
throw(call.Otto, err.Error())
|
||||
}
|
||||
_, err = call.Otto.Run(string(data))
|
||||
if err != nil {
|
||||
throw(call.Otto, err.Error())
|
||||
}
|
||||
return otto.TrueValue()
|
||||
}
|
||||
|
||||
func throw(vm *otto.Otto, str string) {
|
||||
panic(vm.MakeCustomError("Error", str))
|
||||
}
|
||||
|
||||
func reverse(call otto.FunctionCall) otto.Value {
|
||||
if len(call.ArgumentList) != 1 {
|
||||
throw(call.Otto, "REV takes exactly one argument")
|
||||
}
|
||||
dom := call.Argument(0).String()
|
||||
rev, err := transform.ReverseDomainName(dom)
|
||||
if err != nil {
|
||||
throw(call.Otto, err.Error())
|
||||
}
|
||||
v, _ := otto.ToValue(rev)
|
||||
return v
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
var REG = NewRegistrar("Third-Party","NONE");
|
||||
var CF = NewDnsProvider("bind", "BIND")
|
||||
D("example.tld",REG,DnsProvider(CF),
|
||||
DefaultTTL(301),
|
||||
A("foo","1.2.3.4", TTL(301))
|
||||
);
|
|
@ -1,8 +0,0 @@
|
|||
[
|
||||
{
|
||||
"type":"A",
|
||||
"name":"foo",
|
||||
"target":"1.2.3.4",
|
||||
"ttl":301
|
||||
}
|
||||
]
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
foo:
|
||||
ttl: 301
|
||||
type: A
|
||||
value: 1.2.3.4
|
|
@ -1,7 +0,0 @@
|
|||
var REG = NewRegistrar("Third-Party","NONE");
|
||||
var CF = NewDnsProvider("bind", "BIND")
|
||||
D("example.tld",REG,DnsProvider(CF),
|
||||
DefaultTTL(302),
|
||||
A("foo","1.2.3.4"),
|
||||
A("foo","1.2.3.5")
|
||||
);
|
|
@ -1,4 +0,0 @@
|
|||
[
|
||||
{"type":"A","name":"foo","target":"1.2.3.4","ttl":302},
|
||||
{"type":"A","name":"foo","target":"1.2.3.5","ttl":302}
|
||||
]
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
foo:
|
||||
ttl: 302
|
||||
type: A
|
||||
values:
|
||||
- 1.2.3.4
|
||||
- 1.2.3.5
|
|
@ -1,10 +0,0 @@
|
|||
var REG = NewRegistrar("Third-Party","NONE");
|
||||
var CF = NewDnsProvider("bind", "BIND")
|
||||
D("example.tld",REG,DnsProvider(CF),
|
||||
DefaultTTL(303),
|
||||
A("one","1.2.3.3"),
|
||||
A("foo","1.2.3.4"),
|
||||
A("foo","1.2.3.5"),
|
||||
MX("foo", 10, "mx1.example.com."),
|
||||
MX("foo", 10, "mx2.example.com.")
|
||||
);
|
|
@ -1,7 +0,0 @@
|
|||
[
|
||||
{"type":"A","name":"foo","target":"1.2.3.4","ttl":303},
|
||||
{"type":"A","name":"foo","target":"1.2.3.5","ttl":303},
|
||||
{"type":"MX","name":"foo","target":"mx1.example.com.","ttl":303,"mxpreference":10},
|
||||
{"type":"MX","name":"foo","target":"mx2.example.com.","ttl":303,"mxpreference":10},
|
||||
{"type":"A","name":"one","target":"1.2.3.3","ttl":303}
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
---
|
||||
foo:
|
||||
- ttl: 303
|
||||
type: A
|
||||
values:
|
||||
- 1.2.3.4
|
||||
- 1.2.3.5
|
||||
- ttl: 303
|
||||
type: MX
|
||||
values:
|
||||
- priority: 10
|
||||
value: mx1.example.com.
|
||||
- priority: 10
|
||||
value: mx2.example.com.
|
||||
one:
|
||||
ttl: 303
|
||||
type: A
|
||||
value: 1.2.3.3
|
|
@ -1,9 +0,0 @@
|
|||
[
|
||||
{"type":"CNAME","name":"*","target":"www.example.com."},
|
||||
{"type":"A","name":"@","target":"1.2.3.4","ttl":304},
|
||||
{"type":"A","name":"@","target":"1.2.3.5","ttl":304},
|
||||
{"type":"A","name":"www","target":"1.2.3.4","ttl":304},
|
||||
{"type":"A","name":"www","target":"1.2.3.5","ttl":304},
|
||||
{"type":"A","name":"www.sub","target":"1.2.3.6","ttl":304},
|
||||
{"type":"A","name":"www.sub","target":"1.2.3.7","ttl":304}
|
||||
]
|
|
@ -1,22 +0,0 @@
|
|||
---
|
||||
'':
|
||||
type: A
|
||||
ttl: 304
|
||||
values:
|
||||
- 1.2.3.4
|
||||
- 1.2.3.5
|
||||
'*':
|
||||
type: CNAME
|
||||
value: www.example.com.
|
||||
www:
|
||||
type: A
|
||||
ttl: 304
|
||||
values:
|
||||
- 1.2.3.4
|
||||
- 1.2.3.5
|
||||
www.sub:
|
||||
type: A
|
||||
ttl: 304
|
||||
values:
|
||||
- 1.2.3.6
|
||||
- 1.2.3.7
|
|
@ -1,6 +0,0 @@
|
|||
[
|
||||
{"type":"A","name":"@","target":"1.2.3.4","ttl":305},
|
||||
{"type":"A","name":"@","target":"1.2.3.5","ttl":305},
|
||||
{"type":"MX","name":"@","target":"mx1.example.com.","ttl":305,"mxpreference":10},
|
||||
{"type":"MX","name":"@","target":"mx2.example.com.","ttl":305,"mxpreference":10}
|
||||
]
|
|
@ -1,14 +0,0 @@
|
|||
---
|
||||
'':
|
||||
- type: A
|
||||
ttl: 305
|
||||
values:
|
||||
- 1.2.3.4
|
||||
- 1.2.3.5
|
||||
- type: MX
|
||||
ttl: 305
|
||||
values:
|
||||
- priority: 10
|
||||
value: mx1.example.com.
|
||||
- priority: 10
|
||||
value: mx2.example.com.
|
|
@ -1,9 +0,0 @@
|
|||
[
|
||||
{
|
||||
"type":"A",
|
||||
"name":"@",
|
||||
"target":"1.2.3.4",
|
||||
"ttl":306
|
||||
}
|
||||
]
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
'':
|
||||
type: A
|
||||
ttl: 306
|
||||
value: 1.2.3.4
|
|
@ -1,12 +0,0 @@
|
|||
// FYI: go-yaml writes an empty string as ""; python's yaml writes an empty string as "".
|
||||
// For that reason:
|
||||
// 006-apex: tests YAML -> JSON.
|
||||
// 007-apex-quote: tests JS -> YAML -> JSON
|
||||
// It would be nice if go-yaml has an option to write '' instead of "".
|
||||
|
||||
var REG = NewRegistrar("Third-Party","NONE");
|
||||
var CF = NewDnsProvider("bind", "BIND")
|
||||
D("example.tld",REG,DnsProvider(CF),
|
||||
DefaultTTL(307),
|
||||
A("@","1.2.3.4")
|
||||
);
|
|
@ -1,8 +0,0 @@
|
|||
[
|
||||
{
|
||||
"type":"A",
|
||||
"name":"@",
|
||||
"target":"1.2.3.4",
|
||||
"ttl":307
|
||||
}
|
||||
]
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"":
|
||||
ttl: 307
|
||||
type: A
|
||||
value: 1.2.3.4
|
|
@ -1,269 +0,0 @@
|
|||
package octoyaml
|
||||
|
||||
/*
|
||||
This module handles reading OctoDNS yaml files. Sadly the YAML files
|
||||
are so entirely flexible that parsing them is a nighmare. We UnMarshalYAML
|
||||
them into a slice of interfaces mapped to interfaces, then use reflection
|
||||
to walk the tree, interpreting what we find along the way. As we collect
|
||||
data we output models.RecordConfig objects.
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// ReadYaml parses a yaml input and returns a list of RecordConfigs
|
||||
func ReadYaml(r io.Reader, origin string) (models.Records, error) {
|
||||
results := models.Records{}
|
||||
|
||||
// Slurp the YAML into a string.
|
||||
ydata, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can not read yaml filehandle: %w", err)
|
||||
}
|
||||
|
||||
// Unmarshal the mystery data into a structure we can relect into.
|
||||
var mysterydata map[string]interface{}
|
||||
err = yaml.Unmarshal(ydata, &mysterydata)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not unmarshal yaml: %w", err)
|
||||
}
|
||||
//printer.Printf("ReadYaml: mysterydata == %v\n", mysterydata)
|
||||
|
||||
// Traverse every key/value pair.
|
||||
for k, v := range mysterydata { // Each label
|
||||
// k, v: k is the label, v is everything we know about the label.
|
||||
// In other code, k1, v2 refers to one level deeper, k3, k3 refers to
|
||||
// one more level deeper, and so on.
|
||||
//printer.Printf("ReadYaml: NEXT KEY\n")
|
||||
//printer.Printf("ReadYaml: KEY=%s v.(type)=%s\n", k, reflect.TypeOf(v).String())
|
||||
switch v.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
// The value is itself a map. This means we have a label with
|
||||
// with one or more records, each of them are all the same rtype.
|
||||
// parseLeaf will handle both of these forms:
|
||||
// For example, this:
|
||||
// 'www':
|
||||
// type: A
|
||||
// values:
|
||||
// - 1.2.3.4
|
||||
// - 1.2.3.5
|
||||
// or
|
||||
// 'www':
|
||||
// type: CNAME
|
||||
// value: foo.example.com.
|
||||
results, err = parseLeaf(results, k, v, origin)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("leaf (%v) error: %w", v, err)
|
||||
}
|
||||
case []interface{}:
|
||||
// The value is a list. This means we have a label with
|
||||
// multiple records, each of them may be different rtypes.
|
||||
// We need to call parseLeaf() once for each rtype.
|
||||
// For example, this:
|
||||
// 'www':
|
||||
// - type: A
|
||||
// values:
|
||||
// - 1.2.3.4
|
||||
// - 1.2.3.5
|
||||
// - type: MX
|
||||
// values:
|
||||
// - priority: 10
|
||||
// value: mx1.example.com.
|
||||
// - priority: 10
|
||||
// value: mx2.example.com.
|
||||
for i, v3 := range v.([]interface{}) { // All the label's list
|
||||
_ = i
|
||||
//printer.Printf("ReadYaml: list key=%s i=%d v3.(type)=%s\n", k, i, typeof(v3))
|
||||
switch v3.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
//printer.Printf("ReadYaml: v3=%v\n", v3)
|
||||
results, err = parseLeaf(results, k, v3, origin)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("leaf v3=%v: %w", v3, err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown type in list3: k=%s v.(type)=%T v=%v", k, v, v)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown type in list1: k=%s v.(type)=%T v=%v", k, v, v)
|
||||
}
|
||||
}
|
||||
|
||||
sortRecs(results, origin)
|
||||
//printer.Printf("ReadYaml: RESULTS=%v\n", results)
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func parseLeaf(results models.Records, k string, v interface{}, origin string) (models.Records, error) {
|
||||
var rType, rTarget string
|
||||
var rTTL uint32
|
||||
rTargets := []string{}
|
||||
var someresults models.Records
|
||||
for k2, v2 := range v.(map[interface{}]interface{}) { // All the label's items
|
||||
// printer.Printf("ReadYaml: ifs tk2=%s tv2=%s len(rTargets)=%d\n", typeof(k2), typeof(v2), len(rTargets))
|
||||
if typeof(k2) == "string" && (typeof(v2) == "string" || typeof(v2) == "int") {
|
||||
// The 2nd level key is a string, and the 2nd level value is a string or int.
|
||||
// Here are 3 examples:
|
||||
// type: CNAME
|
||||
// value: foo.example.com.
|
||||
// ttl: 3
|
||||
//printer.Printf("parseLeaf: k2=%s v2=%v\n", k2, v2)
|
||||
switch k2.(string) {
|
||||
case "type":
|
||||
rType = v2.(string)
|
||||
case "ttl":
|
||||
var err error
|
||||
rTTL, err = decodeTTL(v2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parseLeaf: can not parse ttl (%v)", v2)
|
||||
}
|
||||
case "value":
|
||||
rTarget = v2.(string)
|
||||
case "values":
|
||||
switch v2.(type) {
|
||||
case string:
|
||||
rTarget = v2.(string)
|
||||
default:
|
||||
return nil, fmt.Errorf("parseLeaf: unknown type in values: rtpe=%s k=%s k2=%s v2.(type)=%T v2=%v", rType, k, k2, v2, v2)
|
||||
}
|
||||
default:
|
||||
panic("Should not happen")
|
||||
}
|
||||
} else if typeof(k2) == "string" && typeof(v2) == "[]interface {}" {
|
||||
// The 2nd level key is a string, and the 2nd level value is a list.
|
||||
someresults = nil
|
||||
for _, v3 := range v2.([]interface{}) {
|
||||
switch v3.(type) {
|
||||
case string:
|
||||
// Example:
|
||||
// values:
|
||||
// - 1.2.3.1
|
||||
// - 1.2.3.2
|
||||
// - 1.2.3.3
|
||||
// We collect all the values for later, when we'll need to generate
|
||||
// one RecordConfig for each value.
|
||||
//printer.Printf("parseLeaf: s-append %s\n", v3.(string))
|
||||
rTargets = append(rTargets, v3.(string))
|
||||
case map[interface{}]interface{}:
|
||||
// Example:
|
||||
// values:
|
||||
// - priority: 10
|
||||
// value: mx1.example.com.
|
||||
// - priority: 10
|
||||
// value: mx2.example.com.
|
||||
// We collect the individual values. When we are done with this level,
|
||||
// we should have enough to generate a single RecordConfig.
|
||||
newRc := newRecordConfig(k, rType, "", rTTL, origin)
|
||||
for k4, v4 := range v3.(map[interface{}]interface{}) {
|
||||
//printer.Printf("parseLeaf: k4=%s v4=%s\n", k4, v4)
|
||||
switch k4.(string) {
|
||||
case "priority": // MX,SRV
|
||||
priority := uint16(v4.(int))
|
||||
newRc.MxPreference = priority
|
||||
newRc.SrvPriority = priority
|
||||
// Assign it to both places. We'll zap the wrong one later.
|
||||
case "weight": // SRV
|
||||
newRc.SrvWeight = uint16(v4.(int))
|
||||
case "port": // SRV
|
||||
newRc.SrvPort = uint16(v4.(int))
|
||||
case "value": // MX
|
||||
newRc.SetTarget(v4.(string))
|
||||
}
|
||||
}
|
||||
//printer.Printf("parseLeaf: append %v\n", newRc)
|
||||
someresults = append(someresults, newRc)
|
||||
default:
|
||||
return nil, fmt.Errorf("parseLeaf: unknown type in map: rtype=%s k=%s v3.(type)=%T v3=%v", rType, k, v3, v3)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("parseLeaf: unknown type in level 2: k=%s k2=%s v.2(type)=%T v2=%v", k, k2, v2, v2)
|
||||
}
|
||||
}
|
||||
// printer.Printf("parseLeaf: Target=(%v)\n", rTarget)
|
||||
// printer.Printf("parseLeaf: len(rTargets)=%d\n", len(rTargets))
|
||||
// printer.Printf("parseLeaf: len(someresults)=%d\n", len(someresults))
|
||||
|
||||
// We've now looped through everything about one label. Make the RecordConfig(s).
|
||||
|
||||
if len(someresults) > 0 {
|
||||
// We have many results. Generate a RecordConfig for each one.
|
||||
for _, r := range someresults {
|
||||
r.Type = rType
|
||||
r.TTL = rTTL
|
||||
results = append(results, r)
|
||||
// Earlier we didn't know what the priority was for. Now that we know the rType,
|
||||
// we zap the wrong one.
|
||||
switch r.Type {
|
||||
case "MX":
|
||||
r.SrvPriority = 0
|
||||
case "SRV":
|
||||
r.MxPreference = 0
|
||||
default:
|
||||
panic("ugh")
|
||||
}
|
||||
}
|
||||
} else if rTarget != "" && len(rTargets) == 0 {
|
||||
// The file used "value". Generate a single RecordConfig
|
||||
//printer.Printf("parseLeaf: 1-newRecordConfig(%v, %v, %v, %v, %v)\n", k, rType, rTarget, rTTL, origin)
|
||||
results = append(results, newRecordConfig(k, rType, rTarget, rTTL, origin))
|
||||
} else {
|
||||
// The file used "values" so now we must generate a RecordConfig for each value.
|
||||
for _, target := range rTargets {
|
||||
//printer.Printf("parseLeaf: 3-newRecordConfig(%v, %v, %v, %v, %v)\n", k, rType, target, rTTL, origin)
|
||||
results = append(results, newRecordConfig(k, rType, target, rTTL, origin))
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// newRecordConfig is a RecordConfig factory.
|
||||
func newRecordConfig(rname, rtype, target string, ttl uint32, origin string) *models.RecordConfig {
|
||||
rc := &models.RecordConfig{
|
||||
Type: rtype,
|
||||
TTL: ttl,
|
||||
}
|
||||
rc.SetLabel(rname, origin)
|
||||
switch rtype {
|
||||
case "SPF", "TXT":
|
||||
rc.SetTargetTXT(target)
|
||||
default:
|
||||
rc.SetTarget(target)
|
||||
}
|
||||
return rc
|
||||
}
|
||||
|
||||
// typeof returns a string that indicates v's type:
|
||||
func typeof(v interface{}) string {
|
||||
// Cite: https://stackoverflow.com/a/20170555/71978
|
||||
return reflect.TypeOf(v).String()
|
||||
}
|
||||
|
||||
// decodeTTL decodes an interface into a TTL value.
|
||||
// This is useful when you don't know if a TTL arrived as a string or int.
|
||||
func decodeTTL(ttl interface{}) (uint32, error) {
|
||||
switch ttl.(type) {
|
||||
case uint32:
|
||||
return ttl.(uint32), nil
|
||||
case string:
|
||||
s := ttl.(string)
|
||||
t, err := strconv.ParseUint(s, 10, 32)
|
||||
return uint32(t), fmt.Errorf("decodeTTL failed to parse (%s): %w", s, err)
|
||||
case int:
|
||||
i := ttl.(int)
|
||||
if i < 0 {
|
||||
return 0, fmt.Errorf("ttl cannot be negative (%d)", i)
|
||||
}
|
||||
return uint32(i), nil
|
||||
}
|
||||
panic("I don't know what type this TTL is")
|
||||
}
|
|
@ -1,186 +0,0 @@
|
|||
package octoyaml
|
||||
|
||||
/*
|
||||
|
||||
Test
|
||||
DSL -> yaml
|
||||
YAML -> JSON
|
||||
|
||||
001-test.js
|
||||
001-test.yaml
|
||||
001-test.json
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
testifyrequire "github.com/stretchr/testify/require"
|
||||
"github.com/tdewolff/minify/v2"
|
||||
minjson "github.com/tdewolff/minify/v2/json"
|
||||
)
|
||||
|
||||
const (
|
||||
testDir = "octodns/octoyaml/parse_tests"
|
||||
errorDir = "octodns/octoyaml/error_tests"
|
||||
)
|
||||
|
||||
func init() {
|
||||
os.Chdir("../..") // go up a directory so we helpers.js is in a consistent place.
|
||||
}
|
||||
|
||||
func TestYamlWrite(t *testing.T) {
|
||||
|
||||
// Read a .JS and make sure we can generate the expected YAML.
|
||||
|
||||
files, err := os.ReadDir(testDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, f := range files {
|
||||
|
||||
// Run JS -> conf
|
||||
|
||||
// run all js files that start with a number. Skip others.
|
||||
if filepath.Ext(f.Name()) != ".js" || !unicode.IsNumber(rune(f.Name()[0])) {
|
||||
continue
|
||||
}
|
||||
|
||||
m := minify.New()
|
||||
m.AddFunc("json", minjson.Minify)
|
||||
|
||||
t.Run(f.Name(), func(t *testing.T) {
|
||||
fname := filepath.Join(testDir, f.Name())
|
||||
fmt.Printf("Filename: %v\n", fname)
|
||||
content, err := os.ReadFile(fname)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conf, err := ExecuteJavascript(string(content), true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
basename := f.Name()[:len(f.Name())-3] // Remove ".js"
|
||||
|
||||
// Run conf -> YAML
|
||||
|
||||
actualYAML := bytes.NewBuffer([]byte{})
|
||||
dom := conf.FindDomain("example.tld")
|
||||
if dom == nil {
|
||||
panic(fmt.Sprintf("FILE %s does not mention domain '%s'", f.Name(), "example.tld"))
|
||||
}
|
||||
|
||||
err = WriteYaml(actualYAML, conf.FindDomain("example.tld").Records, "example.tld")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Read expected YAML
|
||||
|
||||
expectedFile := filepath.Join(testDir, basename+".yaml")
|
||||
expectedData, err := os.ReadFile(expectedFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expectedYAML := expectedData
|
||||
|
||||
// Compare YAML and expectedData
|
||||
|
||||
if string(expectedYAML) != actualYAML.String() {
|
||||
t.Error("Expected and actual YAML don't match")
|
||||
t.Log("Expected:", string(expectedYAML))
|
||||
t.Log("Actual :", actualYAML.String())
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestYamlRead(t *testing.T) {
|
||||
|
||||
// Read a .YAML and make sure it matches the RecordConfig (.JSON).
|
||||
|
||||
minifyFlag := true
|
||||
|
||||
files, err := os.ReadDir(testDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, f := range files {
|
||||
// run all yaml files that start with a number. Skip others.
|
||||
if filepath.Ext(f.Name()) != ".yaml" || !unicode.IsNumber(rune(f.Name()[0])) {
|
||||
continue
|
||||
}
|
||||
basename := f.Name()[:len(f.Name())-5] // remove ".yaml"
|
||||
|
||||
m := minify.New()
|
||||
m.AddFunc("json", minjson.Minify)
|
||||
|
||||
t.Run(f.Name(), func(t *testing.T) {
|
||||
|
||||
// Parse YAML
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(testDir, f.Name()))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
content = nil
|
||||
} else {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
recs, err := ReadYaml(bytes.NewBufferString(string(content)), "example.tld")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
//fmt.Printf("DEBUG: CONTENT=%s\n", string(content))
|
||||
//fmt.Printf("DEBUG: RECS=%v\n", recs)
|
||||
|
||||
// YAML -> JSON
|
||||
|
||||
actualJSON, err := json.MarshalIndent(recs, "", " ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if minifyFlag {
|
||||
// fmt.Printf("DEBUG: actualJSON-full: %s\n", actualJSON)
|
||||
actualJSON, err = m.Bytes("json", actualJSON)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// fmt.Printf("DEBUG: actualJSON-mini: %s\n", actualJSON)
|
||||
|
||||
// Read expected JSON
|
||||
expectedFile := filepath.Join(testDir, basename+".json")
|
||||
expectedData, err := os.ReadFile(expectedFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
fmt.Println("SKIPPING")
|
||||
t.Log("Skipping (no .json)")
|
||||
return
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
var expectedJSON string
|
||||
if minifyFlag {
|
||||
expectedJSON, err = m.String("json", string(expectedData))
|
||||
} else {
|
||||
expectedJSON = string(expectedData)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
//fmt.Printf("DEBUG: EXPECTED=%s\n", string(expectedJSON))
|
||||
//fmt.Printf("DEBUG: ACTUAL =%s\n", string(actualJSON))
|
||||
|
||||
es, as := string(expectedJSON), string(actualJSON)
|
||||
testifyrequire.JSONEqf(t, es, as, "EXPECTING %q = \n```\n%s\n```", expectedFile, as)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,136 +0,0 @@
|
|||
package octoyaml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sort"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"github.com/StackExchange/dnscontrol/v3/pkg/natsort"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
)
|
||||
|
||||
type genYamlData struct {
|
||||
Origin string
|
||||
DefaultTTL uint32
|
||||
Records models.Records
|
||||
}
|
||||
|
||||
func sortRecs(recs models.Records, origin string) {
|
||||
z := &genYamlData{
|
||||
Origin: dnsutil.AddOrigin(origin, "."),
|
||||
Records: recs,
|
||||
}
|
||||
sort.Sort(z)
|
||||
}
|
||||
|
||||
func (z *genYamlData) Len() int { return len(z.Records) }
|
||||
func (z *genYamlData) Swap(i, j int) { z.Records[i], z.Records[j] = z.Records[j], z.Records[i] }
|
||||
func (z *genYamlData) Less(i, j int) bool {
|
||||
a, b := z.Records[i], z.Records[j]
|
||||
compA, compB := a.GetLabel(), b.GetLabel()
|
||||
if compA != compB {
|
||||
if compA == z.Origin+"." {
|
||||
compA = "@"
|
||||
}
|
||||
if compB == z.Origin+"." {
|
||||
compB = "@"
|
||||
}
|
||||
return zoneLabelLess(compA, compB)
|
||||
}
|
||||
rrtypeA, rrtypeB := a.Type, b.Type
|
||||
if rrtypeA != rrtypeB {
|
||||
return zoneRrtypeLess(rrtypeA, rrtypeB)
|
||||
}
|
||||
switch rrtypeA { // #rtype_variations
|
||||
case "NS", "TXT", "TLSA":
|
||||
// pass through.
|
||||
case "A":
|
||||
ta2, tb2 := net.ParseIP(a.GetTargetField()), net.ParseIP(b.GetTargetField())
|
||||
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 := net.ParseIP(a.GetTargetField()), net.ParseIP(b.GetTargetField())
|
||||
ipa, ipb := ta2.To16(), tb2.To16()
|
||||
return bytes.Compare(ipa, ipb) == -1
|
||||
case "MX":
|
||||
pa, pb := a.MxPreference, b.MxPreference
|
||||
return pa < pb
|
||||
case "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, a.SrvWeight
|
||||
if pa != pb {
|
||||
return pa < pb
|
||||
}
|
||||
case "PTR":
|
||||
pa, pb := a.GetTargetField(), b.GetTargetField()
|
||||
if pa != pb {
|
||||
return pa < pb
|
||||
}
|
||||
case "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:
|
||||
panic(fmt.Sprintf("genYamlData Less: unimplemented rtype %v", a.Type))
|
||||
// We panic so that we quickly find any switch statements
|
||||
// that have not been updated for a new RR type.
|
||||
}
|
||||
return a.GetTargetSortable() < b.GetTargetSortable()
|
||||
}
|
||||
|
||||
func zoneLabelLess(a, b string) bool {
|
||||
return natsort.Less(a, b)
|
||||
// octodns-validate wants a "natural sort" (i.e. foo10 comes after foo3).
|
||||
// We emulate this with the natsort package.
|
||||
// If you need to disable that validatation:
|
||||
// Edit env/lib/python2.7/site-packages/octodns/yaml.py
|
||||
// Change line 27: OLD: if key != expected
|
||||
// NEW: if False and key != expected
|
||||
}
|
||||
|
||||
func zoneRrtypeLess(a, b string) 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 == "SOA" {
|
||||
return true
|
||||
}
|
||||
if b == "SOA" {
|
||||
return false
|
||||
}
|
||||
if a == "NS" {
|
||||
return true
|
||||
}
|
||||
if b == "NS" {
|
||||
return false
|
||||
}
|
||||
return a < b
|
||||
}
|
|
@ -1,321 +0,0 @@
|
|||
package octoyaml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// WriteYaml outputs a yaml version of a list of RecordConfig.
|
||||
func WriteYaml(w io.Writer, records models.Records, origin string) error {
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pick the most common TTL as the default so we can
|
||||
// write the fewest "ttl:" lines.
|
||||
defaultTTL := mostCommonTTL(records)
|
||||
|
||||
// Make a copy of the records, since we want to sort and muck with them.
|
||||
recsCopy := models.Records{}
|
||||
recsCopy = append(recsCopy, records...)
|
||||
for _, r := range recsCopy {
|
||||
if r.GetLabel() == "@" {
|
||||
//r.Name = ""
|
||||
r.UnsafeSetLabelNull()
|
||||
}
|
||||
}
|
||||
|
||||
z := &genYamlData{
|
||||
Origin: dnsutil.AddOrigin(origin, "."),
|
||||
DefaultTTL: defaultTTL,
|
||||
Records: recsCopy,
|
||||
}
|
||||
|
||||
// Sort in the weird order that OctoDNS expects:
|
||||
sort.Sort(z)
|
||||
|
||||
// Generate the YAML:
|
||||
fmt.Fprintln(w, "---")
|
||||
yb, err := yaml.Marshal(z.genInterfaceList(w))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(yb)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// genInterfaceList outputs YAML ordered slices for the entire zone.
|
||||
// Each item in the list is an interface that will MarshallYAML to
|
||||
// the desired output.
|
||||
func (z *genYamlData) genInterfaceList(w io.Writer) yaml.MapSlice {
|
||||
var yam yaml.MapSlice
|
||||
// Group the records by label.
|
||||
order, groups := z.Records.GroupedByLabel()
|
||||
// For each group, generate the YAML.
|
||||
for _, label := range order {
|
||||
group := groups[label]
|
||||
// Within the group, sort the similar Types together:
|
||||
sort.SliceStable(group, func(i, j int) bool { return zoneRrtypeLess(group[i].Type, group[j].Type) })
|
||||
// Generate the YAML records:
|
||||
yam = append(yam, oneLabel(group))
|
||||
}
|
||||
return yam
|
||||
}
|
||||
|
||||
// "simple" records are when a label has a single rtype.
|
||||
// It may have a single (simple) or multiple (many) values.
|
||||
|
||||
// Used to generate:
|
||||
//
|
||||
// label:
|
||||
// type: A
|
||||
// value: 1.2.3.4
|
||||
type simple struct {
|
||||
TTL uint32 `yaml:"ttl,omitempty"`
|
||||
Type string `yaml:"type"`
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
|
||||
// Used to generate:
|
||||
//
|
||||
// label:
|
||||
// type: A
|
||||
// values:
|
||||
// - 1.2.3.4
|
||||
// - 2.3.4.5
|
||||
type many struct {
|
||||
TTL uint32 `yaml:"ttl,omitempty"`
|
||||
Type string `yaml:"type"`
|
||||
Values []string `yaml:"values"`
|
||||
}
|
||||
|
||||
// complexItems are when a single label has multiple rtypes
|
||||
// associated with it. For example, a label with both an A and MX record.
|
||||
type complexItems []interface{}
|
||||
|
||||
// Used to generate a complex item with either a single value or multiple values:
|
||||
// 'thing': >> complexVals
|
||||
// - type: CNAME
|
||||
// value: newplace.example.com. << value
|
||||
//
|
||||
// 'www':
|
||||
// - type: A
|
||||
// values:
|
||||
// - 1.2.3.4 << values
|
||||
// - 1.2.3.5 << values
|
||||
// - type: MX
|
||||
// values:
|
||||
// - priority: 10 << fields
|
||||
// value: mx1.example.com. << fields
|
||||
// - priority: 10 << fields
|
||||
// value: mx2.example.com. << fields
|
||||
type complexVals struct {
|
||||
TTL uint32 `yaml:"ttl,omitempty"`
|
||||
Type string `yaml:"type"`
|
||||
Value string `yaml:"value,omitempty"`
|
||||
Values []string `yaml:"values,omitempty"`
|
||||
}
|
||||
|
||||
// Used to generate rtypes like MX rand SRV ecords, which have multiple
|
||||
// fields within the rtype.
|
||||
type complexFields struct {
|
||||
TTL uint32 `yaml:"ttl,omitempty"`
|
||||
Type string `yaml:"type"`
|
||||
Fields []fields `yaml:"values,omitempty"`
|
||||
}
|
||||
|
||||
// Used to generate the fields themselves:
|
||||
type fields struct {
|
||||
Priority uint16 `yaml:"priority,omitempty"`
|
||||
SrvWeight uint16 `yaml:"weight,omitempty"`
|
||||
SrvPort uint16 `yaml:"port,omitempty"`
|
||||
Value string `yaml:"value,omitempty"`
|
||||
}
|
||||
|
||||
// FIXME(tlim): An MX record with .Priority=0 will not output the priority.
|
||||
|
||||
// sameType returns true if all records have the same type.
|
||||
func sameType(records models.Records) bool {
|
||||
t := records[0].Type
|
||||
for _, r := range records {
|
||||
if r.Type != t {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// oneLabel handles all the DNS records associated with a single label.
|
||||
// It dispatches the right code whether the label is simple, many, or complex.
|
||||
func oneLabel(records models.Records) yaml.MapItem {
|
||||
item := yaml.MapItem{
|
||||
// a yaml.MapItem is a YAML map that retains the key order.
|
||||
Key: records[0].GetLabel(),
|
||||
}
|
||||
// Special case labels with a single record:
|
||||
if len(records) == 1 {
|
||||
switch rtype := records[0].Type; rtype {
|
||||
case "A", "CNAME", "NS", "PTR", "TXT":
|
||||
v := simple{
|
||||
Type: rtype,
|
||||
Value: records[0].GetTargetField(),
|
||||
TTL: records[0].TTL,
|
||||
}
|
||||
if v.Type == "TXT" {
|
||||
v.Value = strings.Replace(models.StripQuotes(v.Value), `;`, `\;`, -1)
|
||||
}
|
||||
//printer.Printf("yamlwrite:oneLabel: simple ttl=%d\n", v.TTL)
|
||||
item.Value = v
|
||||
//printer.Printf("yamlwrite:oneLabel: SIMPLE=%v\n", item)
|
||||
return item
|
||||
case "MX", "SRV":
|
||||
// Always processed as a complex{}
|
||||
default:
|
||||
panic(fmt.Errorf("yamlwrite:oneLabel:len1 rtype not implemented: %s", rtype))
|
||||
}
|
||||
}
|
||||
|
||||
// Special case labels with many records, all the same rType:
|
||||
if sameType(records) {
|
||||
switch rtype := records[0].Type; rtype {
|
||||
case "A", "CNAME", "NS":
|
||||
v := many{
|
||||
Type: rtype,
|
||||
TTL: records[0].TTL,
|
||||
}
|
||||
for _, rec := range records {
|
||||
v.Values = append(v.Values, rec.GetTargetField())
|
||||
}
|
||||
item.Value = v
|
||||
//printer.Printf("SIMPLE=%v\n", item)
|
||||
return item
|
||||
case "MX", "SRV":
|
||||
// Always processed as a complex{}
|
||||
default:
|
||||
panic(fmt.Errorf("oneLabel:many rtype not implemented: %s", rtype))
|
||||
}
|
||||
}
|
||||
|
||||
// All other labels are complexItems
|
||||
|
||||
var low int // First index of a run.
|
||||
var lst complexItems
|
||||
var last = records[0].Type
|
||||
for i := range records {
|
||||
if records[i].Type != last {
|
||||
//printer.Printf("yamlwrite:oneLabel: Calling oneType( [%d:%d] ) last=%s type=%s\n", low, i, last, records[0].Type)
|
||||
lst = append(lst, oneType(records[low:i]))
|
||||
low = i // Current is the first of a run.
|
||||
last = records[i].Type
|
||||
}
|
||||
if i == (len(records) - 1) {
|
||||
// we are on the last element.
|
||||
//printer.Printf("yamlwrite:oneLabel: Calling oneType( [%d:%d] ) last=%s type=%s\n", low, i+1, last, records[0].Type)
|
||||
lst = append(lst, oneType(records[low:i+1]))
|
||||
}
|
||||
}
|
||||
item.Value = lst
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
// oneType returns interfaces that will MarshalYAML properly for a label with
|
||||
// one or more records, all the same rtype.
|
||||
func oneType(records models.Records) interface{} {
|
||||
//printer.Printf("yamlwrite:oneType len=%d type=%s\n", len(records), records[0].Type)
|
||||
rtype := records[0].Type
|
||||
switch rtype {
|
||||
case "A", "AAAA", "NS":
|
||||
vv := complexVals{
|
||||
Type: rtype,
|
||||
TTL: records[0].TTL,
|
||||
}
|
||||
if len(records) == 1 {
|
||||
vv.Value = records[0].GetTargetField()
|
||||
} else {
|
||||
for _, rc := range records {
|
||||
vv.Values = append(vv.Values, rc.GetTargetCombined())
|
||||
}
|
||||
}
|
||||
return vv
|
||||
case "MX":
|
||||
vv := complexFields{
|
||||
Type: rtype,
|
||||
TTL: records[0].TTL,
|
||||
}
|
||||
for _, rc := range records {
|
||||
vv.Fields = append(vv.Fields, fields{
|
||||
Value: rc.GetTargetField(),
|
||||
Priority: rc.MxPreference,
|
||||
})
|
||||
}
|
||||
return vv
|
||||
case "SRV":
|
||||
vv := complexFields{
|
||||
Type: rtype,
|
||||
TTL: records[0].TTL,
|
||||
}
|
||||
for _, rc := range records {
|
||||
vv.Fields = append(vv.Fields, fields{
|
||||
Value: rc.GetTargetField(),
|
||||
Priority: rc.SrvPriority,
|
||||
SrvWeight: rc.SrvWeight,
|
||||
SrvPort: rc.SrvPort,
|
||||
})
|
||||
}
|
||||
return vv
|
||||
case "TXT":
|
||||
vv := complexVals{
|
||||
Type: rtype,
|
||||
TTL: records[0].TTL,
|
||||
}
|
||||
if len(records) == 1 {
|
||||
vv.Value = strings.Replace(models.StripQuotes(records[0].GetTargetField()), `;`, `\;`, -1)
|
||||
} else {
|
||||
for _, rc := range records {
|
||||
vv.Values = append(vv.Values, models.StripQuotes(rc.GetTargetCombined()))
|
||||
}
|
||||
}
|
||||
return vv
|
||||
|
||||
default:
|
||||
panic(fmt.Errorf("yamlwrite:oneType rtype=%s not implemented", rtype))
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
checks = ["inherit", "-S1034"]
|
84
providers/octodns/testdata/production.yaml
vendored
84
providers/octodns/testdata/production.yaml
vendored
|
@ -1,84 +0,0 @@
|
|||
---
|
||||
providers:
|
||||
config:
|
||||
class: octodns.provider.yaml.YamlProvider
|
||||
directory: ./config
|
||||
zones:
|
||||
apptivate.ms.:
|
||||
sources:
|
||||
- config
|
||||
askubuntu.com.:
|
||||
sources:
|
||||
- config
|
||||
blogoverflow.com.:
|
||||
sources:
|
||||
- config
|
||||
bosun.org.:
|
||||
sources:
|
||||
- config
|
||||
hashcode.ru.:
|
||||
sources:
|
||||
- config
|
||||
isstackoverflowdownforeveryoneorjustme.com.:
|
||||
sources:
|
||||
- config
|
||||
mathoverflow.net.:
|
||||
sources:
|
||||
- config
|
||||
miniprofiler.com.:
|
||||
sources:
|
||||
- config
|
||||
s.tk.:
|
||||
sources:
|
||||
- config
|
||||
serverfault.com.:
|
||||
sources:
|
||||
- config
|
||||
sezn.ru.:
|
||||
sources:
|
||||
- config
|
||||
sstatic.net.:
|
||||
sources:
|
||||
- config
|
||||
stackapps.com.:
|
||||
sources:
|
||||
- config
|
||||
stackauth.com.:
|
||||
sources:
|
||||
- config
|
||||
stackenterprise.co.:
|
||||
sources:
|
||||
- config
|
||||
stackex.com.:
|
||||
sources:
|
||||
- config
|
||||
stackexchange.com.:
|
||||
sources:
|
||||
- config
|
||||
stackoverflow.blog.:
|
||||
sources:
|
||||
- config
|
||||
stackoverflow.com.:
|
||||
sources:
|
||||
- config
|
||||
stackoverflow.design.:
|
||||
sources:
|
||||
- config
|
||||
stackoverflow.email.:
|
||||
sources:
|
||||
- config
|
||||
stackoverflow.ru.:
|
||||
sources:
|
||||
- config
|
||||
stackoverflowbusiness.com.:
|
||||
sources:
|
||||
- config
|
||||
stacksnippets.net.:
|
||||
sources:
|
||||
- config
|
||||
superuser.com.:
|
||||
sources:
|
||||
- config
|
||||
teststackoverflow.com.:
|
||||
sources:
|
||||
- config
|
Loading…
Reference in a new issue