| // 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/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/collections" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| ) | |
| // StackItemConfig is a type set containing all of the address types that make | |
| // sense to consider as belonging statically to a [Stack]. | |
| type StackItemConfig[T any] interface { | |
| inStackConfigSigil() | |
| String() string | |
| collections.UniqueKeyer[T] | |
| } | |
| // StackItemDynamic is a type set containing all of the address types that make | |
| // sense to consider as belonging dynamically to a [StackInstance]. | |
| type StackItemDynamic[T any] interface { | |
| inStackInstanceSigil() | |
| String() string | |
| collections.UniqueKeyer[T] | |
| } | |
| // InStackConfig is the generic form of addresses representing configuration | |
| // objects belonging to particular nodes in the static tree of stack | |
| // configurations. | |
| type InStackConfig[T StackItemConfig[T]] struct { | |
| Stack Stack | |
| Item T | |
| } | |
| func Config[T StackItemConfig[T]](stackAddr Stack, relAddr T) InStackConfig[T] { | |
| return InStackConfig[T]{ | |
| Stack: stackAddr, | |
| Item: relAddr, | |
| } | |
| } | |
| func (ist InStackConfig[T]) String() string { | |
| if ist.Stack.IsRoot() { | |
| return ist.Item.String() | |
| } | |
| return ist.Stack.String() + "." + ist.Item.String() | |
| } | |
| func (ist InStackConfig[T]) UniqueKey() collections.UniqueKey[InStackConfig[T]] { | |
| return inStackConfigKey[T]{ | |
| stackKey: ist.Stack.UniqueKey(), | |
| itemKey: ist.Item.UniqueKey(), | |
| } | |
| } | |
| type inStackConfigKey[T StackItemConfig[T]] struct { | |
| stackKey collections.UniqueKey[Stack] | |
| itemKey collections.UniqueKey[T] | |
| } | |
| // IsUniqueKey implements collections.UniqueKey. | |
| func (inStackConfigKey[T]) IsUniqueKey(InStackConfig[T]) {} | |
| // InStackInstance is the generic form of addresses representing dynamic | |
| // instances of objects that exist within an instance of a stack. | |
| type InStackInstance[T StackItemDynamic[T]] struct { | |
| Stack StackInstance | |
| Item T | |
| } | |
| func Absolute[T StackItemDynamic[T]](stackAddr StackInstance, relAddr T) InStackInstance[T] { | |
| return InStackInstance[T]{ | |
| Stack: stackAddr, | |
| Item: relAddr, | |
| } | |
| } | |
| func (ist InStackInstance[T]) String() string { | |
| if ist.Stack.IsRoot() { | |
| return ist.Item.String() | |
| } | |
| return ist.Stack.String() + "." + ist.Item.String() | |
| } | |
| func (ist InStackInstance[T]) UniqueKey() collections.UniqueKey[InStackInstance[T]] { | |
| return inStackInstanceKey[T]{ | |
| stackKey: ist.Stack.UniqueKey(), | |
| itemKey: ist.Item.UniqueKey(), | |
| } | |
| } | |
| type inStackInstanceKey[T StackItemDynamic[T]] struct { | |
| stackKey collections.UniqueKey[StackInstance] | |
| itemKey collections.UniqueKey[T] | |
| } | |
| // IsUniqueKey implements collections.UniqueKey. | |
| func (inStackInstanceKey[T]) IsUniqueKey(InStackInstance[T]) {} | |
| // ConfigForAbs returns the "in stack config" equivalent of the given | |
| // "in stack instance" (absolute) address by just discarding any | |
| // instance keys from the stack instance steps. | |
| func ConfigForAbs[T interface { | |
| StackItemDynamic[T] | |
| StackItemConfig[T] | |
| }](absAddr InStackInstance[T]) InStackConfig[T] { | |
| return Config(absAddr.Stack.ConfigAddr(), absAddr.Item) | |
| } | |
| // parseInStackInstancePrefix parses as many nested stack traversal steps | |
| // as possible from the start of the given traversal, and then returns | |
| // the resulting StackInstance address along with a relative traversal | |
| // covering all of the remaining traversal steps, if any. | |
| func parseInStackInstancePrefix(traversal hcl.Traversal) (StackInstance, hcl.Traversal, tfdiags.Diagnostics) { | |
| if len(traversal) == 0 { | |
| return RootStackInstance, nil, nil | |
| } | |
| const errSummary = "Invalid stack instance address" | |
| var diags tfdiags.Diagnostics | |
| var stackInst StackInstance | |
| Steps: | |
| for len(traversal) > 0 { | |
| switch step := traversal[0].(type) { | |
| case hcl.TraverseRoot: | |
| if step.Name != "stack" { | |
| break Steps | |
| } | |
| case hcl.TraverseAttr: | |
| if step.Name != "stack" { | |
| break Steps | |
| } | |
| default: | |
| break Steps | |
| } | |
| // If we get here then we know that we're expecting a valid | |
| // stack instance step prefix, which always consists of the | |
| // literal step "stack" (which we found above) followed | |
| // by an embedded stack name. That might then be followed | |
| // by one optional index step for a multi-instance embedded stack. | |
| if len(traversal) < 2 { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: errSummary, | |
| Detail: "The \"stack\" keyword must be followed by an attribute specifying the name of the embedded stack.", | |
| Subject: traversal.SourceRange().Ptr(), | |
| }) | |
| return nil, nil, diags | |
| } | |
| nameStep, ok := traversal[1].(hcl.TraverseAttr) | |
| if !ok { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: errSummary, | |
| Detail: "The \"stack\" keyword must be followed by an attribute specifying the name of the embedded stack.", | |
| Subject: traversal[1].SourceRange().Ptr(), | |
| }) | |
| return nil, nil, diags | |
| } | |
| if !hclsyntax.ValidIdentifier(nameStep.Name) { | |
| // This check is redundant since the HCL parser should've caught | |
| // an invalid identifier while parsing this traversal, but this | |
| // is here for robustness in case we obtained this traversal | |
| // value in an unusual way. | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: errSummary, | |
| Detail: "A stack name must be a valid identifier.", | |
| Subject: nameStep.SourceRange().Ptr(), | |
| }) | |
| return nil, nil, diags | |
| } | |
| addrStep := StackInstanceStep{ | |
| Name: nameStep.Name, | |
| Key: addrs.NoKey, | |
| } | |
| traversal = traversal[2:] // consume the first two steps that we already dealt with | |
| if len(traversal) > 0 { | |
| switch idxStep := traversal[0].(type) { | |
| case hcl.TraverseIndex: | |
| var err error | |
| addrStep.Key, err = addrs.ParseInstanceKey(idxStep.Key) | |
| if err != nil { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: errSummary, | |
| Detail: fmt.Sprintf("Invalid instance key: %s.", err), | |
| Subject: idxStep.SourceRange().Ptr(), | |
| }) | |
| return nil, nil, diags | |
| } | |
| traversal = traversal[1:] // consume the step we just dealt with | |
| case hcl.TraverseSplat: | |
| addrStep.Key = addrs.WildcardKey | |
| traversal = traversal[1:] | |
| } | |
| } | |
| stackInst = append(stackInst, addrStep) | |
| } | |
| return stackInst, forceTraversalRelative(traversal), diags | |
| } | |
| // forceTraversalRelative takes any traversal and if it's absolute transforms | |
| // it into a relative one by changing the first step from a TraverseRoot | |
| // to an equivalent TraverseAttr. | |
| func forceTraversalRelative(given hcl.Traversal) hcl.Traversal { | |
| if len(given) == 0 { | |
| return nil | |
| } | |
| firstStep, ok := given[0].(hcl.TraverseRoot) | |
| if !ok { | |
| return given | |
| } | |
| // If we get here then we have an absolute traversal. We shouldn't | |
| // mutate the backing array of the traversal because others might | |
| // still be using it, so we'll allocate a new traversal and copy | |
| // the steps into it. | |
| ret := make(hcl.Traversal, len(given)) | |
| ret[0] = hcl.TraverseAttr{ | |
| Name: firstStep.Name, | |
| SrcRange: firstStep.SrcRange, | |
| } | |
| copy(ret[1:], given[1:]) | |
| return ret | |
| } |