Merge branch 'main' into provider-defined-function-during-init

pull/38497/head
Austin Valle 3 weeks ago
commit b8bdde09ca

@ -0,0 +1,5 @@
kind: BUG FIXES
body: Fix non-const variable checks on `init`
time: 2026-04-29T18:35:07.99622+02:00
custom:
Issue: "38470"

@ -0,0 +1,5 @@
kind: BUG FIXES
body: Fix panic for types modules with no expanded instances
time: 2026-04-30T10:38:20.648469-04:00
custom:
Issue: "38491"

@ -0,0 +1,5 @@
kind: BUG FIXES
body: Avoid warnings in 'terraform output -raw'
time: 2026-04-30T12:14:33.373975+02:00
custom:
Issue: "38487"

@ -0,0 +1,5 @@
kind: BUG FIXES
body: Ignore undeclared variable values from the cloud backend
time: 2026-04-30T15:42:41.89732+02:00
custom:
Issue: "38490"

@ -54,7 +54,7 @@ jobs:
.changie.yaml
.changes/
sparse-checkout-cone-mode: false
ref: ${{ github.ref }} # Ref refers to the target branch of this PR
ref: ${{ github.base_ref }} # Base ref refers to the target branch of this PR
- name: "Check for changelog entry"
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0

@ -59,6 +59,11 @@ func ParseUndeclaredVariableValues(vv map[string]arguments.UnparsedVariableValue
// variables, because users will often set these globally
// when they are used across many (but not necessarily all)
// configurations.
case terraform.ValueFromCloud:
// We allow and ignore undeclared names fetched from the cloud
// backend, because users will often set these globally or via
// varsets when they are used across many (but not necessarily all)
// workspaces.
case terraform.ValueFromCLIArg:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,

@ -303,6 +303,6 @@ func (v *remoteStoredVariableValue) ParseVariableValue(mode configs.VariablePars
// roughly speaking, a similar idea to entering variable values at
// the interactive CLI prompts. It's not a perfect correspondance,
// but it's closer than the other options.
SourceType: terraform.ValueFromInput,
SourceType: terraform.ValueFromCloud,
}, diags
}

