// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package backend import ( "fmt" "github.com/hashicorp/hcl/v2" "github.com/placeholderplaceholderplaceholder/opentf/internal/configs" "github.com/placeholderplaceholderplaceholder/opentf/internal/terraform" "github.com/placeholderplaceholderplaceholder/opentf/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 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 OpenTF 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 OpenTF 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 OpenTF 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 OpenTF 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 OpenTF 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 }