opentofu/internal/addrs/checkable.go

195 lines
6.1 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package addrs
import (
"fmt"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/opentofu/opentofu/internal/tfdiags"
)
// Checkable is an interface implemented by all address types that can contain
// condition blocks.
type Checkable interface {
UniqueKeyer
checkableSigil()
// CheckRule returns the address of an individual check rule of a specified
// type and index within this checkable container.
CheckRule(CheckRuleType, int) CheckRule
// 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 checkable.go
const (
CheckableKindInvalid CheckableKind = 0
CheckableResource CheckableKind = 'R'
CheckableOutputValue CheckableKind = 'O'
CheckableCheck CheckableKind = 'C'
CheckableInputVariable CheckableKind = 'I'
)
// 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
}
getCheckableName := func(keyword string, descriptor string) (string, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
var name string
if len(remain) != 2 {
diags = diags.Append(hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid checkable address",
Detail: fmt.Sprintf("%s address must have only one attribute part after the keyword '%s', giving the name of the %s.", cases.Title(language.English, cases.NoLower).String(keyword), keyword, descriptor),
Subject: remain.SourceRange().Ptr(),
})
}
if remain.RootName() != keyword {
diags = diags.Append(hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid checkable address",
Detail: fmt.Sprintf("%s address must follow the module address with the keyword '%s'.", cases.Title(language.English, cases.NoLower).String(keyword), keyword),
Subject: remain.SourceRange().Ptr(),
})
}
if step, ok := remain[1].(hcl.TraverseAttr); !ok {
diags = diags.Append(hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid checkable address",
Detail: fmt.Sprintf("%s address must have only one attribute part after the keyword '%s', giving the name of the %s.", cases.Title(language.English, cases.NoLower).String(keyword), keyword, descriptor),
Subject: remain.SourceRange().Ptr(),
})
} else {
name = step.Name
}
return name, 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:
name, nameDiags := getCheckableName("output", "output value")
diags = diags.Append(nameDiags)
if diags.HasErrors() {
return nil, diags
}
return OutputValue{Name: name}.Absolute(path), diags
case CheckableCheck:
name, nameDiags := getCheckableName("check", "check block")
diags = diags.Append(nameDiags)
if diags.HasErrors() {
return nil, diags
}
return Check{Name: name}.Absolute(path), diags
case CheckableInputVariable:
name, nameDiags := getCheckableName("var", "variable value")
diags = diags.Append(nameDiags)
if diags.HasErrors() {
return nil, diags
}
return InputVariable{Name: name}.Absolute(path), diags
default:
panic(fmt.Sprintf("unsupported CheckableKind %s", kind))
}
}