Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/v1.15/BUG FIXES-20260115-103000.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: BUG FIXES
body: 'stacks: component instances should report no-op plan/apply. This solves a UI inconsistency with convergence destroy plans '
time: 2026-01-15T10:30:00.72402+01:00
custom:
Issue: "38049"
24 changes: 24 additions & 0 deletions internal/stacks/stackruntime/apply_destroy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1718,6 +1718,18 @@ func TestApplyDestroy(t *testing.T) {
InstanceAddrs: []stackaddrs.AbsComponentInstance{mustAbsComponentInstance("component.self")},
},
},
PendingComponentInstancePlan: collections.NewSet[stackaddrs.AbsComponentInstance](
mustAbsComponentInstance("component.self"),
),
BeginComponentInstancePlan: collections.NewSet[stackaddrs.AbsComponentInstance](
mustAbsComponentInstance("component.self"),
),
EndComponentInstancePlan: collections.NewSet[stackaddrs.AbsComponentInstance](
mustAbsComponentInstance("component.self"),
),
ReportComponentInstancePlanned: []*hooks.ComponentInstanceChange{{
Addr: mustAbsComponentInstance("component.self"),
}},
},
wantAppliedHooks: &ExpectedHooks{
ComponentExpanded: []*hooks.ComponentInstances{
Expand All @@ -1726,6 +1738,18 @@ func TestApplyDestroy(t *testing.T) {
InstanceAddrs: []stackaddrs.AbsComponentInstance{mustAbsComponentInstance("component.self")},
},
},
PendingComponentInstanceApply: collections.NewSet[stackaddrs.AbsComponentInstance](
mustAbsComponentInstance("component.self"),
),
BeginComponentInstanceApply: collections.NewSet[stackaddrs.AbsComponentInstance](
mustAbsComponentInstance("component.self"),
),
EndComponentInstanceApply: collections.NewSet[stackaddrs.AbsComponentInstance](
mustAbsComponentInstance("component.self"),
),
ReportComponentInstanceApplied: []*hooks.ComponentInstanceChange{{
Addr: mustAbsComponentInstance("component.self"),
}},
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
"github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks"
"github.com/hashicorp/terraform/internal/stacks/stackstate"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/terraform"
Expand Down Expand Up @@ -192,6 +193,7 @@ func (c *ComponentInstance) CheckModuleTreePlan(ctx context.Context) (*plans.Pla
ctx, c.tracingName()+" modules", &c.moduleTreePlan,
func(ctx context.Context) (*plans.Plan, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
h := hooksFromContext(ctx)

if c.mode == plans.DestroyMode {

Expand All @@ -203,7 +205,13 @@ func (c *ComponentInstance) CheckModuleTreePlan(ctx context.Context) (*plans.Pla
// and never applied, or that it was previously destroyed
// via an earlier destroy operation.
//
// Return a dummy plan:
// Return a dummy plan and send dummy events:
hookSingle(ctx, h.PendingComponentInstancePlan, c.Addr())
seq, ctx := hookBegin(ctx, h.BeginComponentInstancePlan, h.ContextAttach, c.Addr())
hookMore(ctx, seq, h.ReportComponentInstancePlanned, &hooks.ComponentInstanceChange{
Addr: c.Addr(),
})
hookMore(ctx, seq, h.EndComponentInstancePlan, c.Addr())
return &plans.Plan{
UIMode: plans.DestroyMode,
Complete: true,
Expand All @@ -220,7 +228,6 @@ func (c *ComponentInstance) CheckModuleTreePlan(ctx context.Context) (*plans.Pla
// outputs from this component can read from the refresh result
// without causing a cycle.

h := hooksFromContext(ctx)
hookSingle(ctx, h.PendingComponentInstancePlan, c.Addr())
seq, planCtx := hookBegin(ctx, h.BeginComponentInstancePlan, h.ContextAttach, c.Addr())

Expand Down Expand Up @@ -336,7 +343,6 @@ func (c *ComponentInstance) CheckModuleTreePlan(ctx context.Context) (*plans.Pla
}
}

h := hooksFromContext(ctx)
hookSingle(ctx, h.PendingComponentInstancePlan, c.Addr())
seq, ctx := hookBegin(ctx, h.BeginComponentInstancePlan, h.ContextAttach, c.Addr())
plan, moreDiags := PlanComponentInstance(ctx, c.main, c.PlanPrevState(), opts, []terraform.Hook{
Expand Down Expand Up @@ -382,6 +388,15 @@ func (c *ComponentInstance) ApplyModuleTreePlan(ctx context.Context, plan *plans

// If we're destroying and there's nothing to destroy, then we can
// consider this a no-op.
// We still need to report through the hooks that this component instance has been handled.
h := hooksFromContext(ctx)
hookSingle(ctx, hooksFromContext(ctx).PendingComponentInstanceApply, c.Addr())
seq, ctx := hookBegin(ctx, h.BeginComponentInstanceApply, h.ContextAttach, c.Addr())
hookMore(ctx, seq, h.ReportComponentInstanceApplied, &hooks.ComponentInstanceChange{
Addr: c.Addr(),
})
hookMore(ctx, seq, h.EndComponentInstanceApply, c.Addr())

return &ComponentInstanceApplyResult{
FinalState: plan.PriorState, // after refresh
AffectedResourceInstanceObjects: resourceInstanceObjectsAffectedByStackPlan(stackPlan),
Expand Down