| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package schema | |
| import ( | |
| "encoding/json" | |
| "fmt" | |
| "reflect" | |
| "strconv" | |
| "testing" | |
| "time" | |
| "github.com/google/go-cmp/cmp" | |
| "github.com/hashicorp/terraform/internal/configs/hcl2shim" | |
| "github.com/hashicorp/terraform/internal/legacy/terraform" | |
| "github.com/zclconf/go-cty/cty" | |
| ctyjson "github.com/zclconf/go-cty/cty/json" | |
| ) | |
| func TestResourceApply_create(t *testing.T) { | |
| r := &Resource{ | |
| SchemaVersion: 2, | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| called := false | |
| r.Create = func(d *ResourceData, m interface{}) error { | |
| called = true | |
| d.SetId("foo") | |
| return nil | |
| } | |
| var s *terraform.InstanceState = nil | |
| d := &terraform.InstanceDiff{ | |
| Attributes: map[string]*terraform.ResourceAttrDiff{ | |
| "foo": &terraform.ResourceAttrDiff{ | |
| New: "42", | |
| }, | |
| }, | |
| } | |
| actual, err := r.Apply(s, d, nil) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| if !called { | |
| t.Fatal("not called") | |
| } | |
| expected := &terraform.InstanceState{ | |
| ID: "foo", | |
| Attributes: map[string]string{ | |
| "id": "foo", | |
| "foo": "42", | |
| }, | |
| Meta: map[string]interface{}{ | |
| "schema_version": "2", | |
| }, | |
| } | |
| if !reflect.DeepEqual(actual, expected) { | |
| t.Fatalf("bad: %#v", actual) | |
| } | |
| } | |
| func TestResourceApply_Timeout_state(t *testing.T) { | |
| r := &Resource{ | |
| SchemaVersion: 2, | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| Timeouts: &ResourceTimeout{ | |
| Create: DefaultTimeout(40 * time.Minute), | |
| Update: DefaultTimeout(80 * time.Minute), | |
| Delete: DefaultTimeout(40 * time.Minute), | |
| }, | |
| } | |
| called := false | |
| r.Create = func(d *ResourceData, m interface{}) error { | |
| called = true | |
| d.SetId("foo") | |
| return nil | |
| } | |
| var s *terraform.InstanceState = nil | |
| d := &terraform.InstanceDiff{ | |
| Attributes: map[string]*terraform.ResourceAttrDiff{ | |
| "foo": &terraform.ResourceAttrDiff{ | |
| New: "42", | |
| }, | |
| }, | |
| } | |
| diffTimeout := &ResourceTimeout{ | |
| Create: DefaultTimeout(40 * time.Minute), | |
| Update: DefaultTimeout(80 * time.Minute), | |
| Delete: DefaultTimeout(40 * time.Minute), | |
| } | |
| if err := diffTimeout.DiffEncode(d); err != nil { | |
| t.Fatalf("Error encoding timeout to diff: %s", err) | |
| } | |
| actual, err := r.Apply(s, d, nil) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| if !called { | |
| t.Fatal("not called") | |
| } | |
| expected := &terraform.InstanceState{ | |
| ID: "foo", | |
| Attributes: map[string]string{ | |
| "id": "foo", | |
| "foo": "42", | |
| }, | |
| Meta: map[string]interface{}{ | |
| "schema_version": "2", | |
| TimeoutKey: expectedForValues(40, 0, 80, 40, 0), | |
| }, | |
| } | |
| if !reflect.DeepEqual(actual, expected) { | |
| t.Fatalf("Not equal in Timeout State:\n\texpected: %#v\n\tactual: %#v", expected.Meta, actual.Meta) | |
| } | |
| } | |
| // Regression test to ensure that the meta data is read from state, if a | |
| // resource is destroyed and the timeout meta is no longer available from the | |
| // config | |
| func TestResourceApply_Timeout_destroy(t *testing.T) { | |
| timeouts := &ResourceTimeout{ | |
| Create: DefaultTimeout(40 * time.Minute), | |
| Update: DefaultTimeout(80 * time.Minute), | |
| Delete: DefaultTimeout(40 * time.Minute), | |
| } | |
| r := &Resource{ | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| Timeouts: timeouts, | |
| } | |
| called := false | |
| var delTimeout time.Duration | |
| r.Delete = func(d *ResourceData, m interface{}) error { | |
| delTimeout = d.Timeout(TimeoutDelete) | |
| called = true | |
| return nil | |
| } | |
| s := &terraform.InstanceState{ | |
| ID: "bar", | |
| } | |
| if err := timeouts.StateEncode(s); err != nil { | |
| t.Fatalf("Error encoding to state: %s", err) | |
| } | |
| d := &terraform.InstanceDiff{ | |
| Destroy: true, | |
| } | |
| actual, err := r.Apply(s, d, nil) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| if !called { | |
| t.Fatal("delete not called") | |
| } | |
| if *timeouts.Delete != delTimeout { | |
| t.Fatalf("timeouts don't match, expected (%#v), got (%#v)", timeouts.Delete, delTimeout) | |
| } | |
| if actual != nil { | |
| t.Fatalf("bad: %#v", actual) | |
| } | |
| } | |
| func TestResourceDiff_Timeout_diff(t *testing.T) { | |
| r := &Resource{ | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| Timeouts: &ResourceTimeout{ | |
| Create: DefaultTimeout(40 * time.Minute), | |
| Update: DefaultTimeout(80 * time.Minute), | |
| Delete: DefaultTimeout(40 * time.Minute), | |
| }, | |
| } | |
| r.Create = func(d *ResourceData, m interface{}) error { | |
| d.SetId("foo") | |
| return nil | |
| } | |
| conf := terraform.NewResourceConfigRaw( | |
| map[string]interface{}{ | |
| "foo": 42, | |
| TimeoutsConfigKey: map[string]interface{}{ | |
| "create": "2h", | |
| }, | |
| }, | |
| ) | |
| var s *terraform.InstanceState | |
| actual, err := r.Diff(s, conf, nil) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| expected := &terraform.InstanceDiff{ | |
| Attributes: map[string]*terraform.ResourceAttrDiff{ | |
| "foo": &terraform.ResourceAttrDiff{ | |
| New: "42", | |
| }, | |
| }, | |
| } | |
| diffTimeout := &ResourceTimeout{ | |
| Create: DefaultTimeout(120 * time.Minute), | |
| Update: DefaultTimeout(80 * time.Minute), | |
| Delete: DefaultTimeout(40 * time.Minute), | |
| } | |
| if err := diffTimeout.DiffEncode(expected); err != nil { | |
| t.Fatalf("Error encoding timeout to diff: %s", err) | |
| } | |
| if !reflect.DeepEqual(actual, expected) { | |
| t.Fatalf("Not equal Meta in Timeout Diff:\n\texpected: %#v\n\tactual: %#v", expected.Meta, actual.Meta) | |
| } | |
| } | |
| func TestResourceDiff_CustomizeFunc(t *testing.T) { | |
| r := &Resource{ | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| var called bool | |
| r.CustomizeDiff = func(d *ResourceDiff, m interface{}) error { | |
| called = true | |
| return nil | |
| } | |
| conf := terraform.NewResourceConfigRaw( | |
| map[string]interface{}{ | |
| "foo": 42, | |
| }, | |
| ) | |
| var s *terraform.InstanceState | |
| _, err := r.Diff(s, conf, nil) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| if !called { | |
| t.Fatalf("diff customization not called") | |
| } | |
| } | |
| func TestResourceApply_destroy(t *testing.T) { | |
| r := &Resource{ | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| called := false | |
| r.Delete = func(d *ResourceData, m interface{}) error { | |
| called = true | |
| return nil | |
| } | |
| s := &terraform.InstanceState{ | |
| ID: "bar", | |
| } | |
| d := &terraform.InstanceDiff{ | |
| Destroy: true, | |
| } | |
| actual, err := r.Apply(s, d, nil) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| if !called { | |
| t.Fatal("delete not called") | |
| } | |
| if actual != nil { | |
| t.Fatalf("bad: %#v", actual) | |
| } | |
| } | |
| func TestResourceApply_destroyCreate(t *testing.T) { | |
| r := &Resource{ | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| "tags": &Schema{ | |
| Type: TypeMap, | |
| Optional: true, | |
| Computed: true, | |
| }, | |
| }, | |
| } | |
| change := false | |
| r.Create = func(d *ResourceData, m interface{}) error { | |
| change = d.HasChange("tags") | |
| d.SetId("foo") | |
| return nil | |
| } | |
| r.Delete = func(d *ResourceData, m interface{}) error { | |
| return nil | |
| } | |
| var s *terraform.InstanceState = &terraform.InstanceState{ | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "foo": "bar", | |
| "tags.Name": "foo", | |
| }, | |
| } | |
| d := &terraform.InstanceDiff{ | |
| Attributes: map[string]*terraform.ResourceAttrDiff{ | |
| "foo": &terraform.ResourceAttrDiff{ | |
| New: "42", | |
| RequiresNew: true, | |
| }, | |
| "tags.Name": &terraform.ResourceAttrDiff{ | |
| Old: "foo", | |
| New: "foo", | |
| RequiresNew: true, | |
| }, | |
| }, | |
| } | |
| actual, err := r.Apply(s, d, nil) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| if !change { | |
| t.Fatal("should have change") | |
| } | |
| expected := &terraform.InstanceState{ | |
| ID: "foo", | |
| Attributes: map[string]string{ | |
| "id": "foo", | |
| "foo": "42", | |
| "tags.%": "1", | |
| "tags.Name": "foo", | |
| }, | |
| } | |
| if !reflect.DeepEqual(actual, expected) { | |
| t.Fatalf("bad: %#v", actual) | |
| } | |
| } | |
| func TestResourceApply_destroyPartial(t *testing.T) { | |
| r := &Resource{ | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| SchemaVersion: 3, | |
| } | |
| r.Delete = func(d *ResourceData, m interface{}) error { | |
| d.Set("foo", 42) | |
| return fmt.Errorf("some error") | |
| } | |
| s := &terraform.InstanceState{ | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "foo": "12", | |
| }, | |
| } | |
| d := &terraform.InstanceDiff{ | |
| Destroy: true, | |
| } | |
| actual, err := r.Apply(s, d, nil) | |
| if err == nil { | |
| t.Fatal("should error") | |
| } | |
| expected := &terraform.InstanceState{ | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "id": "bar", | |
| "foo": "42", | |
| }, | |
| Meta: map[string]interface{}{ | |
| "schema_version": "3", | |
| }, | |
| } | |
| if !reflect.DeepEqual(actual, expected) { | |
| t.Fatalf("expected:\n%#v\n\ngot:\n%#v", expected, actual) | |
| } | |
| } | |
| func TestResourceApply_update(t *testing.T) { | |
| r := &Resource{ | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| r.Update = func(d *ResourceData, m interface{}) error { | |
| d.Set("foo", 42) | |
| return nil | |
| } | |
| s := &terraform.InstanceState{ | |
| ID: "foo", | |
| Attributes: map[string]string{ | |
| "foo": "12", | |
| }, | |
| } | |
| d := &terraform.InstanceDiff{ | |
| Attributes: map[string]*terraform.ResourceAttrDiff{ | |
| "foo": &terraform.ResourceAttrDiff{ | |
| New: "13", | |
| }, | |
| }, | |
| } | |
| actual, err := r.Apply(s, d, nil) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| expected := &terraform.InstanceState{ | |
| ID: "foo", | |
| Attributes: map[string]string{ | |
| "id": "foo", | |
| "foo": "42", | |
| }, | |
| } | |
| if !reflect.DeepEqual(actual, expected) { | |
| t.Fatalf("bad: %#v", actual) | |
| } | |
| } | |
| func TestResourceApply_updateNoCallback(t *testing.T) { | |
| r := &Resource{ | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| r.Update = nil | |
| s := &terraform.InstanceState{ | |
| ID: "foo", | |
| Attributes: map[string]string{ | |
| "foo": "12", | |
| }, | |
| } | |
| d := &terraform.InstanceDiff{ | |
| Attributes: map[string]*terraform.ResourceAttrDiff{ | |
| "foo": &terraform.ResourceAttrDiff{ | |
| New: "13", | |
| }, | |
| }, | |
| } | |
| actual, err := r.Apply(s, d, nil) | |
| if err == nil { | |
| t.Fatal("should error") | |
| } | |
| expected := &terraform.InstanceState{ | |
| ID: "foo", | |
| Attributes: map[string]string{ | |
| "foo": "12", | |
| }, | |
| } | |
| if !reflect.DeepEqual(actual, expected) { | |
| t.Fatalf("bad: %#v", actual) | |
| } | |
| } | |
| func TestResourceApply_isNewResource(t *testing.T) { | |
| r := &Resource{ | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeString, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| updateFunc := func(d *ResourceData, m interface{}) error { | |
| d.Set("foo", "updated") | |
| if d.IsNewResource() { | |
| d.Set("foo", "new-resource") | |
| } | |
| return nil | |
| } | |
| r.Create = func(d *ResourceData, m interface{}) error { | |
| d.SetId("foo") | |
| d.Set("foo", "created") | |
| return updateFunc(d, m) | |
| } | |
| r.Update = updateFunc | |
| d := &terraform.InstanceDiff{ | |
| Attributes: map[string]*terraform.ResourceAttrDiff{ | |
| "foo": &terraform.ResourceAttrDiff{ | |
| New: "bla-blah", | |
| }, | |
| }, | |
| } | |
| // positive test | |
| var s *terraform.InstanceState = nil | |
| actual, err := r.Apply(s, d, nil) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| expected := &terraform.InstanceState{ | |
| ID: "foo", | |
| Attributes: map[string]string{ | |
| "id": "foo", | |
| "foo": "new-resource", | |
| }, | |
| } | |
| if !reflect.DeepEqual(actual, expected) { | |
| t.Fatalf("actual: %#v\nexpected: %#v", | |
| actual, expected) | |
| } | |
| // negative test | |
| s = &terraform.InstanceState{ | |
| ID: "foo", | |
| Attributes: map[string]string{ | |
| "id": "foo", | |
| "foo": "new-resource", | |
| }, | |
| } | |
| actual, err = r.Apply(s, d, nil) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| expected = &terraform.InstanceState{ | |
| ID: "foo", | |
| Attributes: map[string]string{ | |
| "id": "foo", | |
| "foo": "updated", | |
| }, | |
| } | |
| if !reflect.DeepEqual(actual, expected) { | |
| t.Fatalf("actual: %#v\nexpected: %#v", | |
| actual, expected) | |
| } | |
| } | |
| func TestResourceInternalValidate(t *testing.T) { | |
| cases := []struct { | |
| In *Resource | |
| Writable bool | |
| Err bool | |
| }{ | |
| 0: { | |
| nil, | |
| true, | |
| true, | |
| }, | |
| // No optional and no required | |
| 1: { | |
| &Resource{ | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| Required: true, | |
| }, | |
| }, | |
| }, | |
| true, | |
| true, | |
| }, | |
| // Update undefined for non-ForceNew field | |
| 2: { | |
| &Resource{ | |
| Create: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Schema: map[string]*Schema{ | |
| "boo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| }, | |
| true, | |
| true, | |
| }, | |
| // Update defined for ForceNew field | |
| 3: { | |
| &Resource{ | |
| Create: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Update: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Schema: map[string]*Schema{ | |
| "goo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| ForceNew: true, | |
| }, | |
| }, | |
| }, | |
| true, | |
| true, | |
| }, | |
| // non-writable doesn't need Update, Create or Delete | |
| 4: { | |
| &Resource{ | |
| Schema: map[string]*Schema{ | |
| "goo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| }, | |
| false, | |
| false, | |
| }, | |
| // non-writable *must not* have Create | |
| 5: { | |
| &Resource{ | |
| Create: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Schema: map[string]*Schema{ | |
| "goo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| }, | |
| false, | |
| true, | |
| }, | |
| // writable must have Read | |
| 6: { | |
| &Resource{ | |
| Create: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Update: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Delete: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Schema: map[string]*Schema{ | |
| "goo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| }, | |
| true, | |
| true, | |
| }, | |
| // writable must have Delete | |
| 7: { | |
| &Resource{ | |
| Create: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Read: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Update: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Schema: map[string]*Schema{ | |
| "goo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| }, | |
| true, | |
| true, | |
| }, | |
| 8: { // Reserved name at root should be disallowed | |
| &Resource{ | |
| Create: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Read: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Update: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Delete: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Schema: map[string]*Schema{ | |
| "count": { | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| }, | |
| true, | |
| true, | |
| }, | |
| 9: { // Reserved name at nested levels should be allowed | |
| &Resource{ | |
| Create: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Read: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Update: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Delete: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Schema: map[string]*Schema{ | |
| "parent_list": &Schema{ | |
| Type: TypeString, | |
| Optional: true, | |
| Elem: &Resource{ | |
| Schema: map[string]*Schema{ | |
| "provisioner": { | |
| Type: TypeString, | |
| Optional: true, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| true, | |
| false, | |
| }, | |
| 10: { // Provider reserved name should be allowed in resource | |
| &Resource{ | |
| Create: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Read: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Update: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Delete: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Schema: map[string]*Schema{ | |
| "alias": &Schema{ | |
| Type: TypeString, | |
| Optional: true, | |
| }, | |
| }, | |
| }, | |
| true, | |
| false, | |
| }, | |
| 11: { // ID should be allowed in data source | |
| &Resource{ | |
| Read: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Schema: map[string]*Schema{ | |
| "id": &Schema{ | |
| Type: TypeString, | |
| Optional: true, | |
| }, | |
| }, | |
| }, | |
| false, | |
| false, | |
| }, | |
| 12: { // Deprecated ID should be allowed in resource | |
| &Resource{ | |
| Create: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Read: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Update: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Delete: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Schema: map[string]*Schema{ | |
| "id": &Schema{ | |
| Type: TypeString, | |
| Optional: true, | |
| Deprecated: "Use x_id instead", | |
| }, | |
| }, | |
| }, | |
| true, | |
| false, | |
| }, | |
| 13: { // non-writable must not define CustomizeDiff | |
| &Resource{ | |
| Read: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Schema: map[string]*Schema{ | |
| "goo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| CustomizeDiff: func(*ResourceDiff, interface{}) error { return nil }, | |
| }, | |
| false, | |
| true, | |
| }, | |
| 14: { // Deprecated resource | |
| &Resource{ | |
| Read: func(d *ResourceData, meta interface{}) error { return nil }, | |
| Schema: map[string]*Schema{ | |
| "goo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| DeprecationMessage: "This resource has been deprecated.", | |
| }, | |
| true, | |
| true, | |
| }, | |
| } | |
| for i, tc := range cases { | |
| t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { | |
| sm := schemaMap{} | |
| if tc.In != nil { | |
| sm = schemaMap(tc.In.Schema) | |
| } | |
| err := tc.In.InternalValidate(sm, tc.Writable) | |
| if err != nil && !tc.Err { | |
| t.Fatalf("%d: expected validation to pass: %s", i, err) | |
| } | |
| if err == nil && tc.Err { | |
| t.Fatalf("%d: expected validation to fail", i) | |
| } | |
| }) | |
| } | |
| } | |
| func TestResourceRefresh(t *testing.T) { | |
| r := &Resource{ | |
| SchemaVersion: 2, | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| r.Read = func(d *ResourceData, m interface{}) error { | |
| if m != 42 { | |
| return fmt.Errorf("meta not passed") | |
| } | |
| return d.Set("foo", d.Get("foo").(int)+1) | |
| } | |
| s := &terraform.InstanceState{ | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "foo": "12", | |
| }, | |
| } | |
| expected := &terraform.InstanceState{ | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "id": "bar", | |
| "foo": "13", | |
| }, | |
| Meta: map[string]interface{}{ | |
| "schema_version": "2", | |
| }, | |
| } | |
| actual, err := r.Refresh(s, 42) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| if !reflect.DeepEqual(actual, expected) { | |
| t.Fatalf("bad: %#v", actual) | |
| } | |
| } | |
| func TestResourceRefresh_blankId(t *testing.T) { | |
| r := &Resource{ | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| r.Read = func(d *ResourceData, m interface{}) error { | |
| d.SetId("foo") | |
| return nil | |
| } | |
| s := &terraform.InstanceState{ | |
| ID: "", | |
| Attributes: map[string]string{}, | |
| } | |
| actual, err := r.Refresh(s, 42) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| if actual != nil { | |
| t.Fatalf("bad: %#v", actual) | |
| } | |
| } | |
| func TestResourceRefresh_delete(t *testing.T) { | |
| r := &Resource{ | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| r.Read = func(d *ResourceData, m interface{}) error { | |
| d.SetId("") | |
| return nil | |
| } | |
| s := &terraform.InstanceState{ | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "foo": "12", | |
| }, | |
| } | |
| actual, err := r.Refresh(s, 42) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| if actual != nil { | |
| t.Fatalf("bad: %#v", actual) | |
| } | |
| } | |
| func TestResourceRefresh_existsError(t *testing.T) { | |
| r := &Resource{ | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| r.Exists = func(*ResourceData, interface{}) (bool, error) { | |
| return false, fmt.Errorf("error") | |
| } | |
| r.Read = func(d *ResourceData, m interface{}) error { | |
| panic("shouldn't be called") | |
| } | |
| s := &terraform.InstanceState{ | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "foo": "12", | |
| }, | |
| } | |
| actual, err := r.Refresh(s, 42) | |
| if err == nil { | |
| t.Fatalf("should error") | |
| } | |
| if !reflect.DeepEqual(actual, s) { | |
| t.Fatalf("bad: %#v", actual) | |
| } | |
| } | |
| func TestResourceRefresh_noExists(t *testing.T) { | |
| r := &Resource{ | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| r.Exists = func(*ResourceData, interface{}) (bool, error) { | |
| return false, nil | |
| } | |
| r.Read = func(d *ResourceData, m interface{}) error { | |
| panic("shouldn't be called") | |
| } | |
| s := &terraform.InstanceState{ | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "foo": "12", | |
| }, | |
| } | |
| actual, err := r.Refresh(s, 42) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| if actual != nil { | |
| t.Fatalf("should have no state") | |
| } | |
| } | |
| func TestResourceRefresh_needsMigration(t *testing.T) { | |
| // Schema v2 it deals only in newfoo, which tracks foo as an int | |
| r := &Resource{ | |
| SchemaVersion: 2, | |
| Schema: map[string]*Schema{ | |
| "newfoo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| r.Read = func(d *ResourceData, m interface{}) error { | |
| return d.Set("newfoo", d.Get("newfoo").(int)+1) | |
| } | |
| r.MigrateState = func( | |
| v int, | |
| s *terraform.InstanceState, | |
| meta interface{}) (*terraform.InstanceState, error) { | |
| // Real state migration functions will probably switch on this value, | |
| // but we'll just assert on it for now. | |
| if v != 1 { | |
| t.Fatalf("Expected StateSchemaVersion to be 1, got %d", v) | |
| } | |
| if meta != 42 { | |
| t.Fatal("Expected meta to be passed through to the migration function") | |
| } | |
| oldfoo, err := strconv.ParseFloat(s.Attributes["oldfoo"], 64) | |
| if err != nil { | |
| t.Fatalf("err: %#v", err) | |
| } | |
| s.Attributes["newfoo"] = strconv.Itoa(int(oldfoo * 10)) | |
| delete(s.Attributes, "oldfoo") | |
| return s, nil | |
| } | |
| // State is v1 and deals in oldfoo, which tracked foo as a float at 1/10th | |
| // the scale of newfoo | |
| s := &terraform.InstanceState{ | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "oldfoo": "1.2", | |
| }, | |
| Meta: map[string]interface{}{ | |
| "schema_version": "1", | |
| }, | |
| } | |
| actual, err := r.Refresh(s, 42) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| expected := &terraform.InstanceState{ | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "id": "bar", | |
| "newfoo": "13", | |
| }, | |
| Meta: map[string]interface{}{ | |
| "schema_version": "2", | |
| }, | |
| } | |
| if !reflect.DeepEqual(actual, expected) { | |
| t.Fatalf("bad:\n\nexpected: %#v\ngot: %#v", expected, actual) | |
| } | |
| } | |
| func TestResourceRefresh_noMigrationNeeded(t *testing.T) { | |
| r := &Resource{ | |
| SchemaVersion: 2, | |
| Schema: map[string]*Schema{ | |
| "newfoo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| r.Read = func(d *ResourceData, m interface{}) error { | |
| return d.Set("newfoo", d.Get("newfoo").(int)+1) | |
| } | |
| r.MigrateState = func( | |
| v int, | |
| s *terraform.InstanceState, | |
| meta interface{}) (*terraform.InstanceState, error) { | |
| t.Fatal("Migrate function shouldn't be called!") | |
| return nil, nil | |
| } | |
| s := &terraform.InstanceState{ | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "newfoo": "12", | |
| }, | |
| Meta: map[string]interface{}{ | |
| "schema_version": "2", | |
| }, | |
| } | |
| actual, err := r.Refresh(s, nil) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| expected := &terraform.InstanceState{ | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "id": "bar", | |
| "newfoo": "13", | |
| }, | |
| Meta: map[string]interface{}{ | |
| "schema_version": "2", | |
| }, | |
| } | |
| if !reflect.DeepEqual(actual, expected) { | |
| t.Fatalf("bad:\n\nexpected: %#v\ngot: %#v", expected, actual) | |
| } | |
| } | |
| func TestResourceRefresh_stateSchemaVersionUnset(t *testing.T) { | |
| r := &Resource{ | |
| // Version 1 > Version 0 | |
| SchemaVersion: 1, | |
| Schema: map[string]*Schema{ | |
| "newfoo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| r.Read = func(d *ResourceData, m interface{}) error { | |
| return d.Set("newfoo", d.Get("newfoo").(int)+1) | |
| } | |
| r.MigrateState = func( | |
| v int, | |
| s *terraform.InstanceState, | |
| meta interface{}) (*terraform.InstanceState, error) { | |
| s.Attributes["newfoo"] = s.Attributes["oldfoo"] | |
| return s, nil | |
| } | |
| s := &terraform.InstanceState{ | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "oldfoo": "12", | |
| }, | |
| } | |
| actual, err := r.Refresh(s, nil) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| expected := &terraform.InstanceState{ | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "id": "bar", | |
| "newfoo": "13", | |
| }, | |
| Meta: map[string]interface{}{ | |
| "schema_version": "1", | |
| }, | |
| } | |
| if !reflect.DeepEqual(actual, expected) { | |
| t.Fatalf("bad:\n\nexpected: %#v\ngot: %#v", expected, actual) | |
| } | |
| } | |
| func TestResourceRefresh_migrateStateErr(t *testing.T) { | |
| r := &Resource{ | |
| SchemaVersion: 2, | |
| Schema: map[string]*Schema{ | |
| "newfoo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| r.Read = func(d *ResourceData, m interface{}) error { | |
| t.Fatal("Read should never be called!") | |
| return nil | |
| } | |
| r.MigrateState = func( | |
| v int, | |
| s *terraform.InstanceState, | |
| meta interface{}) (*terraform.InstanceState, error) { | |
| return s, fmt.Errorf("triggering an error") | |
| } | |
| s := &terraform.InstanceState{ | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "oldfoo": "12", | |
| }, | |
| } | |
| _, err := r.Refresh(s, nil) | |
| if err == nil { | |
| t.Fatal("expected error, but got none!") | |
| } | |
| } | |
| func TestResourceData(t *testing.T) { | |
| r := &Resource{ | |
| SchemaVersion: 2, | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| state := &terraform.InstanceState{ | |
| ID: "foo", | |
| Attributes: map[string]string{ | |
| "id": "foo", | |
| "foo": "42", | |
| }, | |
| } | |
| data := r.Data(state) | |
| if data.Id() != "foo" { | |
| t.Fatalf("err: %s", data.Id()) | |
| } | |
| if v := data.Get("foo"); v != 42 { | |
| t.Fatalf("bad: %#v", v) | |
| } | |
| // Set expectations | |
| state.Meta = map[string]interface{}{ | |
| "schema_version": "2", | |
| } | |
| result := data.State() | |
| if !reflect.DeepEqual(result, state) { | |
| t.Fatalf("bad: %#v", result) | |
| } | |
| } | |
| func TestResourceData_blank(t *testing.T) { | |
| r := &Resource{ | |
| SchemaVersion: 2, | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| data := r.Data(nil) | |
| if data.Id() != "" { | |
| t.Fatalf("err: %s", data.Id()) | |
| } | |
| if v := data.Get("foo"); v != 0 { | |
| t.Fatalf("bad: %#v", v) | |
| } | |
| } | |
| func TestResourceData_timeouts(t *testing.T) { | |
| one := 1 * time.Second | |
| two := 2 * time.Second | |
| three := 3 * time.Second | |
| four := 4 * time.Second | |
| five := 5 * time.Second | |
| timeouts := &ResourceTimeout{ | |
| Create: &one, | |
| Read: &two, | |
| Update: &three, | |
| Delete: &four, | |
| Default: &five, | |
| } | |
| r := &Resource{ | |
| SchemaVersion: 2, | |
| Schema: map[string]*Schema{ | |
| "foo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| Timeouts: timeouts, | |
| } | |
| data := r.Data(nil) | |
| if data.Id() != "" { | |
| t.Fatalf("err: %s", data.Id()) | |
| } | |
| if !reflect.DeepEqual(timeouts, data.timeouts) { | |
| t.Fatalf("incorrect ResourceData timeouts: %#v\n", *data.timeouts) | |
| } | |
| } | |
| func TestResource_UpgradeState(t *testing.T) { | |
| // While this really only calls itself and therefore doesn't test any of | |
| // the Resource code directly, it still serves as an example of registering | |
| // a StateUpgrader. | |
| r := &Resource{ | |
| SchemaVersion: 2, | |
| Schema: map[string]*Schema{ | |
| "newfoo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| r.StateUpgraders = []StateUpgrader{ | |
| { | |
| Version: 1, | |
| Type: cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| "oldfoo": cty.Number, | |
| }), | |
| Upgrade: func(m map[string]interface{}, meta interface{}) (map[string]interface{}, error) { | |
| oldfoo, ok := m["oldfoo"].(float64) | |
| if !ok { | |
| t.Fatalf("expected 1.2, got %#v", m["oldfoo"]) | |
| } | |
| m["newfoo"] = int(oldfoo * 10) | |
| delete(m, "oldfoo") | |
| return m, nil | |
| }, | |
| }, | |
| } | |
| oldStateAttrs := map[string]string{ | |
| "id": "bar", | |
| "oldfoo": "1.2", | |
| } | |
| // convert the legacy flatmap state to the json equivalent | |
| ty := r.StateUpgraders[0].Type | |
| val, err := hcl2shim.HCL2ValueFromFlatmap(oldStateAttrs, ty) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| js, err := ctyjson.Marshal(val, ty) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // unmarshal the state using the json default types | |
| var m map[string]interface{} | |
| if err := json.Unmarshal(js, &m); err != nil { | |
| t.Fatal(err) | |
| } | |
| actual, err := r.StateUpgraders[0].Upgrade(m, nil) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| expected := map[string]interface{}{ | |
| "id": "bar", | |
| "newfoo": 12, | |
| } | |
| if !reflect.DeepEqual(expected, actual) { | |
| t.Fatalf("expected: %#v\ngot: %#v\n", expected, actual) | |
| } | |
| } | |
| func TestResource_ValidateUpgradeState(t *testing.T) { | |
| r := &Resource{ | |
| SchemaVersion: 3, | |
| Schema: map[string]*Schema{ | |
| "newfoo": &Schema{ | |
| Type: TypeInt, | |
| Optional: true, | |
| }, | |
| }, | |
| } | |
| if err := r.InternalValidate(nil, true); err != nil { | |
| t.Fatal(err) | |
| } | |
| r.StateUpgraders = append(r.StateUpgraders, StateUpgrader{ | |
| Version: 2, | |
| Type: cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| }), | |
| Upgrade: func(m map[string]interface{}, _ interface{}) (map[string]interface{}, error) { | |
| return m, nil | |
| }, | |
| }) | |
| if err := r.InternalValidate(nil, true); err != nil { | |
| t.Fatal(err) | |
| } | |
| // check for missing type | |
| r.StateUpgraders[0].Type = cty.Type{} | |
| if err := r.InternalValidate(nil, true); err == nil { | |
| t.Fatal("StateUpgrader must have type") | |
| } | |
| r.StateUpgraders[0].Type = cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| }) | |
| // check for missing Upgrade func | |
| r.StateUpgraders[0].Upgrade = nil | |
| if err := r.InternalValidate(nil, true); err == nil { | |
| t.Fatal("StateUpgrader must have an Upgrade func") | |
| } | |
| r.StateUpgraders[0].Upgrade = func(m map[string]interface{}, _ interface{}) (map[string]interface{}, error) { | |
| return m, nil | |
| } | |
| // check for skipped version | |
| r.StateUpgraders[0].Version = 0 | |
| r.StateUpgraders = append(r.StateUpgraders, StateUpgrader{ | |
| Version: 2, | |
| Type: cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| }), | |
| Upgrade: func(m map[string]interface{}, _ interface{}) (map[string]interface{}, error) { | |
| return m, nil | |
| }, | |
| }) | |
| if err := r.InternalValidate(nil, true); err == nil { | |
| t.Fatal("StateUpgraders cannot skip versions") | |
| } | |
| // add the missing version, but fail because it's still out of order | |
| r.StateUpgraders = append(r.StateUpgraders, StateUpgrader{ | |
| Version: 1, | |
| Type: cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| }), | |
| Upgrade: func(m map[string]interface{}, _ interface{}) (map[string]interface{}, error) { | |
| return m, nil | |
| }, | |
| }) | |
| if err := r.InternalValidate(nil, true); err == nil { | |
| t.Fatal("upgraders must be defined in order") | |
| } | |
| r.StateUpgraders[1], r.StateUpgraders[2] = r.StateUpgraders[2], r.StateUpgraders[1] | |
| if err := r.InternalValidate(nil, true); err != nil { | |
| t.Fatal(err) | |
| } | |
| // can't add an upgrader for a schema >= the current version | |
| r.StateUpgraders = append(r.StateUpgraders, StateUpgrader{ | |
| Version: 3, | |
| Type: cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| }), | |
| Upgrade: func(m map[string]interface{}, _ interface{}) (map[string]interface{}, error) { | |
| return m, nil | |
| }, | |
| }) | |
| if err := r.InternalValidate(nil, true); err == nil { | |
| t.Fatal("StateUpgraders cannot have a version >= current SchemaVersion") | |
| } | |
| } | |
| // The legacy provider will need to be able to handle both types of schema | |
| // transformations, which has been retrofitted into the Refresh method. | |
| func TestResource_migrateAndUpgrade(t *testing.T) { | |
| r := &Resource{ | |
| SchemaVersion: 4, | |
| Schema: map[string]*Schema{ | |
| "four": { | |
| Type: TypeInt, | |
| Required: true, | |
| }, | |
| }, | |
| // this MigrateState will take the state to version 2 | |
| MigrateState: func(v int, is *terraform.InstanceState, _ interface{}) (*terraform.InstanceState, error) { | |
| switch v { | |
| case 0: | |
| _, ok := is.Attributes["zero"] | |
| if !ok { | |
| return nil, fmt.Errorf("zero not found in %#v", is.Attributes) | |
| } | |
| is.Attributes["one"] = "1" | |
| delete(is.Attributes, "zero") | |
| fallthrough | |
| case 1: | |
| _, ok := is.Attributes["one"] | |
| if !ok { | |
| return nil, fmt.Errorf("one not found in %#v", is.Attributes) | |
| } | |
| is.Attributes["two"] = "2" | |
| delete(is.Attributes, "one") | |
| default: | |
| return nil, fmt.Errorf("invalid schema version %d", v) | |
| } | |
| return is, nil | |
| }, | |
| } | |
| r.Read = func(d *ResourceData, m interface{}) error { | |
| return d.Set("four", 4) | |
| } | |
| r.StateUpgraders = []StateUpgrader{ | |
| { | |
| Version: 2, | |
| Type: cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| "two": cty.Number, | |
| }), | |
| Upgrade: func(m map[string]interface{}, meta interface{}) (map[string]interface{}, error) { | |
| _, ok := m["two"].(float64) | |
| if !ok { | |
| return nil, fmt.Errorf("two not found in %#v", m) | |
| } | |
| m["three"] = float64(3) | |
| delete(m, "two") | |
| return m, nil | |
| }, | |
| }, | |
| { | |
| Version: 3, | |
| Type: cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| "three": cty.Number, | |
| }), | |
| Upgrade: func(m map[string]interface{}, meta interface{}) (map[string]interface{}, error) { | |
| _, ok := m["three"].(float64) | |
| if !ok { | |
| return nil, fmt.Errorf("three not found in %#v", m) | |
| } | |
| m["four"] = float64(4) | |
| delete(m, "three") | |
| return m, nil | |
| }, | |
| }, | |
| } | |
| testStates := []*terraform.InstanceState{ | |
| { | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "id": "bar", | |
| "zero": "0", | |
| }, | |
| Meta: map[string]interface{}{ | |
| "schema_version": "0", | |
| }, | |
| }, | |
| { | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "id": "bar", | |
| "one": "1", | |
| }, | |
| Meta: map[string]interface{}{ | |
| "schema_version": "1", | |
| }, | |
| }, | |
| { | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "id": "bar", | |
| "two": "2", | |
| }, | |
| Meta: map[string]interface{}{ | |
| "schema_version": "2", | |
| }, | |
| }, | |
| { | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "id": "bar", | |
| "three": "3", | |
| }, | |
| Meta: map[string]interface{}{ | |
| "schema_version": "3", | |
| }, | |
| }, | |
| { | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "id": "bar", | |
| "four": "4", | |
| }, | |
| Meta: map[string]interface{}{ | |
| "schema_version": "4", | |
| }, | |
| }, | |
| } | |
| for i, s := range testStates { | |
| t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { | |
| newState, err := r.Refresh(s, nil) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| expected := &terraform.InstanceState{ | |
| ID: "bar", | |
| Attributes: map[string]string{ | |
| "id": "bar", | |
| "four": "4", | |
| }, | |
| Meta: map[string]interface{}{ | |
| "schema_version": "4", | |
| }, | |
| } | |
| if !cmp.Equal(expected, newState, equateEmpty) { | |
| t.Fatal(cmp.Diff(expected, newState, equateEmpty)) | |
| } | |
| }) | |
| } | |
| } |