stacks: do not include removed block instances that do not exist in state (#35693)

* stacks: include a warning if a removed block targets a non-existent component

* check plan/apply stages separately

* no warning
pull/35706/head
Liam Cervante 2 years ago committed by GitHub
parent 5a38b5323f
commit 99a94908e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -3147,8 +3147,6 @@ func TestApply_RemovedBlocks(t *testing.T) {
)
// TODO: Add tests for and implement the following cases:
// - Add a test for a removed block targeting state that has already been
// removed.
// - Add a test for a removed block that forgets instead of destroys.
tcs := map[string]struct {

@ -104,19 +104,48 @@ func (r *Removed) Instances(ctx context.Context, phase EvalPhase) (map[addrs.Ins
return instancesResult[*RemovedInstance]{}, diags
}
// First, evaluate the for_each value to get the set of instances the
// user has asked to be removed.
result := instancesMap(forEachValue, func(ik addrs.InstanceKey, rd instances.RepetitionData) *RemovedInstance {
return newRemovedInstance(r, ik, rd, false)
})
addrs := make([]stackaddrs.AbsComponentInstance, 0, len(result.insts))
for _, ci := range result.insts {
addrs = append(addrs, ci.Addr())
// Now, filter out any instances that are not known to the previous
// state. This means the user has targeted a component that (a) never
// existed or (b) was removed in a previous operation.
//
// This stops us emitting planned and applied changes for instances that
// do not exist.
knownAddrs := make([]stackaddrs.AbsComponentInstance, 0, len(result.insts))
knownInstances := make(map[addrs.InstanceKey]*RemovedInstance, len(result.insts))
for key, ci := range result.insts {
switch phase {
case PlanPhase:
if r.main.PlanPrevState().HasComponentInstance(ci.Addr()) {
knownInstances[key] = ci
knownAddrs = append(knownAddrs, ci.Addr())
continue
}
case ApplyPhase:
if _, ok := r.main.PlanBeingApplied().Components.GetOk(ci.Addr()); ok {
knownInstances[key] = ci
knownAddrs = append(knownAddrs, ci.Addr())
continue
}
default:
// Otherwise, we're running in a stage that doesn't evaluate
// a state or the plan so we'll just include everything.
knownInstances[key] = ci
knownAddrs = append(knownAddrs, ci.Addr())
}
}
result.insts = knownInstances
h := hooksFromContext(ctx)
hookSingle(ctx, h.ComponentExpanded, &hooks.ComponentInstances{
ComponentAddr: r.Addr(),
InstanceAddrs: addrs,
InstanceAddrs: knownAddrs,
})
return result, diags

@ -4724,6 +4724,122 @@ func TestPlan_RemovedBlocks(t *testing.T) {
},
},
},
"absent component": {
source: filepath.Join("with-single-input", "removed-component"),
wantPlanChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
},
},
"absent component instance": {
source: filepath.Join("with-single-input", "removed-component-instance"),
initialState: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"removed\"]")).
AddInputVariable("id", cty.StringVal("a")).
AddInputVariable("input", cty.StringVal("a"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "a",
"value": "a",
}),
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("a", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})).
Build(),
inputs: map[string]cty.Value{
"input": cty.SetVal([]cty.Value{
cty.StringVal("a"),
}),
"removed": cty.SetVal([]cty.Value{
cty.StringVal("b"), // Doesn't exist!
}),
},
wantPlanChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
// we're expecting the new component to be created
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self[\"a\"]"),
PlanComplete: true,
PlanApplyable: false, // no changes
Action: plans.Update,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"input": nil,
"id": nil,
},
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeResourceInstancePlanned{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ChangeSrc: plans.ChangeSrc{
Action: plans.NoOp,
Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"value": cty.StringVal("a"),
})),
},
ProviderAddr: mustDefaultRootProvider("testing"),
},
PriorStateSrc: &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "a",
"value": "a",
}),
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "input"},
Value: cty.SetVal([]cty.Value{
cty.StringVal("a"),
}),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "removed"},
Value: cty.SetVal([]cty.Value{
cty.StringVal("b"),
}),
},
},
},
"orphaned component": {
source: filepath.Join("with-single-input", "removed-component-instance"),
initialState: stackstate.NewStateBuilder().

Loading…
Cancel
Save