diff --git a/internal/stacks/stackruntime/internal/stackeval/input_variable.go b/internal/stacks/stackruntime/internal/stackeval/input_variable.go index ebcdc5037f..48a883f71c 100644 --- a/internal/stacks/stackruntime/internal/stackeval/input_variable.go +++ b/internal/stacks/stackruntime/internal/stackeval/input_variable.go @@ -477,12 +477,8 @@ func evalVariableValidation(validation *stackconfig.CheckRule, hclCtx *hcl.EvalC const errInvalidValue = "Invalid value for variable" var diags tfdiags.Diagnostics - // Evaluate both condition and error message up front. The error message is - // always inspected for structural problems (sensitive/ephemeral marks), not only - // when the condition fails. result, moreDiags := validation.Condition.Value(hclCtx) diags = diags.Append(moreDiags) - errorValue, errorDiags := validation.ErrorMessage.Value(hclCtx) if moreDiags.HasErrors() { // If we couldn't evaluate the condition at all (syntax error, etc.), @@ -529,27 +525,24 @@ func evalVariableValidation(validation *stackconfig.CheckRule, hclCtx *hcl.EvalC // The marks don't affect the validation result, only how we handle the error message. result, _ = result.Unmark() - // Always process and validate the error_message expression, even when the condition - // passes — an invalid error message (sensitive, ephemeral, non-string, etc.) should - // be flagged regardless of whether the check succeeds or fails. + // Always evaluate the error_message expression, even when the condition passes — + // unknown, sensitive, or ephemeral values in the message are structural problems + // regardless of whether the check succeeds or fails. + errorValue, errorDiags := validation.ErrorMessage.Value(hclCtx) diags = diags.Append(errorDiags) var errorMessage string if !errorDiags.HasErrors() { if !errorValue.IsKnown() { - if !result.True() { - // An unknown error message is only a problem when the condition actually - // fails, since we need to display it to the user. - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid error message", - Detail: "Unsuitable value for error message: expression refers to values that won't be known until the apply phase.", - Subject: validation.ErrorMessage.Range().Ptr(), - Expression: validation.ErrorMessage, - EvalContext: hclCtx, - }) - return diags - } + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid error message", + Detail: "Unsuitable value for error message: expression refers to values that won't be known until the apply phase.", + Subject: validation.ErrorMessage.Range().Ptr(), + Expression: validation.ErrorMessage, + EvalContext: hclCtx, + }) + return diags } else if !errorValue.IsNull() { errorValue, err = convert.Convert(errorValue, cty.String) if err != nil { diff --git a/internal/stacks/stackruntime/internal/stackeval/input_variable_test.go b/internal/stacks/stackruntime/internal/stackeval/input_variable_test.go index 2288c62b24..e8c3f27457 100644 --- a/internal/stacks/stackruntime/internal/stackeval/input_variable_test.go +++ b/internal/stacks/stackruntime/internal/stackeval/input_variable_test.go @@ -665,9 +665,12 @@ func TestEvalVariableValidation(t *testing.T) { // --- Unknown error message --- - t.Run("error message unknown, condition fails → Invalid error message (no failure diag)", func(t *testing.T) { - // We must flag the unknown error message but must NOT also emit - // "Invalid value for variable" because we return early. + t.Run("error message unknown, condition fails → Invalid error message only", func(t *testing.T) { + // An unknown error_message is always a structural problem: the validation + // block is invalid regardless of whether the condition passes or fails, + // because Terraform can never safely display the message. + // We return early on the unknown message, so "Invalid value for variable" + // must NOT also be emitted. rule := makeFakeRule( hcltest.MockExprLiteral(cty.False), hcltest.MockExprLiteral(cty.UnknownVal(cty.String)), @@ -684,16 +687,26 @@ func TestEvalVariableValidation(t *testing.T) { } }) - t.Run("error message unknown, condition passes → no diagnostic about error message", func(t *testing.T) { - // Unknown error message is only a problem when the condition actually - // fails (because we'd need to display it). When the condition passes - // the unknown error message must be silently ignored. + t.Run("error message unknown, condition passes → Invalid error message only", func(t *testing.T) { + // An unknown error_message is always a structural problem: the validation + // block is invalid regardless of whether the condition passes or fails, + // because Terraform can never safely display the message. + // We return early on the unknown message, so "Invalid value for variable" + // must NOT be emitted even though the condition passed. rule := makeFakeRule( hcltest.MockExprLiteral(cty.True), hcltest.MockExprLiteral(cty.UnknownVal(cty.String)), ) diags := evalVariableValidation(rule, makeVarCtx(cty.StringVal("good")), valueRange) - assertNoDiags(t, diags) + assertMatchingDiag(t, diags, func(d tfdiags.Diagnostic) bool { + return d.Severity() == tfdiags.Error && + d.Description().Summary == "Invalid error message" + }) + for _, d := range diags { + if d.Description().Summary == "Invalid value for variable" { + t.Errorf("unexpected 'Invalid value for variable' when error message is unknown") + } + } }) // --- Sensitive variable value in condition expression ---