2023-09-27 19:59:48 -04:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
2023-05-22 20:09:53 -04:00
package stackconfig
import (
2023-05-24 13:12:19 -04:00
"fmt"
2023-05-22 20:09:53 -04:00
"github.com/apparentlymart/go-versions/versions/constraints"
"github.com/hashicorp/go-slug/sourceaddrs"
2025-03-19 05:39:50 -04:00
"github.com/hashicorp/go-slug/sourcebundle"
2023-05-22 20:09:53 -04:00
"github.com/hashicorp/hcl/v2"
2023-05-24 13:12:19 -04:00
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsyntax"
2025-03-19 05:39:50 -04:00
stackparser "github.com/hashicorp/terraform/internal/stacks/stackconfig/parser"
2024-08-12 09:02:36 -04:00
2023-05-22 20:09:53 -04:00
"github.com/hashicorp/terraform/internal/addrs"
2024-08-12 09:02:36 -04:00
"github.com/hashicorp/terraform/internal/configs"
2023-05-24 13:12:19 -04:00
"github.com/hashicorp/terraform/internal/tfdiags"
2023-05-22 20:09:53 -04:00
)
// 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
2023-05-24 13:12:19 -04:00
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
2023-05-22 20:09:53 -04:00
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
2023-05-24 13:12:19 -04:00
2024-08-12 09:02:36 -04:00
// DependsOn forces a dependency between this resource and the list
// resources, allowing users to specify ordering of components without
// direct references.
DependsOn [ ] hcl . Traversal
2023-05-24 13:12:19 -04:00
DeclRange tfdiags . SourceRange
}
2025-03-19 05:39:50 -04:00
// 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
}
2023-05-24 13:12:19 -04:00
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 {
2024-09-05 06:22:16 -04:00
var providerDiags tfdiags . Diagnostics
ret . ProviderConfigs , providerDiags = decodeProvidersAttribute ( attr )
diags = diags . Append ( providerDiags )
2023-05-24 13:12:19 -04:00
}
2024-08-12 09:02:36 -04:00
if attr , exists := content . Attributes [ "depends_on" ] ; exists {
ret . DependsOn , hclDiags = configs . DecodeDependsOn ( attr )
diags = diags . Append ( hclDiags )
}
2023-05-24 13:12:19 -04:00
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
}
2024-09-05 06:22:16 -04:00
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
}
2023-05-24 13:12:19 -04:00
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 } ,
2024-08-12 09:02:36 -04:00
{ Name : "depends_on" , Required : false } ,
2023-05-24 13:12:19 -04:00
} ,
2023-05-22 20:09:53 -04:00
}