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 }