diff --git a/internal/terraform/context_plan.go b/internal/terraform/context_plan.go index fce564bf3..20124bd88 100644 --- a/internal/terraform/context_plan.go +++ b/internal/terraform/context_plan.go @@ -499,6 +499,14 @@ func (c *Context) driftedResources(config *configs.Config, oldState, newState *s continue } 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) schema, _ := schemas.ResourceTypeConfig( @@ -547,20 +555,30 @@ func (c *Context) driftedResources(config *configs.Config, oldState, newState *s newVal = cty.NullVal(ty) } - if oldVal.RawEquals(newVal) { + if oldVal.RawEquals(newVal) && addr.Equal(prevRunAddr) { // No drift if the two values are semantically equivalent + // and no move has happened continue } - // We can only detect updates and deletes as drift. - action := plans.Update - if newVal.IsNull() { + // We can detect three types of changes after refreshing state, + // only two of which are easily understood as "drift": + // + // - 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 - } - - prevRunAddr := addr - if move, ok := moves[addr.UniqueKey()]; ok { - prevRunAddr = move.From + case !oldVal.RawEquals(newVal): + action = plans.Update + default: + action = plans.NoOp } change := &plans.ResourceInstanceChange{