| // 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"}}, | |
| }, | |
| } |