Merge pull request #28796 from hashicorp/jbardin/drift-deposed

plan: handle deposed instances when rendering drift
This commit is contained in:
James Bardin 2021-05-25 08:33:05 -04:00 committed by GitHub
commit 332045a4e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 132 additions and 15 deletions

View File

@ -172,6 +172,12 @@ func ResourceInstanceDrift(
action := plans.Update action := plans.Update
switch { switch {
case before == nil || before.Current == nil:
// before should never be nil, but before.Current can be if the
// instance was deposed. There is nothing to render for a deposed
// instance, since we intend to remove it.
return ""
case after == nil || after.Current == nil: case after == nil || after.Current == nil:
// The object was deleted // The object was deleted
buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] has been deleted", dispAddr))) buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] has been deleted", dispAddr)))

View File

@ -112,6 +112,33 @@ func TestOperation_planNoChanges(t *testing.T) {
}, },
"No objects need to be destroyed.", "No objects need to be destroyed.",
}, },
"no drift to display with only deposed instances": {
// changes in deposed instances will cause a change in state, but
// have nothing to display to the user
func(schemas *terraform.Schemas) *plans.Plan {
return &plans.Plan{
UIMode: plans.NormalMode,
Changes: plans.NewChanges(),
PrevRunState: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceDeposed(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "somewhere",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
states.NewDeposedKey(),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"foo": "ok", "bars":[]}`),
},
addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
)
}),
PriorState: states.NewState(),
}
},
"no differences, so no changes are needed.",
},
"drift detected in normal mode": { "drift detected in normal mode": {
func(schemas *terraform.Schemas) *plans.Plan { func(schemas *terraform.Schemas) *plans.Plan {
return &plans.Plan{ return &plans.Plan{
@ -121,14 +148,14 @@ func TestOperation_planNoChanges(t *testing.T) {
state.SetResourceInstanceCurrent( state.SetResourceInstanceCurrent(
addrs.Resource{ addrs.Resource{
Mode: addrs.ManagedResourceMode, Mode: addrs.ManagedResourceMode,
Type: "something", Type: "test_resource",
Name: "somewhere", Name: "somewhere",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{ &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady, Status: states.ObjectReady,
AttrsJSON: []byte(`{}`), AttrsJSON: []byte(`{}`),
}, },
addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewBuiltInProvider("test")), addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
) )
}), }),
PriorState: states.NewState(), PriorState: states.NewState(),
@ -136,6 +163,69 @@ func TestOperation_planNoChanges(t *testing.T) {
}, },
"to update the Terraform state to match, create and apply a refresh-only plan", "to update the Terraform state to match, create and apply a refresh-only plan",
}, },
"drift detected with deposed": {
func(schemas *terraform.Schemas) *plans.Plan {
return &plans.Plan{
UIMode: plans.NormalMode,
Changes: plans.NewChanges(),
PrevRunState: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "changes",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"foo":"b"}`),
},
addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
)
state.SetResourceInstanceDeposed(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "broken",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
states.NewDeposedKey(),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"foo":"c"}`),
},
addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
)
}),
PriorState: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "changed",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"foo":"b"}`),
},
addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
)
state.SetResourceInstanceDeposed(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "broken",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
states.NewDeposedKey(),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"foo":"d"}`),
},
addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
)
}),
}
},
"to update the Terraform state to match, create and apply a refresh-only plan",
},
"drift detected in refresh-only mode": { "drift detected in refresh-only mode": {
func(schemas *terraform.Schemas) *plans.Plan { func(schemas *terraform.Schemas) *plans.Plan {
return &plans.Plan{ return &plans.Plan{
@ -145,14 +235,14 @@ func TestOperation_planNoChanges(t *testing.T) {
state.SetResourceInstanceCurrent( state.SetResourceInstanceCurrent(
addrs.Resource{ addrs.Resource{
Mode: addrs.ManagedResourceMode, Mode: addrs.ManagedResourceMode,
Type: "something", Type: "test_resource",
Name: "somewhere", Name: "somewhere",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{ &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady, Status: states.ObjectReady,
AttrsJSON: []byte(`{}`), AttrsJSON: []byte(`{}`),
}, },
addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewBuiltInProvider("test")), addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
) )
}), }),
PriorState: states.NewState(), PriorState: states.NewState(),
@ -169,14 +259,14 @@ func TestOperation_planNoChanges(t *testing.T) {
state.SetResourceInstanceCurrent( state.SetResourceInstanceCurrent(
addrs.Resource{ addrs.Resource{
Mode: addrs.ManagedResourceMode, Mode: addrs.ManagedResourceMode,
Type: "something", Type: "test_resource",
Name: "somewhere", Name: "somewhere",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{ &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady, Status: states.ObjectReady,
AttrsJSON: []byte(`{}`), AttrsJSON: []byte(`{}`),
}, },
addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewBuiltInProvider("test")), addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
) )
}), }),
PriorState: states.NewState(), PriorState: states.NewState(),

View File

@ -339,17 +339,16 @@ func renderPlan(plan *plans.Plan, schemas *terraform.Schemas, view *View) {
// line of output, and guarantees to always produce whole lines terminated // line of output, and guarantees to always produce whole lines terminated
// by newline characters. // by newline characters.
func renderChangesDetectedByRefresh(before, after *states.State, schemas *terraform.Schemas, view *View) bool { func renderChangesDetectedByRefresh(before, after *states.State, schemas *terraform.Schemas, view *View) bool {
// ManagedResourceEqual checks that the state is exactly equal for all
// managed resources; but semantically equivalent states, or changes to
// deposed instances may not actually represent changes we need to present
// to the user, so for now this only serves as a short-circuit to skip
// attempting to render the diffs below.
if after.ManagedResourcesEqual(before) { if after.ManagedResourcesEqual(before) {
return false return false
} }
view.streams.Print( var diffs []string
view.colorize.Color("[reset]\n[bold][cyan]Note:[reset][bold] Objects have changed outside of Terraform[reset]\n\n"),
)
view.streams.Print(format.WordWrap(
"Terraform detected the following changes made outside of Terraform since the last \"terraform apply\":\n\n",
view.outputColumns(),
))
for _, bms := range before.Modules { for _, bms := range before.Modules {
for _, brs := range bms.Resources { for _, brs := range bms.Resources {
@ -374,6 +373,10 @@ func renderChangesDetectedByRefresh(before, after *states.State, schemas *terraf
} }
for key, bis := range brs.Instances { for key, bis := range brs.Instances {
if bis.Current == nil {
// No current instance to render here
continue
}
var pis *states.ResourceInstance var pis *states.ResourceInstance
if prs != nil { if prs != nil {
pis = prs.Instance(key) pis = prs.Instance(key)
@ -386,13 +389,31 @@ func renderChangesDetectedByRefresh(before, after *states.State, schemas *terraf
view.colorize, view.colorize,
) )
if diff != "" { if diff != "" {
view.streams.Print(diff) diffs = append(diffs, diff)
} }
} }
} }
} }
// If we only have changes regarding deposed instances, or the diff
// renderer is suppressing irrelevant changes from the legacy SDK, there
// may not have been anything to display to the user.
if len(diffs) > 0 {
view.streams.Print(
view.colorize.Color("[reset]\n[bold][cyan]Note:[reset][bold] Objects have changed outside of Terraform[reset]\n\n"),
)
view.streams.Print(format.WordWrap(
"Terraform detected the following changes made outside of Terraform since the last \"terraform apply\":\n\n",
view.outputColumns(),
))
for _, diff := range diffs {
view.streams.Print(diff)
}
return true return true
}
return false
} }
const planHeaderIntro = ` const planHeaderIntro = `