| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package backendrun | |
| import ( | |
| "fmt" | |
| "github.com/hashicorp/hcl/v2" | |
| "github.com/zclconf/go-cty/cty" | |
| "github.com/hashicorp/terraform/internal/command/arguments" | |
| "github.com/hashicorp/terraform/internal/configs" | |
| "github.com/hashicorp/terraform/internal/terraform" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| ) | |
| // 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]arguments.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.ValueFromCloud: | |
| // We allow and ignore undeclared names fetched from the cloud | |
| // backend, because users will often set these globally or via | |
| // varsets when they are used across many (but not necessarily all) | |
| // workspaces. | |
| 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]arguments.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]arguments.UnparsedVariableValue, decls map[string]*configs.Variable) (terraform.InputValues, tfdiags.Diagnostics) { | |
| return parseVariableValues(vv, decls, false) | |
| } | |
| // ParseConstVariableValues is like ParseVariableValues but only produces | |
| // errors for missing const variables. Non-const required variables that are | |
| // missing will still receive placeholder values but won't produce errors. | |
| // This is used during early configuration loading (e.g. module installation) | |
| // where only const variables are needed for module source resolution. | |
| func ParseConstVariableValues(vv map[string]arguments.UnparsedVariableValue, decls map[string]*configs.Variable) (terraform.InputValues, tfdiags.Diagnostics) { | |
| return parseVariableValues(vv, decls, true) | |
| } | |
| func parseVariableValues(vv map[string]arguments.UnparsedVariableValue, decls map[string]*configs.Variable, constOnly bool) (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. | |
| shouldError := vc.Required() | |
| if constOnly { | |
| shouldError = vc.Const && vc.Required() | |
| } | |
| if shouldError { | |
| 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(), | |
| }) | |
| } | |
| if vc.Required() { | |
| // 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 | |
| } | |
| // HasUnsatisfiedConstVariables checks whether any const variables declared in | |
| // the given module are required but not yet present in the provided variable | |
| // values map. This is used to determine whether we need to fetch additional | |
| // variable values from a backend before loading the full configuration. | |
| func HasUnsatisfiedConstVariables(vv map[string]arguments.UnparsedVariableValue, decls map[string]*configs.Variable) bool { | |
| for name, vc := range decls { | |
| if vc.Const && vc.Required() { | |
| if _, defined := vv[name]; !defined { | |
| return true | |
| } | |
| } | |
| } | |
| return false | |
| } |