| package backend |
| |
| import ( |
| "fmt" |
| |
| "github.com/hashicorp/hcl/v2" |
| "github.com/hashicorp/terraform/internal/configs" |
| "github.com/hashicorp/terraform/internal/terraform" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| "github.com/zclconf/go-cty/cty" |
| ) |
| |
| // UnparsedVariableValue represents a variable value provided by the caller |
| // whose parsing must be deferred until configuration is available. |
| // |
| // This exists to allow processing of variable-setting arguments (e.g. in the |
| // command package) to be separated from parsing (in the backend package). |
| type UnparsedVariableValue interface { |
| // ParseVariableValue information in the provided variable configuration |
| // to parse (if necessary) and return the variable value encapsulated in |
| // the receiver. |
| // |
| // If error diagnostics are returned, the resulting value may be invalid |
| // or incomplete. |
| ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) |
| } |
| |
| // ParseUndeclaredVariableValues processes a map of unparsed variable values |
| // and returns an input values map of the ones not declared in the specified |
| // declaration map along with detailed diagnostics about values of undeclared |
| // variables being present, depending on the source of these values. If more |
| // than two undeclared values are present in file form (config, auto, -var-file) |
| // the remaining errors are summarized to avoid a massive list of errors. |
| func ParseUndeclaredVariableValues(vv map[string]UnparsedVariableValue, decls map[string]*configs.Variable) (terraform.InputValues, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| ret := make(terraform.InputValues, len(vv)) |
| seenUndeclaredInFile := 0 |
| |
| for name, rv := range vv { |
| if _, declared := decls[name]; declared { |
| // Only interested in parsing undeclared variables |
| continue |
| } |
| |
| val, valDiags := rv.ParseVariableValue(configs.VariableParseLiteral) |
| if valDiags.HasErrors() { |
| continue |
| } |
| |
| ret[name] = val |
| |
| switch val.SourceType { |
| case terraform.ValueFromConfig, terraform.ValueFromAutoFile, terraform.ValueFromNamedFile: |
| // We allow undeclared names for variable values from files and warn in case |
| // users have forgotten a variable {} declaration or have a typo in their var name. |
| // Some users will actively ignore this warning because they use a .tfvars file |
| // across multiple configurations. |
| if seenUndeclaredInFile < 2 { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Warning, |
| "Value for undeclared variable", |
| fmt.Sprintf("The root module does not declare a variable named %q but a value was found in file %q. If you meant to use this value, add a \"variable\" block to the configuration.\n\nTo silence these warnings, use TF_VAR_... environment variables to provide certain \"global\" settings to all configurations in your organization. To reduce the verbosity of these warnings, use the -compact-warnings option.", name, val.SourceRange.Filename), |
| )) |
| } |
| seenUndeclaredInFile++ |
| |
| case terraform.ValueFromEnvVar: |
| // We allow and ignore undeclared names for environment |
| // variables, because users will often set these globally |
| // when they are used across many (but not necessarily all) |
| // configurations. |
| case terraform.ValueFromCLIArg: |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "Value for undeclared variable", |
| fmt.Sprintf("A variable named %q was assigned on the command line, but the root module does not declare a variable of that name. To use this value, add a \"variable\" block to the configuration.", name), |
| )) |
| default: |
| // For all other source types we are more vague, but other situations |
| // don't generally crop up at this layer in practice. |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "Value for undeclared variable", |
| fmt.Sprintf("A variable named %q was assigned a value, but the root module does not declare a variable of that name. To use this value, add a \"variable\" block to the configuration.", name), |
| )) |
| } |
| } |
| |
| if seenUndeclaredInFile > 2 { |
| extras := seenUndeclaredInFile - 2 |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagWarning, |
| Summary: "Values for undeclared variables", |
| Detail: fmt.Sprintf("In addition to the other similar warnings shown, %d other variable(s) defined without being declared.", extras), |
| }) |
| } |
| |
| return ret, diags |
| } |
| |
| // ParseDeclaredVariableValues processes a map of unparsed variable values |
| // and returns an input values map of the ones declared in the specified |
| // variable declaration mapping. Diagnostics will be populating with |
| // any variable parsing errors encountered within this collection. |
| func ParseDeclaredVariableValues(vv map[string]UnparsedVariableValue, decls map[string]*configs.Variable) (terraform.InputValues, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| ret := make(terraform.InputValues, len(vv)) |
| |
| for name, rv := range vv { |
| var mode configs.VariableParsingMode |
| config, declared := decls[name] |
| |
| if declared { |
| mode = config.ParsingMode |
| } else { |
| // Only interested in parsing declared variables |
| continue |
| } |
| |
| val, valDiags := rv.ParseVariableValue(mode) |
| diags = diags.Append(valDiags) |
| if valDiags.HasErrors() { |
| continue |
| } |
| |
| ret[name] = val |
| } |
| |
| return ret, diags |
| } |
| |
| // Checks all given terraform.InputValues variable maps for the existance of |
| // a named variable |
| func isDefinedAny(name string, maps ...terraform.InputValues) bool { |
| for _, m := range maps { |
| if _, defined := m[name]; defined { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // ParseVariableValues processes a map of unparsed variable values by |
| // correlating each one with the given variable declarations which should |
| // be from a root module. |
| // |
| // The map of unparsed variable values should include variables from all |
| // possible root module declarations sources such that it is as complete as |
| // it can possibly be for the current operation. If any declared variables |
| // are not included in the map, ParseVariableValues will either substitute |
| // a configured default value or produce an error. |
| // |
| // If this function returns without any errors in the diagnostics, the |
| // resulting input values map is guaranteed to be valid and ready to pass |
| // to terraform.NewContext. If the diagnostics contains errors, the returned |
| // InputValues may be incomplete but will include the subset of variables |
| // that were successfully processed, allowing for careful analysis of the |
| // partial result. |
| func ParseVariableValues(vv map[string]UnparsedVariableValue, decls map[string]*configs.Variable) (terraform.InputValues, tfdiags.Diagnostics) { |
| ret, diags := ParseDeclaredVariableValues(vv, decls) |
| undeclared, diagsUndeclared := ParseUndeclaredVariableValues(vv, decls) |
| |
| diags = diags.Append(diagsUndeclared) |
| |
| // By this point we should've gathered all of the required root module |
| // variables from one of the many possible sources. We'll now populate |
| // any we haven't gathered as unset placeholders which Terraform Core |
| // can then react to. |
| for name, vc := range decls { |
| if isDefinedAny(name, ret, undeclared) { |
| continue |
| } |
| |
| // This check is redundant with a check made in Terraform Core when |
| // processing undeclared variables, but allows us to generate a more |
| // specific error message which mentions -var and -var-file command |
| // line options, whereas the one in Terraform Core is more general |
| // due to supporting both root and child module variables. |
| if vc.Required() { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "No value for required variable", |
| Detail: fmt.Sprintf("The root module input variable %q is not set, and has no default value. Use a -var or -var-file command line argument to provide a value for this variable.", name), |
| Subject: vc.DeclRange.Ptr(), |
| }) |
| |
| // We'll include a placeholder value anyway, just so that our |
| // result is complete for any calling code that wants to cautiously |
| // analyze it for diagnostic purposes. Since our diagnostics now |
| // includes an error, normal processing will ignore this result. |
| ret[name] = &terraform.InputValue{ |
| Value: cty.DynamicVal, |
| SourceType: terraform.ValueFromConfig, |
| SourceRange: tfdiags.SourceRangeFromHCL(vc.DeclRange), |
| } |
| } else { |
| // We're still required to put an entry for this variable |
| // in the mapping to be explicit to Terraform Core that we |
| // visited it, but its value will be cty.NilVal to represent |
| // that it wasn't set at all at this layer, and so Terraform Core |
| // should substitute a default if available, or generate an error |
| // if not. |
| ret[name] = &terraform.InputValue{ |
| Value: cty.NilVal, |
| SourceType: terraform.ValueFromConfig, |
| SourceRange: tfdiags.SourceRangeFromHCL(vc.DeclRange), |
| } |
| } |
| } |
| |
| return ret, diags |
| } |