github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/configs/named_values.go (about) 1 package configs 2 3 import ( 4 "fmt" 5 "unicode" 6 7 "github.com/hashicorp/hcl/v2" 8 "github.com/hashicorp/hcl/v2/ext/typeexpr" 9 "github.com/hashicorp/hcl/v2/gohcl" 10 "github.com/hashicorp/hcl/v2/hclsyntax" 11 "github.com/zclconf/go-cty/cty" 12 "github.com/zclconf/go-cty/cty/convert" 13 14 "github.com/hashicorp/terraform/addrs" 15 ) 16 17 // A consistent detail message for all "not a valid identifier" diagnostics. 18 const badIdentifierDetail = "A name must start with a letter or underscore and may contain only letters, digits, underscores, and dashes." 19 20 // Variable represents a "variable" block in a module or file. 21 type Variable struct { 22 Name string 23 Description string 24 Default cty.Value 25 Type cty.Type 26 ParsingMode VariableParsingMode 27 Validations []*VariableValidation 28 29 DescriptionSet bool 30 31 DeclRange hcl.Range 32 } 33 34 func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagnostics) { 35 v := &Variable{ 36 Name: block.Labels[0], 37 DeclRange: block.DefRange, 38 } 39 40 // Unless we're building an override, we'll set some defaults 41 // which we might override with attributes below. We leave these 42 // as zero-value in the override case so we can recognize whether 43 // or not they are set when we merge. 44 if !override { 45 v.Type = cty.DynamicPseudoType 46 v.ParsingMode = VariableParseLiteral 47 } 48 49 content, diags := block.Body.Content(variableBlockSchema) 50 51 if !hclsyntax.ValidIdentifier(v.Name) { 52 diags = append(diags, &hcl.Diagnostic{ 53 Severity: hcl.DiagError, 54 Summary: "Invalid variable name", 55 Detail: badIdentifierDetail, 56 Subject: &block.LabelRanges[0], 57 }) 58 } 59 60 // Don't allow declaration of variables that would conflict with the 61 // reserved attribute and block type names in a "module" block, since 62 // these won't be usable for child modules. 63 for _, attr := range moduleBlockSchema.Attributes { 64 if attr.Name == v.Name { 65 diags = append(diags, &hcl.Diagnostic{ 66 Severity: hcl.DiagError, 67 Summary: "Invalid variable name", 68 Detail: fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", attr.Name), 69 Subject: &block.LabelRanges[0], 70 }) 71 } 72 } 73 for _, blockS := range moduleBlockSchema.Blocks { 74 if blockS.Type == v.Name { 75 diags = append(diags, &hcl.Diagnostic{ 76 Severity: hcl.DiagError, 77 Summary: "Invalid variable name", 78 Detail: fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", blockS.Type), 79 Subject: &block.LabelRanges[0], 80 }) 81 } 82 } 83 84 if attr, exists := content.Attributes["description"]; exists { 85 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Description) 86 diags = append(diags, valDiags...) 87 v.DescriptionSet = true 88 } 89 90 if attr, exists := content.Attributes["type"]; exists { 91 ty, parseMode, tyDiags := decodeVariableType(attr.Expr) 92 diags = append(diags, tyDiags...) 93 v.Type = ty 94 v.ParsingMode = parseMode 95 } 96 97 if attr, exists := content.Attributes["default"]; exists { 98 val, valDiags := attr.Expr.Value(nil) 99 diags = append(diags, valDiags...) 100 101 // Convert the default to the expected type so we can catch invalid 102 // defaults early and allow later code to assume validity. 103 // Note that this depends on us having already processed any "type" 104 // attribute above. 105 // However, we can't do this if we're in an override file where 106 // the type might not be set; we'll catch that during merge. 107 if v.Type != cty.NilType { 108 var err error 109 val, err = convert.Convert(val, v.Type) 110 if err != nil { 111 diags = append(diags, &hcl.Diagnostic{ 112 Severity: hcl.DiagError, 113 Summary: "Invalid default value for variable", 114 Detail: fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err), 115 Subject: attr.Expr.Range().Ptr(), 116 }) 117 val = cty.DynamicVal 118 } 119 } 120 121 v.Default = val 122 } 123 124 for _, block := range content.Blocks { 125 switch block.Type { 126 127 case "validation": 128 vv, moreDiags := decodeVariableValidationBlock(v.Name, block, override) 129 diags = append(diags, moreDiags...) 130 v.Validations = append(v.Validations, vv) 131 132 default: 133 // The above cases should be exhaustive for all block types 134 // defined in variableBlockSchema 135 panic(fmt.Sprintf("unhandled block type %q", block.Type)) 136 } 137 } 138 139 return v, diags 140 } 141 142 func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl.Diagnostics) { 143 if exprIsNativeQuotedString(expr) { 144 // Here we're accepting the pre-0.12 form of variable type argument where 145 // the string values "string", "list" and "map" are accepted has a hint 146 // about the type used primarily for deciding how to parse values 147 // given on the command line and in environment variables. 148 // Only the native syntax ends up in this codepath; we handle the 149 // JSON syntax (which is, of course, quoted even in the new format) 150 // in the normal codepath below. 151 val, diags := expr.Value(nil) 152 if diags.HasErrors() { 153 return cty.DynamicPseudoType, VariableParseHCL, diags 154 } 155 str := val.AsString() 156 switch str { 157 case "string": 158 diags = append(diags, &hcl.Diagnostic{ 159 Severity: hcl.DiagWarning, 160 Summary: "Quoted type constraints are deprecated", 161 Detail: "Terraform 0.11 and earlier required type constraints to be given in quotes, but that form is now deprecated and will be removed in a future version of Terraform. To silence this warning, remove the quotes around \"string\".", 162 Subject: expr.Range().Ptr(), 163 }) 164 return cty.String, VariableParseLiteral, diags 165 case "list": 166 diags = append(diags, &hcl.Diagnostic{ 167 Severity: hcl.DiagWarning, 168 Summary: "Quoted type constraints are deprecated", 169 Detail: "Terraform 0.11 and earlier required type constraints to be given in quotes, but that form is now deprecated and will be removed in a future version of Terraform. To silence this warning, remove the quotes around \"list\" and write list(string) instead to explicitly indicate that the list elements are strings.", 170 Subject: expr.Range().Ptr(), 171 }) 172 return cty.List(cty.DynamicPseudoType), VariableParseHCL, diags 173 case "map": 174 diags = append(diags, &hcl.Diagnostic{ 175 Severity: hcl.DiagWarning, 176 Summary: "Quoted type constraints are deprecated", 177 Detail: "Terraform 0.11 and earlier required type constraints to be given in quotes, but that form is now deprecated and will be removed in a future version of Terraform. To silence this warning, remove the quotes around \"map\" and write map(string) instead to explicitly indicate that the map elements are strings.", 178 Subject: expr.Range().Ptr(), 179 }) 180 return cty.Map(cty.DynamicPseudoType), VariableParseHCL, diags 181 default: 182 return cty.DynamicPseudoType, VariableParseHCL, hcl.Diagnostics{{ 183 Severity: hcl.DiagError, 184 Summary: "Invalid legacy variable type hint", 185 Detail: `The legacy variable type hint form, using a quoted string, allows only the values "string", "list", and "map". To provide a full type expression, remove the surrounding quotes and give the type expression directly.`, 186 Subject: expr.Range().Ptr(), 187 }} 188 } 189 } 190 191 // First we'll deal with some shorthand forms that the HCL-level type 192 // expression parser doesn't include. These both emulate pre-0.12 behavior 193 // of allowing a list or map of any element type as long as all of the 194 // elements are consistent. This is the same as list(any) or map(any). 195 switch hcl.ExprAsKeyword(expr) { 196 case "list": 197 return cty.List(cty.DynamicPseudoType), VariableParseHCL, nil 198 case "map": 199 return cty.Map(cty.DynamicPseudoType), VariableParseHCL, nil 200 } 201 202 ty, diags := typeexpr.TypeConstraint(expr) 203 if diags.HasErrors() { 204 return cty.DynamicPseudoType, VariableParseHCL, diags 205 } 206 207 switch { 208 case ty.IsPrimitiveType(): 209 // Primitive types use literal parsing. 210 return ty, VariableParseLiteral, diags 211 default: 212 // Everything else uses HCL parsing 213 return ty, VariableParseHCL, diags 214 } 215 } 216 217 // Required returns true if this variable is required to be set by the caller, 218 // or false if there is a default value that will be used when it isn't set. 219 func (v *Variable) Required() bool { 220 return v.Default == cty.NilVal 221 } 222 223 // VariableParsingMode defines how values of a particular variable given by 224 // text-only mechanisms (command line arguments and environment variables) 225 // should be parsed to produce the final value. 226 type VariableParsingMode rune 227 228 // VariableParseLiteral is a variable parsing mode that just takes the given 229 // string directly as a cty.String value. 230 const VariableParseLiteral VariableParsingMode = 'L' 231 232 // VariableParseHCL is a variable parsing mode that attempts to parse the given 233 // string as an HCL expression and returns the result. 234 const VariableParseHCL VariableParsingMode = 'H' 235 236 // Parse uses the receiving parsing mode to process the given variable value 237 // string, returning the result along with any diagnostics. 238 // 239 // A VariableParsingMode does not know the expected type of the corresponding 240 // variable, so it's the caller's responsibility to attempt to convert the 241 // result to the appropriate type and return to the user any diagnostics that 242 // conversion may produce. 243 // 244 // The given name is used to create a synthetic filename in case any diagnostics 245 // must be generated about the given string value. This should be the name 246 // of the root module variable whose value will be populated from the given 247 // string. 248 // 249 // If the returned diagnostics has errors, the returned value may not be 250 // valid. 251 func (m VariableParsingMode) Parse(name, value string) (cty.Value, hcl.Diagnostics) { 252 switch m { 253 case VariableParseLiteral: 254 return cty.StringVal(value), nil 255 case VariableParseHCL: 256 fakeFilename := fmt.Sprintf("<value for var.%s>", name) 257 expr, diags := hclsyntax.ParseExpression([]byte(value), fakeFilename, hcl.Pos{Line: 1, Column: 1}) 258 if diags.HasErrors() { 259 return cty.DynamicVal, diags 260 } 261 val, valDiags := expr.Value(nil) 262 diags = append(diags, valDiags...) 263 return val, diags 264 default: 265 // Should never happen 266 panic(fmt.Errorf("Parse called on invalid VariableParsingMode %#v", m)) 267 } 268 } 269 270 // VariableValidation represents a configuration-defined validation rule 271 // for a particular input variable, given as a "validation" block inside 272 // a "variable" block. 273 type VariableValidation struct { 274 // Condition is an expression that refers to the variable being tested 275 // and contains no other references. The expression must return true 276 // to indicate that the value is valid or false to indicate that it is 277 // invalid. If the expression produces an error, that's considered a bug 278 // in the module defining the validation rule, not an error in the caller. 279 Condition hcl.Expression 280 281 // ErrorMessage is one or more full sentences, which would need to be in 282 // English for consistency with the rest of the error message output but 283 // can in practice be in any language as long as it ends with a period. 284 // The message should describe what is required for the condition to return 285 // true in a way that would make sense to a caller of the module. 286 ErrorMessage string 287 288 DeclRange hcl.Range 289 } 290 291 func decodeVariableValidationBlock(varName string, block *hcl.Block, override bool) (*VariableValidation, hcl.Diagnostics) { 292 var diags hcl.Diagnostics 293 vv := &VariableValidation{ 294 DeclRange: block.DefRange, 295 } 296 297 if override { 298 // For now we'll just forbid overriding validation blocks, to simplify 299 // the initial design. If we can find a clear use-case for overriding 300 // validations in override files and there's a way to define it that 301 // isn't confusing then we could relax this. 302 diags = diags.Append(&hcl.Diagnostic{ 303 Severity: hcl.DiagError, 304 Summary: "Can't override variable validation rules", 305 Detail: "Variable \"validation\" blocks cannot be used in override files.", 306 Subject: vv.DeclRange.Ptr(), 307 }) 308 return vv, diags 309 } 310 311 content, moreDiags := block.Body.Content(variableValidationBlockSchema) 312 diags = append(diags, moreDiags...) 313 314 if attr, exists := content.Attributes["condition"]; exists { 315 vv.Condition = attr.Expr 316 317 // The validation condition can only refer to the variable itself, 318 // to ensure that the variable declaration can't create additional 319 // edges in the dependency graph. 320 goodRefs := 0 321 for _, traversal := range vv.Condition.Variables() { 322 ref, moreDiags := addrs.ParseRef(traversal) 323 if !moreDiags.HasErrors() { 324 if addr, ok := ref.Subject.(addrs.InputVariable); ok { 325 if addr.Name == varName { 326 goodRefs++ 327 continue // Reference is valid 328 } 329 } 330 } 331 // If we fall out here then the reference is invalid. 332 diags = diags.Append(&hcl.Diagnostic{ 333 Severity: hcl.DiagError, 334 Summary: "Invalid reference in variable validation", 335 Detail: fmt.Sprintf("The condition for variable %q can only refer to the variable itself, using var.%s.", varName, varName), 336 Subject: traversal.SourceRange().Ptr(), 337 }) 338 } 339 if goodRefs < 1 { 340 diags = diags.Append(&hcl.Diagnostic{ 341 Severity: hcl.DiagError, 342 Summary: "Invalid variable validation condition", 343 Detail: fmt.Sprintf("The condition for variable %q must refer to var.%s in order to test incoming values.", varName, varName), 344 Subject: attr.Expr.Range().Ptr(), 345 }) 346 } 347 } 348 349 if attr, exists := content.Attributes["error_message"]; exists { 350 moreDiags := gohcl.DecodeExpression(attr.Expr, nil, &vv.ErrorMessage) 351 diags = append(diags, moreDiags...) 352 if !moreDiags.HasErrors() { 353 const errSummary = "Invalid validation error message" 354 switch { 355 case vv.ErrorMessage == "": 356 diags = diags.Append(&hcl.Diagnostic{ 357 Severity: hcl.DiagError, 358 Summary: errSummary, 359 Detail: "An empty string is not a valid nor useful error message.", 360 Subject: attr.Expr.Range().Ptr(), 361 }) 362 case !looksLikeSentences(vv.ErrorMessage): 363 // Because we're going to include this string verbatim as part 364 // of a bigger error message written in our usual style in 365 // English, we'll require the given error message to conform 366 // to that. We might relax this in future if e.g. we start 367 // presenting these error messages in a different way, or if 368 // Terraform starts supporting producing error messages in 369 // other human languages, etc. 370 // For pragmatism we also allow sentences ending with 371 // exclamation points, but we don't mention it explicitly here 372 // because that's not really consistent with the Terraform UI 373 // writing style. 374 diags = diags.Append(&hcl.Diagnostic{ 375 Severity: hcl.DiagError, 376 Summary: errSummary, 377 Detail: "Validation error message must be at least one full English sentence starting with an uppercase letter and ending with a period or question mark.", 378 Subject: attr.Expr.Range().Ptr(), 379 }) 380 } 381 } 382 } 383 384 return vv, diags 385 } 386 387 // looksLikeSentence is a simple heuristic that encourages writing error 388 // messages that will be presentable when included as part of a larger 389 // Terraform error diagnostic whose other text is written in the Terraform 390 // UI writing style. 391 // 392 // This is intentionally not a very strong validation since we're assuming 393 // that module authors want to write good messages and might just need a nudge 394 // about Terraform's specific style, rather than that they are going to try 395 // to work around these rules to write a lower-quality message. 396 func looksLikeSentences(s string) bool { 397 if len(s) < 1 { 398 return false 399 } 400 runes := []rune(s) // HCL guarantees that all strings are valid UTF-8 401 first := runes[0] 402 last := runes[len(s)-1] 403 404 // If the first rune is a letter then it must be an uppercase letter. 405 // (This will only see the first rune in a multi-rune combining sequence, 406 // but the first rune is generally the letter if any are, and if not then 407 // we'll just ignore it because we're primarily expecting English messages 408 // right now anyway, for consistency with all of Terraform's other output.) 409 if unicode.IsLetter(first) && !unicode.IsUpper(first) { 410 return false 411 } 412 413 // The string must be at least one full sentence, which implies having 414 // sentence-ending punctuation. 415 // (This assumes that if a sentence ends with quotes then the period 416 // will be outside the quotes, which is consistent with Terraform's UI 417 // writing style.) 418 return last == '.' || last == '?' || last == '!' 419 } 420 421 // Output represents an "output" block in a module or file. 422 type Output struct { 423 Name string 424 Description string 425 Expr hcl.Expression 426 DependsOn []hcl.Traversal 427 Sensitive bool 428 429 DescriptionSet bool 430 SensitiveSet bool 431 432 DeclRange hcl.Range 433 } 434 435 func decodeOutputBlock(block *hcl.Block, override bool) (*Output, hcl.Diagnostics) { 436 o := &Output{ 437 Name: block.Labels[0], 438 DeclRange: block.DefRange, 439 } 440 441 schema := outputBlockSchema 442 if override { 443 schema = schemaForOverrides(schema) 444 } 445 446 content, diags := block.Body.Content(schema) 447 448 if !hclsyntax.ValidIdentifier(o.Name) { 449 diags = append(diags, &hcl.Diagnostic{ 450 Severity: hcl.DiagError, 451 Summary: "Invalid output name", 452 Detail: badIdentifierDetail, 453 Subject: &block.LabelRanges[0], 454 }) 455 } 456 457 if attr, exists := content.Attributes["description"]; exists { 458 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Description) 459 diags = append(diags, valDiags...) 460 o.DescriptionSet = true 461 } 462 463 if attr, exists := content.Attributes["value"]; exists { 464 o.Expr = attr.Expr 465 } 466 467 if attr, exists := content.Attributes["sensitive"]; exists { 468 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Sensitive) 469 diags = append(diags, valDiags...) 470 o.SensitiveSet = true 471 } 472 473 if attr, exists := content.Attributes["depends_on"]; exists { 474 deps, depsDiags := decodeDependsOn(attr) 475 diags = append(diags, depsDiags...) 476 o.DependsOn = append(o.DependsOn, deps...) 477 } 478 479 return o, diags 480 } 481 482 // Local represents a single entry from a "locals" block in a module or file. 483 // The "locals" block itself is not represented, because it serves only to 484 // provide context for us to interpret its contents. 485 type Local struct { 486 Name string 487 Expr hcl.Expression 488 489 DeclRange hcl.Range 490 } 491 492 func decodeLocalsBlock(block *hcl.Block) ([]*Local, hcl.Diagnostics) { 493 attrs, diags := block.Body.JustAttributes() 494 if len(attrs) == 0 { 495 return nil, diags 496 } 497 498 locals := make([]*Local, 0, len(attrs)) 499 for name, attr := range attrs { 500 if !hclsyntax.ValidIdentifier(name) { 501 diags = append(diags, &hcl.Diagnostic{ 502 Severity: hcl.DiagError, 503 Summary: "Invalid local value name", 504 Detail: badIdentifierDetail, 505 Subject: &attr.NameRange, 506 }) 507 } 508 509 locals = append(locals, &Local{ 510 Name: name, 511 Expr: attr.Expr, 512 DeclRange: attr.Range, 513 }) 514 } 515 return locals, diags 516 } 517 518 // Addr returns the address of the local value declared by the receiver, 519 // relative to its containing module. 520 func (l *Local) Addr() addrs.LocalValue { 521 return addrs.LocalValue{ 522 Name: l.Name, 523 } 524 } 525 526 var variableBlockSchema = &hcl.BodySchema{ 527 Attributes: []hcl.AttributeSchema{ 528 { 529 Name: "description", 530 }, 531 { 532 Name: "default", 533 }, 534 { 535 Name: "type", 536 }, 537 }, 538 Blocks: []hcl.BlockHeaderSchema{ 539 { 540 Type: "validation", 541 }, 542 }, 543 } 544 545 var variableValidationBlockSchema = &hcl.BodySchema{ 546 Attributes: []hcl.AttributeSchema{ 547 { 548 Name: "condition", 549 Required: true, 550 }, 551 { 552 Name: "error_message", 553 Required: true, 554 }, 555 }, 556 } 557 558 var outputBlockSchema = &hcl.BodySchema{ 559 Attributes: []hcl.AttributeSchema{ 560 { 561 Name: "description", 562 }, 563 { 564 Name: "value", 565 Required: true, 566 }, 567 { 568 Name: "depends_on", 569 }, 570 { 571 Name: "sensitive", 572 }, 573 }, 574 }