github.com/teknogeek/dnscontrol@v0.2.8/providers/octodns/octoyaml/write.go (about)

     1  package octoyaml
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"sort"
     7  	"strings"
     8  
     9  	"github.com/StackExchange/dnscontrol/models"
    10  	"github.com/miekg/dns/dnsutil"
    11  	"github.com/pkg/errors"
    12  	yaml "gopkg.in/yaml.v2"
    13  )
    14  
    15  // WriteYaml outputs a yaml version of a list of RecordConfig.
    16  func WriteYaml(w io.Writer, records models.Records, origin string) error {
    17  	if len(records) == 0 {
    18  		return nil
    19  	}
    20  
    21  	// Pick the most common TTL as the default so we can
    22  	// write the fewest "ttl:" lines.
    23  	defaultTTL := mostCommonTTL(records)
    24  
    25  	// Make a copy of the records, since we want to sort and muck with them.
    26  	recsCopy := models.Records{}
    27  	for _, r := range records {
    28  		recsCopy = append(recsCopy, r)
    29  	}
    30  	for _, r := range recsCopy {
    31  		if r.GetLabel() == "@" {
    32  			//r.Name = ""
    33  			r.UnsafeSetLabelNull()
    34  		}
    35  	}
    36  
    37  	z := &genYamlData{
    38  		Origin:     dnsutil.AddOrigin(origin, "."),
    39  		DefaultTTL: defaultTTL,
    40  		Records:    recsCopy,
    41  	}
    42  
    43  	// Sort in the weird order that OctoDNS expects:
    44  	sort.Sort(z)
    45  
    46  	// Generate the YAML:
    47  	fmt.Fprintln(w, "---")
    48  	yb, err := yaml.Marshal(z.genInterfaceList(w))
    49  	if err != nil {
    50  		return err
    51  	}
    52  	_, err = w.Write(yb)
    53  
    54  	return err
    55  }
    56  
    57  // genInterfaceList outputs YAML ordered slices for the entire zone.
    58  // Each item in the list is an interface that will MarshallYAML to
    59  // the desired output.
    60  func (z *genYamlData) genInterfaceList(w io.Writer) yaml.MapSlice {
    61  	var yam yaml.MapSlice
    62  	// Group the records by label.
    63  	order, groups := z.Records.GroupedByLabel()
    64  	// For each group, generate the YAML.
    65  	for _, label := range order {
    66  		group := groups[label]
    67  		// Within the group, sort the similar Types together:
    68  		sort.SliceStable(group, func(i, j int) bool { return zoneRrtypeLess(group[i].Type, group[j].Type) })
    69  		// Generate the YAML records:
    70  		yam = append(yam, oneLabel(group))
    71  	}
    72  	return yam
    73  }
    74  
    75  // "simple" records are when a label has a single rtype.
    76  // It may have a single (simple) or multiple (many) values.
    77  
    78  // Used to generate:
    79  //  label:
    80  //    type: A
    81  //    value: 1.2.3.4
    82  type simple struct {
    83  	TTL   uint32 `yaml:"ttl,omitempty"`
    84  	Type  string `yaml:"type"`
    85  	Value string `yaml:"value"`
    86  }
    87  
    88  // Used to generate:
    89  //  label:
    90  //    type: A
    91  //    values:
    92  //    - 1.2.3.4
    93  //    - 2.3.4.5
    94  type many struct {
    95  	TTL    uint32   `yaml:"ttl,omitempty"`
    96  	Type   string   `yaml:"type"`
    97  	Values []string `yaml:"values"`
    98  }
    99  
   100  // complexItems are when a single label has multiple rtypes
   101  // associated with it. For example, a label with both an A and MX record.
   102  type complexItems []interface{}
   103  
   104  // Used to generate a complex item with either a single value or multiple values:
   105  // 'thing':                             >> complexVals
   106  //   - type: CNAME
   107  //     value: newplace.example.com.     << value
   108  // 'www':
   109  //   - type: A
   110  //     values:
   111  //       - 1.2.3.4                      << values
   112  //       - 1.2.3.5                      << values
   113  //   - type: MX
   114  //     values:
   115  //       - priority: 10                 << fields
   116  //         value: mx1.example.com.      << fields
   117  //       - priority: 10                 << fields
   118  //         value: mx2.example.com.      << fields
   119  type complexVals struct {
   120  	TTL    uint32   `yaml:"ttl,omitempty"`
   121  	Type   string   `yaml:"type"`
   122  	Value  string   `yaml:"value,omitempty"`
   123  	Values []string `yaml:"values,omitempty"`
   124  }
   125  
   126  // Used to generate rtypes like MX rand SRV ecords, which have multiple
   127  // fields within the rtype.
   128  type complexFields struct {
   129  	TTL    uint32   `yaml:"ttl,omitempty"`
   130  	Type   string   `yaml:"type"`
   131  	Fields []fields `yaml:"values,omitempty"`
   132  }
   133  
   134  // Used to generate the fields themselves:
   135  type fields struct {
   136  	Priority  uint16 `yaml:"priority,omitempty"`
   137  	SrvWeight uint16 `yaml:"weight,omitempty"`
   138  	SrvPort   uint16 `yaml:"port,omitempty"`
   139  	Value     string `yaml:"value,omitempty"`
   140  }
   141  
   142  // FIXME(tlim): An MX record with .Priority=0 will not output the priority.
   143  
   144  // sameType returns true if all records have the same type.
   145  func sameType(records models.Records) bool {
   146  	t := records[0].Type
   147  	for _, r := range records {
   148  		if r.Type != t {
   149  			return false
   150  		}
   151  	}
   152  	return true
   153  }
   154  
   155  // oneLabel handles all the DNS records associated with a single label.
   156  // It dispatches the right code whether the label is simple, many, or complex.
   157  func oneLabel(records models.Records) yaml.MapItem {
   158  	item := yaml.MapItem{
   159  		// a yaml.MapItem is a YAML map that retains the key order.
   160  		Key: records[0].GetLabel(),
   161  	}
   162  	//  Special case labels with a single record:
   163  	if len(records) == 1 {
   164  		switch rtype := records[0].Type; rtype {
   165  		case "A", "CNAME", "NS", "PTR", "TXT":
   166  			v := simple{
   167  				Type:  rtype,
   168  				Value: records[0].GetTargetField(),
   169  				TTL:   records[0].TTL,
   170  			}
   171  			if v.Type == "TXT" {
   172  				v.Value = strings.Replace(models.StripQuotes(v.Value), `;`, `\;`, -1)
   173  			}
   174  			//fmt.Printf("yamlwrite:oneLabel: simple ttl=%d\n", v.TTL)
   175  			item.Value = v
   176  			//fmt.Printf("yamlwrite:oneLabel: SIMPLE=%v\n", item)
   177  			return item
   178  		case "MX", "SRV":
   179  			// Always processed as a complex{}
   180  		default:
   181  			panic(errors.Errorf("yamlwrite:oneLabel:len1 rtype not implemented: %s", rtype))
   182  		}
   183  	}
   184  
   185  	//  Special case labels with many records, all the same rType:
   186  	if sameType(records) {
   187  		switch rtype := records[0].Type; rtype {
   188  		case "A", "CNAME", "NS":
   189  			v := many{
   190  				Type: rtype,
   191  				TTL:  records[0].TTL,
   192  			}
   193  			for _, rec := range records {
   194  				v.Values = append(v.Values, rec.GetTargetField())
   195  			}
   196  			item.Value = v
   197  			//fmt.Printf("SIMPLE=%v\n", item)
   198  			return item
   199  		case "MX", "SRV":
   200  			// Always processed as a complex{}
   201  		default:
   202  			panic(errors.Errorf("oneLabel:many rtype not implemented: %s", rtype))
   203  		}
   204  	}
   205  
   206  	// All other labels are complexItems
   207  
   208  	var low int // First index of a run.
   209  	var lst complexItems
   210  	var last = records[0].Type
   211  	for i := range records {
   212  		if records[i].Type != last {
   213  			//fmt.Printf("yamlwrite:oneLabel: Calling oneType( [%d:%d] ) last=%s type=%s\n", low, i, last, records[0].Type)
   214  			lst = append(lst, oneType(records[low:i]))
   215  			low = i // Current is the first of a run.
   216  			last = records[i].Type
   217  		}
   218  		if i == (len(records) - 1) {
   219  			// we are on the last element.
   220  			//fmt.Printf("yamlwrite:oneLabel: Calling oneType( [%d:%d] ) last=%s type=%s\n", low, i+1, last, records[0].Type)
   221  			lst = append(lst, oneType(records[low:i+1]))
   222  		}
   223  	}
   224  	item.Value = lst
   225  
   226  	return item
   227  }
   228  
   229  // oneType returns interfaces that will MarshalYAML properly for a label with
   230  // one or more records, all the same rtype.
   231  func oneType(records models.Records) interface{} {
   232  	//fmt.Printf("yamlwrite:oneType len=%d type=%s\n", len(records), records[0].Type)
   233  	rtype := records[0].Type
   234  	switch rtype {
   235  	case "A", "AAAA", "NS":
   236  		vv := complexVals{
   237  			Type: rtype,
   238  			TTL:  records[0].TTL,
   239  		}
   240  		if len(records) == 1 {
   241  			vv.Value = records[0].GetTargetField()
   242  		} else {
   243  			for _, rc := range records {
   244  				vv.Values = append(vv.Values, rc.GetTargetCombined())
   245  			}
   246  		}
   247  		return vv
   248  	case "MX":
   249  		vv := complexFields{
   250  			Type: rtype,
   251  			TTL:  records[0].TTL,
   252  		}
   253  		for _, rc := range records {
   254  			vv.Fields = append(vv.Fields, fields{
   255  				Value:    rc.GetTargetField(),
   256  				Priority: rc.MxPreference,
   257  			})
   258  		}
   259  		return vv
   260  	case "SRV":
   261  		vv := complexFields{
   262  			Type: rtype,
   263  			TTL:  records[0].TTL,
   264  		}
   265  		for _, rc := range records {
   266  			vv.Fields = append(vv.Fields, fields{
   267  				Value:     rc.GetTargetField(),
   268  				Priority:  rc.SrvPriority,
   269  				SrvWeight: rc.SrvWeight,
   270  				SrvPort:   rc.SrvPort,
   271  			})
   272  		}
   273  		return vv
   274  	case "TXT":
   275  		vv := complexVals{
   276  			Type: rtype,
   277  			TTL:  records[0].TTL,
   278  		}
   279  		if len(records) == 1 {
   280  			vv.Value = strings.Replace(models.StripQuotes(records[0].GetTargetField()), `;`, `\;`, -1)
   281  		} else {
   282  			for _, rc := range records {
   283  				vv.Values = append(vv.Values, models.StripQuotes(rc.GetTargetCombined()))
   284  			}
   285  		}
   286  		return vv
   287  
   288  	default:
   289  		panic(errors.Errorf("yamlwrite:oneType rtype=%s not implemented", rtype))
   290  	}
   291  }
   292  
   293  // mostCommonTTL returns the most common TTL in a set of records. If there is
   294  // a tie, the highest TTL is selected. This makes the results consistent.
   295  // NS records are not included in the analysis because Tom said so.
   296  func mostCommonTTL(records models.Records) uint32 {
   297  	// Index the TTLs in use:
   298  	d := make(map[uint32]int)
   299  	for _, r := range records {
   300  		if r.Type != "NS" {
   301  			d[r.TTL]++
   302  		}
   303  	}
   304  	// Find the largest count:
   305  	var mc int
   306  	for _, value := range d {
   307  		if value > mc {
   308  			mc = value
   309  		}
   310  	}
   311  	// Find the largest key with that count:
   312  	var mk uint32
   313  	for key, value := range d {
   314  		if value == mc {
   315  			if key > mk {
   316  				mk = key
   317  			}
   318  		}
   319  	}
   320  	return mk
   321  }