github.com/pulumi/terraform@v1.4.0/pkg/configs/named_values.go (about) 1 package configs 2 3 import ( 4 "fmt" 5 6 "github.com/hashicorp/hcl/v2" 7 "github.com/hashicorp/hcl/v2/ext/typeexpr" 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/pulumi/terraform/pkg/addrs" 14 ) 15 16 // A consistent detail message for all "not a valid identifier" diagnostics. 17 const badIdentifierDetail = "A name must start with a letter or underscore and may contain only letters, digits, underscores, and dashes." 18 19 // Variable represents a "variable" block in a module or file. 20 type Variable struct { 21 Name string 22 Description string 23 Default cty.Value 24 25 // Type is the concrete type of the variable value. 26 Type cty.Type 27 // ConstraintType is used for decoding and type conversions, and may 28 // contain nested ObjectWithOptionalAttr types. 29 ConstraintType cty.Type 30 TypeDefaults *typeexpr.Defaults 31 32 ParsingMode VariableParsingMode 33 Validations []*CheckRule 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, tyDefaults, parseMode, tyDiags := decodeVariableType(attr.Expr) 107 diags = append(diags, tyDiags...) 108 v.ConstraintType = ty 109 v.TypeDefaults = tyDefaults 110 v.Type = ty.WithoutOptionalAttributesDeep() 111 v.ParsingMode = parseMode 112 } 113 114 if attr, exists := content.Attributes["sensitive"]; exists { 115 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Sensitive) 116 diags = append(diags, valDiags...) 117 v.SensitiveSet = true 118 } 119 120 if attr, exists := content.Attributes["nullable"]; exists { 121 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Nullable) 122 diags = append(diags, valDiags...) 123 v.NullableSet = true 124 } else { 125 // The current default is true, which is subject to change in a future 126 // language edition. 127 v.Nullable = true 128 } 129 130 if attr, exists := content.Attributes["default"]; exists { 131 val, valDiags := attr.Expr.Value(nil) 132 diags = append(diags, valDiags...) 133 134 // Convert the default to the expected type so we can catch invalid 135 // defaults early and allow later code to assume validity. 136 // Note that this depends on us having already processed any "type" 137 // attribute above. 138 // However, we can't do this if we're in an override file where 139 // the type might not be set; we'll catch that during merge. 140 if v.ConstraintType != cty.NilType { 141 var err error 142 // If the type constraint has defaults, we must apply those 143 // defaults to the variable default value before type conversion, 144 // unless the default value is null. Null is excluded from the 145 // type default application process as a special case, to allow 146 // nullable variables to have a null default value. 147 if v.TypeDefaults != nil && !val.IsNull() { 148 val = v.TypeDefaults.Apply(val) 149 } 150 val, err = convert.Convert(val, v.ConstraintType) 151 if err != nil { 152 diags = append(diags, &hcl.Diagnostic{ 153 Severity: hcl.DiagError, 154 Summary: "Invalid default value for variable", 155 Detail: fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err), 156 Subject: attr.Expr.Range().Ptr(), 157 }) 158 val = cty.DynamicVal 159 } 160 } 161 162 if !v.Nullable && val.IsNull() { 163 diags = append(diags, &hcl.Diagnostic{ 164 Severity: hcl.DiagError, 165 Summary: "Invalid default value for variable", 166 Detail: "A null default value is not valid when nullable=false.", 167 Subject: attr.Expr.Range().Ptr(), 168 }) 169 } 170 171 v.Default = val 172 } 173 174 for _, block := range content.Blocks { 175 switch block.Type { 176 177 case "validation": 178 vv, moreDiags := decodeVariableValidationBlock(v.Name, block, override) 179 diags = append(diags, moreDiags...) 180 v.Validations = append(v.Validations, vv) 181 182 default: 183 // The above cases should be exhaustive for all block types 184 // defined in variableBlockSchema 185 panic(fmt.Sprintf("unhandled block type %q", block.Type)) 186 } 187 } 188 189 return v, diags 190 } 191 192 func decodeVariableType(expr hcl.Expression) (cty.Type, *typeexpr.Defaults, VariableParsingMode, hcl.Diagnostics) { 193 if exprIsNativeQuotedString(expr) { 194 // If a user provides the pre-0.12 form of variable type argument where 195 // the string values "string", "list" and "map" are accepted, we 196 // provide an error to point the user towards using the type system 197 // correctly has a hint. 198 // Only the native syntax ends up in this codepath; we handle the 199 // JSON syntax (which is, of course, quoted within the type system) 200 // in the normal codepath below. 201 val, diags := expr.Value(nil) 202 if diags.HasErrors() { 203 return cty.DynamicPseudoType, nil, VariableParseHCL, diags 204 } 205 str := val.AsString() 206 switch str { 207 case "string": 208 diags = append(diags, &hcl.Diagnostic{ 209 Severity: hcl.DiagError, 210 Summary: "Invalid quoted type constraints", 211 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\".", 212 Subject: expr.Range().Ptr(), 213 }) 214 return cty.DynamicPseudoType, nil, VariableParseLiteral, diags 215 case "list": 216 diags = append(diags, &hcl.Diagnostic{ 217 Severity: hcl.DiagError, 218 Summary: "Invalid quoted type constraints", 219 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.", 220 Subject: expr.Range().Ptr(), 221 }) 222 return cty.DynamicPseudoType, nil, VariableParseHCL, diags 223 case "map": 224 diags = append(diags, &hcl.Diagnostic{ 225 Severity: hcl.DiagError, 226 Summary: "Invalid quoted type constraints", 227 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.", 228 Subject: expr.Range().Ptr(), 229 }) 230 return cty.DynamicPseudoType, nil, VariableParseHCL, diags 231 default: 232 return cty.DynamicPseudoType, nil, VariableParseHCL, hcl.Diagnostics{{ 233 Severity: hcl.DiagError, 234 Summary: "Invalid legacy variable type hint", 235 Detail: `To provide a full type expression, remove the surrounding quotes and give the type expression directly.`, 236 Subject: expr.Range().Ptr(), 237 }} 238 } 239 } 240 241 // First we'll deal with some shorthand forms that the HCL-level type 242 // expression parser doesn't include. These both emulate pre-0.12 behavior 243 // of allowing a list or map of any element type as long as all of the 244 // elements are consistent. This is the same as list(any) or map(any). 245 switch hcl.ExprAsKeyword(expr) { 246 case "list": 247 return cty.List(cty.DynamicPseudoType), nil, VariableParseHCL, nil 248 case "map": 249 return cty.Map(cty.DynamicPseudoType), nil, VariableParseHCL, nil 250 } 251 252 ty, typeDefaults, diags := typeexpr.TypeConstraintWithDefaults(expr) 253 if diags.HasErrors() { 254 return cty.DynamicPseudoType, nil, VariableParseHCL, diags 255 } 256 257 switch { 258 case ty.IsPrimitiveType(): 259 // Primitive types use literal parsing. 260 return ty, typeDefaults, VariableParseLiteral, diags 261 default: 262 // Everything else uses HCL parsing 263 return ty, typeDefaults, VariableParseHCL, diags 264 } 265 } 266 267 func (v *Variable) Addr() addrs.InputVariable { 268 return addrs.InputVariable{Name: v.Name} 269 } 270 271 // Required returns true if this variable is required to be set by the caller, 272 // or false if there is a default value that will be used when it isn't set. 273 func (v *Variable) Required() bool { 274 return v.Default == cty.NilVal 275 } 276 277 // VariableParsingMode defines how values of a particular variable given by 278 // text-only mechanisms (command line arguments and environment variables) 279 // should be parsed to produce the final value. 280 type VariableParsingMode rune 281 282 // VariableParseLiteral is a variable parsing mode that just takes the given 283 // string directly as a cty.String value. 284 const VariableParseLiteral VariableParsingMode = 'L' 285 286 // VariableParseHCL is a variable parsing mode that attempts to parse the given 287 // string as an HCL expression and returns the result. 288 const VariableParseHCL VariableParsingMode = 'H' 289 290 // Parse uses the receiving parsing mode to process the given variable value 291 // string, returning the result along with any diagnostics. 292 // 293 // A VariableParsingMode does not know the expected type of the corresponding 294 // variable, so it's the caller's responsibility to attempt to convert the 295 // result to the appropriate type and return to the user any diagnostics that 296 // conversion may produce. 297 // 298 // The given name is used to create a synthetic filename in case any diagnostics 299 // must be generated about the given string value. This should be the name 300 // of the root module variable whose value will be populated from the given 301 // string. 302 // 303 // If the returned diagnostics has errors, the returned value may not be 304 // valid. 305 func (m VariableParsingMode) Parse(name, value string) (cty.Value, hcl.Diagnostics) { 306 switch m { 307 case VariableParseLiteral: 308 return cty.StringVal(value), nil 309 case VariableParseHCL: 310 fakeFilename := fmt.Sprintf("<value for var.%s>", name) 311 expr, diags := hclsyntax.ParseExpression([]byte(value), fakeFilename, hcl.Pos{Line: 1, Column: 1}) 312 if diags.HasErrors() { 313 return cty.DynamicVal, diags 314 } 315 val, valDiags := expr.Value(nil) 316 diags = append(diags, valDiags...) 317 return val, diags 318 default: 319 // Should never happen 320 panic(fmt.Errorf("Parse called on invalid VariableParsingMode %#v", m)) 321 } 322 } 323 324 // decodeVariableValidationBlock is a wrapper around decodeCheckRuleBlock 325 // that imposes the additional rule that the condition expression can refer 326 // only to an input variable of the given name. 327 func decodeVariableValidationBlock(varName string, block *hcl.Block, override bool) (*CheckRule, hcl.Diagnostics) { 328 vv, diags := decodeCheckRuleBlock(block, override) 329 if vv.Condition != nil { 330 // The validation condition can only refer to the variable itself, 331 // to ensure that the variable declaration can't create additional 332 // edges in the dependency graph. 333 goodRefs := 0 334 for _, traversal := range vv.Condition.Variables() { 335 ref, moreDiags := addrs.ParseRef(traversal) 336 if !moreDiags.HasErrors() { 337 if addr, ok := ref.Subject.(addrs.InputVariable); ok { 338 if addr.Name == varName { 339 goodRefs++ 340 continue // Reference is valid 341 } 342 } 343 } 344 // If we fall out here then the reference is invalid. 345 diags = diags.Append(&hcl.Diagnostic{ 346 Severity: hcl.DiagError, 347 Summary: "Invalid reference in variable validation", 348 Detail: fmt.Sprintf("The condition for variable %q can only refer to the variable itself, using var.%s.", varName, varName), 349 Subject: traversal.SourceRange().Ptr(), 350 }) 351 } 352 if goodRefs < 1 { 353 diags = diags.Append(&hcl.Diagnostic{ 354 Severity: hcl.DiagError, 355 Summary: "Invalid variable validation condition", 356 Detail: fmt.Sprintf("The condition for variable %q must refer to var.%s in order to test incoming values.", varName, varName), 357 Subject: vv.Condition.Range().Ptr(), 358 }) 359 } 360 } 361 362 if vv.ErrorMessage != nil { 363 // The same applies to the validation error message, except that 364 // references are not required. A string literal is a valid error 365 // message. 366 goodRefs := 0 367 for _, traversal := range vv.ErrorMessage.Variables() { 368 ref, moreDiags := addrs.ParseRef(traversal) 369 if !moreDiags.HasErrors() { 370 if addr, ok := ref.Subject.(addrs.InputVariable); ok { 371 if addr.Name == varName { 372 goodRefs++ 373 continue // Reference is valid 374 } 375 } 376 } 377 // If we fall out here then the reference is invalid. 378 diags = diags.Append(&hcl.Diagnostic{ 379 Severity: hcl.DiagError, 380 Summary: "Invalid reference in variable validation", 381 Detail: fmt.Sprintf("The error message for variable %q can only refer to the variable itself, using var.%s.", varName, varName), 382 Subject: traversal.SourceRange().Ptr(), 383 }) 384 } 385 } 386 387 return vv, diags 388 } 389 390 // Output represents an "output" block in a module or file. 391 type Output struct { 392 Name string 393 Description string 394 Expr hcl.Expression 395 DependsOn []hcl.Traversal 396 Sensitive bool 397 398 Preconditions []*CheckRule 399 400 DescriptionSet bool 401 SensitiveSet bool 402 403 DeclRange hcl.Range 404 } 405 406 func decodeOutputBlock(block *hcl.Block, override bool) (*Output, hcl.Diagnostics) { 407 var diags hcl.Diagnostics 408 409 o := &Output{ 410 Name: block.Labels[0], 411 DeclRange: block.DefRange, 412 } 413 414 schema := outputBlockSchema 415 if override { 416 schema = schemaForOverrides(schema) 417 } 418 419 content, moreDiags := block.Body.Content(schema) 420 diags = append(diags, moreDiags...) 421 422 if !hclsyntax.ValidIdentifier(o.Name) { 423 diags = append(diags, &hcl.Diagnostic{ 424 Severity: hcl.DiagError, 425 Summary: "Invalid output name", 426 Detail: badIdentifierDetail, 427 Subject: &block.LabelRanges[0], 428 }) 429 } 430 431 if attr, exists := content.Attributes["description"]; exists { 432 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Description) 433 diags = append(diags, valDiags...) 434 o.DescriptionSet = true 435 } 436 437 if attr, exists := content.Attributes["value"]; exists { 438 o.Expr = attr.Expr 439 } 440 441 if attr, exists := content.Attributes["sensitive"]; exists { 442 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Sensitive) 443 diags = append(diags, valDiags...) 444 o.SensitiveSet = true 445 } 446 447 if attr, exists := content.Attributes["depends_on"]; exists { 448 deps, depsDiags := decodeDependsOn(attr) 449 diags = append(diags, depsDiags...) 450 o.DependsOn = append(o.DependsOn, deps...) 451 } 452 453 for _, block := range content.Blocks { 454 switch block.Type { 455 case "precondition": 456 cr, moreDiags := decodeCheckRuleBlock(block, override) 457 diags = append(diags, moreDiags...) 458 o.Preconditions = append(o.Preconditions, cr) 459 case "postcondition": 460 diags = append(diags, &hcl.Diagnostic{ 461 Severity: hcl.DiagError, 462 Summary: "Postconditions are not allowed", 463 Detail: "Output values can only have preconditions, not postconditions.", 464 Subject: block.TypeRange.Ptr(), 465 }) 466 default: 467 // The cases above should be exhaustive for all block types 468 // defined in the block type schema, so this shouldn't happen. 469 panic(fmt.Sprintf("unexpected lifecycle sub-block type %q", block.Type)) 470 } 471 } 472 473 return o, diags 474 } 475 476 func (o *Output) Addr() addrs.OutputValue { 477 return addrs.OutputValue{Name: o.Name} 478 } 479 480 // Local represents a single entry from a "locals" block in a module or file. 481 // The "locals" block itself is not represented, because it serves only to 482 // provide context for us to interpret its contents. 483 type Local struct { 484 Name string 485 Expr hcl.Expression 486 487 DeclRange hcl.Range 488 } 489 490 func decodeLocalsBlock(block *hcl.Block) ([]*Local, hcl.Diagnostics) { 491 attrs, diags := block.Body.JustAttributes() 492 if len(attrs) == 0 { 493 return nil, diags 494 } 495 496 locals := make([]*Local, 0, len(attrs)) 497 for name, attr := range attrs { 498 if !hclsyntax.ValidIdentifier(name) { 499 diags = append(diags, &hcl.Diagnostic{ 500 Severity: hcl.DiagError, 501 Summary: "Invalid local value name", 502 Detail: badIdentifierDetail, 503 Subject: &attr.NameRange, 504 }) 505 } 506 507 locals = append(locals, &Local{ 508 Name: name, 509 Expr: attr.Expr, 510 DeclRange: attr.Range, 511 }) 512 } 513 return locals, diags 514 } 515 516 // Addr returns the address of the local value declared by the receiver, 517 // relative to its containing module. 518 func (l *Local) Addr() addrs.LocalValue { 519 return addrs.LocalValue{ 520 Name: l.Name, 521 } 522 } 523 524 var variableBlockSchema = &hcl.BodySchema{ 525 Attributes: []hcl.AttributeSchema{ 526 { 527 Name: "description", 528 }, 529 { 530 Name: "default", 531 }, 532 { 533 Name: "type", 534 }, 535 { 536 Name: "sensitive", 537 }, 538 { 539 Name: "nullable", 540 }, 541 }, 542 Blocks: []hcl.BlockHeaderSchema{ 543 { 544 Type: "validation", 545 }, 546 }, 547 } 548 549 var outputBlockSchema = &hcl.BodySchema{ 550 Attributes: []hcl.AttributeSchema{ 551 { 552 Name: "description", 553 }, 554 { 555 Name: "value", 556 Required: true, 557 }, 558 { 559 Name: "depends_on", 560 }, 561 { 562 Name: "sensitive", 563 }, 564 }, 565 Blocks: []hcl.BlockHeaderSchema{ 566 {Type: "precondition"}, 567 {Type: "postcondition"}, 568 }, 569 }