mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-21 06:02:58 -06:00
3785619f93
The "checks" package is an expansion what we previously called plans.Conditions to accommodate a new requirement that we be able to track which checks we're expecting to run even if we don't actually get around to running them, which will be helpful when we start using checks as part of our module testing story because test reporting tools appreciate there being a relatively consistent set of test cases from one run to the next. So far this should be essentially a no-op change from an external functionality standpoint, aside from some minor adjustments to how we report some of the error and warning cases from condition evaluation in light of the fact that the "checks" package can now track errors as a different outcome than a failure of a valid check. As is often the case with anything which changes what we track in the EvalContext and persist between plan and apply, Terraform Core is pretty brittle and so this had knock-on effects elsewhere too. Again, the goal is for these changes to not create any material externally-visible difference, and just to accommodate the new assumption that there will always be a "checks" object available for tracking during a graph walk.
238 lines
7.9 KiB
Go
238 lines
7.9 KiB
Go
package terraform
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/convert"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/checks"
|
|
"github.com/hashicorp/terraform/internal/configs"
|
|
"github.com/hashicorp/terraform/internal/instances"
|
|
"github.com/hashicorp/terraform/internal/lang"
|
|
"github.com/hashicorp/terraform/internal/lang/marks"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
)
|
|
|
|
// 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.
|
|
func evalCheckRules(typ addrs.CheckType, rules []*configs.CheckRule, ctx EvalContext, self addrs.Checkable, keyData instances.RepetitionData, diagSeverity tfdiags.Severity) tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
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
|
|
}
|
|
|
|
if len(rules) == 0 {
|
|
// Nothing to do
|
|
return nil
|
|
}
|
|
|
|
severity := diagSeverity.ToHCL()
|
|
|
|
for i, rule := range rules {
|
|
result, ruleDiags := evalCheckRule(typ, rule, ctx, self, keyData, severity)
|
|
diags = diags.Append(ruleDiags)
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
type checkResult struct {
|
|
Status checks.Status
|
|
FailureMessage string
|
|
}
|
|
|
|
func evalCheckRule(typ addrs.CheckType, rule *configs.CheckRule, ctx EvalContext, self addrs.Checkable, keyData instances.RepetitionData, severity hcl.DiagnosticSeverity) (checkResult, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
const errInvalidCondition = "Invalid condition result"
|
|
|
|
refs, moreDiags := lang.ReferencesInExpr(rule.Condition)
|
|
diags = diags.Append(moreDiags)
|
|
moreRefs, moreDiags := lang.ReferencesInExpr(rule.ErrorMessage)
|
|
diags = diags.Append(moreDiags)
|
|
refs = append(refs, moreRefs...)
|
|
|
|
var selfReference addrs.Referenceable
|
|
// Only resource postconditions can refer to self
|
|
if typ == addrs.ResourcePostcondition {
|
|
switch s := self.(type) {
|
|
case addrs.AbsResourceInstance:
|
|
selfReference = s.Resource
|
|
default:
|
|
panic(fmt.Sprintf("Invalid self reference type %t", self))
|
|
}
|
|
}
|
|
scope := ctx.EvaluationScope(selfReference, keyData)
|
|
|
|
hclCtx, moreDiags := scope.EvalContext(refs)
|
|
diags = diags.Append(moreDiags)
|
|
|
|
resultVal, hclDiags := rule.Condition.Value(hclCtx)
|
|
diags = diags.Append(hclDiags)
|
|
|
|
// 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.
|
|
errorMessage, moreDiags := evalCheckErrorMessage(rule.ErrorMessage, hclCtx)
|
|
diags = diags.Append(moreDiags)
|
|
|
|
if diags.HasErrors() {
|
|
log.Printf("[TRACE] evalCheckRule: %s: %s", typ, diags.Err().Error())
|
|
}
|
|
|
|
if !resultVal.IsKnown() {
|
|
// We'll wait until we've learned more, then.
|
|
return checkResult{Status: checks.StatusUnknown}, diags
|
|
}
|
|
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.
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: errInvalidCondition,
|
|
Detail: "Condition expression must return either true or false, not null.",
|
|
Subject: rule.Condition.Range().Ptr(),
|
|
Expression: rule.Condition,
|
|
EvalContext: hclCtx,
|
|
})
|
|
return checkResult{Status: checks.StatusError}, diags
|
|
}
|
|
var err error
|
|
resultVal, err = convert.Convert(resultVal, cty.Bool)
|
|
if err != nil {
|
|
// 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.
|
|
detail := fmt.Sprintf("Invalid condition result value: %s.", tfdiags.FormatError(err))
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: errInvalidCondition,
|
|
Detail: detail,
|
|
Subject: rule.Condition.Range().Ptr(),
|
|
Expression: rule.Condition,
|
|
EvalContext: hclCtx,
|
|
})
|
|
return checkResult{Status: checks.StatusError}, diags
|
|
}
|
|
|
|
// The condition result may be marked if the expression refers to a
|
|
// sensitive value.
|
|
resultVal, _ = resultVal.Unmark()
|
|
|
|
status := checks.StatusForCtyValue(resultVal)
|
|
|
|
if status != checks.StatusFail {
|
|
return checkResult{Status: status}, diags
|
|
}
|
|
|
|
errorMessageForDiags := errorMessage
|
|
if errorMessageForDiags == "" {
|
|
errorMessageForDiags = "This check failed, but has an invalid error message as described in the other accompanying messages."
|
|
}
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
// 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.
|
|
Severity: severity,
|
|
Summary: fmt.Sprintf("%s failed", typ.Description()),
|
|
Detail: errorMessageForDiags,
|
|
Subject: rule.Condition.Range().Ptr(),
|
|
Expression: rule.Condition,
|
|
EvalContext: hclCtx,
|
|
})
|
|
|
|
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",
|
|
Detail: `The error expression used to explain this condition refers to sensitive values, so Terraform will not display the resulting message.
|
|
|
|
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
|
|
}
|