terraform/internal/terraform/variables.go

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
}