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" } ],