Skip to content

fix: Fix panic on deposed object with precondition and nop change#38587

Open
armsnyder wants to merge 1 commit into
hashicorp:mainfrom
armsnyder:38586-deposed-noop-with-conditions-panic
Open

fix: Fix panic on deposed object with precondition and nop change#38587
armsnyder wants to merge 1 commit into
hashicorp:mainfrom
armsnyder:38586-deposed-noop-with-conditions-panic

Conversation

@armsnyder
Copy link
Copy Markdown

Fixes #38586

When the plan contained a no-op change for a deposed object on a resource whose configuration declared a lifecycle.precondition or lifecycle.postcondition, terraform apply would panic with runtime error: invalid memory address or nil pointer dereference in (*NodeAbstractResourceInstance).readDiff.

Why this happens

Two ingredients combine:

  1. DiffTransformer (internal/terraform/transform_diff.go), for a plans.NoOp change, calls hasConfigConditions(addr):

    case plans.NoOp:
        // For a no-op change we don't take any action but we still
        // run any condition checks associated with the object...
        update = t.hasConfigConditions(addr)

    hasConfigConditions returns true whenever the resource block declares preconditions or postconditions. When update is true, a regular NodeApplyableResourceInstance is created for the non-deposed address. The deposed key is never propagated to that node.

  2. The resulting apply node calls readDiff (internal/terraform/node_resource_abstract_instance.go):

    change := changes.GetResourceInstanceChange(addr, addrs.NotDeposed)
    
    log.Printf("[TRACE] readDiff: Read %s change from plan for %s", change.Action, n.Addr)

    which would panic on change.Action because change is nil.

    Every downstream caller (managedResourceExecute, dataResourceExecute) already guards against change == nil, so the nil-check appears to have been intended.

Fix

Two changes, both small and independent:

  • internal/terraform/transform_diff.go (root cause): in the plans.NoOp arm, only call hasConfigConditions when the change is for the current (non-deposed) object. Preconditions and postconditions only apply to the current object; a NoOp on a deposed object never needs an update node.
  • internal/terraform/node_resource_abstract_instance.go (defense in depth): restore the nil-check in readDiff. The function is supposed to return nil when no change is recorded — all callers already handle it — and only the trace log line was panicking.

Either change alone is sufficient to prevent this specific panic (verified by reverting them one at a time and re-running the regression test). I am including both because the DiffTransformer change is the correct semantic fix and the readDiff guard restores the documented contract; happy to drop the second commit if reviewers prefer the minimum change.

Test

Adds TestContext2Apply_deposedNoLongerExists_withConditions in internal/terraform/context_apply2_test.go. It constructs an orphan-deposed-object state for a count = 0 resource that has a lifecycle.precondition, with the provider's ReadResource returning null. With both fixes reverted the test reproduces the original panic with the same stack trace (node_resource_abstract_instance.go:178node_resource_apply_instance.go:219). With either fix in place it passes.

$ go test ./internal/terraform/ -run TestContext2Apply_deposedNoLongerExists_withConditions -v
=== RUN   TestContext2Apply_deposedNoLongerExists_withConditions
--- PASS: TestContext2Apply_deposedNoLongerExists_withConditions (0.00s)
PASS
ok    github.com/hashicorp/terraform/internal/terraform    0.016s

$ go test ./internal/terraform/
ok    github.com/hashicorp/terraform/internal/terraform    6.5s

The closely related existing test TestContext2Plan_deposedNoLongerExists (context_plan2_test.go) covers the plan-side scenario but stops short of Apply and uses a resource without conditions, so it does not exercise this path.

Target Release

1.16.x

Rollback Plan

  • If a change needs to be reverted, we will roll out an update to the code within 7 days.

Changes to Security Controls

No changes to security controls.

CHANGELOG entry

  • This change is user-facing and I added a changelog entry.
  • This change is not user-facing.

Entry: .changes/v1.16/BUG FIXES-20260514-081915.yaml.

AI Usage

