github.com/opentofu/opentofu@v1.7.1/internal/legacy/tofu/diff_test.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package tofu 7 8 import ( 9 "fmt" 10 "reflect" 11 "strconv" 12 "strings" 13 "testing" 14 15 "github.com/opentofu/opentofu/internal/addrs" 16 ) 17 18 func TestDiffEmpty(t *testing.T) { 19 var diff *Diff 20 if !diff.Empty() { 21 t.Fatal("should be empty") 22 } 23 24 diff = new(Diff) 25 if !diff.Empty() { 26 t.Fatal("should be empty") 27 } 28 29 mod := diff.AddModule(addrs.RootModuleInstance) 30 mod.Resources["nodeA"] = &InstanceDiff{ 31 Attributes: map[string]*ResourceAttrDiff{ 32 "foo": &ResourceAttrDiff{ 33 Old: "foo", 34 New: "bar", 35 }, 36 }, 37 } 38 39 if diff.Empty() { 40 t.Fatal("should not be empty") 41 } 42 } 43 44 func TestDiffEmpty_taintedIsNotEmpty(t *testing.T) { 45 diff := new(Diff) 46 47 mod := diff.AddModule(addrs.RootModuleInstance) 48 mod.Resources["nodeA"] = &InstanceDiff{ 49 DestroyTainted: true, 50 } 51 52 if diff.Empty() { 53 t.Fatal("should not be empty, since DestroyTainted was set") 54 } 55 } 56 57 func TestDiffEqual(t *testing.T) { 58 cases := map[string]struct { 59 D1, D2 *Diff 60 Equal bool 61 }{ 62 "nil": { 63 nil, 64 new(Diff), 65 false, 66 }, 67 68 "empty": { 69 new(Diff), 70 new(Diff), 71 true, 72 }, 73 74 "different module order": { 75 &Diff{ 76 Modules: []*ModuleDiff{ 77 &ModuleDiff{Path: []string{"root", "foo"}}, 78 &ModuleDiff{Path: []string{"root", "bar"}}, 79 }, 80 }, 81 &Diff{ 82 Modules: []*ModuleDiff{ 83 &ModuleDiff{Path: []string{"root", "bar"}}, 84 &ModuleDiff{Path: []string{"root", "foo"}}, 85 }, 86 }, 87 true, 88 }, 89 90 "different module diff destroys": { 91 &Diff{ 92 Modules: []*ModuleDiff{ 93 &ModuleDiff{Path: []string{"root", "foo"}, Destroy: true}, 94 }, 95 }, 96 &Diff{ 97 Modules: []*ModuleDiff{ 98 &ModuleDiff{Path: []string{"root", "foo"}, Destroy: false}, 99 }, 100 }, 101 true, 102 }, 103 } 104 105 for name, tc := range cases { 106 t.Run(name, func(t *testing.T) { 107 actual := tc.D1.Equal(tc.D2) 108 if actual != tc.Equal { 109 t.Fatalf("expected: %v\n\n%#v\n\n%#v", tc.Equal, tc.D1, tc.D2) 110 } 111 }) 112 } 113 } 114 115 func TestDiffPrune(t *testing.T) { 116 cases := map[string]struct { 117 D1, D2 *Diff 118 }{ 119 "nil": { 120 nil, 121 nil, 122 }, 123 124 "empty": { 125 new(Diff), 126 new(Diff), 127 }, 128 129 "empty module": { 130 &Diff{ 131 Modules: []*ModuleDiff{ 132 &ModuleDiff{Path: []string{"root", "foo"}}, 133 }, 134 }, 135 &Diff{}, 136 }, 137 138 "destroy module": { 139 &Diff{ 140 Modules: []*ModuleDiff{ 141 &ModuleDiff{Path: []string{"root", "foo"}, Destroy: true}, 142 }, 143 }, 144 &Diff{ 145 Modules: []*ModuleDiff{ 146 &ModuleDiff{Path: []string{"root", "foo"}, Destroy: true}, 147 }, 148 }, 149 }, 150 } 151 152 for name, tc := range cases { 153 t.Run(name, func(t *testing.T) { 154 tc.D1.Prune() 155 if !tc.D1.Equal(tc.D2) { 156 t.Fatalf("bad:\n\n%#v\n\n%#v", tc.D1, tc.D2) 157 } 158 }) 159 } 160 } 161 162 func TestModuleDiff_ChangeType(t *testing.T) { 163 cases := []struct { 164 Diff *ModuleDiff 165 Result DiffChangeType 166 }{ 167 { 168 &ModuleDiff{}, 169 DiffNone, 170 }, 171 { 172 &ModuleDiff{ 173 Resources: map[string]*InstanceDiff{ 174 "foo": &InstanceDiff{Destroy: true}, 175 }, 176 }, 177 DiffDestroy, 178 }, 179 { 180 &ModuleDiff{ 181 Resources: map[string]*InstanceDiff{ 182 "foo": &InstanceDiff{ 183 Attributes: map[string]*ResourceAttrDiff{ 184 "foo": &ResourceAttrDiff{ 185 Old: "", 186 New: "bar", 187 }, 188 }, 189 }, 190 }, 191 }, 192 DiffUpdate, 193 }, 194 { 195 &ModuleDiff{ 196 Resources: map[string]*InstanceDiff{ 197 "foo": &InstanceDiff{ 198 Attributes: map[string]*ResourceAttrDiff{ 199 "foo": &ResourceAttrDiff{ 200 Old: "", 201 New: "bar", 202 RequiresNew: true, 203 }, 204 }, 205 }, 206 }, 207 }, 208 DiffCreate, 209 }, 210 { 211 &ModuleDiff{ 212 Resources: map[string]*InstanceDiff{ 213 "foo": &InstanceDiff{ 214 Destroy: true, 215 Attributes: map[string]*ResourceAttrDiff{ 216 "foo": &ResourceAttrDiff{ 217 Old: "", 218 New: "bar", 219 RequiresNew: true, 220 }, 221 }, 222 }, 223 }, 224 }, 225 DiffUpdate, 226 }, 227 } 228 229 for i, tc := range cases { 230 actual := tc.Diff.ChangeType() 231 if actual != tc.Result { 232 t.Fatalf("%d: %#v", i, actual) 233 } 234 } 235 } 236 237 func TestDiff_DeepCopy(t *testing.T) { 238 cases := map[string]*Diff{ 239 "empty": &Diff{}, 240 241 "basic diff": &Diff{ 242 Modules: []*ModuleDiff{ 243 &ModuleDiff{ 244 Path: []string{"root"}, 245 Resources: map[string]*InstanceDiff{ 246 "aws_instance.foo": &InstanceDiff{ 247 Attributes: map[string]*ResourceAttrDiff{ 248 "num": &ResourceAttrDiff{ 249 Old: "0", 250 New: "2", 251 }, 252 }, 253 }, 254 }, 255 }, 256 }, 257 }, 258 } 259 260 for name, tc := range cases { 261 t.Run(name, func(t *testing.T) { 262 dup := tc.DeepCopy() 263 if !reflect.DeepEqual(dup, tc) { 264 t.Fatalf("\n%#v\n\n%#v", dup, tc) 265 } 266 }) 267 } 268 } 269 270 func TestModuleDiff_Empty(t *testing.T) { 271 diff := new(ModuleDiff) 272 if !diff.Empty() { 273 t.Fatal("should be empty") 274 } 275 276 diff.Resources = map[string]*InstanceDiff{ 277 "nodeA": &InstanceDiff{}, 278 } 279 280 if !diff.Empty() { 281 t.Fatal("should be empty") 282 } 283 284 diff.Resources["nodeA"].Attributes = map[string]*ResourceAttrDiff{ 285 "foo": &ResourceAttrDiff{ 286 Old: "foo", 287 New: "bar", 288 }, 289 } 290 291 if diff.Empty() { 292 t.Fatal("should not be empty") 293 } 294 295 diff.Resources["nodeA"].Attributes = nil 296 diff.Resources["nodeA"].Destroy = true 297 298 if diff.Empty() { 299 t.Fatal("should not be empty") 300 } 301 } 302 303 func TestModuleDiff_String(t *testing.T) { 304 diff := &ModuleDiff{ 305 Resources: map[string]*InstanceDiff{ 306 "nodeA": &InstanceDiff{ 307 Attributes: map[string]*ResourceAttrDiff{ 308 "foo": &ResourceAttrDiff{ 309 Old: "foo", 310 New: "bar", 311 }, 312 "bar": &ResourceAttrDiff{ 313 Old: "foo", 314 NewComputed: true, 315 }, 316 "longfoo": &ResourceAttrDiff{ 317 Old: "foo", 318 New: "bar", 319 RequiresNew: true, 320 }, 321 "secretfoo": &ResourceAttrDiff{ 322 Old: "foo", 323 New: "bar", 324 Sensitive: true, 325 }, 326 }, 327 }, 328 }, 329 } 330 331 actual := strings.TrimSpace(diff.String()) 332 expected := strings.TrimSpace(moduleDiffStrBasic) 333 if actual != expected { 334 t.Fatalf("bad:\n%s", actual) 335 } 336 } 337 338 func TestInstanceDiff_ChangeType(t *testing.T) { 339 cases := []struct { 340 Diff *InstanceDiff 341 Result DiffChangeType 342 }{ 343 { 344 &InstanceDiff{}, 345 DiffNone, 346 }, 347 { 348 &InstanceDiff{Destroy: true}, 349 DiffDestroy, 350 }, 351 { 352 &InstanceDiff{ 353 Attributes: map[string]*ResourceAttrDiff{ 354 "foo": &ResourceAttrDiff{ 355 Old: "", 356 New: "bar", 357 }, 358 }, 359 }, 360 DiffUpdate, 361 }, 362 { 363 &InstanceDiff{ 364 Attributes: map[string]*ResourceAttrDiff{ 365 "foo": &ResourceAttrDiff{ 366 Old: "", 367 New: "bar", 368 RequiresNew: true, 369 }, 370 }, 371 }, 372 DiffCreate, 373 }, 374 { 375 &InstanceDiff{ 376 Destroy: true, 377 Attributes: map[string]*ResourceAttrDiff{ 378 "foo": &ResourceAttrDiff{ 379 Old: "", 380 New: "bar", 381 RequiresNew: true, 382 }, 383 }, 384 }, 385 DiffDestroyCreate, 386 }, 387 { 388 &InstanceDiff{ 389 DestroyTainted: true, 390 Attributes: map[string]*ResourceAttrDiff{ 391 "foo": &ResourceAttrDiff{ 392 Old: "", 393 New: "bar", 394 RequiresNew: true, 395 }, 396 }, 397 }, 398 DiffDestroyCreate, 399 }, 400 } 401 402 for i, tc := range cases { 403 actual := tc.Diff.ChangeType() 404 if actual != tc.Result { 405 t.Fatalf("%d: %#v", i, actual) 406 } 407 } 408 } 409 410 func TestInstanceDiff_Empty(t *testing.T) { 411 var rd *InstanceDiff 412 413 if !rd.Empty() { 414 t.Fatal("should be empty") 415 } 416 417 rd = new(InstanceDiff) 418 419 if !rd.Empty() { 420 t.Fatal("should be empty") 421 } 422 423 rd = &InstanceDiff{Destroy: true} 424 425 if rd.Empty() { 426 t.Fatal("should not be empty") 427 } 428 429 rd = &InstanceDiff{ 430 Attributes: map[string]*ResourceAttrDiff{ 431 "foo": &ResourceAttrDiff{ 432 New: "bar", 433 }, 434 }, 435 } 436 437 if rd.Empty() { 438 t.Fatal("should not be empty") 439 } 440 } 441 442 func TestModuleDiff_Instances(t *testing.T) { 443 yesDiff := &InstanceDiff{Destroy: true} 444 noDiff := &InstanceDiff{Destroy: true, DestroyTainted: true} 445 446 cases := []struct { 447 Diff *ModuleDiff 448 Id string 449 Result []*InstanceDiff 450 }{ 451 { 452 &ModuleDiff{ 453 Resources: map[string]*InstanceDiff{ 454 "foo": yesDiff, 455 "bar": noDiff, 456 }, 457 }, 458 "foo", 459 []*InstanceDiff{ 460 yesDiff, 461 }, 462 }, 463 464 { 465 &ModuleDiff{ 466 Resources: map[string]*InstanceDiff{ 467 "foo": yesDiff, 468 "foo.0": yesDiff, 469 "bar": noDiff, 470 }, 471 }, 472 "foo", 473 []*InstanceDiff{ 474 yesDiff, 475 yesDiff, 476 }, 477 }, 478 479 { 480 &ModuleDiff{ 481 Resources: map[string]*InstanceDiff{ 482 "foo": yesDiff, 483 "foo.0": yesDiff, 484 "foo_bar": noDiff, 485 "bar": noDiff, 486 }, 487 }, 488 "foo", 489 []*InstanceDiff{ 490 yesDiff, 491 yesDiff, 492 }, 493 }, 494 } 495 496 for i, tc := range cases { 497 actual := tc.Diff.Instances(tc.Id) 498 if !reflect.DeepEqual(actual, tc.Result) { 499 t.Fatalf("%d: %#v", i, actual) 500 } 501 } 502 } 503 504 func TestInstanceDiff_RequiresNew(t *testing.T) { 505 rd := &InstanceDiff{ 506 Attributes: map[string]*ResourceAttrDiff{ 507 "foo": &ResourceAttrDiff{}, 508 }, 509 } 510 511 if rd.RequiresNew() { 512 t.Fatal("should not require new") 513 } 514 515 rd.Attributes["foo"].RequiresNew = true 516 517 if !rd.RequiresNew() { 518 t.Fatal("should require new") 519 } 520 } 521 522 func TestInstanceDiff_RequiresNew_nil(t *testing.T) { 523 var rd *InstanceDiff 524 525 if rd.RequiresNew() { 526 t.Fatal("should not require new") 527 } 528 } 529 530 func TestInstanceDiffSame(t *testing.T) { 531 cases := []struct { 532 One, Two *InstanceDiff 533 Same bool 534 Reason string 535 }{ 536 { 537 &InstanceDiff{}, 538 &InstanceDiff{}, 539 true, 540 "", 541 }, 542 543 { 544 nil, 545 nil, 546 true, 547 "", 548 }, 549 550 { 551 &InstanceDiff{Destroy: false}, 552 &InstanceDiff{Destroy: true}, 553 false, 554 "diff: Destroy; old: false, new: true", 555 }, 556 557 { 558 &InstanceDiff{Destroy: true}, 559 &InstanceDiff{Destroy: true}, 560 true, 561 "", 562 }, 563 564 { 565 &InstanceDiff{ 566 Attributes: map[string]*ResourceAttrDiff{ 567 "foo": &ResourceAttrDiff{}, 568 }, 569 }, 570 &InstanceDiff{ 571 Attributes: map[string]*ResourceAttrDiff{ 572 "foo": &ResourceAttrDiff{}, 573 }, 574 }, 575 true, 576 "", 577 }, 578 579 { 580 &InstanceDiff{ 581 Attributes: map[string]*ResourceAttrDiff{ 582 "bar": &ResourceAttrDiff{}, 583 }, 584 }, 585 &InstanceDiff{ 586 Attributes: map[string]*ResourceAttrDiff{ 587 "foo": &ResourceAttrDiff{}, 588 }, 589 }, 590 false, 591 "attribute mismatch: bar", 592 }, 593 594 // Extra attributes 595 { 596 &InstanceDiff{ 597 Attributes: map[string]*ResourceAttrDiff{ 598 "foo": &ResourceAttrDiff{}, 599 }, 600 }, 601 &InstanceDiff{ 602 Attributes: map[string]*ResourceAttrDiff{ 603 "foo": &ResourceAttrDiff{}, 604 "bar": &ResourceAttrDiff{}, 605 }, 606 }, 607 false, 608 "extra attributes: bar", 609 }, 610 611 { 612 &InstanceDiff{ 613 Attributes: map[string]*ResourceAttrDiff{ 614 "foo": &ResourceAttrDiff{RequiresNew: true}, 615 }, 616 }, 617 &InstanceDiff{ 618 Attributes: map[string]*ResourceAttrDiff{ 619 "foo": &ResourceAttrDiff{RequiresNew: false}, 620 }, 621 }, 622 false, 623 "diff RequiresNew; old: true, new: false", 624 }, 625 626 // NewComputed on primitive 627 { 628 &InstanceDiff{ 629 Attributes: map[string]*ResourceAttrDiff{ 630 "foo": &ResourceAttrDiff{ 631 Old: "", 632 New: "${var.foo}", 633 NewComputed: true, 634 }, 635 }, 636 }, 637 &InstanceDiff{ 638 Attributes: map[string]*ResourceAttrDiff{ 639 "foo": &ResourceAttrDiff{ 640 Old: "0", 641 New: "1", 642 }, 643 }, 644 }, 645 true, 646 "", 647 }, 648 649 // NewComputed on primitive, removed 650 { 651 &InstanceDiff{ 652 Attributes: map[string]*ResourceAttrDiff{ 653 "foo": &ResourceAttrDiff{ 654 Old: "", 655 New: "${var.foo}", 656 NewComputed: true, 657 }, 658 }, 659 }, 660 &InstanceDiff{ 661 Attributes: map[string]*ResourceAttrDiff{}, 662 }, 663 true, 664 "", 665 }, 666 667 // NewComputed on set, removed 668 { 669 &InstanceDiff{ 670 Attributes: map[string]*ResourceAttrDiff{ 671 "foo.#": &ResourceAttrDiff{ 672 Old: "", 673 New: "", 674 NewComputed: true, 675 }, 676 }, 677 }, 678 &InstanceDiff{ 679 Attributes: map[string]*ResourceAttrDiff{ 680 "foo.1": &ResourceAttrDiff{ 681 Old: "foo", 682 New: "", 683 NewRemoved: true, 684 }, 685 "foo.2": &ResourceAttrDiff{ 686 Old: "", 687 New: "bar", 688 }, 689 }, 690 }, 691 true, 692 "", 693 }, 694 695 { 696 &InstanceDiff{ 697 Attributes: map[string]*ResourceAttrDiff{ 698 "foo.#": &ResourceAttrDiff{NewComputed: true}, 699 }, 700 }, 701 &InstanceDiff{ 702 Attributes: map[string]*ResourceAttrDiff{ 703 "foo.#": &ResourceAttrDiff{ 704 Old: "0", 705 New: "1", 706 }, 707 "foo.0": &ResourceAttrDiff{ 708 Old: "", 709 New: "12", 710 }, 711 }, 712 }, 713 true, 714 "", 715 }, 716 717 { 718 &InstanceDiff{ 719 Attributes: map[string]*ResourceAttrDiff{ 720 "foo.#": &ResourceAttrDiff{ 721 Old: "0", 722 New: "1", 723 }, 724 "foo.~35964334.bar": &ResourceAttrDiff{ 725 Old: "", 726 New: "${var.foo}", 727 }, 728 }, 729 }, 730 &InstanceDiff{ 731 Attributes: map[string]*ResourceAttrDiff{ 732 "foo.#": &ResourceAttrDiff{ 733 Old: "0", 734 New: "1", 735 }, 736 "foo.87654323.bar": &ResourceAttrDiff{ 737 Old: "", 738 New: "12", 739 }, 740 }, 741 }, 742 true, 743 "", 744 }, 745 746 { 747 &InstanceDiff{ 748 Attributes: map[string]*ResourceAttrDiff{ 749 "foo.#": &ResourceAttrDiff{ 750 Old: "0", 751 NewComputed: true, 752 }, 753 }, 754 }, 755 &InstanceDiff{ 756 Attributes: map[string]*ResourceAttrDiff{}, 757 }, 758 true, 759 "", 760 }, 761 762 // Computed can change RequiresNew by removal, and that's okay 763 { 764 &InstanceDiff{ 765 Attributes: map[string]*ResourceAttrDiff{ 766 "foo.#": &ResourceAttrDiff{ 767 Old: "0", 768 NewComputed: true, 769 RequiresNew: true, 770 }, 771 }, 772 }, 773 &InstanceDiff{ 774 Attributes: map[string]*ResourceAttrDiff{}, 775 }, 776 true, 777 "", 778 }, 779 780 // Computed can change Destroy by removal, and that's okay 781 { 782 &InstanceDiff{ 783 Attributes: map[string]*ResourceAttrDiff{ 784 "foo.#": &ResourceAttrDiff{ 785 Old: "0", 786 NewComputed: true, 787 RequiresNew: true, 788 }, 789 }, 790 791 Destroy: true, 792 }, 793 &InstanceDiff{ 794 Attributes: map[string]*ResourceAttrDiff{}, 795 }, 796 true, 797 "", 798 }, 799 800 // Computed can change Destroy by elements 801 { 802 &InstanceDiff{ 803 Attributes: map[string]*ResourceAttrDiff{ 804 "foo.#": &ResourceAttrDiff{ 805 Old: "0", 806 NewComputed: true, 807 RequiresNew: true, 808 }, 809 }, 810 811 Destroy: true, 812 }, 813 &InstanceDiff{ 814 Attributes: map[string]*ResourceAttrDiff{ 815 "foo.#": &ResourceAttrDiff{ 816 Old: "1", 817 New: "1", 818 }, 819 "foo.12": &ResourceAttrDiff{ 820 Old: "4", 821 New: "12", 822 RequiresNew: true, 823 }, 824 }, 825 826 Destroy: true, 827 }, 828 true, 829 "", 830 }, 831 832 // Computed sets may not contain all fields in the original diff, and 833 // because multiple entries for the same set can compute to the same 834 // hash before the values are computed or interpolated, the overall 835 // count can change as well. 836 { 837 &InstanceDiff{ 838 Attributes: map[string]*ResourceAttrDiff{ 839 "foo.#": &ResourceAttrDiff{ 840 Old: "0", 841 New: "1", 842 }, 843 "foo.~35964334.bar": &ResourceAttrDiff{ 844 Old: "", 845 New: "${var.foo}", 846 }, 847 }, 848 }, 849 &InstanceDiff{ 850 Attributes: map[string]*ResourceAttrDiff{ 851 "foo.#": &ResourceAttrDiff{ 852 Old: "0", 853 New: "2", 854 }, 855 "foo.87654323.bar": &ResourceAttrDiff{ 856 Old: "", 857 New: "12", 858 }, 859 "foo.87654325.bar": &ResourceAttrDiff{ 860 Old: "", 861 New: "12", 862 }, 863 "foo.87654325.baz": &ResourceAttrDiff{ 864 Old: "", 865 New: "12", 866 }, 867 }, 868 }, 869 true, 870 "", 871 }, 872 873 // Computed values in maps will fail the "Same" check as well 874 { 875 &InstanceDiff{ 876 Attributes: map[string]*ResourceAttrDiff{ 877 "foo.%": &ResourceAttrDiff{ 878 Old: "", 879 New: "", 880 NewComputed: true, 881 }, 882 }, 883 }, 884 &InstanceDiff{ 885 Attributes: map[string]*ResourceAttrDiff{ 886 "foo.%": &ResourceAttrDiff{ 887 Old: "0", 888 New: "1", 889 NewComputed: false, 890 }, 891 "foo.val": &ResourceAttrDiff{ 892 Old: "", 893 New: "something", 894 }, 895 }, 896 }, 897 true, 898 "", 899 }, 900 901 // In a DESTROY/CREATE scenario, the plan diff will be run against the 902 // state of the old instance, while the apply diff will be run against an 903 // empty state (because the state is cleared when the destroy runs.) 904 // For complex attributes, this can result in keys that seem to disappear 905 // between the two diffs, when in reality everything is working just fine. 906 // 907 // Same() needs to take into account this scenario by analyzing NewRemoved 908 // and treating as "Same" a diff that does indeed have that key removed. 909 { 910 &InstanceDiff{ 911 Attributes: map[string]*ResourceAttrDiff{ 912 "somemap.oldkey": &ResourceAttrDiff{ 913 Old: "long ago", 914 New: "", 915 NewRemoved: true, 916 }, 917 "somemap.newkey": &ResourceAttrDiff{ 918 Old: "", 919 New: "brave new world", 920 }, 921 }, 922 }, 923 &InstanceDiff{ 924 Attributes: map[string]*ResourceAttrDiff{ 925 "somemap.newkey": &ResourceAttrDiff{ 926 Old: "", 927 New: "brave new world", 928 }, 929 }, 930 }, 931 true, 932 "", 933 }, 934 935 // Another thing that can occur in DESTROY/CREATE scenarios is that list 936 // values that are going to zero have diffs that show up at plan time but 937 // are gone at apply time. The NewRemoved handling catches the fields and 938 // treats them as OK, but it also needs to treat the .# field itself as 939 // okay to be present in the old diff but not in the new one. 940 { 941 &InstanceDiff{ 942 Attributes: map[string]*ResourceAttrDiff{ 943 "reqnew": &ResourceAttrDiff{ 944 Old: "old", 945 New: "new", 946 RequiresNew: true, 947 }, 948 "somemap.#": &ResourceAttrDiff{ 949 Old: "1", 950 New: "0", 951 }, 952 "somemap.oldkey": &ResourceAttrDiff{ 953 Old: "long ago", 954 New: "", 955 NewRemoved: true, 956 }, 957 }, 958 }, 959 &InstanceDiff{ 960 Attributes: map[string]*ResourceAttrDiff{ 961 "reqnew": &ResourceAttrDiff{ 962 Old: "", 963 New: "new", 964 RequiresNew: true, 965 }, 966 }, 967 }, 968 true, 969 "", 970 }, 971 972 { 973 &InstanceDiff{ 974 Attributes: map[string]*ResourceAttrDiff{ 975 "reqnew": &ResourceAttrDiff{ 976 Old: "old", 977 New: "new", 978 RequiresNew: true, 979 }, 980 "somemap.%": &ResourceAttrDiff{ 981 Old: "1", 982 New: "0", 983 }, 984 "somemap.oldkey": &ResourceAttrDiff{ 985 Old: "long ago", 986 New: "", 987 NewRemoved: true, 988 }, 989 }, 990 }, 991 &InstanceDiff{ 992 Attributes: map[string]*ResourceAttrDiff{ 993 "reqnew": &ResourceAttrDiff{ 994 Old: "", 995 New: "new", 996 RequiresNew: true, 997 }, 998 }, 999 }, 1000 true, 1001 "", 1002 }, 1003 1004 // Innner computed set should allow outer change in key 1005 { 1006 &InstanceDiff{ 1007 Attributes: map[string]*ResourceAttrDiff{ 1008 "foo.#": &ResourceAttrDiff{ 1009 Old: "0", 1010 New: "1", 1011 }, 1012 "foo.~1.outer_val": &ResourceAttrDiff{ 1013 Old: "", 1014 New: "foo", 1015 }, 1016 "foo.~1.inner.#": &ResourceAttrDiff{ 1017 Old: "0", 1018 New: "1", 1019 }, 1020 "foo.~1.inner.~2.value": &ResourceAttrDiff{ 1021 Old: "", 1022 New: "${var.bar}", 1023 NewComputed: true, 1024 }, 1025 }, 1026 }, 1027 &InstanceDiff{ 1028 Attributes: map[string]*ResourceAttrDiff{ 1029 "foo.#": &ResourceAttrDiff{ 1030 Old: "0", 1031 New: "1", 1032 }, 1033 "foo.12.outer_val": &ResourceAttrDiff{ 1034 Old: "", 1035 New: "foo", 1036 }, 1037 "foo.12.inner.#": &ResourceAttrDiff{ 1038 Old: "0", 1039 New: "1", 1040 }, 1041 "foo.12.inner.42.value": &ResourceAttrDiff{ 1042 Old: "", 1043 New: "baz", 1044 }, 1045 }, 1046 }, 1047 true, 1048 "", 1049 }, 1050 1051 // Innner computed list should allow outer change in key 1052 { 1053 &InstanceDiff{ 1054 Attributes: map[string]*ResourceAttrDiff{ 1055 "foo.#": &ResourceAttrDiff{ 1056 Old: "0", 1057 New: "1", 1058 }, 1059 "foo.~1.outer_val": &ResourceAttrDiff{ 1060 Old: "", 1061 New: "foo", 1062 }, 1063 "foo.~1.inner.#": &ResourceAttrDiff{ 1064 Old: "0", 1065 New: "1", 1066 }, 1067 "foo.~1.inner.0.value": &ResourceAttrDiff{ 1068 Old: "", 1069 New: "${var.bar}", 1070 NewComputed: true, 1071 }, 1072 }, 1073 }, 1074 &InstanceDiff{ 1075 Attributes: map[string]*ResourceAttrDiff{ 1076 "foo.#": &ResourceAttrDiff{ 1077 Old: "0", 1078 New: "1", 1079 }, 1080 "foo.12.outer_val": &ResourceAttrDiff{ 1081 Old: "", 1082 New: "foo", 1083 }, 1084 "foo.12.inner.#": &ResourceAttrDiff{ 1085 Old: "0", 1086 New: "1", 1087 }, 1088 "foo.12.inner.0.value": &ResourceAttrDiff{ 1089 Old: "", 1090 New: "baz", 1091 }, 1092 }, 1093 }, 1094 true, 1095 "", 1096 }, 1097 1098 // When removing all collection items, the diff is allowed to contain 1099 // nothing when re-creating the resource. This should be the "Same" 1100 // since we said we were going from 1 to 0. 1101 { 1102 &InstanceDiff{ 1103 Attributes: map[string]*ResourceAttrDiff{ 1104 "foo.%": &ResourceAttrDiff{ 1105 Old: "1", 1106 New: "0", 1107 RequiresNew: true, 1108 }, 1109 "foo.bar": &ResourceAttrDiff{ 1110 Old: "baz", 1111 New: "", 1112 NewRemoved: true, 1113 RequiresNew: true, 1114 }, 1115 }, 1116 }, 1117 &InstanceDiff{}, 1118 true, 1119 "", 1120 }, 1121 1122 { 1123 &InstanceDiff{ 1124 Attributes: map[string]*ResourceAttrDiff{ 1125 "foo.#": &ResourceAttrDiff{ 1126 Old: "1", 1127 New: "0", 1128 RequiresNew: true, 1129 }, 1130 "foo.0": &ResourceAttrDiff{ 1131 Old: "baz", 1132 New: "", 1133 NewRemoved: true, 1134 RequiresNew: true, 1135 }, 1136 }, 1137 }, 1138 &InstanceDiff{}, 1139 true, 1140 "", 1141 }, 1142 1143 // Make sure that DestroyTainted diffs pass as well, especially when diff 1144 // two works off of no state. 1145 { 1146 &InstanceDiff{ 1147 DestroyTainted: true, 1148 Attributes: map[string]*ResourceAttrDiff{ 1149 "foo": &ResourceAttrDiff{ 1150 Old: "foo", 1151 New: "foo", 1152 }, 1153 }, 1154 }, 1155 &InstanceDiff{ 1156 DestroyTainted: true, 1157 Attributes: map[string]*ResourceAttrDiff{ 1158 "foo": &ResourceAttrDiff{ 1159 Old: "", 1160 New: "foo", 1161 }, 1162 }, 1163 }, 1164 true, 1165 "", 1166 }, 1167 // RequiresNew in different attribute 1168 { 1169 &InstanceDiff{ 1170 Attributes: map[string]*ResourceAttrDiff{ 1171 "foo": &ResourceAttrDiff{ 1172 Old: "foo", 1173 New: "foo", 1174 }, 1175 "bar": &ResourceAttrDiff{ 1176 Old: "bar", 1177 New: "baz", 1178 RequiresNew: true, 1179 }, 1180 }, 1181 }, 1182 &InstanceDiff{ 1183 Attributes: map[string]*ResourceAttrDiff{ 1184 "foo": &ResourceAttrDiff{ 1185 Old: "", 1186 New: "foo", 1187 }, 1188 "bar": &ResourceAttrDiff{ 1189 Old: "", 1190 New: "baz", 1191 RequiresNew: true, 1192 }, 1193 }, 1194 }, 1195 true, 1196 "", 1197 }, 1198 } 1199 1200 for i, tc := range cases { 1201 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 1202 same, reason := tc.One.Same(tc.Two) 1203 if same != tc.Same { 1204 t.Fatalf("%d: expected same: %t, got %t (%s)\n\n one: %#v\n\ntwo: %#v", 1205 i, tc.Same, same, reason, tc.One, tc.Two) 1206 } 1207 if reason != tc.Reason { 1208 t.Fatalf( 1209 "%d: bad reason\n\nexpected: %#v\n\ngot: %#v", i, tc.Reason, reason) 1210 } 1211 }) 1212 } 1213 } 1214 1215 const moduleDiffStrBasic = ` 1216 CREATE: nodeA 1217 bar: "foo" => "<computed>" 1218 foo: "foo" => "bar" 1219 longfoo: "foo" => "bar" (forces new resource) 1220 secretfoo: "<sensitive>" => "<sensitive>" (attribute changed) 1221 ` 1222 1223 func TestCountFlatmapContainerValues(t *testing.T) { 1224 for i, tc := range []struct { 1225 attrs map[string]string 1226 key string 1227 count string 1228 }{ 1229 { 1230 attrs: map[string]string{"set.2.list.#": "9999", "set.2.list.0": "x", "set.2.list.0.z": "y", "set.2.attr": "bar", "set.#": "9999"}, 1231 key: "set.2.list.#", 1232 count: "1", 1233 }, 1234 { 1235 attrs: map[string]string{"set.2.list.#": "9999", "set.2.list.0": "x", "set.2.list.0.z": "y", "set.2.attr": "bar", "set.#": "9999"}, 1236 key: "set.#", 1237 count: "1", 1238 }, 1239 { 1240 attrs: map[string]string{"set.2.list.0": "x", "set.2.list.0.z": "y", "set.2.attr": "bar", "set.#": "9999"}, 1241 key: "set.#", 1242 count: "1", 1243 }, 1244 { 1245 attrs: map[string]string{"map.#": "3", "map.a": "b", "map.a.#": "0", "map.b": "4"}, 1246 key: "map.#", 1247 count: "2", 1248 }, 1249 } { 1250 t.Run(strconv.Itoa(i), func(t *testing.T) { 1251 count := countFlatmapContainerValues(tc.key, tc.attrs) 1252 if count != tc.count { 1253 t.Fatalf("expected %q, got %q", tc.count, count) 1254 } 1255 }) 1256 } 1257 }