stacks: allow multiple removed blocks to target the same component (#36693)

* stacks: allow multiple removed blocks to target the same component

* make linter happy
pull/36702/head^2
Liam Cervante 1 year ago committed by GitHub
parent 7c1e420c45
commit c16d466773
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -228,18 +228,42 @@ func stackConfigMetaforProto(cfgNode *stackconfig.ConfigNode, stackAddr stackadd
// Currently Components are the only thing that can be removed
for name, rc := range cfgNode.Stack.Removed {
cProto := &stacks.FindStackConfigurationComponents_Removed{
SourceAddr: rc.FinalSourceAddr.String(),
ComponentAddr: stackaddrs.Config(stackAddr, stackaddrs.Component{Name: rc.FromComponent.Name}).String(),
Destroy: rc.Destroy,
var blocks []*stacks.FindStackConfigurationComponents_Removed_Block
for _, rc := range rc {
cProto := &stacks.FindStackConfigurationComponents_Removed_Block{
SourceAddr: rc.FinalSourceAddr.String(),
ComponentAddr: stackaddrs.Config(stackAddr, stackaddrs.Component{Name: rc.FromComponent.Name}).String(),
Destroy: rc.Destroy,
}
switch {
case rc.ForEach != nil:
cProto.Instances = stacks.FindStackConfigurationComponents_FOR_EACH
default:
cProto.Instances = stacks.FindStackConfigurationComponents_SINGLE
}
blocks = append(blocks, cProto)
}
switch {
case rc.ForEach != nil:
cProto.Instances = stacks.FindStackConfigurationComponents_FOR_EACH
default:
cProto.Instances = stacks.FindStackConfigurationComponents_SINGLE
ret.Removed[name] = &stacks.FindStackConfigurationComponents_Removed{
// in order to ensure as much backwards and forwards compatibility
// as possible, we're going to set the deprecated single fields
// with the first run block
SourceAddr: rc[0].FinalSourceAddr.String(),
Instances: func() stacks.FindStackConfigurationComponents_Instances {
switch {
case rc[0].ForEach != nil:
return stacks.FindStackConfigurationComponents_FOR_EACH
default:
return stacks.FindStackConfigurationComponents_SINGLE
}
}(),
ComponentAddr: stackaddrs.Config(stackAddr, stackaddrs.Component{Name: rc[0].FromComponent.Name}).String(),
Destroy: rc[0].Destroy,
// We return all the values here:
Blocks: blocks,
}
ret.Removed[name] = cProto
}
return ret

File diff suppressed because it is too large Load Diff

@ -116,10 +116,18 @@ message FindStackConfigurationComponents {
string component_addr = 3;
}
message Removed {
string source_addr = 1;
Instances instances = 2;
string component_addr = 3;
bool destroy = 4;
string source_addr = 1 [deprecated = true];
Instances instances = 2 [deprecated = true];
string component_addr = 3 [deprecated = true];
bool destroy = 4 [deprecated = true];
message Block {
string source_addr = 1;
Instances instances = 2;
string component_addr = 3;
bool destroy = 4;
}
repeated Block blocks = 5;
}
message InputVariable {
bool optional = 1;

@ -191,22 +191,24 @@ func loadConfigDir(sourceAddr sourceaddrs.FinalSource, sources *sourcebundle.Bun
cmpn.FinalSourceAddr = effectiveSourceAddr
}
for _, rmvd := range stack.Removed {
effectiveSourceAddr, err := resolveFinalSourceAddr(sourceAddr, rmvd.SourceAddr, rmvd.VersionConstraints, sources)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid source address",
Detail: fmt.Sprintf(
"Cannot use %q as a source address here: %s.",
rmvd.SourceAddr, err,
),
Subject: rmvd.SourceAddrRange.ToHCL().Ptr(),
})
continue
}
for _, blocks := range stack.Removed {
for _, rmvd := range blocks {
effectiveSourceAddr, err := resolveFinalSourceAddr(sourceAddr, rmvd.SourceAddr, rmvd.VersionConstraints, sources)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid source address",
Detail: fmt.Sprintf(
"Cannot use %q as a source address here: %s.",
rmvd.SourceAddr, err,
),
Subject: rmvd.SourceAddrRange.ToHCL().Ptr(),
})
continue
}
rmvd.FinalSourceAddr = effectiveSourceAddr
rmvd.FinalSourceAddr = effectiveSourceAddr
}
}
return ret, diags

