dnscontrol/providers/octodns/octoyaml/read.go
Armand Grillet 0d9cc35deb
Add SPF support for RecordConfig (#1020)
This type is identical to TXT but used for other purposes, it is
officially supported by OctoDNS.

Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
2021-01-24 15:36:48 -05:00

271 lines
8.6 KiB
Go

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"
"io/ioutil"
"reflect"
"strconv"
yaml "gopkg.in/yaml.v2"
"github.com/StackExchange/dnscontrol/v3/models"
)
// 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 := ioutil.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)
}
//fmt.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.
//fmt.Printf("ReadYaml: NEXT KEY\n")
//fmt.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
//fmt.Printf("ReadYaml: list key=%s i=%d v3.(type)=%s\n", k, i, typeof(v3))
switch v3.(type) {
case map[interface{}]interface{}:
//fmt.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)
//fmt.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
// fmt.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
//fmt.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.
//fmt.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{}) {
//fmt.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))
}
}
//fmt.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)
}
}
// fmt.Printf("parseLeaf: Target=(%v)\n", rTarget)
// fmt.Printf("parseLeaf: len(rTargets)=%d\n", len(rTargets))
// fmt.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
//fmt.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 {
//fmt.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")
}