mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-01-12 02:17:43 +08:00
0d9cc35deb
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>
271 lines
8.6 KiB
Go
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")
|
|
}
|