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