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