Skip to content

Commit b1213f7

Browse files
backend/local: don't panic when an instance has only a deposed object
This unusual situation isn't supposed to arise in normal use, but it can come up in practice in some edge-case scenarios where Terraform fails in a severe way during a create_before_destroy. Some earlier versions of Terraform also had bugs in their handling of deposed objects, so this may also arise if upgrading from one of those older versions with some leftover deposed objects in the state.
1 parent 2d19482 commit b1213f7

2 files changed

Lines changed: 119 additions & 3 deletions

File tree

backend/local/backend_plan.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,10 @@ func (b *Local) renderPlan(plan *plans.Plan, state *states.State, schemas *terra
260260
// check if the change is due to a tainted resource
261261
tainted := false
262262
if !state.Empty() {
263-
rs := state.ResourceInstance(rcs.Addr)
264-
if rs != nil {
265-
tainted = rs.Current.Status == states.ObjectTainted
263+
if is := state.ResourceInstance(rcs.Addr); is != nil {
264+
if obj := is.GetGeneration(rcs.DeposedKey.Generation()); obj != nil {
265+
tainted = obj.Status == states.ObjectTainted
266+
}
266267
}
267268
}
268269

backend/local/backend_plan_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,121 @@ Plan: 1 to add, 0 to change, 1 to destroy.`
193193
}
194194
}
195195

196+
func TestLocal_planDeposedOnly(t *testing.T) {
197+
b, cleanup := TestLocal(t)
198+
defer cleanup()
199+
p := TestLocalProvider(t, b, "test", planFixtureSchema())
200+
testStateFile(t, b.StatePath, states.BuildState(func(ss *states.SyncState) {
201+
ss.SetResourceInstanceDeposed(
202+
addrs.Resource{
203+
Mode: addrs.ManagedResourceMode,
204+
Type: "test_instance",
205+
Name: "foo",
206+
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
207+
states.DeposedKey("00000000"),
208+
&states.ResourceInstanceObjectSrc{
209+
Status: states.ObjectReady,
210+
AttrsJSON: []byte(`{
211+
"ami": "bar",
212+
"network_interface": [{
213+
"device_index": 0,
214+
"description": "Main network interface"
215+
}]
216+
}`),
217+
},
218+
addrs.ProviderConfig{
219+
Type: "test",
220+
}.Absolute(addrs.RootModuleInstance),
221+
)
222+
}))
223+
b.CLI = cli.NewMockUi()
224+
outDir := testTempDir(t)
225+
defer os.RemoveAll(outDir)
226+
planPath := filepath.Join(outDir, "plan.tfplan")
227+
op, configCleanup := testOperationPlan(t, "./test-fixtures/plan")
228+
defer configCleanup()
229+
op.PlanRefresh = true
230+
op.PlanOutPath = planPath
231+
cfg := cty.ObjectVal(map[string]cty.Value{
232+
"path": cty.StringVal(b.StatePath),
233+
})
234+
cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
235+
if err != nil {
236+
t.Fatal(err)
237+
}
238+
op.PlanOutBackend = &plans.Backend{
239+
// Just a placeholder so that we can generate a valid plan file.
240+
Type: "local",
241+
Config: cfgRaw,
242+
}
243+
run, err := b.Operation(context.Background(), op)
244+
if err != nil {
245+
t.Fatalf("bad: %s", err)
246+
}
247+
<-run.Done()
248+
if run.Result != backend.OperationSuccess {
249+
t.Fatalf("plan operation failed")
250+
}
251+
if !p.ReadResourceCalled {
252+
t.Fatal("ReadResource should be called")
253+
}
254+
if run.PlanEmpty {
255+
t.Fatal("plan should not be empty")
256+
}
257+
258+
// The deposed object and the current object are distinct, so our
259+
// plan includes separate actions for each of them. This strange situation
260+
// is not common: it should arise only if Terraform fails during
261+
// a create-before-destroy when the create hasn't completed yet but
262+
// in a severe way that prevents the previous object from being restored
263+
// as "current".
264+
//
265+
// However, that situation was more common in some earlier Terraform
266+
// versions where deposed objects were not managed properly, so this
267+
// can arise when upgrading from an older version with deposed objects
268+
// already in the state.
269+
//
270+
// This is one of the few cases where we expose the idea of "deposed" in
271+
// the UI, including the user-unfriendly "deposed key" (00000000 in this
272+
// case) just so that users can correlate this with what they might
273+
// see in `terraform show` and in the subsequent apply output, because
274+
// it's also possible for there to be _multiple_ deposed objects, in the
275+
// unlikely event that create_before_destroy _keeps_ crashing across
276+
// subsequent runs.
277+
expectedOutput := `An execution plan has been generated and is shown below.
278+
Resource actions are indicated with the following symbols:
279+
+ create
280+
- destroy
281+
282+
Terraform will perform the following actions:
283+
284+
# test_instance.foo will be created
285+
+ resource "test_instance" "foo" {
286+
+ ami = "bar"
287+
288+
+ network_interface {
289+
+ description = "Main network interface"
290+
+ device_index = 0
291+
}
292+
}
293+
294+
# test_instance.foo (deposed object 00000000) will be destroyed
295+
- resource "test_instance" "foo" {
296+
- ami = "bar" -> null
297+
298+
- network_interface {
299+
- description = "Main network interface" -> null
300+
- device_index = 0 -> null
301+
}
302+
}
303+
304+
Plan: 1 to add, 0 to change, 1 to destroy.`
305+
output := b.CLI.(*cli.MockUi).OutputWriter.String()
306+
if !strings.Contains(output, expectedOutput) {
307+
t.Fatalf("Unexpected output:\n%s\n\nwant output containing:\n%s", output, expectedOutput)
308+
}
309+
}
310+
196311
func TestLocal_planTainted_createBeforeDestroy(t *testing.T) {
197312
b, cleanup := TestLocal(t)
198313
defer cleanup()

0 commit comments

Comments
 (0)