Skip to content

Commit 32f9fa9

Browse files
authored
Merge pull request #30613 from hashicorp/alisdair/check-rule-error-message-expressions
core: Check rule error message expressions
2 parents 979ac3d + f21d0e8 commit 32f9fa9

16 files changed

+691
-148
lines changed

internal/configs/checks.go

Lines changed: 9 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ package configs
22

33
import (
44
"fmt"
5-
"unicode"
65

76
"github.com/hashicorp/hcl/v2"
8-
"github.com/hashicorp/hcl/v2/gohcl"
97
"github.com/hashicorp/terraform/internal/addrs"
108
"github.com/hashicorp/terraform/internal/lang"
119
)
@@ -24,12 +22,15 @@ type CheckRule struct {
2422
// input variables can only refer to the variable that is being validated.
2523
Condition hcl.Expression
2624

27-
// ErrorMessage is one or more full sentences, which would need to be in
25+
// ErrorMessage should be one or more full sentences, which should be in
2826
// English for consistency with the rest of the error message output but
29-
// can in practice be in any language as long as it ends with a period.
30-
// The message should describe what is required for the condition to return
31-
// true in a way that would make sense to a caller of the module.
32-
ErrorMessage string
27+
// can in practice be in any language. The message should describe what is
28+
// required for the condition to return true in a way that would make sense
29+
// to a caller of the module.
30+
//
31+
// The error message expression has the same variables available for
32+
// interpolation as the corresponding condition.
33+
ErrorMessage hcl.Expression
3334

3435
DeclRange hcl.Range
3536
}
@@ -111,77 +112,12 @@ func decodeCheckRuleBlock(block *hcl.Block, override bool) (*CheckRule, hcl.Diag
111112
}
112113

113114
if attr, exists := content.Attributes["error_message"]; exists {
114-
moreDiags := gohcl.DecodeExpression(attr.Expr, nil, &cr.ErrorMessage)
115-
diags = append(diags, moreDiags...)
116-
if !moreDiags.HasErrors() {
117-
const errSummary = "Invalid validation error message"
118-
switch {
119-
case cr.ErrorMessage == "":
120-
diags = diags.Append(&hcl.Diagnostic{
121-
Severity: hcl.DiagError,
122-
Summary: errSummary,
123-
Detail: "An empty string is not a valid nor useful error message.",
124-
Subject: attr.Expr.Range().Ptr(),
125-
})
126-
case !looksLikeSentences(cr.ErrorMessage):
127-
// Because we're going to include this string verbatim as part
128-
// of a bigger error message written in our usual style in
129-
// English, we'll require the given error message to conform
130-
// to that. We might relax this in future if e.g. we start
131-
// presenting these error messages in a different way, or if
132-
// Terraform starts supporting producing error messages in
133-
// other human languages, etc.
134-
// For pragmatism we also allow sentences ending with
135-
// exclamation points, but we don't mention it explicitly here
136-
// because that's not really consistent with the Terraform UI
137-
// writing style.
138-
diags = diags.Append(&hcl.Diagnostic{
139-
Severity: hcl.DiagError,
140-
Summary: errSummary,
141-
Detail: "The validation error message must be at least one full sentence starting with an uppercase letter and ending with a period or question mark.\n\nYour given message will be included as part of a larger Terraform error message, written as English prose. For broadly-shared modules we suggest using a similar writing style so that the overall result will be consistent.",
142-
Subject: attr.Expr.Range().Ptr(),
143-
})
144-
}
145-
}
115+
cr.ErrorMessage = attr.Expr
146116
}
147117

148118
return cr, diags
149119
}
150120

