core: Compute resource drift after plan walk

Rather than delaying resource drift detection until it is ready to be
presented, here we perform that computation after the plan walk has
completed. The resulting drift is represented like planned resource
changes, using a slice of ResourceInstanceChangeSrc values.
This commit is contained in:
Alisdair McDiarmid 2021-09-13 16:29:15 -04:00
parent b4594551f7
commit bebf1ad23a
4 changed files with 165 additions and 4 deletions

View File

@ -31,6 +31,7 @@ type Plan struct {
VariableValues map[string]DynamicValue VariableValues map[string]DynamicValue
Changes *Changes Changes *Changes
DriftedResources []*ResourceInstanceChangeSrc
TargetAddrs []addrs.Targetable TargetAddrs []addrs.Targetable
ForceReplaceAddrs []addrs.AbsResourceInstance ForceReplaceAddrs []addrs.AbsResourceInstance
ProviderSHA256s map[string][]byte ProviderSHA256s map[string][]byte

View File

@ -347,11 +347,17 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, r
diags = diags.Append(walkDiags) diags = diags.Append(walkDiags)
diags = diags.Append(c.postPlanValidateMoves(config, moveStmts, walker.InstanceExpander.AllInstances())) 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{ plan := &plans.Plan{
UIMode: opts.Mode, UIMode: opts.Mode,
Changes: changes, Changes: changes,
PriorState: walker.RefreshState.Close(), DriftedResources: driftedResources,
PrevRunState: walker.PrevRunState.Close(), PrevRunState: prevRunState,
PriorState: priorState,
// Other fields get populated by Context.Plan after we return // 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 // 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 // (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. // by the "terraform graph" command when asked to render a plan-time graph.

View File

@ -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 // Because the configuration still mentions test_object.a, we should've
// planned to recreate it in order to fix the drift. // planned to recreate it in order to fix the drift.
for _, c := range plan.Changes.Resources { 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) 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) { func TestContext2Plan_invalidSensitiveModuleOutput(t *testing.T) {

View File

@ -219,6 +219,10 @@ func TestContext2Refresh_targeted(t *testing.T) {
ResourceTypes: map[string]*configschema.Block{ ResourceTypes: map[string]*configschema.Block{
"aws_elb": { "aws_elb": {
Attributes: map[string]*configschema.Attribute{ Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"instances": { "instances": {
Type: cty.Set(cty.String), Type: cty.Set(cty.String),
Optional: true, Optional: true,
@ -295,6 +299,10 @@ func TestContext2Refresh_targetedCount(t *testing.T) {
ResourceTypes: map[string]*configschema.Block{ ResourceTypes: map[string]*configschema.Block{
"aws_elb": { "aws_elb": {
Attributes: map[string]*configschema.Attribute{ Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"instances": { "instances": {
Type: cty.Set(cty.String), Type: cty.Set(cty.String),
Optional: true, Optional: true,
@ -381,6 +389,10 @@ func TestContext2Refresh_targetedCountIndex(t *testing.T) {
ResourceTypes: map[string]*configschema.Block{ ResourceTypes: map[string]*configschema.Block{
"aws_elb": { "aws_elb": {
Attributes: map[string]*configschema.Attribute{ Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"instances": { "instances": {
Type: cty.Set(cty.String), Type: cty.Set(cty.String),
Optional: true, Optional: true,