github.com/hashicorp/terraform-plugin-sdk@v1.17.2/terraform/variables.go (about)

     1  package terraform
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/hashicorp/hcl/v2"
     7  	"github.com/zclconf/go-cty/cty"
     8  	"github.com/zclconf/go-cty/cty/convert"
     9  
    10  	"github.com/hashicorp/terraform-plugin-sdk/internal/configs"
    11  	"github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags"
    12  )
    13  
    14  // InputValue represents a value for a variable in the root module, provided
    15  // as part of the definition of an operation.
    16  type InputValue struct {
    17  	Value      cty.Value
    18  	SourceType ValueSourceType
    19  
    20  	// SourceRange provides source location information for values whose
    21  	// SourceType is either ValueFromConfig or ValueFromFile. It is not
    22  	// populated for other source types, and so should not be used.
    23  	SourceRange tfdiags.SourceRange
    24  }
    25  
    26  // ValueSourceType describes what broad category of source location provided
    27  // a particular value.
    28  type ValueSourceType rune
    29  
    30  const (
    31  	// ValueFromUnknown is the zero value of ValueSourceType and is not valid.
    32  	ValueFromUnknown ValueSourceType = 0
    33  
    34  	// ValueFromConfig indicates that a value came from a .tf or .tf.json file,
    35  	// e.g. the default value defined for a variable.
    36  	ValueFromConfig ValueSourceType = 'C'
    37  
    38  	// ValueFromAutoFile indicates that a value came from a "values file", like
    39  	// a .tfvars file, that was implicitly loaded by naming convention.
    40  	ValueFromAutoFile ValueSourceType = 'F'
    41  
    42  	// ValueFromNamedFile indicates that a value came from a named "values file",
    43  	// like a .tfvars file, that was passed explicitly on the command line (e.g.
    44  	// -var-file=foo.tfvars).
    45  	ValueFromNamedFile ValueSourceType = 'N'
    46  
    47  	// ValueFromCLIArg indicates that the value was provided directly in
    48  	// a CLI argument. The name of this argument is not recorded and so it must
    49  	// be inferred from context.
    50  	ValueFromCLIArg ValueSourceType = 'A'
    51  
    52  	// ValueFromEnvVar indicates that the value was provided via an environment
    53  	// variable. The name of the variable is not recorded and so it must be
    54  	// inferred from context.
    55  	ValueFromEnvVar ValueSourceType = 'E'
    56  
    57  	// ValueFromInput indicates that the value was provided at an interactive
    58  	// input prompt.
    59  	ValueFromInput ValueSourceType = 'I'
    60  
    61  	// ValueFromPlan indicates that the value was retrieved from a stored plan.
    62  	ValueFromPlan ValueSourceType = 'P'
    63  
    64  	// ValueFromCaller indicates that the value was explicitly overridden by
    65  	// a caller to Context.SetVariable after the context was constructed.
    66  	ValueFromCaller ValueSourceType = 'S'
    67  )
    68  
    69  func (v *InputValue) GoString() string {
    70  	if (v.SourceRange != tfdiags.SourceRange{}) {
    71  		return fmt.Sprintf("&terraform.InputValue{Value: %#v, SourceType: %#v, SourceRange: %#v}", v.Value, v.SourceType, v.SourceRange)
    72  	} else {
    73  		return fmt.Sprintf("&terraform.InputValue{Value: %#v, SourceType: %#v}", v.Value, v.SourceType)
    74  	}
    75  }
    76  
    77  func (v ValueSourceType) GoString() string {
    78  	return fmt.Sprintf("terraform.%s", v)
    79  }
    80  
    81  //go:generate go run golang.org/x/tools/cmd/stringer -type ValueSourceType
    82  
    83  // InputValues is a map of InputValue instances.
    84  type InputValues map[string]*InputValue
    85  
    86  // InputValuesFromCaller turns the given map of naked values into an
    87  // InputValues that attributes each value to "a caller", using the source
    88  // type ValueFromCaller. This is primarily useful for testing purposes.
    89  //
    90  // This should not be used as a general way to convert map[string]cty.Value
    91  // into InputValues, since in most real cases we want to set a suitable
    92  // other SourceType and possibly SourceRange value.
    93  func InputValuesFromCaller(vals map[string]cty.Value) InputValues {
    94  	ret := make(InputValues, len(vals))
    95  	for k, v := range vals {
    96  		ret[k] = &InputValue{
    97  			Value:      v,
    98  			SourceType: ValueFromCaller,
    99  		}
   100  	}
   101  	return ret
   102  }
   103  
   104  // Override merges the given value maps with the receiver, overriding any
   105  // conflicting keys so that the latest definition wins.
   106  func (vv InputValues) Override(others ...InputValues) InputValues {
   107  	// FIXME: This should check to see if any of the values are maps and
   108  	// merge them if so, in order to preserve the behavior from prior to
   109  	// Terraform 0.12.
   110  	ret := make(InputValues)
   111  	for k, v := range vv {
   112  		ret[k] = v
   113  	}
   114  	for _, other := range others {
   115  		for k, v := range other {
   116  			ret[k] = v
   117  		}
   118  	}
   119  	return ret
   120  }
   121  
   122  // JustValues returns a map that just includes the values, discarding the
   123  // source information.
   124  func (vv InputValues) JustValues() map[string]cty.Value {
   125  	ret := make(map[string]cty.Value, len(vv))
   126  	for k, v := range vv {
   127  		ret[k] = v.Value
   128  	}
   129  	return ret
   130  }
   131  
   132  // DefaultVariableValues returns an InputValues map representing the default
   133  // values specified for variables in the given configuration map.
   134  func DefaultVariableValues(configs map[string]*configs.Variable) InputValues {
   135  	ret := make(InputValues)
   136  	for k, c := range configs {
   137  		if c.Default == cty.NilVal {
   138  			continue
   139  		}
   140  		ret[k] = &InputValue{
   141  			Value:       c.Default,
   142  			SourceType:  ValueFromConfig,
   143  			SourceRange: tfdiags.SourceRangeFromHCL(c.DeclRange),
   144  		}
   145  	}
   146  	return ret
   147  }
   148  
   149  // SameValues returns true if the given InputValues has the same values as
   150  // the receiever, disregarding the source types and source ranges.
   151  //
   152  // Values are compared using the cty "RawEquals" method, which means that
   153  // unknown values can be considered equal to one another if they are of the
   154  // same type.
   155  func (vv InputValues) SameValues(other InputValues) bool {
   156  	if len(vv) != len(other) {
   157  		return false
   158  	}
   159  
   160  	for k, v := range vv {
   161  		ov, exists := other[k]
   162  		if !exists {
   163  			return false
   164  		}
   165  		if !v.Value.RawEquals(ov.Value) {
   166  			return false
   167  		}
   168  	}
   169  
   170  	return true
   171  }
   172  
   173  // HasValues returns true if the reciever has the same values as in the given
   174  // map, disregarding the source types and source ranges.
   175  //
   176  // Values are compared using the cty "RawEquals" method, which means that
   177  // unknown values can be considered equal to one another if they are of the
   178  // same type.
   179  func (vv InputValues) HasValues(vals map[string]cty.Value) bool {
   180  	if len(vv) != len(vals) {
   181  		return false
   182  	}
   183  
   184  	for k, v := range vv {
   185  		oVal, exists := vals[k]
   186  		if !exists {
   187  			return false
   188  		}
   189  		if !v.Value.RawEquals(oVal) {
   190  			return false
   191  		}
   192  	}
   193  
   194  	return true
   195  }
   196  
   197  // Identical returns true if the given InputValues has the same values,
   198  // source types, and source ranges as the receiver.
   199  //
   200  // Values are compared using the cty "RawEquals" method, which means that
   201  // unknown values can be considered equal to one another if they are of the
   202  // same type.
   203  //
   204  // This method is primarily for testing. For most practical purposes, it's
   205  // better to use SameValues or HasValues.
   206  func (vv InputValues) Identical(other InputValues) bool {
   207  	if len(vv) != len(other) {
   208  		return false
   209  	}
   210  
   211  	for k, v := range vv {
   212  		ov, exists := other[k]
   213  		if !exists {
   214  			return false
   215  		}
   216  		if !v.Value.RawEquals(ov.Value) {
   217  			return false
   218  		}
   219  		if v.SourceType != ov.SourceType {
   220  			return false
   221  		}
   222  		if v.SourceRange != ov.SourceRange {
   223  			return false
   224  		}
   225  	}
   226  
   227  	return true
   228  }
   229  
   230  // checkInputVariables ensures that variable values supplied at the UI conform
   231  // to their corresponding declarations in configuration.
   232  //
   233  // The set of values is considered valid only if the returned diagnostics
   234  // does not contain errors. A valid set of values may still produce warnings,
   235  // which should be returned to the user.
   236  func checkInputVariables(vcs map[string]*configs.Variable, vs InputValues) tfdiags.Diagnostics {
   237  	var diags tfdiags.Diagnostics
   238  
   239  	for name, vc := range vcs {
   240  		val, isSet := vs[name]
   241  		if !isSet {
   242  			// Always an error, since the caller should already have included
   243  			// default values from the configuration in the values map.
   244  			diags = diags.Append(tfdiags.Sourceless(
   245  				tfdiags.Error,
   246  				"Unassigned variable",
   247  				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),
   248  			))
   249  			continue
   250  		}
   251  
   252  		wantType := vc.Type
   253  
   254  		// A given value is valid if it can convert to the desired type.
   255  		_, err := convert.Convert(val.Value, wantType)
   256  		if err != nil {
   257  			switch val.SourceType {
   258  			case ValueFromConfig, ValueFromAutoFile, ValueFromNamedFile:
   259  				// We have source location information for these.
   260  				diags = diags.Append(&hcl.Diagnostic{
   261  					Severity: hcl.DiagError,
   262  					Summary:  "Invalid value for input variable",
   263  					Detail:   fmt.Sprintf("The given value is not valid for variable %q: %s.", name, err),
   264  					Subject:  val.SourceRange.ToHCL().Ptr(),
   265  				})
   266  			case ValueFromEnvVar:
   267  				diags = diags.Append(tfdiags.Sourceless(
   268  					tfdiags.Error,
   269  					"Invalid value for input variable",
   270  					fmt.Sprintf("The environment variable TF_VAR_%s does not contain a valid value for variable %q: %s.", name, name, err),
   271  				))
   272  			case ValueFromCLIArg:
   273  				diags = diags.Append(tfdiags.Sourceless(
   274  					tfdiags.Error,
   275  					"Invalid value for input variable",
   276  					fmt.Sprintf("The argument -var=\"%s=...\" does not contain a valid value for variable %q: %s.", name, name, err),
   277  				))
   278  			case ValueFromInput:
   279  				diags = diags.Append(tfdiags.Sourceless(
   280  					tfdiags.Error,
   281  					"Invalid value for input variable",
   282  					fmt.Sprintf("The value entered for variable %q is not valid: %s.", name, err),
   283  				))
   284  			default:
   285  				// The above gets us good coverage for the situations users
   286  				// are likely to encounter with their own inputs. The other
   287  				// cases are generally implementation bugs, so we'll just
   288  				// use a generic error for these.
   289  				diags = diags.Append(tfdiags.Sourceless(
   290  					tfdiags.Error,
   291  					"Invalid value for input variable",
   292  					fmt.Sprintf("The value provided for variable %q is not valid: %s.", name, err),
   293  				))
   294  			}
   295  		}
   296  	}
   297  
   298  	// Check for any variables that are assigned without being configured.
   299  	// This is always an implementation error in the caller, because we
   300  	// expect undefined variables to be caught during context construction
   301  	// where there is better context to report it well.
   302  	for name := range vs {
   303  		if _, defined := vcs[name]; !defined {
   304  			diags = diags.Append(tfdiags.Sourceless(
   305  				tfdiags.Error,
   306  				"Value assigned to undeclared variable",
   307  				fmt.Sprintf("A value was assigned to an undeclared input variable %q.", name),
   308  			))
   309  		}
   310  	}
   311  
   312  	return diags
   313  }