OCTODNS: Remove octodns provider (#1910)

This commit is contained in:
Tom Limoncelli 2023-01-01 14:52:22 -05:00 committed by GitHub
parent 5c0801f4a8
commit bec7e1e865
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 0 additions and 1566 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +0,0 @@
[
{
"type":"A",
"name":"foo",
"target":"1.2.3.4",
"ttl":301
}
]

View file

@ -1,5 +0,0 @@
---
foo:
ttl: 301
type: A
value: 1.2.3.4

View file

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

View file

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

View file

@ -1,7 +0,0 @@
---
foo:
ttl: 302
type: A
values:
- 1.2.3.4
- 1.2.3.5

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +0,0 @@
[
{
"type":"A",
"name":"@",
"target":"1.2.3.4",
"ttl":306
}
]

View file

@ -1,5 +0,0 @@
---
'':
type: A
ttl: 306
value: 1.2.3.4

View file

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

View file

@ -1,8 +0,0 @@
[
{
"type":"A",
"name":"@",
"target":"1.2.3.4",
"ttl":307
}
]

View file

@ -1,5 +0,0 @@
---
"":
ttl: 307
type: A
value: 1.2.3.4

View file

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

View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
checks = ["inherit", "-S1034"]

View file

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