| // 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 | |
| } |