Files
opentf/internal/addrs/checkable.go
Martin Atkins e74bf2d0a1 go.mod: Use the new "tool" directive
Previously the Go toolchain had no explicit support for "tools" and so we
used the typical Go community workaround of adding "tools.go" files (two,
for some reason) that existed only to trick the Go toolchain into
considering the tools as dependencies we could track in go.mod.

Go 1.24 introduced explicit support for tracking tools as part of go.mod,
and the ability to run those using "go tool" instead of "go run", and so
this commit switches us over to using that strategy for everything we were
previously managing in tools.go.

There are some intentional exceptions here:

- The protobuf-compile script can't use "go tool" or "go run" because the
  tools in question are run only indirectly through protoc. However, we
  do still use the "tool" directive in go.mod to tell the Go toolchain that
  we depend on those tools, so that it'll track which versions we are
  currently using as part of go.mod.
- Our golangci-lint Makefile target uses "go run" to run a specific
  version of golangci-lint. We _intentionally_ don't consider that tool
  to be a direct dependency of OpenTofu because it has a lot of indirect
  dependencies that would pollute our go.mod file. Therefore that continues
  to use "go run" after this commit.
- Both of our tools.go files previously referred to
  github.com/nishanths/exhaustive , but nothing actually appears to be
  using that tool in the current OpenTofu tree, so it's no longer a
  dependency after this commit.

All of the dependencies we have _only_ for tools are now classified as
"indirect" in the go.mod file. This is the default behavior of the Go
toolchain and appears to be motivated by making it clearer that these
modules do not contribute anything to the runtime behavior of OpenTofu.
This also corrected a historical oddity in our go.mod where for some reason
the "indirect" dependencies had been split across two different "require"
directives; they are now all grouped together in a single directive.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-10-10 07:06:56 -03:00

197 lines
6.2 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 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 tool 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))
}
}