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.
This commit is contained in:
Martin Atkins 2021-04-28 12:02:34 -07:00
parent 2c4bd08fd9
commit b802237e03
20 changed files with 667 additions and 236 deletions

View File

@ -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:

View File

@ -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"
@ -144,6 +138,7 @@ func TestResourceChange_primitiveTypes(t *testing.T) {
},
"string force-new update": {
Action: plans.DeleteThenCreate,
ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
@ -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,9 +369,10 @@ new line
`,
},
// tainted resources
// tainted objects
"replace tainted resource": {
Action: plans.DeleteThenCreate,
ActionReason: plans.ResourceInstanceReplaceBecauseTainted,
Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
@ -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
@ -412,6 +400,7 @@ new line
},
"force replacement with empty before value": {
Action: plans.DeleteThenCreate,
ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("name"),
@ -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
@ -440,6 +428,7 @@ new line
},
"force replacement with empty before value legacy": {
Action: plans.DeleteThenCreate,
ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("name"),
@ -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)
@ -694,6 +676,7 @@ func TestResourceChange_JSON(t *testing.T) {
},
"force-new update": {
Action: plans.DeleteThenCreate,
ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
@ -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)
@ -759,6 +740,7 @@ func TestResourceChange_JSON(t *testing.T) {
},
"force-new update (whitespace change)": {
Action: plans.DeleteThenCreate,
ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
@ -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)
@ -1258,6 +1226,7 @@ func TestResourceChange_primitiveList(t *testing.T) {
},
"force-new update - insertion": {
Action: plans.DeleteThenCreate,
ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
@ -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)
@ -1699,6 +1657,7 @@ func TestResourceChange_primitiveSet(t *testing.T) {
},
"force-new update - insertion": {
Action: plans.DeleteThenCreate,
ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
@ -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)
@ -2062,6 +2012,7 @@ func TestResourceChange_map(t *testing.T) {
},
"force-new update - insertion": {
Action: plans.DeleteThenCreate,
ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
@ -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" {
@ -2412,6 +2355,7 @@ func TestResourceChange_nestedList(t *testing.T) {
},
"force-new update (inside blocks)": {
Action: plans.DeleteThenCreate,
ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
@ -2455,7 +2399,6 @@ func TestResourceChange_nestedList(t *testing.T) {
cty.GetAttrStep{Name: "mount_point"},
},
),
Tainted: false,
Schema: testSchema(configschema.NestingList),
ExpectedOutput: ` # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
@ -2476,6 +2419,7 @@ func TestResourceChange_nestedList(t *testing.T) {
},
"force-new update (whole block)": {
Action: plans.DeleteThenCreate,
ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
@ -2511,7 +2455,6 @@ 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),
ExpectedOutput: ` # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
@ -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" {
@ -2781,6 +2719,7 @@ func TestResourceChange_nestedSet(t *testing.T) {
},
"force-new update (whole block)": {
Action: plans.DeleteThenCreate,
ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
@ -2816,7 +2755,6 @@ 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),
ExpectedOutput: ` # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
@ -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" {
@ -3074,6 +3008,7 @@ func TestResourceChange_nestedMap(t *testing.T) {
},
"force-new update (whole block)": {
Action: plans.DeleteThenCreate,
ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
@ -3117,7 +3052,6 @@ func TestResourceChange_nestedMap(t *testing.T) {
},
cty.Path{cty.GetAttrStep{Name: "disks"}},
),
Tainted: false,
Schema: testSchema(configschema.NestingMap),
ExpectedOutput: ` # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
@ -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))

View File

@ -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)
}

View File

@ -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"`
}

View File

@ -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 {

View File

@ -0,0 +1,3 @@
resource "test_instance" "test" {
ami = "force-replace"
}

View File

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

View File

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

View File

@ -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,
))

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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 {

View File

@ -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
}

View File

@ -85,6 +85,7 @@ func TestTFPlanRoundTrip(t *testing.T) {
RequiredReplace: cty.NewPathSet(
cty.GetAttrPath("boop"),
),
ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
},
{
Addr: addrs.Resource{

View File

@ -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) + ")"
}
}

View File

@ -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,6 +3888,7 @@ func TestContext2Plan_taint(t *testing.T) {
}
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)
@ -3798,8 +3896,11 @@ func TestContext2Plan_taint(t *testing.T) {
switch i := ric.Addr.String(); i {
case "aws_instance.bar":
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.UnknownVal(cty.String),
@ -3807,12 +3908,16 @@ func TestContext2Plan_taint(t *testing.T) {
"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)
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)
}
})
}
}
@ -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"),

View File

@ -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,
}

View File

@ -0,0 +1,3 @@
resource "test_thing" "foo" {
v = "goodbye"
}

View File

@ -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 <change-representation> is detailed in a section below.
"change": <change-representation>
"change": <change-representation>,
// "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"
}
],