diff --git a/.changes/v1.13/BUG FIXES-20250911-142038.yaml b/.changes/v1.13/BUG FIXES-20250911-142038.yaml new file mode 100644 index 000000000000..4355a4c01788 --- /dev/null +++ b/.changes/v1.13/BUG FIXES-20250911-142038.yaml @@ -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" diff --git a/internal/terraform/context_plan_test.go b/internal/terraform/context_plan_test.go index d5e446771dd5..8dc83b1b5675 100644 --- a/internal/terraform/context_plan_test.go +++ b/internal/terraform/context_plan_test.go @@ -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") @@ -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{ diff --git a/internal/terraform/eval_variable.go b/internal/terraform/eval_variable.go index 6e70cf6e030c..bbdff8661f9f 100644 --- a/internal/terraform/eval_variable.go +++ b/internal/terraform/eval_variable.go @@ -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, + }) + } } } diff --git a/internal/terraform/testdata/validate-variable-custom-validations-sensitive/validate-variable-custom-validations-sensitive.tf b/internal/terraform/testdata/validate-variable-custom-validations-sensitive/validate-variable-custom-validations-sensitive.tf new file mode 100644 index 000000000000..f9767b642b98 --- /dev/null +++ b/internal/terraform/testdata/validate-variable-custom-validations-sensitive/validate-variable-custom-validations-sensitive.tf @@ -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 +}