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