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