Merge pull request #30613 from hashicorp/alisdair/check-rule-error-message-expressions
core: Check rule error message expressions
This commit is contained in:
commit
32f9fa9aac
|
@ -2,10 +2,8 @@ package configs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
"github.com/hashicorp/hcl/v2/gohcl"
|
|
||||||
"github.com/hashicorp/terraform/internal/addrs"
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
"github.com/hashicorp/terraform/internal/lang"
|
"github.com/hashicorp/terraform/internal/lang"
|
||||||
)
|
)
|
||||||
|
@ -24,12 +22,15 @@ type CheckRule struct {
|
||||||
// input variables can only refer to the variable that is being validated.
|
// input variables can only refer to the variable that is being validated.
|
||||||
Condition hcl.Expression
|
Condition hcl.Expression
|
||||||
|
|
||||||
// ErrorMessage is one or more full sentences, which would need to be in
|
// ErrorMessage should be one or more full sentences, which should be in
|
||||||
// English for consistency with the rest of the error message output but
|
// English for consistency with the rest of the error message output but
|
||||||
// can in practice be in any language as long as it ends with a period.
|
// can in practice be in any language. The message should describe what is
|
||||||
// The message should describe what is required for the condition to return
|
// required for the condition to return true in a way that would make sense
|
||||||
// true in a way that would make sense to a caller of the module.
|
// to a caller of the module.
|
||||||
ErrorMessage string
|
//
|
||||||
|
// The error message expression has the same variables available for
|
||||||
|
// interpolation as the corresponding condition.
|
||||||
|
ErrorMessage hcl.Expression
|
||||||
|
|
||||||
DeclRange hcl.Range
|
DeclRange hcl.Range
|
||||||
}
|
}
|
||||||
|
@ -111,77 +112,12 @@ func decodeCheckRuleBlock(block *hcl.Block, override bool) (*CheckRule, hcl.Diag
|
||||||
}
|
}
|
||||||
|
|
||||||
if attr, exists := content.Attributes["error_message"]; exists {
|
if attr, exists := content.Attributes["error_message"]; exists {
|
||||||
moreDiags := gohcl.DecodeExpression(attr.Expr, nil, &cr.ErrorMessage)
|
cr.ErrorMessage = attr.Expr
|
||||||
diags = append(diags, moreDiags...)
|
|
||||||
if !moreDiags.HasErrors() {
|
|
||||||
const errSummary = "Invalid validation error message"
|
|
||||||
switch {
|
|
||||||
case cr.ErrorMessage == "":
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: errSummary,
|
|
||||||
Detail: "An empty string is not a valid nor useful error message.",
|
|
||||||
Subject: attr.Expr.Range().Ptr(),
|
|
||||||
})
|
|
||||||
case !looksLikeSentences(cr.ErrorMessage):
|
|
||||||
// Because we're going to include this string verbatim as part
|
|
||||||
// of a bigger error message written in our usual style in
|
|
||||||
// English, we'll require the given error message to conform
|
|
||||||
// to that. We might relax this in future if e.g. we start
|
|
||||||
// presenting these error messages in a different way, or if
|
|
||||||
// Terraform starts supporting producing error messages in
|
|
||||||
// other human languages, etc.
|
|
||||||
// For pragmatism we also allow sentences ending with
|
|
||||||
// exclamation points, but we don't mention it explicitly here
|
|
||||||
// because that's not really consistent with the Terraform UI
|
|
||||||
// writing style.
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: errSummary,
|
|
||||||
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.",
|
|
||||||
Subject: attr.Expr.Range().Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cr, diags
|
return cr, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// looksLikeSentence is a simple heuristic that encourages writing error
|
|
||||||
// messages that will be presentable when included as part of a larger
|
|
||||||
// Terraform error diagnostic whose other text is written in the Terraform
|
|
||||||
// UI writing style.
|
|
||||||
//
|
|
||||||
// This is intentionally not a very strong validation since we're assuming
|
|
||||||
// that module authors want to write good messages and might just need a nudge
|
|
||||||
// about Terraform's specific style, rather than that they are going to try
|
|
||||||
// to work around these rules to write a lower-quality message.
|
|
||||||
func looksLikeSentences(s string) bool {
|
|
||||||
if len(s) < 1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
runes := []rune(s) // HCL guarantees that all strings are valid UTF-8
|
|
||||||
first := runes[0]
|
|
||||||
last := runes[len(runes)-1]
|
|
||||||
|
|
||||||
// If the first rune is a letter then it must be an uppercase letter.
|
|
||||||
// (This will only see the first rune in a multi-rune combining sequence,
|
|
||||||
// but the first rune is generally the letter if any are, and if not then
|
|
||||||
// we'll just ignore it because we're primarily expecting English messages
|
|
||||||
// right now anyway, for consistency with all of Terraform's other output.)
|
|
||||||
if unicode.IsLetter(first) && !unicode.IsUpper(first) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// The string must be at least one full sentence, which implies having
|
|
||||||
// sentence-ending punctuation.
|
|
||||||
// (This assumes that if a sentence ends with quotes then the period
|
|
||||||
// will be outside the quotes, which is consistent with Terraform's UI
|
|
||||||
// writing style.)
|
|
||||||
return last == '.' || last == '?' || last == '!'
|
|
||||||
}
|
|
||||||
|
|
||||||
var checkRuleBlockSchema = &hcl.BodySchema{
|
var checkRuleBlockSchema = &hcl.BodySchema{
|
||||||
Attributes: []hcl.AttributeSchema{
|
Attributes: []hcl.AttributeSchema{
|
||||||
{
|
{
|
||||||
|
|
|
@ -345,6 +345,31 @@ func decodeVariableValidationBlock(varName string, block *hcl.Block, override bo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if vv.ErrorMessage != nil {
|
||||||
|
// The same applies to the validation error message, except that
|
||||||
|
// references are not required. A string literal is a valid error
|
||||||
|
// message.
|
||||||
|
goodRefs := 0
|
||||||
|
for _, traversal := range vv.ErrorMessage.Variables() {
|
||||||
|
ref, moreDiags := addrs.ParseRef(traversal)
|
||||||
|
if !moreDiags.HasErrors() {
|
||||||
|
if addr, ok := ref.Subject.(addrs.InputVariable); ok {
|
||||||
|
if addr.Name == varName {
|
||||||
|
goodRefs++
|
||||||
|
continue // Reference is valid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we fall out here then the reference is invalid.
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid reference in variable validation",
|
||||||
|
Detail: fmt.Sprintf("The error message for variable %q can only refer to the variable itself, using var.%s.", varName, varName),
|
||||||
|
Subject: traversal.SourceRange().Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return vv, diags
|
return vv, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
package configs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_looksLikeSentences(t *testing.T) {
|
|
||||||
tests := map[string]struct {
|
|
||||||
args string
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
"empty sentence": {
|
|
||||||
args: "",
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
"valid sentence": {
|
|
||||||
args: "A valid sentence.",
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
"valid sentence with an accent": {
|
|
||||||
args: `A Valid sentence with an accent "é".`,
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, tt := range tests {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
if got := looksLikeSentences(tt.args); got != tt.want {
|
|
||||||
t.Errorf("looksLikeSentences() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
variable "validation" {
|
|
||||||
validation {
|
|
||||||
condition = var.validation != 4
|
|
||||||
error_message = "not four" # ERROR: Invalid validation error message
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,3 +9,10 @@ variable "validation" {
|
||||||
error_message = "Must be five."
|
error_message = "Must be five."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "validation_error_expression" {
|
||||||
|
validation {
|
||||||
|
condition = var.validation_error_expression != 1
|
||||||
|
error_message = "Cannot equal ${local.foo}." # ERROR: Invalid reference in variable validation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,3 +4,19 @@ variable "validation" {
|
||||||
error_message = "Must be five."
|
error_message = "Must be five."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "validation_function" {
|
||||||
|
type = list(string)
|
||||||
|
validation {
|
||||||
|
condition = length(var.validation_function) > 0
|
||||||
|
error_message = "Must not be empty."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "validation_error_expression" {
|
||||||
|
type = list(string)
|
||||||
|
validation {
|
||||||
|
condition = length(var.validation_error_expression) < 10
|
||||||
|
error_message = "Too long (${length(var.validation_error_expression)} is greater than 10)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2561,7 +2561,7 @@ func TestContext2Plan_preconditionErrors(t *testing.T) {
|
||||||
{
|
{
|
||||||
"test_resource.c.value",
|
"test_resource.c.value",
|
||||||
"Invalid condition result",
|
"Invalid condition result",
|
||||||
"Invalid validation condition result value: a bool is required",
|
"Invalid condition result value: a bool is required",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2095,3 +2095,293 @@ func TestContext2Validate_nonNullableVariableDefaultValidation(t *testing.T) {
|
||||||
t.Fatal(diags.ErrWithWarnings())
|
t.Fatal(diags.ErrWithWarnings())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContext2Validate_precondition_good(t *testing.T) {
|
||||||
|
p := testProvider("aws")
|
||||||
|
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||||
|
ResourceTypes: map[string]*configschema.Block{
|
||||||
|
"aws_instance": {
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"foo": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions]
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "input" {
|
||||||
|
type = string
|
||||||
|
default = "foo"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "test" {
|
||||||
|
foo = var.input
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
precondition {
|
||||||
|
condition = length(var.input) > 0
|
||||||
|
error_message = "Input cannot be empty."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
diags := ctx.Validate(m)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Fatal(diags.ErrWithWarnings())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext2Validate_precondition_badCondition(t *testing.T) {
|
||||||
|
p := testProvider("aws")
|
||||||
|
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||||
|
ResourceTypes: map[string]*configschema.Block{
|
||||||
|
"aws_instance": {
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"foo": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions]
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "input" {
|
||||||
|
type = string
|
||||||
|
default = "foo"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "test" {
|
||||||
|
foo = var.input
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
precondition {
|
||||||
|
condition = length(one(var.input)) == 1
|
||||||
|
error_message = "You can't do that."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
diags := ctx.Validate(m)
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Fatalf("succeeded; want error")
|
||||||
|
}
|
||||||
|
if got, want := diags.Err().Error(), "Invalid function argument"; !strings.Contains(got, want) {
|
||||||
|
t.Errorf("unexpected error.\ngot: %s\nshould contain: %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext2Validate_precondition_badErrorMessage(t *testing.T) {
|
||||||
|
p := testProvider("aws")
|
||||||
|
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||||
|
ResourceTypes: map[string]*configschema.Block{
|
||||||
|
"aws_instance": {
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"foo": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions]
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "input" {
|
||||||
|
type = string
|
||||||
|
default = "foo"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "test" {
|
||||||
|
foo = var.input
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
precondition {
|
||||||
|
condition = var.input != "foo"
|
||||||
|
error_message = "This is a bad use of a function: ${one(var.input)}."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
diags := ctx.Validate(m)
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Fatalf("succeeded; want error")
|
||||||
|
}
|
||||||
|
if got, want := diags.Err().Error(), "Invalid function argument"; !strings.Contains(got, want) {
|
||||||
|
t.Errorf("unexpected error.\ngot: %s\nshould contain: %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext2Validate_postcondition_good(t *testing.T) {
|
||||||
|
p := testProvider("aws")
|
||||||
|
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||||
|
ResourceTypes: map[string]*configschema.Block{
|
||||||
|
"aws_instance": {
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"foo": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "test" {
|
||||||
|
foo = "foo"
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
postcondition {
|
||||||
|
condition = length(self.foo) > 0
|
||||||
|
error_message = "Input cannot be empty."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
diags := ctx.Validate(m)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Fatal(diags.ErrWithWarnings())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext2Validate_postcondition_badCondition(t *testing.T) {
|
||||||
|
p := testProvider("aws")
|
||||||
|
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||||
|
ResourceTypes: map[string]*configschema.Block{
|
||||||
|
"aws_instance": {
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"foo": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// This postcondition's condition expression does not refer to self, which
|
||||||
|
// is unrealistic. This is because at the time of writing the test, self is
|
||||||
|
// always an unknown value of dynamic type during validation. As a result,
|
||||||
|
// validation of conditions which refer to resource arguments is not
|
||||||
|
// possible until plan time. For now we exercise the code by referring to
|
||||||
|
// an input variable.
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions]
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "input" {
|
||||||
|
type = string
|
||||||
|
default = "foo"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "test" {
|
||||||
|
foo = var.input
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
postcondition {
|
||||||
|
condition = length(one(var.input)) == 1
|
||||||
|
error_message = "You can't do that."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
diags := ctx.Validate(m)
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Fatalf("succeeded; want error")
|
||||||
|
}
|
||||||
|
if got, want := diags.Err().Error(), "Invalid function argument"; !strings.Contains(got, want) {
|
||||||
|
t.Errorf("unexpected error.\ngot: %s\nshould contain: %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext2Validate_postcondition_badErrorMessage(t *testing.T) {
|
||||||
|
p := testProvider("aws")
|
||||||
|
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||||
|
ResourceTypes: map[string]*configschema.Block{
|
||||||
|
"aws_instance": {
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"foo": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "test" {
|
||||||
|
foo = "foo"
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
postcondition {
|
||||||
|
condition = self.foo != "foo"
|
||||||
|
error_message = "This is a bad use of a function: ${one("foo")}."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
diags := ctx.Validate(m)
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Fatalf("succeeded; want error")
|
||||||
|
}
|
||||||
|
if got, want := diags.Err().Error(), "Invalid function argument"; !strings.Contains(got, want) {
|
||||||
|
t.Errorf("unexpected error.\ngot: %s\nshould contain: %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package terraform
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
@ -59,12 +60,20 @@ func evalCheckRules(typ checkType, rules []*configs.CheckRule, ctx EvalContext,
|
||||||
|
|
||||||
refs, moreDiags := lang.ReferencesInExpr(rule.Condition)
|
refs, moreDiags := lang.ReferencesInExpr(rule.Condition)
|
||||||
ruleDiags = ruleDiags.Append(moreDiags)
|
ruleDiags = ruleDiags.Append(moreDiags)
|
||||||
|
moreRefs, moreDiags := lang.ReferencesInExpr(rule.ErrorMessage)
|
||||||
|
ruleDiags = ruleDiags.Append(moreDiags)
|
||||||
|
refs = append(refs, moreRefs...)
|
||||||
|
|
||||||
scope := ctx.EvaluationScope(self, keyData)
|
scope := ctx.EvaluationScope(self, keyData)
|
||||||
hclCtx, moreDiags := scope.EvalContext(refs)
|
hclCtx, moreDiags := scope.EvalContext(refs)
|
||||||
ruleDiags = ruleDiags.Append(moreDiags)
|
ruleDiags = ruleDiags.Append(moreDiags)
|
||||||
|
|
||||||
result, hclDiags := rule.Condition.Value(hclCtx)
|
result, hclDiags := rule.Condition.Value(hclCtx)
|
||||||
ruleDiags = ruleDiags.Append(hclDiags)
|
ruleDiags = ruleDiags.Append(hclDiags)
|
||||||
|
|
||||||
|
errorValue, errorDiags := rule.ErrorMessage.Value(hclCtx)
|
||||||
|
ruleDiags = ruleDiags.Append(errorDiags)
|
||||||
|
|
||||||
diags = diags.Append(ruleDiags)
|
diags = diags.Append(ruleDiags)
|
||||||
|
|
||||||
if ruleDiags.HasErrors() {
|
if ruleDiags.HasErrors() {
|
||||||
|
@ -91,7 +100,7 @@ func evalCheckRules(typ checkType, rules []*configs.CheckRule, ctx EvalContext,
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
Severity: hcl.DiagError,
|
Severity: hcl.DiagError,
|
||||||
Summary: errInvalidCondition,
|
Summary: errInvalidCondition,
|
||||||
Detail: fmt.Sprintf("Invalid validation condition result value: %s.", tfdiags.FormatError(err)),
|
Detail: fmt.Sprintf("Invalid condition result value: %s.", tfdiags.FormatError(err)),
|
||||||
Subject: rule.Condition.Range().Ptr(),
|
Subject: rule.Condition.Range().Ptr(),
|
||||||
Expression: rule.Condition,
|
Expression: rule.Condition,
|
||||||
EvalContext: hclCtx,
|
EvalContext: hclCtx,
|
||||||
|
@ -99,16 +108,38 @@ func evalCheckRules(typ checkType, rules []*configs.CheckRule, ctx EvalContext,
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.False() {
|
if result.True() {
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
continue
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: typ.FailureSummary(),
|
|
||||||
Detail: rule.ErrorMessage,
|
|
||||||
Subject: rule.Condition.Range().Ptr(),
|
|
||||||
Expression: rule.Condition,
|
|
||||||
EvalContext: hclCtx,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errorMessage string
|
||||||
|
if !errorDiags.HasErrors() && errorValue.IsKnown() && !errorValue.IsNull() {
|
||||||
|
var err error
|
||||||
|
errorValue, err = convert.Convert(errorValue, cty.String)
|
||||||
|
if err != nil {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid error message",
|
||||||
|
Detail: fmt.Sprintf("Unsuitable value for error message: %s.", tfdiags.FormatError(err)),
|
||||||
|
Subject: rule.ErrorMessage.Range().Ptr(),
|
||||||
|
Expression: rule.ErrorMessage,
|
||||||
|
EvalContext: hclCtx,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
errorMessage = strings.TrimSpace(errorValue.AsString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if errorMessage == "" {
|
||||||
|
errorMessage = "Failed to evaluate condition error message."
|
||||||
|
}
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: typ.FailureSummary(),
|
||||||
|
Detail: errorMessage,
|
||||||
|
Subject: rule.Condition.Range().Ptr(),
|
||||||
|
Expression: rule.Condition,
|
||||||
|
EvalContext: hclCtx,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return diags
|
return diags
|
||||||
|
|
|
@ -3,8 +3,10 @@ package terraform
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
"github.com/hashicorp/hcl/v2/gohcl"
|
||||||
"github.com/hashicorp/terraform/internal/addrs"
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
"github.com/hashicorp/terraform/internal/configs"
|
"github.com/hashicorp/terraform/internal/configs"
|
||||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||||
|
@ -205,11 +207,67 @@ func evalVariableValidations(addr addrs.AbsInputVariableInstance, config *config
|
||||||
for _, validation := range config.Validations {
|
for _, validation := range config.Validations {
|
||||||
const errInvalidCondition = "Invalid variable validation result"
|
const errInvalidCondition = "Invalid variable validation result"
|
||||||
const errInvalidValue = "Invalid value for variable"
|
const errInvalidValue = "Invalid value for variable"
|
||||||
|
var ruleDiags tfdiags.Diagnostics
|
||||||
|
|
||||||
result, moreDiags := validation.Condition.Value(hclCtx)
|
result, moreDiags := validation.Condition.Value(hclCtx)
|
||||||
diags = diags.Append(moreDiags)
|
ruleDiags = ruleDiags.Append(moreDiags)
|
||||||
if moreDiags.HasErrors() {
|
errorValue, errorDiags := validation.ErrorMessage.Value(hclCtx)
|
||||||
log.Printf("[TRACE] evalVariableValidations: %s rule %s condition expression failed: %s", addr, validation.DeclRange, diags.Err().Error())
|
|
||||||
|
// The following error handling is a workaround to preserve backwards
|
||||||
|
// compatibility. Due to an implementation quirk, all prior versions of
|
||||||
|
// Terraform would treat error messages specified using JSON
|
||||||
|
// configuration syntax (.tf.json) as string literals, even if they
|
||||||
|
// contained the "${" template expression operator. This behaviour did
|
||||||
|
// not match that of HCL configuration syntax, where a template
|
||||||
|
// expression would result in a validation error.
|
||||||
|
//
|
||||||
|
// As a result, users writing or generating JSON configuration syntax
|
||||||
|
// may have specified error messages which are invalid template
|
||||||
|
// expressions. As we add support for error message expressions, we are
|
||||||
|
// unable to perfectly distinguish between these two cases.
|
||||||
|
//
|
||||||
|
// To ensure that we don't break backwards compatibility, we have the
|
||||||
|
// below fallback logic if the error message fails to evaluate. This
|
||||||
|
// should only have any effect for JSON configurations. The gohcl
|
||||||
|
// DecodeExpression function behaves differently when the source of the
|
||||||
|
// expression is a JSON configuration file and a nil context is passed.
|
||||||
|
if errorDiags.HasErrors() {
|
||||||
|
// Attempt to decode the expression as a string literal. Passing
|
||||||
|
// nil as the context forces a JSON syntax string value to be
|
||||||
|
// interpreted as a string literal.
|
||||||
|
var errorString string
|
||||||
|
moreErrorDiags := gohcl.DecodeExpression(validation.ErrorMessage, nil, &errorString)
|
||||||
|
if !moreErrorDiags.HasErrors() {
|
||||||
|
// Decoding succeeded, meaning that this is a JSON syntax
|
||||||
|
// string value. We rewrap that as a cty value to allow later
|
||||||
|
// decoding to succeed.
|
||||||
|
errorValue = cty.StringVal(errorString)
|
||||||
|
|
||||||
|
// This warning diagnostic explains this odd behaviour, while
|
||||||
|
// giving us an escape hatch to change this to a hard failure
|
||||||
|
// in some future Terraform 1.x version.
|
||||||
|
errorDiags = hcl.Diagnostics{
|
||||||
|
&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagWarning,
|
||||||
|
Summary: "Validation error message expression is invalid",
|
||||||
|
Detail: fmt.Sprintf("The error message provided could not be evaluated as an expression, so Terraform is interpreting it as a string literal.\n\nIn future versions of Terraform, this will be considered an error. Please file a GitHub issue if this would break your workflow.\n\n%s", errorDiags.Error()),
|
||||||
|
Subject: validation.ErrorMessage.Range().Ptr(),
|
||||||
|
Context: validation.DeclRange.Ptr(),
|
||||||
|
Expression: validation.ErrorMessage,
|
||||||
|
EvalContext: hclCtx,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want to either report the original diagnostics if the
|
||||||
|
// fallback failed, or the warning generated above if it succeeded.
|
||||||
|
ruleDiags = ruleDiags.Append(errorDiags)
|
||||||
|
}
|
||||||
|
|
||||||
|
diags = diags.Append(ruleDiags)
|
||||||
|
|
||||||
|
if ruleDiags.HasErrors() {
|
||||||
|
log.Printf("[TRACE] evalVariableValidations: %s rule %s check rule evaluation failed: %s", addr, validation.DeclRange, ruleDiags.Err().Error())
|
||||||
}
|
}
|
||||||
if !result.IsKnown() {
|
if !result.IsKnown() {
|
||||||
log.Printf("[TRACE] evalVariableValidations: %s rule %s condition value is unknown, so skipping validation for now", addr, validation.DeclRange)
|
log.Printf("[TRACE] evalVariableValidations: %s rule %s condition value is unknown, so skipping validation for now", addr, validation.DeclRange)
|
||||||
|
@ -245,26 +303,53 @@ func evalVariableValidations(addr addrs.AbsInputVariableInstance, config *config
|
||||||
// we discard the marks now.
|
// we discard the marks now.
|
||||||
result, _ = result.Unmark()
|
result, _ = result.Unmark()
|
||||||
|
|
||||||
if result.False() {
|
if result.True() {
|
||||||
if expr != nil {
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorMessage string
|
||||||
|
if !errorDiags.HasErrors() && errorValue.IsKnown() && !errorValue.IsNull() {
|
||||||
|
var err error
|
||||||
|
errorValue, err = convert.Convert(errorValue, cty.String)
|
||||||
|
if err != nil {
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
Severity: hcl.DiagError,
|
Severity: hcl.DiagError,
|
||||||
Summary: errInvalidValue,
|
Summary: "Invalid error message",
|
||||||
Detail: fmt.Sprintf("%s\n\nThis was checked by the validation rule at %s.", validation.ErrorMessage, validation.DeclRange.String()),
|
Detail: fmt.Sprintf("Unsuitable value for error message: %s.", tfdiags.FormatError(err)),
|
||||||
Subject: expr.Range().Ptr(),
|
Subject: validation.ErrorMessage.Range().Ptr(),
|
||||||
|
Expression: validation.ErrorMessage,
|
||||||
|
EvalContext: hclCtx,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Since we don't have a source expression for a root module
|
errorMessage = strings.TrimSpace(errorValue.AsString())
|
||||||
// variable, we'll just report the error from the perspective
|
|
||||||
// of the variable declaration itself.
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: errInvalidValue,
|
|
||||||
Detail: fmt.Sprintf("%s\n\nThis was checked by the validation rule at %s.", validation.ErrorMessage, validation.DeclRange.String()),
|
|
||||||
Subject: config.DeclRange.Ptr(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if errorMessage == "" {
|
||||||
|
errorMessage = "Failed to evaluate condition error message."
|
||||||
|
}
|
||||||
|
|
||||||
|
if expr != nil {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: errInvalidValue,
|
||||||
|
Detail: fmt.Sprintf("%s\n\nThis was checked by the validation rule at %s.", errorMessage, validation.DeclRange.String()),
|
||||||
|
Subject: expr.Range().Ptr(),
|
||||||
|
Expression: validation.Condition,
|
||||||
|
EvalContext: hclCtx,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Since we don't have a source expression for a root module
|
||||||
|
// variable, we'll just report the error from the perspective
|
||||||
|
// of the variable declaration itself.
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: errInvalidValue,
|
||||||
|
Detail: fmt.Sprintf("%s\n\nThis was checked by the validation rule at %s.", errorMessage, validation.DeclRange.String()),
|
||||||
|
Subject: config.DeclRange.Ptr(),
|
||||||
|
Expression: validation.Condition,
|
||||||
|
EvalContext: hclCtx,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return diags
|
return diags
|
||||||
|
|
|
@ -2,12 +2,14 @@ package terraform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/internal/addrs"
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
|
"github.com/hashicorp/terraform/internal/lang"
|
||||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -561,3 +563,155 @@ func TestPrepareFinalInputVariableValue(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These tests cover the JSON syntax configuration edge case handling,
|
||||||
|
// the background of which is described in detail in comments in the
|
||||||
|
// evalVariableValidations function. Future versions of Terraform may
|
||||||
|
// be able to remove this behaviour altogether.
|
||||||
|
func TestEvalVariableValidations_jsonErrorMessageEdgeCase(t *testing.T) {
|
||||||
|
cfgSrc := `{
|
||||||
|
"variable": {
|
||||||
|
"valid": {
|
||||||
|
"type": "string",
|
||||||
|
"validation": {
|
||||||
|
"condition": "${var.valid != \"bar\"}",
|
||||||
|
"error_message": "Valid template string ${var.valid}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"invalid": {
|
||||||
|
"type": "string",
|
||||||
|
"validation": {
|
||||||
|
"condition": "${var.invalid != \"bar\"}",
|
||||||
|
"error_message": "Invalid template string ${"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
cfg := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf.json": cfgSrc,
|
||||||
|
})
|
||||||
|
variableConfigs := cfg.Module.Variables
|
||||||
|
|
||||||
|
// Because we loaded our pseudo-module from a temporary file, the
|
||||||
|
// declaration source ranges will have unpredictable filenames. We'll
|
||||||
|
// fix that here just to make things easier below.
|
||||||
|
for _, vc := range variableConfigs {
|
||||||
|
vc.DeclRange.Filename = "main.tf.json"
|
||||||
|
for _, v := range vc.Validations {
|
||||||
|
v.DeclRange.Filename = "main.tf.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
varName string
|
||||||
|
given cty.Value
|
||||||
|
wantErr []string
|
||||||
|
wantWarn []string
|
||||||
|
}{
|
||||||
|
// Valid variable validation declaration, assigned value which passes
|
||||||
|
// the condition generates no diagnostics.
|
||||||
|
{
|
||||||
|
varName: "valid",
|
||||||
|
given: cty.StringVal("foo"),
|
||||||
|
},
|
||||||
|
// Assigning a value which fails the condition generates an error
|
||||||
|
// message with the expression successfully evaluated.
|
||||||
|
{
|
||||||
|
varName: "valid",
|
||||||
|
given: cty.StringVal("bar"),
|
||||||
|
wantErr: []string{
|
||||||
|
"Invalid value for variable",
|
||||||
|
"Valid template string bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Invalid variable validation declaration due to an unparseable
|
||||||
|
// template string. Assigning a value which passes the condition
|
||||||
|
// results in a warning about the error message.
|
||||||
|
{
|
||||||
|
varName: "invalid",
|
||||||
|
given: cty.StringVal("foo"),
|
||||||
|
wantWarn: []string{
|
||||||
|
"Validation error message expression is invalid",
|
||||||
|
"Missing expression; Expected the start of an expression, but found the end of the file.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Assigning a value which fails the condition generates an error
|
||||||
|
// message including the configured string interpreted as a literal
|
||||||
|
// value, and the same warning diagnostic as above.
|
||||||
|
{
|
||||||
|
varName: "invalid",
|
||||||
|
given: cty.StringVal("bar"),
|
||||||
|
wantErr: []string{
|
||||||
|
"Invalid value for variable",
|
||||||
|
"Invalid template string ${",
|
||||||
|
},
|
||||||
|
wantWarn: []string{
|
||||||
|
"Validation error message expression is invalid",
|
||||||
|
"Missing expression; Expected the start of an expression, but found the end of the file.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%s %#v", test.varName, test.given), func(t *testing.T) {
|
||||||
|
varAddr := addrs.InputVariable{Name: test.varName}.Absolute(addrs.RootModuleInstance)
|
||||||
|
varCfg := variableConfigs[test.varName]
|
||||||
|
if varCfg == nil {
|
||||||
|
t.Fatalf("invalid variable name %q", test.varName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a mock context to allow the function under test to
|
||||||
|
// retrieve the variable value and evaluate the expressions
|
||||||
|
ctx := &MockEvalContext{}
|
||||||
|
|
||||||
|
// We need a minimal scope to allow basic functions to be passed to
|
||||||
|
// the HCL scope
|
||||||
|
ctx.EvaluationScopeScope = &lang.Scope{}
|
||||||
|
ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value {
|
||||||
|
if got, want := addr.String(), varAddr.String(); got != want {
|
||||||
|
t.Errorf("incorrect argument to GetVariableValue: got %s, want %s", got, want)
|
||||||
|
}
|
||||||
|
return test.given
|
||||||
|
}
|
||||||
|
|
||||||
|
gotDiags := evalVariableValidations(
|
||||||
|
varAddr, varCfg, nil, ctx,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(test.wantErr) == 0 && len(test.wantWarn) == 0 {
|
||||||
|
if len(gotDiags) > 0 {
|
||||||
|
t.Errorf("no diags expected, got %s", gotDiags.Err().Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wantErrs:
|
||||||
|
for _, want := range test.wantErr {
|
||||||
|
for _, diag := range gotDiags {
|
||||||
|
if diag.Severity() != tfdiags.Error {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
desc := diag.Description()
|
||||||
|
if strings.Contains(desc.Summary, want) || strings.Contains(desc.Detail, want) {
|
||||||
|
continue wantErrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Errorf("no error diagnostics found containing %q\ngot: %s", want, gotDiags.Err().Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
wantWarns:
|
||||||
|
for _, want := range test.wantWarn {
|
||||||
|
for _, diag := range gotDiags {
|
||||||
|
if diag.Severity() != tfdiags.Warning {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
desc := diag.Description()
|
||||||
|
if strings.Contains(desc.Summary, want) || strings.Contains(desc.Detail, want) {
|
||||||
|
continue wantWarns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Errorf("no warning diagnostics found containing %q\ngot: %s", want, gotDiags.Err().Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -176,10 +176,14 @@ func (n *NodeAbstractResource) References() []*addrs.Reference {
|
||||||
for _, check := range c.Preconditions {
|
for _, check := range c.Preconditions {
|
||||||
refs, _ := lang.ReferencesInExpr(check.Condition)
|
refs, _ := lang.ReferencesInExpr(check.Condition)
|
||||||
result = append(result, refs...)
|
result = append(result, refs...)
|
||||||
|
refs, _ = lang.ReferencesInExpr(check.ErrorMessage)
|
||||||
|
result = append(result, refs...)
|
||||||
}
|
}
|
||||||
for _, check := range c.Postconditions {
|
for _, check := range c.Postconditions {
|
||||||
refs, _ := lang.ReferencesInExpr(check.Condition)
|
refs, _ := lang.ReferencesInExpr(check.Condition)
|
||||||
result = append(result, refs...)
|
result = append(result, refs...)
|
||||||
|
refs, _ = lang.ReferencesInExpr(check.ErrorMessage)
|
||||||
|
result = append(result, refs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -41,6 +41,18 @@ func (n *NodeValidatableResource) Path() addrs.ModuleInstance {
|
||||||
func (n *NodeValidatableResource) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
|
func (n *NodeValidatableResource) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
|
||||||
diags = diags.Append(n.validateResource(ctx))
|
diags = diags.Append(n.validateResource(ctx))
|
||||||
|
|
||||||
|
var self addrs.Referenceable
|
||||||
|
switch {
|
||||||
|
case n.Config.Count != nil:
|
||||||
|
self = n.Addr.Resource.Instance(addrs.IntKey(0))
|
||||||
|
case n.Config.ForEach != nil:
|
||||||
|
self = n.Addr.Resource.Instance(addrs.StringKey(""))
|
||||||
|
default:
|
||||||
|
self = n.Addr.Resource.Instance(addrs.NoKey)
|
||||||
|
}
|
||||||
|
diags = diags.Append(validateCheckRules(ctx, n.Config.Preconditions, nil))
|
||||||
|
diags = diags.Append(validateCheckRules(ctx, n.Config.Postconditions, self))
|
||||||
|
|
||||||
if managed := n.Config.Managed; managed != nil {
|
if managed := n.Config.Managed; managed != nil {
|
||||||
hasCount := n.Config.Count != nil
|
hasCount := n.Config.Count != nil
|
||||||
hasForEach := n.Config.ForEach != nil
|
hasForEach := n.Config.ForEach != nil
|
||||||
|
@ -466,6 +478,20 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateCheckRules(ctx EvalContext, crs []*configs.CheckRule, self addrs.Referenceable) tfdiags.Diagnostics {
|
||||||
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
|
for _, cr := range crs {
|
||||||
|
_, conditionDiags := ctx.EvaluateExpr(cr.Condition, cty.Bool, self)
|
||||||
|
diags = diags.Append(conditionDiags)
|
||||||
|
|
||||||
|
_, errorMessageDiags := ctx.EvaluateExpr(cr.ErrorMessage, cty.String, self)
|
||||||
|
diags = diags.Append(errorMessageDiags)
|
||||||
|
}
|
||||||
|
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
func validateCount(ctx EvalContext, expr hcl.Expression) (diags tfdiags.Diagnostics) {
|
func validateCount(ctx EvalContext, expr hcl.Expression) (diags tfdiags.Diagnostics) {
|
||||||
val, countDiags := evaluateCountExpressionValue(expr, ctx)
|
val, countDiags := evaluateCountExpressionValue(expr, ctx)
|
||||||
// If the value isn't known then that's the best we can do for now, but
|
// If the value isn't known then that's the best we can do for now, but
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
"github.com/hashicorp/hcl/v2/hcltest"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/internal/addrs"
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
|
@ -101,7 +102,7 @@ func TestNodeRootVariableExecute(t *testing.T) {
|
||||||
}
|
}
|
||||||
return cty.True, nil
|
return cty.True, nil
|
||||||
}),
|
}),
|
||||||
ErrorMessage: "Must be a number.",
|
ErrorMessage: hcltest.MockExprLiteral(cty.StringVal("Must be a number.")),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -335,11 +335,15 @@ Error: Resource postcondition failed
|
||||||
The selected AMI must be tagged with the Component value "nomad-server".
|
The selected AMI must be tagged with the Component value "nomad-server".
|
||||||
```
|
```
|
||||||
|
|
||||||
The `error_message` argument must always be a literal string, and should
|
The `error_message` argument can be any expression which evaluates to a string.
|
||||||
typically be written as a full sentence in a style similar to Terraform's own
|
This includes literal strings, heredocs, and template expressions. Multi-line
|
||||||
error messages. Terraform will show the given message alongside the name
|
error messages are supported, and lines with leading whitespace will not be
|
||||||
of the resource that detected the problem and any outside values used as part
|
word wrapped.
|
||||||
of the condition expression.
|
|
||||||
|
Error message should typically be written as one or more full sentences in a
|
||||||
|
style similar to Terraform's own error messages. Terraform will show the given
|
||||||
|
message alongside the name of the resource that detected the problem and any
|
||||||
|
outside values used as part of the condition expression.
|
||||||
|
|
||||||
## Preconditions or Postconditions?
|
## Preconditions or Postconditions?
|
||||||
|
|
||||||
|
|
|
@ -198,10 +198,13 @@ variable "image_id" {
|
||||||
```
|
```
|
||||||
|
|
||||||
If `condition` evaluates to `false`, Terraform will produce an error message
|
If `condition` evaluates to `false`, Terraform will produce an error message
|
||||||
that includes the sentences given in `error_message`. The error message string
|
that includes the result of the `error_message` expression. The error message
|
||||||
should be at least one full sentence explaining the constraint that failed,
|
should be at least one full sentence explaining the constraint that failed,
|
||||||
using a sentence structure similar to the above examples.
|
using a sentence structure similar to the above examples.
|
||||||
|
|
||||||
|
Error messages can be literal strings, heredocs, or template expressions. The
|
||||||
|
only valid reference in an error message is the variable under validation.
|
||||||
|
|
||||||
Multiple `validation` blocks can be declared in which case error messages
|
Multiple `validation` blocks can be declared in which case error messages
|
||||||
will be returned for _all_ failed conditions.
|
will be returned for _all_ failed conditions.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue