github.com/opentofu/opentofu@v1.7.1/internal/tofu/eval_variable.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 tofu 7 8 import ( 9 "fmt" 10 "log" 11 "strings" 12 13 "github.com/hashicorp/hcl/v2" 14 "github.com/hashicorp/hcl/v2/gohcl" 15 "github.com/zclconf/go-cty/cty" 16 "github.com/zclconf/go-cty/cty/convert" 17 18 "github.com/opentofu/opentofu/internal/addrs" 19 "github.com/opentofu/opentofu/internal/checks" 20 "github.com/opentofu/opentofu/internal/configs" 21 "github.com/opentofu/opentofu/internal/lang/marks" 22 "github.com/opentofu/opentofu/internal/tfdiags" 23 ) 24 25 func prepareFinalInputVariableValue(addr addrs.AbsInputVariableInstance, raw *InputValue, cfg *configs.Variable) (cty.Value, tfdiags.Diagnostics) { 26 var diags tfdiags.Diagnostics 27 28 convertTy := cfg.ConstraintType 29 log.Printf("[TRACE] prepareFinalInputVariableValue: preparing %s", addr) 30 31 var defaultVal cty.Value 32 if cfg.Default != cty.NilVal { 33 log.Printf("[TRACE] prepareFinalInputVariableValue: %s has a default value", addr) 34 var err error 35 defaultVal, err = convert.Convert(cfg.Default, convertTy) 36 if err != nil { 37 // Validation of the declaration should typically catch this, 38 // but we'll check it here too to be robust. 39 diags = diags.Append(&hcl.Diagnostic{ 40 Severity: hcl.DiagError, 41 Summary: "Invalid default value for module argument", 42 Detail: fmt.Sprintf( 43 "The default value for variable %q is incompatible with its type constraint: %s.", 44 cfg.Name, err, 45 ), 46 Subject: &cfg.DeclRange, 47 }) 48 // We'll return a placeholder unknown value to avoid producing 49 // redundant downstream errors. 50 return cty.UnknownVal(cfg.Type), diags 51 } 52 } 53 54 var sourceRange tfdiags.SourceRange 55 var nonFileSource string 56 if raw.HasSourceRange() { 57 sourceRange = raw.SourceRange 58 } else { 59 // If the value came from a place that isn't a file and thus doesn't 60 // have its own source range, we'll use the declaration range as 61 // our source range and generate some slightly different error 62 // messages. 63 sourceRange = tfdiags.SourceRangeFromHCL(cfg.DeclRange) 64 switch raw.SourceType { 65 case ValueFromCLIArg: 66 nonFileSource = fmt.Sprintf("set using -var=\"%s=...\"", addr.Variable.Name) 67 case ValueFromEnvVar: 68 nonFileSource = fmt.Sprintf("set using the TF_VAR_%s environment variable", addr.Variable.Name) 69 case ValueFromInput: 70 nonFileSource = "set using an interactive prompt" 71 default: 72 nonFileSource = "set from outside of the configuration" 73 } 74 } 75 76 given := raw.Value 77 if given == cty.NilVal { // The variable wasn't set at all (even to null) 78 log.Printf("[TRACE] prepareFinalInputVariableValue: %s has no defined value", addr) 79 if cfg.Required() { 80 // NOTE: The CLI layer typically checks for itself whether all of 81 // the required _root_ module variables are set, which would 82 // mask this error with a more specific one that refers to the 83 // CLI features for setting such variables. We can get here for 84 // child module variables, though. 85 log.Printf("[ERROR] prepareFinalInputVariableValue: %s is required but is not set", addr) 86 diags = diags.Append(&hcl.Diagnostic{ 87 Severity: hcl.DiagError, 88 Summary: `Required variable not set`, 89 Detail: fmt.Sprintf(`The variable %q is required, but is not set.`, addr.Variable.Name), 90 Subject: cfg.DeclRange.Ptr(), 91 }) 92 // We'll return a placeholder unknown value to avoid producing 93 // redundant downstream errors. 94 return cty.UnknownVal(cfg.Type), diags 95 } 96 97 given = defaultVal // must be set, because we checked above that the variable isn't required 98 } 99 100 // Apply defaults from the variable's type constraint to the converted value, 101 // unless the converted value is null. We do not apply defaults to top-level 102 // null values, as doing so could prevent assigning null to a nullable 103 // variable. 104 if cfg.TypeDefaults != nil && !given.IsNull() { 105 given = cfg.TypeDefaults.Apply(given) 106 } 107 108 val, err := convert.Convert(given, convertTy) 109 if err != nil { 110 log.Printf("[ERROR] prepareFinalInputVariableValue: %s has unsuitable type\n got: %s\n want: %s", addr, given.Type(), convertTy) 111 var detail string 112 var subject *hcl.Range 113 if nonFileSource != "" { 114 detail = fmt.Sprintf( 115 "Unsuitable value for %s %s: %s.", 116 addr, nonFileSource, err, 117 ) 118 subject = cfg.DeclRange.Ptr() 119 } else { 120 detail = fmt.Sprintf( 121 "The given value is not suitable for %s declared at %s: %s.", 122 addr, cfg.DeclRange.String(), err, 123 ) 124 subject = sourceRange.ToHCL().Ptr() 125 126 // In some workflows, the operator running tofu does not have access to the variables 127 // themselves. They are for example stored in encrypted files that will be used by the CI toolset 128 // and not by the operator directly. In such a case, the failing secret value should not be 129 // displayed to the operator 130 if cfg.Sensitive { 131 detail = fmt.Sprintf( 132 "The given value is not suitable for %s, which is sensitive: %s. Invalid value defined at %s.", 133 addr, err, sourceRange.ToHCL(), 134 ) 135 subject = cfg.DeclRange.Ptr() 136 } 137 } 138 139 diags = diags.Append(&hcl.Diagnostic{ 140 Severity: hcl.DiagError, 141 Summary: "Invalid value for input variable", 142 Detail: detail, 143 Subject: subject, 144 }) 145 // We'll return a placeholder unknown value to avoid producing 146 // redundant downstream errors. 147 return cty.UnknownVal(cfg.Type), diags 148 } 149 150 // By the time we get here, we know: 151 // - val matches the variable's type constraint 152 // - val is definitely not cty.NilVal, but might be a null value if the given was already null. 153 // 154 // That means we just need to handle the case where the value is null, 155 // which might mean we need to use the default value, or produce an error. 156 // 157 // For historical reasons we do this only for a "non-nullable" variable. 158 // Nullable variables just appear as null if they were set to null, 159 // regardless of any default value. 160 if val.IsNull() && !cfg.Nullable { 161 log.Printf("[TRACE] prepareFinalInputVariableValue: %s is defined as null", addr) 162 if defaultVal != cty.NilVal { 163 val = defaultVal 164 } else { 165 log.Printf("[ERROR] prepareFinalInputVariableValue: %s is non-nullable but set to null, and is required", addr) 166 if nonFileSource != "" { 167 diags = diags.Append(&hcl.Diagnostic{ 168 Severity: hcl.DiagError, 169 Summary: `Required variable not set`, 170 Detail: fmt.Sprintf( 171 "Unsuitable value for %s %s: required variable may not be set to null.", 172 addr, nonFileSource, 173 ), 174 Subject: cfg.DeclRange.Ptr(), 175 }) 176 } else { 177 diags = diags.Append(&hcl.Diagnostic{ 178 Severity: hcl.DiagError, 179 Summary: `Required variable not set`, 180 Detail: fmt.Sprintf( 181 "The given value is not suitable for %s defined at %s: required variable may not be set to null.", 182 addr, cfg.DeclRange.String(), 183 ), 184 Subject: sourceRange.ToHCL().Ptr(), 185 }) 186 } 187 // Stub out our return value so that the semantic checker doesn't 188 // produce redundant downstream errors. 189 val = cty.UnknownVal(cfg.Type) 190 } 191 } 192 193 return val, diags 194 } 195 196 // evalVariableValidations ensures that all of the configured custom validations 197 // for a variable are passing. 198 // 199 // This must be used only after any side-effects that make the value of the 200 // variable available for use in expression evaluation, such as 201 // EvalModuleCallArgument for variables in descendent modules. 202 func evalVariableValidations(addr addrs.AbsInputVariableInstance, config *configs.Variable, expr hcl.Expression, ctx EvalContext) (diags tfdiags.Diagnostics) { 203 if config == nil || len(config.Validations) == 0 { 204 log.Printf("[TRACE] evalVariableValidations: no validation rules declared for %s, so skipping", addr) 205 return nil 206 } 207 log.Printf("[TRACE] evalVariableValidations: validating %s", addr) 208 209 checkState := ctx.Checks() 210 if !checkState.ConfigHasChecks(addr.ConfigCheckable()) { 211 // We have nothing to do if this object doesn't have any checks, 212 // but the "rules" slice should agree that we don't. 213 if ct := len(config.Validations); ct != 0 { 214 panic(fmt.Sprintf("check state says that %s should have no rules, but it has %d", addr, ct)) 215 } 216 return diags 217 } 218 219 // Variable nodes evaluate in the parent module to where they were declared 220 // because the value expression (n.Expr, if set) comes from the calling 221 // "module" block in the parent module. 222 // 223 // Validation expressions are statically validated (during configuration 224 // loading) to refer only to the variable being validated, so we can 225 // bypass our usual evaluation machinery here and just produce a minimal 226 // evaluation context containing just the required value, and thus avoid 227 // the problem that ctx's evaluation functions refer to the wrong module. 228 val := ctx.GetVariableValue(addr) 229 if val == cty.NilVal { 230 diags = diags.Append(&hcl.Diagnostic{ 231 Severity: hcl.DiagError, 232 Summary: "No final value for variable", 233 Detail: fmt.Sprintf("OpenTofu doesn't have a final value for %s during validation. This is a bug in OpenTofu; please report it!", addr), 234 }) 235 return diags 236 } 237 hclCtx := &hcl.EvalContext{ 238 Variables: map[string]cty.Value{ 239 "var": cty.ObjectVal(map[string]cty.Value{ 240 config.Name: val, 241 }), 242 }, 243 Functions: ctx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey).Functions(), 244 } 245 246 for ix, validation := range config.Validations { 247 result, ruleDiags := evalVariableValidation(validation, hclCtx, addr, config, expr, ix) 248 diags = diags.Append(ruleDiags) 249 250 log.Printf("[TRACE] evalVariableValidations: %s status is now %s", addr, result.Status) 251 if result.Status == checks.StatusFail { 252 checkState.ReportCheckFailure(addr, addrs.InputValidation, ix, result.FailureMessage) 253 } else { 254 checkState.ReportCheckResult(addr, addrs.InputValidation, ix, result.Status) 255 } 256 } 257 258 return diags 259 } 260 261 func evalVariableValidation(validation *configs.CheckRule, hclCtx *hcl.EvalContext, addr addrs.AbsInputVariableInstance, config *configs.Variable, expr hcl.Expression, ix int) (checkResult, tfdiags.Diagnostics) { 262 const errInvalidCondition = "Invalid variable validation result" 263 const errInvalidValue = "Invalid value for variable" 264 var diags tfdiags.Diagnostics 265 266 result, moreDiags := validation.Condition.Value(hclCtx) 267 diags = diags.Append(moreDiags) 268 errorValue, errorDiags := validation.ErrorMessage.Value(hclCtx) 269 270 // The following error handling is a workaround to preserve backwards 271 // compatibility. Due to an implementation quirk, all prior versions of 272 // Terraform would treat error messages specified using JSON 273 // configuration syntax (.tf.json) as string literals, even if they 274 // contained the "${" template expression operator. This behaviour did 275 // not match that of HCL configuration syntax, where a template 276 // expression would result in a validation error. 277 // 278 // As a result, users writing or generating JSON configuration syntax 279 // may have specified error messages which are invalid template 280 // expressions. As we add support for error message expressions, we are 281 // unable to perfectly distinguish between these two cases. 282 // 283 // To ensure that we don't break backwards compatibility, we have the 284 // below fallback logic if the error message fails to evaluate. This 285 // should only have any effect for JSON configurations. The gohcl 286 // DecodeExpression function behaves differently when the source of the 287 // expression is a JSON configuration file and a nil context is passed. 288 if errorDiags.HasErrors() { 289 // Attempt to decode the expression as a string literal. Passing 290 // nil as the context forces a JSON syntax string value to be 291 // interpreted as a string literal. 292 var errorString string 293 moreErrorDiags := gohcl.DecodeExpression(validation.ErrorMessage, nil, &errorString) 294 if !moreErrorDiags.HasErrors() { 295 // Decoding succeeded, meaning that this is a JSON syntax 296 // string value. We rewrap that as a cty value to allow later 297 // decoding to succeed. 298 errorValue = cty.StringVal(errorString) 299 300 // This warning diagnostic explains this odd behaviour, while 301 // giving us an escape hatch to change this to a hard failure 302 // in some future OpenTofu 1.x version. 303 errorDiags = hcl.Diagnostics{ 304 &hcl.Diagnostic{ 305 Severity: hcl.DiagWarning, 306 Summary: "Validation error message expression is invalid", 307 Detail: fmt.Sprintf("The error message provided could not be evaluated as an expression, so OpenTofu is interpreting it as a string literal.\n\nIn future versions of OpenTofu, this will be considered an error. Please file a GitHub issue if this would break your workflow.\n\n%s", errorDiags.Error()), 308 Subject: validation.ErrorMessage.Range().Ptr(), 309 Context: validation.DeclRange.Ptr(), 310 Expression: validation.ErrorMessage, 311 EvalContext: hclCtx, 312 }, 313 } 314 } 315 316 // We want to either report the original diagnostics if the 317 // fallback failed, or the warning generated above if it succeeded. 318 diags = diags.Append(errorDiags) 319 } 320 321 if diags.HasErrors() { 322 log.Printf("[TRACE] evalVariableValidations: %s rule %s check rule evaluation failed: %s", addr, validation.DeclRange, diags.Err().Error()) 323 } 324 if !result.IsKnown() { 325 log.Printf("[TRACE] evalVariableValidations: %s rule %s condition value is unknown, so skipping validation for now", addr, validation.DeclRange) 326 327 return checkResult{Status: checks.StatusUnknown}, diags // We'll wait until we've learned more, then. 328 } 329 if result.IsNull() { 330 diags = diags.Append(&hcl.Diagnostic{ 331 Severity: hcl.DiagError, 332 Summary: errInvalidCondition, 333 Detail: "Validation condition expression must return either true or false, not null.", 334 Subject: validation.Condition.Range().Ptr(), 335 Expression: validation.Condition, 336 EvalContext: hclCtx, 337 }) 338 return checkResult{Status: checks.StatusError}, diags 339 } 340 var err error 341 result, err = convert.Convert(result, cty.Bool) 342 if err != nil { 343 diags = diags.Append(&hcl.Diagnostic{ 344 Severity: hcl.DiagError, 345 Summary: errInvalidCondition, 346 Detail: fmt.Sprintf("Invalid validation condition result value: %s.", tfdiags.FormatError(err)), 347 Subject: validation.Condition.Range().Ptr(), 348 Expression: validation.Condition, 349 EvalContext: hclCtx, 350 }) 351 return checkResult{Status: checks.StatusError}, diags 352 } 353 354 // Validation condition may be marked if the input variable is bound to 355 // a sensitive value. This is irrelevant to the validation process, so 356 // we discard the marks now. 357 result, _ = result.Unmark() 358 status := checks.StatusForCtyValue(result) 359 360 if status != checks.StatusFail { 361 return checkResult{Status: status}, diags 362 } 363 364 var errorMessage string 365 if !errorDiags.HasErrors() && errorValue.IsKnown() && !errorValue.IsNull() { 366 var err error 367 errorValue, err = convert.Convert(errorValue, cty.String) 368 if err != nil { 369 diags = diags.Append(&hcl.Diagnostic{ 370 Severity: hcl.DiagError, 371 Summary: "Invalid error message", 372 Detail: fmt.Sprintf("Unsuitable value for error message: %s.", tfdiags.FormatError(err)), 373 Subject: validation.ErrorMessage.Range().Ptr(), 374 Expression: validation.ErrorMessage, 375 EvalContext: hclCtx, 376 }) 377 } else { 378 if marks.Has(errorValue, marks.Sensitive) { 379 diags = diags.Append(&hcl.Diagnostic{ 380 Severity: hcl.DiagError, 381 382 Summary: "Error message refers to sensitive values", 383 Detail: `The error expression used to explain this condition refers to sensitive values. OpenTofu will not display the resulting message. 384 385 You can correct this by removing references to sensitive values, or by carefully using the nonsensitive() function if the expression will not reveal the sensitive data.`, 386 387 Subject: validation.ErrorMessage.Range().Ptr(), 388 Expression: validation.ErrorMessage, 389 EvalContext: hclCtx, 390 }) 391 errorMessage = "The error message included a sensitive value, so it will not be displayed." 392 } else { 393 errorMessage = strings.TrimSpace(errorValue.AsString()) 394 } 395 } 396 } 397 if errorMessage == "" { 398 errorMessage = "Failed to evaluate condition error message." 399 } 400 401 if expr != nil { 402 diags = diags.Append(&hcl.Diagnostic{ 403 Severity: hcl.DiagError, 404 Summary: errInvalidValue, 405 Detail: fmt.Sprintf("%s\n\nThis was checked by the validation rule at %s.", errorMessage, validation.DeclRange.String()), 406 Subject: expr.Range().Ptr(), 407 Expression: validation.Condition, 408 EvalContext: hclCtx, 409 Extra: &addrs.CheckRuleDiagnosticExtra{ 410 CheckRule: addr.CheckRule(addrs.InputValidation, ix), 411 }, 412 }) 413 } else { 414 // Since we don't have a source expression for a root module 415 // variable, we'll just report the error from the perspective 416 // of the variable declaration itself. 417 diags = diags.Append(&hcl.Diagnostic{ 418 Severity: hcl.DiagError, 419 Summary: errInvalidValue, 420 Detail: fmt.Sprintf("%s\n\nThis was checked by the validation rule at %s.", errorMessage, validation.DeclRange.String()), 421 Subject: config.DeclRange.Ptr(), 422 Expression: validation.Condition, 423 EvalContext: hclCtx, 424 Extra: &addrs.CheckRuleDiagnosticExtra{ 425 CheckRule: addr.CheckRule(addrs.InputValidation, ix), 426 }, 427 }) 428 } 429 430 return checkResult{ 431 Status: status, 432 FailureMessage: errorMessage, 433 }, diags 434 }