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