@ -169,7 +169,8 @@ func TestRemoteContextWithVars(t *testing.T) {
&tfe.VariableCreateOptions{
Category: &catTerraform,
},
`Value for undeclared variable: A variable named "key" was assigned a value, but the root module does not declare a variable of that name. To use this value, add a "variable" block to the configuration.`,
// We don't expect an error for values of undeclared variables
``,
},
"environment variable": {
&tfe.VariableCreateOptions{
@ -279,7 +280,7 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) {
terraform.InputValues{
varName1: &terraform.InputValue{
Value: cty.StringVal(varValue1),
SourceType: terraform.ValueFromInput,
SourceType: terraform.ValueFromCloud,
SourceRange: tfdiags.SourceRange{
Filename: "",
Start: tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
@ -288,7 +289,7 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) {
},
varName2: &terraform.InputValue{
Value: cty.StringVal(varValue2),
SourceType: terraform.ValueFromInput,
SourceType: terraform.ValueFromCloud,
SourceRange: tfdiags.SourceRange{
Filename: "",
Start: tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
@ -297,7 +298,7 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) {
},
varName3: &terraform.InputValue{
Value: cty.StringVal(varValue3),
SourceType: terraform.ValueFromInput,
SourceType: terraform.ValueFromCloud,
SourceRange: tfdiags.SourceRange{
Filename: "",
Start: tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
@ -328,7 +329,7 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) {
terraform.InputValues{
varName1: &terraform.InputValue{
Value: cty.StringVal(varValue1),
SourceType: terraform.ValueFromInput,
SourceType: terraform.ValueFromCloud,
SourceRange: tfdiags.SourceRange{
Filename: "",
Start: tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
@ -337,7 +338,7 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) {
},
varName2: &terraform.InputValue{
Value: cty.StringVal(varValue2),
SourceType: terraform.ValueFromInput,
SourceType: terraform.ValueFromCloud,
SourceRange: tfdiags.SourceRange{
Filename: "",
Start: tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
@ -373,7 +374,7 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) {
terraform.InputValues{
varName1: &terraform.InputValue{
Value: cty.StringVal(varValue1),
SourceType: terraform.ValueFromInput,
SourceType: terraform.ValueFromCloud,
SourceRange: tfdiags.SourceRange{
Filename: "",
Start: tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},
@ -382,7 +383,7 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) {
},
varName2: &terraform.InputValue{
Value: cty.StringVal(varValue2),
SourceType: terraform.ValueFromInput,
SourceType: terraform.ValueFromCloud,
SourceRange: tfdiags.SourceRange{
Filename: "",
Start: tfdiags.SourcePos{Line: 0, Column: 0, Byte: 0},

@ -13,9 +13,13 @@ import (
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend"
backendInit "github.com/hashicorp/terraform/internal/backend/init"
"github.com/hashicorp/terraform/internal/backend/remote-state/inmem"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/states/statefile"
"github.com/hashicorp/terraform/internal/tfdiags"
)
func TestOutput(t *testing.T) {
@ -381,3 +385,81 @@ func TestOutput_stateDefault(t *testing.T) {
t.Fatalf("bad: %#v", actual)
}
}
// deprecatedInmemBackend wraps the inmem backend and injects a deprecation
// warning from PrepareConfig, simulating a backend with deprecated attributes
// (like the S3 backend's dynamodb_table).
type deprecatedInmemBackend struct {
backend.Backend
}
func (b *deprecatedInmemBackend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) {
newObj, diags := b.Backend.PrepareConfig(obj)
diags = diags.Append(tfdiags.SimpleWarning(`The attribute "deprecated_attr" is deprecated.`))
return newObj, diags
}
func TestOutputRaw_warningsSuppressed(t *testing.T) {
// Pre-populate the inmem backend with a state containing an output value
inmem.Reset()
originalState := states.BuildState(func(s *states.SyncState) {
s.SetOutputValue(
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
cty.StringVal("bar"),
false,
)
})
// Register a backend that wraps inmem with a deprecation warning,
// simulating a backend like S3 whose PrepareConfig warns about
// deprecated attributes (e.g. dynamodb_table).
backendInit.Set("inmem", func() backend.Backend {
return &deprecatedInmemBackend{Backend: inmem.New()}
})
defer backendInit.Set("inmem", inmem.New)
td := t.TempDir()
testCopyDir(t, testFixturePath("output-backend-with-deprecation"), td)
t.Chdir(td)
// Write the state into the inmem backend's default workspace
b := inmem.New()
b.Configure(cty.ObjectVal(map[string]cty.Value{
"lock_id": cty.NullVal(cty.String),
}))
sMgr, sDiags := b.StateMgr(backend.DefaultStateName)
if sDiags.HasErrors() {
t.Fatalf("unexpected error: %s", sDiags.Err())
}
sMgr.WriteState(originalState)
if err := sMgr.PersistState(nil); err != nil {
t.Fatalf("unexpected error: %s", err)
}
view, done := testView(t)
c := &OutputCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
View: view,
},
}
args := []string{"-raw", "foo"}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatalf("unexpected exit code %d\nstderr:\n%s", code, output.Stderr())
}
// The key assertion: warnings must not appear in raw output
// as they would be indistinguishable from the value.
stderr := output.Stderr()
if strings.Contains(stderr, "deprecated") {
t.Fatalf("warnings should be suppressed, got:\n%s", stderr)
}
actual := strings.TrimSpace(output.Stdout())
if actual != `bar` {
t.Fatalf("expected output \"bar\", got: %#v", actual)
}
}

@ -0,0 +1,12 @@
{
"version": 3,
"serial": 0,
"lineage": "test-output-deprecation",
"backend": {
"type": "inmem",
"config": {
"lock_id": null
},
"hash": 3947750061
}
}

@ -0,0 +1,7 @@
terraform {
backend "inmem" {}
}
output "foo" {
value = "bar"
}

@ -176,7 +176,11 @@ func (v *OutputRaw) Output(name string, outputs map[string]*states.OutputValue)
}
func (v *OutputRaw) Diagnostics(diags tfdiags.Diagnostics) {
v.view.Diagnostics(diags)
// filter out warnings as these wouldn't be expected in raw mode
// as they typically don't influence exit code so user cannot
// expect them in stdout
errsOnly := diags.ErrorsOnly()
v.view.Diagnostics(errsOnly)
}
// The OutputJSON implementation renders outputs as JSON values. When rendering

@ -697,22 +697,35 @@ module "example" {
"non-const variable validation does not run during init": {
module: map[string]string{
"main.tf": `
variable "some" {
type = string
}
variable "name" {
type = string
default = "bad"
validation {
condition = var.name != "bad"
condition = var.name != var.some
error_message = "must not be bad"
}
}
module "example" {
source = "./modules/fixed"
source = "./modules/example"
name = var.name
}
`,
},
mockedLoadModuleCalls: map[string]map[string]string{
"./modules/example": {
"main.tf": `
variable "name" {
type = string
}
`},
},
expectLoadModuleCalls: []*configs.ModuleRequest{{
SourceAddr: mustModuleSource(t, "./modules/fixed"),
SourceAddr: mustModuleSource(t, "./modules/example"),
}},
},
} {

@ -302,7 +302,7 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd
// that are disabled, etc. Terraform's static validation leans towards
// being liberal in what it accepts because the subsequent plan walk has
// more information available and so can be more conservative.
if d.Operation == walkValidate {
if d.Operation == walkValidate || (d.Operation == walkInit && !config.Const) {
// We should still capture the statically-configured marks during
// the validate walk.
ret := cty.UnknownVal(config.Type)
@ -438,23 +438,25 @@ func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.Sourc
noDynamicTypes = noDynamicTypes && !out.ConstraintType.HasDynamicTypes()
}
if d.Operation == walkValidate && typeDefined {
atys := make(map[string]cty.Type, len(outputConfigs))
as := make(map[string]cty.Value, len(outputConfigs))
for name, c := range outputConfigs {
// atys is used to create the module object type for expanded modules
atys[name] = c.ConstraintType
// the unknown val can be used when we return a single module
// instance with unknown outputs
val := cty.UnknownVal(c.ConstraintType)
if c.DeprecatedSet {
val = val.Mark(marks.NewDeprecation(c.Deprecated, absAddr.Output(name).ConfigOutputValue().ForDisplay()))
}
as[name] = val
// build up the type of the configured module output object
atys := make(map[string]cty.Type, len(outputConfigs))
// and create a single unknown instance value for validation
as := make(map[string]cty.Value, len(outputConfigs))
for name, c := range outputConfigs {
// atys is used to create the module object type for expanded modules
atys[name] = c.ConstraintType
// the unknown val can be used when we return a single module
// instance with unknown outputs
val := cty.UnknownVal(c.ConstraintType)
if c.DeprecatedSet {
val = val.Mark(marks.NewDeprecation(c.Deprecated, absAddr.Output(name).ConfigOutputValue().ForDisplay()))
}
instTy := cty.Object(atys)
as[name] = val
}
instTy := cty.Object(atys)
if d.Operation == walkValidate && typeDefined {
switch {
case callConfig.Count != nil && noDynamicTypes:
return cty.UnknownVal(cty.List(instTy)), diags
@ -580,9 +582,12 @@ func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.Sourc
elems = append(elems, instVal)
diags = diags.Append(moreDiags)
}
if noDynamicTypes {
switch {
case noDynamicTypes && len(elems) == 0:
return cty.ListValEmpty(instTy), diags
case noDynamicTypes:
return cty.ListVal(elems), diags
} else {
default:
return cty.TupleVal(elems), diags
}
@ -593,9 +598,13 @@ func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.Sourc
attrs[string(instKey.(addrs.StringKey))] = instVal
diags = diags.Append(moreDiags)
}
if noDynamicTypes {
switch {
case noDynamicTypes && len(attrs) == 0:
return cty.MapValEmpty(instTy), diags
case noDynamicTypes:
return cty.MapVal(attrs), diags
} else {
default:
return cty.ObjectVal(attrs), diags
}

@ -612,6 +612,14 @@ func TestEvaluatorGetModule_validateTypedOutputs(t *testing.T) {
"out": cty.String,
}))),
},
"empty_count": {
configureModuleCall: func(call *configs.ModuleCall) {
call.Count = hcltest.MockExprLiteral(cty.NumberIntVal(0))
},
want: cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
"out": cty.String,
}))),
},
"for_each": {
configureModuleCall: func(call *configs.ModuleCall) {
call.ForEach = hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
@ -729,6 +737,15 @@ func TestEvaluatorGetModule_planTypedOutputs(t *testing.T) {
cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("second").Mark(marks.Sensitive)}),
}),
},
"empty_count": {
setupInstances: func(expander *instances.Expander) {
expander.SetModuleCount(addrs.RootModuleInstance, addrs.ModuleCall{Name: "mod"}, 0)
},
setupOutputs: func(namedValues *namedvals.State) {
// explicitly setting no values
},
want: cty.ListValEmpty(cty.Object(map[string]cty.Type{"out": cty.String})),
},
"for_each": {
setupInstances: func(expander *instances.Expander) {
expander.SetModuleForEach(addrs.RootModuleInstance, addrs.ModuleCall{Name: "mod"}, map[string]cty.Value{
@ -755,6 +772,15 @@ func TestEvaluatorGetModule_planTypedOutputs(t *testing.T) {
"b": cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("second").Mark(marks.Sensitive)}),
}),
},
"empty_for_each": {
setupInstances: func(expander *instances.Expander) {
expander.SetModuleForEach(addrs.RootModuleInstance, addrs.ModuleCall{Name: "mod"}, map[string]cty.Value{})
},
setupOutputs: func(namedValues *namedvals.State) {
// no values for empty for_each
},
want: cty.MapValEmpty(cty.Object(map[string]cty.Type{"out": cty.String})),
},
}
for name, test := range tests {

@ -17,6 +17,7 @@ func _() {
_ = x[ValueFromInput-73]
_ = x[ValueFromPlan-80]
_ = x[ValueFromCaller-83]
_ = x[ValueFromCloud-84]
}
const (
@ -27,11 +28,12 @@ const (
_ValueSourceType_name_4 = "ValueFromInput"
_ValueSourceType_name_5 = "ValueFromNamedFile"
_ValueSourceType_name_6 = "ValueFromPlan"
_ValueSourceType_name_7 = "ValueFromCaller"
_ValueSourceType_name_7 = "ValueFromCallerValueFromCloud"
)
var (
_ValueSourceType_index_3 = [...]uint8{0, 15, 32}
_ValueSourceType_index_7 = [...]uint8{0, 15, 29}
)
func (i ValueSourceType) String() string {
@ -51,8 +53,9 @@ func (i ValueSourceType) String() string {
return _ValueSourceType_name_5
case i == 80:
return _ValueSourceType_name_6
case i == 83:
return _ValueSourceType_name_7
case 83 <= i && i <= 84:
i -= 83
return _ValueSourceType_name_7[_ValueSourceType_index_7[i]:_ValueSourceType_index_7[i+1]]
default:
return "ValueSourceType(" + strconv.FormatInt(int64(i), 10) + ")"
}

@ -105,6 +105,10 @@ const (
// ValueFromCaller indicates that the value was explicitly overridden by
// a caller to Context.SetVariable after the context was constructed.
ValueFromCaller ValueSourceType = 'S'
// ValueFromCloud indicates that the value was retrieved from a remote source,
// such as HCP Terraform or Terraform Enterprise.
ValueFromCloud ValueSourceType = 'T'
)
func (v *InputValue) GoString() string {
@ -153,6 +157,8 @@ func (v ValueSourceType) DiagnosticLabel() string {
return "set by an interactive input"
case ValueFromPlan:
return "set by the plan"
case ValueFromCloud:
return "set by the cloud backend"
default:
return "unknown"
}

@ -193,6 +193,17 @@ func (diags Diagnostics) Warnings() Diagnostics {
return warns
}
// ErrorsOnly returns a Diagnostics list containing only diagnostics with a severity of Error.
func (diags Diagnostics) ErrorsOnly() Diagnostics {
var errorsOnly = Diagnostics{}
for _, diag := range diags {
if diag.Severity() == Error {
errorsOnly = append(errorsOnly, diag)
}
}
return errorsOnly
}
// HasErrors returns true if any of the diagnostics in the list have
// a severity of Error.
func (diags Diagnostics) HasErrors() bool {

Loading…
Cancel
Save