diff --git a/internal/command/views/operation_test.go b/internal/command/views/operation_test.go index b2e4f8f8e..56ced3577 100644 --- a/internal/command/views/operation_test.go +++ b/internal/command/views/operation_test.go @@ -189,6 +189,55 @@ func TestOperation_planNoChanges(t *testing.T) { }, "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": { func(schemas *terraform.Schemas) *plans.Plan { return &plans.Plan{ diff --git a/internal/command/views/plan.go b/internal/command/views/plan.go index 175c26369..b861ddcfd 100644 --- a/internal/command/views/plan.go +++ b/internal/command/views/plan.go @@ -96,9 +96,24 @@ func (v *PlanJSON) HelpPrompt() { // The plan renderer is used by the Operation view (for plan and apply // commands) and the Show view (for the show command). 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 { - renderChangesDetectedByRefresh(plan.DriftedResources, schemas, view) + renderChangesDetectedByRefresh(driftedResources, schemas, view) switch plan.UIMode { case plans.RefreshOnlyMode: view.streams.Println(format.WordWrap( @@ -368,10 +383,6 @@ func renderChangesDetectedByRefresh(drs []*plans.ResourceInstanceChangeSrc, sche }) for _, rcs := range drs { - if rcs.Action == plans.NoOp && !rcs.Moved() { - continue - } - providerSchema := schemas.ProviderSchema(rcs.ProviderAddr.Provider) if providerSchema == nil { // Should never happen diff --git a/internal/terraform/context_plan.go b/internal/terraform/context_plan.go index 20124bd88..3735dd91c 100644 --- a/internal/terraform/context_plan.go +++ b/internal/terraform/context_plan.go @@ -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) { 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 // resource instances. return nil, diags diff --git a/internal/terraform/context_plan2_test.go b/internal/terraform/context_plan2_test.go index 46185ae22..1167f234c 100644 --- a/internal/terraform/context_plan2_test.go +++ b/internal/terraform/context_plan2_test.go @@ -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 != "" { 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) + } }) }