opentofu/internal/terraform/variables.go
Martin Atkins 9ebc3e1cd2 core: More accurate error message for invalid variable values
In earlier Terraform versions we had an extra validation step prior to
the graph walk which tried to partially validate root module input
variable values (just checking their type constraints) and then return
error messages which specified as accurately as possible where the value
had originally come from.

We're now handling that sort of validation exclusively during the graph
walk so that we can share the main logic between both root module and
child module variable values, but previously that shared code wasn't
able to generate such specific information about where the values had
originated, because it was adapted from code originally written to only
deal with child module variables.

Here then we restore a similar level of detail as before, when we're
processing root module variables. For child module variables, we use
synthetic InputValue objects which state that the value was declared
in the configuration, thus causing us to produce a similar sort of error
message as we would've before which includes a source range covering
the argument expression in the calling module block.
2022-01-10 12:26:54 -08:00

316 lines
11 KiB
Go

package terraform
import (
"fmt"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// InputValue represents a raw value for a root module input variable as
// provided by the external caller into a function like terraform.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 Terraform Core itself
// as part of performing the requested operation.
//
// A Terraform 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.
//
// Terraform 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 absense 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 Terraform 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("&terraform.InputValue{Value: %#v, SourceType: %#v, SourceRange: %#v}", v.Value, v.SourceType, v.SourceRange)
} else {
return fmt.Sprintf("&terraform.InputValue{Value: %#v, SourceType: %#v}", v.Value, v.SourceType)
}
}
// HasSourceRange returns true if the reciever 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 reciever 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("terraform.%s", v)
}
//go:generate go run 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 receiever, 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 reciever 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 Terraform; 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
}