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