stacks: follow component progress cycle for empty destroys

We expect a component instances to report its plan/apply starting and ending as
well as reporting the progress / result. This should also be the case for no-ops
like an empty component instance.
pull/38056/head^2
Daniel Schmidt 3 months ago
parent 1a36d365c7
commit b926263c03

@ -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"

@ -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{
@ -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"),
}},
},
},
},

@ -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"
@ -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 {
@ -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,
@ -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())
@ -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{
@ -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),

Loading…
Cancel
Save