// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package stackaddrs import ( "fmt" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/tfdiags" ) type RemovedFrom struct { Stack []StackRemovedFrom // Component to be removed. Optional, if not set then the whole stack // should be removed. Component *ComponentRemovedFrom } func (rf RemovedFrom) TargetStack() Stack { stack := make(Stack, 0, len(rf.Stack)) for _, step := range rf.Stack { stack = append(stack, StackStep{Name: step.Name}) } return stack } func (rf RemovedFrom) TargetConfigComponent() ConfigComponent { if rf.Component == nil { panic("should call TargetStack() when no component was specified") } return ConfigComponent{ Stack: rf.TargetStack(), Item: Component{ rf.Component.Name, }, } } func (rf RemovedFrom) Variables() []hcl.Traversal { var traversals []hcl.Traversal for _, step := range rf.Stack { if step.Index != nil { traversals = append(traversals, step.Index.Variables()...) } } if rf.Component != nil && rf.Component.Index != nil { traversals = append(traversals, rf.Component.Index.Variables()...) } return traversals } func (rf RemovedFrom) TargetStackInstance(ctx *hcl.EvalContext, parent StackInstance) (StackInstance, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics var stackInstance StackInstance for _, stack := range rf.Stack { step, moreDiags := stack.StackInstanceStep(ctx) diags = diags.Append(moreDiags) stackInstance = append(stackInstance, step) } return append(parent, stackInstance...), diags } func (rf RemovedFrom) TargetAbsComponentInstance(ctx *hcl.EvalContext, parent StackInstance) (AbsComponentInstance, tfdiags.Diagnostics) { if rf.Component == nil { panic("should call TargetStackInstance() when no component was specified") } var diags tfdiags.Diagnostics stackInstance, moreDiags := rf.TargetStackInstance(ctx, parent) diags = diags.Append(moreDiags) componentInstance, moreDiags := rf.Component.ComponentInstance(ctx) diags = diags.Append(moreDiags) return AbsComponentInstance{Stack: stackInstance, Item: componentInstance}, diags } type StackRemovedFrom struct { Name string Index hcl.Expression } func (rf StackRemovedFrom) StackStep() StackStep { return StackStep{Name: rf.Name} } func (rf StackRemovedFrom) StackInstanceStep(ctx *hcl.EvalContext) (StackInstanceStep, tfdiags.Diagnostics) { key, diags := exprAsKey(rf.Index, ctx) return StackInstanceStep{ Name: rf.Name, Key: key, }, diags } type ComponentRemovedFrom struct { Name string Index hcl.Expression } func (rf ComponentRemovedFrom) Component() Component { return Component{ Name: rf.Name, } } func (rf ComponentRemovedFrom) ComponentInstance(ctx *hcl.EvalContext) (ComponentInstance, tfdiags.Diagnostics) { key, diags := exprAsKey(rf.Index, ctx) return ComponentInstance{ Component: Component{ Name: rf.Name, }, Key: key, }, diags } // ParseRemovedFrom parses the "from" attribute of a "removed" block in a // configuration and returns the address of the configuration object being // removed. // // In addition to the address, this function also returns a traversal that // represents the unparsed index within the from expression. Users can // optionally specify a specific index of a component to target. func ParseRemovedFrom(expr hcl.Expression) (RemovedFrom, tfdiags.Diagnostics) { // we always return the same diagnostic from this function when we // error, so we'll encapsulate it here. diag := &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid 'from' attribute", Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.", Subject: expr.Range().Ptr(), } var diags tfdiags.Diagnostics removedFrom := RemovedFrom{} current, moreDiags := exprToComponentTraversal(expr) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { return RemovedFrom{}, diags } for current != nil { // we're going to parse the traversal in sets of 2-3 depending on // the indices, so we'll check that now. nextTraversal := current.Current for len(nextTraversal) > 0 { var currentTraversal hcl.Traversal var indexExpr hcl.Expression switch { case len(nextTraversal) < 2: // this is simply an error, we always need at least 2 values // for either stack.name or component.name. return RemovedFrom{}, diags.Append(diag) case len(nextTraversal) == 2: indexExpr = current.Index currentTraversal = nextTraversal nextTraversal = nil case len(nextTraversal) == 3: if current.Index != nil { // this is an error, the last traversal should be taking // its index from the outer value if it exists, and to be // exactly three means something is invalid somewhere. return RemovedFrom{}, diags.Append(diag) } index, ok := nextTraversal[2].(hcl.TraverseIndex) if !ok { // This is an error, with exactly 3 we don't have another // traversal to go to after this so the last entry must // be the index. return RemovedFrom{}, diags.Append(diag) } currentTraversal = nextTraversal nextTraversal = nil indexExpr = hcl.StaticExpr(index.Key, index.SrcRange) default: // len(nextTraversal) > 3 if index, ok := nextTraversal[2].(hcl.TraverseIndex); ok { currentTraversal = nextTraversal[:3] nextTraversal = nextTraversal[3:] indexExpr = hcl.StaticExpr(index.Key, index.SrcRange) break } currentTraversal = nextTraversal[:2] nextTraversal = nextTraversal[2:] } var name string switch root := currentTraversal[0].(type) { case hcl.TraverseRoot: name = root.Name case hcl.TraverseAttr: name = root.Name default: return RemovedFrom{}, diags.Append(diag) } switch name { case "component": name, ok := currentTraversal[1].(hcl.TraverseAttr) if !ok { return RemovedFrom{}, diags.Append(diag) } if len(nextTraversal) > 0 || current.Rest != nil { return RemovedFrom{}, diags.Append(diag) } removedFrom.Component = &ComponentRemovedFrom{ Name: name.Name, Index: indexExpr, } return removedFrom, diags case "stack": name, ok := currentTraversal[1].(hcl.TraverseAttr) if !ok { return RemovedFrom{}, diags.Append(diag) } removedFrom.Stack = append(removedFrom.Stack, StackRemovedFrom{ Name: name.Name, Index: indexExpr, }) default: return RemovedFrom{}, diags.Append(diag) } } current = current.Rest } // if we fall out, then we're just targeting a stack directly instead of a // component in a stack return removedFrom, diags } type parsedFromExpr struct { Current hcl.Traversal Index hcl.Expression Rest *parsedFromExpr } // exprToComponentTraversal converts an HCL expression into a traversal that // represents the component being targeted. We have to handle parsing this // ourselves because removed block from arguments can contain index expressions // which are not supported by hcl.AbsTraversalForExpr. // // The return values are (1) the part of the expression that can be converted // into a traversal, (2) the index at the end of the traversal if it is an // expression, (3) the remainder of the expression that needs to be parsed // after (1) has been, and (4) the diagnostics. func exprToComponentTraversal(expr hcl.Expression) (*parsedFromExpr, hcl.Diagnostics) { switch e := expr.(type) { case *hclsyntax.IndexExpr: current, diags := exprToComponentTraversal(e.Collection) if diags.HasErrors() { return nil, diags } for next := current; next != nil; next = next.Rest { if next.Rest == nil { next.Index = e.Key } } return current, diags case *hclsyntax.RelativeTraversalExpr: current, diags := exprToComponentTraversal(e.Source) if diags.HasErrors() { return nil, diags } for next := current; next != nil; next = next.Rest { if next.Rest == nil { next.Rest = &parsedFromExpr{ Current: e.Traversal, } break } } return current, diags default: // For anything else, just rely on the default traversal logic. t, diags := hcl.AbsTraversalForExpr(expr) if diags.HasErrors() { return nil, diags } return &parsedFromExpr{ Current: t, Index: nil, Rest: nil, }, diags } } func exprAsKey(expr hcl.Expression, ctx *hcl.EvalContext) (addrs.InstanceKey, tfdiags.Diagnostics) { if expr == nil { return addrs.NoKey, nil } var diags tfdiags.Diagnostics value, moreDiags := expr.Value(ctx) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { return addrs.WildcardKey, diags } if value.IsNull() { return addrs.WildcardKey, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid `from` attribute", Detail: "The `from` attribute has an invalid index: cannot be null.", Subject: expr.Range().Ptr(), Expression: expr, EvalContext: ctx, }) } if !value.IsKnown() { switch value.Type() { case cty.String, cty.Number: // this is potentially the right type, so we'll allow this return addrs.WildcardKey, diags case cty.DynamicPseudoType: // not ideal, but we can't confirm this for sure so we'll allow it return addrs.WildcardKey, diags default: // bad, this isn't the right type even if we don't know what the // value actually will be in the end return addrs.WildcardKey, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid `from` attribute", Detail: "The `from` attribute has an invalid index: either a string or integer is required.", Subject: expr.Range().Ptr(), Expression: expr, EvalContext: ctx, }) } } key, err := addrs.ParseInstanceKey(value) if err != nil { return addrs.WildcardKey, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid `from` attribute", Detail: fmt.Sprintf("The `from` attribute has an invalid index: %s.", err), Subject: expr.Range().Ptr(), Expression: expr, EvalContext: ctx, }) } return key, diags }