I worked with Claude to localize the panic, identify the two contributing code paths, and draft the regression test. I read and reasoned about every line of the diff myself. The DiffTransformer guard and the readDiff nil-check are both small, local changes, and I empirically validated the necessity of each by reverting them in isolation and confirming the test still passes or panics as expected. The PR description and changelog wording were drafted with AI assistance and edited by me to reduce verbosity.

@armsnyder armsnyder requested a review from a team as a code owner May 14, 2026 08:24
Copy link
Copy Markdown
Contributor

@mildwonkey mildwonkey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @armsnyder , thanks for submitting this, and we're sorry you ran into this bad behavior!

The change in transform_diff is good, but please update the comment for accuracy (I left a vague suggestion). Let's also skip the change readDiff; i'll be removing that shortly :)

Comment thread internal/terraform/transform_diff.go Outdated
// results of other changes.
update = t.hasConfigConditions(addr)
//
// Condition checks only apply to the current (non-deposed)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more accurate comment would be something along the lines of:

A noop deposed change occurs when the instance no longer exists, so there's no reason to process conditions.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, that's better. Updated.

}

change := changes.GetResourceInstanceChange(addr, addrs.NotDeposed)
if change == nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave this check out, please! We're actually going to refactor and remove readDiff entirely (and it's all thanks to you reminding us that it's not really necessary now that we don't need schemas to read diffs anymore)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

@mildwonkey mildwonkey self-assigned this May 14, 2026
@armsnyder armsnyder force-pushed the 38586-deposed-noop-with-conditions-panic branch from d9b1efc to 4aad5fc Compare May 14, 2026 18:44
@armsnyder armsnyder requested a review from mildwonkey May 14, 2026 18:45
@armsnyder armsnyder force-pushed the 38586-deposed-noop-with-conditions-panic branch 2 times, most recently from c68d9d8 to 21800cb Compare May 14, 2026 18:48
@armsnyder
Copy link
Copy Markdown
Author

CI is failing due to changelog entry added in recently merged #38352

@crw crw added the bug label May 14, 2026
@SarahFrench SarahFrench mentioned this pull request May 15, 2026
3 tasks
@armsnyder armsnyder force-pushed the 38586-deposed-noop-with-conditions-panic branch from 21800cb to a945611 Compare May 17, 2026 00:59
@armsnyder
Copy link
Copy Markdown
Author

Rebased to pick up the changelog CI fix from main

@armsnyder armsnyder force-pushed the 38586-deposed-noop-with-conditions-panic branch from a945611 to 3d1218c Compare May 17, 2026 01:01
@armsnyder
Copy link
Copy Markdown
Author

armsnyder commented May 17, 2026

I am not sure why Validate Changelog CI is failing now.

  /usr/bin/git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --filter=blob:none --depth=1 origin +refs/heads/38586-deposed-noop-with-conditions-panic*:refs/remotes/origin/38586-deposed-noop-with-conditions-panic* +refs/tags/38586-deposed-noop-with-conditions-panic*:refs/tags/38586-deposed-noop-with-conditions-panic*
  The process '/usr/bin/git' failed with exit code 1

I see changes were made to the CI recently in #38598 and #38599.

@austinvalle
Copy link
Copy Markdown
Member

I am not sure why Validate Changelog CI is failing now.

  /usr/bin/git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --filter=blob:none --depth=1 origin +refs/heads/38586-deposed-noop-with-conditions-panic*:refs/remotes/origin/38586-deposed-noop-with-conditions-panic* +refs/tags/38586-deposed-noop-with-conditions-panic*:refs/tags/38586-deposed-noop-with-conditions-panic*
  The process '/usr/bin/git' failed with exit code 1

I see changes were made to the CI recently in #38598 and #38599.

Yeah that is unrelated to your PR here, but the recently merged #38607 should resolve that CI failure 👍🏻 (GitHub actions will probably need to resync once you update your branch)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Terraform crash when deposed object with precondition no longer exists in remote system

4 participants