mirror of
https://github.com/hashicorp/terraform.git
synced 2026-02-19 02:39:17 -05:00
335 lines
11 KiB
Go
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},
|
|
},
|
|
}
|