opentofu/internal/configs/checks.go

261 lines
7.7 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package configs
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/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
exprs := []hcl.Expression{
cr.Condition,
cr.ErrorMessage,
}
for _, expr := range exprs {
if expr == nil {
continue
}
refs, _ := lang.References(addrs.ParseRef, expr.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: expr.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,
},
},
}
// Check represents a configuration defined check block.
//
// A check block contains 0-1 data blocks, and 0-n assert blocks. The check
// block will load the data block, and execute the assert blocks as check rules
// during the plan and apply Terraform operations.
type Check struct {
Name string
DataResource *Resource
Asserts []*CheckRule
DeclRange hcl.Range
}
func (c Check) Addr() addrs.Check {
return addrs.Check{
Name: c.Name,
}
}
func (c Check) Accessible(addr addrs.Referenceable) bool {
if check, ok := addr.(addrs.Check); ok {
return check.Equal(c.Addr())
}
return false
}
func decodeCheckBlock(block *hcl.Block, override bool) (*Check, hcl.Diagnostics) {
var diags hcl.Diagnostics
check := &Check{
Name: block.Labels[0],
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: "Can't override check blocks",
Detail: "Override files cannot override check blocks.",
Subject: check.DeclRange.Ptr(),
})
return check, diags
}
content, moreDiags := block.Body.Content(checkBlockSchema)
diags = append(diags, moreDiags...)
if !hclsyntax.ValidIdentifier(check.Name) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid check block name",
Detail: badIdentifierDetail,
Subject: &block.LabelRanges[0],
})
}
for _, block := range content.Blocks {
switch block.Type {
case "data":
if check.DataResource != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Multiple data resource blocks",
Detail: fmt.Sprintf("This check block already has a data resource defined at %s.", check.DataResource.DeclRange.Ptr()),
Subject: block.DefRange.Ptr(),
})
continue
}
data, moreDiags := decodeDataBlock(block, override, true)
diags = append(diags, moreDiags...)
if !moreDiags.HasErrors() {
// Connect this data block back up to this check block.
data.Container = check
// Finally, save the data block.
check.DataResource = data
}
case "assert":
assert, moreDiags := decodeCheckRuleBlock(block, override)
diags = append(diags, moreDiags...)
if !moreDiags.HasErrors() {
check.Asserts = append(check.Asserts, assert)
}
default:
panic(fmt.Sprintf("unhandled check nested block %q", block.Type))
}
}
if len(check.Asserts) == 0 {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Zero assert blocks",
Detail: "Check blocks must have at least one assert block.",
Subject: check.DeclRange.Ptr(),
})
}
return check, diags
}
var checkBlockSchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{Type: "data", LabelNames: []string{"type", "name"}},
{Type: "assert"},
},
}