cli: Omit move-only drift, except for refresh-only

The set of drifted resources now includes move-only changes, where the
object value is identical but a move has been executed. In normal
operation, we previousl displayed these moves twice: once as part of
drift output, and once as part of planned changes.

As of this commit we omit move-only changes from drift display, except
for refresh-only plans. This fixes the redundant output.
This commit is contained in:
Alisdair McDiarmid 2021-09-16 12:44:13 -04:00
parent 61b2d8e3fe
commit 638784b195
4 changed files with 142 additions and 7 deletions

View File

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

View File

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

View File

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

View File

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