github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/diff/diff.go (about) 1 package diff 2 3 import ( 4 "fmt" 5 "sort" 6 7 "github.com/gobwas/glob" 8 9 "github.com/StackExchange/dnscontrol/v2/models" 10 "github.com/StackExchange/dnscontrol/v2/pkg/printer" 11 ) 12 13 // Correlation stores a difference between two domains. 14 type Correlation struct { 15 d *differ 16 Existing *models.RecordConfig 17 Desired *models.RecordConfig 18 } 19 20 // Changeset stores many Correlation. 21 type Changeset []Correlation 22 23 // Differ is an interface for computing the difference between two zones. 24 type Differ interface { 25 // IncrementalDiff performs a diff on a record-by-record basis, and returns a sets for which records need to be created, deleted, or modified. 26 IncrementalDiff(existing []*models.RecordConfig) (unchanged, create, toDelete, modify Changeset) 27 // ChangedGroups performs a diff more appropriate for providers with a "RecordSet" model, where all records with the same name and type are grouped. 28 // 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. 29 ChangedGroups(existing []*models.RecordConfig) map[models.RecordKey][]string 30 } 31 32 // New is a constructor for a Differ. 33 func New(dc *models.DomainConfig, extraValues ...func(*models.RecordConfig) map[string]string) Differ { 34 return &differ{ 35 dc: dc, 36 extraValues: extraValues, 37 38 // compile IGNORE glob patterns 39 compiledIgnoredLabels: compileIgnoredLabels(dc.IgnoredLabels), 40 } 41 } 42 43 type differ struct { 44 dc *models.DomainConfig 45 extraValues []func(*models.RecordConfig) map[string]string 46 47 compiledIgnoredLabels []glob.Glob 48 } 49 50 // get normalized content for record. target, ttl, mxprio, and specified metadata 51 func (d *differ) content(r *models.RecordConfig) string { 52 // NB(tlim): This function will eventually be replaced by calling 53 // r.GetTargetDiffable(). In the meanwhile, this function compares 54 // its output with r.GetTargetDiffable() to make sure the same 55 // results are generated. Once we have confidence, this function will go away. 56 content := fmt.Sprintf("%v ttl=%d", r.GetTargetCombined(), r.TTL) 57 if r.Type == "SOA" { 58 content = fmt.Sprintf("%s %v %d %d %d %d ttl=%d", r.Target, r.SoaMbox, r.SoaRefresh, r.SoaRetry, r.SoaExpire, r.SoaMinttl, r.TTL) // SoaSerial is not used in comparison 59 } 60 var allMaps []map[string]string 61 for _, f := range d.extraValues { 62 // sort the extra values map keys to perform a deterministic 63 // comparison since Golang maps iteration order is not guaranteed 64 valueMap := f(r) 65 allMaps = append(allMaps, valueMap) 66 keys := make([]string, 0) 67 for k := range valueMap { 68 keys = append(keys, k) 69 } 70 sort.Strings(keys) 71 for _, k := range keys { 72 v := valueMap[k] 73 content += fmt.Sprintf(" %s=%s", k, v) 74 } 75 } 76 control := r.ToDiffable(allMaps...) 77 if control != content { 78 fmt.Printf("CONTROL=%q CONTENT=%q\n", control, content) 79 panic("OOPS! control != content") 80 } 81 return content 82 } 83 84 func (d *differ) IncrementalDiff(existing []*models.RecordConfig) (unchanged, create, toDelete, modify Changeset) { 85 unchanged = Changeset{} 86 create = Changeset{} 87 toDelete = Changeset{} 88 modify = Changeset{} 89 desired := d.dc.Records 90 91 // sort existing and desired by name 92 93 existingByNameAndType := map[models.RecordKey][]*models.RecordConfig{} 94 desiredByNameAndType := map[models.RecordKey][]*models.RecordConfig{} 95 for _, e := range existing { 96 if d.matchIgnored(e.GetLabel()) { 97 printer.Debugf("Ignoring record %s %s due to IGNORE\n", e.GetLabel(), e.Type) 98 } else { 99 k := e.Key() 100 existingByNameAndType[k] = append(existingByNameAndType[k], e) 101 } 102 } 103 for _, dr := range desired { 104 if d.matchIgnored(dr.GetLabel()) { 105 panic(fmt.Sprintf("Trying to update/add IGNOREd record: %s %s", dr.GetLabel(), dr.Type)) 106 } else { 107 k := dr.Key() 108 desiredByNameAndType[k] = append(desiredByNameAndType[k], dr) 109 } 110 } 111 // if NO_PURGE is set, just remove anything that is only in existing. 112 if d.dc.KeepUnknown { 113 for k := range existingByNameAndType { 114 if _, ok := desiredByNameAndType[k]; !ok { 115 printer.Debugf("Ignoring record set %s %s due to NO_PURGE\n", k.Type, k.NameFQDN) 116 delete(existingByNameAndType, k) 117 } 118 } 119 } 120 // Look through existing records. This will give us changes and deletions and some additions. 121 // Each iteration is only for a single type/name record set 122 for key, existingRecords := range existingByNameAndType { 123 desiredRecords := desiredByNameAndType[key] 124 125 // Very first, get rid of any identical records. Easy. 126 for i := len(existingRecords) - 1; i >= 0; i-- { 127 ex := existingRecords[i] 128 for j, de := range desiredRecords { 129 if d.content(de) != d.content(ex) { 130 continue 131 } 132 unchanged = append(unchanged, Correlation{d, ex, de}) 133 existingRecords = existingRecords[:i+copy(existingRecords[i:], existingRecords[i+1:])] 134 desiredRecords = desiredRecords[:j+copy(desiredRecords[j:], desiredRecords[j+1:])] 135 break 136 } 137 } 138 139 // Next, match by target. This will give the most natural modifications. 140 for i := len(existingRecords) - 1; i >= 0; i-- { 141 ex := existingRecords[i] 142 for j, de := range desiredRecords { 143 if de.GetTargetField() == ex.GetTargetField() { 144 // two records share a target, but different content (ttl or metadata changes) 145 modify = append(modify, Correlation{d, ex, de}) 146 // remove from both slices by index 147 existingRecords = existingRecords[:i+copy(existingRecords[i:], existingRecords[i+1:])] 148 desiredRecords = desiredRecords[:j+copy(desiredRecords[j:], desiredRecords[j+1:])] 149 break 150 } 151 } 152 } 153 154 desiredLookup := map[string]*models.RecordConfig{} 155 existingLookup := map[string]*models.RecordConfig{} 156 // build index based on normalized content data 157 for _, ex := range existingRecords { 158 normalized := d.content(ex) 159 if existingLookup[normalized] != nil { 160 panic(fmt.Sprintf("DUPLICATE E_RECORD FOUND: %s %s", key, normalized)) 161 } 162 existingLookup[normalized] = ex 163 } 164 for _, de := range desiredRecords { 165 normalized := d.content(de) 166 if desiredLookup[normalized] != nil { 167 panic(fmt.Sprintf("DUPLICATE D_RECORD FOUND: %s %s", key, normalized)) 168 } 169 desiredLookup[normalized] = de 170 } 171 // if a record is in both, it is unchanged 172 for norm, ex := range existingLookup { 173 if de, ok := desiredLookup[norm]; ok { 174 unchanged = append(unchanged, Correlation{d, ex, de}) 175 delete(existingLookup, norm) 176 delete(desiredLookup, norm) 177 } 178 } 179 // sort records by normalized text. Keeps behaviour deterministic 180 existingStrings, desiredStrings := sortedKeys(existingLookup), sortedKeys(desiredLookup) 181 // Modifications. Take 1 from each side. 182 for len(desiredStrings) > 0 && len(existingStrings) > 0 { 183 modify = append(modify, Correlation{d, existingLookup[existingStrings[0]], desiredLookup[desiredStrings[0]]}) 184 existingStrings = existingStrings[1:] 185 desiredStrings = desiredStrings[1:] 186 } 187 // If desired still has things they are additions 188 for _, norm := range desiredStrings { 189 rec := desiredLookup[norm] 190 create = append(create, Correlation{d, nil, rec}) 191 } 192 // if found , but not desired, delete it 193 for _, norm := range existingStrings { 194 rec := existingLookup[norm] 195 toDelete = append(toDelete, Correlation{d, rec, nil}) 196 } 197 // remove this set from the desired list to indicate we have processed it. 198 delete(desiredByNameAndType, key) 199 } 200 201 // any name/type sets not already processed are pure additions 202 for name := range existingByNameAndType { 203 delete(desiredByNameAndType, name) 204 } 205 for _, desiredList := range desiredByNameAndType { 206 for _, rec := range desiredList { 207 create = append(create, Correlation{d, nil, rec}) 208 } 209 } 210 return 211 } 212 213 func (d *differ) ChangedGroups(existing []*models.RecordConfig) map[models.RecordKey][]string { 214 changedKeys := map[models.RecordKey][]string{} 215 _, create, delete, modify := d.IncrementalDiff(existing) 216 for _, c := range create { 217 changedKeys[c.Desired.Key()] = append(changedKeys[c.Desired.Key()], c.String()) 218 } 219 for _, d := range delete { 220 changedKeys[d.Existing.Key()] = append(changedKeys[d.Existing.Key()], d.String()) 221 } 222 for _, m := range modify { 223 changedKeys[m.Desired.Key()] = append(changedKeys[m.Desired.Key()], m.String()) 224 } 225 return changedKeys 226 } 227 228 // DebugKeyMapMap debug prints the results from ChangedGroups. 229 func DebugKeyMapMap(note string, m map[models.RecordKey][]string) { 230 // The output isn't pretty but it is useful. 231 fmt.Println("DEBUG:", note) 232 233 // Extract the keys 234 var keys []models.RecordKey 235 for k := range m { 236 keys = append(keys, k) 237 } 238 sort.SliceStable(keys, func(i, j int) bool { 239 if keys[i].NameFQDN == keys[j].NameFQDN { 240 return keys[i].Type < keys[j].Type 241 } 242 return keys[i].NameFQDN < keys[j].NameFQDN 243 }) 244 245 // Pretty print the map: 246 for _, k := range keys { 247 fmt.Printf(" %v %v:\n", k.Type, k.NameFQDN) 248 for _, s := range m[k] { 249 fmt.Printf(" -- %q\n", s) 250 } 251 } 252 } 253 254 func (c Correlation) String() string { 255 if c.Existing == nil { 256 return fmt.Sprintf("CREATE %s %s %s", c.Desired.Type, c.Desired.GetLabelFQDN(), c.d.content(c.Desired)) 257 } 258 if c.Desired == nil { 259 return fmt.Sprintf("DELETE %s %s %s", c.Existing.Type, c.Existing.GetLabelFQDN(), c.d.content(c.Existing)) 260 } 261 return fmt.Sprintf("MODIFY %s %s: (%s) -> (%s)", c.Existing.Type, c.Existing.GetLabelFQDN(), c.d.content(c.Existing), c.d.content(c.Desired)) 262 } 263 264 func sortedKeys(m map[string]*models.RecordConfig) []string { 265 s := []string{} 266 for v := range m { 267 s = append(s, v) 268 } 269 sort.Strings(s) 270 return s 271 } 272 273 func compileIgnoredLabels(ignoredLabels []string) []glob.Glob { 274 result := make([]glob.Glob, 0, len(ignoredLabels)) 275 276 for _, tst := range ignoredLabels { 277 g, err := glob.Compile(tst, '.') 278 if err != nil { 279 panic(fmt.Sprintf("Failed to compile IGNORE pattern %q: %v", tst, err)) 280 } 281 282 result = append(result, g) 283 } 284 285 return result 286 } 287 288 func (d *differ) matchIgnored(name string) bool { 289 for _, tst := range d.compiledIgnoredLabels { 290 if tst.Match(name) { 291 return true 292 } 293 } 294 return false 295 }