terraform/internal/stacks/stackconfig/component.go
2025-03-19 10:39:50 +01:00

335 lines
11 KiB
Go

// Copyright (c) HashiCorp, Inc.
// 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},
},
}