2024-02-08 03:48:59 -06:00
|
|
|
// Copyright (c) The OpenTofu Authors
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
2023-05-02 10:33:06 -05:00
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2023-09-20 07:16:53 -05:00
|
|
|
package tofu
|
2020-11-20 18:03:11 -06:00
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"log"
|
core: Check rule error message expressions
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.
2022-02-03 13:14:21 -06:00
|
|
|
"strings"
|
2020-11-20 18:03:11 -06:00
|
|
|
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/zclconf/go-cty/cty/convert"
|
|
|
|
|
2023-09-20 06:35:35 -05:00
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
|
|
"github.com/opentofu/opentofu/internal/checks"
|
|
|
|
"github.com/opentofu/opentofu/internal/configs"
|
|
|
|
"github.com/opentofu/opentofu/internal/instances"
|
|
|
|
"github.com/opentofu/opentofu/internal/lang"
|
|
|
|
"github.com/opentofu/opentofu/internal/lang/marks"
|
|
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
2020-11-20 18:03:11 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
// evalCheckRules ensures that all of the given check rules pass against
|
|
|
|
// the given HCL evaluation context.
|
|
|
|
//
|
|
|
|
// If any check rules produce an unknown result then they will be silently
|
|
|
|
// ignored on the assumption that the same checks will be run again later
|
|
|
|
// with fewer unknown values in the EvalContext.
|
|
|
|
//
|
|
|
|
// If any of the rules do not pass, the returned diagnostics will contain
|
|
|
|
// errors. Otherwise, it will either be empty or contain only warnings.
|
2023-03-23 03:04:21 -05:00
|
|
|
func evalCheckRules(typ addrs.CheckRuleType, rules []*configs.CheckRule, ctx EvalContext, self addrs.Checkable, keyData instances.RepetitionData, diagSeverity tfdiags.Severity) tfdiags.Diagnostics {
|
2022-03-28 06:24:28 -05:00
|
|
|
var diags tfdiags.Diagnostics
|
|
|
|
|
2022-06-15 20:00:20 -05:00
|
|
|
checkState := ctx.Checks()
|
|
|
|
if !checkState.ConfigHasChecks(self.ConfigCheckable()) {
|
|
|
|
// We have nothing to do if this object doesn't have any checks,
|
|
|
|
// but the "rules" slice should agree that we don't.
|
|
|
|
if ct := len(rules); ct != 0 {
|
|
|
|
panic(fmt.Sprintf("check state says that %s should have no rules, but it has %d", self, ct))
|
|
|
|
}
|
|
|
|
return diags
|
|
|
|
}
|
|
|
|
|
2020-11-20 18:03:11 -06:00
|
|
|
if len(rules) == 0 {
|
|
|
|
// Nothing to do
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-03-09 15:04:01 -06:00
|
|
|
severity := diagSeverity.ToHCL()
|
|
|
|
|
2022-03-28 06:24:28 -05:00
|
|
|
for i, rule := range rules {
|
2023-07-10 05:30:44 -05:00
|
|
|
result, ruleDiags := evalCheckRule(addrs.NewCheckRule(self, typ, i), rule, ctx, keyData, severity)
|
2022-03-28 06:24:28 -05:00
|
|
|
diags = diags.Append(ruleDiags)
|
2022-06-15 20:00:20 -05:00
|
|
|
|
|
|
|
log.Printf("[TRACE] evalCheckRules: %s status is now %s", self, result.Status)
|
|
|
|
if result.Status == checks.StatusFail {
|
|
|
|
checkState.ReportCheckFailure(self, typ, i, result.FailureMessage)
|
|
|
|
} else {
|
|
|
|
checkState.ReportCheckResult(self, typ, i, result.Status)
|
|
|
|
}
|
2022-03-28 06:24:28 -05:00
|
|
|
}
|
core: Check rule error message expressions
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.
2022-02-03 13:14:21 -06:00
|
|
|
|
2022-03-28 06:24:28 -05:00
|
|
|
return diags
|
|
|
|
}
|
2020-11-20 18:03:11 -06:00
|
|
|
|
2022-06-15 20:00:20 -05:00
|
|
|
type checkResult struct {
|
|
|
|
Status checks.Status
|
|
|
|
FailureMessage string
|
|
|
|
}
|
|
|
|
|
2023-07-10 05:30:44 -05:00
|
|
|
func validateCheckRule(addr addrs.CheckRule, rule *configs.CheckRule, ctx EvalContext, keyData instances.RepetitionData) (string, *hcl.EvalContext, tfdiags.Diagnostics) {
|
2022-03-28 06:24:28 -05:00
|
|
|
var diags tfdiags.Diagnostics
|
core: Check rule error message expressions
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.
2022-02-03 13:14:21 -06:00
|
|
|
|
2023-06-28 02:47:24 -05:00
|
|
|
refs, moreDiags := lang.ReferencesInExpr(addrs.ParseRef, rule.Condition)
|
2022-03-28 06:24:28 -05:00
|
|
|
diags = diags.Append(moreDiags)
|
2023-06-28 02:47:24 -05:00
|
|
|
moreRefs, moreDiags := lang.ReferencesInExpr(addrs.ParseRef, rule.ErrorMessage)
|
2022-03-28 06:24:28 -05:00
|
|
|
diags = diags.Append(moreDiags)
|
|
|
|
refs = append(refs, moreRefs...)
|
core: Check rule error message expressions
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.
2022-02-03 13:14:21 -06:00
|
|
|
|
2023-03-23 10:07:31 -05:00
|
|
|
var selfReference, sourceReference addrs.Referenceable
|
2023-07-10 05:30:44 -05:00
|
|
|
switch addr.Type {
|
2023-03-23 10:07:31 -05:00
|
|
|
case addrs.ResourcePostcondition:
|
2023-07-10 05:30:44 -05:00
|
|
|
switch s := addr.Container.(type) {
|
2022-03-28 06:24:28 -05:00
|
|
|
case addrs.AbsResourceInstance:
|
2023-03-23 10:07:31 -05:00
|
|
|
// Only resource postconditions can refer to self
|
2022-03-28 06:24:28 -05:00
|
|
|
selfReference = s.Resource
|
|
|
|
default:
|
2023-07-10 05:30:44 -05:00
|
|
|
panic(fmt.Sprintf("Invalid self reference type %t", addr.Container))
|
2020-11-20 18:03:11 -06:00
|
|
|
}
|
2023-03-23 10:07:31 -05:00
|
|
|
case addrs.CheckAssertion:
|
2023-07-10 05:30:44 -05:00
|
|
|
switch s := addr.Container.(type) {
|
2023-03-23 10:07:31 -05:00
|
|
|
case addrs.AbsCheck:
|
|
|
|
// Only check blocks have scoped resources so need to specify their
|
|
|
|
// source.
|
|
|
|
sourceReference = s.Check
|
|
|
|
default:
|
2023-07-10 05:30:44 -05:00
|
|
|
panic(fmt.Sprintf("Invalid source reference type %t", addr.Container))
|
2023-03-23 10:07:31 -05:00
|
|
|
}
|
2022-03-28 06:24:28 -05:00
|
|
|
}
|
2023-03-23 10:07:31 -05:00
|
|
|
scope := ctx.EvaluationScope(selfReference, sourceReference, keyData)
|
2020-11-20 18:03:11 -06:00
|
|
|
|
2022-03-28 06:24:28 -05:00
|
|
|
hclCtx, moreDiags := scope.EvalContext(refs)
|
|
|
|
diags = diags.Append(moreDiags)
|
|
|
|
|
2023-03-23 10:07:31 -05:00
|
|
|
errorMessage, moreDiags := evalCheckErrorMessage(rule.ErrorMessage, hclCtx)
|
|
|
|
diags = diags.Append(moreDiags)
|
|
|
|
|
|
|
|
return errorMessage, hclCtx, diags
|
|
|
|
}
|
2022-03-28 06:24:28 -05:00
|
|
|
|
2023-07-10 05:30:44 -05:00
|
|
|
func evalCheckRule(addr addrs.CheckRule, rule *configs.CheckRule, ctx EvalContext, keyData instances.RepetitionData, severity hcl.DiagnosticSeverity) (checkResult, tfdiags.Diagnostics) {
|
2022-06-15 20:00:20 -05:00
|
|
|
// NOTE: Intentionally not passing the caller's selected severity in here,
|
|
|
|
// because this reports errors in the configuration itself, not the failure
|
|
|
|
// of an otherwise-valid condition.
|
2023-07-10 05:30:44 -05:00
|
|
|
errorMessage, hclCtx, diags := validateCheckRule(addr, rule, ctx, keyData)
|
2023-03-23 10:07:31 -05:00
|
|
|
|
|
|
|
const errInvalidCondition = "Invalid condition result"
|
|
|
|
|
|
|
|
resultVal, hclDiags := rule.Condition.Value(hclCtx)
|
|
|
|
diags = diags.Append(hclDiags)
|
2022-03-28 06:24:28 -05:00
|
|
|
|
|
|
|
if diags.HasErrors() {
|
2023-07-10 05:30:44 -05:00
|
|
|
log.Printf("[TRACE] evalCheckRule: %s: %s", addr.Type, diags.Err().Error())
|
2022-12-14 13:45:01 -06:00
|
|
|
return checkResult{Status: checks.StatusError}, diags
|
2022-03-28 06:24:28 -05:00
|
|
|
}
|
|
|
|
|
2022-06-15 20:00:20 -05:00
|
|
|
if !resultVal.IsKnown() {
|
2023-03-23 10:07:31 -05:00
|
|
|
|
|
|
|
// Check assertions warn if a status is unknown.
|
2023-07-10 05:30:44 -05:00
|
|
|
if addr.Type == addrs.CheckAssertion {
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
2023-03-23 10:07:31 -05:00
|
|
|
Severity: hcl.DiagWarning,
|
2023-07-10 05:30:44 -05:00
|
|
|
Summary: fmt.Sprintf("%s known after apply", addr.Type.Description()),
|
2023-03-23 10:07:31 -05:00
|
|
|
Detail: "The condition could not be evaluated at this time, a result will be known when this plan is applied.",
|
|
|
|
Subject: rule.Condition.Range().Ptr(),
|
|
|
|
Expression: rule.Condition,
|
|
|
|
EvalContext: hclCtx,
|
2023-07-10 05:30:44 -05:00
|
|
|
Extra: &addrs.CheckRuleDiagnosticExtra{
|
|
|
|
CheckRule: addr,
|
|
|
|
},
|
|
|
|
})
|
2023-03-23 10:07:31 -05:00
|
|
|
}
|
|
|
|
|
2022-03-28 06:24:28 -05:00
|
|
|
// We'll wait until we've learned more, then.
|
2022-06-15 20:00:20 -05:00
|
|
|
return checkResult{Status: checks.StatusUnknown}, diags
|
2022-03-28 06:24:28 -05:00
|
|
|
}
|
2022-06-15 20:00:20 -05:00
|
|
|
if resultVal.IsNull() {
|
|
|
|
// NOTE: Intentionally not passing the caller's selected severity in here,
|
|
|
|
// because this reports errors in the configuration itself, not the failure
|
|
|
|
// of an otherwise-valid condition.
|
2022-03-28 06:24:28 -05:00
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
2022-06-15 20:00:20 -05:00
|
|
|
Severity: hcl.DiagError,
|
2022-03-28 06:24:28 -05:00
|
|
|
Summary: errInvalidCondition,
|
|
|
|
Detail: "Condition expression must return either true or false, not null.",
|
|
|
|
Subject: rule.Condition.Range().Ptr(),
|
|
|
|
Expression: rule.Condition,
|
|
|
|
EvalContext: hclCtx,
|
|
|
|
})
|
2022-06-15 20:00:20 -05:00
|
|
|
return checkResult{Status: checks.StatusError}, diags
|
2022-03-28 06:24:28 -05:00
|
|
|
}
|
|
|
|
var err error
|
2022-06-15 20:00:20 -05:00
|
|
|
resultVal, err = convert.Convert(resultVal, cty.Bool)
|
2022-03-28 06:24:28 -05:00
|
|
|
if err != nil {
|
2022-06-15 20:00:20 -05:00
|
|
|
// NOTE: Intentionally not passing the caller's selected severity in here,
|
|
|
|
// because this reports errors in the configuration itself, not the failure
|
|
|
|
// of an otherwise-valid condition.
|
2022-03-28 06:24:28 -05:00
|
|
|
detail := fmt.Sprintf("Invalid condition result value: %s.", tfdiags.FormatError(err))
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
2022-06-15 20:00:20 -05:00
|
|
|
Severity: hcl.DiagError,
|
2022-03-28 06:24:28 -05:00
|
|
|
Summary: errInvalidCondition,
|
|
|
|
Detail: detail,
|
|
|
|
Subject: rule.Condition.Range().Ptr(),
|
|
|
|
Expression: rule.Condition,
|
|
|
|
EvalContext: hclCtx,
|
|
|
|
})
|
2022-06-15 20:00:20 -05:00
|
|
|
return checkResult{Status: checks.StatusError}, diags
|
2022-03-28 06:24:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// The condition result may be marked if the expression refers to a
|
|
|
|
// sensitive value.
|
2022-06-15 20:00:20 -05:00
|
|
|
resultVal, _ = resultVal.Unmark()
|
2022-03-28 06:24:28 -05:00
|
|
|
|
2022-06-15 20:00:20 -05:00
|
|
|
status := checks.StatusForCtyValue(resultVal)
|
core: Check rule error message expressions
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.
2022-02-03 13:14:21 -06:00
|
|
|
|
2022-06-15 20:00:20 -05:00
|
|
|
if status != checks.StatusFail {
|
|
|
|
return checkResult{Status: status}, diags
|
2020-11-20 18:03:11 -06:00
|
|
|
}
|
2022-06-15 20:00:20 -05:00
|
|
|
|
|
|
|
errorMessageForDiags := errorMessage
|
|
|
|
if errorMessageForDiags == "" {
|
|
|
|
errorMessageForDiags = "This check failed, but has an invalid error message as described in the other accompanying messages."
|
2022-03-28 06:24:28 -05:00
|
|
|
}
|
2023-07-10 05:30:44 -05:00
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
2022-06-15 20:00:20 -05:00
|
|
|
// The caller gets to choose the severity of this one, because we
|
|
|
|
// treat condition failures as warnings in the presence of
|
|
|
|
// certain special planning options.
|
2022-03-28 06:24:28 -05:00
|
|
|
Severity: severity,
|
2023-07-10 05:30:44 -05:00
|
|
|
Summary: fmt.Sprintf("%s failed", addr.Type.Description()),
|
2022-06-15 20:00:20 -05:00
|
|
|
Detail: errorMessageForDiags,
|
2022-03-28 06:24:28 -05:00
|
|
|
Subject: rule.Condition.Range().Ptr(),
|
|
|
|
Expression: rule.Condition,
|
|
|
|
EvalContext: hclCtx,
|
2023-07-10 05:30:44 -05:00
|
|
|
Extra: &addrs.CheckRuleDiagnosticExtra{
|
|
|
|
CheckRule: addr,
|
|
|
|
},
|
|
|
|
})
|
2022-06-15 20:00:20 -05:00
|
|
|
|
|
|
|
return checkResult{
|
|
|
|
Status: status,
|
|
|
|
FailureMessage: errorMessage,
|
|
|
|
}, diags
|
|
|
|
}
|
|
|
|
|
|
|
|
// evalCheckErrorMessage makes a best effort to evaluate the given expression,
|
|
|
|
// as an error message string.
|
|
|
|
//
|
|
|
|
// It will either return a non-empty message string or it'll return diagnostics
|
|
|
|
// with either errors or warnings that explain why the given expression isn't
|
|
|
|
// acceptable.
|
|
|
|
func evalCheckErrorMessage(expr hcl.Expression, hclCtx *hcl.EvalContext) (string, tfdiags.Diagnostics) {
|
|
|
|
var diags tfdiags.Diagnostics
|
|
|
|
|
|
|
|
val, hclDiags := expr.Value(hclCtx)
|
|
|
|
diags = diags.Append(hclDiags)
|
|
|
|
if hclDiags.HasErrors() {
|
|
|
|
return "", diags
|
|
|
|
}
|
|
|
|
|
|
|
|
val, err := convert.Convert(val, 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: expr.Range().Ptr(),
|
|
|
|
Expression: expr,
|
|
|
|
EvalContext: hclCtx,
|
|
|
|
})
|
|
|
|
return "", diags
|
|
|
|
}
|
|
|
|
if !val.IsKnown() {
|
|
|
|
return "", diags
|
|
|
|
}
|
|
|
|
if val.IsNull() {
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: "Invalid error message",
|
|
|
|
Detail: "Unsuitable value for error message: must not be null.",
|
|
|
|
Subject: expr.Range().Ptr(),
|
|
|
|
Expression: expr,
|
|
|
|
EvalContext: hclCtx,
|
|
|
|
})
|
|
|
|
return "", diags
|
|
|
|
}
|
|
|
|
|
|
|
|
val, valMarks := val.Unmark()
|
|
|
|
if _, sensitive := valMarks[marks.Sensitive]; sensitive {
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagWarning,
|
|
|
|
Summary: "Error message refers to sensitive values",
|
2023-09-21 07:38:46 -05:00
|
|
|
Detail: `The error expression used to explain this condition refers to sensitive values, so OpenTofu will not display the resulting message.
|
2022-06-15 20:00:20 -05:00
|
|
|
|
|
|
|
You can correct this by removing references to sensitive values, or by carefully using the nonsensitive() function if the expression will not reveal the sensitive data.`,
|
|
|
|
Subject: expr.Range().Ptr(),
|
|
|
|
Expression: expr,
|
|
|
|
EvalContext: hclCtx,
|
|
|
|
})
|
|
|
|
return "", diags
|
|
|
|
}
|
|
|
|
|
|
|
|
// NOTE: We've discarded any other marks the string might have been carrying,
|
|
|
|
// aside from the sensitive mark.
|
|
|
|
|
|
|
|
return strings.TrimSpace(val.AsString()), diags
|
2020-11-20 18:03:11 -06:00
|
|
|
}
|