github.com/teknogeek/dnscontrol@v0.2.8/providers/diff/diff.go (about)

     1  package diff
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  
     7  	"github.com/StackExchange/dnscontrol/models"
     8  	"github.com/StackExchange/dnscontrol/pkg/printer"
     9  )
    10  
    11  // Correlation stores a difference between two domains.
    12  type Correlation struct {
    13  	d        *differ
    14  	Existing *models.RecordConfig
    15  	Desired  *models.RecordConfig
    16  }
    17  
    18  // Changeset stores many Correlation.
    19  type Changeset []Correlation
    20  
    21  // Differ is an interface for computing the difference between two zones.
    22  type Differ interface {
    23  	// IncrementalDiff performs a diff on a record-by-record basis, and returns a sets for which records need to be created, deleted, or modified.
    24  	IncrementalDiff(existing []*models.RecordConfig) (unchanged, create, toDelete, modify Changeset)
    25  	// ChangedGroups performs a diff more appropriate for providers with a "RecordSet" model, where all records with the same name and type are grouped.
    26  	// Individual record changes are often not useful in such scenarios. Instead we return a map of record keys to a list of change descriptions within that group.
    27  	ChangedGroups(existing []*models.RecordConfig) map[models.RecordKey][]string
    28  }
    29  
    30  // New is a constructor for a Differ.
    31  func New(dc *models.DomainConfig, extraValues ...func(*models.RecordConfig) map[string]string) Differ {
    32  	return &differ{
    33  		dc:          dc,
    34  		extraValues: extraValues,
    35  	}
    36  }
    37  
    38  type differ struct {
    39  	dc          *models.DomainConfig
    40  	extraValues []func(*models.RecordConfig) map[string]string
    41  }
    42  
    43  // get normalized content for record. target, ttl, mxprio, and specified metadata
    44  func (d *differ) content(r *models.RecordConfig) string {
    45  	content := fmt.Sprintf("%v ttl=%d", r.GetTargetCombined(), r.TTL)
    46  	for _, f := range d.extraValues {
    47  		// sort the extra values map keys to perform a deterministic
    48  		// comparison since Golang maps iteration order is not guaranteed
    49  		valueMap := f(r)
    50  		keys := make([]string, 0)
    51  		for k := range valueMap {
    52  			keys = append(keys, k)
    53  		}
    54  		sort.Strings(keys)
    55  		for _, k := range keys {
    56  			v := valueMap[k]
    57  			content += fmt.Sprintf(" %s=%s", k, v)
    58  		}
    59  	}
    60  	return content
    61  }
    62  
    63  func (d *differ) IncrementalDiff(existing []*models.RecordConfig) (unchanged, create, toDelete, modify Changeset) {
    64  	unchanged = Changeset{}
    65  	create = Changeset{}
    66  	toDelete = Changeset{}
    67  	modify = Changeset{}
    68  	desired := d.dc.Records
    69  
    70  	// sort existing and desired by name
    71  
    72  	existingByNameAndType := map[models.RecordKey][]*models.RecordConfig{}
    73  	desiredByNameAndType := map[models.RecordKey][]*models.RecordConfig{}
    74  	for _, e := range existing {
    75  		if d.matchIgnored(e.GetLabel()) {
    76  			printer.Debugf("Ignoring record %s %s due to IGNORE\n", e.GetLabel(), e.Type)
    77  		} else {
    78  			k := e.Key()
    79  			existingByNameAndType[k] = append(existingByNameAndType[k], e)
    80  		}
    81  	}
    82  	for _, dr := range desired {
    83  		if d.matchIgnored(dr.GetLabel()) {
    84  			panic(fmt.Sprintf("Trying to update/add IGNOREd record: %s %s", dr.GetLabel(), dr.Type))
    85  		} else {
    86  			k := dr.Key()
    87  			desiredByNameAndType[k] = append(desiredByNameAndType[k], dr)
    88  		}
    89  	}
    90  	// if NO_PURGE is set, just remove anything that is only in existing.
    91  	if d.dc.KeepUnknown {
    92  		for k := range existingByNameAndType {
    93  			if _, ok := desiredByNameAndType[k]; !ok {
    94  				printer.Debugf("Ignoring record set %s %s due to NO_PURGE\n", k.Type, k.NameFQDN)
    95  				delete(existingByNameAndType, k)
    96  			}
    97  		}
    98  	}
    99  	// Look through existing records. This will give us changes and deletions and some additions.
   100  	// Each iteration is only for a single type/name record set
   101  	for key, existingRecords := range existingByNameAndType {
   102  		desiredRecords := desiredByNameAndType[key]
   103  		// first look through records that are the same target on both sides. Those are either modifications or unchanged
   104  		for i := len(existingRecords) - 1; i >= 0; i-- {
   105  			ex := existingRecords[i]
   106  			for j, de := range desiredRecords {
   107  				if de.GetTargetField() == ex.GetTargetField() {
   108  					// they're either identical or should be a modification of each other (ttl or metadata changes)
   109  					if d.content(de) == d.content(ex) {
   110  						unchanged = append(unchanged, Correlation{d, ex, de})
   111  					} else {
   112  						modify = append(modify, Correlation{d, ex, de})
   113  					}
   114  					// remove from both slices by index
   115  					existingRecords = existingRecords[:i+copy(existingRecords[i:], existingRecords[i+1:])]
   116  					desiredRecords = desiredRecords[:j+copy(desiredRecords[j:], desiredRecords[j+1:])]
   117  					break
   118  				}
   119  			}
   120  		}
   121  
   122  		desiredLookup := map[string]*models.RecordConfig{}
   123  		existingLookup := map[string]*models.RecordConfig{}
   124  		// build index based on normalized content data
   125  		for _, ex := range existingRecords {
   126  			normalized := d.content(ex)
   127  			if existingLookup[normalized] != nil {
   128  				panic(fmt.Sprintf("DUPLICATE E_RECORD FOUND: %s %s", key, normalized))
   129  			}
   130  			existingLookup[normalized] = ex
   131  		}
   132  		for _, de := range desiredRecords {
   133  			normalized := d.content(de)
   134  			if desiredLookup[normalized] != nil {
   135  				panic(fmt.Sprintf("DUPLICATE D_RECORD FOUND: %s %s", key, normalized))
   136  			}
   137  			desiredLookup[normalized] = de
   138  		}
   139  		// if a record is in both, it is unchanged
   140  		for norm, ex := range existingLookup {
   141  			if de, ok := desiredLookup[norm]; ok {
   142  				unchanged = append(unchanged, Correlation{d, ex, de})
   143  				delete(existingLookup, norm)
   144  				delete(desiredLookup, norm)
   145  			}
   146  		}
   147  		// sort records by normalized text. Keeps behaviour deterministic
   148  		existingStrings, desiredStrings := sortedKeys(existingLookup), sortedKeys(desiredLookup)
   149  		// Modifications. Take 1 from each side.
   150  		for len(desiredStrings) > 0 && len(existingStrings) > 0 {
   151  			modify = append(modify, Correlation{d, existingLookup[existingStrings[0]], desiredLookup[desiredStrings[0]]})
   152  			existingStrings = existingStrings[1:]
   153  			desiredStrings = desiredStrings[1:]
   154  		}
   155  		// If desired still has things they are additions
   156  		for _, norm := range desiredStrings {
   157  			rec := desiredLookup[norm]
   158  			create = append(create, Correlation{d, nil, rec})
   159  		}
   160  		// if found , but not desired, delete it
   161  		for _, norm := range existingStrings {
   162  			rec := existingLookup[norm]
   163  			toDelete = append(toDelete, Correlation{d, rec, nil})
   164  		}
   165  		// remove this set from the desired list to indicate we have processed it.
   166  		delete(desiredByNameAndType, key)
   167  	}
   168  
   169  	// any name/type sets not already processed are pure additions
   170  	for name := range existingByNameAndType {
   171  		delete(desiredByNameAndType, name)
   172  	}
   173  	for _, desiredList := range desiredByNameAndType {
   174  		for _, rec := range desiredList {
   175  			create = append(create, Correlation{d, nil, rec})
   176  		}
   177  	}
   178  	return
   179  }
   180  
   181  func (d *differ) ChangedGroups(existing []*models.RecordConfig) map[models.RecordKey][]string {
   182  	changedKeys := map[models.RecordKey][]string{}
   183  	_, create, delete, modify := d.IncrementalDiff(existing)
   184  	for _, c := range create {
   185  		changedKeys[c.Desired.Key()] = append(changedKeys[c.Desired.Key()], c.String())
   186  	}
   187  	for _, d := range delete {
   188  		changedKeys[d.Existing.Key()] = append(changedKeys[d.Existing.Key()], d.String())
   189  	}
   190  	for _, m := range modify {
   191  		changedKeys[m.Desired.Key()] = append(changedKeys[m.Desired.Key()], m.String())
   192  	}
   193  	return changedKeys
   194  }
   195  
   196  func (c Correlation) String() string {
   197  	if c.Existing == nil {
   198  		return fmt.Sprintf("CREATE %s %s %s", c.Desired.Type, c.Desired.GetLabelFQDN(), c.d.content(c.Desired))
   199  	}
   200  	if c.Desired == nil {
   201  		return fmt.Sprintf("DELETE %s %s %s", c.Existing.Type, c.Existing.GetLabelFQDN(), c.d.content(c.Existing))
   202  	}
   203  	return fmt.Sprintf("MODIFY %s %s: (%s) -> (%s)", c.Existing.Type, c.Existing.GetLabelFQDN(), c.d.content(c.Existing), c.d.content(c.Desired))
   204  }
   205  
   206  func sortedKeys(m map[string]*models.RecordConfig) []string {
   207  	s := []string{}
   208  	for v := range m {
   209  		s = append(s, v)
   210  	}
   211  	sort.Strings(s)
   212  	return s
   213  }
   214  
   215  func (d *differ) matchIgnored(name string) bool {
   216  	for _, tst := range d.dc.IgnoredLabels {
   217  		if name == tst {
   218  			return true
   219  		}
   220  	}
   221  	return false
   222  }