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 }