github.com/opentofu/opentofu@v1.7.1/internal/backend/unparsed_value.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package backend 7 8 import ( 9 "fmt" 10 11 "github.com/hashicorp/hcl/v2" 12 "github.com/opentofu/opentofu/internal/configs" 13 "github.com/opentofu/opentofu/internal/tfdiags" 14 "github.com/opentofu/opentofu/internal/tofu" 15 "github.com/zclconf/go-cty/cty" 16 ) 17 18 // UnparsedVariableValue represents a variable value provided by the caller 19 // whose parsing must be deferred until configuration is available. 20 // 21 // This exists to allow processing of variable-setting arguments (e.g. in the 22 // command package) to be separated from parsing (in the backend package). 23 type UnparsedVariableValue interface { 24 // ParseVariableValue information in the provided variable configuration 25 // to parse (if necessary) and return the variable value encapsulated in 26 // the receiver. 27 // 28 // If error diagnostics are returned, the resulting value may be invalid 29 // or incomplete. 30 ParseVariableValue(mode configs.VariableParsingMode) (*tofu.InputValue, tfdiags.Diagnostics) 31 } 32 33 // ParseUndeclaredVariableValues processes a map of unparsed variable values 34 // and returns an input values map of the ones not declared in the specified 35 // declaration map along with detailed diagnostics about values of undeclared 36 // variables being present, depending on the source of these values. If more 37 // than two undeclared values are present in file form (config, auto, -var-file) 38 // the remaining errors are summarized to avoid a massive list of errors. 39 func ParseUndeclaredVariableValues(vv map[string]UnparsedVariableValue, decls map[string]*configs.Variable) (tofu.InputValues, tfdiags.Diagnostics) { 40 var diags tfdiags.Diagnostics 41 ret := make(tofu.InputValues, len(vv)) 42 seenUndeclaredInFile := 0 43 44 for name, rv := range vv { 45 if _, declared := decls[name]; declared { 46 // Only interested in parsing undeclared variables 47 continue 48 } 49 50 val, valDiags := rv.ParseVariableValue(configs.VariableParseLiteral) 51 if valDiags.HasErrors() { 52 continue 53 } 54 55 ret[name] = val 56 57 switch val.SourceType { 58 case tofu.ValueFromConfig, tofu.ValueFromAutoFile, tofu.ValueFromNamedFile: 59 // We allow undeclared names for variable values from files and warn in case 60 // users have forgotten a variable {} declaration or have a typo in their var name. 61 // Some users will actively ignore this warning because they use a .tfvars file 62 // across multiple configurations. 63 if seenUndeclaredInFile < 2 { 64 diags = diags.Append(tfdiags.Sourceless( 65 tfdiags.Warning, 66 "Value for undeclared variable", 67 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), 68 )) 69 } 70 seenUndeclaredInFile++ 71 72 case tofu.ValueFromEnvVar: 73 // We allow and ignore undeclared names for environment 74 // variables, because users will often set these globally 75 // when they are used across many (but not necessarily all) 76 // configurations. 77 case tofu.ValueFromCLIArg: 78 diags = diags.Append(tfdiags.Sourceless( 79 tfdiags.Error, 80 "Value for undeclared variable", 81 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), 82 )) 83 default: 84 // For all other source types we are more vague, but other situations 85 // don't generally crop up at this layer in practice. 86 diags = diags.Append(tfdiags.Sourceless( 87 tfdiags.Error, 88 "Value for undeclared variable", 89 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), 90 )) 91 } 92 } 93 94 if seenUndeclaredInFile > 2 { 95 extras := seenUndeclaredInFile - 2 96 diags = diags.Append(&hcl.Diagnostic{ 97 Severity: hcl.DiagWarning, 98 Summary: "Values for undeclared variables", 99 Detail: fmt.Sprintf("In addition to the other similar warnings shown, %d other variable(s) defined without being declared.", extras), 100 }) 101 } 102 103 return ret, diags 104 } 105 106 // ParseDeclaredVariableValues processes a map of unparsed variable values 107 // and returns an input values map of the ones declared in the specified 108 // variable declaration mapping. Diagnostics will be populating with 109 // any variable parsing errors encountered within this collection. 110 func ParseDeclaredVariableValues(vv map[string]UnparsedVariableValue, decls map[string]*configs.Variable) (tofu.InputValues, tfdiags.Diagnostics) { 111 var diags tfdiags.Diagnostics 112 ret := make(tofu.InputValues, len(vv)) 113 114 for name, rv := range vv { 115 var mode configs.VariableParsingMode 116 config, declared := decls[name] 117 118 if declared { 119 mode = config.ParsingMode 120 } else { 121 // Only interested in parsing declared variables 122 continue 123 } 124 125 val, valDiags := rv.ParseVariableValue(mode) 126 diags = diags.Append(valDiags) 127 if valDiags.HasErrors() { 128 continue 129 } 130 131 ret[name] = val 132 } 133 134 return ret, diags 135 } 136 137 // Checks all given tofu.InputValues variable maps for the existance of 138 // a named variable 139 func isDefinedAny(name string, maps ...tofu.InputValues) bool { 140 for _, m := range maps { 141 if _, defined := m[name]; defined { 142 return true 143 } 144 } 145 return false 146 } 147 148 // ParseVariableValues processes a map of unparsed variable values by 149 // correlating each one with the given variable declarations which should 150 // be from a root module. 151 // 152 // The map of unparsed variable values should include variables from all 153 // possible root module declarations sources such that it is as complete as 154 // it can possibly be for the current operation. If any declared variables 155 // are not included in the map, ParseVariableValues will either substitute 156 // a configured default value or produce an error. 157 // 158 // If this function returns without any errors in the diagnostics, the 159 // resulting input values map is guaranteed to be valid and ready to pass 160 // to tofu.NewContext. If the diagnostics contains errors, the returned 161 // InputValues may be incomplete but will include the subset of variables 162 // that were successfully processed, allowing for careful analysis of the 163 // partial result. 164 func ParseVariableValues(vv map[string]UnparsedVariableValue, decls map[string]*configs.Variable) (tofu.InputValues, tfdiags.Diagnostics) { 165 ret, diags := ParseDeclaredVariableValues(vv, decls) 166 undeclared, diagsUndeclared := ParseUndeclaredVariableValues(vv, decls) 167 168 diags = diags.Append(diagsUndeclared) 169 170 // By this point we should've gathered all the required root module 171 // variables from one of the many possible sources. We'll now populate 172 // any we haven't gathered as unset placeholders which OpenTofu Core 173 // can then react to. 174 for name, vc := range decls { 175 if isDefinedAny(name, ret, undeclared) { 176 continue 177 } 178 179 // This check is redundant with a check made in OpenTofu Core when 180 // processing undeclared variables, but allows us to generate a more 181 // specific error message which mentions -var and -var-file command 182 // line options, whereas the one in OpenTofu Core is more general 183 // due to supporting both root and child module variables. 184 if vc.Required() { 185 diags = diags.Append(&hcl.Diagnostic{ 186 Severity: hcl.DiagError, 187 Summary: "No value for required variable", 188 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), 189 Subject: vc.DeclRange.Ptr(), 190 }) 191 192 // We'll include a placeholder value anyway, just so that our 193 // result is complete for any calling code that wants to cautiously 194 // analyze it for diagnostic purposes. Since our diagnostics now 195 // includes an error, normal processing will ignore this result. 196 ret[name] = &tofu.InputValue{ 197 Value: cty.DynamicVal, 198 SourceType: tofu.ValueFromConfig, 199 SourceRange: tfdiags.SourceRangeFromHCL(vc.DeclRange), 200 } 201 } else { 202 // We're still required to put an entry for this variable 203 // in the mapping to be explicit to OpenTofu Core that we 204 // visited it, but its value will be cty.NilVal to represent 205 // that it wasn't set at all at this layer, and so OpenTofu Core 206 // should substitute a default if available, or generate an error 207 // if not. 208 ret[name] = &tofu.InputValue{ 209 Value: cty.NilVal, 210 SourceType: tofu.ValueFromConfig, 211 SourceRange: tfdiags.SourceRangeFromHCL(vc.DeclRange), 212 } 213 } 214 } 215 216 return ret, diags 217 }