// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package stackconfig import ( "github.com/apparentlymart/go-versions/versions/constraints" "github.com/hashicorp/go-slug/sourceaddrs" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/tfdiags" ) // EmbeddedStack describes a call to another stack configuration whose // declarations should be included as part of the overall stack configuration // tree. // // An embedded stack exists only as a child of another stack and doesn't have // its own independent identity outside of that calling stack. // // HCP Terraform offers a related concept of "linked stacks" where the // deployment configuration for one stack can refer to the outputs of another, // while the other stack retains its own independent identity and lifecycle, // but that concept only makes sense in an environment like HCP Terraform // where the stack outputs can be published for external consumption. type EmbeddedStack struct { Name string SourceAddr sourceaddrs.Source VersionConstraints constraints.IntersectionSpec SourceAddrRange, VersionConstraintsRange tfdiags.SourceRange // FinalSourceAddr is populated only when a configuration is loaded // through [LoadConfigDir], and in that case contains the finalized // address produced by resolving the SourceAddr field relative to // the address of the file where the component was declared. This // is the address to use if you intend to load the component's // root module from a source bundle. FinalSourceAddr sourceaddrs.FinalSource ForEach hcl.Expression // Inputs is an expression that should produce a value that can convert // to an object type derived from the child stack's input variable // declarations, and whose attribute values will then be used to populate // those input variables. Inputs hcl.Expression DependsOn []hcl.Traversal DeclRange tfdiags.SourceRange } func decodeEmbeddedStackBlock(block *hcl.Block) (*EmbeddedStack, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics ret := &EmbeddedStack{ Name: block.Labels[0], DeclRange: tfdiags.SourceRangeFromHCL(block.DefRange), } if !hclsyntax.ValidIdentifier(ret.Name) { diags = diags.Append(invalidNameDiagnostic( "Invalid name for call to embedded stack", block.LabelRanges[0], )) return nil, diags } content, hclDiags := block.Body.Content(embeddedStackBlockSchema) diags = diags.Append(hclDiags) if hclDiags.HasErrors() { return nil, diags } sourceAddr, versionConstraints, moreDiags := decodeSourceAddrArguments( content.Attributes["source"], content.Attributes["version"], ) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { return nil, diags } ret.SourceAddr = sourceAddr ret.VersionConstraints = versionConstraints ret.SourceAddrRange = tfdiags.SourceRangeFromHCL(content.Attributes["source"].Range) if content.Attributes["version"] != nil { ret.VersionConstraintsRange = tfdiags.SourceRangeFromHCL(content.Attributes["version"].Range) } // Now that we've populated the mandatory source location fields we can // safely return a partial ret if we encounter any further errors, as // long as we leave the other fields either unset or in some other // reasonable state for careful partial analysis. if attr, ok := content.Attributes["for_each"]; ok { ret.ForEach = attr.Expr } if attr, ok := content.Attributes["inputs"]; ok { ret.Inputs = attr.Expr } if attr, ok := content.Attributes["depends_on"]; ok { ret.DependsOn, hclDiags = configs.DecodeDependsOn(attr) diags = diags.Append(hclDiags) } return ret, diags } var embeddedStackBlockSchema = &hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ {Name: "source", Required: true}, {Name: "version", Required: false}, {Name: "for_each", Required: false}, {Name: "inputs", Required: false}, {Name: "depends_on", Required: false}, }, }