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.13/BUG FIXES-20250911-142038.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: BUG FIXES
body: 'variable validation: keep sensitive and ephemeral metadata when evaluating variable conditions.'
time: 2025-09-11T14:20:38.411183+02:00
custom:
Issue: "37595"
35 changes: 34 additions & 1 deletion internal/terraform/context_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6974,7 +6974,7 @@ func TestContext2Plan_variableCustomValidationsCrossRef(t *testing.T) {
}
}

func TestContext2Plan_variableCustomValidationsSensitive(t *testing.T) {
func TestContext2Plan_variableCustomValidationsChildSensitive(t *testing.T) {
m := testModule(t, "validate-variable-custom-validations-child-sensitive")

p := testProvider("test")
Expand All @@ -6993,6 +6993,39 @@ func TestContext2Plan_variableCustomValidationsSensitive(t *testing.T) {
}
}

func TestContext2Plan_variableCustomValidationsSensitive(t *testing.T) {
m := testModule(t, "validate-variable-custom-validations-sensitive")

p := testProvider("test")
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})

_, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, InputValues{
"input": {
Value: cty.StringVal("short"),
},
}))
if len(diags) != 1 {
t.Fatal("wanted exactly one error")
}
if diff := cmp.Diff(diags[0].Description(), tfdiags.Description{
Summary: "Invalid value for variable",
Detail: "too short\n\nThis was checked by the validation rule at testdata/validate-variable-custom-validations-sensitive/validate-variable-custom-validations-sensitive.tf:4,3-13.",
}); len(diff) > 0 {
t.Error(diff)
}

vars := diags[0].FromExpr().EvalContext.Variables["var"].AsValueMap()

_, ms := vars["input"].Unmark()
if _, ok := ms[marks.Sensitive]; !ok {
t.Error("should have been marked as sensitive")
}
}

func TestContext2Plan_nullOutputNoOp(t *testing.T) {
// this should always plan a NoOp change for the output
m := testModuleInline(t, map[string]string{
Expand Down
52 changes: 27 additions & 25 deletions internal/terraform/eval_variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,31 +281,33 @@ func evalVariableValidations(addr addrs.AbsInputVariableInstance, ctx EvalContex
// Although that behavior was accidental, it makes simple validation rules
// more useful and is protected by compatibility promises, and so we'll
// fake it here by overwriting the unknown value that scope.EvalContext
// will have inserted with a possibly-more-known value using the same
// strategy our special code used to use.
ourVal := ctx.NamedValues().GetInputVariableValue(addr)
if ourVal != cty.NilVal {
// (it would be weird for ourVal to be nil here, but we'll tolerate it
// because it was scope.EvalContext's responsibility to check for the
// absent final value, and even if it didn't we'll just get an
// evaluation error when evaluating the expressions below anyway.)

// Our goal here is to make sure that a reference to the variable
// we're checking will evaluate to ourVal, regardless of what else
// scope.EvalContext might have put in the variables table.
if hclCtx.Variables == nil {
hclCtx.Variables = make(map[string]cty.Value)
}
if varsVal, ok := hclCtx.Variables["var"]; ok {
// Unfortunately we need to unpack and repack the object here,
// because cty values are immutable.
attrs := varsVal.AsValueMap()
attrs[addr.Variable.Name] = ourVal
hclCtx.Variables["var"] = cty.ObjectVal(attrs)
} else {
hclCtx.Variables["var"] = cty.ObjectVal(map[string]cty.Value{
addr.Variable.Name: ourVal,
})
// will have inserted during validate walks with a possibly-more-known value
// using the same strategy our special code used to use.
if validateWalk {
ourVal := ctx.NamedValues().GetInputVariableValue(addr)
if ourVal != cty.NilVal {
// (it would be weird for ourVal to be nil here, but we'll tolerate it
// because it was scope.EvalContext's responsibility to check for the
// absent final value, and even if it didn't we'll just get an
// evaluation error when evaluating the expressions below anyway.)

// Our goal here is to make sure that a reference to the variable
// we're checking will evaluate to ourVal, regardless of what else
// scope.EvalContext might have put in the variables table.
if hclCtx.Variables == nil {
hclCtx.Variables = make(map[string]cty.Value)
}
if varsVal, ok := hclCtx.Variables["var"]; ok {
// Unfortunately we need to unpack and repack the object here,
// because cty values are immutable.
attrs := varsVal.AsValueMap()
attrs[addr.Variable.Name] = ourVal
hclCtx.Variables["var"] = cty.ObjectVal(attrs)
} else {
hclCtx.Variables["var"] = cty.ObjectVal(map[string]cty.Value{
addr.Variable.Name: ourVal,
})
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

variable "input" {
type = string
validation {
condition = length(var.input) > 5
error_message = "too short"
}
sensitive = true
}

output "value" {
value = var.input
}