diff --git a/internal/plans/plan.go b/internal/plans/plan.go index 68e60ad98a..a96a056480 100644 --- a/internal/plans/plan.go +++ b/internal/plans/plan.go @@ -31,6 +31,7 @@ type Plan struct { VariableValues map[string]DynamicValue Changes *Changes + DriftedResources []*ResourceInstanceChangeSrc TargetAddrs []addrs.Targetable ForceReplaceAddrs []addrs.AbsResourceInstance ProviderSHA256s map[string][]byte diff --git a/internal/terraform/context_plan.go b/internal/terraform/context_plan.go index 248673254c..0326e08330 100644 --- a/internal/terraform/context_plan.go +++ b/internal/terraform/context_plan.go @@ -347,11 +347,17 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, r diags = diags.Append(walkDiags) diags = diags.Append(c.postPlanValidateMoves(config, moveStmts, walker.InstanceExpander.AllInstances())) + prevRunState = walker.PrevRunState.Close() + priorState := walker.RefreshState.Close() + driftedResources, driftDiags := c.driftedResources(config, prevRunState, priorState, moveResults) + diags = diags.Append(driftDiags) + plan := &plans.Plan{ - UIMode: opts.Mode, - Changes: changes, - PriorState: walker.RefreshState.Close(), - PrevRunState: walker.PrevRunState.Close(), + UIMode: opts.Mode, + Changes: changes, + DriftedResources: driftedResources, + PrevRunState: prevRunState, + PriorState: priorState, // Other fields get populated by Context.Plan after we return } @@ -398,6 +404,126 @@ 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) { + // Nothing to do, because we only detect and report drift for managed + // resource instances. + return nil, diags + } + + schemas, schemaDiags := c.Schemas(config, newState) + diags = diags.Append(schemaDiags) + if diags.HasErrors() { + return nil, diags + } + + var drs []*plans.ResourceInstanceChangeSrc + + for _, ms := range oldState.Modules { + for _, rs := range ms.Resources { + if rs.Addr.Resource.Mode != addrs.ManagedResourceMode { + // Drift reporting is only for managed resources + continue + } + + provider := rs.ProviderConfig.Provider + for key, oldIS := range rs.Instances { + if oldIS.Current == nil { + // Not interested in instances that only have deposed objects + continue + } + addr := rs.Addr.Instance(key) + newIS := newState.ResourceInstance(addr) + + schema, _ := schemas.ResourceTypeConfig( + provider, + addr.Resource.Resource.Mode, + addr.Resource.Resource.Type, + ) + if schema == nil { + // This should never happen, but just in case + return nil, diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Missing resource schema from provider", + fmt.Sprintf("No resource schema found for %s.", addr.Resource.Resource.Type), + )) + } + ty := schema.ImpliedType() + + oldObj, err := oldIS.Current.Decode(ty) + if err != nil { + // This should also never happen + return nil, diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to decode resource from state", + fmt.Sprintf("Error decoding %q from previous state: %s", addr.String(), err), + )) + } + + var newObj *states.ResourceInstanceObject + if newIS != nil && newIS.Current != nil { + newObj, err = newIS.Current.Decode(ty) + if err != nil { + // This should also never happen + return nil, diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to decode resource from state", + fmt.Sprintf("Error decoding %q from prior state: %s", addr.String(), err), + )) + } + } + + var oldVal, newVal cty.Value + oldVal = oldObj.Value + if newObj != nil { + newVal = newObj.Value + } else { + newVal = cty.NullVal(ty) + } + + if oldVal.RawEquals(newVal) { + // No drift if the two values are semantically equivalent + continue + } + + // We can only detect updates and deletes as drift. + action := plans.Update + if newVal.IsNull() { + action = plans.Delete + } + + prevRunAddr := addr + if move, ok := moves[addr.UniqueKey()]; ok { + prevRunAddr = move.From + } + + change := &plans.ResourceInstanceChange{ + Addr: addr, + PrevRunAddr: prevRunAddr, + ProviderAddr: rs.ProviderConfig, + Change: plans.Change{ + Action: action, + Before: oldVal, + After: newVal, + }, + } + + changeSrc, err := change.Encode(ty) + if err != nil { + diags = diags.Append(err) + return nil, diags + } + + drs = append(drs, changeSrc) + } + } + } + + return drs, diags +} + // PlanGraphForUI is a last vestage of graphs in the public interface of Context // (as opposed to graphs as an implementation detail) intended only for use // by the "terraform graph" command when asked to render a plan-time graph. diff --git a/internal/terraform/context_plan2_test.go b/internal/terraform/context_plan2_test.go index 53053fedad..fc670ce312 100644 --- a/internal/terraform/context_plan2_test.go +++ b/internal/terraform/context_plan2_test.go @@ -106,6 +106,23 @@ resource "test_object" "a" { } } + // This situation should result in a drifted resource change. + var drifted *plans.ResourceInstanceChangeSrc + for _, dr := range plan.DriftedResources { + if dr.Addr.Equal(addr) { + drifted = dr + break + } + } + + if drifted == nil { + t.Errorf("instance %s is missing from the drifted resource changes", addr) + } else { + if got, want := drifted.Action, plans.Delete; got != want { + t.Errorf("unexpected instance %s drifted resource change action. got: %s, want: %s", addr, got, want) + } + } + // Because the configuration still mentions test_object.a, we should've // planned to recreate it in order to fix the drift. for _, c := range plan.Changes.Resources { @@ -1037,6 +1054,11 @@ func TestContext2Plan_refreshOnlyMode_deposed(t *testing.T) { t.Errorf("wrong value for output value 'out'\ngot: %#v\nwant: %#v", got, want) } } + + // Deposed objects should not be represented in drift. + if len(plan.DriftedResources) > 0 { + t.Errorf("unexpected drifted resources (%d)", len(plan.DriftedResources)) + } } func TestContext2Plan_invalidSensitiveModuleOutput(t *testing.T) { diff --git a/internal/terraform/context_refresh_test.go b/internal/terraform/context_refresh_test.go index dd319254a6..49cd02e0ea 100644 --- a/internal/terraform/context_refresh_test.go +++ b/internal/terraform/context_refresh_test.go @@ -219,6 +219,10 @@ func TestContext2Refresh_targeted(t *testing.T) { ResourceTypes: map[string]*configschema.Block{ "aws_elb": { Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, "instances": { Type: cty.Set(cty.String), Optional: true, @@ -295,6 +299,10 @@ func TestContext2Refresh_targetedCount(t *testing.T) { ResourceTypes: map[string]*configschema.Block{ "aws_elb": { Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, "instances": { Type: cty.Set(cty.String), Optional: true, @@ -381,6 +389,10 @@ func TestContext2Refresh_targetedCountIndex(t *testing.T) { ResourceTypes: map[string]*configschema.Block{ "aws_elb": { Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, "instances": { Type: cty.Set(cty.String), Optional: true,