mirror of
https://github.com/hashicorp/terraform.git
synced 2026-03-21 18:10:30 -04:00
374 lines
10 KiB
Go
374 lines
10 KiB
Go
// 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
|
|
}
|