Skip to content

Commit bebf1ad

Browse files
committed
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.
1 parent b459455 commit bebf1ad

4 files changed

Lines changed: 165 additions & 4 deletions

File tree

internal/plans/plan.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type Plan struct {
3131

3232
VariableValues map[string]DynamicValue
3333
Changes *Changes
34+
DriftedResources []*ResourceInstanceChangeSrc
3435
TargetAddrs []addrs.Targetable
3536
ForceReplaceAddrs []addrs.AbsResourceInstance
3637
ProviderSHA256s map[string][]byte

internal/terraform/context_plan.go

Lines changed: 130 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -347,11 +347,17 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, r
347347
diags = diags.Append(walkDiags)
348348
diags = diags.Append(c.postPlanValidateMoves(config, moveStmts, walker.InstanceExpander.AllInstances()))
349349

350+
prevRunState = walker.PrevRunState.Close()
351+
priorState := walker.RefreshState.Close()
352+
driftedResources, driftDiags := c.driftedResources(config, prevRunState, priorState, moveResults)
353+
diags = diags.Append(driftDiags)
354+
350355
plan := &plans.Plan{
351-
UIMode: opts.Mode,
352-
Changes: changes,
353-
PriorState: walker.RefreshState.Close(),
354-
PrevRunState: walker.PrevRunState.Close(),
356+
UIMode: opts.Mode,
357+
Changes: changes,
358+
DriftedResources: driftedResources,
359+
PrevRunState: prevRunState,
360+
PriorState: priorState,
355361

356362
// Other fields get populated by Context.Plan after we return
357363
}
@@ -398,6 +404,126 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State,
398404
}
399405
}
400406

407+
func (c *Context) driftedResources(config *configs.Config, oldState, newState *states.State, moves map[addrs.UniqueKey]refactoring.MoveResult) ([]*plans.ResourceInstanceChangeSrc, tfdiags.Diagnostics) {
408+
var diags tfdiags.Diagnostics
409+
410+
if newState.ManagedResourcesEqual(oldState) {
411+
// Nothing to do, because we only detect and report drift for managed
412+
// resource instances.
413+
return nil, diags
414+
}
415+
416+
schemas, schemaDiags := c.Schemas(config, newState)
417+
diags = diags.Append(schemaDiags)
418+
if diags.HasErrors() {
419+
return nil, diags
420+
}
421+
422+
var drs []*plans.ResourceInstanceChangeSrc
423+
424+
for _, ms := range oldState.Modules {
425+
for _, rs := range ms.Resources {
426+
if rs.Addr.Resource.Mode != addrs.ManagedResourceMode {
427+
// Drift reporting is only for managed resources
428+
continue
429+
}
430+
431+
provider := rs.ProviderConfig.Provider
432+
for key, oldIS := range rs.Instances {
433+
if oldIS.Current == nil {
434+
// Not interested in instances that only have deposed objects
435+
continue
436+
}
437+
addr := rs.Addr.Instance(key)
438+
newIS := newState.ResourceInstance(addr)
439+
440+
schema, _ := schemas.ResourceTypeConfig(
441+
provider,
442+
addr.Resource.Resource.Mode,
443+
addr.Resource.Resource.Type,
444+
)
445+
if schema == nil {
446+
// This should never happen, but just in case
447+
return nil, diags.Append(tfdiags.Sourceless(
448+
tfdiags.Error,
449+
"Missing resource schema from provider",
450+
fmt.Sprintf("No resource schema found for %s.", addr.Resource.Resource.Type),
451+
))
452+
}
453+
ty := schema.ImpliedType()
454+
455+
oldObj, err := oldIS.Current.Decode(ty)
456+
if err != nil {
457+
// This should also never happen
458+
return nil, diags.Append(tfdiags.Sourceless(
459+
tfdiags.Error,
460+
"Failed to decode resource from state",
461+
fmt.Sprintf("Error decoding %q from previous state: %s", addr.String(), err),
462+
))
463+
}
464+
465+
var newObj *states.ResourceInstanceObject
466+
if newIS != nil && newIS.Current != nil {
467+
newObj, err = newIS.Current.Decode(ty)
468+
if err != nil {
469+
// This should also never happen
470+
return nil, diags.Append(tfdiags.Sourceless(
471+
tfdiags.Error,
472+
"Failed to decode resource from state",
473+
fmt.Sprintf("Error decoding %q from prior state: %s", addr.String(), err),
474+
))
475+
}
476+
}
477+
478+
var oldVal, newVal cty.Value
479+
oldVal = oldObj.Value
480+
if newObj != nil {
481+
newVal = newObj.Value
482+
} else {
483+
newVal = cty.NullVal(ty)
484+
}
485+
486+
if oldVal.RawEquals(newVal) {
487+
// No drift if the two values are semantically equivalent
488+
continue
489+
}
490+
491+
// We can only detect updates and deletes as drift.
492+
action := plans.Update
493+
if newVal.IsNull() {
494+
action = plans.Delete
495+
}
496+
497+
prevRunAddr := addr
498+
if move, ok := moves[addr.UniqueKey()]; ok {
499+
prevRunAddr = move.From
500+
}
501+
502+
change := &plans.ResourceInstanceChange{
503+
Addr: addr,
504+
PrevRunAddr: prevRunAddr,
505+
ProviderAddr: rs.ProviderConfig,
506+
Change: plans.Change{
507+
Action: action,
508+
Before: oldVal,
509+
After: newVal,
510+
},
511+
}
512+
513+
changeSrc, err := change.Encode(ty)
514+
if err != nil {
515+
diags = diags.Append(err)
516+
return nil, diags
517+
}
518+
519+
drs = append(drs, changeSrc)
520+
}
521+
}
522+
}
523+
524+
return drs, diags
525+
}
526+
401527
// PlanGraphForUI is a last vestage of graphs in the public interface of Context
402528
// (as opposed to graphs as an implementation detail) intended only for use
403529
// by the "terraform graph" command when asked to render a plan-time graph.

