mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 09:48:32 -05:00
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>
321 lines
11 KiB
Go
321 lines
11 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 tofu
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/opentofu/opentofu/internal/configs"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
)
|
|
|
|
// InputValue represents a raw value for a root module input variable as
|
|
// provided by the external caller into a function like tofu.Context.Plan.
|
|
//
|
|
// InputValue should represent as directly as possible what the user set the
|
|
// variable to, without any attempt to convert the value to the variable's
|
|
// type constraint or substitute the configured default values for variables
|
|
// that wasn't set. Those adjustments will be handled by OpenTofu Core itself
|
|
// as part of performing the requested operation.
|
|
//
|
|
// A OpenTofu Core caller must provide an InputValue object for each of the
|
|
// variables declared in the root module, even if the end user didn't provide
|
|
// an explicit value for some of them. See the Value field documentation for
|
|
// how to handle that situation.
|
|
//
|
|
// OpenTofu Core also internally uses InputValue to represent the raw value
|
|
// provided for a variable in a child module call, following the same
|
|
// conventions. However, that's an implementation detail not visible to
|
|
// outside callers.
|
|
type InputValue struct {
|
|
// Value is the raw value as provided by the user as part of the plan
|
|
// options, or a corresponding similar data structure for non-plan
|
|
// operations.
|
|
//
|
|
// If a particular variable declared in the root module is _not_ set by
|
|
// the user then the caller must still provide an InputValue for it but
|
|
// must set Value to cty.NilVal to represent the absence of a value.
|
|
// This requirement is to help detect situations where the caller isn't
|
|
// correctly detecting and handling all of the declared variables.
|
|
//
|
|
// For historical reasons it's important that callers distinguish the
|
|
// situation of the value not being set at all (cty.NilVal) from the
|
|
// situation of it being explicitly set to null (a cty.NullVal result):
|
|
// for "nullable" input variables that distinction unfortunately decides
|
|
// whether the final value will be the variable's default or will be
|
|
// explicitly null.
|
|
Value cty.Value
|
|
|
|
// SourceType is a high-level category for where the value of Value
|
|
// came from, which OpenTofu Core uses to tailor some of its error
|
|
// messages to be more helpful to the user.
|
|
//
|
|
// Some SourceType values should be accompanied by a populated SourceRange
|
|
// value. See that field's documentation below for more information.
|
|
SourceType ValueSourceType
|
|
|
|
// SourceRange provides source location information for values whose
|
|
// SourceType is either ValueFromConfig, ValueFromNamedFile, or
|
|
// ValueForNormalFile. It is not populated for other source types, and so
|
|
// should not be used.
|
|
SourceRange tfdiags.SourceRange
|
|
}
|
|
|
|
// ValueSourceType describes what broad category of source location provided
|
|
// a particular value.
|
|
type ValueSourceType rune
|
|
|
|
const (
|
|
// ValueFromUnknown is the zero value of ValueSourceType and is not valid.
|
|
ValueFromUnknown ValueSourceType = 0
|
|
|
|
// ValueFromConfig indicates that a value came from a .tf or .tf.json file,
|
|
// e.g. the default value defined for a variable.
|
|
ValueFromConfig ValueSourceType = 'C'
|
|
|
|
// ValueFromAutoFile indicates that a value came from a "values file", like
|
|
// a .tfvars file, that was implicitly loaded by naming convention.
|
|
ValueFromAutoFile ValueSourceType = 'F'
|
|
|
|
// ValueFromNamedFile indicates that a value came from a named "values file",
|
|
// like a .tfvars file, that was passed explicitly on the command line (e.g.
|
|
// -var-file=foo.tfvars).
|
|
ValueFromNamedFile ValueSourceType = 'N'
|
|
|
|
// ValueFromCLIArg indicates that the value was provided directly in
|
|
// a CLI argument. The name of this argument is not recorded and so it must
|
|
// be inferred from context.
|
|
ValueFromCLIArg ValueSourceType = 'A'
|
|
|
|
// ValueFromEnvVar indicates that the value was provided via an environment
|
|
// variable. The name of the variable is not recorded and so it must be
|
|
// inferred from context.
|
|
ValueFromEnvVar ValueSourceType = 'E'
|
|
|
|
// ValueFromInput indicates that the value was provided at an interactive
|
|
// input prompt.
|
|
ValueFromInput ValueSourceType = 'I'
|
|
|
|
// ValueFromPlan indicates that the value was retrieved from a stored plan.
|
|
ValueFromPlan ValueSourceType = 'P'
|
|
|
|
// ValueFromCaller indicates that the value was explicitly overridden by
|
|
// a caller to Context.SetVariable after the context was constructed.
|
|
ValueFromCaller ValueSourceType = 'S'
|
|
)
|
|
|
|
func (v *InputValue) GoString() string {
|
|
if (v.SourceRange != tfdiags.SourceRange{}) {
|
|
return fmt.Sprintf("&tofu.InputValue{Value: %#v, SourceType: %#v, SourceRange: %#v}", v.Value, v.SourceType, v.SourceRange)
|
|
} else {
|
|
return fmt.Sprintf("&tofu.InputValue{Value: %#v, SourceType: %#v}", v.Value, v.SourceType)
|
|
}
|
|
}
|
|
|
|
// HasSourceRange returns true if the receiver has a source type for which
|
|
// we expect the SourceRange field to be populated with a valid range.
|
|
func (v *InputValue) HasSourceRange() bool {
|
|
return v.SourceType.HasSourceRange()
|
|
}
|
|
|
|
// HasSourceRange returns true if the receiver is one of the source types
|
|
// that is used along with a valid SourceRange field when appearing inside an
|
|
// InputValue object.
|
|
func (v ValueSourceType) HasSourceRange() bool {
|
|
switch v {
|
|
case ValueFromConfig, ValueFromAutoFile, ValueFromNamedFile:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (v ValueSourceType) GoString() string {
|
|
return fmt.Sprintf("tofu.%s", v)
|
|
}
|
|
|
|
//go:generate go tool golang.org/x/tools/cmd/stringer -type ValueSourceType
|
|
|
|
// InputValues is a map of InputValue instances.
|
|
type InputValues map[string]*InputValue
|
|
|
|
// InputValuesFromCaller turns the given map of naked values into an
|
|
// InputValues that attributes each value to "a caller", using the source
|
|
// type ValueFromCaller. This is primarily useful for testing purposes.
|
|
//
|
|
// This should not be used as a general way to convert map[string]cty.Value
|
|
// into InputValues, since in most real cases we want to set a suitable
|
|
// other SourceType and possibly SourceRange value.
|
|
func InputValuesFromCaller(vals map[string]cty.Value) InputValues {
|
|
ret := make(InputValues, len(vals))
|
|
for k, v := range vals {
|
|
ret[k] = &InputValue{
|
|
Value: v,
|
|
SourceType: ValueFromCaller,
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// Override merges the given value maps with the receiver, overriding any
|
|
// conflicting keys so that the latest definition wins.
|
|
func (vv InputValues) Override(others ...InputValues) InputValues {
|
|
// FIXME: This should check to see if any of the values are maps and
|
|
// merge them if so, in order to preserve the behavior from prior to
|
|
// Terraform 0.12.
|
|
ret := make(InputValues)
|
|
for k, v := range vv {
|
|
ret[k] = v
|
|
}
|
|
for _, other := range others {
|
|
for k, v := range other {
|
|
ret[k] = v
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// JustValues returns a map that just includes the values, discarding the
|
|
// source information.
|
|
func (vv InputValues) JustValues() map[string]cty.Value {
|
|
ret := make(map[string]cty.Value, len(vv))
|
|
for k, v := range vv {
|
|
ret[k] = v.Value
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// SameValues returns true if the given InputValues has the same values as
|
|
// the receiver, disregarding the source types and source ranges.
|
|
//
|
|
// Values are compared using the cty "RawEquals" method, which means that
|
|
// unknown values can be considered equal to one another if they are of the
|
|
// same type.
|
|
func (vv InputValues) SameValues(other InputValues) bool {
|
|
if len(vv) != len(other) {
|
|
return false
|
|
}
|
|
|
|
for k, v := range vv {
|
|
ov, exists := other[k]
|
|
if !exists {
|
|
return false
|
|
}
|
|
if !v.Value.RawEquals(ov.Value) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// HasValues returns true if the receiver has the same values as in the given
|
|
// map, disregarding the source types and source ranges.
|
|
//
|
|
// Values are compared using the cty "RawEquals" method, which means that
|
|
// unknown values can be considered equal to one another if they are of the
|
|
// same type.
|
|
func (vv InputValues) HasValues(vals map[string]cty.Value) bool {
|
|
if len(vv) != len(vals) {
|
|
return false
|
|
}
|
|
|
|
for k, v := range vv {
|
|
oVal, exists := vals[k]
|
|
if !exists {
|
|
return false
|
|
}
|
|
if !v.Value.RawEquals(oVal) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Identical returns true if the given InputValues has the same values,
|
|
// source types, and source ranges as the receiver.
|
|
//
|
|
// Values are compared using the cty "RawEquals" method, which means that
|
|
// unknown values can be considered equal to one another if they are of the
|
|
// same type.
|
|
//
|
|
// This method is primarily for testing. For most practical purposes, it's
|
|
// better to use SameValues or HasValues.
|
|
func (vv InputValues) Identical(other InputValues) bool {
|
|
if len(vv) != len(other) {
|
|
return false
|
|
}
|
|
|
|
for k, v := range vv {
|
|
ov, exists := other[k]
|
|
if !exists {
|
|
return false
|
|
}
|
|
if !v.Value.RawEquals(ov.Value) {
|
|
return false
|
|
}
|
|
if v.SourceType != ov.SourceType {
|
|
return false
|
|
}
|
|
if v.SourceRange != ov.SourceRange {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// checkInputVariables ensures that the caller provided an InputValue
|
|
// definition for each root module variable declared in the configuration.
|
|
// The caller must provide an InputVariables with keys exactly matching
|
|
// the declared variables, though some of them may be marked explicitly
|
|
// unset by their values being cty.NilVal.
|
|
//
|
|
// This doesn't perform any type checking, default value substitution, or
|
|
// validation checks. Those are all handled during a graph walk when we
|
|
// visit the graph nodes representing each root variable.
|
|
//
|
|
// The set of values is considered valid only if the returned diagnostics
|
|
// does not contain errors. A valid set of values may still produce warnings,
|
|
// which should be returned to the user.
|
|
func checkInputVariables(vcs map[string]*configs.Variable, vs InputValues) tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
for name := range vcs {
|
|
_, isSet := vs[name]
|
|
if !isSet {
|
|
// Always an error, since the caller should have produced an
|
|
// item with Value: cty.NilVal to be explicit that it offered
|
|
// an opportunity to set this variable.
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Unassigned variable",
|
|
fmt.Sprintf("The input variable %q has not been assigned a value. This is a bug in OpenTofu; please report it in a GitHub issue.", name),
|
|
))
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Check for any variables that are assigned without being configured.
|
|
// This is always an implementation error in the caller, because we
|
|
// expect undefined variables to be caught during context construction
|
|
// where there is better context to report it well.
|
|
for name := range vs {
|
|
if _, defined := vcs[name]; !defined {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Value assigned to undeclared variable",
|
|
fmt.Sprintf("A value was assigned to an undeclared input variable %q.", name),
|
|
))
|
|
}
|
|
}
|
|
|
|
return diags
|
|
}
|