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