github.com/paybyphone/terraform@v0.9.5-0.20170613192930-9706042ddd51/terraform/diff.go (about) 1 package terraform 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "reflect" 8 "regexp" 9 "sort" 10 "strings" 11 "sync" 12 13 "github.com/mitchellh/copystructure" 14 ) 15 16 // DiffChangeType is an enum with the kind of changes a diff has planned. 17 type DiffChangeType byte 18 19 const ( 20 DiffInvalid DiffChangeType = iota 21 DiffNone 22 DiffCreate 23 DiffUpdate 24 DiffDestroy 25 DiffDestroyCreate 26 ) 27 28 // multiVal matches the index key to a flatmapped set, list or map 29 var multiVal = regexp.MustCompile(`\.(#|%)$`) 30 31 // Diff trackes the changes that are necessary to apply a configuration 32 // to an existing infrastructure. 33 type Diff struct { 34 // Modules contains all the modules that have a diff 35 Modules []*ModuleDiff 36 } 37 38 // Prune cleans out unused structures in the diff without affecting 39 // the behavior of the diff at all. 40 // 41 // This is not safe to call concurrently. This is safe to call on a 42 // nil Diff. 43 func (d *Diff) Prune() { 44 if d == nil { 45 return 46 } 47 48 // Prune all empty modules 49 newModules := make([]*ModuleDiff, 0, len(d.Modules)) 50 for _, m := range d.Modules { 51 // If the module isn't empty, we keep it 52 if !m.Empty() { 53 newModules = append(newModules, m) 54 } 55 } 56 if len(newModules) == 0 { 57 newModules = nil 58 } 59 d.Modules = newModules 60 } 61 62 // AddModule adds the module with the given path to the diff. 63 // 64 // This should be the preferred method to add module diffs since it 65 // allows us to optimize lookups later as well as control sorting. 66 func (d *Diff) AddModule(path []string) *ModuleDiff { 67 m := &ModuleDiff{Path: path} 68 m.init() 69 d.Modules = append(d.Modules, m) 70 return m 71 } 72 73 // ModuleByPath is used to lookup the module diff for the given path. 74 // This should be the preferred lookup mechanism as it allows for future 75 // lookup optimizations. 76 func (d *Diff) ModuleByPath(path []string) *ModuleDiff { 77 if d == nil { 78 return nil 79 } 80 for _, mod := range d.Modules { 81 if mod.Path == nil { 82 panic("missing module path") 83 } 84 if reflect.DeepEqual(mod.Path, path) { 85 return mod 86 } 87 } 88 return nil 89 } 90 91 // RootModule returns the ModuleState for the root module 92 func (d *Diff) RootModule() *ModuleDiff { 93 root := d.ModuleByPath(rootModulePath) 94 if root == nil { 95 panic("missing root module") 96 } 97 return root 98 } 99 100 // Empty returns true if the diff has no changes. 101 func (d *Diff) Empty() bool { 102 if d == nil { 103 return true 104 } 105 106 for _, m := range d.Modules { 107 if !m.Empty() { 108 return false 109 } 110 } 111 112 return true 113 } 114 115 // Equal compares two diffs for exact equality. 116 // 117 // This is different from the Same comparison that is supported which 118 // checks for operation equality taking into account computed values. Equal 119 // instead checks for exact equality. 120 func (d *Diff) Equal(d2 *Diff) bool { 121 // If one is nil, they must both be nil 122 if d == nil || d2 == nil { 123 return d == d2 124 } 125 126 // Sort the modules 127 sort.Sort(moduleDiffSort(d.Modules)) 128 sort.Sort(moduleDiffSort(d2.Modules)) 129 130 // Copy since we have to modify the module destroy flag to false so 131 // we don't compare that. TODO: delete this when we get rid of the 132 // destroy flag on modules. 133 dCopy := d.DeepCopy() 134 d2Copy := d2.DeepCopy() 135 for _, m := range dCopy.Modules { 136 m.Destroy = false 137 } 138 for _, m := range d2Copy.Modules { 139 m.Destroy = false 140 } 141 142 // Use DeepEqual 143 return reflect.DeepEqual(dCopy, d2Copy) 144 } 145 146 // DeepCopy performs a deep copy of all parts of the Diff, making the 147 // resulting Diff safe to use without modifying this one. 148 func (d *Diff) DeepCopy() *Diff { 149 copy, err := copystructure.Config{Lock: true}.Copy(d) 150 if err != nil { 151 panic(err) 152 } 153 154 return copy.(*Diff) 155 } 156 157 func (d *Diff) String() string { 158 var buf bytes.Buffer 159 160 keys := make([]string, 0, len(d.Modules)) 161 lookup := make(map[string]*ModuleDiff) 162 for _, m := range d.Modules { 163 key := fmt.Sprintf("module.%s", strings.Join(m.Path[1:], ".")) 164 keys = append(keys, key) 165 lookup[key] = m 166 } 167 sort.Strings(keys) 168 169 for _, key := range keys { 170 m := lookup[key] 171 mStr := m.String() 172 173 // If we're the root module, we just write the output directly. 174 if reflect.DeepEqual(m.Path, rootModulePath) { 175 buf.WriteString(mStr + "\n") 176 continue 177 } 178 179 buf.WriteString(fmt.Sprintf("%s:\n", key)) 180 181 s := bufio.NewScanner(strings.NewReader(mStr)) 182 for s.Scan() { 183 buf.WriteString(fmt.Sprintf(" %s\n", s.Text())) 184 } 185 } 186 187 return strings.TrimSpace(buf.String()) 188 } 189 190 func (d *Diff) init() { 191 if d.Modules == nil { 192 rootDiff := &ModuleDiff{Path: rootModulePath} 193 d.Modules = []*ModuleDiff{rootDiff} 194 } 195 for _, m := range d.Modules { 196 m.init() 197 } 198 } 199 200 // ModuleDiff tracks the differences between resources to apply within 201 // a single module. 202 type ModuleDiff struct { 203 Path []string 204 Resources map[string]*InstanceDiff 205 Destroy bool // Set only by the destroy plan 206 } 207 208 func (d *ModuleDiff) init() { 209 if d.Resources == nil { 210 d.Resources = make(map[string]*InstanceDiff) 211 } 212 for _, r := range d.Resources { 213 r.init() 214 } 215 } 216 217 // ChangeType returns the type of changes that the diff for this 218 // module includes. 219 // 220 // At a module level, this will only be DiffNone, DiffUpdate, DiffDestroy, or 221 // DiffCreate. If an instance within the module has a DiffDestroyCreate 222 // then this will register as a DiffCreate for a module. 223 func (d *ModuleDiff) ChangeType() DiffChangeType { 224 result := DiffNone 225 for _, r := range d.Resources { 226 change := r.ChangeType() 227 switch change { 228 case DiffCreate, DiffDestroy: 229 if result == DiffNone { 230 result = change 231 } 232 case DiffDestroyCreate, DiffUpdate: 233 result = DiffUpdate 234 } 235 } 236 237 return result 238 } 239 240 // Empty returns true if the diff has no changes within this module. 241 func (d *ModuleDiff) Empty() bool { 242 if d.Destroy { 243 return false 244 } 245 246 if len(d.Resources) == 0 { 247 return true 248 } 249 250 for _, rd := range d.Resources { 251 if !rd.Empty() { 252 return false 253 } 254 } 255 256 return true 257 } 258 259 // Instances returns the instance diffs for the id given. This can return 260 // multiple instance diffs if there are counts within the resource. 261 func (d *ModuleDiff) Instances(id string) []*InstanceDiff { 262 var result []*InstanceDiff 263 for k, diff := range d.Resources { 264 if k == id || strings.HasPrefix(k, id+".") { 265 if !diff.Empty() { 266 result = append(result, diff) 267 } 268 } 269 } 270 271 return result 272 } 273 274 // IsRoot says whether or not this module diff is for the root module. 275 func (d *ModuleDiff) IsRoot() bool { 276 return reflect.DeepEqual(d.Path, rootModulePath) 277 } 278 279 // String outputs the diff in a long but command-line friendly output 280 // format that users can read to quickly inspect a diff. 281 func (d *ModuleDiff) String() string { 282 var buf bytes.Buffer 283 284 names := make([]string, 0, len(d.Resources)) 285 for name, _ := range d.Resources { 286 names = append(names, name) 287 } 288 sort.Strings(names) 289 290 for _, name := range names { 291 rdiff := d.Resources[name] 292 293 crud := "UPDATE" 294 switch { 295 case rdiff.RequiresNew() && (rdiff.GetDestroy() || rdiff.GetDestroyTainted()): 296 crud = "DESTROY/CREATE" 297 case rdiff.GetDestroy() || rdiff.GetDestroyDeposed(): 298 crud = "DESTROY" 299 case rdiff.RequiresNew(): 300 crud = "CREATE" 301 } 302 303 extra := "" 304 if !rdiff.GetDestroy() && rdiff.GetDestroyDeposed() { 305 extra = " (deposed only)" 306 } 307 308 buf.WriteString(fmt.Sprintf( 309 "%s: %s%s\n", 310 crud, 311 name, 312 extra)) 313 314 keyLen := 0 315 rdiffAttrs := rdiff.CopyAttributes() 316 keys := make([]string, 0, len(rdiffAttrs)) 317 for key, _ := range rdiffAttrs { 318 if key == "id" { 319 continue 320 } 321 322 keys = append(keys, key) 323 if len(key) > keyLen { 324 keyLen = len(key) 325 } 326 } 327 sort.Strings(keys) 328 329 for _, attrK := range keys { 330 attrDiff, _ := rdiff.GetAttribute(attrK) 331 332 v := attrDiff.New 333 u := attrDiff.Old 334 if attrDiff.NewComputed { 335 v = "<computed>" 336 } 337 338 if attrDiff.Sensitive { 339 u = "<sensitive>" 340 v = "<sensitive>" 341 } 342 343 updateMsg := "" 344 if attrDiff.RequiresNew { 345 updateMsg = " (forces new resource)" 346 } else if attrDiff.Sensitive { 347 updateMsg = " (attribute changed)" 348 } 349 350 buf.WriteString(fmt.Sprintf( 351 " %s:%s %#v => %#v%s\n", 352 attrK, 353 strings.Repeat(" ", keyLen-len(attrK)), 354 u, 355 v, 356 updateMsg)) 357 } 358 } 359 360 return buf.String() 361 } 362 363 // InstanceDiff is the diff of a resource from some state to another. 364 type InstanceDiff struct { 365 mu sync.Mutex 366 Attributes map[string]*ResourceAttrDiff 367 Destroy bool 368 DestroyDeposed bool 369 DestroyTainted bool 370 371 // Meta is a simple K/V map that is stored in a diff and persisted to 372 // plans but otherwise is completely ignored by Terraform core. It is 373 // mean to be used for additional data a resource may want to pass through. 374 // The value here must only contain Go primitives and collections. 375 Meta map[string]interface{} 376 } 377 378 func (d *InstanceDiff) Lock() { d.mu.Lock() } 379 func (d *InstanceDiff) Unlock() { d.mu.Unlock() } 380 381 // ResourceAttrDiff is the diff of a single attribute of a resource. 382 type ResourceAttrDiff struct { 383 Old string // Old Value 384 New string // New Value 385 NewComputed bool // True if new value is computed (unknown currently) 386 NewRemoved bool // True if this attribute is being removed 387 NewExtra interface{} // Extra information for the provider 388 RequiresNew bool // True if change requires new resource 389 Sensitive bool // True if the data should not be displayed in UI output 390 Type DiffAttrType 391 } 392 393 // Empty returns true if the diff for this attr is neutral 394 func (d *ResourceAttrDiff) Empty() bool { 395 return d.Old == d.New && !d.NewComputed && !d.NewRemoved 396 } 397 398 func (d *ResourceAttrDiff) GoString() string { 399 return fmt.Sprintf("*%#v", *d) 400 } 401 402 // DiffAttrType is an enum type that says whether a resource attribute 403 // diff is an input attribute (comes from the configuration) or an 404 // output attribute (comes as a result of applying the configuration). An 405 // example input would be "ami" for AWS and an example output would be 406 // "private_ip". 407 type DiffAttrType byte 408 409 const ( 410 DiffAttrUnknown DiffAttrType = iota 411 DiffAttrInput 412 DiffAttrOutput 413 ) 414 415 func (d *InstanceDiff) init() { 416 if d.Attributes == nil { 417 d.Attributes = make(map[string]*ResourceAttrDiff) 418 } 419 } 420 421 func NewInstanceDiff() *InstanceDiff { 422 return &InstanceDiff{Attributes: make(map[string]*ResourceAttrDiff)} 423 } 424 425 func (d *InstanceDiff) Copy() (*InstanceDiff, error) { 426 if d == nil { 427 return nil, nil 428 } 429 430 dCopy, err := copystructure.Config{Lock: true}.Copy(d) 431 if err != nil { 432 return nil, err 433 } 434 435 return dCopy.(*InstanceDiff), nil 436 } 437 438 // ChangeType returns the DiffChangeType represented by the diff 439 // for this single instance. 440 func (d *InstanceDiff) ChangeType() DiffChangeType { 441 if d.Empty() { 442 return DiffNone 443 } 444 445 if d.RequiresNew() && (d.GetDestroy() || d.GetDestroyTainted()) { 446 return DiffDestroyCreate 447 } 448 449 if d.GetDestroy() || d.GetDestroyDeposed() { 450 return DiffDestroy 451 } 452 453 if d.RequiresNew() { 454 return DiffCreate 455 } 456 457 return DiffUpdate 458 } 459 460 // Empty returns true if this diff encapsulates no changes. 461 func (d *InstanceDiff) Empty() bool { 462 if d == nil { 463 return true 464 } 465 466 d.mu.Lock() 467 defer d.mu.Unlock() 468 return !d.Destroy && 469 !d.DestroyTainted && 470 !d.DestroyDeposed && 471 len(d.Attributes) == 0 472 } 473 474 // Equal compares two diffs for exact equality. 475 // 476 // This is different from the Same comparison that is supported which 477 // checks for operation equality taking into account computed values. Equal 478 // instead checks for exact equality. 479 func (d *InstanceDiff) Equal(d2 *InstanceDiff) bool { 480 // If one is nil, they must both be nil 481 if d == nil || d2 == nil { 482 return d == d2 483 } 484 485 // Use DeepEqual 486 return reflect.DeepEqual(d, d2) 487 } 488 489 // DeepCopy performs a deep copy of all parts of the InstanceDiff 490 func (d *InstanceDiff) DeepCopy() *InstanceDiff { 491 copy, err := copystructure.Config{Lock: true}.Copy(d) 492 if err != nil { 493 panic(err) 494 } 495 496 return copy.(*InstanceDiff) 497 } 498 499 func (d *InstanceDiff) GoString() string { 500 return fmt.Sprintf("*%#v", InstanceDiff{ 501 Attributes: d.Attributes, 502 Destroy: d.Destroy, 503 DestroyTainted: d.DestroyTainted, 504 DestroyDeposed: d.DestroyDeposed, 505 }) 506 } 507 508 // RequiresNew returns true if the diff requires the creation of a new 509 // resource (implying the destruction of the old). 510 func (d *InstanceDiff) RequiresNew() bool { 511 if d == nil { 512 return false 513 } 514 515 d.mu.Lock() 516 defer d.mu.Unlock() 517 518 return d.requiresNew() 519 } 520 521 func (d *InstanceDiff) requiresNew() bool { 522 if d == nil { 523 return false 524 } 525 526 if d.DestroyTainted { 527 return true 528 } 529 530 for _, rd := range d.Attributes { 531 if rd != nil && rd.RequiresNew { 532 return true 533 } 534 } 535 536 return false 537 } 538 539 func (d *InstanceDiff) GetDestroyDeposed() bool { 540 d.mu.Lock() 541 defer d.mu.Unlock() 542 543 return d.DestroyDeposed 544 } 545 546 func (d *InstanceDiff) SetDestroyDeposed(b bool) { 547 d.mu.Lock() 548 defer d.mu.Unlock() 549 550 d.DestroyDeposed = b 551 } 552 553 // These methods are properly locked, for use outside other InstanceDiff 554 // methods but everywhere else within in the terraform package. 555 // TODO refactor the locking scheme 556 func (d *InstanceDiff) SetTainted(b bool) { 557 d.mu.Lock() 558 defer d.mu.Unlock() 559 560 d.DestroyTainted = b 561 } 562 563 func (d *InstanceDiff) GetDestroyTainted() bool { 564 d.mu.Lock() 565 defer d.mu.Unlock() 566 567 return d.DestroyTainted 568 } 569 570 func (d *InstanceDiff) SetDestroy(b bool) { 571 d.mu.Lock() 572 defer d.mu.Unlock() 573 574 d.Destroy = b 575 } 576 577 func (d *InstanceDiff) GetDestroy() bool { 578 d.mu.Lock() 579 defer d.mu.Unlock() 580 581 return d.Destroy 582 } 583 584 func (d *InstanceDiff) SetAttribute(key string, attr *ResourceAttrDiff) { 585 d.mu.Lock() 586 defer d.mu.Unlock() 587 588 d.Attributes[key] = attr 589 } 590 591 func (d *InstanceDiff) DelAttribute(key string) { 592 d.mu.Lock() 593 defer d.mu.Unlock() 594 595 delete(d.Attributes, key) 596 } 597 598 func (d *InstanceDiff) GetAttribute(key string) (*ResourceAttrDiff, bool) { 599 d.mu.Lock() 600 defer d.mu.Unlock() 601 602 attr, ok := d.Attributes[key] 603 return attr, ok 604 } 605 func (d *InstanceDiff) GetAttributesLen() int { 606 d.mu.Lock() 607 defer d.mu.Unlock() 608 609 return len(d.Attributes) 610 } 611 612 // Safely copies the Attributes map 613 func (d *InstanceDiff) CopyAttributes() map[string]*ResourceAttrDiff { 614 d.mu.Lock() 615 defer d.mu.Unlock() 616 617 attrs := make(map[string]*ResourceAttrDiff) 618 for k, v := range d.Attributes { 619 attrs[k] = v 620 } 621 622 return attrs 623 } 624 625 // Same checks whether or not two InstanceDiff's are the "same". When 626 // we say "same", it is not necessarily exactly equal. Instead, it is 627 // just checking that the same attributes are changing, a destroy 628 // isn't suddenly happening, etc. 629 func (d *InstanceDiff) Same(d2 *InstanceDiff) (bool, string) { 630 // we can safely compare the pointers without a lock 631 switch { 632 case d == nil && d2 == nil: 633 return true, "" 634 case d == nil || d2 == nil: 635 return false, "one nil" 636 case d == d2: 637 return true, "" 638 } 639 640 d.mu.Lock() 641 defer d.mu.Unlock() 642 643 // If we're going from requiring new to NOT requiring new, then we have 644 // to see if all required news were computed. If so, it is allowed since 645 // computed may also mean "same value and therefore not new". 646 oldNew := d.requiresNew() 647 newNew := d2.RequiresNew() 648 if oldNew && !newNew { 649 oldNew = false 650 651 // This section builds a list of ignorable attributes for requiresNew 652 // by removing off any elements of collections going to zero elements. 653 // For collections going to zero, they may not exist at all in the 654 // new diff (and hence RequiresNew == false). 655 ignoreAttrs := make(map[string]struct{}) 656 for k, diffOld := range d.Attributes { 657 if !strings.HasSuffix(k, ".%") && !strings.HasSuffix(k, ".#") { 658 continue 659 } 660 661 // This case is in here as a protection measure. The bug that this 662 // code originally fixed (GH-11349) didn't have to deal with computed 663 // so I'm not 100% sure what the correct behavior is. Best to leave 664 // the old behavior. 665 if diffOld.NewComputed { 666 continue 667 } 668 669 // We're looking for the case a map goes to exactly 0. 670 if diffOld.New != "0" { 671 continue 672 } 673 674 // Found it! Ignore all of these. The prefix here is stripping 675 // off the "%" so it is just "k." 676 prefix := k[:len(k)-1] 677 for k2, _ := range d.Attributes { 678 if strings.HasPrefix(k2, prefix) { 679 ignoreAttrs[k2] = struct{}{} 680 } 681 } 682 } 683 684 for k, rd := range d.Attributes { 685 if _, ok := ignoreAttrs[k]; ok { 686 continue 687 } 688 689 // If the field is requires new and NOT computed, then what 690 // we have is a diff mismatch for sure. We set that the old 691 // diff does REQUIRE a ForceNew. 692 if rd != nil && rd.RequiresNew && !rd.NewComputed { 693 oldNew = true 694 break 695 } 696 } 697 } 698 699 if oldNew != newNew { 700 return false, fmt.Sprintf( 701 "diff RequiresNew; old: %t, new: %t", oldNew, newNew) 702 } 703 704 // Verify that destroy matches. The second boolean here allows us to 705 // have mismatching Destroy if we're moving from RequiresNew true 706 // to false above. Therefore, the second boolean will only pass if 707 // we're moving from Destroy: true to false as well. 708 if d.Destroy != d2.GetDestroy() && d.requiresNew() == oldNew { 709 return false, fmt.Sprintf( 710 "diff: Destroy; old: %t, new: %t", d.Destroy, d2.GetDestroy()) 711 } 712 713 // Go through the old diff and make sure the new diff has all the 714 // same attributes. To start, build up the check map to be all the keys. 715 checkOld := make(map[string]struct{}) 716 checkNew := make(map[string]struct{}) 717 for k, _ := range d.Attributes { 718 checkOld[k] = struct{}{} 719 } 720 for k, _ := range d2.CopyAttributes() { 721 checkNew[k] = struct{}{} 722 } 723 724 // Make an ordered list so we are sure the approximated hashes are left 725 // to process at the end of the loop 726 keys := make([]string, 0, len(d.Attributes)) 727 for k, _ := range d.Attributes { 728 keys = append(keys, k) 729 } 730 sort.StringSlice(keys).Sort() 731 732 for _, k := range keys { 733 diffOld := d.Attributes[k] 734 735 if _, ok := checkOld[k]; !ok { 736 // We're not checking this key for whatever reason (see where 737 // check is modified). 738 continue 739 } 740 741 // Remove this key since we'll never hit it again 742 delete(checkOld, k) 743 delete(checkNew, k) 744 745 _, ok := d2.GetAttribute(k) 746 if !ok { 747 // If there's no new attribute, and the old diff expected the attribute 748 // to be removed, that's just fine. 749 if diffOld.NewRemoved { 750 continue 751 } 752 753 // If the last diff was a computed value then the absense of 754 // that value is allowed since it may mean the value ended up 755 // being the same. 756 if diffOld.NewComputed { 757 ok = true 758 } 759 760 // No exact match, but maybe this is a set containing computed 761 // values. So check if there is an approximate hash in the key 762 // and if so, try to match the key. 763 if strings.Contains(k, "~") { 764 parts := strings.Split(k, ".") 765 parts2 := append([]string(nil), parts...) 766 767 re := regexp.MustCompile(`^~\d+$`) 768 for i, part := range parts { 769 if re.MatchString(part) { 770 // we're going to consider this the base of a 771 // computed hash, and remove all longer matching fields 772 ok = true 773 774 parts2[i] = `\d+` 775 parts2 = parts2[:i+1] 776 break 777 } 778 } 779 780 re, err := regexp.Compile("^" + strings.Join(parts2, `\.`)) 781 if err != nil { 782 return false, fmt.Sprintf("regexp failed to compile; err: %#v", err) 783 } 784 785 for k2, _ := range checkNew { 786 if re.MatchString(k2) { 787 delete(checkNew, k2) 788 } 789 } 790 } 791 792 // This is a little tricky, but when a diff contains a computed 793 // list, set, or map that can only be interpolated after the apply 794 // command has created the dependent resources, it could turn out 795 // that the result is actually the same as the existing state which 796 // would remove the key from the diff. 797 if diffOld.NewComputed && (strings.HasSuffix(k, ".#") || strings.HasSuffix(k, ".%")) { 798 ok = true 799 } 800 801 // Similarly, in a RequiresNew scenario, a list that shows up in the plan 802 // diff can disappear from the apply diff, which is calculated from an 803 // empty state. 804 if d.requiresNew() && (strings.HasSuffix(k, ".#") || strings.HasSuffix(k, ".%")) { 805 ok = true 806 } 807 808 if !ok { 809 return false, fmt.Sprintf("attribute mismatch: %s", k) 810 } 811 } 812 813 // search for the suffix of the base of a [computed] map, list or set. 814 match := multiVal.FindStringSubmatch(k) 815 816 if diffOld.NewComputed && len(match) == 2 { 817 matchLen := len(match[1]) 818 819 // This is a computed list, set, or map, so remove any keys with 820 // this prefix from the check list. 821 kprefix := k[:len(k)-matchLen] 822 for k2, _ := range checkOld { 823 if strings.HasPrefix(k2, kprefix) { 824 delete(checkOld, k2) 825 } 826 } 827 for k2, _ := range checkNew { 828 if strings.HasPrefix(k2, kprefix) { 829 delete(checkNew, k2) 830 } 831 } 832 } 833 834 // TODO: check for the same value if not computed 835 } 836 837 // Check for leftover attributes 838 if len(checkNew) > 0 { 839 extras := make([]string, 0, len(checkNew)) 840 for attr, _ := range checkNew { 841 extras = append(extras, attr) 842 } 843 return false, 844 fmt.Sprintf("extra attributes: %s", strings.Join(extras, ", ")) 845 } 846 847 return true, "" 848 } 849 850 // moduleDiffSort implements sort.Interface to sort module diffs by path. 851 type moduleDiffSort []*ModuleDiff 852 853 func (s moduleDiffSort) Len() int { return len(s) } 854 func (s moduleDiffSort) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 855 func (s moduleDiffSort) Less(i, j int) bool { 856 a := s[i] 857 b := s[j] 858 859 // If the lengths are different, then the shorter one always wins 860 if len(a.Path) != len(b.Path) { 861 return len(a.Path) < len(b.Path) 862 } 863 864 // Otherwise, compare lexically 865 return strings.Join(a.Path, ".") < strings.Join(b.Path, ".") 866 }