mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Error messages for preconditions, postconditions, and custom variable validations have until now been string literals. This commit changes this to treat the field as an HCL expression, which must evaluate to a string. Most commonly this will either be a string literal or a template expression. When the check rule condition is evaluated, we also evaluate the error message. This means that the error message should always evaluate to a string value, even if the condition passes. If it does not, this will result in an error diagnostic. If the condition fails, and the error message also fails to evaluate, we fall back to a default error message. This means that the check rule failure will still be reported, alongside diagnostics explaining why the custom error message failed to render. As part of this change, we also necessarily remove the heuristic about the error message format. This guidance can be readded in future as part of a configuration hint system.
133 lines
4.4 KiB
Go
133 lines
4.4 KiB
Go
package configs
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/lang"
|
|
)
|
|
|
|
// CheckRule represents a configuration-defined validation rule, precondition,
|
|
// or postcondition. Blocks of this sort can appear in a few different places
|
|
// in configuration, including "validation" blocks for variables,
|
|
// and "precondition" and "postcondition" blocks for resources.
|
|
type CheckRule struct {
|
|
// Condition is an expression that must evaluate to true if the condition
|
|
// holds or false if it does not. If the expression produces an error then
|
|
// that's considered to be a bug in the module defining the check.
|
|
//
|
|
// The available variables in a condition expression vary depending on what
|
|
// a check is attached to. For example, validation rules attached to
|
|
// input variables can only refer to the variable that is being validated.
|
|
Condition hcl.Expression
|
|
|
|
// ErrorMessage should be one or more full sentences, which should be in
|
|
// English for consistency with the rest of the error message output but
|
|
// can in practice be in any language. The message should describe what is
|
|
// required for the condition to return true in a way that would make sense
|
|
// to a caller of the module.
|
|
//
|
|
// The error message expression has the same variables available for
|
|
// interpolation as the corresponding condition.
|
|
ErrorMessage hcl.Expression
|
|
|
|
DeclRange hcl.Range
|
|
}
|
|
|
|
// validateSelfReferences looks for references in the check rule matching the
|
|
// specified resource address, returning error diagnostics if such a reference
|
|
// is found.
|
|
func (cr *CheckRule) validateSelfReferences(checkType string, addr addrs.Resource) hcl.Diagnostics {
|
|
var diags hcl.Diagnostics
|
|
refs, _ := lang.References(cr.Condition.Variables())
|
|
for _, ref := range refs {
|
|
var refAddr addrs.Resource
|
|
|
|
switch rs := ref.Subject.(type) {
|
|
case addrs.Resource:
|
|
refAddr = rs
|
|
case addrs.ResourceInstance:
|
|
refAddr = rs.Resource
|
|
default:
|
|
continue
|
|
}
|
|
|
|
if refAddr.Equal(addr) {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("Invalid reference in %s", checkType),
|
|
Detail: fmt.Sprintf("Configuration for %s may not refer to itself.", addr.String()),
|
|
Subject: cr.Condition.Range().Ptr(),
|
|
})
|
|
break
|
|
}
|
|
}
|
|
return diags
|
|
}
|
|
|
|
// decodeCheckRuleBlock decodes the contents of the given block as a check rule.
|
|
//
|
|
// Unlike most of our "decode..." functions, this one can be applied to blocks
|
|
// of various types as long as their body structures are "check-shaped". The
|
|
// function takes the containing block only because some error messages will
|
|
// refer to its location, and the returned object's DeclRange will be the
|
|
// block's header.
|
|
func decodeCheckRuleBlock(block *hcl.Block, override bool) (*CheckRule, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
cr := &CheckRule{
|
|
DeclRange: block.DefRange,
|
|
}
|
|
|
|
if override {
|
|
// For now we'll just forbid overriding check blocks, to simplify
|
|
// the initial design. If we can find a clear use-case for overriding
|
|
// checks in override files and there's a way to define it that
|
|
// isn't confusing then we could relax this.
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("Can't override %s blocks", block.Type),
|
|
Detail: fmt.Sprintf("Override files cannot override %q blocks.", block.Type),
|
|
Subject: cr.DeclRange.Ptr(),
|
|
})
|
|
return cr, diags
|
|
}
|
|
|
|
content, moreDiags := block.Body.Content(checkRuleBlockSchema)
|
|
diags = append(diags, moreDiags...)
|
|
|
|
if attr, exists := content.Attributes["condition"]; exists {
|
|
cr.Condition = attr.Expr
|
|
|
|
if len(cr.Condition.Variables()) == 0 {
|
|
// A condition expression that doesn't refer to any variable is
|
|
// pointless, because its result would always be a constant.
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("Invalid %s expression", block.Type),
|
|
Detail: "The condition expression must refer to at least one object from elsewhere in the configuration, or else its result would not be checking anything.",
|
|
Subject: cr.Condition.Range().Ptr(),
|
|
})
|
|
}
|
|
}
|
|
|
|
if attr, exists := content.Attributes["error_message"]; exists {
|
|
cr.ErrorMessage = attr.Expr
|
|
}
|
|
|
|
return cr, diags
|
|
}
|
|
|
|
var checkRuleBlockSchema = &hcl.BodySchema{
|
|
Attributes: []hcl.AttributeSchema{
|
|
{
|
|
Name: "condition",
|
|
Required: true,
|
|
},
|
|
{
|
|
Name: "error_message",
|
|
Required: true,
|
|
},
|
|
},
|
|
}
|