| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package stackconfig | |
| import ( | |
| "fmt" | |
| "github.com/apparentlymart/go-versions/versions/constraints" | |
| "github.com/hashicorp/go-slug/sourceaddrs" | |
| "github.com/hashicorp/go-slug/sourcebundle" | |
| "github.com/hashicorp/hcl/v2" | |
| "github.com/hashicorp/hcl/v2/gohcl" | |
| "github.com/hashicorp/hcl/v2/hclsyntax" | |
| stackparser "github.com/hashicorp/terraform/internal/stacks/stackconfig/parser" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/configs" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| ) | |
| // Component represents the declaration of a single component within a | |
| // particular [Stack]. | |
| // | |
| // Components are the most important object in a stack configuration, just as | |
| // resources are the most important object in a Terraform module: each one | |
| // refers to a Terraform module that describes the infrastructure that the | |
| // component is "made of". | |
| type Component 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. | |
| // | |
| // If this Component was created through one of the narrower configuration | |
| // loading functions, such as [LoadSingleStackConfig] or [ParseFileSource], | |
| // then this field will be nil and it won't be possible to determine the | |
| // finalized source location for the root module. | |
| 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 component's input variable | |
| // declarations, and whose attribute values will then be used to populate | |
| // those input variables. | |
| Inputs hcl.Expression | |
| // ProviderConfigs describes the mapping between the static provider | |
| // configuration slots declared in the component's root module and the | |
| // dynamic provider configuration objects in scope in the calling | |
| // stack configuration. | |
| // | |
| // This map deals with the slight schism between the stacks language's | |
| // treatment of provider configurations as regular values of a special | |
| // data type vs. the main Terraform language's treatment of provider | |
| // configurations as something special passed out of band from the | |
| // input variables. The overall structure and the map keys are fixed | |
| // statically during decoding, but the final provider configuration objects | |
| // are determined only at runtime by normal expression evaluation. | |
| // | |
| // The keys of this map refer to provider configuration slots inside | |
| // the module being called, but use the local names defined in the | |
| // calling stack configuration. The stacks language runtime will | |
| // translate the caller's local names into the callee's declared provider | |
| // configurations by using the stack configuration's table of local | |
| // provider names. | |
| ProviderConfigs map[addrs.LocalProviderConfig]hcl.Expression | |
| // DependsOn forces a dependency between this resource and the list | |
| // resources, allowing users to specify ordering of components without | |
| // direct references. | |
| DependsOn []hcl.Traversal | |
| DeclRange tfdiags.SourceRange | |
| } | |
| // ModuleConfig returns the module configuration for the given address within | |
| // the provided source bundle. | |
| func (c *Component) ModuleConfig(bundle *sourcebundle.Bundle) (*configs.Config, tfdiags.Diagnostics) { | |
| var diags tfdiags.Diagnostics | |
| parser := configs.NewSourceBundleParser(bundle) | |
| if !parser.IsConfigDir(c.FinalSourceAddr) { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Component configuration not found", | |
| Detail: fmt.Sprintf("No module configuration found for component %q at %s.", c.Name, c.FinalSourceAddr), | |
| Subject: c.SourceAddrRange.ToHCL().Ptr(), | |
| }) | |
| return nil, diags | |
| } | |
| module, moreDiags := parser.LoadConfigDir(c.FinalSourceAddr) | |
| diags = diags.Append(moreDiags) | |
| if module != nil { | |
| walker := stackparser.NewSourceBundleModuleWalker(c.FinalSourceAddr, bundle, parser) | |
| config, moreDiags := configs.BuildConfig(module, walker, nil) | |
| diags = diags.Append(moreDiags) | |
| return config, diags | |
| } | |
| return nil, diags | |
| } | |
| func decodeComponentBlock(block *hcl.Block) (*Component, tfdiags.Diagnostics) { | |
| var diags tfdiags.Diagnostics | |
| ret := &Component{ | |
| Name: block.Labels[0], | |
| DeclRange: tfdiags.SourceRangeFromHCL(block.DefRange), | |
| } | |
| if !hclsyntax.ValidIdentifier(ret.Name) { | |
| diags = diags.Append(invalidNameDiagnostic( | |
| "Invalid component name", | |
| block.LabelRanges[0], | |
| )) | |
| return nil, diags | |
| } | |
| content, hclDiags := block.Body.Content(componentBlockSchema) | |
| 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["providers"]; ok { | |
| var providerDiags tfdiags.Diagnostics | |
| ret.ProviderConfigs, providerDiags = decodeProvidersAttribute(attr) | |
| diags = diags.Append(providerDiags) | |
| } | |
| if attr, exists := content.Attributes["depends_on"]; exists { | |
| ret.DependsOn, hclDiags = configs.DecodeDependsOn(attr) | |
| diags = diags.Append(hclDiags) | |
| } | |
| return ret, diags | |
| } | |
| func decodeSourceAddrArguments(sourceAttr, versionAttr *hcl.Attribute) (sourceaddrs.Source, constraints.IntersectionSpec, tfdiags.Diagnostics) { | |
| var diags tfdiags.Diagnostics | |
| var sourceStr string | |
| hclDiags := gohcl.DecodeExpression(sourceAttr.Expr, nil, &sourceStr) | |
| diags = diags.Append(hclDiags) | |
| if hclDiags.HasErrors() { | |
| return nil, nil, diags | |
| } | |
| sourceAddr, err := sourceaddrs.ParseSource(sourceStr) | |
| if err != nil { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Invalid source address", | |
| Detail: fmt.Sprintf( | |
| "Cannot parse %q as a source address: %s.", | |
| sourceStr, err, | |
| ), | |
| Subject: sourceAttr.Expr.Range().Ptr(), | |
| }) | |
| return nil, nil, diags | |
| } | |
| var versionConstraints constraints.IntersectionSpec | |
| if sourceAddr.SupportsVersionConstraints() { | |
| if versionAttr == nil { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Missing required version constraints", | |
| Detail: "The specified source address requires version constraints specified in a separate \"version\" argument.", | |
| Subject: sourceAttr.Expr.Range().Ptr(), | |
| }) | |
| return nil, nil, diags | |
| } | |
| var versionStr string | |
| hclDiags := gohcl.DecodeExpression(versionAttr.Expr, nil, &versionStr) | |
| diags = diags.Append(hclDiags) | |
| if hclDiags.HasErrors() { | |
| return nil, nil, diags | |
| } | |
| versionConstraints, err = constraints.ParseRubyStyleMulti(versionStr) | |
| if err != nil { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Invalid version constraints", | |
| Detail: fmt.Sprintf( | |
| "Cannot parse %q as source package version constraints: %s.", | |
| versionStr, err, | |
| ), | |
| Subject: versionAttr.Expr.Range().Ptr(), | |
| }) | |
| return nil, nil, diags | |
| } | |
| } else { | |
| if versionAttr != nil { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Unsupported version constraints", | |
| Detail: "The specified source address does not support version constraints.", | |
| Subject: versionAttr.Range.Ptr(), | |
| }) | |
| return nil, nil, diags | |
| } | |
| } | |
| return sourceAddr, versionConstraints, diags | |
| } | |
| func decodeProvidersAttribute(attr *hcl.Attribute) (map[addrs.LocalProviderConfig]hcl.Expression, tfdiags.Diagnostics) { | |
| var diags tfdiags.Diagnostics | |
| // This particular argument has some enforced static structure because | |
| // it's populating an inflexible part of Terraform Core's input. | |
| // This argument, if present, must always be an object constructor | |
| // whose attributes are Terraform Core-style provider configuration | |
| // addresses, but whose values are just arbitrary expressions for now | |
| // and will be resolved into specific provider configuration addresses | |
| // dynamically at runtime. | |
| pairs, hclDiags := hcl.ExprMap(attr.Expr) | |
| diags = diags.Append(hclDiags) | |
| if hclDiags.HasErrors() { | |
| return nil, diags | |
| } | |
| ret := map[addrs.LocalProviderConfig]hcl.Expression{} | |
| for _, pair := range pairs { | |
| insideAddrExpr := pair.Key | |
| outsideAddrExpr := pair.Value | |
| traversal, hclDiags := hcl.AbsTraversalForExpr(insideAddrExpr) | |
| diags = diags.Append(hclDiags) | |
| if hclDiags.HasErrors() { | |
| continue | |
| } | |
| if len(traversal) < 1 || len(traversal) > 2 { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Invalid provider configuration reference", | |
| Detail: "Each item in the providers argument requires a provider local name, optionally followed by a period and then a configuration alias, matching one of the provider configuration import slots declared by the component's root module.", | |
| Subject: insideAddrExpr.Range().Ptr(), | |
| }) | |
| continue | |
| } | |
| localName := traversal.RootName() | |
| if !hclsyntax.ValidIdentifier(localName) { | |
| diags = diags.Append(invalidNameDiagnostic( | |
| "Invalid provider local name", | |
| traversal[0].SourceRange(), | |
| )) | |
| continue | |
| } | |
| var alias string | |
| if len(traversal) > 1 { | |
| aliasStep, ok := traversal[1].(hcl.TraverseAttr) | |
| if !ok { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Invalid provider configuration reference", | |
| Detail: "Provider local name must either stand alone or be followed by a period and then a configuration alias.", | |
| Subject: traversal[1].SourceRange().Ptr(), | |
| }) | |
| continue | |
| } | |
| alias = aliasStep.Name | |
| } | |
| addr := addrs.LocalProviderConfig{ | |
| LocalName: localName, | |
| Alias: alias, | |
| } | |
| if existing, exists := ret[addr]; exists { | |
| diags = diags.Append(&hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Duplicate provider configuration assignment", | |
| Detail: fmt.Sprintf( | |
| "A provider configuration for %s was already assigned at %s.", | |
| addr.StringCompact(), existing.Range().Ptr(), | |
| ), | |
| Subject: outsideAddrExpr.Range().Ptr(), | |
| }) | |
| continue | |
| } else { | |
| ret[addr] = outsideAddrExpr | |
| } | |
| } | |
| return ret, diags | |
| } | |
| var componentBlockSchema = &hcl.BodySchema{ | |
| Attributes: []hcl.AttributeSchema{ | |
| {Name: "source", Required: true}, | |
| {Name: "version", Required: false}, | |
| {Name: "for_each", Required: false}, | |
| {Name: "inputs", Required: false}, | |
| {Name: "providers", Required: false}, | |
| {Name: "depends_on", Required: false}, | |
| }, | |
| } |