Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/v1.16/NEW FEATURES-20260423-120556.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: NEW FEATURES
body: Required provider' source and version can now be evaluated from variables
time: 2026-04-23T12:05:56.014251+02:00
custom:
Issue: "38405"
76 changes: 46 additions & 30 deletions internal/configs/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ type Module struct {

ActiveExperiments experiments.Set

Backend *Backend
StateStore *StateStore
CloudConfig *CloudConfig
ProviderConfigs map[string]*Provider
ProviderRequirements *RequiredProviders
ProviderLocalNames map[addrs.Provider]string
ProviderMetas map[addrs.Provider]*ProviderMeta
Backend *Backend
StateStore *StateStore
CloudConfig *CloudConfig
ProviderConfigs map[string]*Provider
ProviderRequirements *RequiredProviders
ProviderRequirementExprs map[string]*ProviderRequirementExpr
ProviderLocalNames map[addrs.Provider]string
ProviderMetas map[addrs.Provider]*ProviderMeta

Variables map[string]*Variable
Locals map[string]*Local
Expand Down Expand Up @@ -78,12 +79,13 @@ type File struct {

ActiveExperiments experiments.Set

Backends []*Backend
StateStores []*StateStore
CloudConfigs []*CloudConfig
ProviderConfigs []*Provider
ProviderMetas []*ProviderMeta
RequiredProviders []*RequiredProviders
Backends []*Backend
StateStores []*StateStore
CloudConfigs []*CloudConfig
ProviderConfigs []*Provider
ProviderMetas []*ProviderMeta
RequiredProviders []*RequiredProviders
RequiredProviderExprs []*ProviderRequirementExpr

Variables []*Variable
Locals []*Local
Expand Down Expand Up @@ -124,20 +126,21 @@ func NewModuleWithTests(primaryFiles, overrideFiles []*File, testFiles map[strin
func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) {
var diags hcl.Diagnostics
mod := &Module{
ProviderConfigs: map[string]*Provider{},
ProviderLocalNames: map[addrs.Provider]string{},
Variables: map[string]*Variable{},
Locals: map[string]*Local{},
Outputs: map[string]*Output{},
ModuleCalls: map[string]*ModuleCall{},
ManagedResources: map[string]*Resource{},
EphemeralResources: map[string]*Resource{},
DataResources: map[string]*Resource{},
ListResources: map[string]*Resource{},
Checks: map[string]*Check{},
ProviderMetas: map[addrs.Provider]*ProviderMeta{},
Tests: map[string]*TestFile{},
Actions: map[string]*Action{},
ProviderConfigs: map[string]*Provider{},
ProviderLocalNames: map[addrs.Provider]string{},
Variables: map[string]*Variable{},
Locals: map[string]*Local{},
Outputs: map[string]*Output{},
ModuleCalls: map[string]*ModuleCall{},
ManagedResources: map[string]*Resource{},
EphemeralResources: map[string]*Resource{},
DataResources: map[string]*Resource{},
ListResources: map[string]*Resource{},
Checks: map[string]*Check{},
ProviderMetas: map[addrs.Provider]*ProviderMeta{},
ProviderRequirementExprs: map[string]*ProviderRequirementExpr{},
Tests: map[string]*TestFile{},
Actions: map[string]*Action{},
}

// Process the required_providers blocks first, to ensure that all
Expand Down Expand Up @@ -175,6 +178,19 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) {
}
}

// Collect any deferred provider requirement expressions from all files.
for _, file := range primaryFiles {
for _, expr := range file.RequiredProviderExprs {
mod.ProviderRequirementExprs[expr.Name] = expr
}
}

for _, file := range overrideFiles {
for _, expr := range file.RequiredProviderExprs {
mod.ProviderRequirementExprs[expr.Name] = expr
}
}

for _, file := range primaryFiles {
fileDiags := mod.appendFile(file)
diags = append(diags, fileDiags...)
Expand All @@ -188,7 +204,7 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) {
diags = append(diags, checkModuleExperiments(mod)...)

// Generate the FQN -> LocalProviderName map
mod.gatherProviderLocalNames()
mod.GatherProviderLocalNames()

if mod.StateStore != nil {
diags = append(diags, mod.resolveStateStoreProviderType()...)
Expand Down Expand Up @@ -883,11 +899,11 @@ func (m *Module) mergeFile(file *File) hcl.Diagnostics {
return diags
}

// gatherProviderLocalNames is a helper function that populates a map of
// GatherProviderLocalNames is a helper function that populates a map of
// provider FQNs -> provider local names. This information is useful for
// user-facing output, which should include both the FQN and LocalName. It must
// only be populated after the module has been parsed.
func (m *Module) gatherProviderLocalNames() {
func (m *Module) GatherProviderLocalNames() {
providers := make(map[addrs.Provider]string)
for k, v := range m.ProviderRequirements.RequiredProviders {
providers[v.Type] = k
Expand Down
5 changes: 4 additions & 1 deletion internal/configs/parser_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,14 @@ func parseConfigFile(body hcl.Body, diags hcl.Diagnostics, override, allowExperi
}

case "required_providers":
reqs, reqsDiags := decodeRequiredProvidersBlock(innerBlock)
reqs, reqExprs, reqsDiags := decodeRequiredProvidersBlock(innerBlock)
diags = append(diags, reqsDiags...)
if reqs != nil {
file.RequiredProviders = append(file.RequiredProviders, reqs)
}
for _, expr := range reqExprs {
file.RequiredProviderExprs = append(file.RequiredProviderExprs, expr)
}

case "provider_meta":
providerCfg, cfgDiags := decodeProviderMetaBlock(innerBlock)
Expand Down
25 changes: 25 additions & 0 deletions internal/configs/provider_requirement_expr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package configs

import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
)

// ProviderRequirementExpr is a container for deferred HCL expressions for a
// required provider's source and/or version.
type ProviderRequirementExpr struct {
Name string
SourceExpr hcl.Expression
VersionExpr hcl.Expression

ConfigAliases []addrs.LocalProviderConfig

DeclRange hcl.Range
}

func (e *ProviderRequirementExpr) IsEmpty() bool {
return e.SourceExpr == nil && e.VersionExpr == nil
}
68 changes: 64 additions & 4 deletions internal/configs/provider_requirements.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ package configs
import (
"fmt"

version "github.com/hashicorp/go-version"
"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/zclconf/go-cty/cty"
Expand All @@ -30,16 +30,21 @@ type RequiredProviders struct {
DeclRange hcl.Range
}

func decodeRequiredProvidersBlock(block *hcl.Block) (*RequiredProviders, hcl.Diagnostics) {
func decodeRequiredProvidersBlock(block *hcl.Block) (
*RequiredProviders,
map[string]*ProviderRequirementExpr,
hcl.Diagnostics,
) {
attrs, diags := block.Body.JustAttributes()
if diags.HasErrors() {
return nil, diags
return nil, nil, diags
}

ret := &RequiredProviders{
RequiredProviders: make(map[string]*RequiredProvider),
DeclRange: block.DefRange,
}
var deferredExprs map[string]*ProviderRequirementExpr

for name, attr := range attrs {
rp := &RequiredProvider{
Expand Down Expand Up @@ -89,6 +94,14 @@ func decodeRequiredProvidersBlock(block *hcl.Block) (*RequiredProviders, hcl.Dia
continue
}

providerExpr := &ProviderRequirementExpr{
Name: name,
SourceExpr: nil,
VersionExpr: nil,
DeclRange: attr.Expr.Range(),
}
var sourceExpr, versionExpr hcl.Expression

LOOP:
for _, kv := range kvs {
key, keyDiags := kv.Key.Value(nil)
Expand All @@ -109,6 +122,18 @@ func decodeRequiredProvidersBlock(block *hcl.Block) (*RequiredProviders, hcl.Dia

switch key.AsString() {
case "version":
versionExpr = kv.Value

// Store the version expression if it contains variable that
// needs to be evaluated.
//
// Skip the "legacy" pure string resolution of the version
// attribute.
if vars := kv.Value.Variables(); len(vars) > 0 {
providerExpr.VersionExpr = kv.Value
continue
}

vc := VersionConstraint{
DeclRange: attr.Range,
}
Expand Down Expand Up @@ -142,6 +167,18 @@ func decodeRequiredProvidersBlock(block *hcl.Block) (*RequiredProviders, hcl.Dia
rp.Requirement = vc

case "source":
sourceExpr = kv.Value

// Store the source expression if it contains variable that
// needs to be evaluated.
//
// Skip the "legacy" pure string resolution of the source
// attribute.
if vars := kv.Value.Variables(); len(vars) > 0 {
providerExpr.SourceExpr = kv.Value
continue
}

source, err := kv.Value.Value(nil)
if err != nil || !source.Type().Equals(cty.String) {
diags = append(diags, &hcl.Diagnostic{
Expand Down Expand Up @@ -226,6 +263,29 @@ func decodeRequiredProvidersBlock(block *hcl.Block) (*RequiredProviders, hcl.Dia
continue
}

// Provider Expression contains either source or version expression.
// Hydrate the rest, store it into the result map and skip adding it to
// required providers.
if !providerExpr.IsEmpty() {
providerExpr.ConfigAliases = rp.Aliases

if providerExpr.SourceExpr == nil {
providerExpr.SourceExpr = sourceExpr
}

if providerExpr.VersionExpr == nil {
providerExpr.VersionExpr = versionExpr
}

if deferredExprs == nil {
deferredExprs = map[string]*ProviderRequirementExpr{}
}
deferredExprs[name] = providerExpr

// Skip adding it to required providers.
continue
}

// We can add the required provider when there are no errors.
// If a source was not given, create an implied type.
if rp.Type.IsZero() {
Expand All @@ -245,5 +305,5 @@ func decodeRequiredProvidersBlock(block *hcl.Block) (*RequiredProviders, hcl.Dia
ret.RequiredProviders[rp.Name] = rp
}

return ret, diags
return ret, deferredExprs, diags
}
4 changes: 2 additions & 2 deletions internal/configs/provider_requirements_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcltest"
"github.com/hashicorp/terraform/internal/addrs"
Expand Down Expand Up @@ -324,7 +324,7 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {

for name, test := range tests {
t.Run(name, func(t *testing.T) {
got, diags := decodeRequiredProvidersBlock(test.Block)
got, _, diags := decodeRequiredProvidersBlock(test.Block)
if diags.HasErrors() {
if test.Error == "" {
t.Fatalf("unexpected error: %v", diags)
Expand Down
Loading
Loading