package format import ( "fmt" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/helper/experiment" "github.com/hashicorp/terraform/plans" "github.com/mitchellh/colorstring" "github.com/zclconf/go-cty/cty" ) func TestResourceChange_primitiveTypes(t *testing.T) { testCases := map[string]testCase{ "creation": { Action: plans.Create, Mode: addrs.ManagedResourceMode, Before: cty.NullVal(cty.EmptyObject), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + id = (known after apply) } `, }, "creation (null string)": { Action: plans.Create, Mode: addrs.ManagedResourceMode, Before: cty.NullVal(cty.EmptyObject), After: cty.ObjectVal(map[string]cty.Value{ "string": cty.StringVal("null"), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "string": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + string = "null" } `, }, "creation (null string with extra whitespace)": { Action: plans.Create, Mode: addrs.ManagedResourceMode, Before: cty.NullVal(cty.EmptyObject), After: cty.ObjectVal(map[string]cty.Value{ "string": cty.StringVal("null "), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "string": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + string = "null " } `, }, "deletion": { Action: plans.Delete, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), }), After: cty.NullVal(cty.EmptyObject), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be destroyed - resource "test_instance" "example" { - id = "i-02ae66f368e8518a9" -> null } `, }, "deletion (empty string)": { Action: plans.Delete, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "intentionally_long": cty.StringVal(""), }), After: cty.NullVal(cty.EmptyObject), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, "intentionally_long": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be destroyed - resource "test_instance" "example" { - id = "i-02ae66f368e8518a9" -> null } `, }, "string in-place update": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" } `, }, "string force-new update": { Action: plans.DeleteThenCreate, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "ami"}, }), Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement id = "i-02ae66f368e8518a9" } `, }, "string in-place update (null values)": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "unchanged": cty.NullVal(cty.String), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "unchanged": cty.NullVal(cty.String), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "unchanged": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" } `, }, "in-place update of multi-line string field": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "more_lines": cty.StringVal(`original long multi-line string field `), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "more_lines": cty.StringVal(`original extremely long multi-line string field `), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "more_lines": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ more_lines = <<~EOT original - long + extremely long multi-line string field EOT } `, }, "addition of multi-line string field": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "more_lines": cty.NullVal(cty.String), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "more_lines": cty.StringVal(`original new line `), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "more_lines": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) + more_lines = <<~EOT original new line EOT } `, }, "force-new update of multi-line string field": { Action: plans.DeleteThenCreate, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "more_lines": cty.StringVal(`original `), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "more_lines": cty.StringVal(`original new line `), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "more_lines": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "more_lines"}, }), Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ more_lines = <<~EOT # forces replacement original + new line EOT } `, }, // Sensitive "creation with sensitive field": { Action: plans.Create, Mode: addrs.ManagedResourceMode, Before: cty.NullVal(cty.EmptyObject), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "password": cty.StringVal("top-secret"), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, "password": {Type: cty.String, Optional: true, Sensitive: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + id = (known after apply) + password = (sensitive value) } `, }, "update with equal sensitive field": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("blah"), "str": cty.StringVal("before"), "password": cty.StringVal("top-secret"), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "str": cty.StringVal("after"), "password": cty.StringVal("top-secret"), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, "str": {Type: cty.String, Optional: true}, "password": {Type: cty.String, Optional: true, Sensitive: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "blah" -> (known after apply) ~ str = "before" -> "after" # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "blah" -> (known after apply) password = (sensitive value) ~ str = "before" -> "after" } `, }, // tainted resources "replace tainted resource": { Action: plans.DeleteThenCreate, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-AFTER"), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "ami"}, }), Tainted: true, ExpectedOutput: ` # test_instance.example is tainted, so must be replaced -/+ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement ~ id = "i-02ae66f368e8518a9" -> (known after apply) } `, }, "force replacement with empty before value": { Action: plans.DeleteThenCreate, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "name": cty.StringVal("name"), "forced": cty.NullVal(cty.String), }), After: cty.ObjectVal(map[string]cty.Value{ "name": cty.StringVal("name"), "forced": cty.StringVal("example"), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "name": {Type: cty.String, Optional: true}, "forced": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "forced"}, }), Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { + forced = "example" # forces replacement name = "name" } `, }, "force replacement with empty before value legacy": { Action: plans.DeleteThenCreate, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "name": cty.StringVal("name"), "forced": cty.StringVal(""), }), After: cty.ObjectVal(map[string]cty.Value{ "name": cty.StringVal("name"), "forced": cty.StringVal("example"), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "name": {Type: cty.String, Optional: true}, "forced": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "forced"}, }), Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { + forced = "example" # forces replacement name = "name" } `, }, "show all identifying attributes even if unchanged": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "bar": cty.StringVal("bar"), "foo": cty.StringVal("foo"), "name": cty.StringVal("alice"), "tags": cty.MapVal(map[string]cty.Value{ "name": cty.StringVal("bob"), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "bar": cty.StringVal("bar"), "foo": cty.StringVal("foo"), "name": cty.StringVal("alice"), "tags": cty.MapVal(map[string]cty.Value{ "name": cty.StringVal("bob"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "bar": {Type: cty.String, Optional: true}, "foo": {Type: cty.String, Optional: true}, "name": {Type: cty.String, Optional: true}, "tags": {Type: cty.Map(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" name = "alice" tags = { "name" = "bob" } # (2 unchanged attributes hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" bar = "bar" foo = "foo" id = "i-02ae66f368e8518a9" name = "alice" tags = { "name" = "bob" } } `, }, } runTestCases(t, testCases) } func TestResourceChange_JSON(t *testing.T) { testCases := map[string]testCase{ "creation": { Action: plans.Create, Mode: addrs.ManagedResourceMode, Before: cty.NullVal(cty.EmptyObject), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "json_field": cty.StringVal(`{ "str": "value", "list":["a","b", 234, true], "obj": {"key": "val"} }`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "json_field": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + id = (known after apply) + json_field = jsonencode( { + list = [ + "a", + "b", + 234, + true, ] + obj = { + key = "val" } + str = "value" } ) } `, }, "in-place update of object": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "json_field": cty.StringVal(`{"aaa": "value","ccc": 5}`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "json_field": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ { + bbb = "new_value" - ccc = 5 -> null # (1 unchanged element hidden) } ) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ { aaa = "value" + bbb = "new_value" - ccc = 5 -> null } ) } `, }, "in-place update (from empty tuple)": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "json_field": cty.StringVal(`{"aaa": []}`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "json_field": cty.StringVal(`{"aaa": ["value"]}`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "json_field": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ { ~ aaa = [ + "value", ] } ) } `, }, "in-place update (to empty tuple)": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "json_field": cty.StringVal(`{"aaa": ["value"]}`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "json_field": cty.StringVal(`{"aaa": []}`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "json_field": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ { ~ aaa = [ - "value", ] } ) } `, }, "in-place update (tuple of different types)": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "json_field": cty.StringVal(`{"aaa": [42, {"foo":"baz"}, "value"]}`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "json_field": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ { ~ aaa = [ 42, ~ { ~ foo = "bar" -> "baz" }, "value", ] } ) } `, }, "force-new update": { Action: plans.DeleteThenCreate, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "json_field": cty.StringVal(`{"aaa": "value"}`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "json_field": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "json_field"}, }), Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ { + bbb = "new_value" # (1 unchanged element hidden) } # forces replacement ) } `, VerboseOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ { aaa = "value" + bbb = "new_value" } # forces replacement ) } `, }, "in-place update (whitespace change)": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "json_field": cty.StringVal(`{"aaa":"value", "bbb":"another"}`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "json_field": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( # whitespace changes { aaa = "value" bbb = "another" } ) } `, }, "force-new update (whitespace change)": { Action: plans.DeleteThenCreate, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "json_field": cty.StringVal(`{"aaa":"value", "bbb":"another"}`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "json_field": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "json_field"}, }), Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( # whitespace changes force replacement { aaa = "value" bbb = "another" } ) } `, }, "creation (empty)": { Action: plans.Create, Mode: addrs.ManagedResourceMode, Before: cty.NullVal(cty.EmptyObject), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "json_field": cty.StringVal(`{}`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "json_field": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + id = (known after apply) + json_field = jsonencode({}) } `, }, "JSON list item removal": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "json_field": cty.StringVal(`["first","second","third"]`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "json_field": cty.StringVal(`["first","second"]`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "json_field": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ [ # (1 unchanged element hidden) "second", - "third", ] ) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ [ "first", "second", - "third", ] ) } `, }, "JSON list item addition": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "json_field": cty.StringVal(`["first","second"]`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "json_field": cty.StringVal(`["first","second","third"]`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "json_field": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ [ # (1 unchanged element hidden) "second", + "third", ] ) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ [ "first", "second", + "third", ] ) } `, }, "JSON list object addition": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "json_field": cty.StringVal(`{"first":"111"}`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "json_field": cty.StringVal(`{"first":"111","second":"222"}`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "json_field": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ { + second = "222" # (1 unchanged element hidden) } ) } `, }, "JSON object with nested list": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "json_field": cty.StringVal(`{ "Statement": ["first"] }`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "json_field": cty.StringVal(`{ "Statement": ["first", "second"] }`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "json_field": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ { ~ Statement = [ "first", + "second", ] } ) } `, }, "JSON list of objects - adding item": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "json_field": cty.StringVal(`[{"one": "111"}]`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}]`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "json_field": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ [ { one = "111" }, + { + two = "222" }, ] ) } `, }, "JSON list of objects - removing item": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}, {"three": "333"}]`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "json_field": cty.StringVal(`[{"one": "111"}, {"three": "333"}]`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "json_field": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ [ { one = "111" }, - { - two = "222" }, { three = "333" }, ] ) } `, }, "JSON object with list of objects": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "json_field": cty.StringVal(`{"parent":[{"one": "111"}]}`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "json_field": cty.StringVal(`{"parent":[{"one": "111"}, {"two": "222"}]}`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "json_field": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ { ~ parent = [ { one = "111" }, + { + two = "222" }, ] } ) } `, }, "JSON object double nested lists": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "json_field": cty.StringVal(`{"parent":[{"another_list": ["111"]}]}`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "json_field": cty.StringVal(`{"parent":[{"another_list": ["111", "222"]}]}`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "json_field": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ { ~ parent = [ ~ { ~ another_list = [ "111", + "222", ] }, ] } ) } `, }, "in-place update from object to tuple": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "json_field": cty.StringVal(`["aaa", 42, "something"]`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "json_field": {Type: cty.String, Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ json_field = jsonencode( ~ { - aaa = [ - 42, - { - foo = "bar" }, - "value", ] } -> [ + "aaa", + 42, + "something", ] ) } `, }, } runTestCases(t, testCases) } func TestResourceChange_primitiveList(t *testing.T) { testCases := map[string]testCase{ "in-place update - creation": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.NullVal(cty.List(cty.String)), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListVal([]cty.Value{ cty.StringVal("new-element"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "list_field": {Type: cty.List(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) + list_field = [ + "new-element", ] # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) + list_field = [ + "new-element", ] } `, }, "in-place update - first addition": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListValEmpty(cty.String), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListVal([]cty.Value{ cty.StringVal("new-element"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "list_field": {Type: cty.List(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ list_field = [ + "new-element", ] # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ list_field = [ + "new-element", ] } `, }, "in-place update - insertion": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListVal([]cty.Value{ cty.StringVal("aaaa"), cty.StringVal("bbbb"), cty.StringVal("dddd"), cty.StringVal("eeee"), cty.StringVal("ffff"), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListVal([]cty.Value{ cty.StringVal("aaaa"), cty.StringVal("bbbb"), cty.StringVal("cccc"), cty.StringVal("dddd"), cty.StringVal("eeee"), cty.StringVal("ffff"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "list_field": {Type: cty.List(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ list_field = [ # (1 unchanged element hidden) "bbbb", + "cccc", "dddd", # (2 unchanged elements hidden) ] # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ list_field = [ "aaaa", "bbbb", + "cccc", "dddd", "eeee", "ffff", ] } `, }, "force-new update - insertion": { Action: plans.DeleteThenCreate, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListVal([]cty.Value{ cty.StringVal("aaaa"), cty.StringVal("cccc"), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListVal([]cty.Value{ cty.StringVal("aaaa"), cty.StringVal("bbbb"), cty.StringVal("cccc"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "list_field": {Type: cty.List(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "list_field"}, }), Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ list_field = [ # forces replacement "aaaa", + "bbbb", "cccc", ] # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ list_field = [ # forces replacement "aaaa", + "bbbb", "cccc", ] } `, }, "in-place update - deletion": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListVal([]cty.Value{ cty.StringVal("aaaa"), cty.StringVal("bbbb"), cty.StringVal("cccc"), cty.StringVal("dddd"), cty.StringVal("eeee"), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListVal([]cty.Value{ cty.StringVal("bbbb"), cty.StringVal("dddd"), cty.StringVal("eeee"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "list_field": {Type: cty.List(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ list_field = [ - "aaaa", "bbbb", - "cccc", "dddd", # (1 unchanged element hidden) ] # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ list_field = [ - "aaaa", "bbbb", - "cccc", "dddd", "eeee", ] } `, }, "creation - empty list": { Action: plans.Create, Mode: addrs.ManagedResourceMode, Before: cty.NullVal(cty.EmptyObject), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListValEmpty(cty.String), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "list_field": {Type: cty.List(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + ami = "ami-STATIC" + id = (known after apply) + list_field = [] } `, }, "in-place update - full to empty": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListVal([]cty.Value{ cty.StringVal("aaaa"), cty.StringVal("bbbb"), cty.StringVal("cccc"), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListValEmpty(cty.String), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "list_field": {Type: cty.List(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ list_field = [ - "aaaa", - "bbbb", - "cccc", ] # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ list_field = [ - "aaaa", - "bbbb", - "cccc", ] } `, }, "in-place update - null to empty": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.NullVal(cty.List(cty.String)), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListValEmpty(cty.String), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "list_field": {Type: cty.List(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) + list_field = [] # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) + list_field = [] } `, }, "update to unknown element": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListVal([]cty.Value{ cty.StringVal("aaaa"), cty.StringVal("bbbb"), cty.StringVal("cccc"), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListVal([]cty.Value{ cty.StringVal("aaaa"), cty.UnknownVal(cty.String), cty.StringVal("cccc"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "list_field": {Type: cty.List(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ list_field = [ "aaaa", - "bbbb", + (known after apply), "cccc", ] # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ list_field = [ "aaaa", - "bbbb", + (known after apply), "cccc", ] } `, }, "update - two new unknown elements": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListVal([]cty.Value{ cty.StringVal("aaaa"), cty.StringVal("bbbb"), cty.StringVal("cccc"), cty.StringVal("dddd"), cty.StringVal("eeee"), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "list_field": cty.ListVal([]cty.Value{ cty.StringVal("aaaa"), cty.UnknownVal(cty.String), cty.UnknownVal(cty.String), cty.StringVal("cccc"), cty.StringVal("dddd"), cty.StringVal("eeee"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "list_field": {Type: cty.List(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ list_field = [ "aaaa", - "bbbb", + (known after apply), + (known after apply), "cccc", # (2 unchanged elements hidden) ] # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ list_field = [ "aaaa", - "bbbb", + (known after apply), + (known after apply), "cccc", "dddd", "eeee", ] } `, }, } runTestCases(t, testCases) } func TestResourceChange_primitiveTuple(t *testing.T) { testCases := map[string]testCase{ "in-place update": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "tuple_field": cty.TupleVal([]cty.Value{ cty.StringVal("aaaa"), cty.StringVal("bbbb"), cty.StringVal("dddd"), cty.StringVal("eeee"), cty.StringVal("ffff"), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "tuple_field": cty.TupleVal([]cty.Value{ cty.StringVal("aaaa"), cty.StringVal("bbbb"), cty.StringVal("cccc"), cty.StringVal("eeee"), cty.StringVal("ffff"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Required: true}, "tuple_field": {Type: cty.Tuple([]cty.Type{cty.String, cty.String, cty.String, cty.String, cty.String}), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { id = "i-02ae66f368e8518a9" ~ tuple_field = [ # (1 unchanged element hidden) "bbbb", - "dddd", + "cccc", "eeee", # (1 unchanged element hidden) ] } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { id = "i-02ae66f368e8518a9" ~ tuple_field = [ "aaaa", "bbbb", - "dddd", + "cccc", "eeee", "ffff", ] } `, }, } runTestCases(t, testCases) } func TestResourceChange_primitiveSet(t *testing.T) { testCases := map[string]testCase{ "in-place update - creation": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.NullVal(cty.Set(cty.String)), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.SetVal([]cty.Value{ cty.StringVal("new-element"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "set_field": {Type: cty.Set(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) + set_field = [ + "new-element", ] # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) + set_field = [ + "new-element", ] } `, }, "in-place update - first insertion": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.SetValEmpty(cty.String), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.SetVal([]cty.Value{ cty.StringVal("new-element"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "set_field": {Type: cty.Set(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ set_field = [ + "new-element", ] # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ set_field = [ + "new-element", ] } `, }, "in-place update - insertion": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.SetVal([]cty.Value{ cty.StringVal("aaaa"), cty.StringVal("cccc"), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.SetVal([]cty.Value{ cty.StringVal("aaaa"), cty.StringVal("bbbb"), cty.StringVal("cccc"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "set_field": {Type: cty.Set(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ set_field = [ + "bbbb", # (2 unchanged elements hidden) ] # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ set_field = [ "aaaa", + "bbbb", "cccc", ] } `, }, "force-new update - insertion": { Action: plans.DeleteThenCreate, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.SetVal([]cty.Value{ cty.StringVal("aaaa"), cty.StringVal("cccc"), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.SetVal([]cty.Value{ cty.StringVal("aaaa"), cty.StringVal("bbbb"), cty.StringVal("cccc"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "set_field": {Type: cty.Set(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "set_field"}, }), Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ set_field = [ # forces replacement + "bbbb", # (2 unchanged elements hidden) ] # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ set_field = [ # forces replacement "aaaa", + "bbbb", "cccc", ] } `, }, "in-place update - deletion": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.SetVal([]cty.Value{ cty.StringVal("aaaa"), cty.StringVal("bbbb"), cty.StringVal("cccc"), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.SetVal([]cty.Value{ cty.StringVal("bbbb"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "set_field": {Type: cty.Set(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ set_field = [ - "aaaa", - "cccc", # (1 unchanged element hidden) ] # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ set_field = [ - "aaaa", "bbbb", - "cccc", ] } `, }, "creation - empty set": { Action: plans.Create, Mode: addrs.ManagedResourceMode, Before: cty.NullVal(cty.EmptyObject), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.SetValEmpty(cty.String), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "set_field": {Type: cty.Set(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + ami = "ami-STATIC" + id = (known after apply) + set_field = [] } `, }, "in-place update - full to empty set": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.SetVal([]cty.Value{ cty.StringVal("aaaa"), cty.StringVal("bbbb"), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.SetValEmpty(cty.String), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "set_field": {Type: cty.Set(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ set_field = [ - "aaaa", - "bbbb", ] # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ set_field = [ - "aaaa", - "bbbb", ] } `, }, "in-place update - null to empty set": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.NullVal(cty.Set(cty.String)), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.SetValEmpty(cty.String), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "set_field": {Type: cty.Set(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) + set_field = [] # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) + set_field = [] } `, }, "in-place update to unknown": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.SetVal([]cty.Value{ cty.StringVal("aaaa"), cty.StringVal("bbbb"), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.UnknownVal(cty.Set(cty.String)), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "set_field": {Type: cty.Set(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ set_field = [ - "aaaa", - "bbbb", ] -> (known after apply) # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ set_field = [ - "aaaa", - "bbbb", ] -> (known after apply) } `, }, "in-place update to unknown element": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.SetVal([]cty.Value{ cty.StringVal("aaaa"), cty.StringVal("bbbb"), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "set_field": cty.SetVal([]cty.Value{ cty.StringVal("aaaa"), cty.UnknownVal(cty.String), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "set_field": {Type: cty.Set(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ set_field = [ - "bbbb", ~ (known after apply), # (1 unchanged element hidden) ] # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ set_field = [ "aaaa", - "bbbb", ~ (known after apply), ] } `, }, } runTestCases(t, testCases) } func TestResourceChange_map(t *testing.T) { testCases := map[string]testCase{ "in-place update - creation": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "map_field": cty.NullVal(cty.Map(cty.String)), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "map_field": cty.MapVal(map[string]cty.Value{ "new-key": cty.StringVal("new-element"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "map_field": {Type: cty.Map(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) + map_field = { + "new-key" = "new-element" } # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) + map_field = { + "new-key" = "new-element" } } `, }, "in-place update - first insertion": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "map_field": cty.MapValEmpty(cty.String), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "map_field": cty.MapVal(map[string]cty.Value{ "new-key": cty.StringVal("new-element"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "map_field": {Type: cty.Map(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ map_field = { + "new-key" = "new-element" } # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ map_field = { + "new-key" = "new-element" } } `, }, "in-place update - insertion": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "map_field": cty.MapVal(map[string]cty.Value{ "a": cty.StringVal("aaaa"), "c": cty.StringVal("cccc"), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "map_field": cty.MapVal(map[string]cty.Value{ "a": cty.StringVal("aaaa"), "b": cty.StringVal("bbbb"), "c": cty.StringVal("cccc"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "map_field": {Type: cty.Map(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ map_field = { + "b" = "bbbb" # (2 unchanged elements hidden) } # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ map_field = { "a" = "aaaa" + "b" = "bbbb" "c" = "cccc" } } `, }, "force-new update - insertion": { Action: plans.DeleteThenCreate, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "map_field": cty.MapVal(map[string]cty.Value{ "a": cty.StringVal("aaaa"), "c": cty.StringVal("cccc"), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "map_field": cty.MapVal(map[string]cty.Value{ "a": cty.StringVal("aaaa"), "b": cty.StringVal("bbbb"), "c": cty.StringVal("cccc"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "map_field": {Type: cty.Map(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "map_field"}, }), Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ map_field = { # forces replacement + "b" = "bbbb" # (2 unchanged elements hidden) } # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ map_field = { # forces replacement "a" = "aaaa" + "b" = "bbbb" "c" = "cccc" } } `, }, "in-place update - deletion": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "map_field": cty.MapVal(map[string]cty.Value{ "a": cty.StringVal("aaaa"), "b": cty.StringVal("bbbb"), "c": cty.StringVal("cccc"), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "map_field": cty.MapVal(map[string]cty.Value{ "b": cty.StringVal("bbbb"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "map_field": {Type: cty.Map(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ map_field = { - "a" = "aaaa" -> null - "c" = "cccc" -> null # (1 unchanged element hidden) } # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ map_field = { - "a" = "aaaa" -> null "b" = "bbbb" - "c" = "cccc" -> null } } `, }, "creation - empty": { Action: plans.Create, Mode: addrs.ManagedResourceMode, Before: cty.NullVal(cty.EmptyObject), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "map_field": cty.MapValEmpty(cty.String), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "map_field": {Type: cty.Map(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + ami = "ami-STATIC" + id = (known after apply) + map_field = {} } `, }, "update to unknown element": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-STATIC"), "map_field": cty.MapVal(map[string]cty.Value{ "a": cty.StringVal("aaaa"), "b": cty.StringVal("bbbb"), "c": cty.StringVal("cccc"), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "ami": cty.StringVal("ami-STATIC"), "map_field": cty.MapVal(map[string]cty.Value{ "a": cty.StringVal("aaaa"), "b": cty.UnknownVal(cty.String), "c": cty.StringVal("cccc"), }), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "map_field": {Type: cty.Map(cty.String), Optional: true}, }, }, RequiredReplace: cty.NewPathSet(), Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ map_field = { ~ "b" = "bbbb" -> (known after apply) # (2 unchanged elements hidden) } # (1 unchanged attribute hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ami = "ami-STATIC" ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ map_field = { "a" = "aaaa" ~ "b" = "bbbb" -> (known after apply) "c" = "cccc" } } `, }, } runTestCases(t, testCases) } func TestResourceChange_nestedList(t *testing.T) { testCases := map[string]testCase{ "in-place update - equal": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "root_block_device": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), }), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "root_block_device": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), }), }), }), RequiredReplace: cty.NewPathSet(), Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "root_block_device": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "volume_type": { Type: cty.String, Optional: true, Computed: true, }, }, }, Nesting: configschema.NestingList, }, }, }, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" # (1 unchanged block hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" root_block_device { volume_type = "gp2" } } `, }, "in-place update - creation": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{ "volume_type": cty.String, })), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "root_block_device": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.NullVal(cty.String), }), }), }), RequiredReplace: cty.NewPathSet(), Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "root_block_device": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "volume_type": { Type: cty.String, Optional: true, Computed: true, }, }, }, Nesting: configschema.NestingList, }, }, }, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" + root_block_device {} } `, }, "in-place update - first insertion": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{ "volume_type": cty.String, })), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "root_block_device": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), }), }), }), RequiredReplace: cty.NewPathSet(), Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "root_block_device": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "volume_type": { Type: cty.String, Optional: true, Computed: true, }, }, }, Nesting: configschema.NestingList, }, }, }, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" + root_block_device { + volume_type = "gp2" } } `, }, "in-place update - insertion": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "root_block_device": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), "new_field": cty.NullVal(cty.String), }), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "root_block_device": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), "new_field": cty.StringVal("new_value"), }), }), }), RequiredReplace: cty.NewPathSet(), Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "root_block_device": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "volume_type": { Type: cty.String, Optional: true, Computed: true, }, "new_field": { Type: cty.String, Optional: true, Computed: true, }, }, }, Nesting: configschema.NestingList, }, }, }, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" ~ root_block_device { + new_field = "new_value" # (1 unchanged attribute hidden) } } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" ~ root_block_device { + new_field = "new_value" volume_type = "gp2" } } `, }, "force-new update (inside block)": { Action: plans.DeleteThenCreate, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "root_block_device": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), }), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "root_block_device": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("different"), }), }), }), RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "root_block_device"}, cty.IndexStep{Key: cty.NumberIntVal(0)}, cty.GetAttrStep{Name: "volume_type"}, }), Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "root_block_device": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "volume_type": { Type: cty.String, Optional: true, Computed: true, }, }, }, Nesting: configschema.NestingList, }, }, }, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" ~ root_block_device { ~ volume_type = "gp2" -> "different" # forces replacement } } `, }, "force-new update (whole block)": { Action: plans.DeleteThenCreate, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "root_block_device": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), }), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "root_block_device": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("different"), }), }), }), RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "root_block_device"}, }), Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "root_block_device": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "volume_type": { Type: cty.String, Optional: true, Computed: true, }, }, }, Nesting: configschema.NestingList, }, }, }, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" ~ root_block_device { # forces replacement ~ volume_type = "gp2" -> "different" } } `, }, "in-place update - deletion": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "root_block_device": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), "new_field": cty.StringVal("new_value"), }), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{ "volume_type": cty.String, "new_field": cty.String, })), }), RequiredReplace: cty.NewPathSet(), Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "root_block_device": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "volume_type": { Type: cty.String, Optional: true, Computed: true, }, "new_field": { Type: cty.String, Optional: true, Computed: true, }, }, }, Nesting: configschema.NestingList, }, }, }, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" - root_block_device { - new_field = "new_value" -> null - volume_type = "gp2" -> null } } `, }, "with dynamically-typed attribute": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "block": cty.EmptyTupleVal, }), After: cty.ObjectVal(map[string]cty.Value{ "block": cty.TupleVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "attr": cty.StringVal("foo"), }), cty.ObjectVal(map[string]cty.Value{ "attr": cty.True, }), }), }), RequiredReplace: cty.NewPathSet(), Tainted: false, Schema: &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{ "block": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "attr": {Type: cty.DynamicPseudoType, Optional: true}, }, }, Nesting: configschema.NestingList, }, }, }, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { + block { + attr = "foo" } + block { + attr = true } } `, }, } runTestCases(t, testCases) } func TestResourceChange_nestedSet(t *testing.T) { testCases := map[string]testCase{ "in-place update - creation": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ "volume_type": cty.String, })), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "root_block_device": cty.SetVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), }), }), }), RequiredReplace: cty.NewPathSet(), Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "root_block_device": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "volume_type": { Type: cty.String, Optional: true, Computed: true, }, }, }, Nesting: configschema.NestingSet, }, }, }, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" + root_block_device { + volume_type = "gp2" } } `, }, "in-place update - insertion": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "root_block_device": cty.SetVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), "new_field": cty.NullVal(cty.String), }), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "root_block_device": cty.SetVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), "new_field": cty.StringVal("new_value"), }), }), }), RequiredReplace: cty.NewPathSet(), Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "root_block_device": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "volume_type": { Type: cty.String, Optional: true, Computed: true, }, "new_field": { Type: cty.String, Optional: true, Computed: true, }, }, }, Nesting: configschema.NestingSet, }, }, }, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" + root_block_device { + new_field = "new_value" + volume_type = "gp2" } - root_block_device { - volume_type = "gp2" -> null } } `, }, "force-new update (whole block)": { Action: plans.DeleteThenCreate, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "root_block_device": cty.SetVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), }), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "root_block_device": cty.SetVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("different"), }), }), }), RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "root_block_device"}, }), Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "root_block_device": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "volume_type": { Type: cty.String, Optional: true, Computed: true, }, }, }, Nesting: configschema.NestingSet, }, }, }, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" + root_block_device { # forces replacement + volume_type = "different" } - root_block_device { # forces replacement - volume_type = "gp2" -> null } } `, }, "in-place update - deletion": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "root_block_device": cty.SetVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), "new_field": cty.StringVal("new_value"), }), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ "volume_type": cty.String, "new_field": cty.String, })), }), RequiredReplace: cty.NewPathSet(), Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "root_block_device": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "volume_type": { Type: cty.String, Optional: true, Computed: true, }, "new_field": { Type: cty.String, Optional: true, Computed: true, }, }, }, Nesting: configschema.NestingSet, }, }, }, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" - root_block_device { - new_field = "new_value" -> null - volume_type = "gp2" -> null } } `, }, } runTestCases(t, testCases) } func TestResourceChange_nestedMap(t *testing.T) { testCases := map[string]testCase{ "in-place update - creation": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{ "volume_type": cty.String, })), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "root_block_device": cty.MapVal(map[string]cty.Value{ "a": cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), }), }), }), RequiredReplace: cty.NewPathSet(), Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "root_block_device": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "volume_type": { Type: cty.String, Optional: true, Computed: true, }, }, }, Nesting: configschema.NestingMap, }, }, }, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" + root_block_device "a" { + volume_type = "gp2" } } `, }, "in-place update - change attr": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "root_block_device": cty.MapVal(map[string]cty.Value{ "a": cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), "new_field": cty.NullVal(cty.String), }), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "root_block_device": cty.MapVal(map[string]cty.Value{ "a": cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), "new_field": cty.StringVal("new_value"), }), }), }), RequiredReplace: cty.NewPathSet(), Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "root_block_device": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "volume_type": { Type: cty.String, Optional: true, Computed: true, }, "new_field": { Type: cty.String, Optional: true, Computed: true, }, }, }, Nesting: configschema.NestingMap, }, }, }, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" ~ root_block_device "a" { + new_field = "new_value" # (1 unchanged attribute hidden) } } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" ~ root_block_device "a" { + new_field = "new_value" volume_type = "gp2" } } `, }, "in-place update - insertion": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "root_block_device": cty.MapVal(map[string]cty.Value{ "a": cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), "new_field": cty.NullVal(cty.String), }), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "root_block_device": cty.MapVal(map[string]cty.Value{ "a": cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), "new_field": cty.NullVal(cty.String), }), "b": cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), "new_field": cty.StringVal("new_value"), }), }), }), RequiredReplace: cty.NewPathSet(), Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "root_block_device": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "volume_type": { Type: cty.String, Optional: true, Computed: true, }, "new_field": { Type: cty.String, Optional: true, Computed: true, }, }, }, Nesting: configschema.NestingMap, }, }, }, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" + root_block_device "b" { + new_field = "new_value" + volume_type = "gp2" } # (1 unchanged block hidden) } `, VerboseOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" root_block_device "a" { volume_type = "gp2" } + root_block_device "b" { + new_field = "new_value" + volume_type = "gp2" } } `, }, "force-new update (whole block)": { Action: plans.DeleteThenCreate, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "root_block_device": cty.MapVal(map[string]cty.Value{ "a": cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), }), "b": cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("standard"), }), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "root_block_device": cty.MapVal(map[string]cty.Value{ "a": cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("different"), }), "b": cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("standard"), }), }), }), RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "root_block_device"}, cty.IndexStep{Key: cty.StringVal("a")}, }), Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "root_block_device": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "volume_type": { Type: cty.String, Optional: true, Computed: true, }, }, }, Nesting: configschema.NestingMap, }, }, }, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" ~ root_block_device "a" { # forces replacement ~ volume_type = "gp2" -> "different" } # (1 unchanged block hidden) } `, VerboseOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" ~ root_block_device "a" { # forces replacement ~ volume_type = "gp2" -> "different" } root_block_device "b" { volume_type = "standard" } } `, }, "in-place update - deletion": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "root_block_device": cty.MapVal(map[string]cty.Value{ "a": cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), "new_field": cty.StringVal("new_value"), }), }), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{ "volume_type": cty.String, "new_field": cty.String, })), }), RequiredReplace: cty.NewPathSet(), Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "root_block_device": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "volume_type": { Type: cty.String, Optional: true, Computed: true, }, "new_field": { Type: cty.String, Optional: true, Computed: true, }, }, }, Nesting: configschema.NestingMap, }, }, }, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" - root_block_device "a" { - new_field = "new_value" -> null - volume_type = "gp2" -> null } } `, }, "in-place sequence update - deletion": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "list": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("x")}), cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("y")}), }), }), After: cty.ObjectVal(map[string]cty.Value{ "list": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("y")}), cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("z")}), }), }), RequiredReplace: cty.NewPathSet(), Tainted: false, Schema: &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{ "list": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "attr": { Type: cty.String, Required: true, }, }, }, Nesting: configschema.NestingList, }, }, }, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ list { ~ attr = "x" -> "y" } ~ list { ~ attr = "y" -> "z" } } `, }, } runTestCases(t, testCases) } func TestResourceChange_sensitiveVariable(t *testing.T) { testCases := map[string]testCase{ "in-place update - creation": { Action: plans.Update, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{ "volume_type": cty.String, })), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "root_block_device": cty.MapVal(map[string]cty.Value{ "a": cty.ObjectVal(map[string]cty.Value{ "volume_type": cty.StringVal("gp2"), }), }), }), AfterValMarks: []cty.PathValueMarks{ { Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, Marks: cty.NewValueMarks("sensitive"), }}, RequiredReplace: cty.NewPathSet(), Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, }, BlockTypes: map[string]*configschema.NestedBlock{ "root_block_device": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "volume_type": { Type: cty.String, Optional: true, Computed: true, }, }, }, Nesting: configschema.NestingMap, }, }, }, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = (sensitive) id = "i-02ae66f368e8518a9" + root_block_device "a" { + volume_type = "gp2" } } `, }, } runTestCases(t, testCases) } type testCase struct { Action plans.Action Mode addrs.ResourceMode Before cty.Value BeforeValMarks []cty.PathValueMarks AfterValMarks []cty.PathValueMarks After cty.Value Schema *configschema.Block RequiredReplace cty.PathSet Tainted bool ExpectedOutput string // This field and all associated values can be removed if the concise diff // experiment succeeds. VerboseOutput string } func runTestCases(t *testing.T, testCases map[string]testCase) { color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} for name, tc := range testCases { t.Run(name, func(t *testing.T) { ty := tc.Schema.ImpliedType() beforeVal := tc.Before switch { // Some fixups to make the test cases a little easier to write case beforeVal.IsNull(): beforeVal = cty.NullVal(ty) // allow mistyped nulls case !beforeVal.IsKnown(): beforeVal = cty.UnknownVal(ty) // allow mistyped unknowns } before, err := plans.NewDynamicValue(beforeVal, ty) if err != nil { t.Fatal(err) } afterVal := tc.After switch { // Some fixups to make the test cases a little easier to write case afterVal.IsNull(): afterVal = cty.NullVal(ty) // allow mistyped nulls case !afterVal.IsKnown(): afterVal = cty.UnknownVal(ty) // allow mistyped unknowns } after, err := plans.NewDynamicValue(afterVal, ty) if err != nil { t.Fatal(err) } change := &plans.ResourceInstanceChangeSrc{ Addr: addrs.Resource{ Mode: tc.Mode, Type: "test_instance", Name: "example", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), ProviderAddr: addrs.AbsProviderConfig{ Provider: addrs.NewLegacyProvider("test"), Module: addrs.RootModule, }, ChangeSrc: plans.ChangeSrc{ Action: tc.Action, Before: before, After: after, BeforeValMarks: tc.BeforeValMarks, AfterValMarks: tc.AfterValMarks, }, RequiredReplace: tc.RequiredReplace, } experiment.SetEnabled(experiment.X_concise_diff, true) output := ResourceChange(change, tc.Tainted, tc.Schema, color) if output != tc.ExpectedOutput { t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.ExpectedOutput) t.Errorf("%s", cmp.Diff(output, tc.ExpectedOutput)) } // Temporary coverage for verbose diff behaviour. All lines below // in this function can be removed if the concise diff experiment // succeeds. if tc.VerboseOutput == "" { return } experiment.SetEnabled(experiment.X_concise_diff, false) output = ResourceChange(change, tc.Tainted, tc.Schema, color) if output != tc.VerboseOutput { t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.VerboseOutput) t.Errorf("%s", cmp.Diff(output, tc.VerboseOutput)) } }) } } func TestOutputChanges(t *testing.T) { color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} testCases := map[string]struct { changes []*plans.OutputChangeSrc output string }{ "new output value": { []*plans.OutputChangeSrc{ outputChange( "foo", cty.NullVal(cty.DynamicPseudoType), cty.StringVal("bar"), false, ), }, ` + foo = "bar"`, }, "removed output": { []*plans.OutputChangeSrc{ outputChange( "foo", cty.StringVal("bar"), cty.NullVal(cty.DynamicPseudoType), false, ), }, ` - foo = "bar" -> null`, }, "single string change": { []*plans.OutputChangeSrc{ outputChange( "foo", cty.StringVal("bar"), cty.StringVal("baz"), false, ), }, ` ~ foo = "bar" -> "baz"`, }, "element added to list": { []*plans.OutputChangeSrc{ outputChange( "foo", cty.ListVal([]cty.Value{ cty.StringVal("alpha"), cty.StringVal("beta"), cty.StringVal("delta"), cty.StringVal("epsilon"), }), cty.ListVal([]cty.Value{ cty.StringVal("alpha"), cty.StringVal("beta"), cty.StringVal("gamma"), cty.StringVal("delta"), cty.StringVal("epsilon"), }), false, ), }, ` ~ foo = [ # (1 unchanged element hidden) "beta", + "gamma", "delta", # (1 unchanged element hidden) ]`, }, "multiple outputs changed, one sensitive": { []*plans.OutputChangeSrc{ outputChange( "a", cty.NumberIntVal(1), cty.NumberIntVal(2), false, ), outputChange( "b", cty.StringVal("hunter2"), cty.StringVal("correct-horse-battery-staple"), true, ), outputChange( "c", cty.BoolVal(false), cty.BoolVal(true), false, ), }, ` ~ a = 1 -> 2 ~ b = (sensitive value) ~ c = false -> true`, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { experiment.SetEnabled(experiment.X_concise_diff, true) output := OutputChanges(tc.changes, color) if output != tc.output { t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.output) } }) } } func outputChange(name string, before, after cty.Value, sensitive bool) *plans.OutputChangeSrc { addr := addrs.AbsOutputValue{ OutputValue: addrs.OutputValue{Name: name}, } change := &plans.OutputChange{ Addr: addr, Change: plans.Change{ Before: before, After: after, }, Sensitive: sensitive, } changeSrc, err := change.Encode() if err != nil { panic(fmt.Sprintf("failed to encode change for %s: %s", addr, err)) } return changeSrc }