| // 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" | |
| ) | |
| // Component is the address of a "component" block within a stack config. | |
| type Component struct { | |
| Name string | |
| } | |
| func (Component) referenceableSigil() {} | |
| func (Component) inStackConfigSigil() {} | |
| func (Component) inStackInstanceSigil() {} | |
| func (c Component) String() string { | |
| return "component." + c.Name | |
| } | |
| func (c Component) UniqueKey() collections.UniqueKey[Component] { | |
| return c | |
| } | |
| // A Component is its own [collections.UniqueKey]. | |
| func (Component) IsUniqueKey(Component) {} | |
| // ConfigComponent places a [Component] in the context of a particular [Stack]. | |
| type ConfigComponent = InStackConfig[Component] | |
| // AbsComponent places a [Component] in the context of a particular [StackInstance]. | |
| type AbsComponent = InStackInstance[Component] | |
| func AbsComponentToInstance(ist AbsComponent, ik addrs.InstanceKey) AbsComponentInstance { | |
| return AbsComponentInstance{ | |
| Stack: ist.Stack, | |
| Item: ComponentInstance{ | |
| Component: ist.Item, | |
| Key: ik, | |
| }, | |
| } | |
| } | |
| // ComponentInstance is the address of a dynamic instance of a component. | |
| type ComponentInstance struct { | |
| Component Component | |
| Key addrs.InstanceKey | |
| } | |
| func (ComponentInstance) inStackConfigSigil() {} | |
| func (ComponentInstance) inStackInstanceSigil() {} | |
| func (c ComponentInstance) String() string { | |
| if c.Key == nil { | |
| return c.Component.String() | |
| } | |
| return c.Component.String() + c.Key.String() | |
| } | |
| func (c ComponentInstance) UniqueKey() collections.UniqueKey[ComponentInstance] { | |
| return c | |
| } | |
| // A ComponentInstance is its own [collections.UniqueKey]. | |
| func (ComponentInstance) IsUniqueKey(ComponentInstance) {} | |
| // ConfigComponentInstance places a [ComponentInstance] in the context of a | |
| // particular [Stack]. | |
| type ConfigComponentInstance = InStackConfig[ComponentInstance] | |
| // AbsComponentInstance places a [ComponentInstance] in the context of a | |
| // particular [StackInstance]. | |
| type AbsComponentInstance = InStackInstance[ComponentInstance] | |
| func ConfigComponentForAbsInstance(instAddr AbsComponentInstance) ConfigComponent { | |
| configInst := ConfigForAbs(instAddr) // a ConfigComponentInstance | |
| return ConfigComponent{ | |
| Stack: configInst.Stack, | |
| Item: Component{ | |
| Name: configInst.Item.Component.Name, | |
| }, | |
| } | |
| } | |
| func ParseAbsComponentInstance(traversal hcl.Traversal) (AbsComponentInstance, tfdiags.Diagnostics) { | |
| inst, remain, diags := ParseAbsComponentInstanceOnly(traversal) | |
| if diags.HasErrors() { | |
| return AbsComponentInstance{}, diags | |
| } | |
| if len(remain) > 0 { | |
| // Then we have some remaining traversal steps that weren't consumed | |
| // by the component instance address itself, which is an error when the | |
| // caller is using this function. | |
| rng := remain.SourceRange() | |
| // if "remain" is empty then the source range would be zero length, | |
| // and so we'll use the original traversal instead. | |
| if len(remain) == 0 { | |
| rng = traversal.SourceRange() | |
| } | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Invalid component instance address", | |
| Detail: "The component instance address must include the keyword \"component\" followed by a component name.", | |
| Subject: &rng, | |
| }) | |
| return AbsComponentInstance{}, diags | |
| } | |
| return inst, diags | |
| } | |
| func ParseAbsComponentInstanceStr(s string) (AbsComponentInstance, tfdiags.Diagnostics) { | |
| var diags tfdiags.Diagnostics | |
| traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(s), "", hcl.InitialPos) | |
| diags = diags.Append(hclDiags) | |
| if diags.HasErrors() { | |
| return AbsComponentInstance{}, diags | |
| } | |
| ret, moreDiags := ParseAbsComponentInstance(traversal) | |
| diags = diags.Append(moreDiags) | |
| return ret, diags | |
| } | |
| func ParsePartialComponentInstanceStr(s string) (AbsComponentInstance, tfdiags.Diagnostics) { | |
| var diags tfdiags.Diagnostics | |
| traversal, hclDiags := hclsyntax.ParseTraversalPartial([]byte(s), "", hcl.InitialPos) | |
| diags = diags.Append(hclDiags) | |
| if diags.HasErrors() { | |
| return AbsComponentInstance{}, diags | |
| } | |
| ret, moreDiags := ParseAbsComponentInstance(traversal) | |
| diags = diags.Append(moreDiags) | |
| return ret, diags | |
| } | |
| func ParseAbsComponentInstanceStrOnly(s string) (AbsComponentInstance, hcl.Traversal, tfdiags.Diagnostics) { | |
| var diags tfdiags.Diagnostics | |
| traversal, hclDiags := hclsyntax.ParseTraversalPartial([]byte(s), "", hcl.InitialPos) | |
| diags = diags.Append(hclDiags) | |
| if diags.HasErrors() { | |
| return AbsComponentInstance{}, traversal, diags | |
| } | |
| ret, rest, moreDiags := ParseAbsComponentInstanceOnly(traversal) | |
| diags = diags.Append(moreDiags) | |
| return ret, rest, diags | |
| } | |
| func ParseAbsComponentInstanceOnly(traversal hcl.Traversal) (AbsComponentInstance, hcl.Traversal, tfdiags.Diagnostics) { | |
| if traversal.IsRelative() { | |
| // This is always a caller bug: caller must only pass absolute | |
| // traversals in here. | |
| panic("ParseAbsComponentInstanceOnly with relative traversal") | |
| } | |
| stackInst, remain, diags := parseInStackInstancePrefix(traversal) | |
| if diags.HasErrors() { | |
| return AbsComponentInstance{}, remain, diags | |
| } | |
| // "remain" should now be the keyword "component" followed by a valid | |
| // component name, optionally followed by an instance key. | |
| const diagSummary = "Invalid component instance address" | |
| if kwStep, ok := remain[0].(hcl.TraverseAttr); !ok || kwStep.Name != "component" { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: diagSummary, | |
| Detail: "The component instance address must include the keyword \"component\" followed by a component name.", | |
| Subject: remain[0].SourceRange().Ptr(), | |
| }) | |
| return AbsComponentInstance{}, remain, diags | |
| } | |
| remain = remain[1:] | |
| nameStep, ok := remain[0].(hcl.TraverseAttr) | |
| if !ok { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: diagSummary, | |
| Detail: "The component instance address must include the keyword \"component\" followed by a component name.", | |
| Subject: remain[1].SourceRange().Ptr(), | |
| }) | |
| return AbsComponentInstance{}, remain, diags | |
| } | |
| remain = remain[1:] | |
| componentAddr := ComponentInstance{ | |
| Component: Component{Name: nameStep.Name}, | |
| } | |
| if len(remain) > 0 { | |
| switch instStep := remain[0].(type) { | |
| case hcl.TraverseIndex: | |
| var err error | |
| componentAddr.Key, err = addrs.ParseInstanceKey(instStep.Key) | |
| if err != nil { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: diagSummary, | |
| Detail: fmt.Sprintf("Invalid instance key: %s.", err), | |
| Subject: instStep.SourceRange().Ptr(), | |
| }) | |
| return AbsComponentInstance{}, remain, diags | |
| } | |
| remain = remain[1:] | |
| case hcl.TraverseSplat: | |
| componentAddr.Key = addrs.WildcardKey | |
| remain = remain[1:] | |
| } | |
| } | |
| return AbsComponentInstance{ | |
| Stack: stackInst, | |
| Item: componentAddr, | |
| }, remain, diags | |
| } |