github.com/opentofu/opentofu@v1.7.1/internal/tofu/variables.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package tofu
     7  
     8  import (
     9  	"fmt"
    10  
    11  	"github.com/zclconf/go-cty/cty"
    12  
    13  	"github.com/opentofu/opentofu/internal/configs"
    14  	"github.com/opentofu/opentofu/internal/tfdiags"
    15  )
    16  
    17  // InputValue represents a raw value for a root module input variable as
    18  // provided by the external caller into a function like tofu.Context.Plan.
    19  //
    20  // InputValue should represent as directly as possible what the user set the
    21  // variable to, without any attempt to convert the value to the variable's
    22  // type constraint or substitute the configured default values for variables
    23  // that wasn't set. Those adjustments will be handled by OpenTofu Core itself
    24  // as part of performing the requested operation.
    25  //
    26  // A OpenTofu Core caller must provide an InputValue object for each of the
    27  // variables declared in the root module, even if the end user didn't provide
    28  // an explicit value for some of them. See the Value field documentation for
    29  // how to handle that situation.
    30  //
    31  // OpenTofu Core also internally uses InputValue to represent the raw value
    32  // provided for a variable in a child module call, following the same
    33  // conventions. However, that's an implementation detail not visible to
    34  // outside callers.
    35  type InputValue struct {
    36  	// Value is the raw value as provided by the user as part of the plan
    37  	// options, or a corresponding similar data structure for non-plan
    38  	// operations.
    39  	//
    40  	// If a particular variable declared in the root module is _not_ set by
    41  	// the user then the caller must still provide an InputValue for it but
    42  	// must set Value to cty.NilVal to represent the absense of a value.
    43  	// This requirement is to help detect situations where the caller isn't
    44  	// correctly detecting and handling all of the declared variables.
    45  	//
    46  	// For historical reasons it's important that callers distinguish the
    47  	// situation of the value not being set at all (cty.NilVal) from the
    48  	// situation of it being explicitly set to null (a cty.NullVal result):
    49  	// for "nullable" input variables that distinction unfortunately decides
    50  	// whether the final value will be the variable's default or will be
    51  	// explicitly null.
    52  	Value cty.Value
    53  
    54  	// SourceType is a high-level category for where the value of Value
    55  	// came from, which OpenTofu Core uses to tailor some of its error
    56  	// messages to be more helpful to the user.
    57  	//
    58  	// Some SourceType values should be accompanied by a populated SourceRange
    59  	// value. See that field's documentation below for more information.
    60  	SourceType ValueSourceType
    61  
    62  	// SourceRange provides source location information for values whose
    63  	// SourceType is either ValueFromConfig, ValueFromNamedFile, or
    64  	// ValueForNormalFile. It is not populated for other source types, and so
    65  	// should not be used.
    66  	SourceRange tfdiags.SourceRange
    67  }
    68  
    69  // ValueSourceType describes what broad category of source location provided
    70  // a particular value.
    71  type ValueSourceType rune
    72  
    73  const (
    74  	// ValueFromUnknown is the zero value of ValueSourceType and is not valid.
    75  	ValueFromUnknown ValueSourceType = 0
    76  
    77  	// ValueFromConfig indicates that a value came from a .tf or .tf.json file,
    78  	// e.g. the default value defined for a variable.
    79  	ValueFromConfig ValueSourceType = 'C'
    80  
    81  	// ValueFromAutoFile indicates that a value came from a "values file", like
    82  	// a .tfvars file, that was implicitly loaded by naming convention.
    83  	ValueFromAutoFile ValueSourceType = 'F'
    84  
    85  	// ValueFromNamedFile indicates that a value came from a named "values file",
    86  	// like a .tfvars file, that was passed explicitly on the command line (e.g.
    87  	// -var-file=foo.tfvars).
    88  	ValueFromNamedFile ValueSourceType = 'N'
    89  
    90  	// ValueFromCLIArg indicates that the value was provided directly in
    91  	// a CLI argument. The name of this argument is not recorded and so it must
    92  	// be inferred from context.
    93  	ValueFromCLIArg ValueSourceType = 'A'
    94  
    95  	// ValueFromEnvVar indicates that the value was provided via an environment
    96  	// variable. The name of the variable is not recorded and so it must be
    97  	// inferred from context.
    98  	ValueFromEnvVar ValueSourceType = 'E'
    99  
   100  	// ValueFromInput indicates that the value was provided at an interactive
   101  	// input prompt.
   102  	ValueFromInput ValueSourceType = 'I'
   103  
   104  	// ValueFromPlan indicates that the value was retrieved from a stored plan.
   105  	ValueFromPlan ValueSourceType = 'P'
   106  
   107  	// ValueFromCaller indicates that the value was explicitly overridden by
   108  	// a caller to Context.SetVariable after the context was constructed.
   109  	ValueFromCaller ValueSourceType = 'S'
   110  )
   111  
   112  func (v *InputValue) GoString() string {
   113  	if (v.SourceRange != tfdiags.SourceRange{}) {
   114  		return fmt.Sprintf("&tofu.InputValue{Value: %#v, SourceType: %#v, SourceRange: %#v}", v.Value, v.SourceType, v.SourceRange)
   115  	} else {
   116  		return fmt.Sprintf("&tofu.InputValue{Value: %#v, SourceType: %#v}", v.Value, v.SourceType)
   117  	}
   118  }
   119  
   120  // HasSourceRange returns true if the reciever has a source type for which
   121  // we expect the SourceRange field to be populated with a valid range.
   122  func (v *InputValue) HasSourceRange() bool {
   123  	return v.SourceType.HasSourceRange()
   124  }
   125  
   126  // HasSourceRange returns true if the reciever is one of the source types
   127  // that is used along with a valid SourceRange field when appearing inside an
   128  // InputValue object.
   129  func (v ValueSourceType) HasSourceRange() bool {
   130  	switch v {
   131  	case ValueFromConfig, ValueFromAutoFile, ValueFromNamedFile:
   132  		return true
   133  	default:
   134  		return false
   135  	}
   136  }
   137  
   138  func (v ValueSourceType) GoString() string {
   139  	return fmt.Sprintf("tofu.%s", v)
   140  }
   141  
   142  //go:generate go run golang.org/x/tools/cmd/stringer -type ValueSourceType
   143  
   144  // InputValues is a map of InputValue instances.
   145  type InputValues map[string]*InputValue
   146  
   147  // InputValuesFromCaller turns the given map of naked values into an
   148  // InputValues that attributes each value to "a caller", using the source
   149  // type ValueFromCaller. This is primarily useful for testing purposes.
   150  //
   151  // This should not be used as a general way to convert map[string]cty.Value
   152  // into InputValues, since in most real cases we want to set a suitable
   153  // other SourceType and possibly SourceRange value.
   154  func InputValuesFromCaller(vals map[string]cty.Value) InputValues {
   155  	ret := make(InputValues, len(vals))
   156  	for k, v := range vals {
   157  		ret[k] = &InputValue{
   158  			Value:      v,
   159  			SourceType: ValueFromCaller,
   160  		}
   161  	}
   162  	return ret
   163  }
   164  
   165  // Override merges the given value maps with the receiver, overriding any
   166  // conflicting keys so that the latest definition wins.
   167  func (vv InputValues) Override(others ...InputValues) InputValues {
   168  	// FIXME: This should check to see if any of the values are maps and
   169  	// merge them if so, in order to preserve the behavior from prior to
   170  	// Terraform 0.12.
   171  	ret := make(InputValues)
   172  	for k, v := range vv {
   173  		ret[k] = v
   174  	}
   175  	for _, other := range others {
   176  		for k, v := range other {
   177  			ret[k] = v
   178  		}
   179  	}
   180  	return ret
   181  }
   182  
   183  // JustValues returns a map that just includes the values, discarding the
   184  // source information.
   185  func (vv InputValues) JustValues() map[string]cty.Value {
   186  	ret := make(map[string]cty.Value, len(vv))
   187  	for k, v := range vv {
   188  		ret[k] = v.Value
   189  	}
   190  	return ret
   191  }
   192  
   193  // SameValues returns true if the given InputValues has the same values as
   194  // the receiever, disregarding the source types and source ranges.
   195  //
   196  // Values are compared using the cty "RawEquals" method, which means that
   197  // unknown values can be considered equal to one another if they are of the
   198  // same type.
   199  func (vv InputValues) SameValues(other InputValues) bool {
   200  	if len(vv) != len(other) {
   201  		return false
   202  	}
   203  
   204  	for k, v := range vv {
   205  		ov, exists := other[k]
   206  		if !exists {
   207  			return false
   208  		}
   209  		if !v.Value.RawEquals(ov.Value) {
   210  			return false
   211  		}
   212  	}
   213  
   214  	return true
   215  }
   216  
   217  // HasValues returns true if the reciever has the same values as in the given
   218  // map, disregarding the source types and source ranges.
   219  //
   220  // Values are compared using the cty "RawEquals" method, which means that
   221  // unknown values can be considered equal to one another if they are of the
   222  // same type.
   223  func (vv InputValues) HasValues(vals map[string]cty.Value) bool {
   224  	if len(vv) != len(vals) {
   225  		return false
   226  	}
   227  
   228  	for k, v := range vv {
   229  		oVal, exists := vals[k]
   230  		if !exists {
   231  			return false
   232  		}
   233  		if !v.Value.RawEquals(oVal) {
   234  			return false
   235  		}
   236  	}
   237  
   238  	return true
   239  }
   240  
   241  // Identical returns true if the given InputValues has the same values,
   242  // source types, and source ranges as the receiver.
   243  //
   244  // Values are compared using the cty "RawEquals" method, which means that
   245  // unknown values can be considered equal to one another if they are of the
   246  // same type.
   247  //
   248  // This method is primarily for testing. For most practical purposes, it's
   249  // better to use SameValues or HasValues.
   250  func (vv InputValues) Identical(other InputValues) bool {
   251  	if len(vv) != len(other) {
   252  		return false
   253  	}
   254  
   255  	for k, v := range vv {
   256  		ov, exists := other[k]
   257  		if !exists {
   258  			return false
   259  		}
   260  		if !v.Value.RawEquals(ov.Value) {
   261  			return false
   262  		}
   263  		if v.SourceType != ov.SourceType {
   264  			return false
   265  		}
   266  		if v.SourceRange != ov.SourceRange {
   267  			return false
   268  		}
   269  	}
   270  
   271  	return true
   272  }
   273  
   274  // checkInputVariables ensures that the caller provided an InputValue
   275  // definition for each root module variable declared in the configuration.
   276  // The caller must provide an InputVariables with keys exactly matching
   277  // the declared variables, though some of them may be marked explicitly
   278  // unset by their values being cty.NilVal.
   279  //
   280  // This doesn't perform any type checking, default value substitution, or
   281  // validation checks. Those are all handled during a graph walk when we
   282  // visit the graph nodes representing each root variable.
   283  //
   284  // The set of values is considered valid only if the returned diagnostics
   285  // does not contain errors. A valid set of values may still produce warnings,
   286  // which should be returned to the user.
   287  func checkInputVariables(vcs map[string]*configs.Variable, vs InputValues) tfdiags.Diagnostics {
   288  	var diags tfdiags.Diagnostics
   289  
   290  	for name := range vcs {
   291  		_, isSet := vs[name]
   292  		if !isSet {
   293  			// Always an error, since the caller should have produced an
   294  			// item with Value: cty.NilVal to be explicit that it offered
   295  			// an opportunity to set this variable.
   296  			diags = diags.Append(tfdiags.Sourceless(
   297  				tfdiags.Error,
   298  				"Unassigned variable",
   299  				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),
   300  			))
   301  			continue
   302  		}
   303  	}
   304  
   305  	// Check for any variables that are assigned without being configured.
   306  	// This is always an implementation error in the caller, because we
   307  	// expect undefined variables to be caught during context construction
   308  	// where there is better context to report it well.
   309  	for name := range vs {
   310  		if _, defined := vcs[name]; !defined {
   311  			diags = diags.Append(tfdiags.Sourceless(
   312  				tfdiags.Error,
   313  				"Value assigned to undeclared variable",
   314  				fmt.Sprintf("A value was assigned to an undeclared input variable %q.", name),
   315  			))
   316  		}
   317  	}
   318  
   319  	return diags
   320  }