terraform/internal/configs/module_call.go
Daniel Banck 11d819048a Store module source and version expressions
Instead of evaluating and parsing a module source and version on
configuration loading, we now simply store the expression.

Decoding is now done during the graph-based configuration loading in the
module install node.
2026-03-04 11:45:59 +01:00

237 lines
6.4 KiB
Go

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package configs
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
// ModuleCall represents a "module" block in a module or file.
type ModuleCall struct {
Name string
SourceExpr hcl.Expression
Config hcl.Body
VersionExpr hcl.Expression
Count hcl.Expression
ForEach hcl.Expression
Providers []PassedProviderConfig
DependsOn []hcl.Traversal
DeclRange hcl.Range
IgnoreNestedDeprecations bool
}
func decodeModuleBlock(block *hcl.Block, override bool) (*ModuleCall, hcl.Diagnostics) {
var diags hcl.Diagnostics
mc := &ModuleCall{
Name: block.Labels[0],
DeclRange: block.DefRange,
}
schema := moduleBlockSchema
if override {
schema = schemaForOverrides(schema)
}
content, remain, moreDiags := block.Body.PartialContent(schema)
diags = append(diags, moreDiags...)
mc.Config = remain
if !hclsyntax.ValidIdentifier(mc.Name) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid module instance name",
Detail: badIdentifierDetail,
Subject: &block.LabelRanges[0],
})
}
if attr, exists := content.Attributes["version"]; exists {
mc.VersionExpr = attr.Expr
}
if attr, exists := content.Attributes["source"]; exists {
mc.SourceExpr = attr.Expr
}
if attr, exists := content.Attributes["count"]; exists {
mc.Count = attr.Expr
}
if attr, exists := content.Attributes["for_each"]; exists {
if mc.Count != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid combination of "count" and "for_each"`,
Detail: `The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used to be explicit about the number of resources to be created.`,
Subject: &attr.NameRange,
})
}
mc.ForEach = attr.Expr
}
if attr, exists := content.Attributes["depends_on"]; exists {
deps, depsDiags := DecodeDependsOn(attr)
diags = append(diags, depsDiags...)
mc.DependsOn = append(mc.DependsOn, deps...)
}
if attr, exists := content.Attributes["providers"]; exists {
providers, providerDiags := decodePassedProviderConfigs(attr)
diags = append(diags, providerDiags...)
mc.Providers = append(mc.Providers, providers...)
}
if attr, exists := content.Attributes["ignore_nested_deprecations"]; exists {
// We only allow static boolean values for this argument.
val, evalDiags := attr.Expr.Value(&hcl.EvalContext{})
if len(evalDiags.Errs()) > 0 {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid value for ignore_nested_deprecations",
Detail: "The value for ignore_nested_deprecations must be a static boolean (true or false).",
Subject: attr.Expr.Range().Ptr(),
})
}
if val.Type() != cty.Bool {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid type for ignore_nested_deprecations",
Detail: fmt.Sprintf("The value for ignore_nested_deprecations must be a boolean (true or false), but the given value has type %s.", val.Type().FriendlyName()),
Subject: attr.Expr.Range().Ptr(),
})
}
mc.IgnoreNestedDeprecations = val.True()
}
var seenEscapeBlock *hcl.Block
for _, block := range content.Blocks {
switch block.Type {
case "_":
if seenEscapeBlock != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate escaping block",
Detail: fmt.Sprintf(
"The special block type \"_\" can be used to force particular arguments to be interpreted as module input variables rather than as meta-arguments, but each module block can have only one such block. The first escaping block was at %s.",
seenEscapeBlock.DefRange,
),
Subject: &block.DefRange,
})
continue
}
seenEscapeBlock = block
// When there's an escaping block its content merges with the
// existing config we extracted earlier, so later decoding
// will see a blend of both.
mc.Config = hcl.MergeBodies([]hcl.Body{mc.Config, block.Body})
default:
// All of the other block types in our schema are reserved.
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reserved block type name in module block",
Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type),
Subject: &block.TypeRange,
})
}
}
return mc, diags
}
// PassedProviderConfig represents a provider config explicitly passed down to
// a child module, possibly giving it a new local address in the process.
type PassedProviderConfig struct {
InChild *ProviderConfigRef
InParent *ProviderConfigRef
}
func decodePassedProviderConfigs(attr *hcl.Attribute) ([]PassedProviderConfig, hcl.Diagnostics) {
var diags hcl.Diagnostics
var providers []PassedProviderConfig
seen := make(map[string]hcl.Range)
pairs, pDiags := hcl.ExprMap(attr.Expr)
diags = append(diags, pDiags...)
for _, pair := range pairs {
key, keyDiags := decodeProviderConfigRef(pair.Key, "providers")
diags = append(diags, keyDiags...)
value, valueDiags := decodeProviderConfigRef(pair.Value, "providers")
diags = append(diags, valueDiags...)
if keyDiags.HasErrors() || valueDiags.HasErrors() {
continue
}
matchKey := key.String()
if prev, exists := seen[matchKey]; exists {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate provider address",
Detail: fmt.Sprintf("A provider configuration was already passed to %s at %s. Each child provider configuration can be assigned only once.", matchKey, prev),
Subject: pair.Value.Range().Ptr(),
})
continue
}
rng := hcl.RangeBetween(pair.Key.Range(), pair.Value.Range())
seen[matchKey] = rng
providers = append(providers, PassedProviderConfig{
InChild: key,
InParent: value,
})
}
return providers, diags
}
var moduleBlockSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "source",
Required: true,
},
{
Name: "version",
},
{
Name: "count",
},
{
Name: "for_each",
},
{
Name: "depends_on",
},
{
Name: "providers",
},
{
Name: "ignore_nested_deprecations",
},
},
Blocks: []hcl.BlockHeaderSchema{
{Type: "_"}, // meta-argument escaping block
// These are all reserved for future use.
{Type: "lifecycle"},
{Type: "locals"},
{Type: "provider", LabelNames: []string{"type"}},
},
}