github.com/mingfang/terraform@v0.11.12-beta1/helper/schema/schema_test.go (about) 1 package schema 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "os" 8 "reflect" 9 "sort" 10 "strconv" 11 "strings" 12 "testing" 13 14 "github.com/hashicorp/hil" 15 "github.com/hashicorp/hil/ast" 16 "github.com/hashicorp/terraform/config" 17 "github.com/hashicorp/terraform/helper/hashcode" 18 "github.com/hashicorp/terraform/terraform" 19 ) 20 21 func TestEnvDefaultFunc(t *testing.T) { 22 key := "TF_TEST_ENV_DEFAULT_FUNC" 23 defer os.Unsetenv(key) 24 25 f := EnvDefaultFunc(key, "42") 26 if err := os.Setenv(key, "foo"); err != nil { 27 t.Fatalf("err: %s", err) 28 } 29 30 actual, err := f() 31 if err != nil { 32 t.Fatalf("err: %s", err) 33 } 34 if actual != "foo" { 35 t.Fatalf("bad: %#v", actual) 36 } 37 38 if err := os.Unsetenv(key); err != nil { 39 t.Fatalf("err: %s", err) 40 } 41 42 actual, err = f() 43 if err != nil { 44 t.Fatalf("err: %s", err) 45 } 46 if actual != "42" { 47 t.Fatalf("bad: %#v", actual) 48 } 49 } 50 51 func TestMultiEnvDefaultFunc(t *testing.T) { 52 keys := []string{ 53 "TF_TEST_MULTI_ENV_DEFAULT_FUNC1", 54 "TF_TEST_MULTI_ENV_DEFAULT_FUNC2", 55 } 56 defer func() { 57 for _, k := range keys { 58 os.Unsetenv(k) 59 } 60 }() 61 62 // Test that the first key is returned first 63 f := MultiEnvDefaultFunc(keys, "42") 64 if err := os.Setenv(keys[0], "foo"); err != nil { 65 t.Fatalf("err: %s", err) 66 } 67 68 actual, err := f() 69 if err != nil { 70 t.Fatalf("err: %s", err) 71 } 72 if actual != "foo" { 73 t.Fatalf("bad: %#v", actual) 74 } 75 76 if err := os.Unsetenv(keys[0]); err != nil { 77 t.Fatalf("err: %s", err) 78 } 79 80 // Test that the second key is returned if the first one is empty 81 f = MultiEnvDefaultFunc(keys, "42") 82 if err := os.Setenv(keys[1], "foo"); err != nil { 83 t.Fatalf("err: %s", err) 84 } 85 86 actual, err = f() 87 if err != nil { 88 t.Fatalf("err: %s", err) 89 } 90 if actual != "foo" { 91 t.Fatalf("bad: %#v", actual) 92 } 93 94 if err := os.Unsetenv(keys[1]); err != nil { 95 t.Fatalf("err: %s", err) 96 } 97 98 // Test that the default value is returned when no keys are set 99 actual, err = f() 100 if err != nil { 101 t.Fatalf("err: %s", err) 102 } 103 if actual != "42" { 104 t.Fatalf("bad: %#v", actual) 105 } 106 } 107 108 func TestValueType_Zero(t *testing.T) { 109 cases := []struct { 110 Type ValueType 111 Value interface{} 112 }{ 113 {TypeBool, false}, 114 {TypeInt, 0}, 115 {TypeFloat, 0.0}, 116 {TypeString, ""}, 117 {TypeList, []interface{}{}}, 118 {TypeMap, map[string]interface{}{}}, 119 {TypeSet, new(Set)}, 120 } 121 122 for i, tc := range cases { 123 actual := tc.Type.Zero() 124 if !reflect.DeepEqual(actual, tc.Value) { 125 t.Fatalf("%d: %#v != %#v", i, actual, tc.Value) 126 } 127 } 128 } 129 130 func interfaceToVariableSwallowError(input interface{}) ast.Variable { 131 variable, _ := hil.InterfaceToVariable(input) 132 return variable 133 } 134 135 func TestSchemaMap_Diff(t *testing.T) { 136 cases := []struct { 137 Name string 138 Schema map[string]*Schema 139 State *terraform.InstanceState 140 Config map[string]interface{} 141 ConfigVariables map[string]ast.Variable 142 CustomizeDiff CustomizeDiffFunc 143 Diff *terraform.InstanceDiff 144 Err bool 145 }{ 146 { 147 Schema: map[string]*Schema{ 148 "availability_zone": &Schema{ 149 Type: TypeString, 150 Optional: true, 151 Computed: true, 152 ForceNew: true, 153 }, 154 }, 155 156 State: nil, 157 158 Config: map[string]interface{}{ 159 "availability_zone": "foo", 160 }, 161 162 Diff: &terraform.InstanceDiff{ 163 Attributes: map[string]*terraform.ResourceAttrDiff{ 164 "availability_zone": &terraform.ResourceAttrDiff{ 165 Old: "", 166 New: "foo", 167 RequiresNew: true, 168 }, 169 }, 170 }, 171 172 Err: false, 173 }, 174 175 { 176 Schema: map[string]*Schema{ 177 "availability_zone": &Schema{ 178 Type: TypeString, 179 Optional: true, 180 Computed: true, 181 ForceNew: true, 182 }, 183 }, 184 185 State: nil, 186 187 Config: map[string]interface{}{}, 188 189 Diff: &terraform.InstanceDiff{ 190 Attributes: map[string]*terraform.ResourceAttrDiff{ 191 "availability_zone": &terraform.ResourceAttrDiff{ 192 Old: "", 193 NewComputed: true, 194 RequiresNew: true, 195 }, 196 }, 197 }, 198 199 Err: false, 200 }, 201 202 { 203 Schema: map[string]*Schema{ 204 "availability_zone": &Schema{ 205 Type: TypeString, 206 Optional: true, 207 Computed: true, 208 ForceNew: true, 209 }, 210 }, 211 212 State: &terraform.InstanceState{ 213 ID: "foo", 214 }, 215 216 Config: map[string]interface{}{}, 217 218 Diff: nil, 219 220 Err: false, 221 }, 222 223 { 224 Name: "Computed, but set in config", 225 Schema: map[string]*Schema{ 226 "availability_zone": &Schema{ 227 Type: TypeString, 228 Optional: true, 229 Computed: true, 230 }, 231 }, 232 233 State: &terraform.InstanceState{ 234 Attributes: map[string]string{ 235 "availability_zone": "foo", 236 }, 237 }, 238 239 Config: map[string]interface{}{ 240 "availability_zone": "bar", 241 }, 242 243 Diff: &terraform.InstanceDiff{ 244 Attributes: map[string]*terraform.ResourceAttrDiff{ 245 "availability_zone": &terraform.ResourceAttrDiff{ 246 Old: "foo", 247 New: "bar", 248 }, 249 }, 250 }, 251 252 Err: false, 253 }, 254 255 { 256 Name: "Default", 257 Schema: map[string]*Schema{ 258 "availability_zone": &Schema{ 259 Type: TypeString, 260 Optional: true, 261 Default: "foo", 262 }, 263 }, 264 265 State: nil, 266 267 Config: nil, 268 269 Diff: &terraform.InstanceDiff{ 270 Attributes: map[string]*terraform.ResourceAttrDiff{ 271 "availability_zone": &terraform.ResourceAttrDiff{ 272 Old: "", 273 New: "foo", 274 }, 275 }, 276 }, 277 278 Err: false, 279 }, 280 281 { 282 Name: "DefaultFunc, value", 283 Schema: map[string]*Schema{ 284 "availability_zone": &Schema{ 285 Type: TypeString, 286 Optional: true, 287 DefaultFunc: func() (interface{}, error) { 288 return "foo", nil 289 }, 290 }, 291 }, 292 293 State: nil, 294 295 Config: nil, 296 297 Diff: &terraform.InstanceDiff{ 298 Attributes: map[string]*terraform.ResourceAttrDiff{ 299 "availability_zone": &terraform.ResourceAttrDiff{ 300 Old: "", 301 New: "foo", 302 }, 303 }, 304 }, 305 306 Err: false, 307 }, 308 309 { 310 Name: "DefaultFunc, configuration set", 311 Schema: map[string]*Schema{ 312 "availability_zone": &Schema{ 313 Type: TypeString, 314 Optional: true, 315 DefaultFunc: func() (interface{}, error) { 316 return "foo", nil 317 }, 318 }, 319 }, 320 321 State: nil, 322 323 Config: map[string]interface{}{ 324 "availability_zone": "bar", 325 }, 326 327 Diff: &terraform.InstanceDiff{ 328 Attributes: map[string]*terraform.ResourceAttrDiff{ 329 "availability_zone": &terraform.ResourceAttrDiff{ 330 Old: "", 331 New: "bar", 332 }, 333 }, 334 }, 335 336 Err: false, 337 }, 338 339 { 340 Name: "String with StateFunc", 341 Schema: map[string]*Schema{ 342 "availability_zone": &Schema{ 343 Type: TypeString, 344 Optional: true, 345 Computed: true, 346 StateFunc: func(a interface{}) string { 347 return a.(string) + "!" 348 }, 349 }, 350 }, 351 352 State: nil, 353 354 Config: map[string]interface{}{ 355 "availability_zone": "foo", 356 }, 357 358 Diff: &terraform.InstanceDiff{ 359 Attributes: map[string]*terraform.ResourceAttrDiff{ 360 "availability_zone": &terraform.ResourceAttrDiff{ 361 Old: "", 362 New: "foo!", 363 NewExtra: "foo", 364 }, 365 }, 366 }, 367 368 Err: false, 369 }, 370 371 { 372 Name: "StateFunc not called with nil value", 373 Schema: map[string]*Schema{ 374 "availability_zone": &Schema{ 375 Type: TypeString, 376 Optional: true, 377 Computed: true, 378 StateFunc: func(a interface{}) string { 379 t.Fatalf("should not get here!") 380 return "" 381 }, 382 }, 383 }, 384 385 State: nil, 386 387 Config: map[string]interface{}{}, 388 389 Diff: &terraform.InstanceDiff{ 390 Attributes: map[string]*terraform.ResourceAttrDiff{ 391 "availability_zone": &terraform.ResourceAttrDiff{ 392 Old: "", 393 New: "", 394 NewComputed: true, 395 }, 396 }, 397 }, 398 399 Err: false, 400 }, 401 402 { 403 Name: "Variable (just checking)", 404 Schema: map[string]*Schema{ 405 "availability_zone": &Schema{ 406 Type: TypeString, 407 Optional: true, 408 }, 409 }, 410 411 State: nil, 412 413 Config: map[string]interface{}{ 414 "availability_zone": "${var.foo}", 415 }, 416 417 ConfigVariables: map[string]ast.Variable{ 418 "var.foo": interfaceToVariableSwallowError("bar"), 419 }, 420 421 Diff: &terraform.InstanceDiff{ 422 Attributes: map[string]*terraform.ResourceAttrDiff{ 423 "availability_zone": &terraform.ResourceAttrDiff{ 424 Old: "", 425 New: "bar", 426 }, 427 }, 428 }, 429 430 Err: false, 431 }, 432 433 { 434 Name: "Variable computed", 435 Schema: map[string]*Schema{ 436 "availability_zone": &Schema{ 437 Type: TypeString, 438 Optional: true, 439 }, 440 }, 441 442 State: nil, 443 444 Config: map[string]interface{}{ 445 "availability_zone": "${var.foo}", 446 }, 447 448 ConfigVariables: map[string]ast.Variable{ 449 "var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue), 450 }, 451 452 Diff: &terraform.InstanceDiff{ 453 Attributes: map[string]*terraform.ResourceAttrDiff{ 454 "availability_zone": &terraform.ResourceAttrDiff{ 455 Old: "", 456 New: "${var.foo}", 457 NewComputed: true, 458 }, 459 }, 460 }, 461 462 Err: false, 463 }, 464 465 { 466 Name: "Int decode", 467 Schema: map[string]*Schema{ 468 "port": &Schema{ 469 Type: TypeInt, 470 Optional: true, 471 Computed: true, 472 ForceNew: true, 473 }, 474 }, 475 476 State: nil, 477 478 Config: map[string]interface{}{ 479 "port": 27, 480 }, 481 482 Diff: &terraform.InstanceDiff{ 483 Attributes: map[string]*terraform.ResourceAttrDiff{ 484 "port": &terraform.ResourceAttrDiff{ 485 Old: "", 486 New: "27", 487 RequiresNew: true, 488 }, 489 }, 490 }, 491 492 Err: false, 493 }, 494 495 { 496 Name: "bool decode", 497 Schema: map[string]*Schema{ 498 "port": &Schema{ 499 Type: TypeBool, 500 Optional: true, 501 Computed: true, 502 ForceNew: true, 503 }, 504 }, 505 506 State: nil, 507 508 Config: map[string]interface{}{ 509 "port": false, 510 }, 511 512 Diff: &terraform.InstanceDiff{ 513 Attributes: map[string]*terraform.ResourceAttrDiff{ 514 "port": &terraform.ResourceAttrDiff{ 515 Old: "", 516 New: "false", 517 RequiresNew: true, 518 }, 519 }, 520 }, 521 522 Err: false, 523 }, 524 525 { 526 Name: "Bool", 527 Schema: map[string]*Schema{ 528 "delete": &Schema{ 529 Type: TypeBool, 530 Optional: true, 531 Default: false, 532 }, 533 }, 534 535 State: &terraform.InstanceState{ 536 Attributes: map[string]string{ 537 "delete": "false", 538 }, 539 }, 540 541 Config: nil, 542 543 Diff: nil, 544 545 Err: false, 546 }, 547 548 { 549 Name: "List decode", 550 Schema: map[string]*Schema{ 551 "ports": &Schema{ 552 Type: TypeList, 553 Required: true, 554 Elem: &Schema{Type: TypeInt}, 555 }, 556 }, 557 558 State: nil, 559 560 Config: map[string]interface{}{ 561 "ports": []interface{}{1, 2, 5}, 562 }, 563 564 Diff: &terraform.InstanceDiff{ 565 Attributes: map[string]*terraform.ResourceAttrDiff{ 566 "ports.#": &terraform.ResourceAttrDiff{ 567 Old: "0", 568 New: "3", 569 }, 570 "ports.0": &terraform.ResourceAttrDiff{ 571 Old: "", 572 New: "1", 573 }, 574 "ports.1": &terraform.ResourceAttrDiff{ 575 Old: "", 576 New: "2", 577 }, 578 "ports.2": &terraform.ResourceAttrDiff{ 579 Old: "", 580 New: "5", 581 }, 582 }, 583 }, 584 585 Err: false, 586 }, 587 588 { 589 Name: "List decode with promotion", 590 Schema: map[string]*Schema{ 591 "ports": &Schema{ 592 Type: TypeList, 593 Required: true, 594 Elem: &Schema{Type: TypeInt}, 595 PromoteSingle: true, 596 }, 597 }, 598 599 State: nil, 600 601 Config: map[string]interface{}{ 602 "ports": "5", 603 }, 604 605 Diff: &terraform.InstanceDiff{ 606 Attributes: map[string]*terraform.ResourceAttrDiff{ 607 "ports.#": &terraform.ResourceAttrDiff{ 608 Old: "0", 609 New: "1", 610 }, 611 "ports.0": &terraform.ResourceAttrDiff{ 612 Old: "", 613 New: "5", 614 }, 615 }, 616 }, 617 618 Err: false, 619 }, 620 621 { 622 Name: "List decode with promotion with list", 623 Schema: map[string]*Schema{ 624 "ports": &Schema{ 625 Type: TypeList, 626 Required: true, 627 Elem: &Schema{Type: TypeInt}, 628 PromoteSingle: true, 629 }, 630 }, 631 632 State: nil, 633 634 Config: map[string]interface{}{ 635 "ports": []interface{}{"5"}, 636 }, 637 638 Diff: &terraform.InstanceDiff{ 639 Attributes: map[string]*terraform.ResourceAttrDiff{ 640 "ports.#": &terraform.ResourceAttrDiff{ 641 Old: "0", 642 New: "1", 643 }, 644 "ports.0": &terraform.ResourceAttrDiff{ 645 Old: "", 646 New: "5", 647 }, 648 }, 649 }, 650 651 Err: false, 652 }, 653 654 { 655 Schema: map[string]*Schema{ 656 "ports": &Schema{ 657 Type: TypeList, 658 Required: true, 659 Elem: &Schema{Type: TypeInt}, 660 }, 661 }, 662 663 State: nil, 664 665 Config: map[string]interface{}{ 666 "ports": []interface{}{1, "${var.foo}"}, 667 }, 668 669 ConfigVariables: map[string]ast.Variable{ 670 "var.foo": interfaceToVariableSwallowError([]interface{}{"2", "5"}), 671 }, 672 673 Diff: &terraform.InstanceDiff{ 674 Attributes: map[string]*terraform.ResourceAttrDiff{ 675 "ports.#": &terraform.ResourceAttrDiff{ 676 Old: "0", 677 New: "3", 678 }, 679 "ports.0": &terraform.ResourceAttrDiff{ 680 Old: "", 681 New: "1", 682 }, 683 "ports.1": &terraform.ResourceAttrDiff{ 684 Old: "", 685 New: "2", 686 }, 687 "ports.2": &terraform.ResourceAttrDiff{ 688 Old: "", 689 New: "5", 690 }, 691 }, 692 }, 693 694 Err: false, 695 }, 696 697 { 698 Schema: map[string]*Schema{ 699 "ports": &Schema{ 700 Type: TypeList, 701 Required: true, 702 Elem: &Schema{Type: TypeInt}, 703 }, 704 }, 705 706 State: nil, 707 708 Config: map[string]interface{}{ 709 "ports": []interface{}{1, "${var.foo}"}, 710 }, 711 712 ConfigVariables: map[string]ast.Variable{ 713 "var.foo": interfaceToVariableSwallowError([]interface{}{ 714 config.UnknownVariableValue, "5"}), 715 }, 716 717 Diff: &terraform.InstanceDiff{ 718 Attributes: map[string]*terraform.ResourceAttrDiff{ 719 "ports.#": &terraform.ResourceAttrDiff{ 720 Old: "0", 721 New: "", 722 NewComputed: true, 723 }, 724 }, 725 }, 726 727 Err: false, 728 }, 729 730 { 731 Schema: map[string]*Schema{ 732 "ports": &Schema{ 733 Type: TypeList, 734 Required: true, 735 Elem: &Schema{Type: TypeInt}, 736 }, 737 }, 738 739 State: &terraform.InstanceState{ 740 Attributes: map[string]string{ 741 "ports.#": "3", 742 "ports.0": "1", 743 "ports.1": "2", 744 "ports.2": "5", 745 }, 746 }, 747 748 Config: map[string]interface{}{ 749 "ports": []interface{}{1, 2, 5}, 750 }, 751 752 Diff: nil, 753 754 Err: false, 755 }, 756 757 { 758 Name: "", 759 Schema: map[string]*Schema{ 760 "ports": &Schema{ 761 Type: TypeList, 762 Required: true, 763 Elem: &Schema{Type: TypeInt}, 764 }, 765 }, 766 767 State: &terraform.InstanceState{ 768 Attributes: map[string]string{ 769 "ports.#": "2", 770 "ports.0": "1", 771 "ports.1": "2", 772 }, 773 }, 774 775 Config: map[string]interface{}{ 776 "ports": []interface{}{1, 2, 5}, 777 }, 778 779 Diff: &terraform.InstanceDiff{ 780 Attributes: map[string]*terraform.ResourceAttrDiff{ 781 "ports.#": &terraform.ResourceAttrDiff{ 782 Old: "2", 783 New: "3", 784 }, 785 "ports.2": &terraform.ResourceAttrDiff{ 786 Old: "", 787 New: "5", 788 }, 789 }, 790 }, 791 792 Err: false, 793 }, 794 795 { 796 Name: "", 797 Schema: map[string]*Schema{ 798 "ports": &Schema{ 799 Type: TypeList, 800 Required: true, 801 Elem: &Schema{Type: TypeInt}, 802 ForceNew: true, 803 }, 804 }, 805 806 State: nil, 807 808 Config: map[string]interface{}{ 809 "ports": []interface{}{1, 2, 5}, 810 }, 811 812 Diff: &terraform.InstanceDiff{ 813 Attributes: map[string]*terraform.ResourceAttrDiff{ 814 "ports.#": &terraform.ResourceAttrDiff{ 815 Old: "0", 816 New: "3", 817 RequiresNew: true, 818 }, 819 "ports.0": &terraform.ResourceAttrDiff{ 820 Old: "", 821 New: "1", 822 RequiresNew: true, 823 }, 824 "ports.1": &terraform.ResourceAttrDiff{ 825 Old: "", 826 New: "2", 827 RequiresNew: true, 828 }, 829 "ports.2": &terraform.ResourceAttrDiff{ 830 Old: "", 831 New: "5", 832 RequiresNew: true, 833 }, 834 }, 835 }, 836 837 Err: false, 838 }, 839 840 { 841 Name: "", 842 Schema: map[string]*Schema{ 843 "ports": &Schema{ 844 Type: TypeList, 845 Optional: true, 846 Computed: true, 847 Elem: &Schema{Type: TypeInt}, 848 }, 849 }, 850 851 State: nil, 852 853 Config: map[string]interface{}{}, 854 855 Diff: &terraform.InstanceDiff{ 856 Attributes: map[string]*terraform.ResourceAttrDiff{ 857 "ports.#": &terraform.ResourceAttrDiff{ 858 Old: "", 859 NewComputed: true, 860 }, 861 }, 862 }, 863 864 Err: false, 865 }, 866 867 { 868 Name: "List with computed set", 869 Schema: map[string]*Schema{ 870 "config": &Schema{ 871 Type: TypeList, 872 Optional: true, 873 ForceNew: true, 874 MinItems: 1, 875 Elem: &Resource{ 876 Schema: map[string]*Schema{ 877 "name": { 878 Type: TypeString, 879 Required: true, 880 }, 881 882 "rules": { 883 Type: TypeSet, 884 Computed: true, 885 Elem: &Schema{Type: TypeString}, 886 Set: HashString, 887 }, 888 }, 889 }, 890 }, 891 }, 892 893 State: nil, 894 895 Config: map[string]interface{}{ 896 "config": []interface{}{ 897 map[string]interface{}{ 898 "name": "hello", 899 }, 900 }, 901 }, 902 903 Diff: &terraform.InstanceDiff{ 904 Attributes: map[string]*terraform.ResourceAttrDiff{ 905 "config.#": &terraform.ResourceAttrDiff{ 906 Old: "0", 907 New: "1", 908 RequiresNew: true, 909 }, 910 911 "config.0.name": &terraform.ResourceAttrDiff{ 912 Old: "", 913 New: "hello", 914 }, 915 916 "config.0.rules.#": &terraform.ResourceAttrDiff{ 917 Old: "", 918 NewComputed: true, 919 }, 920 }, 921 }, 922 923 Err: false, 924 }, 925 926 { 927 Name: "Set", 928 Schema: map[string]*Schema{ 929 "ports": &Schema{ 930 Type: TypeSet, 931 Required: true, 932 Elem: &Schema{Type: TypeInt}, 933 Set: func(a interface{}) int { 934 return a.(int) 935 }, 936 }, 937 }, 938 939 State: nil, 940 941 Config: map[string]interface{}{ 942 "ports": []interface{}{5, 2, 1}, 943 }, 944 945 Diff: &terraform.InstanceDiff{ 946 Attributes: map[string]*terraform.ResourceAttrDiff{ 947 "ports.#": &terraform.ResourceAttrDiff{ 948 Old: "0", 949 New: "3", 950 }, 951 "ports.1": &terraform.ResourceAttrDiff{ 952 Old: "", 953 New: "1", 954 }, 955 "ports.2": &terraform.ResourceAttrDiff{ 956 Old: "", 957 New: "2", 958 }, 959 "ports.5": &terraform.ResourceAttrDiff{ 960 Old: "", 961 New: "5", 962 }, 963 }, 964 }, 965 966 Err: false, 967 }, 968 969 { 970 Name: "Set", 971 Schema: map[string]*Schema{ 972 "ports": &Schema{ 973 Type: TypeSet, 974 Computed: true, 975 Required: true, 976 Elem: &Schema{Type: TypeInt}, 977 Set: func(a interface{}) int { 978 return a.(int) 979 }, 980 }, 981 }, 982 983 State: &terraform.InstanceState{ 984 Attributes: map[string]string{ 985 "ports.#": "0", 986 }, 987 }, 988 989 Config: nil, 990 991 Diff: nil, 992 993 Err: false, 994 }, 995 996 { 997 Name: "Set", 998 Schema: map[string]*Schema{ 999 "ports": &Schema{ 1000 Type: TypeSet, 1001 Optional: true, 1002 Computed: true, 1003 Elem: &Schema{Type: TypeInt}, 1004 Set: func(a interface{}) int { 1005 return a.(int) 1006 }, 1007 }, 1008 }, 1009 1010 State: nil, 1011 1012 Config: nil, 1013 1014 Diff: &terraform.InstanceDiff{ 1015 Attributes: map[string]*terraform.ResourceAttrDiff{ 1016 "ports.#": &terraform.ResourceAttrDiff{ 1017 Old: "", 1018 NewComputed: true, 1019 }, 1020 }, 1021 }, 1022 1023 Err: false, 1024 }, 1025 1026 { 1027 Name: "Set", 1028 Schema: map[string]*Schema{ 1029 "ports": &Schema{ 1030 Type: TypeSet, 1031 Required: true, 1032 Elem: &Schema{Type: TypeInt}, 1033 Set: func(a interface{}) int { 1034 return a.(int) 1035 }, 1036 }, 1037 }, 1038 1039 State: nil, 1040 1041 Config: map[string]interface{}{ 1042 "ports": []interface{}{"${var.foo}", 1}, 1043 }, 1044 1045 ConfigVariables: map[string]ast.Variable{ 1046 "var.foo": interfaceToVariableSwallowError([]interface{}{"2", "5"}), 1047 }, 1048 1049 Diff: &terraform.InstanceDiff{ 1050 Attributes: map[string]*terraform.ResourceAttrDiff{ 1051 "ports.#": &terraform.ResourceAttrDiff{ 1052 Old: "0", 1053 New: "3", 1054 }, 1055 "ports.1": &terraform.ResourceAttrDiff{ 1056 Old: "", 1057 New: "1", 1058 }, 1059 "ports.2": &terraform.ResourceAttrDiff{ 1060 Old: "", 1061 New: "2", 1062 }, 1063 "ports.5": &terraform.ResourceAttrDiff{ 1064 Old: "", 1065 New: "5", 1066 }, 1067 }, 1068 }, 1069 1070 Err: false, 1071 }, 1072 1073 { 1074 Name: "Set", 1075 Schema: map[string]*Schema{ 1076 "ports": &Schema{ 1077 Type: TypeSet, 1078 Required: true, 1079 Elem: &Schema{Type: TypeInt}, 1080 Set: func(a interface{}) int { 1081 return a.(int) 1082 }, 1083 }, 1084 }, 1085 1086 State: nil, 1087 1088 Config: map[string]interface{}{ 1089 "ports": []interface{}{1, "${var.foo}"}, 1090 }, 1091 1092 ConfigVariables: map[string]ast.Variable{ 1093 "var.foo": interfaceToVariableSwallowError([]interface{}{ 1094 config.UnknownVariableValue, "5"}), 1095 }, 1096 1097 Diff: &terraform.InstanceDiff{ 1098 Attributes: map[string]*terraform.ResourceAttrDiff{ 1099 "ports.#": &terraform.ResourceAttrDiff{ 1100 Old: "", 1101 New: "", 1102 NewComputed: true, 1103 }, 1104 }, 1105 }, 1106 1107 Err: false, 1108 }, 1109 1110 { 1111 Name: "Set", 1112 Schema: map[string]*Schema{ 1113 "ports": &Schema{ 1114 Type: TypeSet, 1115 Required: true, 1116 Elem: &Schema{Type: TypeInt}, 1117 Set: func(a interface{}) int { 1118 return a.(int) 1119 }, 1120 }, 1121 }, 1122 1123 State: &terraform.InstanceState{ 1124 Attributes: map[string]string{ 1125 "ports.#": "2", 1126 "ports.1": "1", 1127 "ports.2": "2", 1128 }, 1129 }, 1130 1131 Config: map[string]interface{}{ 1132 "ports": []interface{}{5, 2, 1}, 1133 }, 1134 1135 Diff: &terraform.InstanceDiff{ 1136 Attributes: map[string]*terraform.ResourceAttrDiff{ 1137 "ports.#": &terraform.ResourceAttrDiff{ 1138 Old: "2", 1139 New: "3", 1140 }, 1141 "ports.1": &terraform.ResourceAttrDiff{ 1142 Old: "1", 1143 New: "1", 1144 }, 1145 "ports.2": &terraform.ResourceAttrDiff{ 1146 Old: "2", 1147 New: "2", 1148 }, 1149 "ports.5": &terraform.ResourceAttrDiff{ 1150 Old: "", 1151 New: "5", 1152 }, 1153 }, 1154 }, 1155 1156 Err: false, 1157 }, 1158 1159 { 1160 Name: "Set", 1161 Schema: map[string]*Schema{ 1162 "ports": &Schema{ 1163 Type: TypeSet, 1164 Required: true, 1165 Elem: &Schema{Type: TypeInt}, 1166 Set: func(a interface{}) int { 1167 return a.(int) 1168 }, 1169 }, 1170 }, 1171 1172 State: &terraform.InstanceState{ 1173 Attributes: map[string]string{ 1174 "ports.#": "2", 1175 "ports.1": "1", 1176 "ports.2": "2", 1177 }, 1178 }, 1179 1180 Config: map[string]interface{}{}, 1181 1182 Diff: &terraform.InstanceDiff{ 1183 Attributes: map[string]*terraform.ResourceAttrDiff{ 1184 "ports.#": &terraform.ResourceAttrDiff{ 1185 Old: "2", 1186 New: "0", 1187 }, 1188 "ports.1": &terraform.ResourceAttrDiff{ 1189 Old: "1", 1190 New: "0", 1191 NewRemoved: true, 1192 }, 1193 "ports.2": &terraform.ResourceAttrDiff{ 1194 Old: "2", 1195 New: "0", 1196 NewRemoved: true, 1197 }, 1198 }, 1199 }, 1200 1201 Err: false, 1202 }, 1203 1204 { 1205 Name: "Set", 1206 Schema: map[string]*Schema{ 1207 "ports": &Schema{ 1208 Type: TypeSet, 1209 Optional: true, 1210 Computed: true, 1211 Elem: &Schema{Type: TypeInt}, 1212 Set: func(a interface{}) int { 1213 return a.(int) 1214 }, 1215 }, 1216 }, 1217 1218 State: &terraform.InstanceState{ 1219 Attributes: map[string]string{ 1220 "availability_zone": "bar", 1221 "ports.#": "1", 1222 "ports.80": "80", 1223 }, 1224 }, 1225 1226 Config: map[string]interface{}{}, 1227 1228 Diff: nil, 1229 1230 Err: false, 1231 }, 1232 1233 { 1234 Name: "Set", 1235 Schema: map[string]*Schema{ 1236 "ingress": &Schema{ 1237 Type: TypeSet, 1238 Required: true, 1239 Elem: &Resource{ 1240 Schema: map[string]*Schema{ 1241 "ports": &Schema{ 1242 Type: TypeList, 1243 Optional: true, 1244 Elem: &Schema{Type: TypeInt}, 1245 }, 1246 }, 1247 }, 1248 Set: func(v interface{}) int { 1249 m := v.(map[string]interface{}) 1250 ps := m["ports"].([]interface{}) 1251 result := 0 1252 for _, p := range ps { 1253 result += p.(int) 1254 } 1255 return result 1256 }, 1257 }, 1258 }, 1259 1260 State: &terraform.InstanceState{ 1261 Attributes: map[string]string{ 1262 "ingress.#": "2", 1263 "ingress.80.ports.#": "1", 1264 "ingress.80.ports.0": "80", 1265 "ingress.443.ports.#": "1", 1266 "ingress.443.ports.0": "443", 1267 }, 1268 }, 1269 1270 Config: map[string]interface{}{ 1271 "ingress": []map[string]interface{}{ 1272 map[string]interface{}{ 1273 "ports": []interface{}{443}, 1274 }, 1275 map[string]interface{}{ 1276 "ports": []interface{}{80}, 1277 }, 1278 }, 1279 }, 1280 1281 Diff: nil, 1282 1283 Err: false, 1284 }, 1285 1286 { 1287 Name: "List of structure decode", 1288 Schema: map[string]*Schema{ 1289 "ingress": &Schema{ 1290 Type: TypeList, 1291 Required: true, 1292 Elem: &Resource{ 1293 Schema: map[string]*Schema{ 1294 "from": &Schema{ 1295 Type: TypeInt, 1296 Required: true, 1297 }, 1298 }, 1299 }, 1300 }, 1301 }, 1302 1303 State: nil, 1304 1305 Config: map[string]interface{}{ 1306 "ingress": []interface{}{ 1307 map[string]interface{}{ 1308 "from": 8080, 1309 }, 1310 }, 1311 }, 1312 1313 Diff: &terraform.InstanceDiff{ 1314 Attributes: map[string]*terraform.ResourceAttrDiff{ 1315 "ingress.#": &terraform.ResourceAttrDiff{ 1316 Old: "0", 1317 New: "1", 1318 }, 1319 "ingress.0.from": &terraform.ResourceAttrDiff{ 1320 Old: "", 1321 New: "8080", 1322 }, 1323 }, 1324 }, 1325 1326 Err: false, 1327 }, 1328 1329 { 1330 Name: "ComputedWhen", 1331 Schema: map[string]*Schema{ 1332 "availability_zone": &Schema{ 1333 Type: TypeString, 1334 Computed: true, 1335 ComputedWhen: []string{"port"}, 1336 }, 1337 1338 "port": &Schema{ 1339 Type: TypeInt, 1340 Optional: true, 1341 }, 1342 }, 1343 1344 State: &terraform.InstanceState{ 1345 Attributes: map[string]string{ 1346 "availability_zone": "foo", 1347 "port": "80", 1348 }, 1349 }, 1350 1351 Config: map[string]interface{}{ 1352 "port": 80, 1353 }, 1354 1355 Diff: nil, 1356 1357 Err: false, 1358 }, 1359 1360 { 1361 Name: "", 1362 Schema: map[string]*Schema{ 1363 "availability_zone": &Schema{ 1364 Type: TypeString, 1365 Computed: true, 1366 ComputedWhen: []string{"port"}, 1367 }, 1368 1369 "port": &Schema{ 1370 Type: TypeInt, 1371 Optional: true, 1372 }, 1373 }, 1374 1375 State: &terraform.InstanceState{ 1376 Attributes: map[string]string{ 1377 "port": "80", 1378 }, 1379 }, 1380 1381 Config: map[string]interface{}{ 1382 "port": 80, 1383 }, 1384 1385 Diff: &terraform.InstanceDiff{ 1386 Attributes: map[string]*terraform.ResourceAttrDiff{ 1387 "availability_zone": &terraform.ResourceAttrDiff{ 1388 NewComputed: true, 1389 }, 1390 }, 1391 }, 1392 1393 Err: false, 1394 }, 1395 1396 /* TODO 1397 { 1398 Schema: map[string]*Schema{ 1399 "availability_zone": &Schema{ 1400 Type: TypeString, 1401 Computed: true, 1402 ComputedWhen: []string{"port"}, 1403 }, 1404 1405 "port": &Schema{ 1406 Type: TypeInt, 1407 Optional: true, 1408 }, 1409 }, 1410 1411 State: &terraform.InstanceState{ 1412 Attributes: map[string]string{ 1413 "availability_zone": "foo", 1414 "port": "80", 1415 }, 1416 }, 1417 1418 Config: map[string]interface{}{ 1419 "port": 8080, 1420 }, 1421 1422 Diff: &terraform.ResourceDiff{ 1423 Attributes: map[string]*terraform.ResourceAttrDiff{ 1424 "availability_zone": &terraform.ResourceAttrDiff{ 1425 Old: "foo", 1426 NewComputed: true, 1427 }, 1428 "port": &terraform.ResourceAttrDiff{ 1429 Old: "80", 1430 New: "8080", 1431 }, 1432 }, 1433 }, 1434 1435 Err: false, 1436 }, 1437 */ 1438 1439 { 1440 Name: "Maps", 1441 Schema: map[string]*Schema{ 1442 "config_vars": &Schema{ 1443 Type: TypeMap, 1444 }, 1445 }, 1446 1447 State: nil, 1448 1449 Config: map[string]interface{}{ 1450 "config_vars": []interface{}{ 1451 map[string]interface{}{ 1452 "bar": "baz", 1453 }, 1454 }, 1455 }, 1456 1457 Diff: &terraform.InstanceDiff{ 1458 Attributes: map[string]*terraform.ResourceAttrDiff{ 1459 "config_vars.%": &terraform.ResourceAttrDiff{ 1460 Old: "0", 1461 New: "1", 1462 }, 1463 1464 "config_vars.bar": &terraform.ResourceAttrDiff{ 1465 Old: "", 1466 New: "baz", 1467 }, 1468 }, 1469 }, 1470 1471 Err: false, 1472 }, 1473 1474 { 1475 Name: "Maps", 1476 Schema: map[string]*Schema{ 1477 "config_vars": &Schema{ 1478 Type: TypeMap, 1479 }, 1480 }, 1481 1482 State: &terraform.InstanceState{ 1483 Attributes: map[string]string{ 1484 "config_vars.foo": "bar", 1485 }, 1486 }, 1487 1488 Config: map[string]interface{}{ 1489 "config_vars": []interface{}{ 1490 map[string]interface{}{ 1491 "bar": "baz", 1492 }, 1493 }, 1494 }, 1495 1496 Diff: &terraform.InstanceDiff{ 1497 Attributes: map[string]*terraform.ResourceAttrDiff{ 1498 "config_vars.foo": &terraform.ResourceAttrDiff{ 1499 Old: "bar", 1500 NewRemoved: true, 1501 }, 1502 "config_vars.bar": &terraform.ResourceAttrDiff{ 1503 Old: "", 1504 New: "baz", 1505 }, 1506 }, 1507 }, 1508 1509 Err: false, 1510 }, 1511 1512 { 1513 Name: "Maps", 1514 Schema: map[string]*Schema{ 1515 "vars": &Schema{ 1516 Type: TypeMap, 1517 Optional: true, 1518 Computed: true, 1519 }, 1520 }, 1521 1522 State: &terraform.InstanceState{ 1523 Attributes: map[string]string{ 1524 "vars.foo": "bar", 1525 }, 1526 }, 1527 1528 Config: map[string]interface{}{ 1529 "vars": []interface{}{ 1530 map[string]interface{}{ 1531 "bar": "baz", 1532 }, 1533 }, 1534 }, 1535 1536 Diff: &terraform.InstanceDiff{ 1537 Attributes: map[string]*terraform.ResourceAttrDiff{ 1538 "vars.foo": &terraform.ResourceAttrDiff{ 1539 Old: "bar", 1540 New: "", 1541 NewRemoved: true, 1542 }, 1543 "vars.bar": &terraform.ResourceAttrDiff{ 1544 Old: "", 1545 New: "baz", 1546 }, 1547 }, 1548 }, 1549 1550 Err: false, 1551 }, 1552 1553 { 1554 Name: "Maps", 1555 Schema: map[string]*Schema{ 1556 "vars": &Schema{ 1557 Type: TypeMap, 1558 Computed: true, 1559 }, 1560 }, 1561 1562 State: &terraform.InstanceState{ 1563 Attributes: map[string]string{ 1564 "vars.foo": "bar", 1565 }, 1566 }, 1567 1568 Config: nil, 1569 1570 Diff: nil, 1571 1572 Err: false, 1573 }, 1574 1575 { 1576 Name: "Maps", 1577 Schema: map[string]*Schema{ 1578 "config_vars": &Schema{ 1579 Type: TypeList, 1580 Elem: &Schema{Type: TypeMap}, 1581 }, 1582 }, 1583 1584 State: &terraform.InstanceState{ 1585 Attributes: map[string]string{ 1586 "config_vars.#": "1", 1587 "config_vars.0.foo": "bar", 1588 }, 1589 }, 1590 1591 Config: map[string]interface{}{ 1592 "config_vars": []interface{}{ 1593 map[string]interface{}{ 1594 "bar": "baz", 1595 }, 1596 }, 1597 }, 1598 1599 Diff: &terraform.InstanceDiff{ 1600 Attributes: map[string]*terraform.ResourceAttrDiff{ 1601 "config_vars.0.foo": &terraform.ResourceAttrDiff{ 1602 Old: "bar", 1603 NewRemoved: true, 1604 }, 1605 "config_vars.0.bar": &terraform.ResourceAttrDiff{ 1606 Old: "", 1607 New: "baz", 1608 }, 1609 }, 1610 }, 1611 1612 Err: false, 1613 }, 1614 1615 { 1616 Name: "Maps", 1617 Schema: map[string]*Schema{ 1618 "config_vars": &Schema{ 1619 Type: TypeList, 1620 Elem: &Schema{Type: TypeMap}, 1621 }, 1622 }, 1623 1624 State: &terraform.InstanceState{ 1625 Attributes: map[string]string{ 1626 "config_vars.#": "1", 1627 "config_vars.0.foo": "bar", 1628 "config_vars.0.bar": "baz", 1629 }, 1630 }, 1631 1632 Config: map[string]interface{}{}, 1633 1634 Diff: &terraform.InstanceDiff{ 1635 Attributes: map[string]*terraform.ResourceAttrDiff{ 1636 "config_vars.#": &terraform.ResourceAttrDiff{ 1637 Old: "1", 1638 New: "0", 1639 }, 1640 "config_vars.0.%": &terraform.ResourceAttrDiff{ 1641 Old: "2", 1642 New: "0", 1643 }, 1644 "config_vars.0.foo": &terraform.ResourceAttrDiff{ 1645 Old: "bar", 1646 NewRemoved: true, 1647 }, 1648 "config_vars.0.bar": &terraform.ResourceAttrDiff{ 1649 Old: "baz", 1650 NewRemoved: true, 1651 }, 1652 }, 1653 }, 1654 1655 Err: false, 1656 }, 1657 1658 { 1659 Name: "ForceNews", 1660 Schema: map[string]*Schema{ 1661 "availability_zone": &Schema{ 1662 Type: TypeString, 1663 Optional: true, 1664 ForceNew: true, 1665 }, 1666 1667 "address": &Schema{ 1668 Type: TypeString, 1669 Optional: true, 1670 Computed: true, 1671 }, 1672 }, 1673 1674 State: &terraform.InstanceState{ 1675 Attributes: map[string]string{ 1676 "availability_zone": "bar", 1677 "address": "foo", 1678 }, 1679 }, 1680 1681 Config: map[string]interface{}{ 1682 "availability_zone": "foo", 1683 }, 1684 1685 Diff: &terraform.InstanceDiff{ 1686 Attributes: map[string]*terraform.ResourceAttrDiff{ 1687 "availability_zone": &terraform.ResourceAttrDiff{ 1688 Old: "bar", 1689 New: "foo", 1690 RequiresNew: true, 1691 }, 1692 1693 "address": &terraform.ResourceAttrDiff{ 1694 Old: "foo", 1695 New: "", 1696 NewComputed: true, 1697 }, 1698 }, 1699 }, 1700 1701 Err: false, 1702 }, 1703 1704 { 1705 Name: "Set", 1706 Schema: map[string]*Schema{ 1707 "availability_zone": &Schema{ 1708 Type: TypeString, 1709 Optional: true, 1710 ForceNew: true, 1711 }, 1712 1713 "ports": &Schema{ 1714 Type: TypeSet, 1715 Optional: true, 1716 Computed: true, 1717 Elem: &Schema{Type: TypeInt}, 1718 Set: func(a interface{}) int { 1719 return a.(int) 1720 }, 1721 }, 1722 }, 1723 1724 State: &terraform.InstanceState{ 1725 Attributes: map[string]string{ 1726 "availability_zone": "bar", 1727 "ports.#": "1", 1728 "ports.80": "80", 1729 }, 1730 }, 1731 1732 Config: map[string]interface{}{ 1733 "availability_zone": "foo", 1734 }, 1735 1736 Diff: &terraform.InstanceDiff{ 1737 Attributes: map[string]*terraform.ResourceAttrDiff{ 1738 "availability_zone": &terraform.ResourceAttrDiff{ 1739 Old: "bar", 1740 New: "foo", 1741 RequiresNew: true, 1742 }, 1743 1744 "ports.#": &terraform.ResourceAttrDiff{ 1745 Old: "1", 1746 New: "", 1747 NewComputed: true, 1748 }, 1749 }, 1750 }, 1751 1752 Err: false, 1753 }, 1754 1755 { 1756 Name: "Set", 1757 Schema: map[string]*Schema{ 1758 "instances": &Schema{ 1759 Type: TypeSet, 1760 Elem: &Schema{Type: TypeString}, 1761 Optional: true, 1762 Computed: true, 1763 Set: func(v interface{}) int { 1764 return len(v.(string)) 1765 }, 1766 }, 1767 }, 1768 1769 State: &terraform.InstanceState{ 1770 Attributes: map[string]string{ 1771 "instances.#": "0", 1772 }, 1773 }, 1774 1775 Config: map[string]interface{}{ 1776 "instances": []interface{}{"${var.foo}"}, 1777 }, 1778 1779 ConfigVariables: map[string]ast.Variable{ 1780 "var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue), 1781 }, 1782 1783 Diff: &terraform.InstanceDiff{ 1784 Attributes: map[string]*terraform.ResourceAttrDiff{ 1785 "instances.#": &terraform.ResourceAttrDiff{ 1786 NewComputed: true, 1787 }, 1788 }, 1789 }, 1790 1791 Err: false, 1792 }, 1793 1794 { 1795 Name: "Set", 1796 Schema: map[string]*Schema{ 1797 "route": &Schema{ 1798 Type: TypeSet, 1799 Optional: true, 1800 Elem: &Resource{ 1801 Schema: map[string]*Schema{ 1802 "index": &Schema{ 1803 Type: TypeInt, 1804 Required: true, 1805 }, 1806 1807 "gateway": &Schema{ 1808 Type: TypeString, 1809 Optional: true, 1810 }, 1811 }, 1812 }, 1813 Set: func(v interface{}) int { 1814 m := v.(map[string]interface{}) 1815 return m["index"].(int) 1816 }, 1817 }, 1818 }, 1819 1820 State: nil, 1821 1822 Config: map[string]interface{}{ 1823 "route": []map[string]interface{}{ 1824 map[string]interface{}{ 1825 "index": "1", 1826 "gateway": "${var.foo}", 1827 }, 1828 }, 1829 }, 1830 1831 ConfigVariables: map[string]ast.Variable{ 1832 "var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue), 1833 }, 1834 1835 Diff: &terraform.InstanceDiff{ 1836 Attributes: map[string]*terraform.ResourceAttrDiff{ 1837 "route.#": &terraform.ResourceAttrDiff{ 1838 Old: "0", 1839 New: "1", 1840 }, 1841 "route.~1.index": &terraform.ResourceAttrDiff{ 1842 Old: "", 1843 New: "1", 1844 }, 1845 "route.~1.gateway": &terraform.ResourceAttrDiff{ 1846 Old: "", 1847 New: "${var.foo}", 1848 NewComputed: true, 1849 }, 1850 }, 1851 }, 1852 1853 Err: false, 1854 }, 1855 1856 { 1857 Name: "Set", 1858 Schema: map[string]*Schema{ 1859 "route": &Schema{ 1860 Type: TypeSet, 1861 Optional: true, 1862 Elem: &Resource{ 1863 Schema: map[string]*Schema{ 1864 "index": &Schema{ 1865 Type: TypeInt, 1866 Required: true, 1867 }, 1868 1869 "gateway": &Schema{ 1870 Type: TypeSet, 1871 Optional: true, 1872 Elem: &Schema{Type: TypeInt}, 1873 Set: func(a interface{}) int { 1874 return a.(int) 1875 }, 1876 }, 1877 }, 1878 }, 1879 Set: func(v interface{}) int { 1880 m := v.(map[string]interface{}) 1881 return m["index"].(int) 1882 }, 1883 }, 1884 }, 1885 1886 State: nil, 1887 1888 Config: map[string]interface{}{ 1889 "route": []map[string]interface{}{ 1890 map[string]interface{}{ 1891 "index": "1", 1892 "gateway": []interface{}{ 1893 "${var.foo}", 1894 }, 1895 }, 1896 }, 1897 }, 1898 1899 ConfigVariables: map[string]ast.Variable{ 1900 "var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue), 1901 }, 1902 1903 Diff: &terraform.InstanceDiff{ 1904 Attributes: map[string]*terraform.ResourceAttrDiff{ 1905 "route.#": &terraform.ResourceAttrDiff{ 1906 Old: "0", 1907 New: "1", 1908 }, 1909 "route.~1.index": &terraform.ResourceAttrDiff{ 1910 Old: "", 1911 New: "1", 1912 }, 1913 "route.~1.gateway.#": &terraform.ResourceAttrDiff{ 1914 NewComputed: true, 1915 }, 1916 }, 1917 }, 1918 1919 Err: false, 1920 }, 1921 1922 { 1923 Name: "Computed maps", 1924 Schema: map[string]*Schema{ 1925 "vars": &Schema{ 1926 Type: TypeMap, 1927 Computed: true, 1928 }, 1929 }, 1930 1931 State: nil, 1932 1933 Config: nil, 1934 1935 Diff: &terraform.InstanceDiff{ 1936 Attributes: map[string]*terraform.ResourceAttrDiff{ 1937 "vars.%": &terraform.ResourceAttrDiff{ 1938 Old: "", 1939 NewComputed: true, 1940 }, 1941 }, 1942 }, 1943 1944 Err: false, 1945 }, 1946 1947 { 1948 Name: "Computed maps", 1949 Schema: map[string]*Schema{ 1950 "vars": &Schema{ 1951 Type: TypeMap, 1952 Computed: true, 1953 }, 1954 }, 1955 1956 State: &terraform.InstanceState{ 1957 Attributes: map[string]string{ 1958 "vars.%": "0", 1959 }, 1960 }, 1961 1962 Config: map[string]interface{}{ 1963 "vars": map[string]interface{}{ 1964 "bar": "${var.foo}", 1965 }, 1966 }, 1967 1968 ConfigVariables: map[string]ast.Variable{ 1969 "var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue), 1970 }, 1971 1972 Diff: &terraform.InstanceDiff{ 1973 Attributes: map[string]*terraform.ResourceAttrDiff{ 1974 "vars.%": &terraform.ResourceAttrDiff{ 1975 Old: "", 1976 NewComputed: true, 1977 }, 1978 }, 1979 }, 1980 1981 Err: false, 1982 }, 1983 1984 { 1985 Name: " - Empty", 1986 Schema: map[string]*Schema{}, 1987 1988 State: &terraform.InstanceState{}, 1989 1990 Config: map[string]interface{}{}, 1991 1992 Diff: nil, 1993 1994 Err: false, 1995 }, 1996 1997 { 1998 Name: "Float", 1999 Schema: map[string]*Schema{ 2000 "some_threshold": &Schema{ 2001 Type: TypeFloat, 2002 }, 2003 }, 2004 2005 State: &terraform.InstanceState{ 2006 Attributes: map[string]string{ 2007 "some_threshold": "567.8", 2008 }, 2009 }, 2010 2011 Config: map[string]interface{}{ 2012 "some_threshold": 12.34, 2013 }, 2014 2015 Diff: &terraform.InstanceDiff{ 2016 Attributes: map[string]*terraform.ResourceAttrDiff{ 2017 "some_threshold": &terraform.ResourceAttrDiff{ 2018 Old: "567.8", 2019 New: "12.34", 2020 }, 2021 }, 2022 }, 2023 2024 Err: false, 2025 }, 2026 2027 { 2028 Name: "https://github.com/hashicorp/terraform/issues/824", 2029 Schema: map[string]*Schema{ 2030 "block_device": &Schema{ 2031 Type: TypeSet, 2032 Optional: true, 2033 Computed: true, 2034 Elem: &Resource{ 2035 Schema: map[string]*Schema{ 2036 "device_name": &Schema{ 2037 Type: TypeString, 2038 Required: true, 2039 }, 2040 "delete_on_termination": &Schema{ 2041 Type: TypeBool, 2042 Optional: true, 2043 Default: true, 2044 }, 2045 }, 2046 }, 2047 Set: func(v interface{}) int { 2048 var buf bytes.Buffer 2049 m := v.(map[string]interface{}) 2050 buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) 2051 buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool))) 2052 return hashcode.String(buf.String()) 2053 }, 2054 }, 2055 }, 2056 2057 State: &terraform.InstanceState{ 2058 Attributes: map[string]string{ 2059 "block_device.#": "2", 2060 "block_device.616397234.delete_on_termination": "true", 2061 "block_device.616397234.device_name": "/dev/sda1", 2062 "block_device.2801811477.delete_on_termination": "true", 2063 "block_device.2801811477.device_name": "/dev/sdx", 2064 }, 2065 }, 2066 2067 Config: map[string]interface{}{ 2068 "block_device": []map[string]interface{}{ 2069 map[string]interface{}{ 2070 "device_name": "/dev/sda1", 2071 }, 2072 map[string]interface{}{ 2073 "device_name": "/dev/sdx", 2074 }, 2075 }, 2076 }, 2077 Diff: nil, 2078 Err: false, 2079 }, 2080 2081 { 2082 Name: "Zero value in state shouldn't result in diff", 2083 Schema: map[string]*Schema{ 2084 "port": &Schema{ 2085 Type: TypeBool, 2086 Optional: true, 2087 ForceNew: true, 2088 }, 2089 }, 2090 2091 State: &terraform.InstanceState{ 2092 Attributes: map[string]string{ 2093 "port": "false", 2094 }, 2095 }, 2096 2097 Config: map[string]interface{}{}, 2098 2099 Diff: nil, 2100 2101 Err: false, 2102 }, 2103 2104 { 2105 Name: "Same as prev, but for sets", 2106 Schema: map[string]*Schema{ 2107 "route": &Schema{ 2108 Type: TypeSet, 2109 Optional: true, 2110 Elem: &Resource{ 2111 Schema: map[string]*Schema{ 2112 "index": &Schema{ 2113 Type: TypeInt, 2114 Required: true, 2115 }, 2116 2117 "gateway": &Schema{ 2118 Type: TypeSet, 2119 Optional: true, 2120 Elem: &Schema{Type: TypeInt}, 2121 Set: func(a interface{}) int { 2122 return a.(int) 2123 }, 2124 }, 2125 }, 2126 }, 2127 Set: func(v interface{}) int { 2128 m := v.(map[string]interface{}) 2129 return m["index"].(int) 2130 }, 2131 }, 2132 }, 2133 2134 State: &terraform.InstanceState{ 2135 Attributes: map[string]string{ 2136 "route.#": "0", 2137 }, 2138 }, 2139 2140 Config: map[string]interface{}{}, 2141 2142 Diff: nil, 2143 2144 Err: false, 2145 }, 2146 2147 { 2148 Name: "A set computed element shouldn't cause a diff", 2149 Schema: map[string]*Schema{ 2150 "active": &Schema{ 2151 Type: TypeBool, 2152 Computed: true, 2153 ForceNew: true, 2154 }, 2155 }, 2156 2157 State: &terraform.InstanceState{ 2158 Attributes: map[string]string{ 2159 "active": "true", 2160 }, 2161 }, 2162 2163 Config: map[string]interface{}{}, 2164 2165 Diff: nil, 2166 2167 Err: false, 2168 }, 2169 2170 { 2171 Name: "An empty set should show up in the diff", 2172 Schema: map[string]*Schema{ 2173 "instances": &Schema{ 2174 Type: TypeSet, 2175 Elem: &Schema{Type: TypeString}, 2176 Optional: true, 2177 ForceNew: true, 2178 Set: func(v interface{}) int { 2179 return len(v.(string)) 2180 }, 2181 }, 2182 }, 2183 2184 State: &terraform.InstanceState{ 2185 Attributes: map[string]string{ 2186 "instances.#": "1", 2187 "instances.3": "foo", 2188 }, 2189 }, 2190 2191 Config: map[string]interface{}{}, 2192 2193 Diff: &terraform.InstanceDiff{ 2194 Attributes: map[string]*terraform.ResourceAttrDiff{ 2195 "instances.#": &terraform.ResourceAttrDiff{ 2196 Old: "1", 2197 New: "0", 2198 RequiresNew: true, 2199 }, 2200 "instances.3": &terraform.ResourceAttrDiff{ 2201 Old: "foo", 2202 New: "", 2203 NewRemoved: true, 2204 RequiresNew: true, 2205 }, 2206 }, 2207 }, 2208 2209 Err: false, 2210 }, 2211 2212 { 2213 Name: "Map with empty value", 2214 Schema: map[string]*Schema{ 2215 "vars": &Schema{ 2216 Type: TypeMap, 2217 }, 2218 }, 2219 2220 State: nil, 2221 2222 Config: map[string]interface{}{ 2223 "vars": map[string]interface{}{ 2224 "foo": "", 2225 }, 2226 }, 2227 2228 Diff: &terraform.InstanceDiff{ 2229 Attributes: map[string]*terraform.ResourceAttrDiff{ 2230 "vars.%": &terraform.ResourceAttrDiff{ 2231 Old: "0", 2232 New: "1", 2233 }, 2234 "vars.foo": &terraform.ResourceAttrDiff{ 2235 Old: "", 2236 New: "", 2237 }, 2238 }, 2239 }, 2240 2241 Err: false, 2242 }, 2243 2244 { 2245 Name: "Unset bool, not in state", 2246 Schema: map[string]*Schema{ 2247 "force": &Schema{ 2248 Type: TypeBool, 2249 Optional: true, 2250 ForceNew: true, 2251 }, 2252 }, 2253 2254 State: nil, 2255 2256 Config: map[string]interface{}{}, 2257 2258 Diff: nil, 2259 2260 Err: false, 2261 }, 2262 2263 { 2264 Name: "Unset set, not in state", 2265 Schema: map[string]*Schema{ 2266 "metadata_keys": &Schema{ 2267 Type: TypeSet, 2268 Optional: true, 2269 ForceNew: true, 2270 Elem: &Schema{Type: TypeInt}, 2271 Set: func(interface{}) int { return 0 }, 2272 }, 2273 }, 2274 2275 State: nil, 2276 2277 Config: map[string]interface{}{}, 2278 2279 Diff: nil, 2280 2281 Err: false, 2282 }, 2283 2284 { 2285 Name: "Unset list in state, should not show up computed", 2286 Schema: map[string]*Schema{ 2287 "metadata_keys": &Schema{ 2288 Type: TypeList, 2289 Optional: true, 2290 Computed: true, 2291 ForceNew: true, 2292 Elem: &Schema{Type: TypeInt}, 2293 }, 2294 }, 2295 2296 State: &terraform.InstanceState{ 2297 Attributes: map[string]string{ 2298 "metadata_keys.#": "0", 2299 }, 2300 }, 2301 2302 Config: map[string]interface{}{}, 2303 2304 Diff: nil, 2305 2306 Err: false, 2307 }, 2308 2309 { 2310 Name: "Set element computed substring", 2311 Schema: map[string]*Schema{ 2312 "ports": &Schema{ 2313 Type: TypeSet, 2314 Required: true, 2315 Elem: &Schema{Type: TypeInt}, 2316 Set: func(a interface{}) int { 2317 return a.(int) 2318 }, 2319 }, 2320 }, 2321 2322 State: nil, 2323 2324 Config: map[string]interface{}{ 2325 "ports": []interface{}{1, "${var.foo}32"}, 2326 }, 2327 2328 ConfigVariables: map[string]ast.Variable{ 2329 "var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue), 2330 }, 2331 2332 Diff: &terraform.InstanceDiff{ 2333 Attributes: map[string]*terraform.ResourceAttrDiff{ 2334 "ports.#": &terraform.ResourceAttrDiff{ 2335 Old: "", 2336 New: "", 2337 NewComputed: true, 2338 }, 2339 }, 2340 }, 2341 2342 Err: false, 2343 }, 2344 2345 { 2346 Name: "Computed map without config that's known to be empty does not generate diff", 2347 Schema: map[string]*Schema{ 2348 "tags": &Schema{ 2349 Type: TypeMap, 2350 Computed: true, 2351 }, 2352 }, 2353 2354 Config: nil, 2355 2356 State: &terraform.InstanceState{ 2357 Attributes: map[string]string{ 2358 "tags.%": "0", 2359 }, 2360 }, 2361 2362 Diff: nil, 2363 2364 Err: false, 2365 }, 2366 2367 { 2368 Name: "Set with hyphen keys", 2369 Schema: map[string]*Schema{ 2370 "route": &Schema{ 2371 Type: TypeSet, 2372 Optional: true, 2373 Elem: &Resource{ 2374 Schema: map[string]*Schema{ 2375 "index": &Schema{ 2376 Type: TypeInt, 2377 Required: true, 2378 }, 2379 2380 "gateway-name": &Schema{ 2381 Type: TypeString, 2382 Optional: true, 2383 }, 2384 }, 2385 }, 2386 Set: func(v interface{}) int { 2387 m := v.(map[string]interface{}) 2388 return m["index"].(int) 2389 }, 2390 }, 2391 }, 2392 2393 State: nil, 2394 2395 Config: map[string]interface{}{ 2396 "route": []map[string]interface{}{ 2397 map[string]interface{}{ 2398 "index": "1", 2399 "gateway-name": "hello", 2400 }, 2401 }, 2402 }, 2403 2404 Diff: &terraform.InstanceDiff{ 2405 Attributes: map[string]*terraform.ResourceAttrDiff{ 2406 "route.#": &terraform.ResourceAttrDiff{ 2407 Old: "0", 2408 New: "1", 2409 }, 2410 "route.1.index": &terraform.ResourceAttrDiff{ 2411 Old: "", 2412 New: "1", 2413 }, 2414 "route.1.gateway-name": &terraform.ResourceAttrDiff{ 2415 Old: "", 2416 New: "hello", 2417 }, 2418 }, 2419 }, 2420 2421 Err: false, 2422 }, 2423 2424 { 2425 Name: ": StateFunc in nested set (#1759)", 2426 Schema: map[string]*Schema{ 2427 "service_account": &Schema{ 2428 Type: TypeList, 2429 Optional: true, 2430 ForceNew: true, 2431 Elem: &Resource{ 2432 Schema: map[string]*Schema{ 2433 "scopes": &Schema{ 2434 Type: TypeSet, 2435 Required: true, 2436 ForceNew: true, 2437 Elem: &Schema{ 2438 Type: TypeString, 2439 StateFunc: func(v interface{}) string { 2440 return v.(string) + "!" 2441 }, 2442 }, 2443 Set: func(v interface{}) int { 2444 i, err := strconv.Atoi(v.(string)) 2445 if err != nil { 2446 t.Fatalf("err: %s", err) 2447 } 2448 return i 2449 }, 2450 }, 2451 }, 2452 }, 2453 }, 2454 }, 2455 2456 State: nil, 2457 2458 Config: map[string]interface{}{ 2459 "service_account": []map[string]interface{}{ 2460 { 2461 "scopes": []interface{}{"123"}, 2462 }, 2463 }, 2464 }, 2465 2466 Diff: &terraform.InstanceDiff{ 2467 Attributes: map[string]*terraform.ResourceAttrDiff{ 2468 "service_account.#": &terraform.ResourceAttrDiff{ 2469 Old: "0", 2470 New: "1", 2471 RequiresNew: true, 2472 }, 2473 "service_account.0.scopes.#": &terraform.ResourceAttrDiff{ 2474 Old: "0", 2475 New: "1", 2476 RequiresNew: true, 2477 }, 2478 "service_account.0.scopes.123": &terraform.ResourceAttrDiff{ 2479 Old: "", 2480 New: "123!", 2481 NewExtra: "123", 2482 RequiresNew: true, 2483 }, 2484 }, 2485 }, 2486 2487 Err: false, 2488 }, 2489 2490 { 2491 Name: "Removing set elements", 2492 Schema: map[string]*Schema{ 2493 "instances": &Schema{ 2494 Type: TypeSet, 2495 Elem: &Schema{Type: TypeString}, 2496 Optional: true, 2497 ForceNew: true, 2498 Set: func(v interface{}) int { 2499 return len(v.(string)) 2500 }, 2501 }, 2502 }, 2503 2504 State: &terraform.InstanceState{ 2505 Attributes: map[string]string{ 2506 "instances.#": "2", 2507 "instances.3": "333", 2508 "instances.2": "22", 2509 }, 2510 }, 2511 2512 Config: map[string]interface{}{ 2513 "instances": []interface{}{"333", "4444"}, 2514 }, 2515 2516 Diff: &terraform.InstanceDiff{ 2517 Attributes: map[string]*terraform.ResourceAttrDiff{ 2518 "instances.#": &terraform.ResourceAttrDiff{ 2519 Old: "2", 2520 New: "2", 2521 }, 2522 "instances.2": &terraform.ResourceAttrDiff{ 2523 Old: "22", 2524 New: "", 2525 NewRemoved: true, 2526 RequiresNew: true, 2527 }, 2528 "instances.3": &terraform.ResourceAttrDiff{ 2529 Old: "333", 2530 New: "333", 2531 }, 2532 "instances.4": &terraform.ResourceAttrDiff{ 2533 Old: "", 2534 New: "4444", 2535 RequiresNew: true, 2536 }, 2537 }, 2538 }, 2539 2540 Err: false, 2541 }, 2542 2543 { 2544 Name: "Bools can be set with 0/1 in config, still get true/false", 2545 Schema: map[string]*Schema{ 2546 "one": &Schema{ 2547 Type: TypeBool, 2548 Optional: true, 2549 }, 2550 "two": &Schema{ 2551 Type: TypeBool, 2552 Optional: true, 2553 }, 2554 "three": &Schema{ 2555 Type: TypeBool, 2556 Optional: true, 2557 }, 2558 }, 2559 2560 State: &terraform.InstanceState{ 2561 Attributes: map[string]string{ 2562 "one": "false", 2563 "two": "true", 2564 "three": "true", 2565 }, 2566 }, 2567 2568 Config: map[string]interface{}{ 2569 "one": "1", 2570 "two": "0", 2571 }, 2572 2573 Diff: &terraform.InstanceDiff{ 2574 Attributes: map[string]*terraform.ResourceAttrDiff{ 2575 "one": &terraform.ResourceAttrDiff{ 2576 Old: "false", 2577 New: "true", 2578 }, 2579 "two": &terraform.ResourceAttrDiff{ 2580 Old: "true", 2581 New: "false", 2582 }, 2583 "three": &terraform.ResourceAttrDiff{ 2584 Old: "true", 2585 New: "false", 2586 NewRemoved: true, 2587 }, 2588 }, 2589 }, 2590 2591 Err: false, 2592 }, 2593 2594 { 2595 Name: "tainted in state w/ no attr changes is still a replacement", 2596 Schema: map[string]*Schema{}, 2597 2598 State: &terraform.InstanceState{ 2599 Attributes: map[string]string{ 2600 "id": "someid", 2601 }, 2602 Tainted: true, 2603 }, 2604 2605 Config: map[string]interface{}{}, 2606 2607 Diff: &terraform.InstanceDiff{ 2608 Attributes: map[string]*terraform.ResourceAttrDiff{}, 2609 DestroyTainted: true, 2610 }, 2611 2612 Err: false, 2613 }, 2614 2615 { 2616 Name: "Set ForceNew only marks the changing element as ForceNew", 2617 Schema: map[string]*Schema{ 2618 "ports": &Schema{ 2619 Type: TypeSet, 2620 Required: true, 2621 ForceNew: true, 2622 Elem: &Schema{Type: TypeInt}, 2623 Set: func(a interface{}) int { 2624 return a.(int) 2625 }, 2626 }, 2627 }, 2628 2629 State: &terraform.InstanceState{ 2630 Attributes: map[string]string{ 2631 "ports.#": "3", 2632 "ports.1": "1", 2633 "ports.2": "2", 2634 "ports.4": "4", 2635 }, 2636 }, 2637 2638 Config: map[string]interface{}{ 2639 "ports": []interface{}{5, 2, 1}, 2640 }, 2641 2642 Diff: &terraform.InstanceDiff{ 2643 Attributes: map[string]*terraform.ResourceAttrDiff{ 2644 "ports.#": &terraform.ResourceAttrDiff{ 2645 Old: "3", 2646 New: "3", 2647 }, 2648 "ports.1": &terraform.ResourceAttrDiff{ 2649 Old: "1", 2650 New: "1", 2651 }, 2652 "ports.2": &terraform.ResourceAttrDiff{ 2653 Old: "2", 2654 New: "2", 2655 }, 2656 "ports.5": &terraform.ResourceAttrDiff{ 2657 Old: "", 2658 New: "5", 2659 RequiresNew: true, 2660 }, 2661 "ports.4": &terraform.ResourceAttrDiff{ 2662 Old: "4", 2663 New: "0", 2664 NewRemoved: true, 2665 RequiresNew: true, 2666 }, 2667 }, 2668 }, 2669 }, 2670 2671 { 2672 Name: "removed optional items should trigger ForceNew", 2673 Schema: map[string]*Schema{ 2674 "description": &Schema{ 2675 Type: TypeString, 2676 ForceNew: true, 2677 Optional: true, 2678 }, 2679 }, 2680 2681 State: &terraform.InstanceState{ 2682 Attributes: map[string]string{ 2683 "description": "foo", 2684 }, 2685 }, 2686 2687 Config: map[string]interface{}{}, 2688 2689 Diff: &terraform.InstanceDiff{ 2690 Attributes: map[string]*terraform.ResourceAttrDiff{ 2691 "description": &terraform.ResourceAttrDiff{ 2692 Old: "foo", 2693 New: "", 2694 RequiresNew: true, 2695 NewRemoved: true, 2696 }, 2697 }, 2698 }, 2699 2700 Err: false, 2701 }, 2702 2703 // GH-7715 2704 { 2705 Name: "computed value for boolean field", 2706 Schema: map[string]*Schema{ 2707 "foo": &Schema{ 2708 Type: TypeBool, 2709 ForceNew: true, 2710 Computed: true, 2711 Optional: true, 2712 }, 2713 }, 2714 2715 State: &terraform.InstanceState{}, 2716 2717 Config: map[string]interface{}{ 2718 "foo": "${var.foo}", 2719 }, 2720 2721 ConfigVariables: map[string]ast.Variable{ 2722 "var.foo": interfaceToVariableSwallowError( 2723 config.UnknownVariableValue), 2724 }, 2725 2726 Diff: &terraform.InstanceDiff{ 2727 Attributes: map[string]*terraform.ResourceAttrDiff{ 2728 "foo": &terraform.ResourceAttrDiff{ 2729 Old: "", 2730 New: "false", 2731 NewComputed: true, 2732 RequiresNew: true, 2733 }, 2734 }, 2735 }, 2736 2737 Err: false, 2738 }, 2739 2740 { 2741 Name: "Set ForceNew marks count as ForceNew if computed", 2742 Schema: map[string]*Schema{ 2743 "ports": &Schema{ 2744 Type: TypeSet, 2745 Required: true, 2746 ForceNew: true, 2747 Elem: &Schema{Type: TypeInt}, 2748 Set: func(a interface{}) int { 2749 return a.(int) 2750 }, 2751 }, 2752 }, 2753 2754 State: &terraform.InstanceState{ 2755 Attributes: map[string]string{ 2756 "ports.#": "3", 2757 "ports.1": "1", 2758 "ports.2": "2", 2759 "ports.4": "4", 2760 }, 2761 }, 2762 2763 Config: map[string]interface{}{ 2764 "ports": []interface{}{"${var.foo}", 2, 1}, 2765 }, 2766 2767 ConfigVariables: map[string]ast.Variable{ 2768 "var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue), 2769 }, 2770 2771 Diff: &terraform.InstanceDiff{ 2772 Attributes: map[string]*terraform.ResourceAttrDiff{ 2773 "ports.#": &terraform.ResourceAttrDiff{ 2774 Old: "3", 2775 New: "", 2776 NewComputed: true, 2777 RequiresNew: true, 2778 }, 2779 }, 2780 }, 2781 }, 2782 2783 { 2784 Name: "List with computed schema and ForceNew", 2785 Schema: map[string]*Schema{ 2786 "config": &Schema{ 2787 Type: TypeList, 2788 Optional: true, 2789 ForceNew: true, 2790 Elem: &Schema{ 2791 Type: TypeString, 2792 }, 2793 }, 2794 }, 2795 2796 State: &terraform.InstanceState{ 2797 Attributes: map[string]string{ 2798 "config.#": "2", 2799 "config.0": "a", 2800 "config.1": "b", 2801 }, 2802 }, 2803 2804 Config: map[string]interface{}{ 2805 "config": []interface{}{"${var.a}", "${var.b}"}, 2806 }, 2807 2808 ConfigVariables: map[string]ast.Variable{ 2809 "var.a": interfaceToVariableSwallowError( 2810 config.UnknownVariableValue), 2811 "var.b": interfaceToVariableSwallowError( 2812 config.UnknownVariableValue), 2813 }, 2814 2815 Diff: &terraform.InstanceDiff{ 2816 Attributes: map[string]*terraform.ResourceAttrDiff{ 2817 "config.#": &terraform.ResourceAttrDiff{ 2818 Old: "2", 2819 New: "", 2820 RequiresNew: true, 2821 NewComputed: true, 2822 }, 2823 }, 2824 }, 2825 2826 Err: false, 2827 }, 2828 2829 { 2830 Name: "overridden diff with a CustomizeDiff function, ForceNew not in schema", 2831 Schema: map[string]*Schema{ 2832 "availability_zone": &Schema{ 2833 Type: TypeString, 2834 Optional: true, 2835 Computed: true, 2836 }, 2837 }, 2838 2839 State: nil, 2840 2841 Config: map[string]interface{}{ 2842 "availability_zone": "foo", 2843 }, 2844 2845 CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { 2846 if err := d.SetNew("availability_zone", "bar"); err != nil { 2847 return err 2848 } 2849 if err := d.ForceNew("availability_zone"); err != nil { 2850 return err 2851 } 2852 return nil 2853 }, 2854 2855 Diff: &terraform.InstanceDiff{ 2856 Attributes: map[string]*terraform.ResourceAttrDiff{ 2857 "availability_zone": &terraform.ResourceAttrDiff{ 2858 Old: "", 2859 New: "bar", 2860 RequiresNew: true, 2861 }, 2862 }, 2863 }, 2864 2865 Err: false, 2866 }, 2867 2868 { 2869 // NOTE: This case is technically impossible in the current 2870 // implementation, because optional+computed values never show up in the 2871 // diff. In the event behavior changes this test should ensure that the 2872 // intended diff still shows up. 2873 Name: "overridden removed attribute diff with a CustomizeDiff function, ForceNew not in schema", 2874 Schema: map[string]*Schema{ 2875 "availability_zone": &Schema{ 2876 Type: TypeString, 2877 Optional: true, 2878 Computed: true, 2879 }, 2880 }, 2881 2882 State: nil, 2883 2884 Config: map[string]interface{}{}, 2885 2886 CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { 2887 if err := d.SetNew("availability_zone", "bar"); err != nil { 2888 return err 2889 } 2890 if err := d.ForceNew("availability_zone"); err != nil { 2891 return err 2892 } 2893 return nil 2894 }, 2895 2896 Diff: &terraform.InstanceDiff{ 2897 Attributes: map[string]*terraform.ResourceAttrDiff{ 2898 "availability_zone": &terraform.ResourceAttrDiff{ 2899 Old: "", 2900 New: "bar", 2901 RequiresNew: true, 2902 }, 2903 }, 2904 }, 2905 2906 Err: false, 2907 }, 2908 2909 { 2910 2911 Name: "overridden diff with a CustomizeDiff function, ForceNew in schema", 2912 Schema: map[string]*Schema{ 2913 "availability_zone": &Schema{ 2914 Type: TypeString, 2915 Optional: true, 2916 Computed: true, 2917 ForceNew: true, 2918 }, 2919 }, 2920 2921 State: nil, 2922 2923 Config: map[string]interface{}{ 2924 "availability_zone": "foo", 2925 }, 2926 2927 CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { 2928 if err := d.SetNew("availability_zone", "bar"); err != nil { 2929 return err 2930 } 2931 return nil 2932 }, 2933 2934 Diff: &terraform.InstanceDiff{ 2935 Attributes: map[string]*terraform.ResourceAttrDiff{ 2936 "availability_zone": &terraform.ResourceAttrDiff{ 2937 Old: "", 2938 New: "bar", 2939 RequiresNew: true, 2940 }, 2941 }, 2942 }, 2943 2944 Err: false, 2945 }, 2946 2947 { 2948 Name: "required field with computed diff added with CustomizeDiff function", 2949 Schema: map[string]*Schema{ 2950 "ami_id": &Schema{ 2951 Type: TypeString, 2952 Required: true, 2953 }, 2954 "instance_id": &Schema{ 2955 Type: TypeString, 2956 Computed: true, 2957 }, 2958 }, 2959 2960 State: nil, 2961 2962 Config: map[string]interface{}{ 2963 "ami_id": "foo", 2964 }, 2965 2966 CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { 2967 if err := d.SetNew("instance_id", "bar"); err != nil { 2968 return err 2969 } 2970 return nil 2971 }, 2972 2973 Diff: &terraform.InstanceDiff{ 2974 Attributes: map[string]*terraform.ResourceAttrDiff{ 2975 "ami_id": &terraform.ResourceAttrDiff{ 2976 Old: "", 2977 New: "foo", 2978 }, 2979 "instance_id": &terraform.ResourceAttrDiff{ 2980 Old: "", 2981 New: "bar", 2982 }, 2983 }, 2984 }, 2985 2986 Err: false, 2987 }, 2988 2989 { 2990 Name: "Set ForceNew only marks the changing element as ForceNew - CustomizeDiffFunc edition", 2991 Schema: map[string]*Schema{ 2992 "ports": &Schema{ 2993 Type: TypeSet, 2994 Optional: true, 2995 Computed: true, 2996 Elem: &Schema{Type: TypeInt}, 2997 Set: func(a interface{}) int { 2998 return a.(int) 2999 }, 3000 }, 3001 }, 3002 3003 State: &terraform.InstanceState{ 3004 Attributes: map[string]string{ 3005 "ports.#": "3", 3006 "ports.1": "1", 3007 "ports.2": "2", 3008 "ports.4": "4", 3009 }, 3010 }, 3011 3012 Config: map[string]interface{}{ 3013 "ports": []interface{}{5, 2, 6}, 3014 }, 3015 3016 CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { 3017 if err := d.SetNew("ports", []interface{}{5, 2, 1}); err != nil { 3018 return err 3019 } 3020 if err := d.ForceNew("ports"); err != nil { 3021 return err 3022 } 3023 return nil 3024 }, 3025 3026 Diff: &terraform.InstanceDiff{ 3027 Attributes: map[string]*terraform.ResourceAttrDiff{ 3028 "ports.#": &terraform.ResourceAttrDiff{ 3029 Old: "3", 3030 New: "3", 3031 }, 3032 "ports.1": &terraform.ResourceAttrDiff{ 3033 Old: "1", 3034 New: "1", 3035 }, 3036 "ports.2": &terraform.ResourceAttrDiff{ 3037 Old: "2", 3038 New: "2", 3039 }, 3040 "ports.5": &terraform.ResourceAttrDiff{ 3041 Old: "", 3042 New: "5", 3043 RequiresNew: true, 3044 }, 3045 "ports.4": &terraform.ResourceAttrDiff{ 3046 Old: "4", 3047 New: "0", 3048 NewRemoved: true, 3049 RequiresNew: true, 3050 }, 3051 }, 3052 }, 3053 }, 3054 3055 { 3056 Name: "tainted resource does not run CustomizeDiffFunc", 3057 Schema: map[string]*Schema{}, 3058 3059 State: &terraform.InstanceState{ 3060 Attributes: map[string]string{ 3061 "id": "someid", 3062 }, 3063 Tainted: true, 3064 }, 3065 3066 Config: map[string]interface{}{}, 3067 3068 CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { 3069 return errors.New("diff customization should not have run") 3070 }, 3071 3072 Diff: &terraform.InstanceDiff{ 3073 Attributes: map[string]*terraform.ResourceAttrDiff{}, 3074 DestroyTainted: true, 3075 }, 3076 3077 Err: false, 3078 }, 3079 3080 { 3081 Name: "NewComputed based on a conditional with CustomizeDiffFunc", 3082 Schema: map[string]*Schema{ 3083 "etag": &Schema{ 3084 Type: TypeString, 3085 Optional: true, 3086 Computed: true, 3087 }, 3088 "version_id": &Schema{ 3089 Type: TypeString, 3090 Computed: true, 3091 }, 3092 }, 3093 3094 State: &terraform.InstanceState{ 3095 Attributes: map[string]string{ 3096 "etag": "foo", 3097 "version_id": "1", 3098 }, 3099 }, 3100 3101 Config: map[string]interface{}{ 3102 "etag": "bar", 3103 }, 3104 3105 CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { 3106 if d.HasChange("etag") { 3107 d.SetNewComputed("version_id") 3108 } 3109 return nil 3110 }, 3111 3112 Diff: &terraform.InstanceDiff{ 3113 Attributes: map[string]*terraform.ResourceAttrDiff{ 3114 "etag": &terraform.ResourceAttrDiff{ 3115 Old: "foo", 3116 New: "bar", 3117 }, 3118 "version_id": &terraform.ResourceAttrDiff{ 3119 Old: "1", 3120 New: "", 3121 NewComputed: true, 3122 }, 3123 }, 3124 }, 3125 3126 Err: false, 3127 }, 3128 3129 { 3130 Name: "vetoing a diff", 3131 Schema: map[string]*Schema{ 3132 "foo": &Schema{ 3133 Type: TypeString, 3134 Optional: true, 3135 Computed: true, 3136 }, 3137 }, 3138 3139 State: &terraform.InstanceState{ 3140 Attributes: map[string]string{ 3141 "foo": "bar", 3142 }, 3143 }, 3144 3145 Config: map[string]interface{}{ 3146 "foo": "baz", 3147 }, 3148 3149 CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { 3150 return fmt.Errorf("diff vetoed") 3151 }, 3152 3153 Err: true, 3154 }, 3155 3156 // A lot of resources currently depended on using the empty string as a 3157 // nil/unset value. 3158 // FIXME: We want this to eventually produce a diff, since there 3159 // technically is a new value in the config. 3160 { 3161 Name: "optional, computed, empty string", 3162 Schema: map[string]*Schema{ 3163 "attr": &Schema{ 3164 Type: TypeString, 3165 Optional: true, 3166 Computed: true, 3167 }, 3168 }, 3169 3170 State: &terraform.InstanceState{ 3171 Attributes: map[string]string{ 3172 "attr": "bar", 3173 }, 3174 }, 3175 3176 // this does necessarily depend on an interpolated value, but this 3177 // is often how it comes about in a configuration, otherwise the 3178 // value would be unset. 3179 Config: map[string]interface{}{ 3180 "attr": "${var.foo}", 3181 }, 3182 3183 ConfigVariables: map[string]ast.Variable{ 3184 "var.foo": interfaceToVariableSwallowError(""), 3185 }, 3186 }, 3187 3188 { 3189 Name: "optional, computed, empty string should not crash in CustomizeDiff", 3190 Schema: map[string]*Schema{ 3191 "unrelated_set": { 3192 Type: TypeSet, 3193 Optional: true, 3194 Elem: &Schema{Type: TypeString}, 3195 }, 3196 "stream_enabled": { 3197 Type: TypeBool, 3198 Optional: true, 3199 }, 3200 "stream_view_type": { 3201 Type: TypeString, 3202 Optional: true, 3203 Computed: true, 3204 }, 3205 }, 3206 3207 State: &terraform.InstanceState{ 3208 Attributes: map[string]string{ 3209 "unrelated_set.#": "0", 3210 "stream_enabled": "true", 3211 "stream_view_type": "KEYS_ONLY", 3212 }, 3213 }, 3214 Config: map[string]interface{}{ 3215 "stream_enabled": false, 3216 "stream_view_type": "", 3217 }, 3218 CustomizeDiff: func(diff *ResourceDiff, v interface{}) error { 3219 v, ok := diff.GetOk("unrelated_set") 3220 if ok { 3221 return fmt.Errorf("Didn't expect unrelated_set: %#v", v) 3222 } 3223 return nil 3224 }, 3225 Diff: &terraform.InstanceDiff{ 3226 Attributes: map[string]*terraform.ResourceAttrDiff{ 3227 "stream_enabled": { 3228 Old: "true", 3229 New: "false", 3230 }, 3231 }, 3232 }, 3233 }, 3234 } 3235 3236 for i, tc := range cases { 3237 t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { 3238 c, err := config.NewRawConfig(tc.Config) 3239 if err != nil { 3240 t.Fatalf("err: %s", err) 3241 } 3242 3243 if len(tc.ConfigVariables) > 0 { 3244 if err := c.Interpolate(tc.ConfigVariables); err != nil { 3245 t.Fatalf("err: %s", err) 3246 } 3247 } 3248 3249 d, err := schemaMap(tc.Schema).Diff(tc.State, terraform.NewResourceConfig(c), tc.CustomizeDiff, nil) 3250 if err != nil != tc.Err { 3251 t.Fatalf("err: %s", err) 3252 } 3253 3254 if !reflect.DeepEqual(tc.Diff, d) { 3255 t.Fatalf("expected:\n%#v\n\ngot:\n%#v", tc.Diff, d) 3256 } 3257 }) 3258 } 3259 } 3260 3261 func TestSchemaMap_Input(t *testing.T) { 3262 cases := map[string]struct { 3263 Schema map[string]*Schema 3264 Config map[string]interface{} 3265 Input map[string]string 3266 Result map[string]interface{} 3267 Err bool 3268 }{ 3269 /* 3270 * String decode 3271 */ 3272 3273 "no input on optional field with no config": { 3274 Schema: map[string]*Schema{ 3275 "availability_zone": &Schema{ 3276 Type: TypeString, 3277 Optional: true, 3278 }, 3279 }, 3280 3281 Input: map[string]string{}, 3282 Result: map[string]interface{}{}, 3283 Err: false, 3284 }, 3285 3286 "input ignored when config has a value": { 3287 Schema: map[string]*Schema{ 3288 "availability_zone": &Schema{ 3289 Type: TypeString, 3290 Optional: true, 3291 }, 3292 }, 3293 3294 Config: map[string]interface{}{ 3295 "availability_zone": "bar", 3296 }, 3297 3298 Input: map[string]string{ 3299 "availability_zone": "foo", 3300 }, 3301 3302 Result: map[string]interface{}{}, 3303 3304 Err: false, 3305 }, 3306 3307 "input ignored when schema has a default": { 3308 Schema: map[string]*Schema{ 3309 "availability_zone": &Schema{ 3310 Type: TypeString, 3311 Default: "foo", 3312 Optional: true, 3313 }, 3314 }, 3315 3316 Input: map[string]string{ 3317 "availability_zone": "bar", 3318 }, 3319 3320 Result: map[string]interface{}{}, 3321 3322 Err: false, 3323 }, 3324 3325 "input ignored when default function returns a value": { 3326 Schema: map[string]*Schema{ 3327 "availability_zone": &Schema{ 3328 Type: TypeString, 3329 DefaultFunc: func() (interface{}, error) { 3330 return "foo", nil 3331 }, 3332 Optional: true, 3333 }, 3334 }, 3335 3336 Input: map[string]string{ 3337 "availability_zone": "bar", 3338 }, 3339 3340 Result: map[string]interface{}{}, 3341 3342 Err: false, 3343 }, 3344 3345 "input ignored when default function returns an empty string": { 3346 Schema: map[string]*Schema{ 3347 "availability_zone": &Schema{ 3348 Type: TypeString, 3349 Default: "", 3350 Optional: true, 3351 }, 3352 }, 3353 3354 Input: map[string]string{ 3355 "availability_zone": "bar", 3356 }, 3357 3358 Result: map[string]interface{}{}, 3359 3360 Err: false, 3361 }, 3362 3363 "input used when default function returns nil": { 3364 Schema: map[string]*Schema{ 3365 "availability_zone": &Schema{ 3366 Type: TypeString, 3367 DefaultFunc: func() (interface{}, error) { 3368 return nil, nil 3369 }, 3370 Required: true, 3371 }, 3372 }, 3373 3374 Input: map[string]string{ 3375 "availability_zone": "bar", 3376 }, 3377 3378 Result: map[string]interface{}{ 3379 "availability_zone": "bar", 3380 }, 3381 3382 Err: false, 3383 }, 3384 3385 "input not used when optional default function returns nil": { 3386 Schema: map[string]*Schema{ 3387 "availability_zone": &Schema{ 3388 Type: TypeString, 3389 DefaultFunc: func() (interface{}, error) { 3390 return nil, nil 3391 }, 3392 Optional: true, 3393 }, 3394 }, 3395 3396 Input: map[string]string{}, 3397 Result: map[string]interface{}{}, 3398 Err: false, 3399 }, 3400 } 3401 3402 for i, tc := range cases { 3403 if tc.Config == nil { 3404 tc.Config = make(map[string]interface{}) 3405 } 3406 3407 c, err := config.NewRawConfig(tc.Config) 3408 if err != nil { 3409 t.Fatalf("err: %s", err) 3410 } 3411 3412 input := new(terraform.MockUIInput) 3413 input.InputReturnMap = tc.Input 3414 3415 rc := terraform.NewResourceConfig(c) 3416 rc.Config = make(map[string]interface{}) 3417 3418 actual, err := schemaMap(tc.Schema).Input(input, rc) 3419 if err != nil != tc.Err { 3420 t.Fatalf("#%v err: %s", i, err) 3421 } 3422 3423 if !reflect.DeepEqual(tc.Result, actual.Config) { 3424 t.Fatalf("#%v: bad:\n\ngot: %#v\nexpected: %#v", i, actual.Config, tc.Result) 3425 } 3426 } 3427 } 3428 3429 func TestSchemaMap_InputDefault(t *testing.T) { 3430 emptyConfig := make(map[string]interface{}) 3431 c, err := config.NewRawConfig(emptyConfig) 3432 if err != nil { 3433 t.Fatalf("err: %s", err) 3434 } 3435 rc := terraform.NewResourceConfig(c) 3436 rc.Config = make(map[string]interface{}) 3437 3438 input := new(terraform.MockUIInput) 3439 input.InputFn = func(opts *terraform.InputOpts) (string, error) { 3440 t.Fatalf("InputFn should not be called on: %#v", opts) 3441 return "", nil 3442 } 3443 3444 schema := map[string]*Schema{ 3445 "availability_zone": &Schema{ 3446 Type: TypeString, 3447 Default: "foo", 3448 Optional: true, 3449 }, 3450 } 3451 actual, err := schemaMap(schema).Input(input, rc) 3452 if err != nil { 3453 t.Fatalf("err: %s", err) 3454 } 3455 3456 expected := map[string]interface{}{} 3457 3458 if !reflect.DeepEqual(expected, actual.Config) { 3459 t.Fatalf("got: %#v\nexpected: %#v", actual.Config, expected) 3460 } 3461 } 3462 3463 func TestSchemaMap_InputDeprecated(t *testing.T) { 3464 emptyConfig := make(map[string]interface{}) 3465 c, err := config.NewRawConfig(emptyConfig) 3466 if err != nil { 3467 t.Fatalf("err: %s", err) 3468 } 3469 rc := terraform.NewResourceConfig(c) 3470 rc.Config = make(map[string]interface{}) 3471 3472 input := new(terraform.MockUIInput) 3473 input.InputFn = func(opts *terraform.InputOpts) (string, error) { 3474 t.Fatalf("InputFn should not be called on: %#v", opts) 3475 return "", nil 3476 } 3477 3478 schema := map[string]*Schema{ 3479 "availability_zone": &Schema{ 3480 Type: TypeString, 3481 Deprecated: "long gone", 3482 Optional: true, 3483 }, 3484 } 3485 actual, err := schemaMap(schema).Input(input, rc) 3486 if err != nil { 3487 t.Fatalf("err: %s", err) 3488 } 3489 3490 expected := map[string]interface{}{} 3491 3492 if !reflect.DeepEqual(expected, actual.Config) { 3493 t.Fatalf("got: %#v\nexpected: %#v", actual.Config, expected) 3494 } 3495 } 3496 3497 func TestSchemaMap_InternalValidate(t *testing.T) { 3498 cases := map[string]struct { 3499 In map[string]*Schema 3500 Err bool 3501 }{ 3502 "nothing": { 3503 nil, 3504 false, 3505 }, 3506 3507 "Both optional and required": { 3508 map[string]*Schema{ 3509 "foo": &Schema{ 3510 Type: TypeInt, 3511 Optional: true, 3512 Required: true, 3513 }, 3514 }, 3515 true, 3516 }, 3517 3518 "No optional and no required": { 3519 map[string]*Schema{ 3520 "foo": &Schema{ 3521 Type: TypeInt, 3522 }, 3523 }, 3524 true, 3525 }, 3526 3527 "Missing Type": { 3528 map[string]*Schema{ 3529 "foo": &Schema{ 3530 Required: true, 3531 }, 3532 }, 3533 true, 3534 }, 3535 3536 "Required but computed": { 3537 map[string]*Schema{ 3538 "foo": &Schema{ 3539 Type: TypeInt, 3540 Required: true, 3541 Computed: true, 3542 }, 3543 }, 3544 true, 3545 }, 3546 3547 "Looks good": { 3548 map[string]*Schema{ 3549 "foo": &Schema{ 3550 Type: TypeString, 3551 Required: true, 3552 }, 3553 }, 3554 false, 3555 }, 3556 3557 "Computed but has default": { 3558 map[string]*Schema{ 3559 "foo": &Schema{ 3560 Type: TypeInt, 3561 Optional: true, 3562 Computed: true, 3563 Default: "foo", 3564 }, 3565 }, 3566 true, 3567 }, 3568 3569 "Required but has default": { 3570 map[string]*Schema{ 3571 "foo": &Schema{ 3572 Type: TypeInt, 3573 Optional: true, 3574 Required: true, 3575 Default: "foo", 3576 }, 3577 }, 3578 true, 3579 }, 3580 3581 "List element not set": { 3582 map[string]*Schema{ 3583 "foo": &Schema{ 3584 Type: TypeList, 3585 }, 3586 }, 3587 true, 3588 }, 3589 3590 "List default": { 3591 map[string]*Schema{ 3592 "foo": &Schema{ 3593 Type: TypeList, 3594 Elem: &Schema{Type: TypeInt}, 3595 Default: "foo", 3596 }, 3597 }, 3598 true, 3599 }, 3600 3601 "List element computed": { 3602 map[string]*Schema{ 3603 "foo": &Schema{ 3604 Type: TypeList, 3605 Optional: true, 3606 Elem: &Schema{ 3607 Type: TypeInt, 3608 Computed: true, 3609 }, 3610 }, 3611 }, 3612 true, 3613 }, 3614 3615 "List element with Set set": { 3616 map[string]*Schema{ 3617 "foo": &Schema{ 3618 Type: TypeList, 3619 Elem: &Schema{Type: TypeInt}, 3620 Set: func(interface{}) int { return 0 }, 3621 Optional: true, 3622 }, 3623 }, 3624 true, 3625 }, 3626 3627 "Set element with no Set set": { 3628 map[string]*Schema{ 3629 "foo": &Schema{ 3630 Type: TypeSet, 3631 Elem: &Schema{Type: TypeInt}, 3632 Optional: true, 3633 }, 3634 }, 3635 false, 3636 }, 3637 3638 "Required but computedWhen": { 3639 map[string]*Schema{ 3640 "foo": &Schema{ 3641 Type: TypeInt, 3642 Required: true, 3643 ComputedWhen: []string{"foo"}, 3644 }, 3645 }, 3646 true, 3647 }, 3648 3649 "Conflicting attributes cannot be required": { 3650 map[string]*Schema{ 3651 "blacklist": &Schema{ 3652 Type: TypeBool, 3653 Required: true, 3654 }, 3655 "whitelist": &Schema{ 3656 Type: TypeBool, 3657 Optional: true, 3658 ConflictsWith: []string{"blacklist"}, 3659 }, 3660 }, 3661 true, 3662 }, 3663 3664 "Attribute with conflicts cannot be required": { 3665 map[string]*Schema{ 3666 "whitelist": &Schema{ 3667 Type: TypeBool, 3668 Required: true, 3669 ConflictsWith: []string{"blacklist"}, 3670 }, 3671 }, 3672 true, 3673 }, 3674 3675 "ConflictsWith cannot be used w/ ComputedWhen": { 3676 map[string]*Schema{ 3677 "blacklist": &Schema{ 3678 Type: TypeBool, 3679 ComputedWhen: []string{"foor"}, 3680 }, 3681 "whitelist": &Schema{ 3682 Type: TypeBool, 3683 Required: true, 3684 ConflictsWith: []string{"blacklist"}, 3685 }, 3686 }, 3687 true, 3688 }, 3689 3690 "Sub-resource invalid": { 3691 map[string]*Schema{ 3692 "foo": &Schema{ 3693 Type: TypeList, 3694 Optional: true, 3695 Elem: &Resource{ 3696 Schema: map[string]*Schema{ 3697 "foo": new(Schema), 3698 }, 3699 }, 3700 }, 3701 }, 3702 true, 3703 }, 3704 3705 "Sub-resource valid": { 3706 map[string]*Schema{ 3707 "foo": &Schema{ 3708 Type: TypeList, 3709 Optional: true, 3710 Elem: &Resource{ 3711 Schema: map[string]*Schema{ 3712 "foo": &Schema{ 3713 Type: TypeInt, 3714 Optional: true, 3715 }, 3716 }, 3717 }, 3718 }, 3719 }, 3720 false, 3721 }, 3722 3723 "ValidateFunc on non-primitive": { 3724 map[string]*Schema{ 3725 "foo": &Schema{ 3726 Type: TypeSet, 3727 Required: true, 3728 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 3729 return 3730 }, 3731 }, 3732 }, 3733 true, 3734 }, 3735 3736 "computed-only field with validateFunc": { 3737 map[string]*Schema{ 3738 "string": &Schema{ 3739 Type: TypeString, 3740 Computed: true, 3741 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 3742 es = append(es, fmt.Errorf("this is not fine")) 3743 return 3744 }, 3745 }, 3746 }, 3747 true, 3748 }, 3749 3750 "computed-only field with diffSuppressFunc": { 3751 map[string]*Schema{ 3752 "string": &Schema{ 3753 Type: TypeString, 3754 Computed: true, 3755 DiffSuppressFunc: func(k, old, new string, d *ResourceData) bool { 3756 // Always suppress any diff 3757 return false 3758 }, 3759 }, 3760 }, 3761 true, 3762 }, 3763 3764 "invalid field name format #1": { 3765 map[string]*Schema{ 3766 "with space": &Schema{ 3767 Type: TypeString, 3768 Optional: true, 3769 }, 3770 }, 3771 true, 3772 }, 3773 3774 "invalid field name format #2": { 3775 map[string]*Schema{ 3776 "WithCapitals": &Schema{ 3777 Type: TypeString, 3778 Optional: true, 3779 }, 3780 }, 3781 true, 3782 }, 3783 3784 "invalid field name format of a Deprecated field": { 3785 map[string]*Schema{ 3786 "WithCapitals": &Schema{ 3787 Type: TypeString, 3788 Optional: true, 3789 Deprecated: "Use with_underscores instead", 3790 }, 3791 }, 3792 false, 3793 }, 3794 3795 "invalid field name format of a Removed field": { 3796 map[string]*Schema{ 3797 "WithCapitals": &Schema{ 3798 Type: TypeString, 3799 Optional: true, 3800 Removed: "Use with_underscores instead", 3801 }, 3802 }, 3803 false, 3804 }, 3805 } 3806 3807 for tn, tc := range cases { 3808 t.Run(tn, func(t *testing.T) { 3809 err := schemaMap(tc.In).InternalValidate(nil) 3810 if err != nil != tc.Err { 3811 if tc.Err { 3812 t.Fatalf("%q: Expected error did not occur:\n\n%#v", tn, tc.In) 3813 } 3814 t.Fatalf("%q: Unexpected error occurred: %s\n\n%#v", tn, err, tc.In) 3815 } 3816 }) 3817 } 3818 3819 } 3820 3821 func TestSchemaMap_DiffSuppress(t *testing.T) { 3822 cases := map[string]struct { 3823 Schema map[string]*Schema 3824 State *terraform.InstanceState 3825 Config map[string]interface{} 3826 ConfigVariables map[string]ast.Variable 3827 ExpectedDiff *terraform.InstanceDiff 3828 Err bool 3829 }{ 3830 "#0 - Suppress otherwise valid diff by returning true": { 3831 Schema: map[string]*Schema{ 3832 "availability_zone": { 3833 Type: TypeString, 3834 Optional: true, 3835 DiffSuppressFunc: func(k, old, new string, d *ResourceData) bool { 3836 // Always suppress any diff 3837 return true 3838 }, 3839 }, 3840 }, 3841 3842 State: nil, 3843 3844 Config: map[string]interface{}{ 3845 "availability_zone": "foo", 3846 }, 3847 3848 ExpectedDiff: nil, 3849 3850 Err: false, 3851 }, 3852 3853 "#1 - Don't suppress diff by returning false": { 3854 Schema: map[string]*Schema{ 3855 "availability_zone": { 3856 Type: TypeString, 3857 Optional: true, 3858 DiffSuppressFunc: func(k, old, new string, d *ResourceData) bool { 3859 // Always suppress any diff 3860 return false 3861 }, 3862 }, 3863 }, 3864 3865 State: nil, 3866 3867 Config: map[string]interface{}{ 3868 "availability_zone": "foo", 3869 }, 3870 3871 ExpectedDiff: &terraform.InstanceDiff{ 3872 Attributes: map[string]*terraform.ResourceAttrDiff{ 3873 "availability_zone": { 3874 Old: "", 3875 New: "foo", 3876 }, 3877 }, 3878 }, 3879 3880 Err: false, 3881 }, 3882 3883 "Default with suppress makes no diff": { 3884 Schema: map[string]*Schema{ 3885 "availability_zone": { 3886 Type: TypeString, 3887 Optional: true, 3888 Default: "foo", 3889 DiffSuppressFunc: func(k, old, new string, d *ResourceData) bool { 3890 return true 3891 }, 3892 }, 3893 }, 3894 3895 State: nil, 3896 3897 Config: map[string]interface{}{}, 3898 3899 ExpectedDiff: nil, 3900 3901 Err: false, 3902 }, 3903 3904 "Default with false suppress makes diff": { 3905 Schema: map[string]*Schema{ 3906 "availability_zone": { 3907 Type: TypeString, 3908 Optional: true, 3909 Default: "foo", 3910 DiffSuppressFunc: func(k, old, new string, d *ResourceData) bool { 3911 return false 3912 }, 3913 }, 3914 }, 3915 3916 State: nil, 3917 3918 Config: map[string]interface{}{}, 3919 3920 ExpectedDiff: &terraform.InstanceDiff{ 3921 Attributes: map[string]*terraform.ResourceAttrDiff{ 3922 "availability_zone": { 3923 Old: "", 3924 New: "foo", 3925 }, 3926 }, 3927 }, 3928 3929 Err: false, 3930 }, 3931 3932 "Complex structure with set of computed string should mark root set as computed": { 3933 Schema: map[string]*Schema{ 3934 "outer": &Schema{ 3935 Type: TypeSet, 3936 Optional: true, 3937 Elem: &Resource{ 3938 Schema: map[string]*Schema{ 3939 "outer_str": &Schema{ 3940 Type: TypeString, 3941 Optional: true, 3942 }, 3943 "inner": &Schema{ 3944 Type: TypeSet, 3945 Optional: true, 3946 Elem: &Resource{ 3947 Schema: map[string]*Schema{ 3948 "inner_str": &Schema{ 3949 Type: TypeString, 3950 Optional: true, 3951 }, 3952 }, 3953 }, 3954 Set: func(v interface{}) int { 3955 return 2 3956 }, 3957 }, 3958 }, 3959 }, 3960 Set: func(v interface{}) int { 3961 return 1 3962 }, 3963 }, 3964 }, 3965 3966 State: nil, 3967 3968 Config: map[string]interface{}{ 3969 "outer": []map[string]interface{}{ 3970 map[string]interface{}{ 3971 "outer_str": "foo", 3972 "inner": []map[string]interface{}{ 3973 map[string]interface{}{ 3974 "inner_str": "${var.bar}", 3975 }, 3976 }, 3977 }, 3978 }, 3979 }, 3980 3981 ConfigVariables: map[string]ast.Variable{ 3982 "var.bar": interfaceToVariableSwallowError(config.UnknownVariableValue), 3983 }, 3984 3985 ExpectedDiff: &terraform.InstanceDiff{ 3986 Attributes: map[string]*terraform.ResourceAttrDiff{ 3987 "outer.#": &terraform.ResourceAttrDiff{ 3988 Old: "0", 3989 New: "1", 3990 }, 3991 "outer.~1.outer_str": &terraform.ResourceAttrDiff{ 3992 Old: "", 3993 New: "foo", 3994 }, 3995 "outer.~1.inner.#": &terraform.ResourceAttrDiff{ 3996 Old: "0", 3997 New: "1", 3998 }, 3999 "outer.~1.inner.~2.inner_str": &terraform.ResourceAttrDiff{ 4000 Old: "", 4001 New: "${var.bar}", 4002 NewComputed: true, 4003 }, 4004 }, 4005 }, 4006 4007 Err: false, 4008 }, 4009 4010 "Complex structure with complex list of computed string should mark root set as computed": { 4011 Schema: map[string]*Schema{ 4012 "outer": &Schema{ 4013 Type: TypeSet, 4014 Optional: true, 4015 Elem: &Resource{ 4016 Schema: map[string]*Schema{ 4017 "outer_str": &Schema{ 4018 Type: TypeString, 4019 Optional: true, 4020 }, 4021 "inner": &Schema{ 4022 Type: TypeList, 4023 Optional: true, 4024 Elem: &Resource{ 4025 Schema: map[string]*Schema{ 4026 "inner_str": &Schema{ 4027 Type: TypeString, 4028 Optional: true, 4029 }, 4030 }, 4031 }, 4032 }, 4033 }, 4034 }, 4035 Set: func(v interface{}) int { 4036 return 1 4037 }, 4038 }, 4039 }, 4040 4041 State: nil, 4042 4043 Config: map[string]interface{}{ 4044 "outer": []map[string]interface{}{ 4045 map[string]interface{}{ 4046 "outer_str": "foo", 4047 "inner": []map[string]interface{}{ 4048 map[string]interface{}{ 4049 "inner_str": "${var.bar}", 4050 }, 4051 }, 4052 }, 4053 }, 4054 }, 4055 4056 ConfigVariables: map[string]ast.Variable{ 4057 "var.bar": interfaceToVariableSwallowError(config.UnknownVariableValue), 4058 }, 4059 4060 ExpectedDiff: &terraform.InstanceDiff{ 4061 Attributes: map[string]*terraform.ResourceAttrDiff{ 4062 "outer.#": &terraform.ResourceAttrDiff{ 4063 Old: "0", 4064 New: "1", 4065 }, 4066 "outer.~1.outer_str": &terraform.ResourceAttrDiff{ 4067 Old: "", 4068 New: "foo", 4069 }, 4070 "outer.~1.inner.#": &terraform.ResourceAttrDiff{ 4071 Old: "0", 4072 New: "1", 4073 }, 4074 "outer.~1.inner.0.inner_str": &terraform.ResourceAttrDiff{ 4075 Old: "", 4076 New: "${var.bar}", 4077 NewComputed: true, 4078 }, 4079 }, 4080 }, 4081 4082 Err: false, 4083 }, 4084 } 4085 4086 for tn, tc := range cases { 4087 t.Run(tn, func(t *testing.T) { 4088 c, err := config.NewRawConfig(tc.Config) 4089 if err != nil { 4090 t.Fatalf("#%q err: %s", tn, err) 4091 } 4092 4093 if len(tc.ConfigVariables) > 0 { 4094 if err := c.Interpolate(tc.ConfigVariables); err != nil { 4095 t.Fatalf("#%q err: %s", tn, err) 4096 } 4097 } 4098 4099 d, err := schemaMap(tc.Schema).Diff(tc.State, terraform.NewResourceConfig(c), nil, nil) 4100 if err != nil != tc.Err { 4101 t.Fatalf("#%q err: %s", tn, err) 4102 } 4103 4104 if !reflect.DeepEqual(tc.ExpectedDiff, d) { 4105 t.Fatalf("#%q:\n\nexpected:\n%#v\n\ngot:\n%#v", tn, tc.ExpectedDiff, d) 4106 } 4107 }) 4108 } 4109 } 4110 4111 func TestSchemaMap_Validate(t *testing.T) { 4112 cases := map[string]struct { 4113 Schema map[string]*Schema 4114 Config map[string]interface{} 4115 Vars map[string]string 4116 Err bool 4117 Errors []error 4118 Warnings []string 4119 }{ 4120 "Good": { 4121 Schema: map[string]*Schema{ 4122 "availability_zone": &Schema{ 4123 Type: TypeString, 4124 Optional: true, 4125 Computed: true, 4126 ForceNew: true, 4127 }, 4128 }, 4129 4130 Config: map[string]interface{}{ 4131 "availability_zone": "foo", 4132 }, 4133 }, 4134 4135 "Good, because the var is not set and that error will come elsewhere": { 4136 Schema: map[string]*Schema{ 4137 "size": &Schema{ 4138 Type: TypeInt, 4139 Required: true, 4140 }, 4141 }, 4142 4143 Config: map[string]interface{}{ 4144 "size": "${var.foo}", 4145 }, 4146 4147 Vars: map[string]string{ 4148 "var.foo": config.UnknownVariableValue, 4149 }, 4150 }, 4151 4152 "Required field not set": { 4153 Schema: map[string]*Schema{ 4154 "availability_zone": &Schema{ 4155 Type: TypeString, 4156 Required: true, 4157 }, 4158 }, 4159 4160 Config: map[string]interface{}{}, 4161 4162 Err: true, 4163 }, 4164 4165 "Invalid basic type": { 4166 Schema: map[string]*Schema{ 4167 "port": &Schema{ 4168 Type: TypeInt, 4169 Required: true, 4170 }, 4171 }, 4172 4173 Config: map[string]interface{}{ 4174 "port": "I am invalid", 4175 }, 4176 4177 Err: true, 4178 }, 4179 4180 "Invalid complex type": { 4181 Schema: map[string]*Schema{ 4182 "user_data": &Schema{ 4183 Type: TypeString, 4184 Optional: true, 4185 }, 4186 }, 4187 4188 Config: map[string]interface{}{ 4189 "user_data": []interface{}{ 4190 map[string]interface{}{ 4191 "foo": "bar", 4192 }, 4193 }, 4194 }, 4195 4196 Err: true, 4197 }, 4198 4199 "Bad type, interpolated": { 4200 Schema: map[string]*Schema{ 4201 "size": &Schema{ 4202 Type: TypeInt, 4203 Required: true, 4204 }, 4205 }, 4206 4207 Config: map[string]interface{}{ 4208 "size": "${var.foo}", 4209 }, 4210 4211 Vars: map[string]string{ 4212 "var.foo": "nope", 4213 }, 4214 4215 Err: true, 4216 }, 4217 4218 "Required but has DefaultFunc": { 4219 Schema: map[string]*Schema{ 4220 "availability_zone": &Schema{ 4221 Type: TypeString, 4222 Required: true, 4223 DefaultFunc: func() (interface{}, error) { 4224 return "foo", nil 4225 }, 4226 }, 4227 }, 4228 4229 Config: nil, 4230 }, 4231 4232 "Required but has DefaultFunc return nil": { 4233 Schema: map[string]*Schema{ 4234 "availability_zone": &Schema{ 4235 Type: TypeString, 4236 Required: true, 4237 DefaultFunc: func() (interface{}, error) { 4238 return nil, nil 4239 }, 4240 }, 4241 }, 4242 4243 Config: nil, 4244 4245 Err: true, 4246 }, 4247 4248 "List with promotion": { 4249 Schema: map[string]*Schema{ 4250 "ingress": &Schema{ 4251 Type: TypeList, 4252 Elem: &Schema{Type: TypeInt}, 4253 PromoteSingle: true, 4254 Optional: true, 4255 }, 4256 }, 4257 4258 Config: map[string]interface{}{ 4259 "ingress": "5", 4260 }, 4261 4262 Err: false, 4263 }, 4264 4265 "List with promotion set as list": { 4266 Schema: map[string]*Schema{ 4267 "ingress": &Schema{ 4268 Type: TypeList, 4269 Elem: &Schema{Type: TypeInt}, 4270 PromoteSingle: true, 4271 Optional: true, 4272 }, 4273 }, 4274 4275 Config: map[string]interface{}{ 4276 "ingress": []interface{}{"5"}, 4277 }, 4278 4279 Err: false, 4280 }, 4281 4282 "Optional sub-resource": { 4283 Schema: map[string]*Schema{ 4284 "ingress": &Schema{ 4285 Type: TypeList, 4286 Elem: &Resource{ 4287 Schema: map[string]*Schema{ 4288 "from": &Schema{ 4289 Type: TypeInt, 4290 Required: true, 4291 }, 4292 }, 4293 }, 4294 }, 4295 }, 4296 4297 Config: map[string]interface{}{}, 4298 4299 Err: false, 4300 }, 4301 4302 "Sub-resource is the wrong type": { 4303 Schema: map[string]*Schema{ 4304 "ingress": &Schema{ 4305 Type: TypeList, 4306 Required: true, 4307 Elem: &Resource{ 4308 Schema: map[string]*Schema{ 4309 "from": &Schema{ 4310 Type: TypeInt, 4311 Required: true, 4312 }, 4313 }, 4314 }, 4315 }, 4316 }, 4317 4318 Config: map[string]interface{}{ 4319 "ingress": []interface{}{"foo"}, 4320 }, 4321 4322 Err: true, 4323 }, 4324 4325 "Not a list": { 4326 Schema: map[string]*Schema{ 4327 "ingress": &Schema{ 4328 Type: TypeList, 4329 Elem: &Resource{ 4330 Schema: map[string]*Schema{ 4331 "from": &Schema{ 4332 Type: TypeInt, 4333 Required: true, 4334 }, 4335 }, 4336 }, 4337 }, 4338 }, 4339 4340 Config: map[string]interface{}{ 4341 "ingress": "foo", 4342 }, 4343 4344 Err: true, 4345 }, 4346 4347 "Required sub-resource field": { 4348 Schema: map[string]*Schema{ 4349 "ingress": &Schema{ 4350 Type: TypeList, 4351 Elem: &Resource{ 4352 Schema: map[string]*Schema{ 4353 "from": &Schema{ 4354 Type: TypeInt, 4355 Required: true, 4356 }, 4357 }, 4358 }, 4359 }, 4360 }, 4361 4362 Config: map[string]interface{}{ 4363 "ingress": []interface{}{ 4364 map[string]interface{}{}, 4365 }, 4366 }, 4367 4368 Err: true, 4369 }, 4370 4371 "Good sub-resource": { 4372 Schema: map[string]*Schema{ 4373 "ingress": &Schema{ 4374 Type: TypeList, 4375 Optional: true, 4376 Elem: &Resource{ 4377 Schema: map[string]*Schema{ 4378 "from": &Schema{ 4379 Type: TypeInt, 4380 Required: true, 4381 }, 4382 }, 4383 }, 4384 }, 4385 }, 4386 4387 Config: map[string]interface{}{ 4388 "ingress": []interface{}{ 4389 map[string]interface{}{ 4390 "from": 80, 4391 }, 4392 }, 4393 }, 4394 4395 Err: false, 4396 }, 4397 4398 "Good sub-resource, interpolated value": { 4399 Schema: map[string]*Schema{ 4400 "ingress": &Schema{ 4401 Type: TypeList, 4402 Optional: true, 4403 Elem: &Resource{ 4404 Schema: map[string]*Schema{ 4405 "from": &Schema{ 4406 Type: TypeInt, 4407 Required: true, 4408 }, 4409 }, 4410 }, 4411 }, 4412 }, 4413 4414 Config: map[string]interface{}{ 4415 "ingress": []interface{}{ 4416 `${map("from", "80")}`, 4417 }, 4418 }, 4419 4420 Vars: map[string]string{}, 4421 4422 Err: false, 4423 }, 4424 4425 "Good sub-resource, computed value": { 4426 Schema: map[string]*Schema{ 4427 "ingress": &Schema{ 4428 Type: TypeList, 4429 Optional: true, 4430 Elem: &Resource{ 4431 Schema: map[string]*Schema{ 4432 "from": &Schema{ 4433 Type: TypeInt, 4434 Optional: true, 4435 }, 4436 }, 4437 }, 4438 }, 4439 }, 4440 4441 Config: map[string]interface{}{ 4442 "ingress": []interface{}{ 4443 `${map("from", var.port)}`, 4444 }, 4445 }, 4446 4447 Vars: map[string]string{ 4448 "var.port": config.UnknownVariableValue, 4449 }, 4450 4451 Err: false, 4452 }, 4453 4454 "Invalid/unknown field": { 4455 Schema: map[string]*Schema{ 4456 "availability_zone": &Schema{ 4457 Type: TypeString, 4458 Optional: true, 4459 Computed: true, 4460 ForceNew: true, 4461 }, 4462 }, 4463 4464 Config: map[string]interface{}{ 4465 "foo": "bar", 4466 }, 4467 4468 Err: true, 4469 }, 4470 4471 "Invalid/unknown field with computed value": { 4472 Schema: map[string]*Schema{ 4473 "availability_zone": &Schema{ 4474 Type: TypeString, 4475 Optional: true, 4476 Computed: true, 4477 ForceNew: true, 4478 }, 4479 }, 4480 4481 Config: map[string]interface{}{ 4482 "foo": "${var.foo}", 4483 }, 4484 4485 Vars: map[string]string{ 4486 "var.foo": config.UnknownVariableValue, 4487 }, 4488 4489 Err: true, 4490 }, 4491 4492 "Computed field set": { 4493 Schema: map[string]*Schema{ 4494 "availability_zone": &Schema{ 4495 Type: TypeString, 4496 Computed: true, 4497 }, 4498 }, 4499 4500 Config: map[string]interface{}{ 4501 "availability_zone": "bar", 4502 }, 4503 4504 Err: true, 4505 }, 4506 4507 "Not a set": { 4508 Schema: map[string]*Schema{ 4509 "ports": &Schema{ 4510 Type: TypeSet, 4511 Required: true, 4512 Elem: &Schema{Type: TypeInt}, 4513 Set: func(a interface{}) int { 4514 return a.(int) 4515 }, 4516 }, 4517 }, 4518 4519 Config: map[string]interface{}{ 4520 "ports": "foo", 4521 }, 4522 4523 Err: true, 4524 }, 4525 4526 "Maps": { 4527 Schema: map[string]*Schema{ 4528 "user_data": &Schema{ 4529 Type: TypeMap, 4530 Optional: true, 4531 }, 4532 }, 4533 4534 Config: map[string]interface{}{ 4535 "user_data": "foo", 4536 }, 4537 4538 Err: true, 4539 }, 4540 4541 "Good map: data surrounded by extra slice": { 4542 Schema: map[string]*Schema{ 4543 "user_data": &Schema{ 4544 Type: TypeMap, 4545 Optional: true, 4546 }, 4547 }, 4548 4549 Config: map[string]interface{}{ 4550 "user_data": []interface{}{ 4551 map[string]interface{}{ 4552 "foo": "bar", 4553 }, 4554 }, 4555 }, 4556 }, 4557 4558 "Good map": { 4559 Schema: map[string]*Schema{ 4560 "user_data": &Schema{ 4561 Type: TypeMap, 4562 Optional: true, 4563 }, 4564 }, 4565 4566 Config: map[string]interface{}{ 4567 "user_data": map[string]interface{}{ 4568 "foo": "bar", 4569 }, 4570 }, 4571 }, 4572 4573 "Map with type specified as value type": { 4574 Schema: map[string]*Schema{ 4575 "user_data": &Schema{ 4576 Type: TypeMap, 4577 Optional: true, 4578 Elem: TypeBool, 4579 }, 4580 }, 4581 4582 Config: map[string]interface{}{ 4583 "user_data": map[string]interface{}{ 4584 "foo": "not_a_bool", 4585 }, 4586 }, 4587 4588 Err: true, 4589 }, 4590 4591 "Map with type specified as nested Schema": { 4592 Schema: map[string]*Schema{ 4593 "user_data": &Schema{ 4594 Type: TypeMap, 4595 Optional: true, 4596 Elem: &Schema{Type: TypeBool}, 4597 }, 4598 }, 4599 4600 Config: map[string]interface{}{ 4601 "user_data": map[string]interface{}{ 4602 "foo": "not_a_bool", 4603 }, 4604 }, 4605 4606 Err: true, 4607 }, 4608 4609 "Bad map: just a slice": { 4610 Schema: map[string]*Schema{ 4611 "user_data": &Schema{ 4612 Type: TypeMap, 4613 Optional: true, 4614 }, 4615 }, 4616 4617 Config: map[string]interface{}{ 4618 "user_data": []interface{}{ 4619 "foo", 4620 }, 4621 }, 4622 4623 Err: true, 4624 }, 4625 4626 "Good set: config has slice with single interpolated value": { 4627 Schema: map[string]*Schema{ 4628 "security_groups": &Schema{ 4629 Type: TypeSet, 4630 Optional: true, 4631 Computed: true, 4632 ForceNew: true, 4633 Elem: &Schema{Type: TypeString}, 4634 Set: func(v interface{}) int { 4635 return len(v.(string)) 4636 }, 4637 }, 4638 }, 4639 4640 Config: map[string]interface{}{ 4641 "security_groups": []interface{}{"${var.foo}"}, 4642 }, 4643 4644 Err: false, 4645 }, 4646 4647 "Bad set: config has single interpolated value": { 4648 Schema: map[string]*Schema{ 4649 "security_groups": &Schema{ 4650 Type: TypeSet, 4651 Optional: true, 4652 Computed: true, 4653 ForceNew: true, 4654 Elem: &Schema{Type: TypeString}, 4655 }, 4656 }, 4657 4658 Config: map[string]interface{}{ 4659 "security_groups": "${var.foo}", 4660 }, 4661 4662 Err: true, 4663 }, 4664 4665 "Bad, subresource should not allow unknown elements": { 4666 Schema: map[string]*Schema{ 4667 "ingress": &Schema{ 4668 Type: TypeList, 4669 Optional: true, 4670 Elem: &Resource{ 4671 Schema: map[string]*Schema{ 4672 "port": &Schema{ 4673 Type: TypeInt, 4674 Required: true, 4675 }, 4676 }, 4677 }, 4678 }, 4679 }, 4680 4681 Config: map[string]interface{}{ 4682 "ingress": []interface{}{ 4683 map[string]interface{}{ 4684 "port": 80, 4685 "other": "yes", 4686 }, 4687 }, 4688 }, 4689 4690 Err: true, 4691 }, 4692 4693 "Bad, subresource should not allow invalid types": { 4694 Schema: map[string]*Schema{ 4695 "ingress": &Schema{ 4696 Type: TypeList, 4697 Optional: true, 4698 Elem: &Resource{ 4699 Schema: map[string]*Schema{ 4700 "port": &Schema{ 4701 Type: TypeInt, 4702 Required: true, 4703 }, 4704 }, 4705 }, 4706 }, 4707 }, 4708 4709 Config: map[string]interface{}{ 4710 "ingress": []interface{}{ 4711 map[string]interface{}{ 4712 "port": "bad", 4713 }, 4714 }, 4715 }, 4716 4717 Err: true, 4718 }, 4719 4720 "Bad, should not allow lists to be assigned to string attributes": { 4721 Schema: map[string]*Schema{ 4722 "availability_zone": &Schema{ 4723 Type: TypeString, 4724 Required: true, 4725 }, 4726 }, 4727 4728 Config: map[string]interface{}{ 4729 "availability_zone": []interface{}{"foo", "bar", "baz"}, 4730 }, 4731 4732 Err: true, 4733 }, 4734 4735 "Bad, should not allow maps to be assigned to string attributes": { 4736 Schema: map[string]*Schema{ 4737 "availability_zone": &Schema{ 4738 Type: TypeString, 4739 Required: true, 4740 }, 4741 }, 4742 4743 Config: map[string]interface{}{ 4744 "availability_zone": map[string]interface{}{"foo": "bar", "baz": "thing"}, 4745 }, 4746 4747 Err: true, 4748 }, 4749 4750 "Deprecated attribute usage generates warning, but not error": { 4751 Schema: map[string]*Schema{ 4752 "old_news": &Schema{ 4753 Type: TypeString, 4754 Optional: true, 4755 Deprecated: "please use 'new_news' instead", 4756 }, 4757 }, 4758 4759 Config: map[string]interface{}{ 4760 "old_news": "extra extra!", 4761 }, 4762 4763 Err: false, 4764 4765 Warnings: []string{ 4766 "\"old_news\": [DEPRECATED] please use 'new_news' instead", 4767 }, 4768 }, 4769 4770 "Deprecated generates no warnings if attr not used": { 4771 Schema: map[string]*Schema{ 4772 "old_news": &Schema{ 4773 Type: TypeString, 4774 Optional: true, 4775 Deprecated: "please use 'new_news' instead", 4776 }, 4777 }, 4778 4779 Err: false, 4780 4781 Warnings: nil, 4782 }, 4783 4784 "Removed attribute usage generates error": { 4785 Schema: map[string]*Schema{ 4786 "long_gone": &Schema{ 4787 Type: TypeString, 4788 Optional: true, 4789 Removed: "no longer supported by Cloud API", 4790 }, 4791 }, 4792 4793 Config: map[string]interface{}{ 4794 "long_gone": "still here!", 4795 }, 4796 4797 Err: true, 4798 Errors: []error{ 4799 fmt.Errorf("\"long_gone\": [REMOVED] no longer supported by Cloud API"), 4800 }, 4801 }, 4802 4803 "Removed generates no errors if attr not used": { 4804 Schema: map[string]*Schema{ 4805 "long_gone": &Schema{ 4806 Type: TypeString, 4807 Optional: true, 4808 Removed: "no longer supported by Cloud API", 4809 }, 4810 }, 4811 4812 Err: false, 4813 }, 4814 4815 "Conflicting attributes generate error": { 4816 Schema: map[string]*Schema{ 4817 "whitelist": &Schema{ 4818 Type: TypeString, 4819 Optional: true, 4820 }, 4821 "blacklist": &Schema{ 4822 Type: TypeString, 4823 Optional: true, 4824 ConflictsWith: []string{"whitelist"}, 4825 }, 4826 }, 4827 4828 Config: map[string]interface{}{ 4829 "whitelist": "white-val", 4830 "blacklist": "black-val", 4831 }, 4832 4833 Err: true, 4834 Errors: []error{ 4835 fmt.Errorf("\"blacklist\": conflicts with whitelist"), 4836 }, 4837 }, 4838 4839 "Required attribute & undefined conflicting optional are good": { 4840 Schema: map[string]*Schema{ 4841 "required_att": &Schema{ 4842 Type: TypeString, 4843 Required: true, 4844 }, 4845 "optional_att": &Schema{ 4846 Type: TypeString, 4847 Optional: true, 4848 ConflictsWith: []string{"required_att"}, 4849 }, 4850 }, 4851 4852 Config: map[string]interface{}{ 4853 "required_att": "required-val", 4854 }, 4855 4856 Err: false, 4857 }, 4858 4859 "Required conflicting attribute & defined optional generate error": { 4860 Schema: map[string]*Schema{ 4861 "required_att": &Schema{ 4862 Type: TypeString, 4863 Required: true, 4864 }, 4865 "optional_att": &Schema{ 4866 Type: TypeString, 4867 Optional: true, 4868 ConflictsWith: []string{"required_att"}, 4869 }, 4870 }, 4871 4872 Config: map[string]interface{}{ 4873 "required_att": "required-val", 4874 "optional_att": "optional-val", 4875 }, 4876 4877 Err: true, 4878 Errors: []error{ 4879 fmt.Errorf(`"optional_att": conflicts with required_att`), 4880 }, 4881 }, 4882 4883 "Computed + Optional fields conflicting with each other": { 4884 Schema: map[string]*Schema{ 4885 "foo_att": &Schema{ 4886 Type: TypeString, 4887 Optional: true, 4888 Computed: true, 4889 ConflictsWith: []string{"bar_att"}, 4890 }, 4891 "bar_att": &Schema{ 4892 Type: TypeString, 4893 Optional: true, 4894 Computed: true, 4895 ConflictsWith: []string{"foo_att"}, 4896 }, 4897 }, 4898 4899 Config: map[string]interface{}{ 4900 "foo_att": "foo-val", 4901 "bar_att": "bar-val", 4902 }, 4903 4904 Err: true, 4905 Errors: []error{ 4906 fmt.Errorf(`"foo_att": conflicts with bar_att`), 4907 fmt.Errorf(`"bar_att": conflicts with foo_att`), 4908 }, 4909 }, 4910 4911 "Computed + Optional fields NOT conflicting with each other": { 4912 Schema: map[string]*Schema{ 4913 "foo_att": &Schema{ 4914 Type: TypeString, 4915 Optional: true, 4916 Computed: true, 4917 ConflictsWith: []string{"bar_att"}, 4918 }, 4919 "bar_att": &Schema{ 4920 Type: TypeString, 4921 Optional: true, 4922 Computed: true, 4923 ConflictsWith: []string{"foo_att"}, 4924 }, 4925 }, 4926 4927 Config: map[string]interface{}{ 4928 "foo_att": "foo-val", 4929 }, 4930 4931 Err: false, 4932 }, 4933 4934 "Computed + Optional fields that conflict with none set": { 4935 Schema: map[string]*Schema{ 4936 "foo_att": &Schema{ 4937 Type: TypeString, 4938 Optional: true, 4939 Computed: true, 4940 ConflictsWith: []string{"bar_att"}, 4941 }, 4942 "bar_att": &Schema{ 4943 Type: TypeString, 4944 Optional: true, 4945 Computed: true, 4946 ConflictsWith: []string{"foo_att"}, 4947 }, 4948 }, 4949 4950 Config: map[string]interface{}{}, 4951 4952 Err: false, 4953 }, 4954 4955 "Good with ValidateFunc": { 4956 Schema: map[string]*Schema{ 4957 "validate_me": &Schema{ 4958 Type: TypeString, 4959 Required: true, 4960 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 4961 return 4962 }, 4963 }, 4964 }, 4965 Config: map[string]interface{}{ 4966 "validate_me": "valid", 4967 }, 4968 Err: false, 4969 }, 4970 4971 "Bad with ValidateFunc": { 4972 Schema: map[string]*Schema{ 4973 "validate_me": &Schema{ 4974 Type: TypeString, 4975 Required: true, 4976 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 4977 es = append(es, fmt.Errorf("something is not right here")) 4978 return 4979 }, 4980 }, 4981 }, 4982 Config: map[string]interface{}{ 4983 "validate_me": "invalid", 4984 }, 4985 Err: true, 4986 Errors: []error{ 4987 fmt.Errorf(`something is not right here`), 4988 }, 4989 }, 4990 4991 "ValidateFunc not called when type does not match": { 4992 Schema: map[string]*Schema{ 4993 "number": &Schema{ 4994 Type: TypeInt, 4995 Required: true, 4996 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 4997 t.Fatalf("Should not have gotten validate call") 4998 return 4999 }, 5000 }, 5001 }, 5002 Config: map[string]interface{}{ 5003 "number": "NaN", 5004 }, 5005 Err: true, 5006 }, 5007 5008 "ValidateFunc gets decoded type": { 5009 Schema: map[string]*Schema{ 5010 "maybe": &Schema{ 5011 Type: TypeBool, 5012 Required: true, 5013 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 5014 if _, ok := v.(bool); !ok { 5015 t.Fatalf("Expected bool, got: %#v", v) 5016 } 5017 return 5018 }, 5019 }, 5020 }, 5021 Config: map[string]interface{}{ 5022 "maybe": "true", 5023 }, 5024 }, 5025 5026 "ValidateFunc is not called with a computed value": { 5027 Schema: map[string]*Schema{ 5028 "validate_me": &Schema{ 5029 Type: TypeString, 5030 Required: true, 5031 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 5032 es = append(es, fmt.Errorf("something is not right here")) 5033 return 5034 }, 5035 }, 5036 }, 5037 Config: map[string]interface{}{ 5038 "validate_me": "${var.foo}", 5039 }, 5040 Vars: map[string]string{ 5041 "var.foo": config.UnknownVariableValue, 5042 }, 5043 5044 Err: false, 5045 }, 5046 5047 "special timeouts field": { 5048 Schema: map[string]*Schema{ 5049 "availability_zone": &Schema{ 5050 Type: TypeString, 5051 Optional: true, 5052 Computed: true, 5053 ForceNew: true, 5054 }, 5055 }, 5056 5057 Config: map[string]interface{}{ 5058 TimeoutsConfigKey: "bar", 5059 }, 5060 5061 Err: false, 5062 }, 5063 5064 "invalid bool field": { 5065 Schema: map[string]*Schema{ 5066 "bool_field": { 5067 Type: TypeBool, 5068 Optional: true, 5069 }, 5070 }, 5071 Config: map[string]interface{}{ 5072 "bool_field": "abcdef", 5073 }, 5074 Err: true, 5075 }, 5076 "invalid integer field": { 5077 Schema: map[string]*Schema{ 5078 "integer_field": { 5079 Type: TypeInt, 5080 Optional: true, 5081 }, 5082 }, 5083 Config: map[string]interface{}{ 5084 "integer_field": "abcdef", 5085 }, 5086 Err: true, 5087 }, 5088 "invalid float field": { 5089 Schema: map[string]*Schema{ 5090 "float_field": { 5091 Type: TypeFloat, 5092 Optional: true, 5093 }, 5094 }, 5095 Config: map[string]interface{}{ 5096 "float_field": "abcdef", 5097 }, 5098 Err: true, 5099 }, 5100 5101 // Invalid map values 5102 "invalid bool map value": { 5103 Schema: map[string]*Schema{ 5104 "boolMap": &Schema{ 5105 Type: TypeMap, 5106 Elem: TypeBool, 5107 Optional: true, 5108 }, 5109 }, 5110 Config: map[string]interface{}{ 5111 "boolMap": map[string]interface{}{ 5112 "boolField": "notbool", 5113 }, 5114 }, 5115 Err: true, 5116 }, 5117 "invalid int map value": { 5118 Schema: map[string]*Schema{ 5119 "intMap": &Schema{ 5120 Type: TypeMap, 5121 Elem: TypeInt, 5122 Optional: true, 5123 }, 5124 }, 5125 Config: map[string]interface{}{ 5126 "intMap": map[string]interface{}{ 5127 "intField": "notInt", 5128 }, 5129 }, 5130 Err: true, 5131 }, 5132 "invalid float map value": { 5133 Schema: map[string]*Schema{ 5134 "floatMap": &Schema{ 5135 Type: TypeMap, 5136 Elem: TypeFloat, 5137 Optional: true, 5138 }, 5139 }, 5140 Config: map[string]interface{}{ 5141 "floatMap": map[string]interface{}{ 5142 "floatField": "notFloat", 5143 }, 5144 }, 5145 Err: true, 5146 }, 5147 5148 "map with positive validate function": { 5149 Schema: map[string]*Schema{ 5150 "floatInt": &Schema{ 5151 Type: TypeMap, 5152 Elem: TypeInt, 5153 Optional: true, 5154 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 5155 return 5156 }, 5157 }, 5158 }, 5159 Config: map[string]interface{}{ 5160 "floatInt": map[string]interface{}{ 5161 "rightAnswer": "42", 5162 "tooMuch": "43", 5163 }, 5164 }, 5165 Err: false, 5166 }, 5167 "map with negative validate function": { 5168 Schema: map[string]*Schema{ 5169 "floatInt": &Schema{ 5170 Type: TypeMap, 5171 Elem: TypeInt, 5172 Optional: true, 5173 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 5174 es = append(es, fmt.Errorf("this is not fine")) 5175 return 5176 }, 5177 }, 5178 }, 5179 Config: map[string]interface{}{ 5180 "floatInt": map[string]interface{}{ 5181 "rightAnswer": "42", 5182 "tooMuch": "43", 5183 }, 5184 }, 5185 Err: true, 5186 }, 5187 5188 // The Validation function should not see interpolation strings from 5189 // non-computed values. 5190 "set with partially computed list and map": { 5191 Schema: map[string]*Schema{ 5192 "outer": &Schema{ 5193 Type: TypeSet, 5194 Optional: true, 5195 Computed: true, 5196 Elem: &Resource{ 5197 Schema: map[string]*Schema{ 5198 "list": &Schema{ 5199 Type: TypeList, 5200 Optional: true, 5201 Elem: &Schema{ 5202 Type: TypeString, 5203 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 5204 if strings.HasPrefix(v.(string), "${") { 5205 es = append(es, fmt.Errorf("should not have interpolations")) 5206 } 5207 return 5208 }, 5209 }, 5210 }, 5211 }, 5212 }, 5213 }, 5214 }, 5215 Config: map[string]interface{}{ 5216 "outer": []map[string]interface{}{ 5217 { 5218 "list": []interface{}{"${var.a}", "${var.b}", "c"}, 5219 }, 5220 }, 5221 }, 5222 Vars: map[string]string{ 5223 "var.a": "A", 5224 "var.b": config.UnknownVariableValue, 5225 }, 5226 Err: false, 5227 }, 5228 } 5229 5230 for tn, tc := range cases { 5231 t.Run(tn, func(t *testing.T) { 5232 c, err := config.NewRawConfig(tc.Config) 5233 if err != nil { 5234 t.Fatalf("err: %s", err) 5235 } 5236 if tc.Vars != nil { 5237 vars := make(map[string]ast.Variable) 5238 for k, v := range tc.Vars { 5239 vars[k] = ast.Variable{Value: v, Type: ast.TypeString} 5240 } 5241 5242 if err := c.Interpolate(vars); err != nil { 5243 t.Fatalf("err: %s", err) 5244 } 5245 } 5246 5247 ws, es := schemaMap(tc.Schema).Validate(terraform.NewResourceConfig(c)) 5248 if len(es) > 0 != tc.Err { 5249 if len(es) == 0 { 5250 t.Errorf("%q: no errors", tn) 5251 } 5252 5253 for _, e := range es { 5254 t.Errorf("%q: err: %s", tn, e) 5255 } 5256 5257 t.FailNow() 5258 } 5259 5260 if !reflect.DeepEqual(ws, tc.Warnings) { 5261 t.Fatalf("%q: warnings:\n\nexpected: %#v\ngot:%#v", tn, tc.Warnings, ws) 5262 } 5263 5264 if tc.Errors != nil { 5265 sort.Sort(errorSort(es)) 5266 sort.Sort(errorSort(tc.Errors)) 5267 5268 if !reflect.DeepEqual(es, tc.Errors) { 5269 t.Fatalf("%q: errors:\n\nexpected: %q\ngot: %q", tn, tc.Errors, es) 5270 } 5271 } 5272 }) 5273 5274 } 5275 } 5276 5277 func TestSchemaSet_ValidateMaxItems(t *testing.T) { 5278 cases := map[string]struct { 5279 Schema map[string]*Schema 5280 State *terraform.InstanceState 5281 Config map[string]interface{} 5282 ConfigVariables map[string]string 5283 Diff *terraform.InstanceDiff 5284 Err bool 5285 Errors []error 5286 }{ 5287 "#0": { 5288 Schema: map[string]*Schema{ 5289 "aliases": &Schema{ 5290 Type: TypeSet, 5291 Optional: true, 5292 MaxItems: 1, 5293 Elem: &Schema{Type: TypeString}, 5294 }, 5295 }, 5296 State: nil, 5297 Config: map[string]interface{}{ 5298 "aliases": []interface{}{"foo", "bar"}, 5299 }, 5300 Diff: nil, 5301 Err: true, 5302 Errors: []error{ 5303 fmt.Errorf("aliases: attribute supports 1 item maximum, config has 2 declared"), 5304 }, 5305 }, 5306 "#1": { 5307 Schema: map[string]*Schema{ 5308 "aliases": &Schema{ 5309 Type: TypeSet, 5310 Optional: true, 5311 Elem: &Schema{Type: TypeString}, 5312 }, 5313 }, 5314 State: nil, 5315 Config: map[string]interface{}{ 5316 "aliases": []interface{}{"foo", "bar"}, 5317 }, 5318 Diff: nil, 5319 Err: false, 5320 Errors: nil, 5321 }, 5322 "#2": { 5323 Schema: map[string]*Schema{ 5324 "aliases": &Schema{ 5325 Type: TypeSet, 5326 Optional: true, 5327 MaxItems: 1, 5328 Elem: &Schema{Type: TypeString}, 5329 }, 5330 }, 5331 State: nil, 5332 Config: map[string]interface{}{ 5333 "aliases": []interface{}{"foo"}, 5334 }, 5335 Diff: nil, 5336 Err: false, 5337 Errors: nil, 5338 }, 5339 } 5340 5341 for tn, tc := range cases { 5342 c, err := config.NewRawConfig(tc.Config) 5343 if err != nil { 5344 t.Fatalf("%q: err: %s", tn, err) 5345 } 5346 _, es := schemaMap(tc.Schema).Validate(terraform.NewResourceConfig(c)) 5347 5348 if len(es) > 0 != tc.Err { 5349 if len(es) == 0 { 5350 t.Errorf("%q: no errors", tn) 5351 } 5352 5353 for _, e := range es { 5354 t.Errorf("%q: err: %s", tn, e) 5355 } 5356 5357 t.FailNow() 5358 } 5359 5360 if tc.Errors != nil { 5361 if !reflect.DeepEqual(es, tc.Errors) { 5362 t.Fatalf("%q: expected: %q\ngot: %q", tn, tc.Errors, es) 5363 } 5364 } 5365 } 5366 } 5367 5368 func TestSchemaSet_ValidateMinItems(t *testing.T) { 5369 cases := map[string]struct { 5370 Schema map[string]*Schema 5371 State *terraform.InstanceState 5372 Config map[string]interface{} 5373 ConfigVariables map[string]string 5374 Diff *terraform.InstanceDiff 5375 Err bool 5376 Errors []error 5377 }{ 5378 "#0": { 5379 Schema: map[string]*Schema{ 5380 "aliases": &Schema{ 5381 Type: TypeSet, 5382 Optional: true, 5383 MinItems: 2, 5384 Elem: &Schema{Type: TypeString}, 5385 }, 5386 }, 5387 State: nil, 5388 Config: map[string]interface{}{ 5389 "aliases": []interface{}{"foo", "bar"}, 5390 }, 5391 Diff: nil, 5392 Err: false, 5393 Errors: nil, 5394 }, 5395 "#1": { 5396 Schema: map[string]*Schema{ 5397 "aliases": &Schema{ 5398 Type: TypeSet, 5399 Optional: true, 5400 Elem: &Schema{Type: TypeString}, 5401 }, 5402 }, 5403 State: nil, 5404 Config: map[string]interface{}{ 5405 "aliases": []interface{}{"foo", "bar"}, 5406 }, 5407 Diff: nil, 5408 Err: false, 5409 Errors: nil, 5410 }, 5411 "#2": { 5412 Schema: map[string]*Schema{ 5413 "aliases": &Schema{ 5414 Type: TypeSet, 5415 Optional: true, 5416 MinItems: 2, 5417 Elem: &Schema{Type: TypeString}, 5418 }, 5419 }, 5420 State: nil, 5421 Config: map[string]interface{}{ 5422 "aliases": []interface{}{"foo"}, 5423 }, 5424 Diff: nil, 5425 Err: true, 5426 Errors: []error{ 5427 fmt.Errorf("aliases: attribute supports 2 item as a minimum, config has 1 declared"), 5428 }, 5429 }, 5430 } 5431 5432 for tn, tc := range cases { 5433 c, err := config.NewRawConfig(tc.Config) 5434 if err != nil { 5435 t.Fatalf("%q: err: %s", tn, err) 5436 } 5437 _, es := schemaMap(tc.Schema).Validate(terraform.NewResourceConfig(c)) 5438 5439 if len(es) > 0 != tc.Err { 5440 if len(es) == 0 { 5441 t.Errorf("%q: no errors", tn) 5442 } 5443 5444 for _, e := range es { 5445 t.Errorf("%q: err: %s", tn, e) 5446 } 5447 5448 t.FailNow() 5449 } 5450 5451 if tc.Errors != nil { 5452 if !reflect.DeepEqual(es, tc.Errors) { 5453 t.Fatalf("%q: expected: %q\ngot: %q", tn, tc.Errors, es) 5454 } 5455 } 5456 } 5457 } 5458 5459 // errorSort implements sort.Interface to sort errors by their error message 5460 type errorSort []error 5461 5462 func (e errorSort) Len() int { return len(e) } 5463 func (e errorSort) Swap(i, j int) { e[i], e[j] = e[j], e[i] } 5464 func (e errorSort) Less(i, j int) bool { 5465 return e[i].Error() < e[j].Error() 5466 } 5467 5468 func TestSchemaMapDeepCopy(t *testing.T) { 5469 schema := map[string]*Schema{ 5470 "foo": &Schema{ 5471 Type: TypeString, 5472 }, 5473 } 5474 source := schemaMap(schema) 5475 dest := source.DeepCopy() 5476 dest["foo"].ForceNew = true 5477 if reflect.DeepEqual(source, dest) { 5478 t.Fatalf("source and dest should not match") 5479 } 5480 }