151-
// looksLikeSentence is a simple heuristic that encourages writing error
152-
// messages that will be presentable when included as part of a larger
153-
// Terraform error diagnostic whose other text is written in the Terraform
154-
// UI writing style.
155-
//
156-
// This is intentionally not a very strong validation since we're assuming
157-
// that module authors want to write good messages and might just need a nudge
158-
// about Terraform's specific style, rather than that they are going to try
159-
// to work around these rules to write a lower-quality message.
160-
func looksLikeSentences(s string) bool {
161-
if len(s) < 1 {
162-
return false
163-
}
164-
runes := []rune(s) // HCL guarantees that all strings are valid UTF-8
165-
first := runes[0]
166-
last := runes[len(runes)-1]
167-
168-
// If the first rune is a letter then it must be an uppercase letter.
169-
// (This will only see the first rune in a multi-rune combining sequence,
170-
// but the first rune is generally the letter if any are, and if not then
171-
// we'll just ignore it because we're primarily expecting English messages
172-
// right now anyway, for consistency with all of Terraform's other output.)
173-
if unicode.IsLetter(first) && !unicode.IsUpper(first) {
174-
return false
175-
}
176-
177-
// The string must be at least one full sentence, which implies having
178-
// sentence-ending punctuation.
179-
// (This assumes that if a sentence ends with quotes then the period
180-
// will be outside the quotes, which is consistent with Terraform's UI
181-
// writing style.)
182-
return last == '.' || last == '?' || last == '!'
183-
}
184-
185121
var checkRuleBlockSchema = &hcl.BodySchema{
186122
Attributes: []hcl.AttributeSchema{
187123
{

internal/configs/named_values.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,31 @@ func decodeVariableValidationBlock(varName string, block *hcl.Block, override bo
345345
}
346346
}
347347

348+
if vv.ErrorMessage != nil {
349+
// The same applies to the validation error message, except that
350+
// references are not required. A string literal is a valid error
351+
// message.
352+
goodRefs := 0
353+
for _, traversal := range vv.ErrorMessage.Variables() {
354+
ref, moreDiags := addrs.ParseRef(traversal)
355+
if !moreDiags.HasErrors() {
356+
if addr, ok := ref.Subject.(addrs.InputVariable); ok {
357+
if addr.Name == varName {
358+
goodRefs++
359+
continue // Reference is valid
360+
}
361+
}
362+
}
363+
// If we fall out here then the reference is invalid.
364+
diags = diags.Append(&hcl.Diagnostic{
365+
Severity: hcl.DiagError,
366+
Summary: "Invalid reference in variable validation",
367+
Detail: fmt.Sprintf("The error message for variable %q can only refer to the variable itself, using var.%s.", varName, varName),
368+
Subject: traversal.SourceRange().Ptr(),
369+
})
370+
}
371+
}
372+
348373
return vv, diags
349374
}
350375

internal/configs/named_values_test.go

Lines changed: 0 additions & 33 deletions
This file was deleted.

internal/configs/testdata/invalid-files/variable-validation-bad-msg.tf

Lines changed: 0 additions & 6 deletions
This file was deleted.

internal/configs/testdata/invalid-files/variable-validation-condition-badref.tf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,10 @@ variable "validation" {
99
error_message = "Must be five."
1010
}
1111
}
12+
13+
variable "validation_error_expression" {
14+
validation {
15+
condition = var.validation_error_expression != 1
16+
error_message = "Cannot equal ${local.foo}." # ERROR: Invalid reference in variable validation
17+
}
18+
}

internal/configs/testdata/valid-files/variable_validation.tf

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,19 @@ variable "validation" {
44
error_message = "Must be five."
55
}
66
}
7+
8+
variable "validation_function" {
9+
type = list(string)
10+
validation {
11+
condition = length(var.validation_function) > 0
12+
error_message = "Must not be empty."
13+
}
14+
}
15+
16+
variable "validation_error_expression" {
17+
type = list(string)
18+
validation {
19+
condition = length(var.validation_error_expression) < 10
20+
error_message = "Too long (${length(var.validation_error_expression)} is greater than 10)."
21+
}
22+
}

internal/terraform/context_plan2_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2561,7 +2561,7 @@ func TestContext2Plan_preconditionErrors(t *testing.T) {
25612561
{
25622562
"test_resource.c.value",
25632563
"Invalid condition result",
2564-
"Invalid validation condition result value: a bool is required",
2564+
"Invalid condition result value: a bool is required",
25652565
},
25662566
}
25672567

0 commit comments

Comments
 (0)