github.com/opentofu/opentofu@v1.7.1/internal/legacy/helper/schema/resource_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 schema 7 8 import ( 9 "encoding/json" 10 "fmt" 11 "reflect" 12 "strconv" 13 "testing" 14 "time" 15 16 "github.com/google/go-cmp/cmp" 17 "github.com/opentofu/opentofu/internal/configs/hcl2shim" 18 "github.com/opentofu/opentofu/internal/legacy/tofu" 19 20 "github.com/zclconf/go-cty/cty" 21 ctyjson "github.com/zclconf/go-cty/cty/json" 22 ) 23 24 func TestResourceApply_create(t *testing.T) { 25 r := &Resource{ 26 SchemaVersion: 2, 27 Schema: map[string]*Schema{ 28 "foo": &Schema{ 29 Type: TypeInt, 30 Optional: true, 31 }, 32 }, 33 } 34 35 called := false 36 r.Create = func(d *ResourceData, m interface{}) error { 37 called = true 38 d.SetId("foo") 39 return nil 40 } 41 42 var s *tofu.InstanceState = nil 43 44 d := &tofu.InstanceDiff{ 45 Attributes: map[string]*tofu.ResourceAttrDiff{ 46 "foo": &tofu.ResourceAttrDiff{ 47 New: "42", 48 }, 49 }, 50 } 51 52 actual, err := r.Apply(s, d, nil) 53 if err != nil { 54 t.Fatalf("err: %s", err) 55 } 56 57 if !called { 58 t.Fatal("not called") 59 } 60 61 expected := &tofu.InstanceState{ 62 ID: "foo", 63 Attributes: map[string]string{ 64 "id": "foo", 65 "foo": "42", 66 }, 67 Meta: map[string]interface{}{ 68 "schema_version": "2", 69 }, 70 } 71 72 if !reflect.DeepEqual(actual, expected) { 73 t.Fatalf("bad: %#v", actual) 74 } 75 } 76 77 func TestResourceApply_Timeout_state(t *testing.T) { 78 r := &Resource{ 79 SchemaVersion: 2, 80 Schema: map[string]*Schema{ 81 "foo": &Schema{ 82 Type: TypeInt, 83 Optional: true, 84 }, 85 }, 86 Timeouts: &ResourceTimeout{ 87 Create: DefaultTimeout(40 * time.Minute), 88 Update: DefaultTimeout(80 * time.Minute), 89 Delete: DefaultTimeout(40 * time.Minute), 90 }, 91 } 92 93 called := false 94 r.Create = func(d *ResourceData, m interface{}) error { 95 called = true 96 d.SetId("foo") 97 return nil 98 } 99 100 var s *tofu.InstanceState = nil 101 102 d := &tofu.InstanceDiff{ 103 Attributes: map[string]*tofu.ResourceAttrDiff{ 104 "foo": &tofu.ResourceAttrDiff{ 105 New: "42", 106 }, 107 }, 108 } 109 110 diffTimeout := &ResourceTimeout{ 111 Create: DefaultTimeout(40 * time.Minute), 112 Update: DefaultTimeout(80 * time.Minute), 113 Delete: DefaultTimeout(40 * time.Minute), 114 } 115 116 if err := diffTimeout.DiffEncode(d); err != nil { 117 t.Fatalf("Error encoding timeout to diff: %s", err) 118 } 119 120 actual, err := r.Apply(s, d, nil) 121 if err != nil { 122 t.Fatalf("err: %s", err) 123 } 124 125 if !called { 126 t.Fatal("not called") 127 } 128 129 expected := &tofu.InstanceState{ 130 ID: "foo", 131 Attributes: map[string]string{ 132 "id": "foo", 133 "foo": "42", 134 }, 135 Meta: map[string]interface{}{ 136 "schema_version": "2", 137 TimeoutKey: expectedForValues(40, 0, 80, 40, 0), 138 }, 139 } 140 141 if !reflect.DeepEqual(actual, expected) { 142 t.Fatalf("Not equal in Timeout State:\n\texpected: %#v\n\tactual: %#v", expected.Meta, actual.Meta) 143 } 144 } 145 146 // Regression test to ensure that the meta data is read from state, if a 147 // resource is destroyed and the timeout meta is no longer available from the 148 // config 149 func TestResourceApply_Timeout_destroy(t *testing.T) { 150 timeouts := &ResourceTimeout{ 151 Create: DefaultTimeout(40 * time.Minute), 152 Update: DefaultTimeout(80 * time.Minute), 153 Delete: DefaultTimeout(40 * time.Minute), 154 } 155 156 r := &Resource{ 157 Schema: map[string]*Schema{ 158 "foo": &Schema{ 159 Type: TypeInt, 160 Optional: true, 161 }, 162 }, 163 Timeouts: timeouts, 164 } 165 166 called := false 167 var delTimeout time.Duration 168 r.Delete = func(d *ResourceData, m interface{}) error { 169 delTimeout = d.Timeout(TimeoutDelete) 170 called = true 171 return nil 172 } 173 174 s := &tofu.InstanceState{ 175 ID: "bar", 176 } 177 178 if err := timeouts.StateEncode(s); err != nil { 179 t.Fatalf("Error encoding to state: %s", err) 180 } 181 182 d := &tofu.InstanceDiff{ 183 Destroy: true, 184 } 185 186 actual, err := r.Apply(s, d, nil) 187 if err != nil { 188 t.Fatalf("err: %s", err) 189 } 190 191 if !called { 192 t.Fatal("delete not called") 193 } 194 195 if *timeouts.Delete != delTimeout { 196 t.Fatalf("timeouts don't match, expected (%#v), got (%#v)", timeouts.Delete, delTimeout) 197 } 198 199 if actual != nil { 200 t.Fatalf("bad: %#v", actual) 201 } 202 } 203 204 func TestResourceDiff_Timeout_diff(t *testing.T) { 205 r := &Resource{ 206 Schema: map[string]*Schema{ 207 "foo": &Schema{ 208 Type: TypeInt, 209 Optional: true, 210 }, 211 }, 212 Timeouts: &ResourceTimeout{ 213 Create: DefaultTimeout(40 * time.Minute), 214 Update: DefaultTimeout(80 * time.Minute), 215 Delete: DefaultTimeout(40 * time.Minute), 216 }, 217 } 218 219 r.Create = func(d *ResourceData, m interface{}) error { 220 d.SetId("foo") 221 return nil 222 } 223 224 conf := tofu.NewResourceConfigRaw( 225 map[string]interface{}{ 226 "foo": 42, 227 TimeoutsConfigKey: map[string]interface{}{ 228 "create": "2h", 229 }, 230 }, 231 ) 232 var s *tofu.InstanceState 233 234 actual, err := r.Diff(s, conf, nil) 235 if err != nil { 236 t.Fatalf("err: %s", err) 237 } 238 239 expected := &tofu.InstanceDiff{ 240 Attributes: map[string]*tofu.ResourceAttrDiff{ 241 "foo": &tofu.ResourceAttrDiff{ 242 New: "42", 243 }, 244 }, 245 } 246 247 diffTimeout := &ResourceTimeout{ 248 Create: DefaultTimeout(120 * time.Minute), 249 Update: DefaultTimeout(80 * time.Minute), 250 Delete: DefaultTimeout(40 * time.Minute), 251 } 252 253 if err := diffTimeout.DiffEncode(expected); err != nil { 254 t.Fatalf("Error encoding timeout to diff: %s", err) 255 } 256 257 if !reflect.DeepEqual(actual, expected) { 258 t.Fatalf("Not equal Meta in Timeout Diff:\n\texpected: %#v\n\tactual: %#v", expected.Meta, actual.Meta) 259 } 260 } 261 262 func TestResourceDiff_CustomizeFunc(t *testing.T) { 263 r := &Resource{ 264 Schema: map[string]*Schema{ 265 "foo": &Schema{ 266 Type: TypeInt, 267 Optional: true, 268 }, 269 }, 270 } 271 272 var called bool 273 274 r.CustomizeDiff = func(d *ResourceDiff, m interface{}) error { 275 called = true 276 return nil 277 } 278 279 conf := tofu.NewResourceConfigRaw( 280 map[string]interface{}{ 281 "foo": 42, 282 }, 283 ) 284 285 var s *tofu.InstanceState 286 287 _, err := r.Diff(s, conf, nil) 288 if err != nil { 289 t.Fatalf("err: %s", err) 290 } 291 292 if !called { 293 t.Fatalf("diff customization not called") 294 } 295 } 296 297 func TestResourceApply_destroy(t *testing.T) { 298 r := &Resource{ 299 Schema: map[string]*Schema{ 300 "foo": &Schema{ 301 Type: TypeInt, 302 Optional: true, 303 }, 304 }, 305 } 306 307 called := false 308 r.Delete = func(d *ResourceData, m interface{}) error { 309 called = true 310 return nil 311 } 312 313 s := &tofu.InstanceState{ 314 ID: "bar", 315 } 316 317 d := &tofu.InstanceDiff{ 318 Destroy: true, 319 } 320 321 actual, err := r.Apply(s, d, nil) 322 if err != nil { 323 t.Fatalf("err: %s", err) 324 } 325 326 if !called { 327 t.Fatal("delete not called") 328 } 329 330 if actual != nil { 331 t.Fatalf("bad: %#v", actual) 332 } 333 } 334 335 func TestResourceApply_destroyCreate(t *testing.T) { 336 r := &Resource{ 337 Schema: map[string]*Schema{ 338 "foo": &Schema{ 339 Type: TypeInt, 340 Optional: true, 341 }, 342 343 "tags": &Schema{ 344 Type: TypeMap, 345 Optional: true, 346 Computed: true, 347 }, 348 }, 349 } 350 351 change := false 352 r.Create = func(d *ResourceData, m interface{}) error { 353 change = d.HasChange("tags") 354 d.SetId("foo") 355 return nil 356 } 357 r.Delete = func(d *ResourceData, m interface{}) error { 358 return nil 359 } 360 361 var s *tofu.InstanceState = &tofu.InstanceState{ 362 ID: "bar", 363 Attributes: map[string]string{ 364 "foo": "bar", 365 "tags.Name": "foo", 366 }, 367 } 368 369 d := &tofu.InstanceDiff{ 370 Attributes: map[string]*tofu.ResourceAttrDiff{ 371 "foo": &tofu.ResourceAttrDiff{ 372 New: "42", 373 RequiresNew: true, 374 }, 375 "tags.Name": &tofu.ResourceAttrDiff{ 376 Old: "foo", 377 New: "foo", 378 RequiresNew: true, 379 }, 380 }, 381 } 382 383 actual, err := r.Apply(s, d, nil) 384 if err != nil { 385 t.Fatalf("err: %s", err) 386 } 387 388 if !change { 389 t.Fatal("should have change") 390 } 391 392 expected := &tofu.InstanceState{ 393 ID: "foo", 394 Attributes: map[string]string{ 395 "id": "foo", 396 "foo": "42", 397 "tags.%": "1", 398 "tags.Name": "foo", 399 }, 400 } 401 402 if !reflect.DeepEqual(actual, expected) { 403 t.Fatalf("bad: %#v", actual) 404 } 405 } 406 407 func TestResourceApply_destroyPartial(t *testing.T) { 408 r := &Resource{ 409 Schema: map[string]*Schema{ 410 "foo": &Schema{ 411 Type: TypeInt, 412 Optional: true, 413 }, 414 }, 415 SchemaVersion: 3, 416 } 417 418 r.Delete = func(d *ResourceData, m interface{}) error { 419 d.Set("foo", 42) 420 return fmt.Errorf("some error") 421 } 422 423 s := &tofu.InstanceState{ 424 ID: "bar", 425 Attributes: map[string]string{ 426 "foo": "12", 427 }, 428 } 429 430 d := &tofu.InstanceDiff{ 431 Destroy: true, 432 } 433 434 actual, err := r.Apply(s, d, nil) 435 if err == nil { 436 t.Fatal("should error") 437 } 438 439 expected := &tofu.InstanceState{ 440 ID: "bar", 441 Attributes: map[string]string{ 442 "id": "bar", 443 "foo": "42", 444 }, 445 Meta: map[string]interface{}{ 446 "schema_version": "3", 447 }, 448 } 449 450 if !reflect.DeepEqual(actual, expected) { 451 t.Fatalf("expected:\n%#v\n\ngot:\n%#v", expected, actual) 452 } 453 } 454 455 func TestResourceApply_update(t *testing.T) { 456 r := &Resource{ 457 Schema: map[string]*Schema{ 458 "foo": &Schema{ 459 Type: TypeInt, 460 Optional: true, 461 }, 462 }, 463 } 464 465 r.Update = func(d *ResourceData, m interface{}) error { 466 d.Set("foo", 42) 467 return nil 468 } 469 470 s := &tofu.InstanceState{ 471 ID: "foo", 472 Attributes: map[string]string{ 473 "foo": "12", 474 }, 475 } 476 477 d := &tofu.InstanceDiff{ 478 Attributes: map[string]*tofu.ResourceAttrDiff{ 479 "foo": &tofu.ResourceAttrDiff{ 480 New: "13", 481 }, 482 }, 483 } 484 485 actual, err := r.Apply(s, d, nil) 486 if err != nil { 487 t.Fatalf("err: %s", err) 488 } 489 490 expected := &tofu.InstanceState{ 491 ID: "foo", 492 Attributes: map[string]string{ 493 "id": "foo", 494 "foo": "42", 495 }, 496 } 497 498 if !reflect.DeepEqual(actual, expected) { 499 t.Fatalf("bad: %#v", actual) 500 } 501 } 502 503 func TestResourceApply_updateNoCallback(t *testing.T) { 504 r := &Resource{ 505 Schema: map[string]*Schema{ 506 "foo": &Schema{ 507 Type: TypeInt, 508 Optional: true, 509 }, 510 }, 511 } 512 513 r.Update = nil 514 515 s := &tofu.InstanceState{ 516 ID: "foo", 517 Attributes: map[string]string{ 518 "foo": "12", 519 }, 520 } 521 522 d := &tofu.InstanceDiff{ 523 Attributes: map[string]*tofu.ResourceAttrDiff{ 524 "foo": &tofu.ResourceAttrDiff{ 525 New: "13", 526 }, 527 }, 528 } 529 530 actual, err := r.Apply(s, d, nil) 531 if err == nil { 532 t.Fatal("should error") 533 } 534 535 expected := &tofu.InstanceState{ 536 ID: "foo", 537 Attributes: map[string]string{ 538 "foo": "12", 539 }, 540 } 541 542 if !reflect.DeepEqual(actual, expected) { 543 t.Fatalf("bad: %#v", actual) 544 } 545 } 546 547 func TestResourceApply_isNewResource(t *testing.T) { 548 r := &Resource{ 549 Schema: map[string]*Schema{ 550 "foo": &Schema{ 551 Type: TypeString, 552 Optional: true, 553 }, 554 }, 555 } 556 557 updateFunc := func(d *ResourceData, m interface{}) error { 558 d.Set("foo", "updated") 559 if d.IsNewResource() { 560 d.Set("foo", "new-resource") 561 } 562 return nil 563 } 564 r.Create = func(d *ResourceData, m interface{}) error { 565 d.SetId("foo") 566 d.Set("foo", "created") 567 return updateFunc(d, m) 568 } 569 r.Update = updateFunc 570 571 d := &tofu.InstanceDiff{ 572 Attributes: map[string]*tofu.ResourceAttrDiff{ 573 "foo": &tofu.ResourceAttrDiff{ 574 New: "bla-blah", 575 }, 576 }, 577 } 578 579 // positive test 580 var s *tofu.InstanceState = nil 581 582 actual, err := r.Apply(s, d, nil) 583 if err != nil { 584 t.Fatalf("err: %s", err) 585 } 586 587 expected := &tofu.InstanceState{ 588 ID: "foo", 589 Attributes: map[string]string{ 590 "id": "foo", 591 "foo": "new-resource", 592 }, 593 } 594 595 if !reflect.DeepEqual(actual, expected) { 596 t.Fatalf("actual: %#v\nexpected: %#v", 597 actual, expected) 598 } 599 600 // negative test 601 s = &tofu.InstanceState{ 602 ID: "foo", 603 Attributes: map[string]string{ 604 "id": "foo", 605 "foo": "new-resource", 606 }, 607 } 608 609 actual, err = r.Apply(s, d, nil) 610 if err != nil { 611 t.Fatalf("err: %s", err) 612 } 613 614 expected = &tofu.InstanceState{ 615 ID: "foo", 616 Attributes: map[string]string{ 617 "id": "foo", 618 "foo": "updated", 619 }, 620 } 621 622 if !reflect.DeepEqual(actual, expected) { 623 t.Fatalf("actual: %#v\nexpected: %#v", 624 actual, expected) 625 } 626 } 627 628 func TestResourceInternalValidate(t *testing.T) { 629 cases := []struct { 630 In *Resource 631 Writable bool 632 Err bool 633 }{ 634 0: { 635 nil, 636 true, 637 true, 638 }, 639 640 // No optional and no required 641 1: { 642 &Resource{ 643 Schema: map[string]*Schema{ 644 "foo": &Schema{ 645 Type: TypeInt, 646 Optional: true, 647 Required: true, 648 }, 649 }, 650 }, 651 true, 652 true, 653 }, 654 655 // Update undefined for non-ForceNew field 656 2: { 657 &Resource{ 658 Create: func(d *ResourceData, meta interface{}) error { return nil }, 659 Schema: map[string]*Schema{ 660 "boo": &Schema{ 661 Type: TypeInt, 662 Optional: true, 663 }, 664 }, 665 }, 666 true, 667 true, 668 }, 669 670 // Update defined for ForceNew field 671 3: { 672 &Resource{ 673 Create: func(d *ResourceData, meta interface{}) error { return nil }, 674 Update: func(d *ResourceData, meta interface{}) error { return nil }, 675 Schema: map[string]*Schema{ 676 "goo": &Schema{ 677 Type: TypeInt, 678 Optional: true, 679 ForceNew: true, 680 }, 681 }, 682 }, 683 true, 684 true, 685 }, 686 687 // non-writable doesn't need Update, Create or Delete 688 4: { 689 &Resource{ 690 Schema: map[string]*Schema{ 691 "goo": &Schema{ 692 Type: TypeInt, 693 Optional: true, 694 }, 695 }, 696 }, 697 false, 698 false, 699 }, 700 701 // non-writable *must not* have Create 702 5: { 703 &Resource{ 704 Create: func(d *ResourceData, meta interface{}) error { return nil }, 705 Schema: map[string]*Schema{ 706 "goo": &Schema{ 707 Type: TypeInt, 708 Optional: true, 709 }, 710 }, 711 }, 712 false, 713 true, 714 }, 715 716 // writable must have Read 717 6: { 718 &Resource{ 719 Create: func(d *ResourceData, meta interface{}) error { return nil }, 720 Update: func(d *ResourceData, meta interface{}) error { return nil }, 721 Delete: func(d *ResourceData, meta interface{}) error { return nil }, 722 Schema: map[string]*Schema{ 723 "goo": &Schema{ 724 Type: TypeInt, 725 Optional: true, 726 }, 727 }, 728 }, 729 true, 730 true, 731 }, 732 733 // writable must have Delete 734 7: { 735 &Resource{ 736 Create: func(d *ResourceData, meta interface{}) error { return nil }, 737 Read: func(d *ResourceData, meta interface{}) error { return nil }, 738 Update: func(d *ResourceData, meta interface{}) error { return nil }, 739 Schema: map[string]*Schema{ 740 "goo": &Schema{ 741 Type: TypeInt, 742 Optional: true, 743 }, 744 }, 745 }, 746 true, 747 true, 748 }, 749 750 8: { // Reserved name at root should be disallowed 751 &Resource{ 752 Create: func(d *ResourceData, meta interface{}) error { return nil }, 753 Read: func(d *ResourceData, meta interface{}) error { return nil }, 754 Update: func(d *ResourceData, meta interface{}) error { return nil }, 755 Delete: func(d *ResourceData, meta interface{}) error { return nil }, 756 Schema: map[string]*Schema{ 757 "count": { 758 Type: TypeInt, 759 Optional: true, 760 }, 761 }, 762 }, 763 true, 764 true, 765 }, 766 767 9: { // Reserved name at nested levels should be allowed 768 &Resource{ 769 Create: func(d *ResourceData, meta interface{}) error { return nil }, 770 Read: func(d *ResourceData, meta interface{}) error { return nil }, 771 Update: func(d *ResourceData, meta interface{}) error { return nil }, 772 Delete: func(d *ResourceData, meta interface{}) error { return nil }, 773 Schema: map[string]*Schema{ 774 "parent_list": &Schema{ 775 Type: TypeString, 776 Optional: true, 777 Elem: &Resource{ 778 Schema: map[string]*Schema{ 779 "provisioner": { 780 Type: TypeString, 781 Optional: true, 782 }, 783 }, 784 }, 785 }, 786 }, 787 }, 788 true, 789 false, 790 }, 791 792 10: { // Provider reserved name should be allowed in resource 793 &Resource{ 794 Create: func(d *ResourceData, meta interface{}) error { return nil }, 795 Read: func(d *ResourceData, meta interface{}) error { return nil }, 796 Update: func(d *ResourceData, meta interface{}) error { return nil }, 797 Delete: func(d *ResourceData, meta interface{}) error { return nil }, 798 Schema: map[string]*Schema{ 799 "alias": &Schema{ 800 Type: TypeString, 801 Optional: true, 802 }, 803 }, 804 }, 805 true, 806 false, 807 }, 808 809 11: { // ID should be allowed in data source 810 &Resource{ 811 Read: func(d *ResourceData, meta interface{}) error { return nil }, 812 Schema: map[string]*Schema{ 813 "id": &Schema{ 814 Type: TypeString, 815 Optional: true, 816 }, 817 }, 818 }, 819 false, 820 false, 821 }, 822 823 12: { // Deprecated ID should be allowed in resource 824 &Resource{ 825 Create: func(d *ResourceData, meta interface{}) error { return nil }, 826 Read: func(d *ResourceData, meta interface{}) error { return nil }, 827 Update: func(d *ResourceData, meta interface{}) error { return nil }, 828 Delete: func(d *ResourceData, meta interface{}) error { return nil }, 829 Schema: map[string]*Schema{ 830 "id": &Schema{ 831 Type: TypeString, 832 Optional: true, 833 Deprecated: "Use x_id instead", 834 }, 835 }, 836 }, 837 true, 838 false, 839 }, 840 841 13: { // non-writable must not define CustomizeDiff 842 &Resource{ 843 Read: func(d *ResourceData, meta interface{}) error { return nil }, 844 Schema: map[string]*Schema{ 845 "goo": &Schema{ 846 Type: TypeInt, 847 Optional: true, 848 }, 849 }, 850 CustomizeDiff: func(*ResourceDiff, interface{}) error { return nil }, 851 }, 852 false, 853 true, 854 }, 855 14: { // Deprecated resource 856 &Resource{ 857 Read: func(d *ResourceData, meta interface{}) error { return nil }, 858 Schema: map[string]*Schema{ 859 "goo": &Schema{ 860 Type: TypeInt, 861 Optional: true, 862 }, 863 }, 864 DeprecationMessage: "This resource has been deprecated.", 865 }, 866 true, 867 true, 868 }, 869 } 870 871 for i, tc := range cases { 872 t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { 873 sm := schemaMap{} 874 if tc.In != nil { 875 sm = schemaMap(tc.In.Schema) 876 } 877 878 err := tc.In.InternalValidate(sm, tc.Writable) 879 if err != nil && !tc.Err { 880 t.Fatalf("%d: expected validation to pass: %s", i, err) 881 } 882 if err == nil && tc.Err { 883 t.Fatalf("%d: expected validation to fail", i) 884 } 885 }) 886 } 887 } 888 889 func TestResourceRefresh(t *testing.T) { 890 r := &Resource{ 891 SchemaVersion: 2, 892 Schema: map[string]*Schema{ 893 "foo": &Schema{ 894 Type: TypeInt, 895 Optional: true, 896 }, 897 }, 898 } 899 900 r.Read = func(d *ResourceData, m interface{}) error { 901 if m != 42 { 902 return fmt.Errorf("meta not passed") 903 } 904 905 return d.Set("foo", d.Get("foo").(int)+1) 906 } 907 908 s := &tofu.InstanceState{ 909 ID: "bar", 910 Attributes: map[string]string{ 911 "foo": "12", 912 }, 913 } 914 915 expected := &tofu.InstanceState{ 916 ID: "bar", 917 Attributes: map[string]string{ 918 "id": "bar", 919 "foo": "13", 920 }, 921 Meta: map[string]interface{}{ 922 "schema_version": "2", 923 }, 924 } 925 926 actual, err := r.Refresh(s, 42) 927 if err != nil { 928 t.Fatalf("err: %s", err) 929 } 930 931 if !reflect.DeepEqual(actual, expected) { 932 t.Fatalf("bad: %#v", actual) 933 } 934 } 935 936 func TestResourceRefresh_blankId(t *testing.T) { 937 r := &Resource{ 938 Schema: map[string]*Schema{ 939 "foo": &Schema{ 940 Type: TypeInt, 941 Optional: true, 942 }, 943 }, 944 } 945 946 r.Read = func(d *ResourceData, m interface{}) error { 947 d.SetId("foo") 948 return nil 949 } 950 951 s := &tofu.InstanceState{ 952 ID: "", 953 Attributes: map[string]string{}, 954 } 955 956 actual, err := r.Refresh(s, 42) 957 if err != nil { 958 t.Fatalf("err: %s", err) 959 } 960 if actual != nil { 961 t.Fatalf("bad: %#v", actual) 962 } 963 } 964 965 func TestResourceRefresh_delete(t *testing.T) { 966 r := &Resource{ 967 Schema: map[string]*Schema{ 968 "foo": &Schema{ 969 Type: TypeInt, 970 Optional: true, 971 }, 972 }, 973 } 974 975 r.Read = func(d *ResourceData, m interface{}) error { 976 d.SetId("") 977 return nil 978 } 979 980 s := &tofu.InstanceState{ 981 ID: "bar", 982 Attributes: map[string]string{ 983 "foo": "12", 984 }, 985 } 986 987 actual, err := r.Refresh(s, 42) 988 if err != nil { 989 t.Fatalf("err: %s", err) 990 } 991 992 if actual != nil { 993 t.Fatalf("bad: %#v", actual) 994 } 995 } 996 997 func TestResourceRefresh_existsError(t *testing.T) { 998 r := &Resource{ 999 Schema: map[string]*Schema{ 1000 "foo": &Schema{ 1001 Type: TypeInt, 1002 Optional: true, 1003 }, 1004 }, 1005 } 1006 1007 r.Exists = func(*ResourceData, interface{}) (bool, error) { 1008 return false, fmt.Errorf("error") 1009 } 1010 1011 r.Read = func(d *ResourceData, m interface{}) error { 1012 panic("shouldn't be called") 1013 } 1014 1015 s := &tofu.InstanceState{ 1016 ID: "bar", 1017 Attributes: map[string]string{ 1018 "foo": "12", 1019 }, 1020 } 1021 1022 actual, err := r.Refresh(s, 42) 1023 if err == nil { 1024 t.Fatalf("should error") 1025 } 1026 if !reflect.DeepEqual(actual, s) { 1027 t.Fatalf("bad: %#v", actual) 1028 } 1029 } 1030 1031 func TestResourceRefresh_noExists(t *testing.T) { 1032 r := &Resource{ 1033 Schema: map[string]*Schema{ 1034 "foo": &Schema{ 1035 Type: TypeInt, 1036 Optional: true, 1037 }, 1038 }, 1039 } 1040 1041 r.Exists = func(*ResourceData, interface{}) (bool, error) { 1042 return false, nil 1043 } 1044 1045 r.Read = func(d *ResourceData, m interface{}) error { 1046 panic("shouldn't be called") 1047 } 1048 1049 s := &tofu.InstanceState{ 1050 ID: "bar", 1051 Attributes: map[string]string{ 1052 "foo": "12", 1053 }, 1054 } 1055 1056 actual, err := r.Refresh(s, 42) 1057 if err != nil { 1058 t.Fatalf("err: %s", err) 1059 } 1060 if actual != nil { 1061 t.Fatalf("should have no state") 1062 } 1063 } 1064 1065 func TestResourceRefresh_needsMigration(t *testing.T) { 1066 // Schema v2 it deals only in newfoo, which tracks foo as an int 1067 r := &Resource{ 1068 SchemaVersion: 2, 1069 Schema: map[string]*Schema{ 1070 "newfoo": &Schema{ 1071 Type: TypeInt, 1072 Optional: true, 1073 }, 1074 }, 1075 } 1076 1077 r.Read = func(d *ResourceData, m interface{}) error { 1078 return d.Set("newfoo", d.Get("newfoo").(int)+1) 1079 } 1080 1081 r.MigrateState = func( 1082 v int, 1083 s *tofu.InstanceState, 1084 meta interface{}) (*tofu.InstanceState, error) { 1085 // Real state migration functions will probably switch on this value, 1086 // but we'll just assert on it for now. 1087 if v != 1 { 1088 t.Fatalf("Expected StateSchemaVersion to be 1, got %d", v) 1089 } 1090 1091 if meta != 42 { 1092 t.Fatal("Expected meta to be passed through to the migration function") 1093 } 1094 1095 oldfoo, err := strconv.ParseFloat(s.Attributes["oldfoo"], 64) 1096 if err != nil { 1097 t.Fatalf("err: %#v", err) 1098 } 1099 s.Attributes["newfoo"] = strconv.Itoa(int(oldfoo * 10)) 1100 delete(s.Attributes, "oldfoo") 1101 1102 return s, nil 1103 } 1104 1105 // State is v1 and deals in oldfoo, which tracked foo as a float at 1/10th 1106 // the scale of newfoo 1107 s := &tofu.InstanceState{ 1108 ID: "bar", 1109 Attributes: map[string]string{ 1110 "oldfoo": "1.2", 1111 }, 1112 Meta: map[string]interface{}{ 1113 "schema_version": "1", 1114 }, 1115 } 1116 1117 actual, err := r.Refresh(s, 42) 1118 if err != nil { 1119 t.Fatalf("err: %s", err) 1120 } 1121 1122 expected := &tofu.InstanceState{ 1123 ID: "bar", 1124 Attributes: map[string]string{ 1125 "id": "bar", 1126 "newfoo": "13", 1127 }, 1128 Meta: map[string]interface{}{ 1129 "schema_version": "2", 1130 }, 1131 } 1132 1133 if !reflect.DeepEqual(actual, expected) { 1134 t.Fatalf("bad:\n\nexpected: %#v\ngot: %#v", expected, actual) 1135 } 1136 } 1137 1138 func TestResourceRefresh_noMigrationNeeded(t *testing.T) { 1139 r := &Resource{ 1140 SchemaVersion: 2, 1141 Schema: map[string]*Schema{ 1142 "newfoo": &Schema{ 1143 Type: TypeInt, 1144 Optional: true, 1145 }, 1146 }, 1147 } 1148 1149 r.Read = func(d *ResourceData, m interface{}) error { 1150 return d.Set("newfoo", d.Get("newfoo").(int)+1) 1151 } 1152 1153 r.MigrateState = func( 1154 v int, 1155 s *tofu.InstanceState, 1156 meta interface{}) (*tofu.InstanceState, error) { 1157 t.Fatal("Migrate function shouldn't be called!") 1158 return nil, nil 1159 } 1160 1161 s := &tofu.InstanceState{ 1162 ID: "bar", 1163 Attributes: map[string]string{ 1164 "newfoo": "12", 1165 }, 1166 Meta: map[string]interface{}{ 1167 "schema_version": "2", 1168 }, 1169 } 1170 1171 actual, err := r.Refresh(s, nil) 1172 if err != nil { 1173 t.Fatalf("err: %s", err) 1174 } 1175 1176 expected := &tofu.InstanceState{ 1177 ID: "bar", 1178 Attributes: map[string]string{ 1179 "id": "bar", 1180 "newfoo": "13", 1181 }, 1182 Meta: map[string]interface{}{ 1183 "schema_version": "2", 1184 }, 1185 } 1186 1187 if !reflect.DeepEqual(actual, expected) { 1188 t.Fatalf("bad:\n\nexpected: %#v\ngot: %#v", expected, actual) 1189 } 1190 } 1191 1192 func TestResourceRefresh_stateSchemaVersionUnset(t *testing.T) { 1193 r := &Resource{ 1194 // Version 1 > Version 0 1195 SchemaVersion: 1, 1196 Schema: map[string]*Schema{ 1197 "newfoo": &Schema{ 1198 Type: TypeInt, 1199 Optional: true, 1200 }, 1201 }, 1202 } 1203 1204 r.Read = func(d *ResourceData, m interface{}) error { 1205 return d.Set("newfoo", d.Get("newfoo").(int)+1) 1206 } 1207 1208 r.MigrateState = func( 1209 v int, 1210 s *tofu.InstanceState, 1211 meta interface{}) (*tofu.InstanceState, error) { 1212 s.Attributes["newfoo"] = s.Attributes["oldfoo"] 1213 return s, nil 1214 } 1215 1216 s := &tofu.InstanceState{ 1217 ID: "bar", 1218 Attributes: map[string]string{ 1219 "oldfoo": "12", 1220 }, 1221 } 1222 1223 actual, err := r.Refresh(s, nil) 1224 if err != nil { 1225 t.Fatalf("err: %s", err) 1226 } 1227 1228 expected := &tofu.InstanceState{ 1229 ID: "bar", 1230 Attributes: map[string]string{ 1231 "id": "bar", 1232 "newfoo": "13", 1233 }, 1234 Meta: map[string]interface{}{ 1235 "schema_version": "1", 1236 }, 1237 } 1238 1239 if !reflect.DeepEqual(actual, expected) { 1240 t.Fatalf("bad:\n\nexpected: %#v\ngot: %#v", expected, actual) 1241 } 1242 } 1243 1244 func TestResourceRefresh_migrateStateErr(t *testing.T) { 1245 r := &Resource{ 1246 SchemaVersion: 2, 1247 Schema: map[string]*Schema{ 1248 "newfoo": &Schema{ 1249 Type: TypeInt, 1250 Optional: true, 1251 }, 1252 }, 1253 } 1254 1255 r.Read = func(d *ResourceData, m interface{}) error { 1256 t.Fatal("Read should never be called!") 1257 return nil 1258 } 1259 1260 r.MigrateState = func( 1261 v int, 1262 s *tofu.InstanceState, 1263 meta interface{}) (*tofu.InstanceState, error) { 1264 return s, fmt.Errorf("triggering an error") 1265 } 1266 1267 s := &tofu.InstanceState{ 1268 ID: "bar", 1269 Attributes: map[string]string{ 1270 "oldfoo": "12", 1271 }, 1272 } 1273 1274 _, err := r.Refresh(s, nil) 1275 if err == nil { 1276 t.Fatal("expected error, but got none!") 1277 } 1278 } 1279 1280 func TestResourceData(t *testing.T) { 1281 r := &Resource{ 1282 SchemaVersion: 2, 1283 Schema: map[string]*Schema{ 1284 "foo": &Schema{ 1285 Type: TypeInt, 1286 Optional: true, 1287 }, 1288 }, 1289 } 1290 1291 state := &tofu.InstanceState{ 1292 ID: "foo", 1293 Attributes: map[string]string{ 1294 "id": "foo", 1295 "foo": "42", 1296 }, 1297 } 1298 1299 data := r.Data(state) 1300 if data.Id() != "foo" { 1301 t.Fatalf("err: %s", data.Id()) 1302 } 1303 if v := data.Get("foo"); v != 42 { 1304 t.Fatalf("bad: %#v", v) 1305 } 1306 1307 // Set expectations 1308 state.Meta = map[string]interface{}{ 1309 "schema_version": "2", 1310 } 1311 1312 result := data.State() 1313 if !reflect.DeepEqual(result, state) { 1314 t.Fatalf("bad: %#v", result) 1315 } 1316 } 1317 1318 func TestResourceData_blank(t *testing.T) { 1319 r := &Resource{ 1320 SchemaVersion: 2, 1321 Schema: map[string]*Schema{ 1322 "foo": &Schema{ 1323 Type: TypeInt, 1324 Optional: true, 1325 }, 1326 }, 1327 } 1328 1329 data := r.Data(nil) 1330 if data.Id() != "" { 1331 t.Fatalf("err: %s", data.Id()) 1332 } 1333 if v := data.Get("foo"); v != 0 { 1334 t.Fatalf("bad: %#v", v) 1335 } 1336 } 1337 1338 func TestResourceData_timeouts(t *testing.T) { 1339 one := 1 * time.Second 1340 two := 2 * time.Second 1341 three := 3 * time.Second 1342 four := 4 * time.Second 1343 five := 5 * time.Second 1344 1345 timeouts := &ResourceTimeout{ 1346 Create: &one, 1347 Read: &two, 1348 Update: &three, 1349 Delete: &four, 1350 Default: &five, 1351 } 1352 1353 r := &Resource{ 1354 SchemaVersion: 2, 1355 Schema: map[string]*Schema{ 1356 "foo": &Schema{ 1357 Type: TypeInt, 1358 Optional: true, 1359 }, 1360 }, 1361 Timeouts: timeouts, 1362 } 1363 1364 data := r.Data(nil) 1365 if data.Id() != "" { 1366 t.Fatalf("err: %s", data.Id()) 1367 } 1368 1369 if !reflect.DeepEqual(timeouts, data.timeouts) { 1370 t.Fatalf("incorrect ResourceData timeouts: %#v\n", *data.timeouts) 1371 } 1372 } 1373 1374 func TestResource_UpgradeState(t *testing.T) { 1375 // While this really only calls itself and therefore doesn't test any of 1376 // the Resource code directly, it still serves as an example of registering 1377 // a StateUpgrader. 1378 r := &Resource{ 1379 SchemaVersion: 2, 1380 Schema: map[string]*Schema{ 1381 "newfoo": &Schema{ 1382 Type: TypeInt, 1383 Optional: true, 1384 }, 1385 }, 1386 } 1387 1388 r.StateUpgraders = []StateUpgrader{ 1389 { 1390 Version: 1, 1391 Type: cty.Object(map[string]cty.Type{ 1392 "id": cty.String, 1393 "oldfoo": cty.Number, 1394 }), 1395 Upgrade: func(m map[string]interface{}, meta interface{}) (map[string]interface{}, error) { 1396 1397 oldfoo, ok := m["oldfoo"].(float64) 1398 if !ok { 1399 t.Fatalf("expected 1.2, got %#v", m["oldfoo"]) 1400 } 1401 m["newfoo"] = int(oldfoo * 10) 1402 delete(m, "oldfoo") 1403 1404 return m, nil 1405 }, 1406 }, 1407 } 1408 1409 oldStateAttrs := map[string]string{ 1410 "id": "bar", 1411 "oldfoo": "1.2", 1412 } 1413 1414 // convert the legacy flatmap state to the json equivalent 1415 ty := r.StateUpgraders[0].Type 1416 val, err := hcl2shim.HCL2ValueFromFlatmap(oldStateAttrs, ty) 1417 if err != nil { 1418 t.Fatal(err) 1419 } 1420 js, err := ctyjson.Marshal(val, ty) 1421 if err != nil { 1422 t.Fatal(err) 1423 } 1424 1425 // unmarshal the state using the json default types 1426 var m map[string]interface{} 1427 if err := json.Unmarshal(js, &m); err != nil { 1428 t.Fatal(err) 1429 } 1430 1431 actual, err := r.StateUpgraders[0].Upgrade(m, nil) 1432 if err != nil { 1433 t.Fatalf("err: %s", err) 1434 } 1435 1436 expected := map[string]interface{}{ 1437 "id": "bar", 1438 "newfoo": 12, 1439 } 1440 1441 if !reflect.DeepEqual(expected, actual) { 1442 t.Fatalf("expected: %#v\ngot: %#v\n", expected, actual) 1443 } 1444 } 1445 1446 func TestResource_ValidateUpgradeState(t *testing.T) { 1447 r := &Resource{ 1448 SchemaVersion: 3, 1449 Schema: map[string]*Schema{ 1450 "newfoo": &Schema{ 1451 Type: TypeInt, 1452 Optional: true, 1453 }, 1454 }, 1455 } 1456 1457 if err := r.InternalValidate(nil, true); err != nil { 1458 t.Fatal(err) 1459 } 1460 1461 r.StateUpgraders = append(r.StateUpgraders, StateUpgrader{ 1462 Version: 2, 1463 Type: cty.Object(map[string]cty.Type{ 1464 "id": cty.String, 1465 }), 1466 Upgrade: func(m map[string]interface{}, _ interface{}) (map[string]interface{}, error) { 1467 return m, nil 1468 }, 1469 }) 1470 if err := r.InternalValidate(nil, true); err != nil { 1471 t.Fatal(err) 1472 } 1473 1474 // check for missing type 1475 r.StateUpgraders[0].Type = cty.Type{} 1476 if err := r.InternalValidate(nil, true); err == nil { 1477 t.Fatal("StateUpgrader must have type") 1478 } 1479 r.StateUpgraders[0].Type = cty.Object(map[string]cty.Type{ 1480 "id": cty.String, 1481 }) 1482 1483 // check for missing Upgrade func 1484 r.StateUpgraders[0].Upgrade = nil 1485 if err := r.InternalValidate(nil, true); err == nil { 1486 t.Fatal("StateUpgrader must have an Upgrade func") 1487 } 1488 r.StateUpgraders[0].Upgrade = func(m map[string]interface{}, _ interface{}) (map[string]interface{}, error) { 1489 return m, nil 1490 } 1491 1492 // check for skipped version 1493 r.StateUpgraders[0].Version = 0 1494 r.StateUpgraders = append(r.StateUpgraders, StateUpgrader{ 1495 Version: 2, 1496 Type: cty.Object(map[string]cty.Type{ 1497 "id": cty.String, 1498 }), 1499 Upgrade: func(m map[string]interface{}, _ interface{}) (map[string]interface{}, error) { 1500 return m, nil 1501 }, 1502 }) 1503 if err := r.InternalValidate(nil, true); err == nil { 1504 t.Fatal("StateUpgraders cannot skip versions") 1505 } 1506 1507 // add the missing version, but fail because it's still out of order 1508 r.StateUpgraders = append(r.StateUpgraders, StateUpgrader{ 1509 Version: 1, 1510 Type: cty.Object(map[string]cty.Type{ 1511 "id": cty.String, 1512 }), 1513 Upgrade: func(m map[string]interface{}, _ interface{}) (map[string]interface{}, error) { 1514 return m, nil 1515 }, 1516 }) 1517 if err := r.InternalValidate(nil, true); err == nil { 1518 t.Fatal("upgraders must be defined in order") 1519 } 1520 1521 r.StateUpgraders[1], r.StateUpgraders[2] = r.StateUpgraders[2], r.StateUpgraders[1] 1522 if err := r.InternalValidate(nil, true); err != nil { 1523 t.Fatal(err) 1524 } 1525 1526 // can't add an upgrader for a schema >= the current version 1527 r.StateUpgraders = append(r.StateUpgraders, StateUpgrader{ 1528 Version: 3, 1529 Type: cty.Object(map[string]cty.Type{ 1530 "id": cty.String, 1531 }), 1532 Upgrade: func(m map[string]interface{}, _ interface{}) (map[string]interface{}, error) { 1533 return m, nil 1534 }, 1535 }) 1536 if err := r.InternalValidate(nil, true); err == nil { 1537 t.Fatal("StateUpgraders cannot have a version >= current SchemaVersion") 1538 } 1539 } 1540 1541 // The legacy provider will need to be able to handle both types of schema 1542 // transformations, which has been retrofitted into the Refresh method. 1543 func TestResource_migrateAndUpgrade(t *testing.T) { 1544 r := &Resource{ 1545 SchemaVersion: 4, 1546 Schema: map[string]*Schema{ 1547 "four": { 1548 Type: TypeInt, 1549 Required: true, 1550 }, 1551 }, 1552 // this MigrateState will take the state to version 2 1553 MigrateState: func(v int, is *tofu.InstanceState, _ interface{}) (*tofu.InstanceState, error) { 1554 switch v { 1555 case 0: 1556 _, ok := is.Attributes["zero"] 1557 if !ok { 1558 return nil, fmt.Errorf("zero not found in %#v", is.Attributes) 1559 } 1560 is.Attributes["one"] = "1" 1561 delete(is.Attributes, "zero") 1562 fallthrough 1563 case 1: 1564 _, ok := is.Attributes["one"] 1565 if !ok { 1566 return nil, fmt.Errorf("one not found in %#v", is.Attributes) 1567 } 1568 is.Attributes["two"] = "2" 1569 delete(is.Attributes, "one") 1570 default: 1571 return nil, fmt.Errorf("invalid schema version %d", v) 1572 } 1573 return is, nil 1574 }, 1575 } 1576 1577 r.Read = func(d *ResourceData, m interface{}) error { 1578 return d.Set("four", 4) 1579 } 1580 1581 r.StateUpgraders = []StateUpgrader{ 1582 { 1583 Version: 2, 1584 Type: cty.Object(map[string]cty.Type{ 1585 "id": cty.String, 1586 "two": cty.Number, 1587 }), 1588 Upgrade: func(m map[string]interface{}, meta interface{}) (map[string]interface{}, error) { 1589 _, ok := m["two"].(float64) 1590 if !ok { 1591 return nil, fmt.Errorf("two not found in %#v", m) 1592 } 1593 m["three"] = float64(3) 1594 delete(m, "two") 1595 return m, nil 1596 }, 1597 }, 1598 { 1599 Version: 3, 1600 Type: cty.Object(map[string]cty.Type{ 1601 "id": cty.String, 1602 "three": cty.Number, 1603 }), 1604 Upgrade: func(m map[string]interface{}, meta interface{}) (map[string]interface{}, error) { 1605 _, ok := m["three"].(float64) 1606 if !ok { 1607 return nil, fmt.Errorf("three not found in %#v", m) 1608 } 1609 m["four"] = float64(4) 1610 delete(m, "three") 1611 return m, nil 1612 }, 1613 }, 1614 } 1615 1616 testStates := []*tofu.InstanceState{ 1617 { 1618 ID: "bar", 1619 Attributes: map[string]string{ 1620 "id": "bar", 1621 "zero": "0", 1622 }, 1623 Meta: map[string]interface{}{ 1624 "schema_version": "0", 1625 }, 1626 }, 1627 { 1628 ID: "bar", 1629 Attributes: map[string]string{ 1630 "id": "bar", 1631 "one": "1", 1632 }, 1633 Meta: map[string]interface{}{ 1634 "schema_version": "1", 1635 }, 1636 }, 1637 { 1638 ID: "bar", 1639 Attributes: map[string]string{ 1640 "id": "bar", 1641 "two": "2", 1642 }, 1643 Meta: map[string]interface{}{ 1644 "schema_version": "2", 1645 }, 1646 }, 1647 { 1648 ID: "bar", 1649 Attributes: map[string]string{ 1650 "id": "bar", 1651 "three": "3", 1652 }, 1653 Meta: map[string]interface{}{ 1654 "schema_version": "3", 1655 }, 1656 }, 1657 { 1658 ID: "bar", 1659 Attributes: map[string]string{ 1660 "id": "bar", 1661 "four": "4", 1662 }, 1663 Meta: map[string]interface{}{ 1664 "schema_version": "4", 1665 }, 1666 }, 1667 } 1668 1669 for i, s := range testStates { 1670 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 1671 newState, err := r.Refresh(s, nil) 1672 if err != nil { 1673 t.Fatal(err) 1674 } 1675 1676 expected := &tofu.InstanceState{ 1677 ID: "bar", 1678 Attributes: map[string]string{ 1679 "id": "bar", 1680 "four": "4", 1681 }, 1682 Meta: map[string]interface{}{ 1683 "schema_version": "4", 1684 }, 1685 } 1686 1687 if !cmp.Equal(expected, newState, equateEmpty) { 1688 t.Fatal(cmp.Diff(expected, newState, equateEmpty)) 1689 } 1690 }) 1691 } 1692 }