mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-01-12 02:17:43 +08:00
b6fd4dffd7
* Lint: Fix ST1005: error strings should not be capitalized * Cleanup: Fix a lot of staticcheck.io warnings
318 lines
8.2 KiB
Go
318 lines
8.2 KiB
Go
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)
|
|
}
|
|
//fmt.Printf("yamlwrite:oneLabel: simple ttl=%d\n", v.TTL)
|
|
item.Value = v
|
|
//fmt.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
|
|
//fmt.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 {
|
|
//fmt.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.
|
|
//fmt.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{} {
|
|
//fmt.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
|
|
}
|