@ -47,7 +47,7 @@ type Declarations struct {
// Removed are the list of components that have been removed from the
// configuration.
Removed map[string]*Removed
Removed map[string][]*Removed
}
func makeDeclarations() Declarations {
@ -58,7 +58,7 @@ func makeDeclarations() Declarations {
LocalValues: make(map[string]*LocalValue),
OutputValues: make(map[string]*OutputValue),
ProviderConfigs: make(map[addrs.LocalProviderConfig]*ProviderConfig),
Removed: make(map[string]*Removed),
Removed: make(map[string][]*Removed),
}
}
@ -82,24 +82,28 @@ func (d *Declarations) addComponent(decl *Component) tfdiags.Diagnostics {
return diags
}
if removed, exists := d.Removed[name]; exists && removed.FromIndex == nil {
// If a component has been removed, we should not also find it in the
// configuration.
//
// If the removed block has an index, then it's possible that only a
// specific instance was removed and not the whole thing. This is okay
// at this point, and will be validated more later. See the addRemoved
// method for more information.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Component exists for removed block",
Detail: fmt.Sprintf(
"A removed block for component %q was declared without an index, but a component block with the same name was declared at %s.\n\nA removed block without an index indicates that the component and all instances were removed from the configuration, and this is not the case.",
name, decl.DeclRange.ToHCL(),
),
Subject: removed.DeclRange.ToHCL().Ptr(),
})
return diags
if blocks, exists := d.Removed[name]; exists {
for _, removed := range blocks {
if removed.FromIndex == nil {
// If a component has been removed, we should not also find it
// in the configuration.
//
// If the removed block has an index, then it's possible that
// only a specific instance was removed and not the whole thing.
// This is okay at this point, and will be validated more later.
// See the addRemoved method for more information.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Component exists for removed block",
Detail: fmt.Sprintf(
"A removed block for component %q was declared without an index, but a component block with the same name was declared at %s.\n\nA removed block without an index indicates that the component and all instances were removed from the configuration, and this is not the case.",
name, decl.DeclRange.ToHCL(),
),
Subject: removed.DeclRange.ToHCL().Ptr(),
})
return diags
}
}
}
d.Components[name] = decl
@ -255,21 +259,6 @@ func (d *Declarations) addRemoved(decl *Removed) tfdiags.Diagnostics {
}
name := decl.FromComponent.Name
// We're going to make sure that all the removed blocks that share the same
// FromComponent are consistent.
if existing, exists := d.Removed[name]; exists {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate removed block",
Detail: fmt.Sprintf(
"A removed block for component %q was already declared at %s.",
name, existing.DeclRange.ToHCL(),
),
Subject: decl.DeclRange.ToHCL().Ptr(),
})
return diags
}
if decl.FromIndex == nil {
// If the removed block does not have an index, then we shouldn't also
// have a component block with the same name. A removed block without
@ -295,7 +284,7 @@ func (d *Declarations) addRemoved(decl *Removed) tfdiags.Diagnostics {
}
}
d.Removed[name] = decl
d.Removed[name] = append(d.Removed[name], decl)
return diags
}
@ -334,10 +323,12 @@ func (d *Declarations) merge(other *Declarations) tfdiags.Diagnostics {
d.addProviderConfig(decl),
)
}
for _, decl := range other.Removed {
diags = diags.Append(
d.addRemoved(decl),
)
for _, blocks := range other.Removed {
for _, decl := range blocks {
diags = diags.Append(
d.addRemoved(decl),
)
}
}
return diags

@ -643,6 +643,226 @@ func TestApply(t *testing.T) {
},
},
},
"duplicate removed blocks": {
path: path.Join("with-single-input", "removed-component-duplicate"),
state: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"one\"]")).
AddInputVariable("id", cty.StringVal("one")).
AddInputVariable("input", cty.StringVal("one"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self[\"one\"].testing_resource.data")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "one",
"value": "one",
}),
})).
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"two\"]")).
AddInputVariable("id", cty.StringVal("two")).
AddInputVariable("input", cty.StringVal("two"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self[\"two\"].testing_resource.data")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "two",
"value": "two",
}),
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("one", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("one"),
"value": cty.StringVal("one"),
})).
AddResource("two", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("two"),
"value": cty.StringVal("two"),
})).
Build(),
cycles: []TestCycle{
{
planMode: plans.NormalMode,
planInputs: map[string]cty.Value{
"input": cty.SetValEmpty(cty.String),
"removed_one": cty.SetVal([]cty.Value{
cty.StringVal("one"),
}),
"removed_two": cty.SetVal([]cty.Value{
cty.StringVal("two"),
}),
},
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self[\"one\"]"),
PlanComplete: true,
PlanApplyable: true,
Mode: plans.DestroyMode,
Action: plans.Delete,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("one")),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("one")),
},
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[\"one\"].testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Delete,
Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("one"),
"value": cty.StringVal("one"),
})),
After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
"id": cty.String,
"value": cty.String,
}))),
},
ProviderAddr: mustDefaultRootProvider("testing"),
},
PriorStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "one",
"value": "one",
}),
Dependencies: make([]addrs.ConfigResource, 0),
Status: states.ObjectReady,
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self[\"two\"]"),
PlanComplete: true,
PlanApplyable: true,
Mode: plans.DestroyMode,
Action: plans.Delete,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("two")),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("two")),
},
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[\"two\"].testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Delete,
Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("two"),
"value": cty.StringVal("two"),
})),
After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
"id": cty.String,
"value": cty.String,
}))),
},
ProviderAddr: mustDefaultRootProvider("testing"),
},
PriorStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "two",
"value": "two",
}),
Dependencies: make([]addrs.ConfigResource, 0),
Status: states.ObjectReady,
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "input"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.SetValEmpty(cty.String),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "removed_one"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.SetVal([]cty.Value{
cty.StringVal("one"),
}),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "removed_two"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.SetVal([]cty.Value{
cty.StringVal("two"),
}),
},
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"one\"]"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"one\"].testing_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: nil,
Schema: providers.Schema{},
},
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"two\"]"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"two\"].testing_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: nil,
Schema: providers.Schema{},
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("input"),
Value: cty.SetValEmpty(cty.String),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("removed_one"),
Value: cty.SetVal([]cty.Value{
cty.StringVal("one"),
}),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("removed_two"),
Value: cty.SetVal([]cty.Value{
cty.StringVal("two"),
}),
},
},
},
},
},
"removed component instance direct": {
path: filepath.Join("with-single-input", "removed-component-instance-direct"),
state: stackstate.NewStateBuilder().

@ -258,6 +258,7 @@ func (c *ComponentInstance) CheckModuleTreePlan(ctx context.Context) (*plans.Pla
// should be reversed. Unfortunately, we can't compute that
// easily so instead we'll use the dependents computed at the
// last apply operation.
Dependents:
for depAddr := range c.PlanPrevDependents(ctx).All() {
depStack := c.main.Stack(ctx, depAddr.Stack, PlanPhase)
if depStack == nil {
@ -266,14 +267,16 @@ func (c *ComponentInstance) CheckModuleTreePlan(ctx context.Context) (*plans.Pla
// doesn't exist so it's fine.
continue
}
depComponent, depRemoved := depStack.ApplyableComponents(ctx, depAddr.Item)
depComponent, depRemoveds := depStack.ApplyableComponents(ctx, depAddr.Item)
if depComponent != nil && !depComponent.PlanIsComplete(ctx) {
opts.ExternalDependencyDeferred = true
break
}
if depRemoved != nil && !depRemoved.PlanIsComplete(ctx) {
opts.ExternalDependencyDeferred = true
break
for _, depRemoved := range depRemoveds {
if !depRemoved.PlanIsComplete(ctx) {
opts.ExternalDependencyDeferred = true
break Dependents
}
}
}
}

@ -100,27 +100,30 @@ func ApplyPlan(ctx context.Context, config *stackconfig.Config, plan *stackplan.
var inst ApplyableComponentInstance
if removed != nil {
if insts, unknown, _ := removed.Instances(ctx, ApplyPhase); unknown {
// It might be that either the removed block
// or component block was deferred but the
// other one had proper changes. We'll note
// this in the logs but just skip processing
// it.
log.Printf("[TRACE]: %s has planned changes, but was unknown. Check further messages to find out if this was an error.", addr)
} else {
for _, i := range insts {
if i.from.Item.Key == addr.Item.Key {
inst = i
break
Blocks:
for _, block := range removed {
if insts, unknown, _ := block.Instances(ctx, ApplyPhase); unknown {
// It might be that either the removed block
// or component block was deferred but the
// other one had proper changes. We'll note
// this in the logs but just skip processing
// it.
log.Printf("[TRACE]: %s has planned changes, but was unknown. Check further messages to find out if this was an error.", addr)
} else {
for _, i := range insts {
if i.from.Item.Key == addr.Item.Key {
inst = i
break Blocks
}
}
}
if inst == nil {
// Again, this might be okay if the component
// block was deferred but the removed block had
// proper changes (or vice versa). We'll note
// this in the logs but just skip processing it.
log.Printf("[TRACE]: %s has planned changes, but does not seem to be declared. Check further messages to find out if this was an error.", addr)
}
}
if inst == nil {
// Again, this might be okay if the component
// block was deferred but the removed block had
// proper changes (or vice versa). We'll note
// this in the logs but just skip processing it.
log.Printf("[TRACE]: %s has planned changes, but does not seem to be declared. Check further messages to find out if this was an error.", addr)
}
}
@ -241,7 +244,12 @@ func ApplyPlan(ctx context.Context, config *stackconfig.Config, plan *stackplan.
span.AddEvent("awaiting predecessor", trace.WithAttributes(
attribute.String("component_addr", waitComponentAddr.String()),
))
success := removed.ApplySuccessful(ctx)
success := true
for _, block := range removed {
if !block.ApplySuccessful(ctx) {
success = false
}
}
if !success {
// If anything we're waiting on does not succeed then we can't proceed without
// violating the dependency invariants.

@ -30,17 +30,19 @@ var (
type Removed struct {
addr stackaddrs.AbsComponent
main *Main
config *RemovedConfig
main *Main
forEachValue perEvalPhase[promising.Once[withDiagnostics[cty.Value]]]
instances perEvalPhase[promising.Once[withDiagnostics[instancesResult[*RemovedInstance]]]]
unknownInstance perEvalPhase[promising.Once[*RemovedInstance]]
}
func newRemoved(main *Main, addr stackaddrs.AbsComponent) *Removed {
func newRemoved(main *Main, addr stackaddrs.AbsComponent, config *RemovedConfig) *Removed {
return &Removed{
addr: addr,
main: main,
addr: addr,
main: main,
config: config,
}
}
@ -74,12 +76,7 @@ func (r *Removed) Stack(ctx context.Context) *Stack {
}
func (r *Removed) Config(ctx context.Context) *RemovedConfig {
configAddr := stackaddrs.ConfigForAbs(r.Addr())
stackConfig := r.main.StackConfig(ctx, configAddr.Stack)
if stackConfig == nil {
return nil
}
return stackConfig.Removed(ctx, configAddr.Item)
return r.config
}
func (r *Removed) ForEachValue(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) {

@ -104,6 +104,7 @@ func (r *RemovedInstance) ModuleTreePlan(ctx context.Context) (*plans.Plan, tfdi
providerClients := configuredProviderClients(ctx, r.main, known, unknown, PlanPhase)
deferred := r.deferred
Dependents:
for depAddr := range r.PlanPrevDependents(ctx).All() {
depStack := r.main.Stack(ctx, depAddr.Stack, PlanPhase)
if depStack == nil {
@ -112,14 +113,16 @@ func (r *RemovedInstance) ModuleTreePlan(ctx context.Context) (*plans.Plan, tfdi
// doesn't exist so it's fine.
break
}
depComponent, depRemoved := depStack.ApplyableComponents(ctx, depAddr.Item)
depComponent, depRemoveds := depStack.ApplyableComponents(ctx, depAddr.Item)
if depComponent != nil && !depComponent.PlanIsComplete(ctx) {
deferred = true
break
}
if depRemoved != nil && !depRemoved.PlanIsComplete(ctx) {
deferred = true
break
for _, depRemoved := range depRemoveds {
if !depRemoved.PlanIsComplete(ctx) {
deferred = true
break Dependents
}
}
}

@ -49,7 +49,7 @@ type Stack struct {
stackCalls map[stackaddrs.StackCall]*StackCall
outputValues map[stackaddrs.OutputValue]*OutputValue
components map[stackaddrs.Component]*Component
removed map[stackaddrs.Component]*Removed
removed map[stackaddrs.Component][]*Removed
providers map[stackaddrs.ProviderConfigRef]*Provider
}
@ -322,7 +322,7 @@ func (s *Stack) Component(ctx context.Context, addr stackaddrs.Component) *Compo
return s.Components(ctx)[addr]
}
func (s *Stack) Removeds(ctx context.Context) map[stackaddrs.Component]*Removed {
func (s *Stack) Removeds(ctx context.Context) map[stackaddrs.Component][]*Removed {
s.mu.Lock()
defer s.mu.Unlock()
@ -330,26 +330,28 @@ func (s *Stack) Removeds(ctx context.Context) map[stackaddrs.Component]*Removed
return s.removed
}
decls := s.ConfigDeclarations(ctx)
ret := make(map[stackaddrs.Component]*Removed, len(decls.Removed))
for _, r := range decls.Removed {
absAddr := stackaddrs.AbsComponent{
Stack: s.Addr(),
Item: r.FromComponent,
decls := s.StackConfig(ctx).Removeds(ctx)
ret := make(map[stackaddrs.Component][]*Removed, len(decls))
for _, blocks := range decls {
for _, r := range blocks {
absAddr := stackaddrs.AbsComponent{
Stack: s.Addr(),
Item: r.config.FromComponent,
}
ret[absAddr.Item] = append(ret[absAddr.Item], newRemoved(s.main, absAddr, r))
}
ret[absAddr.Item] = newRemoved(s.main, absAddr)
}
s.removed = ret
return ret
}
func (s *Stack) Removed(ctx context.Context, addr stackaddrs.Component) *Removed {
func (s *Stack) Removed(ctx context.Context, addr stackaddrs.Component) []*Removed {
return s.Removeds(ctx)[addr]
}
// ApplyableComponents returns the combination of removed blocks and declared
// components for a given component address.
func (s *Stack) ApplyableComponents(ctx context.Context, addr stackaddrs.Component) (*Component, *Removed) {
func (s *Stack) ApplyableComponents(ctx context.Context, addr stackaddrs.Component) (*Component, []*Removed) {
return s.Component(ctx, addr), s.Removed(ctx, addr)
}
@ -675,12 +677,36 @@ func (s *Stack) resolveExpressionReference(
// just inert containers; the plan walk driver must also explore everything
// nested inside the stack and plan those separately.
func (s *Stack) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) {
if !s.IsRoot() {
// Nothing to do for non-root stacks
return nil, nil
var diags tfdiags.Diagnostics
// We're going to validate that all the removed blocks in this stack resolve
// to unique instance addresses.
for _, blocks := range s.Removeds(ctx) {
seen := make(map[addrs.InstanceKey]*RemovedInstance)
for _, block := range blocks {
insts, unknown, _ := block.Instances(ctx, PlanPhase)
if unknown {
continue
}
for _, inst := range insts {
if existing, exists := seen[inst.from.Item.Key]; exists {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid `from` attribute",
Detail: fmt.Sprintf("The `from` attribute resolved to resource instance %s, which is already claimed by another removed block at %s.", inst.from, existing.call.config.config.DeclRange.ToHCL()),
Subject: inst.call.config.config.DeclRange.ToHCL().Ptr(),
})
}
seen[inst.from.Item.Key] = inst
}
}
}
var diags tfdiags.Diagnostics
if !s.IsRoot() {
// Nothing more to do for non-root stacks
return nil, diags
}
// We want to check that all of the components we have in state are
// targeted by something (either a component or a removed block) in
@ -720,7 +746,7 @@ Instance:
continue
}
component, removed := stack.ApplyableComponents(ctx, inst.Item.Component)
component, removeds := stack.ApplyableComponents(ctx, inst.Item.Component)
if component != nil {
insts, unknown := component.Instances(ctx, PlanPhase)
if unknown {
@ -738,14 +764,14 @@ Instance:
}
}
if removed != nil {
for _, removed := range removeds {
insts, unknown, _ := removed.Instances(ctx, PlanPhase)
if unknown {
// We can't determine if the component is targeted or not. This
// is okay, as any changes to this component will be deferred
// anyway and a follow up plan will then detect the missing
// component if it exists.
continue
continue Instance
}
for _, i := range insts {

@ -43,7 +43,7 @@ type StackConfig struct {
outputValues map[stackaddrs.OutputValue]*OutputValueConfig
stackCalls map[stackaddrs.StackCall]*StackCallConfig
components map[stackaddrs.Component]*ComponentConfig
removed map[stackaddrs.Component]*RemovedConfig
removed map[stackaddrs.Component][]*RemovedConfig
providers map[stackaddrs.ProviderConfig]*ProviderConfig
}
@ -64,7 +64,7 @@ func newStackConfig(main *Main, addr stackaddrs.Stack, config *stackconfig.Confi
outputValues: make(map[stackaddrs.OutputValue]*OutputValueConfig, len(config.Stack.Declarations.OutputValues)),
stackCalls: make(map[stackaddrs.StackCall]*StackCallConfig, len(config.Stack.Declarations.EmbeddedStacks)),
components: make(map[stackaddrs.Component]*ComponentConfig, len(config.Stack.Declarations.Components)),
removed: make(map[stackaddrs.Component]*RemovedConfig, len(config.Stack.Declarations.Removed)),
removed: make(map[stackaddrs.Component][]*RemovedConfig, len(config.Stack.Declarations.Removed)),
providers: make(map[stackaddrs.ProviderConfig]*ProviderConfig, len(config.Stack.Declarations.ProviderConfigs)),
}
}
@ -429,18 +429,21 @@ func (s *StackConfig) Components(ctx context.Context) map[stackaddrs.Component]*
// Removed returns a [RemovedConfig] representing the component call
// declared within this stack config that matches the given address, or nil if
// there is no such declaration.
func (s *StackConfig) Removed(ctx context.Context, addr stackaddrs.Component) *RemovedConfig {
func (s *StackConfig) Removed(ctx context.Context, addr stackaddrs.Component) []*RemovedConfig {
s.mu.Lock()
defer s.mu.Unlock()
ret, ok := s.removed[addr]
if !ok {
cfg, ok := s.config.Stack.Removed[addr.Name]
cfgs, ok := s.config.Stack.Removed[addr.Name]
if !ok {
return nil
}
cfgAddr := stackaddrs.Config(s.Addr(), addr)
ret = newRemovedConfig(s.main, cfgAddr, cfg)
for _, cfg := range cfgs {
cfgAddr := stackaddrs.Config(s.Addr(), addr)
removed := newRemovedConfig(s.main, cfgAddr, cfg)
ret = append(ret, removed)
}
s.removed[addr] = ret
}
return ret
@ -448,11 +451,11 @@ func (s *StackConfig) Removed(ctx context.Context, addr stackaddrs.Component) *R
// Removeds returns a map of the objects representing all of the
// removed calls declared inside this stack configuration.
func (s *StackConfig) Removeds(ctx context.Context) map[stackaddrs.Component]*RemovedConfig {
func (s *StackConfig) Removeds(ctx context.Context) map[stackaddrs.Component][]*RemovedConfig {
if len(s.config.Stack.Removed) == 0 {
return nil
}
ret := make(map[stackaddrs.Component]*RemovedConfig, len(s.config.Stack.Removed))
ret := make(map[stackaddrs.Component][]*RemovedConfig, len(s.config.Stack.Removed))
for name := range s.config.Stack.Removed {
addr := stackaddrs.Component{Name: name}
ret[addr] = s.Removed(ctx, addr)

@ -5,6 +5,7 @@ package stackeval
import (
"context"
"sync"
"github.com/zclconf/go-cty/cty"
@ -143,13 +144,13 @@ func walkDynamicObjectsInStack[Output any](
claimedInstances := collections.NewSet[stackaddrs.ComponentInstance]()
removed := stack.Removed(ctx, component.Addr().Item)
if removed != nil {
for _, block := range removed {
// In this case we don't care about the unknown. If the
// removed instances are unknown, then we'll mark everything
// as being part of the component block. So, even if insts
// comes back as unknown and hence empty, we still proceed
// as normal.
insts, _, _ := removed.Instances(ctx, phase)
// removed instances are unknown, then we'll mark
// everything as being part of the component block. So,
// even if insts comes back as unknown and hence empty,
// we still proceed as normal.
insts, _, _ := block.Instances(ctx, phase)
for key := range insts {
claimedInstances.Add(stackaddrs.ComponentInstance{
Component: component.Addr().Item,
@ -194,68 +195,116 @@ func walkDynamicObjectsInStack[Output any](
}
})
}
for _, removed := range stack.Removeds(ctx) {
removed := removed
visit(ctx, walk, removed)
walk.AsyncTask(ctx, func(ctx context.Context) {
insts, unknown, _ := removed.Instances(ctx, phase)
if unknown {
// If the instances claimed by this removed block are unknown,
// then we'll check all the known instances and mark any that
// are not claimed by an equivalent component block as being
// removed by this block but as deferred until the foreach is
// known.
for addr, blocks := range stack.Removeds(ctx) {
// knownInstances are the instances that already exist either
// because they are in the state or the plan.
knownInstances := stack.KnownComponentInstances(addr, phase)
unknownComponentBlock := false
// keep track of all the instances that we have processed so far.
var mutex sync.Mutex
componentInstances := collections.NewSet[stackaddrs.ComponentInstance]()
claimedInstances := collections.NewSet[stackaddrs.ComponentInstance]()
for _, removed := range blocks {
removed := removed
visit(ctx, walk, removed)
walk.AsyncTask(ctx, func(ctx context.Context) {
insts, unknown, _ := removed.Instances(ctx, phase)
if unknown {
// If the instances claimed by this removed block are unknown,
// then we'll check all the known instances and mark any that
// are not claimed by an equivalent component block as being
// removed by this block but as deferred until the foreach is
// known.
// knownInstances are the instances that already exist either
// because they are in the state or the plan.
knownInstances := stack.KnownComponentInstances(removed.Addr().Item, phase)
mutex.Lock()
if componentInstances.Len() == 0 {
// then we'll gather the component instances. we do this
// once, enforced by the mutex and the check above.
component := stack.Component(ctx, removed.Addr().Item)
if component != nil {
insts, unknown := component.Instances(ctx, phase)
if unknown {
// So both the for_each for the removed block and the
// component block is unknown. In this case, we should
// have gathered everything as being "updated" from the
// component and we'll mark nothing as being removed.
unknownComponentBlock = true
mutex.Unlock()
return
}
for key := range insts {
componentInstances.Add(stackaddrs.ComponentInstance{
Component: removed.Addr().Item,
Key: key,
})
}
}
}
mutex.Unlock()
claimedInstances := collections.NewSet[stackaddrs.ComponentInstance]()
component := stack.Component(ctx, removed.Addr().Item)
if component != nil {
insts, unknown := component.Instances(ctx, phase)
if unknown {
// So both the for_each for the removed block and the
// component block is unknown. In this case, we should
// have gathered everything as being "updated" from the
// component and we'll mark nothing as being removed.
if unknownComponentBlock {
// if both the component block and this block are
// unknown, then any instances will have been claimed
// by the component block so we do nothing.
return
}
for key := range insts {
claimedInstances.Add(stackaddrs.ComponentInstance{
Component: removed.Addr().Item,
Key: key,
})
for inst := range knownInstances.All() {
mutex.Lock()
if componentInstances.Has(inst) {
// Then this instance is claimed by the component
// block.
continue
}
if claimedInstances.Has(inst) {
// Then another removed block has processed this
// instance, and we don't want another to try
// and claim it.
continue
}
claimedInstances.Add(inst)
mutex.Unlock()
// This instance is not claimed by the component block, so
// we'll mark it as being removed by the removed block.
inst := newRemovedInstance(removed, stackaddrs.AbsComponentInstance{
Stack: stack.addr,
Item: inst,
}, instances.RepetitionData{
EachKey: inst.Key.Value(),
EachValue: cty.UnknownVal(cty.DynamicPseudoType),
}, true)
visit(ctx, walk, inst)
}
return
}
for inst := range knownInstances.All() {
if claimedInstances.Has(inst) {
// Then this instance is claimed by the component block.
for _, inst := range insts {
mutex.Lock()
if claimedInstances.Has(inst.Addr().Item) {
// this is an error, but it should have been raised
// elsewhere
continue
}
if componentInstances.Has(inst.Addr().Item) {
// this is an error, but it should have been raised
// elsewhere
continue
}
claimedInstances.Add(inst.Addr().Item)
mutex.Unlock()
// This instance is not claimed by the component block, so
// we'll mark it as being removed by the removed block.
inst := newRemovedInstance(removed, stackaddrs.AbsComponentInstance{
Stack: stack.addr,
Item: inst,
}, instances.RepetitionData{
EachKey: inst.Key.Value(),
EachValue: cty.UnknownVal(cty.DynamicPseudoType),
}, true)
visit(ctx, walk, inst)
}
return
}
for _, inst := range insts {
visit(ctx, walk, inst)
}
})
})
}
}
// Now, we'll do the rest of the declarations in the stack. These are

@ -64,8 +64,10 @@ func walkStaticObjectsInStackConfig[Output any](
visit(ctx, walk, obj)
}
for _, obj := range stackConfig.Removeds(ctx) {
visit(ctx, walk, obj)
for _, objs := range stackConfig.Removeds(ctx) {
for _, obj := range objs {
visit(ctx, walk, obj)
}
}
for _, obj := range stackConfig.StackCalls(ctx) {

@ -0,0 +1,59 @@
required_providers {
testing = {
source = "hashicorp/testing"
version = "0.1.0"
}
}
provider "testing" "default" {}
variable "input" {
type = set(string)
}
variable "removed_one" {
type = set(string)
}
variable "removed_two" {
type = set(string)
}
component "self" {
source = "../"
for_each = var.input
providers = {
testing = provider.testing.default
}
inputs = {
id = each.key
input = each.key
}
}
removed {
from = component.self[each.key]
source = "../"
for_each = var.removed_one
providers = {
testing = provider.testing.default
}
}
removed {
from = component.self[each.key]
source = "../"
for_each = var.removed_two
providers = {
testing = provider.testing.default
}
}
Loading…
Cancel
Save