Skip to content
Merged
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.15/ENHANCEMENTS-20260313-162537.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: ENHANCEMENTS
body: As part of supporting variables in module sources, most commands now accept variable values
time: 2026-03-13T16:25:37.792809+01:00
custom:
Issue: "38276"
23 changes: 23 additions & 0 deletions internal/backend/backendrun/const_variables.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package backendrun

import (
"context"

"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/tfdiags"
)

// ConstVariableSupplier is an optional interface that backends can implement
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to make sure to document this properly, could be confusing for people on the remote backend if their backend does not load the vars.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good point. I'll look for a good place to document this

// to supply variable values from their remote storage. This is used to fetch
// const variable values that are needed during early configuration loading
// (e.g., for module source resolution), before a full operation is started.
type ConstVariableSupplier interface {
// FetchVariables retrieves Terraform variable values stored in the
// backend for the given workspace. Only variables that are relevant to
// Terraform (as opposed to environment variables or other categories)
// should be returned.
FetchVariables(ctx context.Context, workspace string) (map[string]arguments.UnparsedVariableValue, tfdiags.Diagnostics)
}
36 changes: 29 additions & 7 deletions internal/backend/backendrun/unparsed_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,20 @@ func isDefinedAny(name string, maps ...terraform.InputValues) bool {
// InputValues may be incomplete but will include the subset of variables
// that were successfully processed, allowing for careful analysis of the
// partial result.
//
// constOnly will only raise a diagnostic error if a required variable is
// missing and is marked as const. Since configuration loading will always
// require values for constant variables, this allows us to use this
// function in both configuration loading and plan/apply contexts where all
// variables are required.
func ParseVariableValues(vv map[string]arguments.UnparsedVariableValue, decls map[string]*configs.Variable, constOnly bool) (terraform.InputValues, tfdiags.Diagnostics) {
func ParseVariableValues(vv map[string]arguments.UnparsedVariableValue, decls map[string]*configs.Variable) (terraform.InputValues, tfdiags.Diagnostics) {
return parseVariableValues(vv, decls, false)
}

// ParseConstVariableValues is like ParseVariableValues but only produces
// errors for missing const variables. Non-const required variables that are
// missing will still receive placeholder values but won't produce errors.
// This is used during early configuration loading (e.g. module installation)
// where only const variables are needed for module source resolution.
func ParseConstVariableValues(vv map[string]arguments.UnparsedVariableValue, decls map[string]*configs.Variable) (terraform.InputValues, tfdiags.Diagnostics) {
return parseVariableValues(vv, decls, true)
}

func parseVariableValues(vv map[string]arguments.UnparsedVariableValue, decls map[string]*configs.Variable, constOnly bool) (terraform.InputValues, tfdiags.Diagnostics) {
ret, diags := ParseDeclaredVariableValues(vv, decls)
undeclared, diagsUndeclared := ParseUndeclaredVariableValues(vv, decls)

Expand Down Expand Up @@ -212,3 +219,18 @@ func ParseVariableValues(vv map[string]arguments.UnparsedVariableValue, decls ma

return ret, diags
}

// HasUnsatisfiedConstVariables checks whether any const variables declared in
// the given module are required but not yet present in the provided variable
// values map. This is used to determine whether we need to fetch additional
// variable values from a backend before loading the full configuration.
func HasUnsatisfiedConstVariables(vv map[string]arguments.UnparsedVariableValue, decls map[string]*configs.Variable) bool {
for name, vc := range decls {
if vc.Const && vc.Required() {
if _, defined := vv[name]; !defined {
return true
}
}
}
return false
}
78 changes: 76 additions & 2 deletions internal/backend/backendrun/unparsed_value_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ func TestUnparsedValue(t *testing.T) {
})

t.Run("ParseVariableValues", func(t *testing.T) {
gotVals, diags := ParseVariableValues(vv, decls, false)
gotVals, diags := ParseVariableValues(vv, decls)
for _, diag := range diags {
t.Logf("%s: %s", diag.Description().Summary, diag.Description().Detail)
}
Expand Down Expand Up @@ -278,7 +278,7 @@ func TestUnparsedValue(t *testing.T) {
},
}

gotVals, diags := ParseVariableValues(vv, decls, true)
gotVals, diags := ParseConstVariableValues(vv, decls)

if got, want := len(diags), 1; got != want {
t.Fatalf("wrong number of diagnostics %d; want %d", got, want)
Expand Down Expand Up @@ -339,6 +339,80 @@ func TestUnparsedValue(t *testing.T) {
})
}

func TestHasUnsatisfiedConstVariables(t *testing.T) {
testCases := map[string]struct {
vv map[string]arguments.UnparsedVariableValue
decls map[string]*configs.Variable
want bool
}{
"no variables": {
vv: nil,
decls: map[string]*configs.Variable{},
want: false,
},
"no const variables": {
vv: nil,
decls: map[string]*configs.Variable{
"regular": {
Name: "regular",
},
},
want: false,
},
"const with default": {
vv: nil,
decls: map[string]*configs.Variable{
"has_default": {
Name: "has_default",
Const: true,
Default: cty.StringVal("default"),
},
},
want: false,
},
"const required and missing": {
vv: nil,
decls: map[string]*configs.Variable{
"required_const": {
Name: "required_const",
Const: true,
},
},
want: true,
},
"const required but provided": {
vv: map[string]arguments.UnparsedVariableValue{
"required_const": testUnparsedVariableValue("value"),
},
decls: map[string]*configs.Variable{
"required_const": {
Name: "required_const",
Const: true,
},
},
want: false,
},
"non-const required and missing": {
vv: nil,
decls: map[string]*configs.Variable{
"regular_required": {
Name: "regular_required",
},
},
want: false,
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got := HasUnsatisfiedConstVariables(tc.vv, tc.decls)
if got != tc.want {
t.Errorf("got %v, want %v", got, tc.want)
}
})
}
}

type testUnparsedVariableValue string

func (v testUnparsedVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
Expand Down
2 changes: 1 addition & 1 deletion internal/backend/local/backend_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ func (b *Local) opApply(
// same parsing logic from the plan to generate the diagnostics.
undeclaredVariables := map[string]arguments.UnparsedVariableValue{}

parsedVars, _ := backendrun.ParseVariableValues(op.Variables, lr.Config.Module.Variables, false)
parsedVars, _ := backendrun.ParseVariableValues(op.Variables, lr.Config.Module.Variables)

for varName := range op.Variables {
parsedVar, parsed := parsedVars[varName]
Expand Down
10 changes: 5 additions & 5 deletions internal/backend/local/backend_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func (b *Local) localRunDirect(op *backendrun.Operation, run *backendrun.LocalRu
rawVariables = b.interactiveCollectVariables(context.TODO(), op.Variables, rootMod.Variables, op.UIIn)
}

variables, varDiags := backendrun.ParseVariableValues(rawVariables, rootMod.Variables, false)
variables, varDiags := backendrun.ParseVariableValues(rawVariables, rootMod.Variables)
diags = diags.Append(varDiags)
if diags.HasErrors() {
return nil, nil, diags
Expand Down Expand Up @@ -271,7 +271,7 @@ func (b *Local) localRunForPlanFile(op *backendrun.Operation, pf *planfile.Reade
return nil, nil, diags
}

variables, varDiags := backendrun.ParseVariableValues(op.Variables, rootMod.Variables, false)
variables, varDiags := backendrun.ParseVariableValues(op.Variables, rootMod.Variables)
diags = diags.Append(varDiags)
if diags.HasErrors() {
return nil, nil, diags
Expand Down Expand Up @@ -496,8 +496,8 @@ func (b *Local) interactiveCollectVariables(ctx context.Context, existing map[st
func (b *Local) stubUnsetRequiredVariables(existing map[string]arguments.UnparsedVariableValue, vcs map[string]*configs.Variable) map[string]arguments.UnparsedVariableValue {
var missing bool // Do we need to add anything?
for name, vc := range vcs {
if !vc.Required() {
continue // We only stub required variables
if !vc.Required() || vc.Const {
continue // We only stub non-const required variables
}
if _, exists := existing[name]; !exists {
missing = true
Expand All @@ -512,7 +512,7 @@ func (b *Local) stubUnsetRequiredVariables(existing map[string]arguments.Unparse
maps.Copy(ret, existing) // don't use clone here, so we can return a non-nil map

for name, vc := range vcs {
if !vc.Required() {
if !vc.Required() || vc.Const {
continue
}
if _, exists := existing[name]; !exists {
Expand Down
2 changes: 1 addition & 1 deletion internal/backend/remote/backend_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ func (b *Remote) hasExplicitVariableValues(op *backendrun.Operation) bool {
// goal here is just to make a best effort count of how many variable
// values are coming from -var or -var-file CLI arguments so that we can
// hint the user that those are not supported for remote operations.
variables, _ := backendrun.ParseVariableValues(op.Variables, config.Variables, false)
variables, _ := backendrun.ParseVariableValues(op.Variables, config.Variables)

// Check for explicitly-defined (-var and -var-file) variables, which the
// remote backend does not support. All other source types are okay,
Expand Down
2 changes: 1 addition & 1 deletion internal/backend/remote/backend_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func (b *Remote) LocalRun(op *backendrun.Operation) (*backendrun.LocalRun, state
}

if op.Variables != nil {
variables, varDiags := backendrun.ParseVariableValues(op.Variables, rootMod.Variables, false)
variables, varDiags := backendrun.ParseVariableValues(op.Variables, rootMod.Variables)
diags = diags.Append(varDiags)
if diags.HasErrors() {
return nil, nil, diags
Expand Down
1 change: 1 addition & 0 deletions internal/cloud/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ type Cloud struct {
var _ backend.Backend = (*Cloud)(nil)
var _ backendrun.OperationsBackend = (*Cloud)(nil)
var _ backendrun.Local = (*Cloud)(nil)
var _ backendrun.ConstVariableSupplier = (*Cloud)(nil)

// New creates a new initialized cloud backend.
func New(services *disco.Disco) *Cloud {
Expand Down
Loading
Loading