mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-24 08:00:17 -06:00
0e4e9f7706
Previously we were attempting to infer the checkable object address kind of a given address by whether it included "output" in the position where a resource type name would otherwise go. That was already potentially risky because we've historically not prevented a resource type named "output", and it's also a forward-compatibility hazard in case we introduce additional object kinds with entirely-new addressing schemes in future. Given that, we'll instead always be explicit about what kind of address we're storing in a wire or file format, so that we can make sure to always use the intended parser when reading an address back into memory, or return an error if we encounter a kind we're not familiar with.
252 lines
7.7 KiB
Go
252 lines
7.7 KiB
Go
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.
|
|
type Check struct {
|
|
Container Checkable
|
|
Type CheckType
|
|
Index int
|
|
}
|
|
|
|
func NewCheck(container Checkable, typ CheckType, index int) Check {
|
|
return Check{
|
|
Container: container,
|
|
Type: typ,
|
|
Index: index,
|
|
}
|
|
}
|
|
|
|
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) UniqueKey() UniqueKey {
|
|
return checkKey{
|
|
ContainerKey: c.Container.UniqueKey(),
|
|
Type: c.Type,
|
|
Index: c.Index,
|
|
}
|
|
}
|
|
|
|
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"
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
path, remain, diags := parseModuleInstancePrefix(traversal)
|
|
if diags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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))
|
|
}
|
|
}
|