command: Render "moved" annotations in plan UI
For resources which are planned to move, render the previous run address as additional information in the plan UI. For the case of a move-only resource (which otherwise is unchanged), we also render that as a planned change, but without any corresponding action symbol. If all changes in the plan are moves without changes, the plan is no longer considered "empty". In this case, we skip rendering the action symbols in the UI.
This commit is contained in:
parent
a9dfe7ba7b
commit
29999c1d6f
|
@ -72,13 +72,27 @@ func ResourceChange(
|
||||||
// Some extra context about this unusual situation.
|
// Some extra context about this unusual situation.
|
||||||
buf.WriteString(color.Color("\n # (left over from a partially-failed replacement of this instance)"))
|
buf.WriteString(color.Color("\n # (left over from a partially-failed replacement of this instance)"))
|
||||||
}
|
}
|
||||||
|
case plans.NoOp:
|
||||||
|
if change.Moved() {
|
||||||
|
buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] has moved to [bold]%s[reset]", change.PrevRunAddr.String(), dispAddr)))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
default:
|
default:
|
||||||
// should never happen, since the above is exhaustive
|
// should never happen, since the above is exhaustive
|
||||||
buf.WriteString(fmt.Sprintf("%s has an action the plan renderer doesn't support (this is a bug)", dispAddr))
|
buf.WriteString(fmt.Sprintf("%s has an action the plan renderer doesn't support (this is a bug)", dispAddr))
|
||||||
}
|
}
|
||||||
buf.WriteString(color.Color("[reset]\n"))
|
buf.WriteString(color.Color("[reset]\n"))
|
||||||
|
|
||||||
buf.WriteString(color.Color(DiffActionSymbol(change.Action)) + " ")
|
if change.Moved() && change.Action != plans.NoOp {
|
||||||
|
buf.WriteString(color.Color(fmt.Sprintf("[bold] # [reset]([bold]%s[reset] has moved to [bold]%s[reset])\n", change.PrevRunAddr.String(), dispAddr)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if change.Moved() && change.Action == plans.NoOp {
|
||||||
|
buf.WriteString(" ")
|
||||||
|
} else {
|
||||||
|
buf.WriteString(color.Color(DiffActionSymbol(change.Action)) + " ")
|
||||||
|
}
|
||||||
|
|
||||||
switch addr.Resource.Resource.Mode {
|
switch addr.Resource.Resource.Mode {
|
||||||
case addrs.ManagedResourceMode:
|
case addrs.ManagedResourceMode:
|
||||||
|
|
|
@ -4448,6 +4448,79 @@ func TestResourceChange_sensitiveVariable(t *testing.T) {
|
||||||
runTestCases(t, testCases)
|
runTestCases(t, testCases)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResourceChange_moved(t *testing.T) {
|
||||||
|
prevRunAddr := addrs.Resource{
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Type: "test_instance",
|
||||||
|
Name: "previous",
|
||||||
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
||||||
|
|
||||||
|
testCases := map[string]testCase{
|
||||||
|
"moved and updated": {
|
||||||
|
PrevRunAddr: prevRunAddr,
|
||||||
|
Action: plans.Update,
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Before: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("12345"),
|
||||||
|
"foo": cty.StringVal("hello"),
|
||||||
|
"bar": cty.StringVal("baz"),
|
||||||
|
}),
|
||||||
|
After: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("12345"),
|
||||||
|
"foo": cty.StringVal("hello"),
|
||||||
|
"bar": cty.StringVal("boop"),
|
||||||
|
}),
|
||||||
|
Schema: &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"id": {Type: cty.String, Computed: true},
|
||||||
|
"foo": {Type: cty.String, Optional: true},
|
||||||
|
"bar": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RequiredReplace: cty.NewPathSet(),
|
||||||
|
ExpectedOutput: ` # test_instance.example will be updated in-place
|
||||||
|
# (test_instance.previous has moved to test_instance.example)
|
||||||
|
~ resource "test_instance" "example" {
|
||||||
|
~ bar = "baz" -> "boop"
|
||||||
|
id = "12345"
|
||||||
|
# (1 unchanged attribute hidden)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"moved without changes": {
|
||||||
|
PrevRunAddr: prevRunAddr,
|
||||||
|
Action: plans.NoOp,
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Before: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("12345"),
|
||||||
|
"foo": cty.StringVal("hello"),
|
||||||
|
"bar": cty.StringVal("baz"),
|
||||||
|
}),
|
||||||
|
After: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("12345"),
|
||||||
|
"foo": cty.StringVal("hello"),
|
||||||
|
"bar": cty.StringVal("baz"),
|
||||||
|
}),
|
||||||
|
Schema: &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"id": {Type: cty.String, Computed: true},
|
||||||
|
"foo": {Type: cty.String, Optional: true},
|
||||||
|
"bar": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RequiredReplace: cty.NewPathSet(),
|
||||||
|
ExpectedOutput: ` # test_instance.previous has moved to test_instance.example
|
||||||
|
resource "test_instance" "example" {
|
||||||
|
id = "12345"
|
||||||
|
# (2 unchanged attributes hidden)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runTestCases(t, testCases)
|
||||||
|
}
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
Action plans.Action
|
Action plans.Action
|
||||||
ActionReason plans.ResourceInstanceChangeActionReason
|
ActionReason plans.ResourceInstanceChangeActionReason
|
||||||
|
@ -4460,6 +4533,7 @@ type testCase struct {
|
||||||
Schema *configschema.Block
|
Schema *configschema.Block
|
||||||
RequiredReplace cty.PathSet
|
RequiredReplace cty.PathSet
|
||||||
ExpectedOutput string
|
ExpectedOutput string
|
||||||
|
PrevRunAddr addrs.AbsResourceInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
func runTestCases(t *testing.T, testCases map[string]testCase) {
|
func runTestCases(t *testing.T, testCases map[string]testCase) {
|
||||||
|
@ -4493,13 +4567,23 @@ func runTestCases(t *testing.T, testCases map[string]testCase) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addr := addrs.Resource{
|
||||||
|
Mode: tc.Mode,
|
||||||
|
Type: "test_instance",
|
||||||
|
Name: "example",
|
||||||
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
||||||
|
|
||||||
|
prevRunAddr := tc.PrevRunAddr
|
||||||
|
// If no previous run address is given, reuse the current address
|
||||||
|
// to make initialization easier
|
||||||
|
if prevRunAddr.Resource.Resource.Type == "" {
|
||||||
|
prevRunAddr = addr
|
||||||
|
}
|
||||||
|
|
||||||
change := &plans.ResourceInstanceChangeSrc{
|
change := &plans.ResourceInstanceChangeSrc{
|
||||||
Addr: addrs.Resource{
|
Addr: addr,
|
||||||
Mode: tc.Mode,
|
PrevRunAddr: prevRunAddr,
|
||||||
Type: "test_instance",
|
DeposedKey: tc.DeposedKey,
|
||||||
Name: "example",
|
|
||||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
|
||||||
DeposedKey: tc.DeposedKey,
|
|
||||||
ProviderAddr: addrs.AbsProviderConfig{
|
ProviderAddr: addrs.AbsProviderConfig{
|
||||||
Provider: addrs.NewDefaultProvider("test"),
|
Provider: addrs.NewDefaultProvider("test"),
|
||||||
Module: addrs.RootModule,
|
Module: addrs.RootModule,
|
||||||
|
|
|
@ -116,7 +116,7 @@ func renderPlan(plan *plans.Plan, schemas *terraform.Schemas, view *View) {
|
||||||
counts := map[plans.Action]int{}
|
counts := map[plans.Action]int{}
|
||||||
var rChanges []*plans.ResourceInstanceChangeSrc
|
var rChanges []*plans.ResourceInstanceChangeSrc
|
||||||
for _, change := range plan.Changes.Resources {
|
for _, change := range plan.Changes.Resources {
|
||||||
if change.Action == plans.NoOp {
|
if change.Action == plans.NoOp && !change.Moved() {
|
||||||
continue // We don't show anything for no-op changes
|
continue // We don't show anything for no-op changes
|
||||||
}
|
}
|
||||||
if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode {
|
if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode {
|
||||||
|
@ -125,7 +125,11 @@ func renderPlan(plan *plans.Plan, schemas *terraform.Schemas, view *View) {
|
||||||
}
|
}
|
||||||
|
|
||||||
rChanges = append(rChanges, change)
|
rChanges = append(rChanges, change)
|
||||||
counts[change.Action]++
|
|
||||||
|
// Don't count move-only changes
|
||||||
|
if change.Action != plans.NoOp {
|
||||||
|
counts[change.Action]++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var changedRootModuleOutputs []*plans.OutputChangeSrc
|
var changedRootModuleOutputs []*plans.OutputChangeSrc
|
||||||
for _, output := range plan.Changes.Outputs {
|
for _, output := range plan.Changes.Outputs {
|
||||||
|
@ -138,7 +142,7 @@ func renderPlan(plan *plans.Plan, schemas *terraform.Schemas, view *View) {
|
||||||
changedRootModuleOutputs = append(changedRootModuleOutputs, output)
|
changedRootModuleOutputs = append(changedRootModuleOutputs, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(counts) == 0 && len(changedRootModuleOutputs) == 0 {
|
if len(rChanges) == 0 && len(changedRootModuleOutputs) == 0 {
|
||||||
// If we didn't find any changes to report at all then this is a
|
// If we didn't find any changes to report at all then this is a
|
||||||
// "No changes" plan. How we'll present this depends on whether
|
// "No changes" plan. How we'll present this depends on whether
|
||||||
// the plan is "applyable" and, if so, whether it had refresh changes
|
// the plan is "applyable" and, if so, whether it had refresh changes
|
||||||
|
@ -225,7 +229,7 @@ func renderPlan(plan *plans.Plan, schemas *terraform.Schemas, view *View) {
|
||||||
view.streams.Println("")
|
view.streams.Println("")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(counts) != 0 {
|
if len(counts) > 0 {
|
||||||
headerBuf := &bytes.Buffer{}
|
headerBuf := &bytes.Buffer{}
|
||||||
fmt.Fprintf(headerBuf, "\n%s\n", strings.TrimSpace(format.WordWrap(planHeaderIntro, view.outputColumns())))
|
fmt.Fprintf(headerBuf, "\n%s\n", strings.TrimSpace(format.WordWrap(planHeaderIntro, view.outputColumns())))
|
||||||
if counts[plans.Create] > 0 {
|
if counts[plans.Create] > 0 {
|
||||||
|
@ -247,9 +251,11 @@ func renderPlan(plan *plans.Plan, schemas *terraform.Schemas, view *View) {
|
||||||
fmt.Fprintf(headerBuf, "%s read (data resources)\n", format.DiffActionSymbol(plans.Read))
|
fmt.Fprintf(headerBuf, "%s read (data resources)\n", format.DiffActionSymbol(plans.Read))
|
||||||
}
|
}
|
||||||
|
|
||||||
view.streams.Println(view.colorize.Color(headerBuf.String()))
|
view.streams.Print(view.colorize.Color(headerBuf.String()))
|
||||||
|
}
|
||||||
|
|
||||||
view.streams.Printf("Terraform will perform the following actions:\n\n")
|
if len(rChanges) > 0 {
|
||||||
|
view.streams.Printf("\nTerraform will perform the following actions:\n\n")
|
||||||
|
|
||||||
// Note: we're modifying the backing slice of this plan object in-place
|
// Note: we're modifying the backing slice of this plan object in-place
|
||||||
// here. The ordering of resource changes in a plan is not significant,
|
// here. The ordering of resource changes in a plan is not significant,
|
||||||
|
@ -265,7 +271,7 @@ func renderPlan(plan *plans.Plan, schemas *terraform.Schemas, view *View) {
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, rcs := range rChanges {
|
for _, rcs := range rChanges {
|
||||||
if rcs.Action == plans.NoOp {
|
if rcs.Action == plans.NoOp && !rcs.Moved() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,12 +63,15 @@ func testPlan(t *testing.T) *plans.Plan {
|
||||||
}
|
}
|
||||||
|
|
||||||
changes := plans.NewChanges()
|
changes := plans.NewChanges()
|
||||||
|
addr := addrs.Resource{
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Type: "test_resource",
|
||||||
|
Name: "foo",
|
||||||
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
||||||
|
|
||||||
changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{
|
changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{
|
||||||
Addr: addrs.Resource{
|
Addr: addr,
|
||||||
Mode: addrs.ManagedResourceMode,
|
PrevRunAddr: addr,
|
||||||
Type: "test_resource",
|
|
||||||
Name: "foo",
|
|
||||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
|
||||||
ProviderAddr: addrs.AbsProviderConfig{
|
ProviderAddr: addrs.AbsProviderConfig{
|
||||||
Provider: addrs.NewDefaultProvider("test"),
|
Provider: addrs.NewDefaultProvider("test"),
|
||||||
Module: addrs.RootModule,
|
Module: addrs.RootModule,
|
||||||
|
|
|
@ -33,7 +33,7 @@ func NewChanges() *Changes {
|
||||||
|
|
||||||
func (c *Changes) Empty() bool {
|
func (c *Changes) Empty() bool {
|
||||||
for _, res := range c.Resources {
|
for _, res := range c.Resources {
|
||||||
if res.Action != NoOp {
|
if res.Action != NoOp || res.Moved() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,6 +125,10 @@ func (rcs *ResourceInstanceChangeSrc) DeepCopy() *ResourceInstanceChangeSrc {
|
||||||
return &ret
|
return &ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rcs *ResourceInstanceChangeSrc) Moved() bool {
|
||||||
|
return !rcs.Addr.Equal(rcs.PrevRunAddr)
|
||||||
|
}
|
||||||
|
|
||||||
// OutputChangeSrc describes a change to an output value.
|
// OutputChangeSrc describes a change to an output value.
|
||||||
type OutputChangeSrc struct {
|
type OutputChangeSrc struct {
|
||||||
// Addr is the absolute address of the output value that the change
|
// Addr is the absolute address of the output value that the change
|
||||||
|
|
|
@ -4,10 +4,127 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
"github.com/hashicorp/terraform/internal/lang/marks"
|
"github.com/hashicorp/terraform/internal/lang/marks"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestChangesEmpty(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
changes *Changes
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
"no changes": {
|
||||||
|
&Changes{},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
"resource change": {
|
||||||
|
&Changes{
|
||||||
|
Resources: []*ResourceInstanceChangeSrc{
|
||||||
|
{
|
||||||
|
Addr: addrs.Resource{
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Type: "test_thing",
|
||||||
|
Name: "woot",
|
||||||
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||||
|
PrevRunAddr: addrs.Resource{
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Type: "test_thing",
|
||||||
|
Name: "woot",
|
||||||
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||||
|
ChangeSrc: ChangeSrc{
|
||||||
|
Action: Update,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
"resource change with no-op action": {
|
||||||
|
&Changes{
|
||||||
|
Resources: []*ResourceInstanceChangeSrc{
|
||||||
|
{
|
||||||
|
Addr: addrs.Resource{
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Type: "test_thing",
|
||||||
|
Name: "woot",
|
||||||
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||||
|
PrevRunAddr: addrs.Resource{
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Type: "test_thing",
|
||||||
|
Name: "woot",
|
||||||
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||||
|
ChangeSrc: ChangeSrc{
|
||||||
|
Action: NoOp,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
"resource moved with no-op change": {
|
||||||
|
&Changes{
|
||||||
|
Resources: []*ResourceInstanceChangeSrc{
|
||||||
|
{
|
||||||
|
Addr: addrs.Resource{
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Type: "test_thing",
|
||||||
|
Name: "woot",
|
||||||
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||||
|
PrevRunAddr: addrs.Resource{
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Type: "test_thing",
|
||||||
|
Name: "toot",
|
||||||
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||||
|
ChangeSrc: ChangeSrc{
|
||||||
|
Action: NoOp,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
"output change": {
|
||||||
|
&Changes{
|
||||||
|
Outputs: []*OutputChangeSrc{
|
||||||
|
{
|
||||||
|
Addr: addrs.OutputValue{
|
||||||
|
Name: "result",
|
||||||
|
}.Absolute(addrs.RootModuleInstance),
|
||||||
|
ChangeSrc: ChangeSrc{
|
||||||
|
Action: Update,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
"output change no-op": {
|
||||||
|
&Changes{
|
||||||
|
Outputs: []*OutputChangeSrc{
|
||||||
|
{
|
||||||
|
Addr: addrs.OutputValue{
|
||||||
|
Name: "result",
|
||||||
|
}.Absolute(addrs.RootModuleInstance),
|
||||||
|
ChangeSrc: ChangeSrc{
|
||||||
|
Action: NoOp,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
if got, want := tc.changes.Empty(), tc.want; got != want {
|
||||||
|
t.Fatalf("unexpected result: got %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestChangeEncodeSensitive(t *testing.T) {
|
func TestChangeEncodeSensitive(t *testing.T) {
|
||||||
testVals := []cty.Value{
|
testVals := []cty.Value{
|
||||||
cty.ObjectVal(map[string]cty.Value{
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
|
Loading…
Reference in New Issue