From b802237e035c65c35cac717eda7e9a01e7663605 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Wed, 28 Apr 2021 12:02:34 -0700 Subject: [PATCH] plans: Track an optional extra "reason" for some planned actions Previously we were repeating some logic in the UI layer in order to recover relevant additional context about a change to report to a user. In order to help keep things consistent, and to have a clearer path for adding more such things in the future, here we capture this user-facing idea of an "action reason" within the plan model, and then use that directly in order to decide how to describe the change to the user. For the moment the "tainted" situation is the only one that gets a special message, matching what we had before, but we can expand on this in future in order to give better feedback about the other replace situations too. This also preemptively includes the "replacing by request" reason, which is currently not reachable but will be used in the near future as part of implementing the -replace=... plan command line option to allow forcing a particular object to be replaced. So far we don't have any special reasons for anything other than replacing, which makes sense because replacing is the only one that is in a sense a special case of another action (Update), but this could expand to other kinds of reasons in the future, such as explaining which of the few different reasons a data source read might be deferred until the apply step. --- command/format/diff.go | 6 +- command/format/diff_test.go | 167 ++++--------- command/jsonplan/plan.go | 13 + command/jsonplan/resource.go | 10 + command/show_test.go | 5 + .../show-json/requires-replace/main.tf | 3 + .../show-json/requires-replace/output.json | 88 +++++++ .../requires-replace/terraform.tfstate | 24 ++ command/views/plan.go | 11 - plans/changes.go | 54 ++++ plans/changes_src.go | 14 ++ plans/internal/planproto/planfile.pb.go | 236 ++++++++++++------ plans/internal/planproto/planfile.proto | 16 ++ plans/planfile/tfplan.go | 26 ++ plans/planfile/tfplan_test.go | 1 + ...sourceinstancechangeactionreason_string.go | 37 +++ terraform/context_plan_test.go | 159 ++++++++++-- terraform/node_resource_abstract_instance.go | 4 + .../testdata/plan-requires-replace/main.tf | 3 + website/docs/internals/json-format.html.md | 26 +- 20 files changed, 667 insertions(+), 236 deletions(-) create mode 100644 command/testdata/show-json/requires-replace/main.tf create mode 100644 command/testdata/show-json/requires-replace/output.json create mode 100644 command/testdata/show-json/requires-replace/terraform.tfstate create mode 100644 plans/resourceinstancechangeactionreason_string.go create mode 100644 terraform/testdata/plan-requires-replace/main.tf diff --git a/command/format/diff.go b/command/format/diff.go index dac787af6..7a35e4fd0 100644 --- a/command/format/diff.go +++ b/command/format/diff.go @@ -30,7 +30,6 @@ import ( // no color codes will be included. func ResourceChange( change *plans.ResourceInstanceChangeSrc, - tainted bool, schema *configschema.Block, color *colorstring.Colorize, ) string { @@ -58,9 +57,10 @@ func ResourceChange( case plans.Update: buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] will be updated in-place", dispAddr))) case plans.CreateThenDelete, plans.DeleteThenCreate: - if tainted { + switch change.ActionReason { + case plans.ResourceInstanceReplaceBecauseTainted: buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] is tainted, so must be [bold][red]replaced", dispAddr))) - } else { + default: buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] must be [bold][red]replaced", dispAddr))) } case plans.Delete: diff --git a/command/format/diff_test.go b/command/format/diff_test.go index a04030509..a24d1cc22 100644 --- a/command/format/diff_test.go +++ b/command/format/diff_test.go @@ -27,7 +27,6 @@ func TestResourceChange_primitiveTypes(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + id = (known after apply) @@ -47,7 +46,6 @@ func TestResourceChange_primitiveTypes(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + string = "null" @@ -67,7 +65,6 @@ func TestResourceChange_primitiveTypes(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + string = "null " @@ -87,7 +84,6 @@ func TestResourceChange_primitiveTypes(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be destroyed - resource "test_instance" "example" { - id = "i-02ae66f368e8518a9" -> null @@ -109,7 +105,6 @@ func TestResourceChange_primitiveTypes(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be destroyed - resource "test_instance" "example" { - id = "i-02ae66f368e8518a9" -> null @@ -134,7 +129,6 @@ func TestResourceChange_primitiveTypes(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" @@ -143,8 +137,9 @@ func TestResourceChange_primitiveTypes(t *testing.T) { `, }, "string force-new update": { - Action: plans.DeleteThenCreate, - Mode: addrs.ManagedResourceMode, + 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"), @@ -162,7 +157,6 @@ func TestResourceChange_primitiveTypes(t *testing.T) { RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "ami"}, }), - Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement @@ -191,7 +185,6 @@ func TestResourceChange_primitiveTypes(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" @@ -227,7 +220,6 @@ field }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -262,7 +254,6 @@ new line }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -296,7 +287,6 @@ new line RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "more_lines"}, }), - Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -338,7 +328,6 @@ new line }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + conn_info = { @@ -371,7 +360,6 @@ new line }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "blah" -> (known after apply) @@ -381,10 +369,11 @@ new line `, }, - // tainted resources + // tainted objects "replace tainted resource": { - Action: plans.DeleteThenCreate, - Mode: addrs.ManagedResourceMode, + 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"), @@ -402,7 +391,6 @@ new line RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "ami"}, }), - Tainted: true, ExpectedOutput: ` # test_instance.example is tainted, so must be replaced -/+ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement @@ -411,8 +399,9 @@ new line `, }, "force replacement with empty before value": { - Action: plans.DeleteThenCreate, - Mode: addrs.ManagedResourceMode, + 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), @@ -430,7 +419,6 @@ new line RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "forced"}, }), - Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { + forced = "example" # forces replacement @@ -439,8 +427,9 @@ new line `, }, "force replacement with empty before value legacy": { - Action: plans.DeleteThenCreate, - Mode: addrs.ManagedResourceMode, + Action: plans.DeleteThenCreate, + ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, + Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ "name": cty.StringVal("name"), "forced": cty.StringVal(""), @@ -458,7 +447,6 @@ new line RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "forced"}, }), - Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { + forced = "example" # forces replacement @@ -500,7 +488,6 @@ new line }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" @@ -539,7 +526,6 @@ func TestResourceChange_JSON(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + id = (known after apply) @@ -578,7 +564,6 @@ func TestResourceChange_JSON(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -610,7 +595,6 @@ func TestResourceChange_JSON(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -642,7 +626,6 @@ func TestResourceChange_JSON(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -674,7 +657,6 @@ func TestResourceChange_JSON(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -693,8 +675,9 @@ func TestResourceChange_JSON(t *testing.T) { `, }, "force-new update": { - Action: plans.DeleteThenCreate, - Mode: addrs.ManagedResourceMode, + 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"}`), @@ -712,7 +695,6 @@ func TestResourceChange_JSON(t *testing.T) { RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "json_field"}, }), - Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -744,7 +726,6 @@ func TestResourceChange_JSON(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -758,8 +739,9 @@ func TestResourceChange_JSON(t *testing.T) { `, }, "force-new update (whitespace change)": { - Action: plans.DeleteThenCreate, - Mode: addrs.ManagedResourceMode, + 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"}`), @@ -778,7 +760,6 @@ func TestResourceChange_JSON(t *testing.T) { RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "json_field"}, }), - Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -806,7 +787,6 @@ func TestResourceChange_JSON(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + id = (known after apply) @@ -832,7 +812,6 @@ func TestResourceChange_JSON(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -864,7 +843,6 @@ func TestResourceChange_JSON(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -896,7 +874,6 @@ func TestResourceChange_JSON(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -931,7 +908,6 @@ func TestResourceChange_JSON(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -964,7 +940,6 @@ func TestResourceChange_JSON(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -999,7 +974,6 @@ func TestResourceChange_JSON(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1037,7 +1011,6 @@ func TestResourceChange_JSON(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1074,7 +1047,6 @@ func TestResourceChange_JSON(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1111,7 +1083,6 @@ func TestResourceChange_JSON(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1162,7 +1133,6 @@ func TestResourceChange_primitiveList(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1196,7 +1166,6 @@ func TestResourceChange_primitiveList(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1241,7 +1210,6 @@ func TestResourceChange_primitiveList(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1257,8 +1225,9 @@ func TestResourceChange_primitiveList(t *testing.T) { `, }, "force-new update - insertion": { - Action: plans.DeleteThenCreate, - Mode: addrs.ManagedResourceMode, + 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"), @@ -1286,7 +1255,6 @@ func TestResourceChange_primitiveList(t *testing.T) { RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "list_field"}, }), - Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1330,7 +1298,6 @@ func TestResourceChange_primitiveList(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1362,7 +1329,6 @@ func TestResourceChange_primitiveList(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + ami = "ami-STATIC" @@ -1396,7 +1362,6 @@ func TestResourceChange_primitiveList(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1430,7 +1395,6 @@ func TestResourceChange_primitiveList(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1468,7 +1432,6 @@ func TestResourceChange_primitiveList(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1516,7 +1479,6 @@ func TestResourceChange_primitiveList(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1568,7 +1530,6 @@ func TestResourceChange_primitiveTuple(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { id = "i-02ae66f368e8518a9" @@ -1612,7 +1573,6 @@ func TestResourceChange_primitiveSet(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1646,7 +1606,6 @@ func TestResourceChange_primitiveSet(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1685,7 +1644,6 @@ func TestResourceChange_primitiveSet(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1698,8 +1656,9 @@ func TestResourceChange_primitiveSet(t *testing.T) { `, }, "force-new update - insertion": { - Action: plans.DeleteThenCreate, - Mode: addrs.ManagedResourceMode, + 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"), @@ -1727,7 +1686,6 @@ func TestResourceChange_primitiveSet(t *testing.T) { RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "set_field"}, }), - Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1766,7 +1724,6 @@ func TestResourceChange_primitiveSet(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1796,7 +1753,6 @@ func TestResourceChange_primitiveSet(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + ami = "ami-STATIC" @@ -1861,7 +1817,6 @@ func TestResourceChange_primitiveSet(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1894,7 +1849,6 @@ func TestResourceChange_primitiveSet(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1933,7 +1887,6 @@ func TestResourceChange_primitiveSet(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -1975,7 +1928,6 @@ func TestResourceChange_map(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -2009,7 +1961,6 @@ func TestResourceChange_map(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -2048,7 +1999,6 @@ func TestResourceChange_map(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -2061,8 +2011,9 @@ func TestResourceChange_map(t *testing.T) { `, }, "force-new update - insertion": { - Action: plans.DeleteThenCreate, - Mode: addrs.ManagedResourceMode, + 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"), @@ -2090,7 +2041,6 @@ func TestResourceChange_map(t *testing.T) { RequiredReplace: cty.NewPathSet(cty.Path{ cty.GetAttrStep{Name: "map_field"}, }), - Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -2129,7 +2079,6 @@ func TestResourceChange_map(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -2159,7 +2108,6 @@ func TestResourceChange_map(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + ami = "ami-STATIC" @@ -2197,7 +2145,6 @@ func TestResourceChange_map(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ id = "i-02ae66f368e8518a9" -> (known after apply) @@ -2249,7 +2196,6 @@ func TestResourceChange_nestedList(t *testing.T) { }), }), RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: testSchema(configschema.NestingList), ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { @@ -2289,7 +2235,6 @@ func TestResourceChange_nestedList(t *testing.T) { }), }), RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: testSchema(configschema.NestingList), ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { @@ -2336,7 +2281,6 @@ func TestResourceChange_nestedList(t *testing.T) { }), }), RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: testSchema(configschema.NestingList), ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { @@ -2390,7 +2334,6 @@ func TestResourceChange_nestedList(t *testing.T) { }), }), RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: testSchemaPlus(configschema.NestingList), ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { @@ -2411,8 +2354,9 @@ func TestResourceChange_nestedList(t *testing.T) { `, }, "force-new update (inside blocks)": { - Action: plans.DeleteThenCreate, - Mode: addrs.ManagedResourceMode, + 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"), @@ -2455,8 +2399,7 @@ func TestResourceChange_nestedList(t *testing.T) { cty.GetAttrStep{Name: "mount_point"}, }, ), - Tainted: false, - Schema: testSchema(configschema.NestingList), + Schema: testSchema(configschema.NestingList), ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" @@ -2475,8 +2418,9 @@ func TestResourceChange_nestedList(t *testing.T) { `, }, "force-new update (whole block)": { - Action: plans.DeleteThenCreate, - Mode: addrs.ManagedResourceMode, + 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"), @@ -2511,8 +2455,7 @@ func TestResourceChange_nestedList(t *testing.T) { cty.Path{cty.GetAttrStep{Name: "root_block_device"}}, cty.Path{cty.GetAttrStep{Name: "disks"}}, ), - Tainted: false, - Schema: testSchema(configschema.NestingList), + Schema: testSchema(configschema.NestingList), ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" @@ -2560,7 +2503,6 @@ func TestResourceChange_nestedList(t *testing.T) { })), }), RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: testSchema(configschema.NestingList), ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { @@ -2596,7 +2538,6 @@ func TestResourceChange_nestedList(t *testing.T) { }), }), RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{ "block": { @@ -2636,7 +2577,6 @@ func TestResourceChange_nestedList(t *testing.T) { }), }), RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{ "list": { @@ -2699,7 +2639,6 @@ func TestResourceChange_nestedSet(t *testing.T) { }), }), RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: testSchema(configschema.NestingSet), ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { @@ -2753,7 +2692,6 @@ func TestResourceChange_nestedSet(t *testing.T) { }), }), RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: testSchemaPlus(configschema.NestingSet), ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { @@ -2780,8 +2718,9 @@ func TestResourceChange_nestedSet(t *testing.T) { `, }, "force-new update (whole block)": { - Action: plans.DeleteThenCreate, - Mode: addrs.ManagedResourceMode, + 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"), @@ -2816,8 +2755,7 @@ func TestResourceChange_nestedSet(t *testing.T) { cty.Path{cty.GetAttrStep{Name: "root_block_device"}}, cty.Path{cty.GetAttrStep{Name: "disks"}}, ), - Tainted: false, - Schema: testSchema(configschema.NestingSet), + Schema: testSchema(configschema.NestingSet), ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" @@ -2874,7 +2812,6 @@ func TestResourceChange_nestedSet(t *testing.T) { })), }), RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: testSchemaPlus(configschema.NestingSet), ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { @@ -2930,7 +2867,6 @@ func TestResourceChange_nestedMap(t *testing.T) { }), }), RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: testSchema(configschema.NestingMap), ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { @@ -2984,7 +2920,6 @@ func TestResourceChange_nestedMap(t *testing.T) { }), }), RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: testSchemaPlus(configschema.NestingMap), ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { @@ -3048,7 +2983,6 @@ func TestResourceChange_nestedMap(t *testing.T) { }), }), RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: testSchemaPlus(configschema.NestingMap), ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { @@ -3073,8 +3007,9 @@ func TestResourceChange_nestedMap(t *testing.T) { `, }, "force-new update (whole block)": { - Action: plans.DeleteThenCreate, - Mode: addrs.ManagedResourceMode, + 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"), @@ -3117,8 +3052,7 @@ func TestResourceChange_nestedMap(t *testing.T) { }, cty.Path{cty.GetAttrStep{Name: "disks"}}, ), - Tainted: false, - Schema: testSchema(configschema.NestingMap), + Schema: testSchema(configschema.NestingMap), ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" @@ -3169,7 +3103,6 @@ func TestResourceChange_nestedMap(t *testing.T) { })), }), RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: testSchemaPlus(configschema.NestingMap), ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { @@ -3256,7 +3189,6 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, @@ -3409,7 +3341,6 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, @@ -3545,7 +3476,6 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, @@ -3681,7 +3611,6 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, @@ -3819,7 +3748,6 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, @@ -3952,7 +3880,6 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }, }, RequiredReplace: cty.NewPathSet(), - Tainted: false, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, @@ -4055,7 +3982,6 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { cty.GetAttrPath("ami"), cty.GetAttrPath("nested_block_set"), ), - Tainted: false, ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { ~ ami = (sensitive) # forces replacement @@ -4074,6 +4000,7 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { type testCase struct { Action plans.Action + ActionReason plans.ResourceInstanceChangeActionReason Mode addrs.ResourceMode Before cty.Value BeforeValMarks []cty.PathValueMarks @@ -4081,7 +4008,6 @@ type testCase struct { After cty.Value Schema *configschema.Block RequiredReplace cty.PathSet - Tainted bool ExpectedOutput string } @@ -4133,10 +4059,11 @@ func runTestCases(t *testing.T, testCases map[string]testCase) { BeforeValMarks: tc.BeforeValMarks, AfterValMarks: tc.AfterValMarks, }, + ActionReason: tc.ActionReason, RequiredReplace: tc.RequiredReplace, } - output := ResourceChange(change, tc.Tainted, tc.Schema, color) + output := ResourceChange(change, tc.Schema, color) if output != tc.ExpectedOutput { t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.ExpectedOutput) t.Errorf("%s", cmp.Diff(output, tc.ExpectedOutput)) diff --git a/command/jsonplan/plan.go b/command/jsonplan/plan.go index 8474ceac0..eeb634255 100644 --- a/command/jsonplan/plan.go +++ b/command/jsonplan/plan.go @@ -289,6 +289,19 @@ func (p *plan) marshalResourceChanges(changes *plans.Changes, schemas *terraform r.Type = addr.Resource.Resource.Type r.ProviderName = rc.ProviderAddr.Provider.String() + switch rc.ActionReason { + case plans.ResourceInstanceChangeNoReason: + r.ActionReason = "" // will be omitted in output + case plans.ResourceInstanceReplaceBecauseCannotUpdate: + r.ActionReason = "replace_because_cannot_update" + case plans.ResourceInstanceReplaceBecauseTainted: + r.ActionReason = "replace_because_tainted" + case plans.ResourceInstanceReplaceByRequest: + r.ActionReason = "replace_by_request" + default: + return fmt.Errorf("resource %s has an unsupported action reason %s", r.Address, rc.ActionReason) + } + p.ResourceChanges = append(p.ResourceChanges, r) } diff --git a/command/jsonplan/resource.go b/command/jsonplan/resource.go index 97e4851a3..3f6e9a50c 100644 --- a/command/jsonplan/resource.go +++ b/command/jsonplan/resource.go @@ -61,4 +61,14 @@ type resourceChange struct { // Change describes the change that will be made to this object Change change `json:"change,omitempty"` + + // ActionReason is a keyword representing some optional extra context + // for why the actions in Change.Actions were chosen. + // + // This extra detail is only for display purposes, to help a UI layer + // present some additional explanation to a human user. The possible + // values here might grow and change over time, so any consumer of this + // information should be resilient to encountering unrecognized values + // and treat them as an unspecified reason. + ActionReason string `json:"action_reason,omitempty"` } diff --git a/command/show_test.go b/command/show_test.go index 2fcafcc19..de535ca5b 100644 --- a/command/show_test.go +++ b/command/show_test.go @@ -572,11 +572,16 @@ func showFixtureProvider() *terraform.MockProvider { if idVal.IsNull() { idVal = cty.UnknownVal(cty.String) } + var reqRep []cty.Path + if amiVal.RawEquals(cty.StringVal("force-replace")) { + reqRep = append(reqRep, cty.GetAttrPath("ami")) + } return providers.PlanResourceChangeResponse{ PlannedState: cty.ObjectVal(map[string]cty.Value{ "id": idVal, "ami": amiVal, }), + RequiresReplace: reqRep, } } p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { diff --git a/command/testdata/show-json/requires-replace/main.tf b/command/testdata/show-json/requires-replace/main.tf new file mode 100644 index 000000000..6be6611c6 --- /dev/null +++ b/command/testdata/show-json/requires-replace/main.tf @@ -0,0 +1,3 @@ +resource "test_instance" "test" { + ami = "force-replace" +} diff --git a/command/testdata/show-json/requires-replace/output.json b/command/testdata/show-json/requires-replace/output.json new file mode 100644 index 000000000..b650e15a5 --- /dev/null +++ b/command/testdata/show-json/requires-replace/output.json @@ -0,0 +1,88 @@ +{ + "format_version": "0.1", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "test_instance.test", + "mode": "managed", + "type": "test_instance", + "name": "test", + "provider_name": "registry.terraform.io/hashicorp/test", + "schema_version": 0, + "values": { + "ami": "force-replace" + } + } + ] + } + }, + "resource_changes": [ + { + "address": "test_instance.test", + "mode": "managed", + "type": "test_instance", + "provider_name": "registry.terraform.io/hashicorp/test", + "name": "test", + "change": { + "actions": [ + "delete", + "create" + ], + "before": { + "ami": "bar", + "id": "placeholder" + }, + "after": { + "ami": "force-replace" + }, + "after_unknown": { + "id": true + }, + "after_sensitive": {}, + "before_sensitive": {} + }, + "action_reason": "replace_because_cannot_update" + } + ], + "prior_state": { + "format_version": "0.1", + "values": { + "root_module": { + "resources": [ + { + "address": "test_instance.test", + "mode": "managed", + "type": "test_instance", + "name": "test", + "schema_version": 0, + "provider_name": "registry.terraform.io/hashicorp/test", + "values": { + "ami": "bar", + "id": "placeholder" + } + } + ] + } + } + }, + "configuration": { + "root_module": { + "resources": [ + { + "address": "test_instance.test", + "mode": "managed", + "type": "test_instance", + "name": "test", + "provider_config_key": "test", + "schema_version": 0, + "expressions": { + "ami": { + "constant_value": "force-replace" + } + } + } + ] + } + } +} diff --git a/command/testdata/show-json/requires-replace/terraform.tfstate b/command/testdata/show-json/requires-replace/terraform.tfstate new file mode 100644 index 000000000..b57f60f84 --- /dev/null +++ b/command/testdata/show-json/requires-replace/terraform.tfstate @@ -0,0 +1,24 @@ +{ + "version": 4, + "terraform_version": "0.12.0", + "serial": 7, + "lineage": "configuredUnchanged", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "test_instance", + "name": "test", + "provider": "provider[\"registry.terraform.io/hashicorp/test\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "ami": "bar", + "id": "placeholder" + } + } + ] + } + ] +} diff --git a/command/views/plan.go b/command/views/plan.go index 631637bcc..2da854767 100644 --- a/command/views/plan.go +++ b/command/views/plan.go @@ -136,19 +136,8 @@ func renderPlan(plan *plans.Plan, baseState *states.State, schemas *terraform.Sc continue } - // check if the change is due to a tainted resource - tainted := false - if !baseState.Empty() { - if is := baseState.ResourceInstance(rcs.Addr); is != nil { - if obj := is.GetGeneration(rcs.DeposedKey.Generation()); obj != nil { - tainted = obj.Status == states.ObjectTainted - } - } - } - view.streams.Println(format.ResourceChange( rcs, - tainted, rSchema, view.colorize, )) diff --git a/plans/changes.go b/plans/changes.go index 1b4a9677f..2ababe778 100644 --- a/plans/changes.go +++ b/plans/changes.go @@ -165,6 +165,22 @@ type ResourceInstanceChange struct { // Change is an embedded description of the change. Change + // ActionReason is an optional extra indication of why we chose the + // action recorded in Change.Action for this particular resource instance. + // + // This is an approximate mechanism only for the purpose of explaining the + // plan to end-users in the UI and is not to be used for any + // decision-making during the apply step; if apply behavior needs to vary + // depending on the "action reason" then the information for that decision + // must be recorded more precisely elsewhere for that purpose. + // + // Sometimes there might be more than one reason for choosing a particular + // action. In that case, it's up to the codepath making that decision to + // decide which value would provide the most relevant explanation to the + // end-user and return that. It's not a goal of this field to represent + // fine details about the planning process. + ActionReason ResourceInstanceChangeActionReason + // RequiredReplace is a set of paths that caused the change action to be // Replace rather than Update. Always nil if the change action is not // Replace. @@ -192,6 +208,7 @@ func (rc *ResourceInstanceChange) Encode(ty cty.Type) (*ResourceInstanceChangeSr DeposedKey: rc.DeposedKey, ProviderAddr: rc.ProviderAddr, ChangeSrc: *cs, + ActionReason: rc.ActionReason, RequiredReplace: rc.RequiredReplace, Private: rc.Private, }, err @@ -277,6 +294,43 @@ func (rc *ResourceInstanceChange) Simplify(destroying bool) *ResourceInstanceCha return rc } +// ResourceInstanceChangeActionReason allows for some extra user-facing +// reasoning for why a particular change action was chosen for a particular +// resource instance. +// +// This only represents sufficient detail to give a suitable explanation to +// an end-user, and mustn't be used for any real decision-making during the +// apply step. +type ResourceInstanceChangeActionReason rune + +//go:generate go run golang.org/x/tools/cmd/stringer -type=ResourceInstanceChangeActionReason changes.go + +const ( + // In most cases there's no special reason for choosing a particular + // action, which is represented by ResourceInstanceChangeNoReason. + ResourceInstanceChangeNoReason ResourceInstanceChangeActionReason = 0 + + // ResourceInstanceReplaceBecauseTainted indicates that the resource + // instance must be replaced because its existing current object is + // marked as "tainted". + ResourceInstanceReplaceBecauseTainted ResourceInstanceChangeActionReason = 'T' + + // ResourceInstanceReplaceByRequest indicates that the resource instance + // is planned to be replaced because a caller specifically asked for it + // to be using ReplaceAddrs. (On the command line, the -replace=... + // planning option.) + ResourceInstanceReplaceByRequest ResourceInstanceChangeActionReason = 'R' + + // ResourceInstanceReplaceBecauseCannotUpdate indicates that the resource + // instance is planned to be replaced because the provider has indicated + // that a requested change cannot be applied as an update. + // + // In this case, the RequiredReplace field will typically be populated on + // the ResourceInstanceChange object to give information about specifically + // which arguments changed in a non-updatable way. + ResourceInstanceReplaceBecauseCannotUpdate ResourceInstanceChangeActionReason = 'F' +) + // OutputChange describes a change to an output value. type OutputChange struct { // Addr is the absolute address of the output value that the change diff --git a/plans/changes_src.go b/plans/changes_src.go index 055f222db..fdc0853cc 100644 --- a/plans/changes_src.go +++ b/plans/changes_src.go @@ -34,6 +34,19 @@ type ResourceInstanceChangeSrc struct { // ChangeSrc is an embedded description of the not-yet-decoded change. ChangeSrc + // ActionReason is an optional extra indication of why we chose the + // action recorded in Change.Action for this particular resource instance. + // + // This is an approximate mechanism only for the purpose of explaining the + // plan to end-users in the UI and is not to be used for any + // decision-making during the apply step; if apply behavior needs to vary + // depending on the "action reason" then the information for that decision + // must be recorded more precisely elsewhere for that purpose. + // + // See the field of the same name in ResourceInstanceChange for more + // details. + ActionReason ResourceInstanceChangeActionReason + // RequiredReplace is a set of paths that caused the change action to be // Replace rather than Update. Always nil if the change action is not // Replace. @@ -58,6 +71,7 @@ func (rcs *ResourceInstanceChangeSrc) Decode(ty cty.Type) (*ResourceInstanceChan DeposedKey: rcs.DeposedKey, ProviderAddr: rcs.ProviderAddr, Change: *change, + ActionReason: rcs.ActionReason, RequiredReplace: rcs.RequiredReplace, Private: rcs.Private, }, nil diff --git a/plans/internal/planproto/planfile.pb.go b/plans/internal/planproto/planfile.pb.go index 1631da460..dc72493b0 100644 --- a/plans/internal/planproto/planfile.pb.go +++ b/plans/internal/planproto/planfile.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.26.0-devel +// protoc-gen-go v1.26.0 // protoc v3.15.6 // source: planfile.proto @@ -83,6 +83,62 @@ func (Action) EnumDescriptor() ([]byte, []int) { return file_planfile_proto_rawDescGZIP(), []int{0} } +// ResourceInstanceActionReason sometimes provides some additional user-facing +// context for why a particular action was chosen for a resource instance. +// This is for user feedback only and never used to drive behavior during the +// subsequent apply step. +type ResourceInstanceActionReason int32 + +const ( + ResourceInstanceActionReason_NONE ResourceInstanceActionReason = 0 + ResourceInstanceActionReason_REPLACE_BECAUSE_TAINTED ResourceInstanceActionReason = 1 + ResourceInstanceActionReason_REPLACE_BY_REQUEST ResourceInstanceActionReason = 2 + ResourceInstanceActionReason_REPLACE_BECAUSE_CANNOT_UPDATE ResourceInstanceActionReason = 3 +) + +// Enum value maps for ResourceInstanceActionReason. +var ( + ResourceInstanceActionReason_name = map[int32]string{ + 0: "NONE", + 1: "REPLACE_BECAUSE_TAINTED", + 2: "REPLACE_BY_REQUEST", + 3: "REPLACE_BECAUSE_CANNOT_UPDATE", + } + ResourceInstanceActionReason_value = map[string]int32{ + "NONE": 0, + "REPLACE_BECAUSE_TAINTED": 1, + "REPLACE_BY_REQUEST": 2, + "REPLACE_BECAUSE_CANNOT_UPDATE": 3, + } +) + +func (x ResourceInstanceActionReason) Enum() *ResourceInstanceActionReason { + p := new(ResourceInstanceActionReason) + *p = x + return p +} + +func (x ResourceInstanceActionReason) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ResourceInstanceActionReason) Descriptor() protoreflect.EnumDescriptor { + return file_planfile_proto_enumTypes[1].Descriptor() +} + +func (ResourceInstanceActionReason) Type() protoreflect.EnumType { + return &file_planfile_proto_enumTypes[1] +} + +func (x ResourceInstanceActionReason) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ResourceInstanceActionReason.Descriptor instead. +func (ResourceInstanceActionReason) EnumDescriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{1} +} + type ResourceInstanceChange_ResourceMode int32 const ( @@ -113,11 +169,11 @@ func (x ResourceInstanceChange_ResourceMode) String() string { } func (ResourceInstanceChange_ResourceMode) Descriptor() protoreflect.EnumDescriptor { - return file_planfile_proto_enumTypes[1].Descriptor() + return file_planfile_proto_enumTypes[2].Descriptor() } func (ResourceInstanceChange_ResourceMode) Type() protoreflect.EnumType { - return &file_planfile_proto_enumTypes[1] + return &file_planfile_proto_enumTypes[2] } func (x ResourceInstanceChange_ResourceMode) Number() protoreflect.EnumNumber { @@ -459,6 +515,10 @@ type ResourceInstanceChange struct { // "replace" rather than "update". Empty for any action other than // "replace". RequiredReplace []*Path `protobuf:"bytes,11,rep,name=required_replace,json=requiredReplace,proto3" json:"required_replace,omitempty"` + // Optional extra user-oriented context for why change.Action was chosen. + // This is for user feedback only and never used to drive behavior during + // apply. + ActionReason ResourceInstanceActionReason `protobuf:"varint,12,opt,name=action_reason,json=actionReason,proto3,enum=tfplan.ResourceInstanceActionReason" json:"action_reason,omitempty"` } func (x *ResourceInstanceChange) Reset() { @@ -577,6 +637,13 @@ func (x *ResourceInstanceChange) GetRequiredReplace() []*Path { return nil } +func (x *ResourceInstanceChange) GetActionReason() ResourceInstanceActionReason { + if x != nil { + return x.ActionReason + } + return ResourceInstanceActionReason_NONE +} + type isResourceInstanceChange_InstanceKey interface { isResourceInstanceChange_InstanceKey() } @@ -972,7 +1039,7 @@ var file_planfile_proto_rawDesc = []byte{ 0x65, 0x72, 0x5f, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x13, 0x61, 0x66, 0x74, 0x65, 0x72, 0x53, 0x65, 0x6e, - 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x73, 0x22, 0xb9, 0x03, 0x0a, 0x16, + 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x73, 0x22, 0x84, 0x04, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x6f, 0x64, @@ -997,43 +1064,56 @@ var file_planfile_proto_rawDesc = []byte{ 0x72, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, - 0x22, 0x25, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x6f, 0x64, 0x65, - 0x12, 0x0b, 0x0a, 0x07, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x10, 0x00, 0x12, 0x08, 0x0a, - 0x04, 0x64, 0x61, 0x74, 0x61, 0x10, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x61, - 0x6e, 0x63, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x22, 0x68, 0x0a, 0x0c, 0x4f, 0x75, 0x74, 0x70, 0x75, - 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x06, 0x63, - 0x68, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x74, 0x66, - 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, 0x63, 0x68, 0x61, - 0x6e, 0x67, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, - 0x65, 0x22, 0x28, 0x0a, 0x0c, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x22, 0x1e, 0x0a, 0x04, 0x48, - 0x61, 0x73, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x22, 0xa5, 0x01, 0x0a, 0x04, - 0x50, 0x61, 0x74, 0x68, 0x12, 0x27, 0x0a, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, - 0x68, 0x2e, 0x53, 0x74, 0x65, 0x70, 0x52, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x1a, 0x74, 0x0a, - 0x04, 0x53, 0x74, 0x65, 0x70, 0x12, 0x27, 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, - 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x37, - 0x0a, 0x0b, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, - 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x00, 0x52, 0x0a, 0x65, 0x6c, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, - 0x74, 0x6f, 0x72, 0x2a, 0x70, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x08, 0x0a, - 0x04, 0x4e, 0x4f, 0x4f, 0x50, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x52, 0x45, 0x41, 0x54, - 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x45, 0x41, 0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, - 0x06, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x45, 0x4c, - 0x45, 0x54, 0x45, 0x10, 0x05, 0x12, 0x16, 0x0a, 0x12, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, - 0x54, 0x48, 0x45, 0x4e, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, - 0x12, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f, 0x44, 0x45, 0x4c, - 0x45, 0x54, 0x45, 0x10, 0x07, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x74, 0x65, - 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x73, 0x2f, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x12, 0x49, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x24, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, + 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, + 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x0c, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x25, 0x0a, 0x0c, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, + 0x10, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x6b, + 0x65, 0x79, 0x22, 0x68, 0x0a, 0x0c, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x43, 0x68, 0x61, 0x6e, + 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, + 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1c, + 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x22, 0x28, 0x0a, 0x0c, + 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, 0x0a, 0x07, + 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, + 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x22, 0x1e, 0x0a, 0x04, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16, + 0x0a, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, + 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x22, 0xa5, 0x01, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x12, + 0x27, 0x0a, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x2e, 0x53, 0x74, 0x65, + 0x70, 0x52, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x1a, 0x74, 0x0a, 0x04, 0x53, 0x74, 0x65, 0x70, + 0x12, 0x27, 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x65, 0x6c, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x48, 0x00, 0x52, 0x0a, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b, + 0x65, 0x79, 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2a, 0x70, + 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4f, 0x50, + 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x08, + 0x0a, 0x04, 0x52, 0x45, 0x41, 0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x50, 0x44, 0x41, + 0x54, 0x45, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x05, + 0x12, 0x16, 0x0a, 0x12, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f, + 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x52, 0x45, 0x41, + 0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x07, + 0x2a, 0x80, 0x01, 0x0a, 0x1c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x52, + 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x54, + 0x41, 0x49, 0x4e, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x45, 0x50, 0x4c, + 0x41, 0x43, 0x45, 0x5f, 0x42, 0x59, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x10, 0x02, + 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, + 0x55, 0x53, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x4e, 0x4f, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, + 0x45, 0x10, 0x03, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x74, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1048,47 +1128,49 @@ func file_planfile_proto_rawDescGZIP() []byte { return file_planfile_proto_rawDescData } -var file_planfile_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_planfile_proto_enumTypes = make([]protoimpl.EnumInfo, 3) var file_planfile_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_planfile_proto_goTypes = []interface{}{ (Action)(0), // 0: tfplan.Action - (ResourceInstanceChange_ResourceMode)(0), // 1: tfplan.ResourceInstanceChange.ResourceMode - (*Plan)(nil), // 2: tfplan.Plan - (*Backend)(nil), // 3: tfplan.Backend - (*Change)(nil), // 4: tfplan.Change - (*ResourceInstanceChange)(nil), // 5: tfplan.ResourceInstanceChange - (*OutputChange)(nil), // 6: tfplan.OutputChange - (*DynamicValue)(nil), // 7: tfplan.DynamicValue - (*Hash)(nil), // 8: tfplan.Hash - (*Path)(nil), // 9: tfplan.Path - nil, // 10: tfplan.Plan.VariablesEntry - nil, // 11: tfplan.Plan.ProviderHashesEntry - (*Path_Step)(nil), // 12: tfplan.Path.Step + (ResourceInstanceActionReason)(0), // 1: tfplan.ResourceInstanceActionReason + (ResourceInstanceChange_ResourceMode)(0), // 2: tfplan.ResourceInstanceChange.ResourceMode + (*Plan)(nil), // 3: tfplan.Plan + (*Backend)(nil), // 4: tfplan.Backend + (*Change)(nil), // 5: tfplan.Change + (*ResourceInstanceChange)(nil), // 6: tfplan.ResourceInstanceChange + (*OutputChange)(nil), // 7: tfplan.OutputChange + (*DynamicValue)(nil), // 8: tfplan.DynamicValue + (*Hash)(nil), // 9: tfplan.Hash + (*Path)(nil), // 10: tfplan.Path + nil, // 11: tfplan.Plan.VariablesEntry + nil, // 12: tfplan.Plan.ProviderHashesEntry + (*Path_Step)(nil), // 13: tfplan.Path.Step } var file_planfile_proto_depIdxs = []int32{ - 10, // 0: tfplan.Plan.variables:type_name -> tfplan.Plan.VariablesEntry - 5, // 1: tfplan.Plan.resource_changes:type_name -> tfplan.ResourceInstanceChange - 6, // 2: tfplan.Plan.output_changes:type_name -> tfplan.OutputChange - 11, // 3: tfplan.Plan.provider_hashes:type_name -> tfplan.Plan.ProviderHashesEntry - 3, // 4: tfplan.Plan.backend:type_name -> tfplan.Backend - 7, // 5: tfplan.Backend.config:type_name -> tfplan.DynamicValue + 11, // 0: tfplan.Plan.variables:type_name -> tfplan.Plan.VariablesEntry + 6, // 1: tfplan.Plan.resource_changes:type_name -> tfplan.ResourceInstanceChange + 7, // 2: tfplan.Plan.output_changes:type_name -> tfplan.OutputChange + 12, // 3: tfplan.Plan.provider_hashes:type_name -> tfplan.Plan.ProviderHashesEntry + 4, // 4: tfplan.Plan.backend:type_name -> tfplan.Backend + 8, // 5: tfplan.Backend.config:type_name -> tfplan.DynamicValue 0, // 6: tfplan.Change.action:type_name -> tfplan.Action - 7, // 7: tfplan.Change.values:type_name -> tfplan.DynamicValue - 9, // 8: tfplan.Change.before_sensitive_paths:type_name -> tfplan.Path - 9, // 9: tfplan.Change.after_sensitive_paths:type_name -> tfplan.Path - 1, // 10: tfplan.ResourceInstanceChange.mode:type_name -> tfplan.ResourceInstanceChange.ResourceMode - 4, // 11: tfplan.ResourceInstanceChange.change:type_name -> tfplan.Change - 9, // 12: tfplan.ResourceInstanceChange.required_replace:type_name -> tfplan.Path - 4, // 13: tfplan.OutputChange.change:type_name -> tfplan.Change - 12, // 14: tfplan.Path.steps:type_name -> tfplan.Path.Step - 7, // 15: tfplan.Plan.VariablesEntry.value:type_name -> tfplan.DynamicValue - 8, // 16: tfplan.Plan.ProviderHashesEntry.value:type_name -> tfplan.Hash - 7, // 17: tfplan.Path.Step.element_key:type_name -> tfplan.DynamicValue - 18, // [18:18] is the sub-list for method output_type - 18, // [18:18] is the sub-list for method input_type - 18, // [18:18] is the sub-list for extension type_name - 18, // [18:18] is the sub-list for extension extendee - 0, // [0:18] is the sub-list for field type_name + 8, // 7: tfplan.Change.values:type_name -> tfplan.DynamicValue + 10, // 8: tfplan.Change.before_sensitive_paths:type_name -> tfplan.Path + 10, // 9: tfplan.Change.after_sensitive_paths:type_name -> tfplan.Path + 2, // 10: tfplan.ResourceInstanceChange.mode:type_name -> tfplan.ResourceInstanceChange.ResourceMode + 5, // 11: tfplan.ResourceInstanceChange.change:type_name -> tfplan.Change + 10, // 12: tfplan.ResourceInstanceChange.required_replace:type_name -> tfplan.Path + 1, // 13: tfplan.ResourceInstanceChange.action_reason:type_name -> tfplan.ResourceInstanceActionReason + 5, // 14: tfplan.OutputChange.change:type_name -> tfplan.Change + 13, // 15: tfplan.Path.steps:type_name -> tfplan.Path.Step + 8, // 16: tfplan.Plan.VariablesEntry.value:type_name -> tfplan.DynamicValue + 9, // 17: tfplan.Plan.ProviderHashesEntry.value:type_name -> tfplan.Hash + 8, // 18: tfplan.Path.Step.element_key:type_name -> tfplan.DynamicValue + 19, // [19:19] is the sub-list for method output_type + 19, // [19:19] is the sub-list for method input_type + 19, // [19:19] is the sub-list for extension type_name + 19, // [19:19] is the sub-list for extension extendee + 0, // [0:19] is the sub-list for field type_name } func init() { file_planfile_proto_init() } @@ -1219,7 +1301,7 @@ func file_planfile_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_planfile_proto_rawDesc, - NumEnums: 2, + NumEnums: 3, NumMessages: 11, NumExtensions: 0, NumServices: 0, diff --git a/plans/internal/planproto/planfile.proto b/plans/internal/planproto/planfile.proto index fe5ef8e43..4efa3b538 100644 --- a/plans/internal/planproto/planfile.proto +++ b/plans/internal/planproto/planfile.proto @@ -97,6 +97,17 @@ message Change { repeated Path after_sensitive_paths = 4; } +// ResourceInstanceActionReason sometimes provides some additional user-facing +// context for why a particular action was chosen for a resource instance. +// This is for user feedback only and never used to drive behavior during the +// subsequent apply step. +enum ResourceInstanceActionReason { + NONE = 0; + REPLACE_BECAUSE_TAINTED = 1; + REPLACE_BY_REQUEST = 2; + REPLACE_BECAUSE_CANNOT_UPDATE = 3; +} + message ResourceInstanceChange { // module_path is an address to the module that defined this resource. // module_path is omitted for resources in the root module. For descendent modules @@ -152,6 +163,11 @@ message ResourceInstanceChange { // "replace" rather than "update". Empty for any action other than // "replace". repeated Path required_replace = 11; + + // Optional extra user-oriented context for why change.Action was chosen. + // This is for user feedback only and never used to drive behavior during + // apply. + ResourceInstanceActionReason action_reason = 12; } message OutputChange { diff --git a/plans/planfile/tfplan.go b/plans/planfile/tfplan.go index 1c47de5f5..7cde834ca 100644 --- a/plans/planfile/tfplan.go +++ b/plans/planfile/tfplan.go @@ -207,6 +207,19 @@ func resourceChangeFromTfplan(rawChange *planproto.ResourceInstanceChange) (*pla ret.ChangeSrc = *change + switch rawChange.ActionReason { + case planproto.ResourceInstanceActionReason_NONE: + ret.ActionReason = plans.ResourceInstanceChangeNoReason + case planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_CANNOT_UPDATE: + ret.ActionReason = plans.ResourceInstanceReplaceBecauseCannotUpdate + case planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_TAINTED: + ret.ActionReason = plans.ResourceInstanceReplaceBecauseTainted + case planproto.ResourceInstanceActionReason_REPLACE_BY_REQUEST: + ret.ActionReason = plans.ResourceInstanceReplaceByRequest + default: + return nil, fmt.Errorf("resource has invalid action reason %s", rawChange.ActionReason) + } + if len(rawChange.Private) != 0 { ret.Private = rawChange.Private } @@ -456,6 +469,19 @@ func resourceChangeToTfplan(change *plans.ResourceInstanceChangeSrc) (*planproto } ret.Change = valChange + switch change.ActionReason { + case plans.ResourceInstanceChangeNoReason: + ret.ActionReason = planproto.ResourceInstanceActionReason_NONE + case plans.ResourceInstanceReplaceBecauseCannotUpdate: + ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_CANNOT_UPDATE + case plans.ResourceInstanceReplaceBecauseTainted: + ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_TAINTED + case plans.ResourceInstanceReplaceByRequest: + ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BY_REQUEST + default: + return nil, fmt.Errorf("resource %s has unsupported action reason %s", relAddr, change.ActionReason) + } + if len(change.Private) > 0 { ret.Private = change.Private } diff --git a/plans/planfile/tfplan_test.go b/plans/planfile/tfplan_test.go index f396338ef..8e8b4d7db 100644 --- a/plans/planfile/tfplan_test.go +++ b/plans/planfile/tfplan_test.go @@ -85,6 +85,7 @@ func TestTFPlanRoundTrip(t *testing.T) { RequiredReplace: cty.NewPathSet( cty.GetAttrPath("boop"), ), + ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, }, { Addr: addrs.Resource{ diff --git a/plans/resourceinstancechangeactionreason_string.go b/plans/resourceinstancechangeactionreason_string.go new file mode 100644 index 000000000..0731f6759 --- /dev/null +++ b/plans/resourceinstancechangeactionreason_string.go @@ -0,0 +1,37 @@ +// Code generated by "stringer -type=ResourceInstanceChangeActionReason changes.go"; DO NOT EDIT. + +package plans + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ResourceInstanceChangeNoReason-0] + _ = x[ResourceInstanceReplaceBecauseTainted-84] + _ = x[ResourceInstanceReplaceByRequest-82] + _ = x[ResourceInstanceReplaceBecauseCannotUpdate-70] +} + +const ( + _ResourceInstanceChangeActionReason_name_0 = "ResourceInstanceChangeNoReason" + _ResourceInstanceChangeActionReason_name_1 = "ResourceInstanceReplaceBecauseCannotUpdate" + _ResourceInstanceChangeActionReason_name_2 = "ResourceInstanceReplaceByRequest" + _ResourceInstanceChangeActionReason_name_3 = "ResourceInstanceReplaceBecauseTainted" +) + +func (i ResourceInstanceChangeActionReason) String() string { + switch { + case i == 0: + return _ResourceInstanceChangeActionReason_name_0 + case i == 70: + return _ResourceInstanceChangeActionReason_name_1 + case i == 82: + return _ResourceInstanceChangeActionReason_name_2 + case i == 84: + return _ResourceInstanceChangeActionReason_name_3 + default: + return "ResourceInstanceChangeActionReason(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/terraform/context_plan_test.go b/terraform/context_plan_test.go index beabcc6ee..657c91607 100644 --- a/terraform/context_plan_test.go +++ b/terraform/context_plan_test.go @@ -3641,10 +3641,16 @@ func TestContext2Plan_orphan(t *testing.T) { if res.Action != plans.Delete { t.Fatalf("resource %s should be removed", i) } + if got, want := ric.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } case "aws_instance.foo": if res.Action != plans.Create { t.Fatalf("resource %s should be created", i) } + if got, want := ric.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } checkVals(t, objectVal(t, schema, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "num": cty.NumberIntVal(2), @@ -3722,6 +3728,9 @@ func TestContext2Plan_state(t *testing.T) { if res.Action != plans.Create { t.Fatalf("resource %s should be created", i) } + if got, want := ric.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } checkVals(t, objectVal(t, schema, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("2"), @@ -3731,6 +3740,9 @@ func TestContext2Plan_state(t *testing.T) { if res.Action != plans.Update { t.Fatalf("resource %s should be updated", i) } + if got, want := ric.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } checkVals(t, objectVal(t, schema, map[string]cty.Value{ "id": cty.StringVal("bar"), "num": cty.NullVal(cty.Number), @@ -3747,6 +3759,91 @@ func TestContext2Plan_state(t *testing.T) { } } +func TestContext2Plan_requiresReplace(t *testing.T) { + m := testModule(t, "plan-requires-replace") + p := testProvider("test") + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Block: &configschema.Block{}, + }, + ResourceTypes: map[string]providers.Schema{ + "test_thing": providers.Schema{ + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "v": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + } + p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + return providers.PlanResourceChangeResponse{ + PlannedState: req.ProposedNewState, + RequiresReplace: []cty.Path{ + cty.GetAttrPath("v"), + }, + } + } + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_thing.foo").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"v":"hello"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + ctx := testContext2(t, &ContextOpts{ + Config: m, + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + State: state, + }) + + plan, diags := ctx.Plan() + if diags.HasErrors() { + t.Fatalf("unexpected errors: %s", diags.Err()) + } + + schema := p.GetProviderSchemaResponse.ResourceTypes["test_thing"].Block + ty := schema.ImpliedType() + + if got, want := len(plan.Changes.Resources), 1; got != want { + t.Fatalf("got %d changes; want %d", got, want) + } + + for _, res := range plan.Changes.Resources { + t.Run(res.Addr.String(), func(t *testing.T) { + ric, err := res.Decode(ty) + if err != nil { + t.Fatal(err) + } + + switch i := ric.Addr.String(); i { + case "test_thing.foo": + if got, want := ric.Action, plans.DeleteThenCreate; got != want { + t.Errorf("wrong action\ngot: %s\nwant: %s", got, want) + } + if got, want := ric.ActionReason, plans.ResourceInstanceReplaceBecauseCannotUpdate; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + checkVals(t, objectVal(t, schema, map[string]cty.Value{ + "v": cty.StringVal("goodbye"), + }), ric.After) + default: + t.Fatalf("unexpected resource instance %s", i) + } + }) + } +} + func TestContext2Plan_taint(t *testing.T) { m := testModule(t, "plan-taint") p := testProvider("aws") @@ -3791,28 +3888,36 @@ func TestContext2Plan_taint(t *testing.T) { } for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) - if err != nil { - t.Fatal(err) - } + t.Run(res.Addr.String(), func(t *testing.T) { + ric, err := res.Decode(ty) + if err != nil { + t.Fatal(err) + } - switch i := ric.Addr.String(); i { - case "aws_instance.bar": - if res.Action != plans.DeleteThenCreate { - t.Fatalf("resource %s should be replaced", i) + switch i := ric.Addr.String(); i { + case "aws_instance.bar": + if got, want := res.Action, plans.DeleteThenCreate; got != want { + t.Errorf("wrong action\ngot: %s\nwant: %s", got, want) + } + if got, want := res.ActionReason, plans.ResourceInstanceReplaceBecauseTainted; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + checkVals(t, objectVal(t, schema, map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.StringVal("2"), + "type": cty.UnknownVal(cty.String), + }), ric.After) + case "aws_instance.foo": + if got, want := res.Action, plans.NoOp; got != want { + t.Errorf("wrong action\ngot: %s\nwant: %s", got, want) + } + if got, want := res.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + default: + t.Fatal("unknown instance:", i) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "foo": cty.StringVal("2"), - "type": cty.UnknownVal(cty.String), - }), ric.After) - case "aws_instance.foo": - if res.Action != plans.NoOp { - t.Fatalf("resource %s should not be changed", i) - } - default: - t.Fatal("unknown instance:", i) - } + }) } } @@ -3870,8 +3975,11 @@ func TestContext2Plan_taintIgnoreChanges(t *testing.T) { switch i := ric.Addr.String(); i { case "aws_instance.foo": - if res.Action != plans.DeleteThenCreate { - t.Fatalf("resource %s should be replaced", i) + if got, want := res.Action, plans.DeleteThenCreate; got != want { + t.Errorf("wrong action\ngot: %s\nwant: %s", got, want) + } + if got, want := res.ActionReason, plans.ResourceInstanceReplaceBecauseTainted; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) } checkVals(t, objectVal(t, schema, map[string]cty.Value{ "id": cty.StringVal("foo"), @@ -3950,8 +4058,11 @@ func TestContext2Plan_taintDestroyInterpolatedCountRace(t *testing.T) { switch i := ric.Addr.String(); i { case "aws_instance.foo[0]": - if res.Action != plans.DeleteThenCreate { - t.Fatalf("resource %s should be replaced, not %s", i, res.Action) + if got, want := ric.Action, plans.DeleteThenCreate; got != want { + t.Errorf("wrong action\ngot: %s\nwant: %s", got, want) + } + if got, want := ric.ActionReason, plans.ResourceInstanceReplaceBecauseTainted; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) } checkVals(t, objectVal(t, schema, map[string]cty.Value{ "id": cty.StringVal("bar"), diff --git a/terraform/node_resource_abstract_instance.go b/terraform/node_resource_abstract_instance.go index e27f84e67..5f178947a 100644 --- a/terraform/node_resource_abstract_instance.go +++ b/terraform/node_resource_abstract_instance.go @@ -817,6 +817,7 @@ func (n *NodeAbstractResourceInstance) plan( eq := eqV.IsKnown() && eqV.True() var action plans.Action + var actionReason plans.ResourceInstanceChangeActionReason switch { case priorVal.IsNull(): action = plans.Create @@ -830,6 +831,7 @@ func (n *NodeAbstractResourceInstance) plan( } else { action = plans.DeleteThenCreate } + actionReason = plans.ResourceInstanceReplaceBecauseCannotUpdate default: action = plans.Update // "Delete" is never chosen here, because deletion plans are always @@ -909,6 +911,7 @@ func (n *NodeAbstractResourceInstance) plan( action = plans.DeleteThenCreate } priorVal = priorValTainted + actionReason = plans.ResourceInstanceReplaceBecauseTainted } // If we plan to write or delete sensitive paths from state, @@ -953,6 +956,7 @@ func (n *NodeAbstractResourceInstance) plan( // Marks will be removed when encoding. After: plannedNewVal, }, + ActionReason: actionReason, RequiredReplace: reqRep, } diff --git a/terraform/testdata/plan-requires-replace/main.tf b/terraform/testdata/plan-requires-replace/main.tf new file mode 100644 index 000000000..23cee56b3 --- /dev/null +++ b/terraform/testdata/plan-requires-replace/main.tf @@ -0,0 +1,3 @@ +resource "test_thing" "foo" { + v = "goodbye" +} diff --git a/website/docs/internals/json-format.html.md b/website/docs/internals/json-format.html.md index 88d3e0e75..52b080ef1 100644 --- a/website/docs/internals/json-format.html.md +++ b/website/docs/internals/json-format.html.md @@ -122,7 +122,31 @@ For ease of consumption by callers, the plan representation includes a partial r // "change" describes the change that will be made to the indicated // object. The is detailed in a section below. - "change": + "change": , + + // "action_reason" is some optional extra context about why the + // actions given inside "change" were selected. This is the JSON + // equivalent of annotations shown in the normal plan output like + // "is tainted, so must be replaced" as opposed to just "must be + // replaced". + // + // These reason codes are display hints only and the set of possible + // hints may change over time. Users of this must be prepared to + // encounter unrecognized reasons and treat them as unspecified reasons. + // + // The current set of possible values is: + // - "replace_because_tainted": the object in question is marked as + // "tainted" in the prior state, so Terraform planned to replace it. + // - "replace_because_cannot_update": the provider indicated that one + // of the requested changes isn't possible without replacing the + // existing object with a new object. + // - "replace_by_request": the user explicitly called for this object + // to be replaced as an option when creating the plan, which therefore + // overrode what would have been a "no-op" or "update" action otherwise. + // + // If there is no special reason to note, Terraform will omit this + // property altogether. + action_reason: "replace_because_tainted" } ],