Merge pull request #29597 from hashicorp/alisdair/move-refresh-ui
cli: Improved plan UI for move-only changes
This commit is contained in:
commit
2b6a1be18f
|
@ -189,6 +189,55 @@ func TestOperation_planNoChanges(t *testing.T) {
|
||||||
},
|
},
|
||||||
"If you were expecting these changes then you can apply this plan",
|
"If you were expecting these changes then you can apply this plan",
|
||||||
},
|
},
|
||||||
|
"move-only changes in refresh-only mode": {
|
||||||
|
func(schemas *terraform.Schemas) *plans.Plan {
|
||||||
|
addr := addrs.Resource{
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Type: "test_resource",
|
||||||
|
Name: "somewhere",
|
||||||
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
||||||
|
addrPrev := addrs.Resource{
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Type: "test_resource",
|
||||||
|
Name: "anywhere",
|
||||||
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
||||||
|
schema, _ := schemas.ResourceTypeConfig(
|
||||||
|
addrs.NewDefaultProvider("test"),
|
||||||
|
addr.Resource.Resource.Mode,
|
||||||
|
addr.Resource.Resource.Type,
|
||||||
|
)
|
||||||
|
ty := schema.ImpliedType()
|
||||||
|
rc := &plans.ResourceInstanceChange{
|
||||||
|
Addr: addr,
|
||||||
|
PrevRunAddr: addrPrev,
|
||||||
|
ProviderAddr: addrs.RootModuleInstance.ProviderConfigDefault(
|
||||||
|
addrs.NewDefaultProvider("test"),
|
||||||
|
),
|
||||||
|
Change: plans.Change{
|
||||||
|
Action: plans.NoOp,
|
||||||
|
Before: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("1234"),
|
||||||
|
"foo": cty.StringVal("bar"),
|
||||||
|
}),
|
||||||
|
After: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("1234"),
|
||||||
|
"foo": cty.StringVal("bar"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rcs, err := rc.Encode(ty)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
drs := []*plans.ResourceInstanceChangeSrc{rcs}
|
||||||
|
return &plans.Plan{
|
||||||
|
UIMode: plans.RefreshOnlyMode,
|
||||||
|
Changes: plans.NewChanges(),
|
||||||
|
DriftedResources: drs,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test_resource.anywhere has moved to test_resource.somewhere",
|
||||||
|
},
|
||||||
"drift detected in destroy mode": {
|
"drift detected in destroy mode": {
|
||||||
func(schemas *terraform.Schemas) *plans.Plan {
|
func(schemas *terraform.Schemas) *plans.Plan {
|
||||||
return &plans.Plan{
|
return &plans.Plan{
|
||||||
|
|
|
@ -96,9 +96,24 @@ func (v *PlanJSON) HelpPrompt() {
|
||||||
// The plan renderer is used by the Operation view (for plan and apply
|
// The plan renderer is used by the Operation view (for plan and apply
|
||||||
// commands) and the Show view (for the show command).
|
// commands) and the Show view (for the show command).
|
||||||
func renderPlan(plan *plans.Plan, schemas *terraform.Schemas, view *View) {
|
func renderPlan(plan *plans.Plan, schemas *terraform.Schemas, view *View) {
|
||||||
haveRefreshChanges := len(plan.DriftedResources) > 0
|
// In refresh-only mode, we show all resources marked as drifted,
|
||||||
|
// including those which have moved without other changes. In other plan
|
||||||
|
// modes, move-only changes will be rendered in the planned changes, so
|
||||||
|
// we skip them here.
|
||||||
|
var driftedResources []*plans.ResourceInstanceChangeSrc
|
||||||
|
if plan.UIMode == plans.RefreshOnlyMode {
|
||||||
|
driftedResources = plan.DriftedResources
|
||||||
|
} else {
|
||||||
|
for _, dr := range plan.DriftedResources {
|
||||||
|
if dr.Action != plans.NoOp {
|
||||||
|
driftedResources = append(driftedResources, dr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
haveRefreshChanges := len(driftedResources) > 0
|
||||||
if haveRefreshChanges {
|
if haveRefreshChanges {
|
||||||
renderChangesDetectedByRefresh(plan.DriftedResources, schemas, view)
|
renderChangesDetectedByRefresh(driftedResources, schemas, view)
|
||||||
switch plan.UIMode {
|
switch plan.UIMode {
|
||||||
case plans.RefreshOnlyMode:
|
case plans.RefreshOnlyMode:
|
||||||
view.streams.Println(format.WordWrap(
|
view.streams.Println(format.WordWrap(
|
||||||
|
@ -368,10 +383,6 @@ func renderChangesDetectedByRefresh(drs []*plans.ResourceInstanceChangeSrc, sche
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, rcs := range drs {
|
for _, rcs := range drs {
|
||||||
if rcs.Action == plans.NoOp && !rcs.Moved() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
providerSchema := schemas.ProviderSchema(rcs.ProviderAddr.Provider)
|
providerSchema := schemas.ProviderSchema(rcs.ProviderAddr.Provider)
|
||||||
if providerSchema == nil {
|
if providerSchema == nil {
|
||||||
// Should never happen
|
// Should never happen
|
||||||
|
|
|
@ -471,7 +471,7 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State,
|
||||||
func (c *Context) driftedResources(config *configs.Config, oldState, newState *states.State, moves map[addrs.UniqueKey]refactoring.MoveResult) ([]*plans.ResourceInstanceChangeSrc, tfdiags.Diagnostics) {
|
func (c *Context) driftedResources(config *configs.Config, oldState, newState *states.State, moves map[addrs.UniqueKey]refactoring.MoveResult) ([]*plans.ResourceInstanceChangeSrc, tfdiags.Diagnostics) {
|
||||||
var diags tfdiags.Diagnostics
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
if newState.ManagedResourcesEqual(oldState) {
|
if newState.ManagedResourcesEqual(oldState) && len(moves) == 0 {
|
||||||
// Nothing to do, because we only detect and report drift for managed
|
// Nothing to do, because we only detect and report drift for managed
|
||||||
// resource instances.
|
// resource instances.
|
||||||
return nil, diags
|
return nil, diags
|
||||||
|
@ -499,6 +499,14 @@ func (c *Context) driftedResources(config *configs.Config, oldState, newState *s
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
addr := rs.Addr.Instance(key)
|
addr := rs.Addr.Instance(key)
|
||||||
|
|
||||||
|
// Previous run address defaults to the current address, but
|
||||||
|
// can differ if the resource moved before refreshing
|
||||||
|
prevRunAddr := addr
|
||||||
|
if move, ok := moves[addr.UniqueKey()]; ok {
|
||||||
|
prevRunAddr = move.From
|
||||||
|
}
|
||||||
|
|
||||||
newIS := newState.ResourceInstance(addr)
|
newIS := newState.ResourceInstance(addr)
|
||||||
|
|
||||||
schema, _ := schemas.ResourceTypeConfig(
|
schema, _ := schemas.ResourceTypeConfig(
|
||||||
|
@ -547,20 +555,30 @@ func (c *Context) driftedResources(config *configs.Config, oldState, newState *s
|
||||||
newVal = cty.NullVal(ty)
|
newVal = cty.NullVal(ty)
|
||||||
}
|
}
|
||||||
|
|
||||||
if oldVal.RawEquals(newVal) {
|
if oldVal.RawEquals(newVal) && addr.Equal(prevRunAddr) {
|
||||||
// No drift if the two values are semantically equivalent
|
// No drift if the two values are semantically equivalent
|
||||||
|
// and no move has happened
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can only detect updates and deletes as drift.
|
// We can detect three types of changes after refreshing state,
|
||||||
action := plans.Update
|
// only two of which are easily understood as "drift":
|
||||||
if newVal.IsNull() {
|
//
|
||||||
|
// - Resources which were deleted outside of Terraform;
|
||||||
|
// - Resources where the object value has changed outside of
|
||||||
|
// Terraform;
|
||||||
|
// - Resources which have been moved without other changes.
|
||||||
|
//
|
||||||
|
// All of these are returned as drift, to allow refresh-only plans
|
||||||
|
// to present a full set of changes which will be applied.
|
||||||
|
var action plans.Action
|
||||||
|
switch {
|
||||||
|
case newVal.IsNull():
|
||||||
action = plans.Delete
|
action = plans.Delete
|
||||||
}
|
case !oldVal.RawEquals(newVal):
|
||||||
|
action = plans.Update
|
||||||
prevRunAddr := addr
|
default:
|
||||||
if move, ok := moves[addr.UniqueKey()]; ok {
|
action = plans.NoOp
|
||||||
prevRunAddr = move.From
|
|
||||||
}
|
}
|
||||||
|
|
||||||
change := &plans.ResourceInstanceChange{
|
change := &plans.ResourceInstanceChange{
|
||||||
|
|
|
@ -984,7 +984,82 @@ The -target option is not for routine use, and is provided only for exceptional
|
||||||
if diff := cmp.Diff(wantDiags, gotDiags); diff != "" {
|
if diff := cmp.Diff(wantDiags, gotDiags); diff != "" {
|
||||||
t.Errorf("wrong diagnostics\n%s", diff)
|
t.Errorf("wrong diagnostics\n%s", diff)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext2Plan_movedResourceRefreshOnly(t *testing.T) {
|
||||||
|
addrA := mustResourceInstanceAddr("test_object.a")
|
||||||
|
addrB := mustResourceInstanceAddr("test_object.b")
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
resource "test_object" "b" {
|
||||||
|
}
|
||||||
|
|
||||||
|
moved {
|
||||||
|
from = test_object.a
|
||||||
|
to = test_object.b
|
||||||
|
}
|
||||||
|
|
||||||
|
terraform {
|
||||||
|
experiments = [config_driven_move]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
state := states.BuildState(func(s *states.SyncState) {
|
||||||
|
// The prior state tracks test_object.a, which we should treat as
|
||||||
|
// test_object.b because of the "moved" block in the config.
|
||||||
|
s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{
|
||||||
|
AttrsJSON: []byte(`{}`),
|
||||||
|
Status: states.ObjectReady,
|
||||||
|
}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
|
||||||
|
})
|
||||||
|
|
||||||
|
p := simpleMockProvider()
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
plan, diags := ctx.Plan(m, state, &PlanOpts{
|
||||||
|
Mode: plans.RefreshOnlyMode,
|
||||||
|
})
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run(addrA.String(), func(t *testing.T) {
|
||||||
|
instPlan := plan.Changes.ResourceInstance(addrA)
|
||||||
|
if instPlan != nil {
|
||||||
|
t.Fatalf("unexpected plan for %s; should've moved to %s", addrA, addrB)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run(addrB.String(), func(t *testing.T) {
|
||||||
|
instPlan := plan.Changes.ResourceInstance(addrB)
|
||||||
|
if instPlan != nil {
|
||||||
|
t.Fatalf("unexpected plan for %s", addrB)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("drift", func(t *testing.T) {
|
||||||
|
var drifted *plans.ResourceInstanceChangeSrc
|
||||||
|
for _, dr := range plan.DriftedResources {
|
||||||
|
if dr.Addr.Equal(addrB) {
|
||||||
|
drifted = dr
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if drifted == nil {
|
||||||
|
t.Fatalf("instance %s is missing from the drifted resource changes", addrB)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := drifted.PrevRunAddr, addrA; !got.Equal(want) {
|
||||||
|
t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
if got, want := drifted.Action, plans.NoOp; got != want {
|
||||||
|
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue