Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Checks: Refactor existing check addrs and add new check block addr #32733

Merged
merged 4 commits into from Mar 23, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
310 changes: 83 additions & 227 deletions internal/addrs/check.go
@@ -1,251 +1,107 @@
package addrs

import (
"fmt"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/internal/tfdiags"
)

// Check is the address of a check rule within a checkable object.
//
// This represents the check rule globally within a configuration, and is used
// during graph evaluation to identify a condition result object to update with
// the result of check rule evaluation.
//
// The check address is not distinct from resource traversals, and check rule
// values are not intended to be available to the language, so the address is
// not Referenceable.
//
// Note also that the check address is only relevant within the scope of a run,
// as reordering check blocks between runs will result in their addresses
// changing. Check is therefore for internal use only and should not be exposed
// in durable artifacts such as state snapshots.
import "fmt"

liamcervante marked this conversation as resolved.
Show resolved Hide resolved
type Check struct {
Container Checkable
Type CheckType
Index int
referenceable
Name string
}

func NewCheck(container Checkable, typ CheckType, index int) Check {
return Check{
Container: container,
Type: typ,
Index: index,
func (c Check) String() string {
return fmt.Sprintf("check.%s", c.Name)
}

func (c Check) InModule(modAddr Module) ConfigCheck {
return ConfigCheck{
Module: modAddr,
Check: c,
}
}

func (c Check) String() string {
container := c.Container.String()
switch c.Type {
case ResourcePrecondition:
return fmt.Sprintf("%s.precondition[%d]", container, c.Index)
case ResourcePostcondition:
return fmt.Sprintf("%s.postcondition[%d]", container, c.Index)
case OutputPrecondition:
return fmt.Sprintf("%s.precondition[%d]", container, c.Index)
default:
// This should not happen
return fmt.Sprintf("%s.condition[%d]", container, c.Index)
func (c Check) Absolute(modAddr ModuleInstance) AbsCheck {
return AbsCheck{
Module: modAddr,
Check: c,
}
}

func (c Check) Equal(o Check) bool {
return c.Name == o.Name
}

func (c Check) UniqueKey() UniqueKey {
return checkKey{
ContainerKey: c.Container.UniqueKey(),
Type: c.Type,
Index: c.Index,
}
return c // A Check is its own UniqueKey
}

type checkKey struct {
ContainerKey UniqueKey
Type CheckType
Index int
}

func (k checkKey) uniqueKeySigil() {}

// CheckType describes a category of check. We use this only to establish
// uniqueness for Check values, and do not expose this concept of "check types"
// (which is subject to change in future) in any durable artifacts such as
// state snapshots.
//
// (See [CheckableKind] for an enumeration that we _do_ use externally, to
// describe the type of object being checked rather than the type of the check
// itself.)
type CheckType int

//go:generate go run golang.org/x/tools/cmd/stringer -type=CheckType check.go

const (
InvalidCondition CheckType = 0
ResourcePrecondition CheckType = 1
ResourcePostcondition CheckType = 2
OutputPrecondition CheckType = 3
)

// Description returns a human-readable description of the check type. This is
// presented in the user interface through a diagnostic summary.
func (c CheckType) Description() string {
switch c {
case ResourcePrecondition:
return "Resource precondition"
case ResourcePostcondition:
return "Resource postcondition"
case OutputPrecondition:
return "Module output value precondition"
default:
// This should not happen
return "Condition"
}
func (c Check) uniqueKeySigil() {}

type ConfigCheck struct {
Module Module
Check Check
}

var _ ConfigCheckable = ConfigCheck{}

func (c ConfigCheck) UniqueKey() UniqueKey {
return configCheckUniqueKey(c.String())
}

func (c ConfigCheck) configCheckableSigil() {}

func (c ConfigCheck) CheckableKind() CheckableKind {
return CheckableCheck
}

// Checkable is an interface implemented by all address types that can contain
// condition blocks.
type Checkable interface {
UniqueKeyer

checkableSigil()

// Check returns the address of an individual check rule of a specified
// type and index within this checkable container.
Check(CheckType, int) Check

// ConfigCheckable returns the address of the configuration construct that
// this Checkable belongs to.
//
// Checkable objects can potentially be dynamically declared during a
// plan operation using constructs like resource for_each, and so
// ConfigCheckable gives us a way to talk about the static containers
// those dynamic objects belong to, in case we wish to group together
// dynamic checkable objects into their static checkable for reporting
// purposes.
ConfigCheckable() ConfigCheckable

CheckableKind() CheckableKind
String() string
}

var (
_ Checkable = AbsResourceInstance{}
_ Checkable = AbsOutputValue{}
)

// CheckableKind describes the different kinds of checkable objects.
type CheckableKind rune

//go:generate go run golang.org/x/tools/cmd/stringer -type=CheckableKind check.go

const (
CheckableKindInvalid CheckableKind = 0
CheckableResource CheckableKind = 'R'
CheckableOutputValue CheckableKind = 'O'
)

// ConfigCheckable is an interfaces implemented by address types that represent
// configuration constructs that can have Checkable addresses associated with
// them.
//
// This address type therefore in a sense represents a container for zero or
// more checkable objects all declared by the same configuration construct,
// so that we can talk about these groups of checkable objects before we're
// ready to decide how many checkable objects belong to each one.
type ConfigCheckable interface {
UniqueKeyer

configCheckableSigil()

CheckableKind() CheckableKind
String() string
}

var (
_ ConfigCheckable = ConfigResource{}
_ ConfigCheckable = ConfigOutputValue{}
)

// ParseCheckableStr attempts to parse the given string as a Checkable address
// of the given kind.
//
// This should be the opposite of Checkable.String for any Checkable address
// type, as long as "kind" is set to the value returned by the address's
// CheckableKind method.
//
// We do not typically expect users to write out checkable addresses as input,
// but we use them as part of some of our wire formats for persisting check
// results between runs.
func ParseCheckableStr(kind CheckableKind, src string) (Checkable, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics

traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(src), "", hcl.InitialPos)
diags = diags.Append(parseDiags)
if parseDiags.HasErrors() {
return nil, diags
func (c ConfigCheck) String() string {
if len(c.Module) == 0 {
return c.Check.String()
}
return fmt.Sprintf("%s.%s", c.Module, c.Check)
}

type AbsCheck struct {
Module ModuleInstance
Check Check
}

var _ Checkable = AbsCheck{}

path, remain, diags := parseModuleInstancePrefix(traversal)
if diags.HasErrors() {
return nil, diags
func (c AbsCheck) UniqueKey() UniqueKey {
return absCheckUniqueKey(c.String())
}

func (c AbsCheck) checkableSigil() {}

func (c AbsCheck) CheckRule(typ CheckRuleType, i int) CheckRule {
return CheckRule{
Container: c,
Type: typ,
Index: i,
}
}

if remain.IsRelative() {
// (relative means that there's either nothing left or what's next isn't an identifier)
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid checkable address",
Detail: "Module path must be followed by either a resource instance address or an output value address.",
Subject: remain.SourceRange().Ptr(),
})
return nil, diags
func (c AbsCheck) ConfigCheckable() ConfigCheckable {
return ConfigCheck{
Module: c.Module.Module(),
Check: c.Check,
}
}

func (c AbsCheck) CheckableKind() CheckableKind {
return CheckableCheck
}

// We use "kind" to disambiguate here because unfortunately we've
// historically never reserved "output" as a possible resource type name
// and so it is in principle possible -- albeit unlikely -- that there
// might be a resource whose type is literally "output".
switch kind {
case CheckableResource:
riAddr, moreDiags := parseResourceInstanceUnderModule(path, remain)
diags = diags.Append(moreDiags)
if diags.HasErrors() {
return nil, diags
}
return riAddr, diags

case CheckableOutputValue:
if len(remain) != 2 {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid checkable address",
Detail: "Output address must have only one attribute part after the keyword 'output', giving the name of the output value.",
Subject: remain.SourceRange().Ptr(),
})
return nil, diags
}
if remain.RootName() != "output" {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid checkable address",
Detail: "Output address must follow the module address with the keyword 'output'.",
Subject: remain.SourceRange().Ptr(),
})
return nil, diags
}
if step, ok := remain[1].(hcl.TraverseAttr); !ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid checkable address",
Detail: "Output address must have only one attribute part after the keyword 'output', giving the name of the output value.",
Subject: remain.SourceRange().Ptr(),
})
return nil, diags
} else {
return OutputValue{Name: step.Name}.Absolute(path), diags
}

default:
panic(fmt.Sprintf("unsupported CheckableKind %s", kind))
func (c AbsCheck) String() string {
if len(c.Module) == 0 {
return c.Check.String()
}
return fmt.Sprintf("%s.%s", c.Module, c.Check)
}

type configCheckUniqueKey string

func (k configCheckUniqueKey) uniqueKeySigil() {}

type absCheckUniqueKey string

func (k absCheckUniqueKey) uniqueKeySigil() {}