internal/terraform/context_plan2_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,23 @@ resource "test_object" "a" {
106106
}
107107
}
108108

109+
// This situation should result in a drifted resource change.
110+
var drifted *plans.ResourceInstanceChangeSrc
111+
for _, dr := range plan.DriftedResources {
112+
if dr.Addr.Equal(addr) {
113+
drifted = dr
114+
break
115+
}
116+
}
117+
118+
if drifted == nil {
119+
t.Errorf("instance %s is missing from the drifted resource changes", addr)
120+
} else {
121+
if got, want := drifted.Action, plans.Delete; got != want {
122+
t.Errorf("unexpected instance %s drifted resource change action. got: %s, want: %s", addr, got, want)
123+
}
124+
}
125+
109126
// Because the configuration still mentions test_object.a, we should've
110127
// planned to recreate it in order to fix the drift.
111128
for _, c := range plan.Changes.Resources {
@@ -1037,6 +1054,11 @@ func TestContext2Plan_refreshOnlyMode_deposed(t *testing.T) {
10371054
t.Errorf("wrong value for output value 'out'\ngot: %#v\nwant: %#v", got, want)
10381055
}
10391056
}
1057+
1058+
// Deposed objects should not be represented in drift.
1059+
if len(plan.DriftedResources) > 0 {
1060+
t.Errorf("unexpected drifted resources (%d)", len(plan.DriftedResources))
1061+
}
10401062
}
10411063

10421064
func TestContext2Plan_invalidSensitiveModuleOutput(t *testing.T) {

internal/terraform/context_refresh_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ func TestContext2Refresh_targeted(t *testing.T) {
219219
ResourceTypes: map[string]*configschema.Block{
220220
"aws_elb": {
221221
Attributes: map[string]*configschema.Attribute{
222+
"id": {
223+
Type: cty.String,
224+
Computed: true,
225+
},
222226
"instances": {
223227
Type: cty.Set(cty.String),
224228
Optional: true,
@@ -295,6 +299,10 @@ func TestContext2Refresh_targetedCount(t *testing.T) {
295299
ResourceTypes: map[string]*configschema.Block{
296300
"aws_elb": {
297301
Attributes: map[string]*configschema.Attribute{
302+
"id": {
303+
Type: cty.String,
304+
Computed: true,
305+
},
298306
"instances": {
299307
Type: cty.Set(cty.String),
300308
Optional: true,
@@ -381,6 +389,10 @@ func TestContext2Refresh_targetedCountIndex(t *testing.T) {
381389
ResourceTypes: map[string]*configschema.Block{
382390
"aws_elb": {
383391
Attributes: map[string]*configschema.Attribute{
392+
"id": {
393+
Type: cty.String,
394+
Computed: true,
395+
},
384396
"instances": {
385397
Type: cty.Set(cty.String),
386398
Optional: true,

0 commit comments

Comments
 (0)