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