| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package jsonformat | |
| import ( | |
| "encoding/json" | |
| "fmt" | |
| "testing" | |
| "github.com/google/go-cmp/cmp" | |
| "github.com/mitchellh/colorstring" | |
| "github.com/zclconf/go-cty/cty" | |
| ctyjson "github.com/zclconf/go-cty/cty/json" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/command/jsonformat/differ" | |
| "github.com/hashicorp/terraform/internal/command/jsonformat/structured" | |
| "github.com/hashicorp/terraform/internal/command/jsonformat/structured/attribute_path" | |
| "github.com/hashicorp/terraform/internal/command/jsonplan" | |
| "github.com/hashicorp/terraform/internal/command/jsonprovider" | |
| "github.com/hashicorp/terraform/internal/configs/configschema" | |
| "github.com/hashicorp/terraform/internal/plans" | |
| "github.com/hashicorp/terraform/internal/providers" | |
| "github.com/hashicorp/terraform/internal/states" | |
| "github.com/hashicorp/terraform/internal/terminal" | |
| "github.com/hashicorp/terraform/internal/terraform" | |
| ) | |
| func TestRenderHuman_InvokeActionPlan(t *testing.T) { | |
| color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} | |
| streams, done := terminal.StreamsForTesting(t) | |
| plan := Plan{ | |
| ActionInvocations: []jsonplan.ActionInvocation{ | |
| { | |
| Address: "action.test_action.action", | |
| Type: "test_action", | |
| Name: "action", | |
| ConfigValues: json.RawMessage("{\"attr\":\"one\"}"), | |
| ConfigSensitive: nil, | |
| ProviderName: "test", | |
| InvokeActionTrigger: new(jsonplan.InvokeActionTrigger), | |
| }, | |
| }, | |
| ProviderSchemas: map[string]*jsonprovider.Provider{ | |
| "test": { | |
| ActionSchemas: map[string]*jsonprovider.ActionSchema{ | |
| "test_action": { | |
| ConfigSchema: &jsonprovider.Block{ | |
| Attributes: map[string]*jsonprovider.Attribute{ | |
| "attr": { | |
| AttributeType: []byte("\"string\""), | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| renderer := Renderer{Colorize: color, Streams: streams} | |
| plan.renderHuman(renderer, plans.RefreshOnlyMode) | |
| want := ` | |
| Terraform will perform the following actions: | |
| Plan: 0 to add, 0 to change, 0 to destroy. Actions: 1 to invoke. | |
| Terraform will invoke the following action(s): | |
| # action.test_action.action will be invoked | |
| # action.test_action.action | |
| action "test_action" "action" { | |
| config { | |
| attr = "one" | |
| } | |
| } | |
| ` | |
| got := done(t).Stdout() | |
| if diff := cmp.Diff(want, got); len(diff) > 0 { | |
| t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) | |
| } | |
| } | |
| func TestRenderHuman_InvokeActionPlanWithRefresh(t *testing.T) { | |
| color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} | |
| streams, done := terminal.StreamsForTesting(t) | |
| plan := Plan{ | |
| ActionInvocations: []jsonplan.ActionInvocation{ | |
| { | |
| Address: "action.test_action.action", | |
| Type: "test_action", | |
| Name: "action", | |
| ConfigValues: json.RawMessage("{\"attr\":\"one\"}"), | |
| ConfigSensitive: nil, | |
| ProviderName: "test", | |
| InvokeActionTrigger: new(jsonplan.InvokeActionTrigger), | |
| }, | |
| }, | |
| ResourceDrift: []jsonplan.ResourceChange{ | |
| { | |
| Address: "aws_instance.foo", | |
| Mode: "managed", | |
| Type: "aws_instance", | |
| Name: "foo", | |
| IndexUnknown: true, | |
| ProviderName: "aws", | |
| Change: jsonplan.Change{ | |
| Actions: []string{"update"}, | |
| Before: marshalJson(t, map[string]interface{}{ | |
| "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| "value": "Hello, World!", | |
| }), | |
| After: marshalJson(t, map[string]interface{}{ | |
| "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| "value": "Hello, World!", | |
| }), | |
| }, | |
| }, | |
| }, | |
| ProviderSchemas: map[string]*jsonprovider.Provider{ | |
| "test": { | |
| ActionSchemas: map[string]*jsonprovider.ActionSchema{ | |
| "test_action": { | |
| ConfigSchema: &jsonprovider.Block{ | |
| Attributes: map[string]*jsonprovider.Attribute{ | |
| "attr": { | |
| AttributeType: []byte("\"string\""), | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| "aws": { | |
| ResourceSchemas: map[string]*jsonprovider.Schema{ | |
| "aws_instance": { | |
| Block: &jsonprovider.Block{ | |
| Attributes: map[string]*jsonprovider.Attribute{ | |
| "id": { | |
| AttributeType: marshalJson(t, "string"), | |
| }, | |
| "ami": { | |
| AttributeType: marshalJson(t, "string"), | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| renderer := Renderer{Colorize: color, Streams: streams} | |
| plan.renderHuman(renderer, plans.RefreshOnlyMode) | |
| want := ` | |
| Note: Objects have changed outside of Terraform | |
| Terraform detected the following changes made outside of Terraform since the | |
| last "terraform apply" which may have affected this plan: | |
| # aws_instance.foo has changed | |
| ~ resource "aws_instance" "foo" { | |
| id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" | |
| } | |
| This is a refresh-only plan, so Terraform will not take any actions to undo | |
| these. If you were expecting these changes then you can apply this plan to | |
| record the updated values in the Terraform state without changing any remote | |
| objects. | |
| ───────────────────────────────────────────────────────────────────────────── | |
| Terraform will perform the following actions: | |
| Plan: 0 to add, 0 to change, 0 to destroy. Actions: 1 to invoke. | |
| Terraform will invoke the following action(s): | |
| # action.test_action.action will be invoked | |
| # action.test_action.action | |
| action "test_action" "action" { | |
| config { | |
| attr = "one" | |
| } | |
| } | |
| ` | |
| got := done(t).Stdout() | |
| if diff := cmp.Diff(want, got); len(diff) > 0 { | |
| t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) | |
| } | |
| } | |
| func TestRenderHuman_EmptyPlan(t *testing.T) { | |
| color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} | |
| streams, done := terminal.StreamsForTesting(t) | |
| plan := Plan{} | |
| renderer := Renderer{Colorize: color, Streams: streams} | |
| plan.renderHuman(renderer, plans.NormalMode) | |
| want := ` | |
| No changes. Your infrastructure matches the configuration. | |
| Terraform has compared your real infrastructure against your configuration | |
| and found no differences, so no changes are needed. | |
| ` | |
| got := done(t).Stdout() | |
| if diff := cmp.Diff(want, got); len(diff) > 0 { | |
| t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) | |
| } | |
| } | |
| func TestRenderHuman_DeferredPlan(t *testing.T) { | |
| color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} | |
| streams, done := terminal.StreamsForTesting(t) | |
| plan := Plan{ | |
| DeferredChanges: []jsonplan.DeferredResourceChange{ | |
| { | |
| Reason: jsonplan.DeferredReasonDeferredPrereq, | |
| ResourceChange: jsonplan.ResourceChange{ | |
| Address: "aws_instance.foo", | |
| Mode: "managed", | |
| Type: "aws_instance", | |
| Name: "foo", | |
| IndexUnknown: true, | |
| ProviderName: "aws", | |
| Change: jsonplan.Change{ | |
| Actions: []string{"update"}, | |
| Before: marshalJson(t, map[string]interface{}{ | |
| "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| "value": "Hello, World!", | |
| }), | |
| After: marshalJson(t, map[string]interface{}{ | |
| "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| "value": "Hello, World!", | |
| }), | |
| }, | |
| }, | |
| }, | |
| }, | |
| ProviderSchemas: map[string]*jsonprovider.Provider{ | |
| "aws": { | |
| ResourceSchemas: map[string]*jsonprovider.Schema{ | |
| "aws_instance": { | |
| Block: &jsonprovider.Block{ | |
| Attributes: map[string]*jsonprovider.Attribute{ | |
| "id": { | |
| AttributeType: marshalJson(t, "string"), | |
| }, | |
| "ami": { | |
| AttributeType: marshalJson(t, "string"), | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| renderer := Renderer{Colorize: color, Streams: streams} | |
| plan.renderHuman(renderer, plans.NormalMode) | |
| want := ` | |
| No current changes. This plan requires another plan to be applied first. | |
| Note: This is a partial plan, parts can only be known in the next plan / apply cycle. | |
| # aws_instance.foo was deferred | |
| # (because a prerequisite for this resource is deferred) | |
| ~ resource "aws_instance" "foo" { | |
| id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" | |
| } | |
| ───────────────────────────────────────────────────────────────────────────── | |
| ` | |
| got := done(t).Stdout() | |
| if diff := cmp.Diff(want, got); len(diff) > 0 { | |
| t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) | |
| } | |
| } | |
| func TestRenderHuman_EmptyOutputs(t *testing.T) { | |
| color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} | |
| streams, done := terminal.StreamsForTesting(t) | |
| outputVal, _ := json.Marshal("some-text") | |
| plan := Plan{ | |
| OutputChanges: map[string]jsonplan.Change{ | |
| "a_string": { | |
| Actions: []string{"no-op"}, | |
| Before: outputVal, | |
| After: outputVal, | |
| }, | |
| }, | |
| } | |
| renderer := Renderer{Colorize: color, Streams: streams} | |
| plan.renderHuman(renderer, plans.NormalMode) | |
| want := ` | |
| No changes. Your infrastructure matches the configuration. | |
| Terraform has compared your real infrastructure against your configuration | |
| and found no differences, so no changes are needed. | |
| ` | |
| got := done(t).Stdout() | |
| if diff := cmp.Diff(want, got); len(diff) > 0 { | |
| t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) | |
| } | |
| } | |
| func TestRenderHuman_Imports(t *testing.T) { | |
| color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} | |
| schemas := map[string]*jsonprovider.Provider{ | |
| "test": { | |
| ResourceSchemas: map[string]*jsonprovider.Schema{ | |
| "test_resource": { | |
| Block: &jsonprovider.Block{ | |
| Attributes: map[string]*jsonprovider.Attribute{ | |
| "id": { | |
| AttributeType: marshalJson(t, "string"), | |
| }, | |
| "value": { | |
| AttributeType: marshalJson(t, "string"), | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| tcs := map[string]struct { | |
| plan Plan | |
| output string | |
| }{ | |
| "simple_import": { | |
| plan: Plan{ | |
| ResourceChanges: []jsonplan.ResourceChange{ | |
| { | |
| Address: "test_resource.resource", | |
| Mode: "managed", | |
| Type: "test_resource", | |
| Name: "resource", | |
| ProviderName: "test", | |
| Change: jsonplan.Change{ | |
| Actions: []string{"no-op"}, | |
| Before: marshalJson(t, map[string]interface{}{ | |
| "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| "value": "Hello, World!", | |
| }), | |
| After: marshalJson(t, map[string]interface{}{ | |
| "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| "value": "Hello, World!", | |
| }), | |
| Importing: &jsonplan.Importing{ | |
| ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| output: ` | |
| Terraform will perform the following actions: | |
| # test_resource.resource will be imported | |
| resource "test_resource" "resource" { | |
| id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" | |
| value = "Hello, World!" | |
| } | |
| Plan: 1 to import, 0 to add, 0 to change, 0 to destroy. | |
| `, | |
| }, | |
| "simple_import_with_generated_config": { | |
| plan: Plan{ | |
| ResourceChanges: []jsonplan.ResourceChange{ | |
| { | |
| Address: "test_resource.resource", | |
| Mode: "managed", | |
| Type: "test_resource", | |
| Name: "resource", | |
| ProviderName: "test", | |
| Change: jsonplan.Change{ | |
| Actions: []string{"no-op"}, | |
| Before: marshalJson(t, map[string]interface{}{ | |
| "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| "value": "Hello, World!", | |
| }), | |
| After: marshalJson(t, map[string]interface{}{ | |
| "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| "value": "Hello, World!", | |
| }), | |
| Importing: &jsonplan.Importing{ | |
| ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| }, | |
| GeneratedConfig: `resource "test_resource" "resource" { | |
| id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" | |
| value = "Hello, World!" | |
| }`, | |
| }, | |
| }, | |
| }, | |
| }, | |
| output: ` | |
| Terraform will perform the following actions: | |
| # test_resource.resource will be imported | |
| # (config will be generated) | |
| resource "test_resource" "resource" { | |
| id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" | |
| value = "Hello, World!" | |
| } | |
| Plan: 1 to import, 0 to add, 0 to change, 0 to destroy. | |
| `, | |
| }, | |
| "import_and_move": { | |
| plan: Plan{ | |
| ResourceChanges: []jsonplan.ResourceChange{ | |
| { | |
| Address: "test_resource.after", | |
| PreviousAddress: "test_resource.before", | |
| Mode: "managed", | |
| Type: "test_resource", | |
| Name: "after", | |
| ProviderName: "test", | |
| Change: jsonplan.Change{ | |
| Actions: []string{"no-op"}, | |
| Before: marshalJson(t, map[string]interface{}{ | |
| "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| "value": "Hello, World!", | |
| }), | |
| After: marshalJson(t, map[string]interface{}{ | |
| "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| "value": "Hello, World!", | |
| }), | |
| Importing: &jsonplan.Importing{ | |
| ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| output: ` | |
| Terraform will perform the following actions: | |
| # test_resource.before has moved to test_resource.after | |
| # (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E") | |
| resource "test_resource" "after" { | |
| id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" | |
| value = "Hello, World!" | |
| } | |
| Plan: 1 to import, 0 to add, 0 to change, 0 to destroy. | |
| `, | |
| }, | |
| "import_move_and_update": { | |
| plan: Plan{ | |
| ResourceChanges: []jsonplan.ResourceChange{ | |
| { | |
| Address: "test_resource.after", | |
| PreviousAddress: "test_resource.before", | |
| Mode: "managed", | |
| Type: "test_resource", | |
| Name: "after", | |
| ProviderName: "test", | |
| Change: jsonplan.Change{ | |
| Actions: []string{"update"}, | |
| Before: marshalJson(t, map[string]interface{}{ | |
| "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| "value": "Hello, World!", | |
| }), | |
| After: marshalJson(t, map[string]interface{}{ | |
| "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| "value": "Hello, Universe!", | |
| }), | |
| Importing: &jsonplan.Importing{ | |
| ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| output: ` | |
| Terraform used the selected providers to generate the following execution | |
| plan. Resource actions are indicated with the following symbols: | |
| ~ update in-place | |
| Terraform will perform the following actions: | |
| # test_resource.after will be updated in-place | |
| # (moved from test_resource.before) | |
| # (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E") | |
| ~ resource "test_resource" "after" { | |
| id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" | |
| ~ value = "Hello, World!" -> "Hello, Universe!" | |
| } | |
| Plan: 1 to import, 0 to add, 1 to change, 0 to destroy. | |
| `, | |
| }, | |
| "import_and_update": { | |
| plan: Plan{ | |
| ResourceChanges: []jsonplan.ResourceChange{ | |
| { | |
| Address: "test_resource.resource", | |
| Mode: "managed", | |
| Type: "test_resource", | |
| Name: "resource", | |
| ProviderName: "test", | |
| Change: jsonplan.Change{ | |
| Actions: []string{"update"}, | |
| Before: marshalJson(t, map[string]interface{}{ | |
| "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| "value": "Hello, World!", | |
| }), | |
| After: marshalJson(t, map[string]interface{}{ | |
| "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| "value": "Hello, Universe!", | |
| }), | |
| Importing: &jsonplan.Importing{ | |
| ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| output: ` | |
| Terraform used the selected providers to generate the following execution | |
| plan. Resource actions are indicated with the following symbols: | |
| ~ update in-place | |
| Terraform will perform the following actions: | |
| # test_resource.resource will be updated in-place | |
| # (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E") | |
| ~ resource "test_resource" "resource" { | |
| id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" | |
| ~ value = "Hello, World!" -> "Hello, Universe!" | |
| } | |
| Plan: 1 to import, 0 to add, 1 to change, 0 to destroy. | |
| `, | |
| }, | |
| "import_and_update_with_no_id": { | |
| plan: Plan{ | |
| ResourceChanges: []jsonplan.ResourceChange{ | |
| { | |
| Address: "test_resource.resource", | |
| Mode: "managed", | |
| Type: "test_resource", | |
| Name: "resource", | |
| ProviderName: "test", | |
| Change: jsonplan.Change{ | |
| Actions: []string{"update"}, | |
| Before: marshalJson(t, map[string]interface{}{ | |
| "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| "value": "Hello, World!", | |
| }), | |
| After: marshalJson(t, map[string]interface{}{ | |
| "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| "value": "Hello, Universe!", | |
| }), | |
| Importing: &jsonplan.Importing{}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| output: ` | |
| Terraform used the selected providers to generate the following execution | |
| plan. Resource actions are indicated with the following symbols: | |
| ~ update in-place | |
| Terraform will perform the following actions: | |
| # test_resource.resource will be updated in-place | |
| # (will be imported first) | |
| ~ resource "test_resource" "resource" { | |
| id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" | |
| ~ value = "Hello, World!" -> "Hello, Universe!" | |
| } | |
| Plan: 1 to import, 0 to add, 1 to change, 0 to destroy. | |
| `, | |
| }, | |
| "import_and_replace": { | |
| plan: Plan{ | |
| ResourceChanges: []jsonplan.ResourceChange{ | |
| { | |
| Address: "test_resource.resource", | |
| Mode: "managed", | |
| Type: "test_resource", | |
| Name: "resource", | |
| ProviderName: "test", | |
| Change: jsonplan.Change{ | |
| Actions: []string{"create", "delete"}, | |
| Before: marshalJson(t, map[string]interface{}{ | |
| "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| "value": "Hello, World!", | |
| }), | |
| After: marshalJson(t, map[string]interface{}{ | |
| "id": "9794FB1F-7260-442F-830C-F2D450E90CE3", | |
| "value": "Hello, World!", | |
| }), | |
| ReplacePaths: marshalJson(t, [][]string{{"id"}}), | |
| Importing: &jsonplan.Importing{ | |
| ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| }, | |
| }, | |
| ActionReason: "", | |
| }, | |
| }, | |
| }, | |
| output: ` | |
| Terraform used the selected providers to generate the following execution | |
| plan. Resource actions are indicated with the following symbols: | |
| +/- create replacement and then destroy | |
| Terraform will perform the following actions: | |
| # test_resource.resource must be replaced | |
| # (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E") | |
| # Warning: this will destroy the imported resource | |
| +/- resource "test_resource" "resource" { | |
| ~ id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" -> "9794FB1F-7260-442F-830C-F2D450E90CE3" # forces replacement | |
| value = "Hello, World!" | |
| } | |
| Plan: 1 to import, 1 to add, 0 to change, 1 to destroy. | |
| `, | |
| }, | |
| } | |
| for name, tc := range tcs { | |
| t.Run(name, func(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| plan := tc.plan | |
| plan.PlanFormatVersion = jsonplan.FormatVersion | |
| plan.ProviderFormatVersion = jsonprovider.FormatVersion | |
| plan.ProviderSchemas = schemas | |
| renderer := Renderer{ | |
| Colorize: color, | |
| Streams: streams, | |
| } | |
| plan.renderHuman(renderer, plans.NormalMode) | |
| got := done(t).Stdout() | |
| want := tc.output | |
| if diff := cmp.Diff(want, got); len(diff) > 0 { | |
| t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) | |
| } | |
| }) | |
| } | |
| } | |
| 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(), | |
| 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(), | |
| 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(), | |
| ExpectedOutput: ` # test_instance.example will be created | |
| + resource "test_instance" "example" { | |
| + string = "null " | |
| }`, | |
| }, | |
| "creation (object with quoted keys)": { | |
| Action: plans.Create, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.NullVal(cty.EmptyObject), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "object": cty.ObjectVal(map[string]cty.Value{ | |
| "unquoted": cty.StringVal("value"), | |
| "quoted:key": cty.StringVal("some-value"), | |
| }), | |
| }), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "object": {Type: cty.Object(map[string]cty.Type{ | |
| "unquoted": cty.String, | |
| "quoted:key": cty.String, | |
| }), Optional: true}, | |
| }, | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example will be created | |
| + resource "test_instance" "example" { | |
| + object = { | |
| + "quoted:key" = "some-value" | |
| + unquoted = "value" | |
| } | |
| }`, | |
| }, | |
| "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(), | |
| ExpectedOutput: ` # test_instance.example will be destroyed | |
| - resource "test_instance" "example" { | |
| - id = "i-02ae66f368e8518a9" -> null | |
| }`, | |
| }, | |
| "deletion of deposed object": { | |
| Action: plans.Delete, | |
| Mode: addrs.ManagedResourceMode, | |
| DeposedKey: states.DeposedKey("byebye"), | |
| 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(), | |
| ExpectedOutput: ` # test_instance.example (deposed object byebye) will be destroyed | |
| # (left over from a partially-failed replacement of this instance) | |
| - 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(), | |
| ExpectedOutput: ` # test_instance.example will be destroyed | |
| - resource "test_instance" "example" { | |
| - id = "i-02ae66f368e8518a9" -> null | |
| # (1 unchanged attribute hidden) | |
| }`, | |
| }, | |
| "forget": { | |
| Action: plans.Forget, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-123"), | |
| }), | |
| After: cty.NullVal(cty.EmptyObject), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Computed: true}, | |
| "ami": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example will no longer be managed by Terraform, but will not be destroyed | |
| # (destroy = false is set in the configuration) | |
| . resource "test_instance" "example" { | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| }`, | |
| }, | |
| "forget (deposed)": { | |
| Action: plans.Forget, | |
| Mode: addrs.ManagedResourceMode, | |
| DeposedKey: states.DeposedKey("adios"), | |
| 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(), | |
| ExpectedOutput: ` # test_instance.example (deposed object adios) will be removed from Terraform state, but will not be destroyed | |
| # (left over from a partially-failed replacement of this instance) | |
| # (destroy = false is set in the configuration) | |
| . resource "test_instance" "example" { | |
| id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| "create-then-forget": { | |
| Action: plans.CreateThenForget, | |
| 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-02999999999999999"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| }), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Computed: true}, | |
| "ami": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| RequiredReplace: cty.NewPathSet(cty.Path{ | |
| cty.GetAttrStep{Name: "ami"}, | |
| }), | |
| ExpectedOutput: ` # test_instance.example must be replaced, but the existing object will not be destroyed | |
| # (destroy = false is set in the configuration) | |
| +/. resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement | |
| ~ id = "i-02ae66f368e8518a9" -> "i-02999999999999999" | |
| }`, | |
| }, | |
| "forget-then-create": { | |
| Action: plans.ForgetThenCreate, | |
| 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-02999999999999999"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| }), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Computed: true}, | |
| "ami": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| RequiredReplace: cty.NewPathSet(cty.Path{ | |
| cty.GetAttrStep{Name: "ami"}, | |
| }), | |
| ExpectedOutput: ` # test_instance.example must be replaced, but the existing object will not be destroyed | |
| # (destroy = false is set in the configuration) | |
| ./+ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement | |
| ~ id = "i-02ae66f368e8518a9" -> "i-02999999999999999" | |
| }`, | |
| }, | |
| "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(), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| "update with quoted key": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "saml:aud": cty.StringVal("https://example.com/saml"), | |
| "zeta": cty.StringVal("alpha"), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "saml:aud": cty.StringVal("https://saml.example.com"), | |
| "zeta": cty.StringVal("alpha"), | |
| }), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| "saml:aud": {Type: cty.String, Optional: true}, | |
| "zeta": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| id = "i-02ae66f368e8518a9" | |
| ~ "saml:aud" = "https://example.com/saml" -> "https://saml.example.com" | |
| # (1 unchanged attribute hidden) | |
| }`, | |
| }, | |
| "string force-new update": { | |
| Action: plans.DeleteThenCreate, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| 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"}, | |
| }), | |
| 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), | |
| "empty": cty.StringVal(""), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "unchanged": cty.NullVal(cty.String), | |
| "empty": 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}, | |
| "empty": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| }`, | |
| }, | |
| "string update (non-legacy)": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "from_null": cty.NullVal(cty.String), | |
| "to_null": cty.StringVal(""), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "from_null": cty.StringVal(""), | |
| "to_null": cty.NullVal(cty.String), | |
| }), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| "from_null": {Type: cty.DynamicPseudoType, Optional: true}, | |
| "to_null": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| + from_null = "" | |
| id = "i-02ae66f368e8518a9" | |
| - to_null = "" -> null | |
| }`, | |
| }, | |
| "string update (non-legacy nested object)": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "obj": cty.ObjectVal(map[string]cty.Value{ | |
| "from_null": cty.NullVal(cty.String), | |
| "to_null": cty.StringVal(""), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "obj": cty.ObjectVal(map[string]cty.Value{ | |
| "from_null": cty.StringVal(""), | |
| "to_null": cty.NullVal(cty.String), | |
| }), | |
| }), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| "obj": {NestedType: &configschema.Object{ | |
| Nesting: configschema.NestingSingle, | |
| Attributes: map[string]*configschema.Attribute{ | |
| "from_null": {Type: cty.DynamicPseudoType, Optional: true}, | |
| "to_null": {Type: cty.String, Optional: true}, | |
| }, | |
| }}, | |
| }, | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| id = "i-02ae66f368e8518a9" | |
| ~ obj = { | |
| + from_null = "" | |
| - to_null = "" -> null | |
| } | |
| }`, | |
| }, | |
| "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(), | |
| 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(), | |
| 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"}, | |
| }), | |
| 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"), | |
| "conn_info": cty.ObjectVal(map[string]cty.Value{ | |
| "user": cty.StringVal("not-secret"), | |
| "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}, | |
| "conn_info": { | |
| NestedType: &configschema.Object{ | |
| Nesting: configschema.NestingSingle, | |
| Attributes: map[string]*configschema.Attribute{ | |
| "user": {Type: cty.String, Optional: true}, | |
| "password": {Type: cty.String, Optional: true, Sensitive: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example will be created | |
| + resource "test_instance" "example" { | |
| + conn_info = { | |
| + password = (sensitive value) | |
| + user = "not-secret" | |
| } | |
| + 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(), | |
| 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) | |
| }`, | |
| }, | |
| // tainted objects | |
| "replace tainted resource": { | |
| Action: plans.DeleteThenCreate, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseTainted, | |
| 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"}, | |
| }), | |
| 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, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| 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"}, | |
| }), | |
| 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, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| 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"}, | |
| }), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| -/+ resource "test_instance" "example" { | |
| + forced = "example" # forces replacement | |
| name = "name" | |
| }`, | |
| }, | |
| "read during apply because of unknown configuration": { | |
| Action: plans.Read, | |
| ActionReason: plans.ResourceInstanceReadBecauseConfigUnknown, | |
| Mode: addrs.DataResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "name": cty.StringVal("name"), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "name": cty.StringVal("name"), | |
| }), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "name": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| ExpectedOutput: ` # data.test_instance.example will be read during apply | |
| # (config refers to values not yet known) | |
| <= data "test_instance" "example" { | |
| name = "name" | |
| }`, | |
| }, | |
| "read during apply because of pending changes to upstream dependency": { | |
| Action: plans.Read, | |
| ActionReason: plans.ResourceInstanceReadBecauseDependencyPending, | |
| Mode: addrs.DataResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "name": cty.StringVal("name"), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "name": cty.StringVal("name"), | |
| }), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "name": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| ExpectedOutput: ` # data.test_instance.example will be read during apply | |
| # (depends on a resource or a module with changes pending) | |
| <= data "test_instance" "example" { | |
| name = "name" | |
| }`, | |
| }, | |
| "read during apply for unspecified reason": { | |
| Action: plans.Read, | |
| Mode: addrs.DataResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "name": cty.StringVal("name"), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "name": cty.StringVal("name"), | |
| }), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "name": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| ExpectedOutput: ` # data.test_instance.example will be read during apply | |
| <= data "test_instance" "example" { | |
| 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(), | |
| 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) | |
| }`, | |
| }, | |
| } | |
| 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(), | |
| 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(), | |
| 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 | |
| # (1 unchanged attribute hidden) | |
| } | |
| ) | |
| }`, | |
| }, | |
| "in-place update of object with quoted keys": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "json_field": cty.StringVal(`{"aaa": "value", "c:c": "old_value"}`), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.UnknownVal(cty.String), | |
| "json_field": cty.StringVal(`{"aaa": "value", "b:bb": "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(), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ id = "i-02ae66f368e8518a9" -> (known after apply) | |
| ~ json_field = jsonencode( | |
| ~ { | |
| + "b:bb" = "new_value" | |
| - "c:c" = "old_value" | |
| # (1 unchanged attribute hidden) | |
| } | |
| ) | |
| }`, | |
| }, | |
| "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(), | |
| 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(), | |
| 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(), | |
| 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, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| 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"}, | |
| }), | |
| 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 attribute hidden) | |
| } # 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(), | |
| 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, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| 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"}, | |
| }), | |
| 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(), | |
| 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(), | |
| 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", | |
| ] | |
| ) | |
| }`, | |
| }, | |
| "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(), | |
| 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", | |
| ] | |
| ) | |
| }`, | |
| }, | |
| "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(), | |
| 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 attribute 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(), | |
| 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(), | |
| 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(), | |
| 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(), | |
| 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(), | |
| 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(), | |
| 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_listObject(t *testing.T) { | |
| testCases := map[string]testCase{ | |
| // https://github.com/hashicorp/terraform/issues/30641 | |
| "updating non-identifying attribute": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "accounts": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("1"), | |
| "name": cty.StringVal("production"), | |
| "status": cty.StringVal("ACTIVE"), | |
| }), | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("2"), | |
| "name": cty.StringVal("staging"), | |
| "status": cty.StringVal("ACTIVE"), | |
| }), | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("3"), | |
| "name": cty.StringVal("disaster-recovery"), | |
| "status": cty.StringVal("ACTIVE"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.UnknownVal(cty.String), | |
| "accounts": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("1"), | |
| "name": cty.StringVal("production"), | |
| "status": cty.StringVal("ACTIVE"), | |
| }), | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("2"), | |
| "name": cty.StringVal("staging"), | |
| "status": cty.StringVal("EXPLODED"), | |
| }), | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("3"), | |
| "name": cty.StringVal("disaster-recovery"), | |
| "status": cty.StringVal("ACTIVE"), | |
| }), | |
| }), | |
| }), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| "accounts": { | |
| Type: cty.List(cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| "name": cty.String, | |
| "status": cty.String, | |
| })), | |
| }, | |
| }, | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ accounts = [ | |
| { | |
| id = "1" | |
| name = "production" | |
| status = "ACTIVE" | |
| }, | |
| ~ { | |
| id = "2" | |
| name = "staging" | |
| ~ status = "ACTIVE" -> "EXPLODED" | |
| }, | |
| { | |
| id = "3" | |
| name = "disaster-recovery" | |
| status = "ACTIVE" | |
| }, | |
| ] | |
| ~ id = "i-02ae66f368e8518a9" -> (known after apply) | |
| }`, | |
| }, | |
| } | |
| 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(), | |
| 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) | |
| }`, | |
| }, | |
| "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(), | |
| 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) | |
| }`, | |
| }, | |
| "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(), | |
| 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) | |
| }`, | |
| }, | |
| "force-new update - insertion": { | |
| Action: plans.DeleteThenCreate, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| 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"}, | |
| }), | |
| 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) | |
| }`, | |
| }, | |
| "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(), | |
| 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) | |
| }`, | |
| }, | |
| "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(), | |
| 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(), | |
| 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) | |
| }`, | |
| }, | |
| "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(), | |
| 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) | |
| }`, | |
| }, | |
| "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(), | |
| 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) | |
| }`, | |
| }, | |
| "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(), | |
| 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) | |
| }`, | |
| }, | |
| } | |
| 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(), | |
| 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) | |
| ] | |
| }`, | |
| }, | |
| } | |
| 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(), | |
| 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) | |
| }`, | |
| }, | |
| "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(), | |
| 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) | |
| }`, | |
| }, | |
| "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(), | |
| 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) | |
| }`, | |
| }, | |
| "force-new update - insertion": { | |
| Action: plans.DeleteThenCreate, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| 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"}, | |
| }), | |
| 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) | |
| }`, | |
| }, | |
| "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(), | |
| 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) | |
| }`, | |
| }, | |
| "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(), | |
| 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) | |
| }`, | |
| }, | |
| "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(), | |
| 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) | |
| }`, | |
| }, | |
| "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(), | |
| 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) | |
| }`, | |
| }, | |
| "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(), | |
| 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) | |
| }`, | |
| }, | |
| } | |
| 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"), | |
| "be:ep": cty.StringVal("boop"), | |
| }), | |
| }), | |
| 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(), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ id = "i-02ae66f368e8518a9" -> (known after apply) | |
| + map_field = { | |
| + "be:ep" = "boop" | |
| + "new-key" = "new-element" | |
| } | |
| # (1 unchanged attribute hidden) | |
| }`, | |
| }, | |
| "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"), | |
| "be:ep": cty.StringVal("boop"), | |
| }), | |
| }), | |
| 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(), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ id = "i-02ae66f368e8518a9" -> (known after apply) | |
| ~ map_field = { | |
| + "be:ep" = "boop" | |
| + "new-key" = "new-element" | |
| } | |
| # (1 unchanged attribute hidden) | |
| }`, | |
| }, | |
| "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"), | |
| "b: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(), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ id = "i-02ae66f368e8518a9" -> (known after apply) | |
| ~ map_field = { | |
| + "b" = "bbbb" | |
| + "b:b" = "bbbb" | |
| # (2 unchanged elements hidden) | |
| } | |
| # (1 unchanged attribute hidden) | |
| }`, | |
| }, | |
| "force-new update - insertion": { | |
| Action: plans.DeleteThenCreate, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| 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"}, | |
| }), | |
| 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) | |
| }`, | |
| }, | |
| "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(), | |
| 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) | |
| }`, | |
| }, | |
| "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(), | |
| 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(), | |
| 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) | |
| }`, | |
| }, | |
| } | |
| 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"), | |
| }), | |
| }), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| }), | |
| 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"), | |
| }), | |
| }), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchema(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 attribute hidden) | |
| # (1 unchanged block hidden) | |
| }`, | |
| }, | |
| "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, | |
| })), | |
| "disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| })), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| })}), | |
| "root_block_device": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.NullVal(cty.String), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchema(configschema.NestingList), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = [ | |
| + { | |
| + mount_point = "/var/diska" | |
| + size = "50GB" | |
| }, | |
| ] | |
| 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, | |
| })), | |
| "disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| })), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.NullVal(cty.String), | |
| }), | |
| }), | |
| "root_block_device": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchema(configschema.NestingList), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = [ | |
| + { | |
| + mount_point = "/var/diska" | |
| }, | |
| ] | |
| 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"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.NullVal(cty.String), | |
| }), | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diskb"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diskb"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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(), | |
| Schema: testSchemaPlus(configschema.NestingList), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = [ | |
| ~ { | |
| + size = "50GB" | |
| # (1 unchanged attribute hidden) | |
| }, | |
| # (1 unchanged element hidden) | |
| ] | |
| id = "i-02ae66f368e8518a9" | |
| ~ root_block_device { | |
| + new_field = "new_value" | |
| # (1 unchanged attribute hidden) | |
| } | |
| }`, | |
| }, | |
| "force-new update (inside blocks)": { | |
| Action: plans.DeleteThenCreate, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diskb"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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"}, | |
| }, | |
| cty.Path{ | |
| cty.GetAttrStep{Name: "disks"}, | |
| cty.IndexStep{Key: cty.NumberIntVal(0)}, | |
| cty.GetAttrStep{Name: "mount_point"}, | |
| }, | |
| ), | |
| Schema: testSchema(configschema.NestingList), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| -/+ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = [ | |
| ~ { | |
| ~ mount_point = "/var/diska" -> "/var/diskb" # forces replacement | |
| # (1 unchanged attribute hidden) | |
| }, | |
| ] | |
| id = "i-02ae66f368e8518a9" | |
| ~ root_block_device { | |
| ~ volume_type = "gp2" -> "different" # forces replacement | |
| } | |
| }`, | |
| }, | |
| "force-new update (whole block)": { | |
| Action: plans.DeleteThenCreate, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diskb"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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.Path{cty.GetAttrStep{Name: "disks"}}, | |
| ), | |
| Schema: testSchema(configschema.NestingList), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| -/+ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = [ # forces replacement | |
| ~ { | |
| ~ mount_point = "/var/diska" -> "/var/diskb" | |
| # (1 unchanged attribute hidden) | |
| }, | |
| ] | |
| 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"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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"), | |
| "disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| })), | |
| "root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| })), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchema(configschema.NestingList), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = [ | |
| - { | |
| - mount_point = "/var/diska" -> null | |
| - size = "50GB" -> null | |
| }, | |
| ] | |
| id = "i-02ae66f368e8518a9" | |
| - root_block_device { | |
| - 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(), | |
| 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 | |
| } | |
| }`, | |
| }, | |
| "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(), | |
| 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" | |
| } | |
| }`, | |
| }, | |
| "in-place update - unknown": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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"), | |
| "disks": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| }))), | |
| "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(), | |
| Schema: testSchemaPlus(configschema.NestingList), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = [ | |
| - { | |
| - mount_point = "/var/diska" -> null | |
| - size = "50GB" -> null | |
| }, | |
| ] -> (known after apply) | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged block hidden) | |
| }`, | |
| }, | |
| "in-place create - unknown block": { | |
| Action: plans.Create, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.NullVal(testSchemaPlus(configschema.NestingList).ImpliedType()), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| "new_field": cty.String, | |
| }))), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaPlus(configschema.NestingList), | |
| ExpectedOutput: ` # test_instance.example will be created | |
| + resource "test_instance" "example" { | |
| + ami = "ami-AFTER" | |
| + disks = [ | |
| + { | |
| + mount_point = "/var/diska" | |
| + size = "50GB" | |
| }, | |
| ] | |
| + id = "i-02ae66f368e8518a9" | |
| + root_block_device (known after apply) | |
| }`, | |
| }, | |
| "in-place update - unknown block": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp1"), | |
| "new_field": cty.StringVal("new_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"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| "new_field": cty.String, | |
| }))), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaPlus(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 attribute hidden) | |
| ~ root_block_device (known after apply) | |
| - root_block_device { | |
| - new_field = "new_value" -> null | |
| - volume_type = "gp1" -> null | |
| } | |
| - root_block_device { | |
| - new_field = "new_value" -> null | |
| - volume_type = "gp2" -> null | |
| } | |
| }`, | |
| }, | |
| "in-place update - unknown block with replace": { | |
| Action: plans.CreateThenDelete, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp1"), | |
| "new_field": cty.StringVal("new_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"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| "new_field": cty.String, | |
| }))), | |
| }), | |
| RequiredReplace: cty.NewPathSet( | |
| cty.GetAttrPath("root_block_device").IndexInt(0).GetAttr("volume_type"), | |
| cty.GetAttrPath("root_block_device").IndexInt(1).GetAttr("volume_type"), | |
| ), | |
| Schema: testSchemaPlus(configschema.NestingList), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| +/- resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| ~ root_block_device (known after apply) | |
| - root_block_device { | |
| - new_field = "new_value" -> null | |
| - volume_type = "gp1" -> null # forces replacement | |
| } | |
| - root_block_device { | |
| - new_field = "new_value" -> null | |
| - volume_type = "gp2" -> null # forces replacement | |
| } | |
| }`, | |
| }, | |
| "in-place update - modification": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diskb"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diskc"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diskb"), | |
| "size": cty.StringVal("75GB"), | |
| }), | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diskc"), | |
| "size": cty.StringVal("25GB"), | |
| }), | |
| }), | |
| "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(), | |
| Schema: testSchemaPlus(configschema.NestingList), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = [ | |
| ~ { | |
| ~ size = "50GB" -> "75GB" | |
| # (1 unchanged attribute hidden) | |
| }, | |
| ~ { | |
| ~ size = "50GB" -> "25GB" | |
| # (1 unchanged attribute hidden) | |
| }, | |
| # (1 unchanged element hidden) | |
| ] | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged block hidden) | |
| }`, | |
| }, | |
| } | |
| runTestCases(t, testCases) | |
| } | |
| func TestResourceChange_nestedSet(t *testing.T) { | |
| testCases := map[string]testCase{ | |
| "creation from null - sensitive set": { | |
| Action: plans.Create, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| "ami": cty.String, | |
| "disks": cty.Set(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| })), | |
| "root_block_device": cty.Set(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"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.NullVal(cty.String), | |
| }), | |
| }), | |
| "root_block_device": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| }), | |
| }), | |
| AfterSensitivePaths: []cty.Path{ | |
| cty.Path{cty.GetAttrStep{Name: "disks"}}, | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchema(configschema.NestingSet), | |
| ExpectedOutput: ` # test_instance.example will be created | |
| + resource "test_instance" "example" { | |
| + ami = "ami-AFTER" | |
| + disks = (sensitive value) | |
| + 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"), | |
| "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| })), | |
| "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"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.NullVal(cty.String), | |
| }), | |
| }), | |
| "root_block_device": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchema(configschema.NestingSet), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = [ | |
| + { | |
| + mount_point = "/var/diska" | |
| }, | |
| ] | |
| id = "i-02ae66f368e8518a9" | |
| + root_block_device { | |
| + volume_type = "gp2" | |
| } | |
| }`, | |
| }, | |
| "in-place update - creation - sensitive set": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| })), | |
| "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"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.NullVal(cty.String), | |
| }), | |
| }), | |
| "root_block_device": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| }), | |
| }), | |
| AfterSensitivePaths: []cty.Path{ | |
| cty.GetAttrPath("disks"), | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchema(configschema.NestingSet), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| # Warning: this attribute value will be marked as sensitive and will not | |
| # display in UI output after applying this change. | |
| ~ disks = (sensitive value) | |
| id = "i-02ae66f368e8518a9" | |
| + root_block_device { | |
| + volume_type = "gp2" | |
| } | |
| }`, | |
| }, | |
| "in-place update - marking set sensitive": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| })), | |
| }), | |
| AfterSensitivePaths: []cty.Path{ | |
| cty.GetAttrPath("disks"), | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchema(configschema.NestingSet), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| # Warning: this attribute value will be marked as sensitive and will not | |
| # display in UI output after applying this change. The value is unchanged. | |
| ~ disks = (sensitive value) | |
| id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| "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"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.NullVal(cty.String), | |
| }), | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diskb"), | |
| "size": cty.StringVal("100GB"), | |
| }), | |
| }), | |
| "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"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diskb"), | |
| "size": cty.StringVal("100GB"), | |
| }), | |
| }), | |
| "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(), | |
| Schema: testSchemaPlus(configschema.NestingSet), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = [ | |
| - { | |
| - mount_point = "/var/diska" -> null | |
| }, | |
| + { | |
| + mount_point = "/var/diska" | |
| + size = "50GB" | |
| }, | |
| # (1 unchanged element hidden) | |
| ] | |
| id = "i-02ae66f368e8518a9" | |
| - root_block_device { | |
| - volume_type = "gp2" -> null | |
| } | |
| + root_block_device { | |
| + new_field = "new_value" | |
| + volume_type = "gp2" | |
| } | |
| }`, | |
| }, | |
| "force-new update (whole block)": { | |
| Action: plans.DeleteThenCreate, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| 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"), | |
| }), | |
| }), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| }), | |
| 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"), | |
| }), | |
| }), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diskb"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet( | |
| cty.Path{cty.GetAttrStep{Name: "root_block_device"}}, | |
| cty.Path{cty.GetAttrStep{Name: "disks"}}, | |
| ), | |
| Schema: testSchema(configschema.NestingSet), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| -/+ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = [ | |
| - { # forces replacement | |
| - mount_point = "/var/diska" -> null | |
| - size = "50GB" -> null | |
| }, | |
| + { # forces replacement | |
| + mount_point = "/var/diskb" | |
| + size = "50GB" | |
| }, | |
| ] | |
| id = "i-02ae66f368e8518a9" | |
| - root_block_device { # forces replacement | |
| - volume_type = "gp2" -> null | |
| } | |
| + root_block_device { # forces replacement | |
| + volume_type = "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.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| "new_field": cty.StringVal("new_value"), | |
| }), | |
| }), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| }), | |
| 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, | |
| })), | |
| "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| })), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaPlus(configschema.NestingSet), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = [ | |
| - { | |
| - mount_point = "/var/diska" -> null | |
| - size = "50GB" -> null | |
| }, | |
| ] | |
| id = "i-02ae66f368e8518a9" | |
| - root_block_device { | |
| - new_field = "new_value" -> null | |
| - volume_type = "gp2" -> null | |
| } | |
| }`, | |
| }, | |
| "in-place update - empty nested sets": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| }))), | |
| "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"), | |
| "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| })), | |
| "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| })), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchema(configschema.NestingSet), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| + disks = [] | |
| id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| "in-place update - null insertion": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| }))), | |
| "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"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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(), | |
| Schema: testSchemaPlus(configschema.NestingSet), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| + disks = [ | |
| + { | |
| + mount_point = "/var/diska" | |
| + size = "50GB" | |
| }, | |
| ] | |
| id = "i-02ae66f368e8518a9" | |
| - root_block_device { | |
| - volume_type = "gp2" -> null | |
| } | |
| + root_block_device { | |
| + new_field = "new_value" | |
| + volume_type = "gp2" | |
| } | |
| }`, | |
| }, | |
| "in-place update - unknown": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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"), | |
| "disks": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| }))), | |
| "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(), | |
| Schema: testSchemaPlus(configschema.NestingSet), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = [ | |
| - { | |
| - mount_point = "/var/diska" -> null | |
| - size = "50GB" -> null | |
| }, | |
| ] -> (known after apply) | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged block hidden) | |
| }`, | |
| }, | |
| "in-place create - unknown block": { | |
| Action: plans.Create, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.NullVal(testSchemaPlus(configschema.NestingSet).ImpliedType()), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| "new_field": cty.String, | |
| }))), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaPlus(configschema.NestingSet), | |
| ExpectedOutput: ` # test_instance.example will be created | |
| + resource "test_instance" "example" { | |
| + ami = "ami-AFTER" | |
| + disks = [ | |
| + { | |
| + mount_point = "/var/diska" | |
| + size = "50GB" | |
| }, | |
| ] | |
| + id = "i-02ae66f368e8518a9" | |
| + root_block_device (known after apply) | |
| }`, | |
| }, | |
| "in-place update - unknown block": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp1"), | |
| "new_field": cty.StringVal("new_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"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| "new_field": cty.String, | |
| }))), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaPlus(configschema.NestingSet), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| ~ root_block_device (known after apply) | |
| - root_block_device { | |
| - new_field = "new_value" -> null | |
| - volume_type = "gp1" -> null | |
| } | |
| - root_block_device { | |
| - new_field = "new_value" -> null | |
| - volume_type = "gp2" -> null | |
| } | |
| }`, | |
| }, | |
| "in-place update - unknown block with replace": { | |
| Action: plans.CreateThenDelete, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp1"), | |
| "new_field": cty.StringVal("new_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"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| "new_field": cty.String, | |
| }))), | |
| }), | |
| RequiredReplace: cty.NewPathSet( | |
| cty.GetAttrPath("root_block_device").IndexInt(0).GetAttr("volume_type"), | |
| cty.GetAttrPath("root_block_device").IndexInt(1).GetAttr("volume_type"), | |
| ), | |
| Schema: testSchemaPlus(configschema.NestingSet), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| +/- resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| ~ root_block_device (known after apply) | |
| - root_block_device { | |
| - new_field = "new_value" -> null | |
| - volume_type = "gp1" -> null # forces replacement | |
| } | |
| - root_block_device { | |
| - new_field = "new_value" -> null | |
| - volume_type = "gp2" -> null # forces replacement | |
| } | |
| }`, | |
| }, | |
| } | |
| runTestCases(t, testCases) | |
| } | |
| func TestResourceChange_nestedMap(t *testing.T) { | |
| testCases := map[string]testCase{ | |
| "creation from null": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.NullVal(cty.String), | |
| "ami": cty.NullVal(cty.String), | |
| "disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| }))), | |
| "root_block_device": cty.NullVal(cty.Map(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"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.NullVal(cty.String), | |
| }), | |
| }), | |
| "root_block_device": cty.MapVal(map[string]cty.Value{ | |
| "a": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchema(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| + ami = "ami-AFTER" | |
| + disks = { | |
| + "disk_a" = { | |
| + mount_point = "/var/diska" | |
| }, | |
| } | |
| + id = "i-02ae66f368e8518a9" | |
| + root_block_device "a" { | |
| + 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"), | |
| "disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| })), | |
| "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"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.NullVal(cty.String), | |
| }), | |
| }), | |
| "root_block_device": cty.MapVal(map[string]cty.Value{ | |
| "a": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchema(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = { | |
| + "disk_a" = { | |
| + mount_point = "/var/diska" | |
| }, | |
| } | |
| 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"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.NullVal(cty.String), | |
| }), | |
| }), | |
| "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"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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(), | |
| Schema: testSchemaPlus(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = { | |
| ~ "disk_a" = { | |
| + size = "50GB" | |
| # (1 unchanged attribute hidden) | |
| }, | |
| } | |
| id = "i-02ae66f368e8518a9" | |
| ~ root_block_device "a" { | |
| + new_field = "new_value" | |
| # (1 unchanged attribute hidden) | |
| } | |
| }`, | |
| }, | |
| "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"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| "disk_2": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/disk2"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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(), | |
| Schema: testSchemaPlus(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = { | |
| + "disk_2" = { | |
| + mount_point = "/var/disk2" | |
| + size = "50GB" | |
| }, | |
| # (1 unchanged element hidden) | |
| } | |
| id = "i-02ae66f368e8518a9" | |
| + root_block_device "b" { | |
| + new_field = "new_value" | |
| + volume_type = "gp2" | |
| } | |
| # (1 unchanged block hidden) | |
| }`, | |
| }, | |
| "force-new update (whole block)": { | |
| Action: plans.DeleteThenCreate, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("100GB"), | |
| }), | |
| }), | |
| "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")}, | |
| }, | |
| cty.Path{cty.GetAttrStep{Name: "disks"}}, | |
| ), | |
| Schema: testSchema(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| -/+ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = { | |
| ~ "disk_a" = { # forces replacement | |
| ~ size = "50GB" -> "100GB" | |
| # (1 unchanged attribute hidden) | |
| }, | |
| } | |
| id = "i-02ae66f368e8518a9" | |
| ~ root_block_device "a" { # forces replacement | |
| ~ volume_type = "gp2" -> "different" | |
| } | |
| # (1 unchanged block hidden) | |
| }`, | |
| }, | |
| "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"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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"), | |
| "disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| })), | |
| "root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| "new_field": cty.String, | |
| })), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaPlus(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = { | |
| - "disk_a" = { | |
| - mount_point = "/var/diska" -> null | |
| - size = "50GB" -> null | |
| }, | |
| } | |
| id = "i-02ae66f368e8518a9" | |
| - root_block_device "a" { | |
| - new_field = "new_value" -> null | |
| - volume_type = "gp2" -> null | |
| } | |
| }`, | |
| }, | |
| "in-place update - unknown": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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"), | |
| "disks": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| }))), | |
| "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(), | |
| Schema: testSchemaPlus(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = { | |
| - "disk_a" = { | |
| - mount_point = "/var/diska" -> null | |
| - size = "50GB" -> null | |
| }, | |
| } -> (known after apply) | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged block hidden) | |
| }`, | |
| }, | |
| "in-place create - unknown block": { | |
| Action: plans.Create, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.NullVal(testSchemaPlus(configschema.NestingMap).ImpliedType()), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "diska": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| "new_field": cty.String, | |
| }))), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaPlus(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be created | |
| + resource "test_instance" "example" { | |
| + ami = "ami-AFTER" | |
| + disks = { | |
| + "diska" = { | |
| + mount_point = "/var/diska" | |
| + size = "50GB" | |
| }, | |
| } | |
| + id = "i-02ae66f368e8518a9" | |
| + root_block_device (known after apply) | |
| }`, | |
| }, | |
| "in-place update - unknown block": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "diska": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.MapVal(map[string]cty.Value{ | |
| "gp1": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp1"), | |
| "new_field": cty.StringVal("new_value"), | |
| }), | |
| "gp2": 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"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "diska": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| "new_field": cty.String, | |
| }))), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaPlus(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| ~ root_block_device (known after apply) | |
| - root_block_device "gp1" { | |
| - new_field = "new_value" -> null | |
| - volume_type = "gp1" -> null | |
| } | |
| - root_block_device "gp2" { | |
| - new_field = "new_value" -> null | |
| - volume_type = "gp2" -> null | |
| } | |
| }`, | |
| }, | |
| "in-place update - unknown block with replace": { | |
| Action: plans.CreateThenDelete, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "diska": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.MapVal(map[string]cty.Value{ | |
| "gp1": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp1"), | |
| "new_field": cty.StringVal("new_value"), | |
| }), | |
| "gp2": 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"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "diska": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| "new_field": cty.String, | |
| }))), | |
| }), | |
| RequiredReplace: cty.NewPathSet( | |
| cty.GetAttrPath("root_block_device").IndexString("gp1").GetAttr("volume_type"), | |
| cty.GetAttrPath("root_block_device").IndexString("gp2").GetAttr("volume_type"), | |
| ), | |
| Schema: testSchemaPlus(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| +/- resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| ~ root_block_device (known after apply) | |
| - root_block_device "gp1" { | |
| - new_field = "new_value" -> null | |
| - volume_type = "gp1" -> null # forces replacement | |
| } | |
| - root_block_device "gp2" { | |
| - new_field = "new_value" -> null | |
| - volume_type = "gp2" -> null # forces replacement | |
| } | |
| }`, | |
| }, | |
| "in-place update - insertion sensitive": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| })), | |
| "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"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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"), | |
| }), | |
| }), | |
| }), | |
| AfterSensitivePaths: []cty.Path{ | |
| cty.GetAttrPath("disks").IndexString("disk_a").GetAttr("mount_point"), | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaPlus(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = { | |
| + "disk_a" = { | |
| + mount_point = (sensitive value) | |
| + size = "50GB" | |
| }, | |
| } | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged block hidden) | |
| }`, | |
| }, | |
| "in-place update - multiple unchanged blocks": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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("gp2"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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("gp2"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchema(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| # (2 unchanged blocks hidden) | |
| }`, | |
| }, | |
| "in-place update - multiple blocks first changed": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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("gp2"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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("gp3"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchema(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| ~ root_block_device "b" { | |
| ~ volume_type = "gp2" -> "gp3" | |
| } | |
| # (1 unchanged block hidden) | |
| }`, | |
| }, | |
| "in-place update - multiple blocks second changed": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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("gp2"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.MapVal(map[string]cty.Value{ | |
| "a": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp3"), | |
| }), | |
| "b": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchema(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| ~ root_block_device "a" { | |
| ~ volume_type = "gp2" -> "gp3" | |
| } | |
| # (1 unchanged block hidden) | |
| }`, | |
| }, | |
| "in-place update - multiple blocks changed": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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("gp2"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.MapVal(map[string]cty.Value{ | |
| "a": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp3"), | |
| }), | |
| "b": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp3"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchema(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| ~ root_block_device "a" { | |
| ~ volume_type = "gp2" -> "gp3" | |
| } | |
| ~ root_block_device "b" { | |
| ~ volume_type = "gp2" -> "gp3" | |
| } | |
| }`, | |
| }, | |
| "in-place update - multiple different unchanged blocks": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.MapVal(map[string]cty.Value{ | |
| "a": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| }), | |
| "leaf_block_device": cty.MapVal(map[string]cty.Value{ | |
| "b": 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"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.MapVal(map[string]cty.Value{ | |
| "a": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| }), | |
| "leaf_block_device": cty.MapVal(map[string]cty.Value{ | |
| "b": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaMultipleBlocks(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| # (2 unchanged blocks hidden) | |
| }`, | |
| }, | |
| "in-place update - multiple different blocks first changed": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.MapVal(map[string]cty.Value{ | |
| "a": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| }), | |
| "leaf_block_device": cty.MapVal(map[string]cty.Value{ | |
| "b": 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"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.MapVal(map[string]cty.Value{ | |
| "a": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| }), | |
| "leaf_block_device": cty.MapVal(map[string]cty.Value{ | |
| "b": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp3"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaMultipleBlocks(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| ~ leaf_block_device "b" { | |
| ~ volume_type = "gp2" -> "gp3" | |
| } | |
| # (1 unchanged block hidden) | |
| }`, | |
| }, | |
| "in-place update - multiple different blocks second changed": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.MapVal(map[string]cty.Value{ | |
| "a": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| }), | |
| "leaf_block_device": cty.MapVal(map[string]cty.Value{ | |
| "b": 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"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.MapVal(map[string]cty.Value{ | |
| "a": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp3"), | |
| }), | |
| }), | |
| "leaf_block_device": cty.MapVal(map[string]cty.Value{ | |
| "b": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaMultipleBlocks(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| ~ root_block_device "a" { | |
| ~ volume_type = "gp2" -> "gp3" | |
| } | |
| # (1 unchanged block hidden) | |
| }`, | |
| }, | |
| "in-place update - multiple different blocks changed": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.MapVal(map[string]cty.Value{ | |
| "a": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| }), | |
| "leaf_block_device": cty.MapVal(map[string]cty.Value{ | |
| "b": 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"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "root_block_device": cty.MapVal(map[string]cty.Value{ | |
| "a": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp3"), | |
| }), | |
| }), | |
| "leaf_block_device": cty.MapVal(map[string]cty.Value{ | |
| "b": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp3"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaMultipleBlocks(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| ~ leaf_block_device "b" { | |
| ~ volume_type = "gp2" -> "gp3" | |
| } | |
| ~ root_block_device "a" { | |
| ~ volume_type = "gp2" -> "gp3" | |
| } | |
| }`, | |
| }, | |
| "in-place update - mixed blocks unchanged": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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("gp2"), | |
| }), | |
| }), | |
| "leaf_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("gp2"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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("gp2"), | |
| }), | |
| }), | |
| "leaf_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("gp2"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaMultipleBlocks(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| # (4 unchanged blocks hidden) | |
| }`, | |
| }, | |
| "in-place update - mixed blocks changed": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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("gp2"), | |
| }), | |
| }), | |
| "leaf_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("gp2"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| "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("gp3"), | |
| }), | |
| }), | |
| "leaf_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("gp3"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaMultipleBlocks(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| ~ leaf_block_device "b" { | |
| ~ volume_type = "gp2" -> "gp3" | |
| } | |
| ~ root_block_device "b" { | |
| ~ volume_type = "gp2" -> "gp3" | |
| } | |
| # (2 unchanged blocks hidden) | |
| }`, | |
| }, | |
| } | |
| runTestCases(t, testCases) | |
| } | |
| func TestResourceChange_nestedSingle(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.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| "disk": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "root_block_device": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| "disk": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchema(configschema.NestingSingle), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| # (1 unchanged block hidden) | |
| }`, | |
| }, | |
| "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.NullVal(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| })), | |
| "disk": cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| })), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disk": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| "root_block_device": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.NullVal(cty.String), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchema(configschema.NestingSingle), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| + disk = { | |
| + mount_point = "/var/diska" | |
| + size = "50GB" | |
| } | |
| id = "i-02ae66f368e8518a9" | |
| + root_block_device {} | |
| }`, | |
| }, | |
| "force-new update (inside blocks)": { | |
| Action: plans.DeleteThenCreate, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disk": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| "root_block_device": 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"), | |
| "disk": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diskb"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| "root_block_device": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("different"), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet( | |
| cty.Path{ | |
| cty.GetAttrStep{Name: "root_block_device"}, | |
| cty.GetAttrStep{Name: "volume_type"}, | |
| }, | |
| cty.Path{ | |
| cty.GetAttrStep{Name: "disk"}, | |
| cty.GetAttrStep{Name: "mount_point"}, | |
| }, | |
| ), | |
| Schema: testSchema(configschema.NestingSingle), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| -/+ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disk = { | |
| ~ mount_point = "/var/diska" -> "/var/diskb" # forces replacement | |
| # (1 unchanged attribute hidden) | |
| } | |
| id = "i-02ae66f368e8518a9" | |
| ~ root_block_device { | |
| ~ volume_type = "gp2" -> "different" # forces replacement | |
| } | |
| }`, | |
| }, | |
| "force-new update (whole block)": { | |
| Action: plans.DeleteThenCreate, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disk": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| "root_block_device": 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"), | |
| "disk": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diskb"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| "root_block_device": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("different"), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet( | |
| cty.Path{cty.GetAttrStep{Name: "root_block_device"}}, | |
| cty.Path{cty.GetAttrStep{Name: "disk"}}, | |
| ), | |
| Schema: testSchema(configschema.NestingSingle), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| -/+ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disk = { # forces replacement | |
| ~ mount_point = "/var/diska" -> "/var/diskb" | |
| # (1 unchanged attribute hidden) | |
| } | |
| 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"), | |
| "disk": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| "root_block_device": 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.NullVal(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| })), | |
| "disk": cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| })), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchema(configschema.NestingSingle), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| - disk = { | |
| - mount_point = "/var/diska" -> null | |
| - size = "50GB" -> null | |
| } -> null | |
| id = "i-02ae66f368e8518a9" | |
| - root_block_device { | |
| - volume_type = "gp2" -> null | |
| } | |
| }`, | |
| }, | |
| "with dynamically-typed attribute": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "block": cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "attr": cty.String, | |
| })), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "block": cty.ObjectVal(map[string]cty.Value{ | |
| "attr": cty.StringVal("foo"), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| 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.NestingSingle, | |
| }, | |
| }, | |
| }, | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| + block { | |
| + attr = "foo" | |
| } | |
| }`, | |
| }, | |
| "in-place update - unknown": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disk": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| "root_block_device": 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"), | |
| "disk": cty.UnknownVal(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| })), | |
| "root_block_device": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| "new_field": cty.StringVal("new_value"), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaPlus(configschema.NestingSingle), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disk = { | |
| ~ mount_point = "/var/diska" -> (known after apply) | |
| ~ size = "50GB" -> (known after apply) | |
| } -> (known after apply) | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged block hidden) | |
| }`, | |
| }, | |
| "in-place create - unknown block": { | |
| Action: plans.Create, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.NullVal(testSchemaPlus(configschema.NestingSingle).ImpliedType()), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disk": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| "root_block_device": cty.UnknownVal(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| "new_field": cty.String, | |
| })), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaPlus(configschema.NestingSingle), | |
| ExpectedOutput: ` # test_instance.example will be created | |
| + resource "test_instance" "example" { | |
| + ami = "ami-AFTER" | |
| + disk = { | |
| + mount_point = "/var/diska" | |
| + size = "50GB" | |
| } | |
| + id = "i-02ae66f368e8518a9" | |
| + root_block_device (known after apply) | |
| }`, | |
| }, | |
| "in-place update - unknown block": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disk": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| "root_block_device": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp1"), | |
| "new_field": cty.StringVal("new_value"), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disk": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| "root_block_device": cty.UnknownVal(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| "new_field": cty.String, | |
| })), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaPlus(configschema.NestingSingle), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| ~ root_block_device { | |
| ~ new_field = "new_value" -> (known after apply) | |
| ~ volume_type = "gp1" -> (known after apply) | |
| } -> (known after apply) | |
| }`, | |
| }, | |
| "in-place update - unknown block with replace": { | |
| Action: plans.CreateThenDelete, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disk": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| "root_block_device": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp1"), | |
| "new_field": cty.StringVal("new_value"), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disk": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| "root_block_device": cty.UnknownVal(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| "new_field": cty.String, | |
| })), | |
| }), | |
| RequiredReplace: cty.NewPathSet( | |
| cty.GetAttrPath("root_block_device").GetAttr("volume_type"), | |
| ), | |
| Schema: testSchemaPlus(configschema.NestingSingle), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| +/- resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| ~ root_block_device { | |
| ~ new_field = "new_value" -> (known after apply) | |
| ~ volume_type = "gp1" -> (known after apply) # forces replacement | |
| } -> (known after apply) | |
| }`, | |
| }, | |
| "in-place update - modification": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disk": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| "root_block_device": 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"), | |
| "disk": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("25GB"), | |
| }), | |
| "root_block_device": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| "new_field": cty.StringVal("new_value"), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaPlus(configschema.NestingSingle), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disk = { | |
| ~ size = "50GB" -> "25GB" | |
| # (1 unchanged attribute hidden) | |
| } | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged block hidden) | |
| }`, | |
| }, | |
| } | |
| runTestCases(t, testCases) | |
| } | |
| func TestResourceChange_nestedMapSensitiveSchema(t *testing.T) { | |
| testCases := map[string]testCase{ | |
| "creation from null": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.NullVal(cty.String), | |
| "ami": cty.NullVal(cty.String), | |
| "disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| }))), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.NullVal(cty.String), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaSensitive(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| + ami = "ami-AFTER" | |
| + disks = (sensitive value) | |
| + id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| "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"), | |
| "disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| })), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.NullVal(cty.String), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaSensitive(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = (sensitive value) | |
| id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| "force-new update (whole block)": { | |
| Action: plans.DeleteThenCreate, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("100GB"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet( | |
| cty.Path{cty.GetAttrStep{Name: "disks"}}, | |
| ), | |
| Schema: testSchemaSensitive(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| -/+ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = (sensitive value) # forces replacement | |
| id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| "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"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| }))), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaSensitive(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| - disks = (sensitive value) -> null | |
| id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| "in-place update - unknown": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.MapVal(map[string]cty.Value{ | |
| "disk_a": cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| }))), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaSensitive(configschema.NestingMap), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = (sensitive value) | |
| id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| } | |
| runTestCases(t, testCases) | |
| } | |
| func TestResourceChange_nestedListSensitiveSchema(t *testing.T) { | |
| testCases := map[string]testCase{ | |
| "creation from null": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.NullVal(cty.String), | |
| "ami": cty.NullVal(cty.String), | |
| "disks": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| }))), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.NullVal(cty.String), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaSensitive(configschema.NestingList), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| + ami = "ami-AFTER" | |
| + disks = (sensitive value) | |
| + id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| "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"), | |
| "disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| })), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.NullVal(cty.String), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaSensitive(configschema.NestingList), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = (sensitive value) | |
| id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| "force-new update (whole block)": { | |
| Action: plans.DeleteThenCreate, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("100GB"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet( | |
| cty.Path{cty.GetAttrStep{Name: "disks"}}, | |
| ), | |
| Schema: testSchemaSensitive(configschema.NestingList), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| -/+ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = (sensitive value) # forces replacement | |
| id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| "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"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| }))), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaSensitive(configschema.NestingList), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| - disks = (sensitive value) -> null | |
| id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| "in-place update - unknown": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| }))), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaSensitive(configschema.NestingList), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = (sensitive value) | |
| id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| } | |
| runTestCases(t, testCases) | |
| } | |
| func TestResourceChange_nestedSetSensitiveSchema(t *testing.T) { | |
| testCases := map[string]testCase{ | |
| "creation from null": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.NullVal(cty.String), | |
| "ami": cty.NullVal(cty.String), | |
| "disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| }))), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.NullVal(cty.String), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaSensitive(configschema.NestingSet), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| + ami = "ami-AFTER" | |
| + disks = (sensitive value) | |
| + id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| "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"), | |
| "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| })), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.NullVal(cty.String), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaSensitive(configschema.NestingSet), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = (sensitive value) | |
| id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| "force-new update (whole block)": { | |
| Action: plans.DeleteThenCreate, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("100GB"), | |
| }), | |
| }), | |
| }), | |
| RequiredReplace: cty.NewPathSet( | |
| cty.Path{cty.GetAttrStep{Name: "disks"}}, | |
| ), | |
| Schema: testSchemaSensitive(configschema.NestingSet), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| -/+ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = (sensitive value) # forces replacement | |
| id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| "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"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| }))), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaSensitive(configschema.NestingSet), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| - disks = (sensitive value) -> null | |
| id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| "in-place update - unknown": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "disks": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "mount_point": cty.StringVal("/var/diska"), | |
| "size": cty.StringVal("50GB"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "disks": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.String, | |
| }))), | |
| }), | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: testSchemaSensitive(configschema.NestingSet), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = "ami-BEFORE" -> "ami-AFTER" | |
| ~ disks = (sensitive value) | |
| id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| } | |
| runTestCases(t, testCases) | |
| } | |
| func TestResourceChange_actionReason(t *testing.T) { | |
| emptySchema := &configschema.Block{} | |
| nullVal := cty.NullVal(cty.EmptyObject) | |
| emptyVal := cty.EmptyObjectVal | |
| testCases := map[string]testCase{ | |
| "delete for no particular reason": { | |
| Action: plans.Delete, | |
| ActionReason: plans.ResourceInstanceChangeNoReason, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: emptyVal, | |
| After: nullVal, | |
| Schema: emptySchema, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example will be destroyed | |
| - resource "test_instance" "example" {}`, | |
| }, | |
| "delete because of wrong repetition mode (NoKey)": { | |
| Action: plans.Delete, | |
| ActionReason: plans.ResourceInstanceDeleteBecauseWrongRepetition, | |
| Mode: addrs.ManagedResourceMode, | |
| InstanceKey: addrs.NoKey, | |
| Before: emptyVal, | |
| After: nullVal, | |
| Schema: emptySchema, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example will be destroyed | |
| # (because resource uses count or for_each) | |
| - resource "test_instance" "example" {}`, | |
| }, | |
| "delete because of wrong repetition mode (IntKey)": { | |
| Action: plans.Delete, | |
| ActionReason: plans.ResourceInstanceDeleteBecauseWrongRepetition, | |
| Mode: addrs.ManagedResourceMode, | |
| InstanceKey: addrs.IntKey(1), | |
| Before: emptyVal, | |
| After: nullVal, | |
| Schema: emptySchema, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example[1] will be destroyed | |
| # (because resource does not use count) | |
| - resource "test_instance" "example" {}`, | |
| }, | |
| "delete because of wrong repetition mode (StringKey)": { | |
| Action: plans.Delete, | |
| ActionReason: plans.ResourceInstanceDeleteBecauseWrongRepetition, | |
| Mode: addrs.ManagedResourceMode, | |
| InstanceKey: addrs.StringKey("a"), | |
| Before: emptyVal, | |
| After: nullVal, | |
| Schema: emptySchema, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example["a"] will be destroyed | |
| # (because resource does not use for_each) | |
| - resource "test_instance" "example" {}`, | |
| }, | |
| "delete because no resource configuration": { | |
| Action: plans.Delete, | |
| ActionReason: plans.ResourceInstanceDeleteBecauseNoResourceConfig, | |
| ModuleInst: addrs.RootModuleInstance.Child("foo", addrs.NoKey), | |
| Mode: addrs.ManagedResourceMode, | |
| Before: emptyVal, | |
| After: nullVal, | |
| Schema: emptySchema, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # module.foo.test_instance.example will be destroyed | |
| # (because test_instance.example is not in configuration) | |
| - resource "test_instance" "example" {}`, | |
| }, | |
| "delete because no module": { | |
| Action: plans.Delete, | |
| ActionReason: plans.ResourceInstanceDeleteBecauseNoModule, | |
| ModuleInst: addrs.RootModuleInstance.Child("foo", addrs.IntKey(1)), | |
| Mode: addrs.ManagedResourceMode, | |
| Before: emptyVal, | |
| After: nullVal, | |
| Schema: emptySchema, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # module.foo[1].test_instance.example will be destroyed | |
| # (because module.foo[1] is not in configuration) | |
| - resource "test_instance" "example" {}`, | |
| }, | |
| "delete because out of range for count": { | |
| Action: plans.Delete, | |
| ActionReason: plans.ResourceInstanceDeleteBecauseCountIndex, | |
| Mode: addrs.ManagedResourceMode, | |
| InstanceKey: addrs.IntKey(1), | |
| Before: emptyVal, | |
| After: nullVal, | |
| Schema: emptySchema, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example[1] will be destroyed | |
| # (because index [1] is out of range for count) | |
| - resource "test_instance" "example" {}`, | |
| }, | |
| "delete because out of range for for_each": { | |
| Action: plans.Delete, | |
| ActionReason: plans.ResourceInstanceDeleteBecauseEachKey, | |
| Mode: addrs.ManagedResourceMode, | |
| InstanceKey: addrs.StringKey("boop"), | |
| Before: emptyVal, | |
| After: nullVal, | |
| Schema: emptySchema, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example["boop"] will be destroyed | |
| # (because key ["boop"] is not in for_each map) | |
| - resource "test_instance" "example" {}`, | |
| }, | |
| "replace for no particular reason (delete first)": { | |
| Action: plans.DeleteThenCreate, | |
| ActionReason: plans.ResourceInstanceChangeNoReason, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: emptyVal, | |
| After: nullVal, | |
| Schema: emptySchema, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| -/+ resource "test_instance" "example" {}`, | |
| }, | |
| "replace for no particular reason (create first)": { | |
| Action: plans.CreateThenDelete, | |
| ActionReason: plans.ResourceInstanceChangeNoReason, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: emptyVal, | |
| After: nullVal, | |
| Schema: emptySchema, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| +/- resource "test_instance" "example" {}`, | |
| }, | |
| "replace by request (delete first)": { | |
| Action: plans.DeleteThenCreate, | |
| ActionReason: plans.ResourceInstanceReplaceByRequest, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: emptyVal, | |
| After: nullVal, | |
| Schema: emptySchema, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example will be replaced, as requested | |
| -/+ resource "test_instance" "example" {}`, | |
| }, | |
| "replace by request (create first)": { | |
| Action: plans.CreateThenDelete, | |
| ActionReason: plans.ResourceInstanceReplaceByRequest, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: emptyVal, | |
| After: nullVal, | |
| Schema: emptySchema, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example will be replaced, as requested | |
| +/- resource "test_instance" "example" {}`, | |
| }, | |
| "replace because tainted (delete first)": { | |
| Action: plans.DeleteThenCreate, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseTainted, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: emptyVal, | |
| After: nullVal, | |
| Schema: emptySchema, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example is tainted, so must be replaced | |
| -/+ resource "test_instance" "example" {}`, | |
| }, | |
| "replace because tainted (create first)": { | |
| Action: plans.CreateThenDelete, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseTainted, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: emptyVal, | |
| After: nullVal, | |
| Schema: emptySchema, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example is tainted, so must be replaced | |
| +/- resource "test_instance" "example" {}`, | |
| }, | |
| "replace because cannot update (delete first)": { | |
| Action: plans.DeleteThenCreate, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: emptyVal, | |
| After: nullVal, | |
| Schema: emptySchema, | |
| RequiredReplace: cty.NewPathSet(), | |
| // This one has no special message, because the fuller explanation | |
| // typically appears inline as a "# forces replacement" comment. | |
| // (not shown here) | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| -/+ resource "test_instance" "example" {}`, | |
| }, | |
| "replace because cannot update (create first)": { | |
| Action: plans.CreateThenDelete, | |
| ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: emptyVal, | |
| After: nullVal, | |
| Schema: emptySchema, | |
| RequiredReplace: cty.NewPathSet(), | |
| // This one has no special message, because the fuller explanation | |
| // typically appears inline as a "# forces replacement" comment. | |
| // (not shown here) | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| +/- resource "test_instance" "example" {}`, | |
| }, | |
| } | |
| runTestCases(t, testCases) | |
| } | |
| func TestResourceChange_sensitiveVariable(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.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-123"), | |
| "map_key": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.NumberIntVal(800), | |
| "dinner": cty.NumberIntVal(2000), | |
| }), | |
| "map_whole": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.StringVal("pizza"), | |
| "dinner": cty.StringVal("pizza"), | |
| }), | |
| "list_field": cty.ListVal([]cty.Value{ | |
| cty.StringVal("hello"), | |
| cty.StringVal("friends"), | |
| cty.StringVal("!"), | |
| }), | |
| "nested_block_list": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "an_attr": cty.StringVal("secretval"), | |
| "another": cty.StringVal("not secret"), | |
| }), | |
| }), | |
| "nested_block_set": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "an_attr": cty.StringVal("secretval"), | |
| "another": cty.StringVal("not secret"), | |
| }), | |
| }), | |
| }), | |
| AfterSensitivePaths: []cty.Path{ | |
| cty.Path{cty.GetAttrStep{Name: "ami"}}, | |
| cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}}, | |
| cty.Path{cty.GetAttrStep{Name: "map_whole"}}, | |
| cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, | |
| // Nested blocks/sets will mark the whole set/block as sensitive | |
| cty.Path{cty.GetAttrStep{Name: "nested_block_list"}}, | |
| cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| "ami": {Type: cty.String, Optional: true}, | |
| "map_whole": {Type: cty.Map(cty.String), Optional: true}, | |
| "map_key": {Type: cty.Map(cty.Number), Optional: true}, | |
| "list_field": {Type: cty.List(cty.String), Optional: true}, | |
| }, | |
| BlockTypes: map[string]*configschema.NestedBlock{ | |
| "nested_block_list": { | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "an_attr": {Type: cty.String, Optional: true}, | |
| "another": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| Nesting: configschema.NestingList, | |
| }, | |
| "nested_block_set": { | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "an_attr": {Type: cty.String, Optional: true}, | |
| "another": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| Nesting: configschema.NestingSet, | |
| }, | |
| }, | |
| }, | |
| ExpectedOutput: ` # test_instance.example will be created | |
| + resource "test_instance" "example" { | |
| + ami = (sensitive value) | |
| + id = "i-02ae66f368e8518a9" | |
| + list_field = [ | |
| + "hello", | |
| + (sensitive value), | |
| + "!", | |
| ] | |
| + map_key = { | |
| + "breakfast" = 800 | |
| + "dinner" = (sensitive value) | |
| } | |
| + map_whole = (sensitive value) | |
| + nested_block_list { | |
| # At least one attribute in this block is (or was) sensitive, | |
| # so its contents will not be displayed. | |
| } | |
| + nested_block_set { | |
| # At least one attribute in this block is (or was) sensitive, | |
| # so its contents will not be displayed. | |
| } | |
| }`, | |
| }, | |
| "in-place update - before sensitive": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "special": cty.BoolVal(true), | |
| "some_number": cty.NumberIntVal(1), | |
| "list_field": cty.ListVal([]cty.Value{ | |
| cty.StringVal("hello"), | |
| cty.StringVal("friends"), | |
| cty.StringVal("!"), | |
| }), | |
| "map_key": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.NumberIntVal(800), | |
| "dinner": cty.NumberIntVal(2000), // sensitive key | |
| }), | |
| "map_whole": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.StringVal("pizza"), | |
| "dinner": cty.StringVal("pizza"), | |
| }), | |
| "nested_block": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "an_attr": cty.StringVal("secretval"), | |
| }), | |
| }), | |
| "nested_block_set": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "an_attr": cty.StringVal("secretval"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "special": cty.BoolVal(false), | |
| "some_number": cty.NumberIntVal(2), | |
| "list_field": cty.ListVal([]cty.Value{ | |
| cty.StringVal("hello"), | |
| cty.StringVal("friends"), | |
| cty.StringVal("."), | |
| }), | |
| "map_key": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.NumberIntVal(800), | |
| "dinner": cty.NumberIntVal(1900), | |
| }), | |
| "map_whole": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.StringVal("cereal"), | |
| "dinner": cty.StringVal("pizza"), | |
| }), | |
| "nested_block": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "an_attr": cty.StringVal("changed"), | |
| }), | |
| }), | |
| "nested_block_set": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "an_attr": cty.StringVal("changed"), | |
| }), | |
| }), | |
| }), | |
| BeforeSensitivePaths: []cty.Path{ | |
| cty.Path{cty.GetAttrStep{Name: "ami"}}, | |
| cty.Path{cty.GetAttrStep{Name: "special"}}, | |
| cty.Path{cty.GetAttrStep{Name: "some_number"}}, | |
| cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}}, | |
| cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, | |
| cty.Path{cty.GetAttrStep{Name: "map_whole"}}, | |
| cty.Path{cty.GetAttrStep{Name: "nested_block"}}, | |
| cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| 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}, | |
| "special": {Type: cty.Bool, Optional: true}, | |
| "some_number": {Type: cty.Number, Optional: true}, | |
| "map_key": {Type: cty.Map(cty.Number), Optional: true}, | |
| "map_whole": {Type: cty.Map(cty.String), Optional: true}, | |
| }, | |
| BlockTypes: map[string]*configschema.NestedBlock{ | |
| "nested_block": { | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "an_attr": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| Nesting: configschema.NestingList, | |
| }, | |
| "nested_block_set": { | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "an_attr": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| Nesting: configschema.NestingSet, | |
| }, | |
| }, | |
| }, | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| # Warning: this attribute value will no longer be marked as sensitive | |
| # after applying this change. | |
| ~ ami = (sensitive value) | |
| id = "i-02ae66f368e8518a9" | |
| ~ list_field = [ | |
| # (1 unchanged element hidden) | |
| "friends", | |
| # Warning: this attribute value will no longer be marked as sensitive | |
| # after applying this change. | |
| ~ (sensitive value), | |
| ] | |
| ~ map_key = { | |
| # Warning: this attribute value will no longer be marked as sensitive | |
| # after applying this change. | |
| ~ "dinner" = (sensitive value) | |
| # (1 unchanged element hidden) | |
| } | |
| # Warning: this attribute value will no longer be marked as sensitive | |
| # after applying this change. | |
| ~ map_whole = (sensitive value) | |
| # Warning: this attribute value will no longer be marked as sensitive | |
| # after applying this change. | |
| ~ some_number = (sensitive value) | |
| # Warning: this attribute value will no longer be marked as sensitive | |
| # after applying this change. | |
| ~ special = (sensitive value) | |
| # Warning: this block will no longer be marked as sensitive | |
| # after applying this change. | |
| ~ nested_block { | |
| # At least one attribute in this block is (or was) sensitive, | |
| # so its contents will not be displayed. | |
| } | |
| - nested_block_set { | |
| # At least one attribute in this block is (or was) sensitive, | |
| # so its contents will not be displayed. | |
| } | |
| + nested_block_set { | |
| + an_attr = "changed" | |
| } | |
| }`, | |
| }, | |
| "in-place update - after sensitive": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "list_field": cty.ListVal([]cty.Value{ | |
| cty.StringVal("hello"), | |
| cty.StringVal("friends"), | |
| }), | |
| "map_key": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.NumberIntVal(800), | |
| "dinner": cty.NumberIntVal(2000), // sensitive key | |
| }), | |
| "map_whole": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.StringVal("pizza"), | |
| "dinner": cty.StringVal("pizza"), | |
| }), | |
| "nested_block_single": cty.ObjectVal(map[string]cty.Value{ | |
| "an_attr": cty.StringVal("original"), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "list_field": cty.ListVal([]cty.Value{ | |
| cty.StringVal("goodbye"), | |
| cty.StringVal("friends"), | |
| }), | |
| "map_key": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.NumberIntVal(700), | |
| "dinner": cty.NumberIntVal(2100), // sensitive key | |
| }), | |
| "map_whole": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.StringVal("cereal"), | |
| "dinner": cty.StringVal("pizza"), | |
| }), | |
| "nested_block_single": cty.ObjectVal(map[string]cty.Value{ | |
| "an_attr": cty.StringVal("changed"), | |
| }), | |
| }), | |
| AfterSensitivePaths: []cty.Path{ | |
| cty.Path{cty.GetAttrStep{Name: "tags"}, cty.IndexStep{Key: cty.StringVal("address")}}, | |
| cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, | |
| cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, | |
| cty.Path{cty.GetAttrStep{Name: "map_whole"}}, | |
| cty.Path{cty.GetAttrStep{Name: "nested_block_single"}}, | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| "list_field": {Type: cty.List(cty.String), Optional: true}, | |
| "map_key": {Type: cty.Map(cty.Number), Optional: true}, | |
| "map_whole": {Type: cty.Map(cty.String), Optional: true}, | |
| }, | |
| BlockTypes: map[string]*configschema.NestedBlock{ | |
| "nested_block_single": { | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "an_attr": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| Nesting: configschema.NestingSingle, | |
| }, | |
| }, | |
| }, | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| id = "i-02ae66f368e8518a9" | |
| ~ list_field = [ | |
| # Warning: this attribute value will be marked as sensitive and will not | |
| # display in UI output after applying this change. | |
| ~ (sensitive value), | |
| "friends", | |
| ] | |
| ~ map_key = { | |
| ~ "breakfast" = 800 -> 700 | |
| # Warning: this attribute value will be marked as sensitive and will not | |
| # display in UI output after applying this change. | |
| ~ "dinner" = (sensitive value) | |
| } | |
| # Warning: this attribute value will be marked as sensitive and will not | |
| # display in UI output after applying this change. | |
| ~ map_whole = (sensitive value) | |
| # Warning: this block will be marked as sensitive and will not | |
| # display in UI output after applying this change. | |
| ~ nested_block_single { | |
| # At least one attribute in this block is (or was) sensitive, | |
| # so its contents will not be displayed. | |
| } | |
| }`, | |
| }, | |
| "in-place update - both sensitive": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "list_field": cty.ListVal([]cty.Value{ | |
| cty.StringVal("hello"), | |
| cty.StringVal("friends"), | |
| }), | |
| "map_key": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.NumberIntVal(800), | |
| "dinner": cty.NumberIntVal(2000), // sensitive key | |
| }), | |
| "map_whole": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.StringVal("pizza"), | |
| "dinner": cty.StringVal("pizza"), | |
| }), | |
| "nested_block_map": cty.MapVal(map[string]cty.Value{ | |
| "foo": cty.ObjectVal(map[string]cty.Value{ | |
| "an_attr": cty.StringVal("original"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "list_field": cty.ListVal([]cty.Value{ | |
| cty.StringVal("goodbye"), | |
| cty.StringVal("friends"), | |
| }), | |
| "map_key": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.NumberIntVal(800), | |
| "dinner": cty.NumberIntVal(1800), // sensitive key | |
| }), | |
| "map_whole": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.StringVal("cereal"), | |
| "dinner": cty.StringVal("pizza"), | |
| }), | |
| "nested_block_map": cty.MapVal(map[string]cty.Value{ | |
| "foo": cty.ObjectVal(map[string]cty.Value{ | |
| "an_attr": cty.UnknownVal(cty.String), | |
| }), | |
| }), | |
| }), | |
| BeforeSensitivePaths: []cty.Path{ | |
| cty.Path{cty.GetAttrStep{Name: "ami"}}, | |
| cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, | |
| cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, | |
| cty.Path{cty.GetAttrStep{Name: "map_whole"}}, | |
| cty.Path{cty.GetAttrStep{Name: "nested_block_map"}}, | |
| }, | |
| AfterSensitivePaths: []cty.Path{ | |
| cty.Path{cty.GetAttrStep{Name: "ami"}}, | |
| cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, | |
| cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, | |
| cty.Path{cty.GetAttrStep{Name: "map_whole"}}, | |
| cty.Path{cty.GetAttrStep{Name: "nested_block_map"}}, | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| 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}, | |
| "map_key": {Type: cty.Map(cty.Number), Optional: true}, | |
| "map_whole": {Type: cty.Map(cty.String), Optional: true}, | |
| }, | |
| BlockTypes: map[string]*configschema.NestedBlock{ | |
| "nested_block_map": { | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "an_attr": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| Nesting: configschema.NestingMap, | |
| }, | |
| }, | |
| }, | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ ami = (sensitive value) | |
| id = "i-02ae66f368e8518a9" | |
| ~ list_field = [ | |
| ~ (sensitive value), | |
| "friends", | |
| ] | |
| ~ map_key = { | |
| ~ "dinner" = (sensitive value) | |
| # (1 unchanged element hidden) | |
| } | |
| ~ map_whole = (sensitive value) | |
| ~ nested_block_map "foo" { | |
| # At least one attribute in this block is (or was) sensitive, | |
| # so its contents will not be displayed. | |
| } | |
| }`, | |
| }, | |
| "in-place update - value unchanged, sensitivity changes": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "special": cty.BoolVal(true), | |
| "some_number": cty.NumberIntVal(1), | |
| "list_field": cty.ListVal([]cty.Value{ | |
| cty.StringVal("hello"), | |
| cty.StringVal("friends"), | |
| cty.StringVal("!"), | |
| }), | |
| "map_key": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.NumberIntVal(800), | |
| "dinner": cty.NumberIntVal(2000), // sensitive key | |
| }), | |
| "map_whole": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.StringVal("pizza"), | |
| "dinner": cty.StringVal("pizza"), | |
| }), | |
| "nested_block": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "an_attr": cty.StringVal("secretval"), | |
| }), | |
| }), | |
| "nested_block_set": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "an_attr": cty.StringVal("secretval"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "special": cty.BoolVal(true), | |
| "some_number": cty.NumberIntVal(1), | |
| "list_field": cty.ListVal([]cty.Value{ | |
| cty.StringVal("hello"), | |
| cty.StringVal("friends"), | |
| cty.StringVal("!"), | |
| }), | |
| "map_key": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.NumberIntVal(800), | |
| "dinner": cty.NumberIntVal(2000), // sensitive key | |
| }), | |
| "map_whole": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.StringVal("pizza"), | |
| "dinner": cty.StringVal("pizza"), | |
| }), | |
| "nested_block": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "an_attr": cty.StringVal("secretval"), | |
| }), | |
| }), | |
| "nested_block_set": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "an_attr": cty.StringVal("secretval"), | |
| }), | |
| }), | |
| }), | |
| BeforeSensitivePaths: []cty.Path{ | |
| cty.Path{cty.GetAttrStep{Name: "ami"}}, | |
| cty.Path{cty.GetAttrStep{Name: "special"}}, | |
| cty.Path{cty.GetAttrStep{Name: "some_number"}}, | |
| cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}}, | |
| cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, | |
| cty.Path{cty.GetAttrStep{Name: "map_whole"}}, | |
| cty.Path{cty.GetAttrStep{Name: "nested_block"}}, | |
| cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| 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}, | |
| "special": {Type: cty.Bool, Optional: true}, | |
| "some_number": {Type: cty.Number, Optional: true}, | |
| "map_key": {Type: cty.Map(cty.Number), Optional: true}, | |
| "map_whole": {Type: cty.Map(cty.String), Optional: true}, | |
| }, | |
| BlockTypes: map[string]*configschema.NestedBlock{ | |
| "nested_block": { | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "an_attr": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| Nesting: configschema.NestingList, | |
| }, | |
| "nested_block_set": { | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "an_attr": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| Nesting: configschema.NestingSet, | |
| }, | |
| }, | |
| }, | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| # Warning: this attribute value will no longer be marked as sensitive | |
| # after applying this change. The value is unchanged. | |
| ~ ami = (sensitive value) | |
| id = "i-02ae66f368e8518a9" | |
| ~ list_field = [ | |
| # (1 unchanged element hidden) | |
| "friends", | |
| # Warning: this attribute value will no longer be marked as sensitive | |
| # after applying this change. The value is unchanged. | |
| ~ (sensitive value), | |
| ] | |
| ~ map_key = { | |
| # Warning: this attribute value will no longer be marked as sensitive | |
| # after applying this change. The value is unchanged. | |
| ~ "dinner" = (sensitive value) | |
| # (1 unchanged element hidden) | |
| } | |
| # Warning: this attribute value will no longer be marked as sensitive | |
| # after applying this change. The value is unchanged. | |
| ~ map_whole = (sensitive value) | |
| # Warning: this attribute value will no longer be marked as sensitive | |
| # after applying this change. The value is unchanged. | |
| ~ some_number = (sensitive value) | |
| # Warning: this attribute value will no longer be marked as sensitive | |
| # after applying this change. The value is unchanged. | |
| ~ special = (sensitive value) | |
| # Warning: this block will no longer be marked as sensitive | |
| # after applying this change. | |
| ~ nested_block { | |
| # At least one attribute in this block is (or was) sensitive, | |
| # so its contents will not be displayed. | |
| } | |
| # Warning: this block will no longer be marked as sensitive | |
| # after applying this change. | |
| ~ nested_block_set { | |
| # At least one attribute in this block is (or was) sensitive, | |
| # so its contents will not be displayed. | |
| } | |
| }`, | |
| }, | |
| "deletion": { | |
| Action: plans.Delete, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "list_field": cty.ListVal([]cty.Value{ | |
| cty.StringVal("hello"), | |
| cty.StringVal("friends"), | |
| }), | |
| "map_key": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.NumberIntVal(800), | |
| "dinner": cty.NumberIntVal(2000), // sensitive key | |
| }), | |
| "map_whole": cty.MapVal(map[string]cty.Value{ | |
| "breakfast": cty.StringVal("pizza"), | |
| "dinner": cty.StringVal("pizza"), | |
| }), | |
| "nested_block": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "an_attr": cty.StringVal("secret"), | |
| "another": cty.StringVal("not secret"), | |
| }), | |
| }), | |
| "nested_block_set": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "an_attr": cty.StringVal("secret"), | |
| "another": cty.StringVal("not secret"), | |
| }), | |
| }), | |
| }), | |
| After: cty.NullVal(cty.EmptyObject), | |
| BeforeSensitivePaths: []cty.Path{ | |
| cty.Path{cty.GetAttrStep{Name: "ami"}}, | |
| cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}}, | |
| cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, | |
| cty.Path{cty.GetAttrStep{Name: "map_whole"}}, | |
| cty.Path{cty.GetAttrStep{Name: "nested_block"}}, | |
| cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| 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}, | |
| "map_key": {Type: cty.Map(cty.Number), Optional: true}, | |
| "map_whole": {Type: cty.Map(cty.String), Optional: true}, | |
| }, | |
| BlockTypes: map[string]*configschema.NestedBlock{ | |
| "nested_block_set": { | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "an_attr": {Type: cty.String, Optional: true}, | |
| "another": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| Nesting: configschema.NestingSet, | |
| }, | |
| }, | |
| }, | |
| ExpectedOutput: ` # test_instance.example will be destroyed | |
| - resource "test_instance" "example" { | |
| - ami = (sensitive value) -> null | |
| - id = "i-02ae66f368e8518a9" -> null | |
| - list_field = [ | |
| - "hello", | |
| - (sensitive value), | |
| ] -> null | |
| - map_key = { | |
| - "breakfast" = 800 | |
| - "dinner" = (sensitive value) | |
| } -> null | |
| - map_whole = (sensitive value) -> null | |
| - nested_block_set { | |
| # At least one attribute in this block is (or was) sensitive, | |
| # so its contents will not be displayed. | |
| } | |
| }`, | |
| }, | |
| "update with sensitive value forcing replacement": { | |
| Action: plans.DeleteThenCreate, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-BEFORE"), | |
| "nested_block_set": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "an_attr": cty.StringVal("secret"), | |
| }), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "ami": cty.StringVal("ami-AFTER"), | |
| "nested_block_set": cty.SetVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "an_attr": cty.StringVal("changed"), | |
| }), | |
| }), | |
| }), | |
| BeforeSensitivePaths: []cty.Path{ | |
| cty.GetAttrPath("ami"), | |
| cty.GetAttrPath("nested_block_set"), | |
| }, | |
| AfterSensitivePaths: []cty.Path{ | |
| cty.GetAttrPath("ami"), | |
| cty.GetAttrPath("nested_block_set"), | |
| }, | |
| 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{ | |
| "nested_block_set": { | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "an_attr": {Type: cty.String, Required: true}, | |
| }, | |
| }, | |
| Nesting: configschema.NestingSet, | |
| }, | |
| }, | |
| }, | |
| RequiredReplace: cty.NewPathSet( | |
| cty.GetAttrPath("ami"), | |
| cty.GetAttrPath("nested_block_set"), | |
| ), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| -/+ resource "test_instance" "example" { | |
| ~ ami = (sensitive value) # forces replacement | |
| id = "i-02ae66f368e8518a9" | |
| - nested_block_set { # forces replacement | |
| # At least one attribute in this block is (or was) sensitive, | |
| # so its contents will not be displayed. | |
| } | |
| + nested_block_set { # forces replacement | |
| # At least one attribute in this block is (or was) sensitive, | |
| # so its contents will not be displayed. | |
| } | |
| }`, | |
| }, | |
| "update with sensitive attribute forcing replacement": { | |
| 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, Computed: true, Sensitive: true}, | |
| }, | |
| }, | |
| RequiredReplace: cty.NewPathSet( | |
| cty.GetAttrPath("ami"), | |
| ), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| -/+ resource "test_instance" "example" { | |
| ~ ami = (sensitive value) # forces replacement | |
| id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| "update with sensitive nested type attribute forcing replacement": { | |
| Action: plans.DeleteThenCreate, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "conn_info": cty.ObjectVal(map[string]cty.Value{ | |
| "user": cty.StringVal("not-secret"), | |
| "password": cty.StringVal("top-secret"), | |
| }), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "conn_info": cty.ObjectVal(map[string]cty.Value{ | |
| "user": cty.StringVal("not-secret"), | |
| "password": cty.StringVal("new-secret"), | |
| }), | |
| }), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| "conn_info": { | |
| NestedType: &configschema.Object{ | |
| Nesting: configschema.NestingSingle, | |
| Attributes: map[string]*configschema.Attribute{ | |
| "user": {Type: cty.String, Optional: true}, | |
| "password": {Type: cty.String, Optional: true, Sensitive: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| RequiredReplace: cty.NewPathSet( | |
| cty.GetAttrPath("conn_info"), | |
| cty.GetAttrPath("password"), | |
| ), | |
| ExpectedOutput: ` # test_instance.example must be replaced | |
| -/+ resource "test_instance" "example" { | |
| ~ conn_info = { # forces replacement | |
| ~ password = (sensitive value) | |
| # (1 unchanged attribute hidden) | |
| } | |
| id = "i-02ae66f368e8518a9" | |
| }`, | |
| }, | |
| } | |
| runTestCases(t, testCases) | |
| } | |
| func TestResourceChange_writeOnlyAttributes(t *testing.T) { | |
| testCases := map[string]testCase{ | |
| "create": { | |
| Action: plans.Create, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| "write_only": cty.String, | |
| "conn_info": cty.Object(map[string]cty.Type{ | |
| "user": cty.String, | |
| "write_only_set_password": cty.String, | |
| }), | |
| "block": cty.List(cty.Object(map[string]cty.Type{ | |
| "user": cty.String, | |
| "block_set_password": cty.String, | |
| })), | |
| })), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "write_only": cty.NullVal(cty.String), | |
| "conn_info": cty.ObjectVal(map[string]cty.Value{ | |
| "user": cty.StringVal("not-secret"), | |
| "write_only_set_password": cty.NullVal(cty.String), | |
| }), | |
| "block": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ | |
| "user": cty.StringVal("this-is-not-secret"), | |
| "block_set_password": cty.NullVal(cty.String), | |
| })}), | |
| }), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| "write_only": {Type: cty.String, WriteOnly: true}, | |
| "conn_info": { | |
| NestedType: &configschema.Object{ | |
| Nesting: configschema.NestingSingle, | |
| Attributes: map[string]*configschema.Attribute{ | |
| "user": {Type: cty.String, Optional: true}, | |
| "write_only_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| BlockTypes: map[string]*configschema.NestedBlock{ | |
| "block": { | |
| Nesting: configschema.NestingList, | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "user": {Type: cty.String, Optional: true}, | |
| "block_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| ExpectedOutput: ` # test_instance.example will be created | |
| + resource "test_instance" "example" { | |
| + conn_info = { | |
| + user = "not-secret" | |
| + write_only_set_password = (sensitive, write-only attribute) | |
| } | |
| + id = "i-02ae66f368e8518a9" | |
| + write_only = (write-only attribute) | |
| + block { | |
| + block_set_password = (write-only attribute) | |
| + user = "this-is-not-secret" | |
| } | |
| }`, | |
| }, | |
| "update attribute": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "write_only": cty.NullVal(cty.String), | |
| "conn_info": cty.ObjectVal(map[string]cty.Value{ | |
| "user": cty.StringVal("not-secret"), | |
| "write_only_set_password": cty.NullVal(cty.String), | |
| }), | |
| "block": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ | |
| "user": cty.StringVal("not-so-secret"), | |
| "block_set_password": cty.NullVal(cty.String), | |
| })}), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a10"), | |
| "write_only": cty.NullVal(cty.String), | |
| "conn_info": cty.ObjectVal(map[string]cty.Value{ | |
| "user": cty.StringVal("not-secret"), | |
| "write_only_set_password": cty.NullVal(cty.String), | |
| }), | |
| "block": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ | |
| "user": cty.StringVal("not-so-secret"), | |
| "block_set_password": cty.NullVal(cty.String), | |
| })}), | |
| }), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| "write_only": {Type: cty.String, WriteOnly: true}, | |
| "conn_info": { | |
| NestedType: &configschema.Object{ | |
| Nesting: configschema.NestingSingle, | |
| Attributes: map[string]*configschema.Attribute{ | |
| "user": {Type: cty.String, Optional: true}, | |
| "write_only_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| BlockTypes: map[string]*configschema.NestedBlock{ | |
| "block": { | |
| Nesting: configschema.NestingList, | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "user": {Type: cty.String, Optional: true}, | |
| "block_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| ~ id = "i-02ae66f368e8518a9" -> "i-02ae66f368e8518a10" | |
| # (2 unchanged attributes hidden) | |
| # (1 unchanged block hidden) | |
| }`, | |
| }, | |
| "update - delete block": { | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "write_only": cty.NullVal(cty.String), | |
| "conn_info": cty.ObjectVal(map[string]cty.Value{ | |
| "user": cty.StringVal("not-secret"), | |
| "write_only_set_password": cty.NullVal(cty.String), | |
| }), | |
| "block": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ | |
| "user": cty.StringVal("not-secret"), | |
| "block_set_password": cty.NullVal(cty.String), | |
| })}), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "write_only": cty.NullVal(cty.String), | |
| "conn_info": cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "user": cty.String, | |
| "write_only_set_password": cty.String, | |
| })), | |
| "block": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ | |
| "user": cty.String, | |
| "block_set_password": cty.String, | |
| }))), | |
| }), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| "write_only": {Type: cty.String, WriteOnly: true}, | |
| "conn_info": { | |
| NestedType: &configschema.Object{ | |
| Nesting: configschema.NestingSingle, | |
| Attributes: map[string]*configschema.Attribute{ | |
| "user": {Type: cty.String, Optional: true}, | |
| "write_only_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| BlockTypes: map[string]*configschema.NestedBlock{ | |
| "block": { | |
| Nesting: configschema.NestingList, | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "user": {Type: cty.String, Optional: true}, | |
| "block_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| ~ resource "test_instance" "example" { | |
| - conn_info = { | |
| - user = "not-secret" -> null | |
| - write_only_set_password = (sensitive, write-only attribute) -> null | |
| } -> null | |
| id = "i-02ae66f368e8518a9" | |
| # (1 unchanged attribute hidden) | |
| - block { | |
| - block_set_password = (write-only attribute) -> null | |
| - user = "not-secret" -> null | |
| } | |
| }`, | |
| }, | |
| "delete": { | |
| Action: plans.Delete, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("i-02ae66f368e8518a9"), | |
| "write_only": cty.NullVal(cty.String), | |
| "conn_info": cty.ObjectVal(map[string]cty.Value{ | |
| "user": cty.StringVal("not-secret"), | |
| "write_only_set_password": cty.NullVal(cty.String), | |
| }), | |
| "block": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ | |
| "user": cty.StringVal("not-secret"), | |
| "block_set_password": cty.NullVal(cty.String), | |
| })}), | |
| }), | |
| After: cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| "write_only": cty.String, | |
| "conn_info": cty.Object(map[string]cty.Type{ | |
| "user": cty.String, | |
| "write_only_set_password": cty.String, | |
| }), | |
| "block": cty.List(cty.Object(map[string]cty.Type{ | |
| "user": cty.String, | |
| "block_set_password": cty.String, | |
| })), | |
| })), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| "write_only": {Type: cty.String, WriteOnly: true}, | |
| "conn_info": { | |
| NestedType: &configschema.Object{ | |
| Nesting: configschema.NestingSingle, | |
| Attributes: map[string]*configschema.Attribute{ | |
| "user": {Type: cty.String, Optional: true}, | |
| "write_only_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| BlockTypes: map[string]*configschema.NestedBlock{ | |
| "block": { | |
| Nesting: configschema.NestingList, | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "user": {Type: cty.String, Optional: true}, | |
| "block_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| ExpectedOutput: ` # test_instance.example will be destroyed | |
| - resource "test_instance" "example" { | |
| - conn_info = { | |
| - user = "not-secret" -> null | |
| - write_only_set_password = (sensitive, write-only attribute) -> null | |
| } -> null | |
| - id = "i-02ae66f368e8518a9" -> null | |
| - write_only = (write-only attribute) -> null | |
| - block { | |
| - block_set_password = (write-only attribute) -> null | |
| - user = "not-secret" -> null | |
| } | |
| }`, | |
| }, | |
| } | |
| runTestCases(t, testCases) | |
| } | |
| func TestResourceChange_moved(t *testing.T) { | |
| prevRunAddr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "previous", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| testCases := map[string]testCase{ | |
| "moved and updated": { | |
| PrevRunAddr: prevRunAddr, | |
| Action: plans.Update, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("12345"), | |
| "foo": cty.StringVal("hello"), | |
| "bar": cty.StringVal("baz"), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("12345"), | |
| "foo": cty.StringVal("hello"), | |
| "bar": cty.StringVal("boop"), | |
| }), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Computed: true}, | |
| "foo": {Type: cty.String, Optional: true}, | |
| "bar": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.example will be updated in-place | |
| # (moved from test_instance.previous) | |
| ~ resource "test_instance" "example" { | |
| ~ bar = "baz" -> "boop" | |
| id = "12345" | |
| # (1 unchanged attribute hidden) | |
| }`, | |
| }, | |
| "moved without changes": { | |
| PrevRunAddr: prevRunAddr, | |
| Action: plans.NoOp, | |
| Mode: addrs.ManagedResourceMode, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("12345"), | |
| "foo": cty.StringVal("hello"), | |
| "bar": cty.StringVal("baz"), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("12345"), | |
| "foo": cty.StringVal("hello"), | |
| "bar": cty.StringVal("baz"), | |
| }), | |
| Schema: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Computed: true}, | |
| "foo": {Type: cty.String, Optional: true}, | |
| "bar": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| RequiredReplace: cty.NewPathSet(), | |
| ExpectedOutput: ` # test_instance.previous has moved to test_instance.example | |
| resource "test_instance" "example" { | |
| id = "12345" | |
| # (2 unchanged attributes hidden) | |
| }`, | |
| }, | |
| } | |
| runTestCases(t, testCases) | |
| } | |
| type testCase struct { | |
| Action plans.Action | |
| ActionReason plans.ResourceInstanceChangeActionReason | |
| ModuleInst addrs.ModuleInstance | |
| Mode addrs.ResourceMode | |
| InstanceKey addrs.InstanceKey | |
| DeposedKey states.DeposedKey | |
| Before cty.Value | |
| BeforeSensitivePaths []cty.Path | |
| After cty.Value | |
| AfterSensitivePaths []cty.Path | |
| Schema *configschema.Block | |
| RequiredReplace cty.PathSet | |
| ExpectedOutput string | |
| PrevRunAddr addrs.AbsResourceInstance | |
| } | |
| 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 | |
| } | |
| 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 | |
| } | |
| addr := addrs.Resource{ | |
| Mode: tc.Mode, | |
| Type: "test_instance", | |
| Name: "example", | |
| }.Instance(tc.InstanceKey).Absolute(tc.ModuleInst) | |
| prevRunAddr := tc.PrevRunAddr | |
| // If no previous run address is given, reuse the current address | |
| // to make initialization easier | |
| if prevRunAddr.Resource.Resource.Type == "" { | |
| prevRunAddr = addr | |
| } | |
| beforeDynamicValue, err := plans.NewDynamicValue(beforeVal, ty) | |
| if err != nil { | |
| t.Fatalf("failed to create dynamic before value: %s", err) | |
| } | |
| afterDynamicValue, err := plans.NewDynamicValue(afterVal, ty) | |
| if err != nil { | |
| t.Fatalf("failed to create dynamic after value: %s", err) | |
| } | |
| src := &plans.ResourceInstanceChangeSrc{ | |
| ChangeSrc: plans.ChangeSrc{ | |
| Action: tc.Action, | |
| Before: beforeDynamicValue, | |
| BeforeSensitivePaths: tc.BeforeSensitivePaths, | |
| After: afterDynamicValue, | |
| AfterSensitivePaths: tc.AfterSensitivePaths, | |
| }, | |
| Addr: addr, | |
| PrevRunAddr: prevRunAddr, | |
| DeposedKey: tc.DeposedKey, | |
| ProviderAddr: addrs.AbsProviderConfig{ | |
| Provider: addrs.NewDefaultProvider("test"), | |
| Module: addrs.RootModule, | |
| }, | |
| ActionReason: tc.ActionReason, | |
| RequiredReplace: tc.RequiredReplace, | |
| } | |
| tfschemas := &terraform.Schemas{ | |
| Providers: map[addrs.Provider]providers.ProviderSchema{ | |
| src.ProviderAddr.Provider: { | |
| ResourceTypes: map[string]providers.Schema{ | |
| src.Addr.Resource.Resource.Type: { | |
| Body: tc.Schema, | |
| }, | |
| }, | |
| DataSources: map[string]providers.Schema{ | |
| src.Addr.Resource.Resource.Type: { | |
| Body: tc.Schema, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| jsonchanges, err := jsonplan.MarshalResourceChanges([]*plans.ResourceInstanceChangeSrc{src}, tfschemas) | |
| if err != nil { | |
| t.Errorf("failed to marshal resource changes: %s", err) | |
| return | |
| } | |
| jsonschemas := jsonprovider.MarshalForRenderer(tfschemas) | |
| change := structured.FromJsonChange(jsonchanges[0].Change, attribute_path.AlwaysMatcher()) | |
| renderer := Renderer{Colorize: color} | |
| diff := diff{ | |
| change: jsonchanges[0], | |
| diff: differ.ComputeDiffForBlock(change, jsonschemas[jsonchanges[0].ProviderName].ResourceSchemas[jsonchanges[0].Type].Block), | |
| } | |
| output, _ := renderHumanDiff(renderer, diff, proposedChange) | |
| if diff := cmp.Diff(output, tc.ExpectedOutput); diff != "" { | |
| t.Errorf("wrong output\nexpected:\n%s\nactual:\n%s\ndiff:\n%s\n", tc.ExpectedOutput, output, diff) | |
| } | |
| }) | |
| } | |
| } | |
| 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) { | |
| changes := &plans.ChangesSrc{ | |
| Outputs: tc.changes, | |
| } | |
| outputs, err := jsonplan.MarshalOutputChanges(changes) | |
| if err != nil { | |
| t.Fatalf("failed to marshal output changes") | |
| } | |
| renderer := Renderer{Colorize: color} | |
| diffs := precomputeDiffs(Plan{ | |
| OutputChanges: outputs, | |
| }, plans.NormalMode) | |
| output := renderHumanDiffOutputs(renderer, diffs.outputs) | |
| if output != tc.output { | |
| t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.output) | |
| } | |
| }) | |
| } | |
| } | |
| func TestResourceChange_deferredActions(t *testing.T) { | |
| color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} | |
| providerAddr := addrs.AbsProviderConfig{ | |
| Provider: addrs.NewDefaultProvider("test"), | |
| Module: addrs.RootModule, | |
| } | |
| testCases := map[string]struct { | |
| changes []*plans.DeferredResourceInstanceChange | |
| output string | |
| }{ | |
| "deferred create action": { | |
| changes: []*plans.DeferredResourceInstanceChange{ | |
| { | |
| DeferredReason: providers.DeferredReasonAbsentPrereq, | |
| Change: &plans.ResourceInstanceChange{ | |
| Addr: addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "instance", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), | |
| ProviderAddr: providerAddr, | |
| Change: plans.Change{ | |
| Action: plans.Create, | |
| Before: cty.NullVal(cty.DynamicPseudoType), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.UnknownVal(cty.String), | |
| "ami": cty.StringVal("bar"), | |
| "disk": cty.UnknownVal(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.Number, | |
| })), | |
| "root_block_device": cty.UnknownVal(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| })), | |
| }), | |
| }, | |
| }, | |
| }, | |
| }, | |
| output: ` # test_instance.instance was deferred | |
| # (because a prerequisite for this resource has not yet been created) | |
| + resource "test_instance" "instance" { | |
| + ami = "bar" | |
| + disk = (known after apply) | |
| + id = (known after apply) | |
| + root_block_device (known after apply) | |
| }`, | |
| }, | |
| "deferred create action unknown for_each": { | |
| changes: []*plans.DeferredResourceInstanceChange{ | |
| { | |
| DeferredReason: providers.DeferredReasonInstanceCountUnknown, | |
| Change: &plans.ResourceInstanceChange{ | |
| Addr: addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "instance", | |
| }.Instance(addrs.WildcardKey).Absolute(addrs.RootModuleInstance), | |
| ProviderAddr: providerAddr, | |
| Change: plans.Change{ | |
| Action: plans.Create, | |
| Before: cty.NullVal(cty.DynamicPseudoType), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.UnknownVal(cty.String), | |
| "ami": cty.StringVal("bar"), | |
| "disk": cty.UnknownVal(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.Number, | |
| })), | |
| "root_block_device": cty.UnknownVal(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| })), | |
| }), | |
| }, | |
| }, | |
| }, | |
| }, | |
| output: ` # test_instance.instance[*] was deferred | |
| # (because the number of resource instances is unknown) | |
| + resource "test_instance" "instance" { | |
| + ami = "bar" | |
| + disk = (known after apply) | |
| + id = (known after apply) | |
| + root_block_device (known after apply) | |
| }`, | |
| }, | |
| "deferred update action": { | |
| changes: []*plans.DeferredResourceInstanceChange{ | |
| { | |
| DeferredReason: providers.DeferredReasonProviderConfigUnknown, | |
| Change: &plans.ResourceInstanceChange{ | |
| Addr: addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "instance", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), | |
| ProviderAddr: providerAddr, | |
| Change: plans.Change{ | |
| Action: plans.Update, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("foo"), | |
| "ami": cty.StringVal("bar"), | |
| "disk": cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.Number, | |
| })), | |
| "root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| })), | |
| }), | |
| After: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("foo"), | |
| "ami": cty.StringVal("baz"), | |
| "disk": cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.Number, | |
| })), | |
| "root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| })), | |
| }), | |
| }, | |
| }, | |
| }, | |
| }, | |
| output: ` # test_instance.instance was deferred | |
| # (because the provider configuration is unknown) | |
| ~ resource "test_instance" "instance" { | |
| ~ ami = "bar" -> "baz" | |
| id = "foo" | |
| }`, | |
| }, | |
| "deferred destroy action": { | |
| changes: []*plans.DeferredResourceInstanceChange{ | |
| { | |
| DeferredReason: providers.DeferredReasonResourceConfigUnknown, | |
| Change: &plans.ResourceInstanceChange{ | |
| Addr: addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "instance", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), | |
| ProviderAddr: providerAddr, | |
| Change: plans.Change{ | |
| Action: plans.Update, | |
| Before: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("foo"), | |
| "ami": cty.StringVal("bar"), | |
| "disk": cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.Number, | |
| })), | |
| "root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| })), | |
| }), | |
| After: cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| "ami": cty.String, | |
| "disk": cty.Object(map[string]cty.Type{ | |
| "mount_point": cty.String, | |
| "size": cty.Number, | |
| }), | |
| "root_block_device": cty.Object(map[string]cty.Type{ | |
| "volume_type": cty.String, | |
| }), | |
| })), | |
| }, | |
| }, | |
| }, | |
| }, | |
| output: ` # test_instance.instance was deferred | |
| # (because the resource configuration is unknown) | |
| ~ resource "test_instance" "instance" { | |
| - ami = "bar" -> null | |
| - id = "foo" -> null | |
| }`, | |
| }, | |
| } | |
| for name, tc := range testCases { | |
| t.Run(name, func(t *testing.T) { | |
| blockSchema := testSchema(configschema.NestingSingle) | |
| fullSchema := &terraform.Schemas{ | |
| Providers: map[addrs.Provider]providers.ProviderSchema{ | |
| providerAddr.Provider: { | |
| ResourceTypes: map[string]providers.Schema{ | |
| "test_instance": { | |
| Body: blockSchema, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| var changes []*plans.DeferredResourceInstanceChangeSrc | |
| for _, change := range tc.changes { | |
| changeSrc, err := change.Encode(providers.Schema{ | |
| Body: blockSchema, | |
| }) | |
| if err != nil { | |
| t.Fatalf("Failed to encode change: %s", err) | |
| } | |
| changes = append(changes, changeSrc) | |
| } | |
| deferredChanges, err := jsonplan.MarshalDeferredResourceChanges(changes, fullSchema) | |
| if err != nil { | |
| t.Fatalf("failed to marshal deferred changes: %s", err) | |
| } | |
| renderer := Renderer{Colorize: color} | |
| jsonschemas := jsonprovider.MarshalForRenderer(fullSchema) | |
| diffs := precomputeDiffs(Plan{ | |
| DeferredChanges: deferredChanges, | |
| ProviderSchemas: jsonschemas, | |
| }, plans.NormalMode) | |
| // TODO: Add diffing for outputs | |
| // TODO: Make sure it's true and either make it a single entity in the test case or deal with a list here | |
| output, _ := renderHumanDeferredDiff(renderer, diffs.deferred[0]) | |
| if diff := cmp.Diff(tc.output, output); len(diff) > 0 { | |
| t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", output, tc.output, diff) | |
| } | |
| }) | |
| } | |
| } | |
| func TestResourceChange_actions(t *testing.T) { | |
| color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} | |
| triggeringResourceAddr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "example", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| defaultResourceChange := jsonplan.ResourceChange{ | |
| Address: triggeringResourceAddr.String(), | |
| Mode: "managed", | |
| Type: "test_instance", | |
| Name: "example", | |
| IndexUnknown: true, | |
| ProviderName: "registry.terraform.io/hashicorp/test", | |
| Change: jsonplan.Change{ | |
| Actions: []string{"create"}, | |
| After: marshalJson(t, map[string]interface{}{ | |
| "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", | |
| "value": "Hello, World!", | |
| }), | |
| }, | |
| } | |
| for name, tc := range map[string]struct { | |
| actionInvocations []jsonplan.ActionInvocation | |
| output string | |
| }{ | |
| "before actions": { | |
| actionInvocations: []jsonplan.ActionInvocation{ | |
| { | |
| Address: "action.test_action.hello", | |
| Type: "test_action", | |
| Name: "hello", | |
| ProviderName: "registry.terraform.io/hashicorp/test", | |
| LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{ | |
| ActionTriggerBlockIndex: 0, | |
| ActionsListIndex: 0, | |
| TriggeringResourceAddress: triggeringResourceAddr.String(), | |
| ActionTriggerEvent: "BeforeCreate", | |
| }, | |
| ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"), | |
| "disk": cty.ObjectVal(map[string]cty.Value{ | |
| "size": cty.StringVal("100"), | |
| }), | |
| "root_block_device": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| })), | |
| }, | |
| }, | |
| output: ` # test_instance.example will be created | |
| + resource "test_instance" "example" { | |
| + id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" | |
| } | |
| # Actions to be invoked before this change in order: | |
| # action.test_action.hello | |
| action "test_action" "hello" { | |
| config { | |
| disk = { | |
| size = "100" | |
| } | |
| id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" | |
| root_block_device { | |
| volume_type = "gp2" | |
| } | |
| } | |
| } | |
| `, | |
| }, | |
| "after actions": { | |
| actionInvocations: []jsonplan.ActionInvocation{ | |
| { | |
| Address: "action.test_action.hello", | |
| Type: "test_action", | |
| Name: "hello", | |
| ProviderName: "registry.terraform.io/hashicorp/test", | |
| LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{ | |
| ActionTriggerBlockIndex: 0, | |
| ActionsListIndex: 0, | |
| TriggeringResourceAddress: triggeringResourceAddr.String(), | |
| ActionTriggerEvent: "AfterCreate", | |
| }, | |
| ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"), | |
| "disk": cty.ObjectVal(map[string]cty.Value{ | |
| "size": cty.StringVal("100"), | |
| }), | |
| "root_block_device": cty.ObjectVal(map[string]cty.Value{ | |
| "volume_type": cty.StringVal("gp2"), | |
| }), | |
| })), | |
| }, | |
| }, | |
| output: ` # test_instance.example will be created | |
| + resource "test_instance" "example" { | |
| + id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" | |
| } | |
| # Actions to be invoked after this change in order: | |
| # action.test_action.hello | |
| action "test_action" "hello" { | |
| config { | |
| disk = { | |
| size = "100" | |
| } | |
| id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" | |
| root_block_device { | |
| volume_type = "gp2" | |
| } | |
| } | |
| } | |
| `, | |
| }, | |
| "before and after actions": { | |
| actionInvocations: []jsonplan.ActionInvocation{ | |
| { | |
| Address: "action.test_action.hello", | |
| Type: "test_action", | |
| Name: "hello", | |
| ProviderName: "registry.terraform.io/hashicorp/test", | |
| LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{ | |
| ActionTriggerBlockIndex: 0, | |
| ActionsListIndex: 0, | |
| TriggeringResourceAddress: triggeringResourceAddr.String(), | |
| ActionTriggerEvent: "BeforeCreate", | |
| }, | |
| ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("first-block-and-action"), | |
| })), | |
| }, | |
| { | |
| Address: "action.test_action.hello", | |
| Type: "test_action", | |
| Name: "hello", | |
| ProviderName: "registry.terraform.io/hashicorp/test", | |
| LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{ | |
| ActionTriggerBlockIndex: 0, | |
| ActionsListIndex: 1, | |
| TriggeringResourceAddress: triggeringResourceAddr.String(), | |
| ActionTriggerEvent: "BeforeCreate", | |
| }, | |
| ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("first-block-second-action"), | |
| })), | |
| }, | |
| { | |
| Address: "action.test_action.hello", | |
| Type: "test_action", | |
| Name: "hello", | |
| ProviderName: "registry.terraform.io/hashicorp/test", | |
| LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{ | |
| ActionTriggerBlockIndex: 1, | |
| ActionsListIndex: 0, | |
| TriggeringResourceAddress: triggeringResourceAddr.String(), | |
| ActionTriggerEvent: "AfterCreate", | |
| }, | |
| ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("second-block-first-action"), | |
| })), | |
| }, | |
| { | |
| Address: "action.test_action.hello", | |
| Type: "test_action", | |
| Name: "hello", | |
| ProviderName: "registry.terraform.io/hashicorp/test", | |
| LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{ | |
| ActionTriggerBlockIndex: 2, | |
| ActionsListIndex: 0, | |
| TriggeringResourceAddress: triggeringResourceAddr.String(), | |
| ActionTriggerEvent: "AfterCreate", | |
| }, | |
| ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("third-block-first-action"), | |
| })), | |
| }, | |
| { | |
| Address: "action.test_action.hello", | |
| Type: "test_action", | |
| Name: "hello", | |
| ProviderName: "registry.terraform.io/hashicorp/test", | |
| LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{ | |
| ActionTriggerBlockIndex: 3, | |
| ActionsListIndex: 0, | |
| TriggeringResourceAddress: triggeringResourceAddr.String(), | |
| ActionTriggerEvent: "BeforeCreate", | |
| }, | |
| ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("fourth-block-first-action"), | |
| })), | |
| }, | |
| }, | |
| output: ` # test_instance.example will be created | |
| + resource "test_instance" "example" { | |
| + id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" | |
| } | |
| # Actions to be invoked before this change in order: | |
| # action.test_action.hello | |
| action "test_action" "hello" { | |
| config { | |
| id = "first-block-and-action" | |
| } | |
| } | |
| # action.test_action.hello | |
| action "test_action" "hello" { | |
| config { | |
| id = "first-block-second-action" | |
| } | |
| } | |
| # action.test_action.hello | |
| action "test_action" "hello" { | |
| config { | |
| id = "fourth-block-first-action" | |
| } | |
| } | |
| # Actions to be invoked after this change in order: | |
| # action.test_action.hello | |
| action "test_action" "hello" { | |
| config { | |
| id = "second-block-first-action" | |
| } | |
| } | |
| # action.test_action.hello | |
| action "test_action" "hello" { | |
| config { | |
| id = "third-block-first-action" | |
| } | |
| } | |
| `, | |
| }, | |
| "no config value": { | |
| actionInvocations: []jsonplan.ActionInvocation{ | |
| { | |
| Address: "action.test_action.hello", | |
| Type: "test_action", | |
| Name: "hello", | |
| ProviderName: "registry.terraform.io/hashicorp/test", | |
| LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{ | |
| ActionTriggerBlockIndex: 0, | |
| ActionsListIndex: 0, | |
| TriggeringResourceAddress: triggeringResourceAddr.String(), | |
| ActionTriggerEvent: "BeforeCreate", | |
| }, | |
| }, | |
| }, | |
| output: ` # test_instance.example will be created | |
| + resource "test_instance" "example" { | |
| + id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" | |
| } | |
| # Actions to be invoked before this change in order: | |
| # action.test_action.hello | |
| action "test_action" "hello" { | |
| } | |
| `, | |
| }, | |
| } { | |
| t.Run(name, func(t *testing.T) { | |
| renderer := Renderer{Colorize: color} | |
| providerAddr := addrs.AbsProviderConfig{ | |
| Provider: addrs.NewDefaultProvider("test"), | |
| } | |
| blockSchema := testSchema(configschema.NestingSingle) | |
| fullSchema := &terraform.Schemas{ | |
| Providers: map[addrs.Provider]providers.ProviderSchema{ | |
| providerAddr.Provider: { | |
| ResourceTypes: map[string]providers.Schema{ | |
| "test_instance": { | |
| Body: blockSchema, | |
| }, | |
| }, | |
| Actions: map[string]providers.ActionSchema{ | |
| "test_action": { | |
| ConfigSchema: blockSchema, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| jsonschemas := jsonprovider.MarshalForRenderer(fullSchema) | |
| diffs := precomputeDiffs(Plan{ | |
| ResourceChanges: []jsonplan.ResourceChange{defaultResourceChange}, | |
| ActionInvocations: tc.actionInvocations, | |
| ProviderSchemas: jsonschemas, | |
| }, plans.NormalMode) | |
| if len(diffs.changes) != 1 { | |
| t.Fatalf("expected exactly one change, got %d", len(diffs.changes)) | |
| } | |
| output, _ := renderHumanDiff(renderer, diffs.changes[0], proposedChange) | |
| if diff := cmp.Diff(tc.output, output); len(diff) > 0 { | |
| t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", output, tc.output, diff) | |
| } | |
| }) | |
| } | |
| } | |
| 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 | |
| } | |
| // A basic test schema using a configurable NestingMode for one (NestedType) attribute and one block | |
| func testSchema(nesting configschema.NestingMode) *configschema.Block { | |
| var diskKey = "disks" | |
| if nesting == configschema.NestingSingle { | |
| diskKey = "disk" | |
| } | |
| return &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| "ami": {Type: cty.String, Optional: true}, | |
| diskKey: { | |
| NestedType: &configschema.Object{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "mount_point": {Type: cty.String, Optional: true}, | |
| "size": {Type: cty.String, Optional: true}, | |
| }, | |
| Nesting: nesting, | |
| }, | |
| }, | |
| }, | |
| 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: nesting, | |
| }, | |
| }, | |
| } | |
| } | |
| // A basic test schema using a configurable NestingMode for one (NestedType) | |
| // attribute marked sensitive. | |
| func testSchemaSensitive(nesting configschema.NestingMode) *configschema.Block { | |
| return &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| "ami": {Type: cty.String, Optional: true}, | |
| "disks": { | |
| Sensitive: true, | |
| NestedType: &configschema.Object{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "mount_point": {Type: cty.String, Optional: true}, | |
| "size": {Type: cty.String, Optional: true}, | |
| }, | |
| Nesting: nesting, | |
| }, | |
| }, | |
| }, | |
| } | |
| } | |
| func testSchemaMultipleBlocks(nesting configschema.NestingMode) *configschema.Block { | |
| return &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| "ami": {Type: cty.String, Optional: true}, | |
| "disks": { | |
| NestedType: &configschema.Object{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "mount_point": {Type: cty.String, Optional: true}, | |
| "size": {Type: cty.String, Optional: true}, | |
| }, | |
| Nesting: nesting, | |
| }, | |
| }, | |
| }, | |
| 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: nesting, | |
| }, | |
| "leaf_block_device": { | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "volume_type": { | |
| Type: cty.String, | |
| Optional: true, | |
| Computed: true, | |
| }, | |
| }, | |
| }, | |
| Nesting: nesting, | |
| }, | |
| }, | |
| } | |
| } | |
| // similar to testSchema with the addition of a "new_field" block | |
| func testSchemaPlus(nesting configschema.NestingMode) *configschema.Block { | |
| var diskKey = "disks" | |
| if nesting == configschema.NestingSingle { | |
| diskKey = "disk" | |
| } | |
| return &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| "ami": {Type: cty.String, Optional: true}, | |
| diskKey: { | |
| NestedType: &configschema.Object{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "mount_point": {Type: cty.String, Optional: true}, | |
| "size": {Type: cty.String, Optional: true}, | |
| }, | |
| Nesting: nesting, | |
| }, | |
| }, | |
| }, | |
| 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: nesting, | |
| }, | |
| }, | |
| } | |
| } | |
| func marshalJson(t *testing.T, data interface{}) json.RawMessage { | |
| result, err := json.Marshal(data) | |
| if err != nil { | |
| t.Fatalf("failed to marshal json: %v", err) | |
| } | |
| return result | |
| } | |
| func marshalCtyJson(t *testing.T, data cty.Value) json.RawMessage { | |
| result, err := ctyjson.Marshal(data, data.Type()) | |
| if err != nil { | |
| t.Fatalf("failed to marshal json: %v", err) | |
| } | |
| return result | |
| } |