github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/configs/resource.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package configs 5 6 import ( 7 "fmt" 8 9 "github.com/hashicorp/hcl/v2" 10 "github.com/hashicorp/hcl/v2/gohcl" 11 "github.com/hashicorp/hcl/v2/hclsyntax" 12 hcljson "github.com/hashicorp/hcl/v2/json" 13 14 "github.com/terramate-io/tf/addrs" 15 "github.com/terramate-io/tf/lang" 16 "github.com/terramate-io/tf/tfdiags" 17 ) 18 19 // Resource represents a "resource" or "data" block in a module or file. 20 type Resource struct { 21 Mode addrs.ResourceMode 22 Name string 23 Type string 24 Config hcl.Body 25 Count hcl.Expression 26 ForEach hcl.Expression 27 28 ProviderConfigRef *ProviderConfigRef 29 Provider addrs.Provider 30 31 Preconditions []*CheckRule 32 Postconditions []*CheckRule 33 34 DependsOn []hcl.Traversal 35 36 TriggersReplacement []hcl.Expression 37 38 // Managed is populated only for Mode = addrs.ManagedResourceMode, 39 // containing the additional fields that apply to managed resources. 40 // For all other resource modes, this field is nil. 41 Managed *ManagedResource 42 43 // Container links a scoped resource back up to the resources that contains 44 // it. This field is referenced during static analysis to check whether any 45 // references are also made from within the same container. 46 // 47 // If this is nil, then this resource is essentially public. 48 Container Container 49 50 DeclRange hcl.Range 51 TypeRange hcl.Range 52 } 53 54 // ManagedResource represents a "resource" block in a module or file. 55 type ManagedResource struct { 56 Connection *Connection 57 Provisioners []*Provisioner 58 59 CreateBeforeDestroy bool 60 PreventDestroy bool 61 IgnoreChanges []hcl.Traversal 62 IgnoreAllChanges bool 63 64 CreateBeforeDestroySet bool 65 PreventDestroySet bool 66 } 67 68 func (r *Resource) moduleUniqueKey() string { 69 return r.Addr().String() 70 } 71 72 // Addr returns a resource address for the receiver that is relative to the 73 // resource's containing module. 74 func (r *Resource) Addr() addrs.Resource { 75 return addrs.Resource{ 76 Mode: r.Mode, 77 Type: r.Type, 78 Name: r.Name, 79 } 80 } 81 82 // ProviderConfigAddr returns the address for the provider configuration that 83 // should be used for this resource. This function returns a default provider 84 // config addr if an explicit "provider" argument was not provided. 85 func (r *Resource) ProviderConfigAddr() addrs.LocalProviderConfig { 86 if r.ProviderConfigRef == nil { 87 // If no specific "provider" argument is given, we want to look up the 88 // provider config where the local name matches the implied provider 89 // from the resource type. This may be different from the resource's 90 // provider type. 91 return addrs.LocalProviderConfig{ 92 LocalName: r.Addr().ImpliedProvider(), 93 } 94 } 95 96 return addrs.LocalProviderConfig{ 97 LocalName: r.ProviderConfigRef.Name, 98 Alias: r.ProviderConfigRef.Alias, 99 } 100 } 101 102 // HasCustomConditions returns true if and only if the resource has at least 103 // one author-specified custom condition. 104 func (r *Resource) HasCustomConditions() bool { 105 return len(r.Postconditions) != 0 || len(r.Preconditions) != 0 106 } 107 108 func decodeResourceBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagnostics) { 109 var diags hcl.Diagnostics 110 r := &Resource{ 111 Mode: addrs.ManagedResourceMode, 112 Type: block.Labels[0], 113 Name: block.Labels[1], 114 DeclRange: block.DefRange, 115 TypeRange: block.LabelRanges[0], 116 Managed: &ManagedResource{}, 117 } 118 119 content, remain, moreDiags := block.Body.PartialContent(ResourceBlockSchema) 120 diags = append(diags, moreDiags...) 121 r.Config = remain 122 123 if !hclsyntax.ValidIdentifier(r.Type) { 124 diags = append(diags, &hcl.Diagnostic{ 125 Severity: hcl.DiagError, 126 Summary: "Invalid resource type name", 127 Detail: badIdentifierDetail, 128 Subject: &block.LabelRanges[0], 129 }) 130 } 131 if !hclsyntax.ValidIdentifier(r.Name) { 132 diags = append(diags, &hcl.Diagnostic{ 133 Severity: hcl.DiagError, 134 Summary: "Invalid resource name", 135 Detail: badIdentifierDetail, 136 Subject: &block.LabelRanges[1], 137 }) 138 } 139 140 if attr, exists := content.Attributes["count"]; exists { 141 r.Count = attr.Expr 142 } 143 144 if attr, exists := content.Attributes["for_each"]; exists { 145 r.ForEach = attr.Expr 146 // Cannot have count and for_each on the same resource block 147 if r.Count != nil { 148 diags = append(diags, &hcl.Diagnostic{ 149 Severity: hcl.DiagError, 150 Summary: `Invalid combination of "count" and "for_each"`, 151 Detail: `The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used to be explicit about the number of resources to be created.`, 152 Subject: &attr.NameRange, 153 }) 154 } 155 } 156 157 if attr, exists := content.Attributes["provider"]; exists { 158 var providerDiags hcl.Diagnostics 159 r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider") 160 diags = append(diags, providerDiags...) 161 } 162 163 if attr, exists := content.Attributes["depends_on"]; exists { 164 deps, depsDiags := decodeDependsOn(attr) 165 diags = append(diags, depsDiags...) 166 r.DependsOn = append(r.DependsOn, deps...) 167 } 168 169 var seenLifecycle *hcl.Block 170 var seenConnection *hcl.Block 171 var seenEscapeBlock *hcl.Block 172 for _, block := range content.Blocks { 173 switch block.Type { 174 case "lifecycle": 175 if seenLifecycle != nil { 176 diags = append(diags, &hcl.Diagnostic{ 177 Severity: hcl.DiagError, 178 Summary: "Duplicate lifecycle block", 179 Detail: fmt.Sprintf("This resource already has a lifecycle block at %s.", seenLifecycle.DefRange), 180 Subject: &block.DefRange, 181 }) 182 continue 183 } 184 seenLifecycle = block 185 186 lcContent, lcDiags := block.Body.Content(resourceLifecycleBlockSchema) 187 diags = append(diags, lcDiags...) 188 189 if attr, exists := lcContent.Attributes["create_before_destroy"]; exists { 190 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.Managed.CreateBeforeDestroy) 191 diags = append(diags, valDiags...) 192 r.Managed.CreateBeforeDestroySet = true 193 } 194 195 if attr, exists := lcContent.Attributes["prevent_destroy"]; exists { 196 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.Managed.PreventDestroy) 197 diags = append(diags, valDiags...) 198 r.Managed.PreventDestroySet = true 199 } 200 201 if attr, exists := lcContent.Attributes["replace_triggered_by"]; exists { 202 exprs, hclDiags := decodeReplaceTriggeredBy(attr.Expr) 203 diags = diags.Extend(hclDiags) 204 205 r.TriggersReplacement = append(r.TriggersReplacement, exprs...) 206 } 207 208 if attr, exists := lcContent.Attributes["ignore_changes"]; exists { 209 210 // ignore_changes can either be a list of relative traversals 211 // or it can be just the keyword "all" to ignore changes to this 212 // resource entirely. 213 // ignore_changes = [ami, instance_type] 214 // ignore_changes = all 215 // We also allow two legacy forms for compatibility with earlier 216 // versions: 217 // ignore_changes = ["ami", "instance_type"] 218 // ignore_changes = ["*"] 219 220 kw := hcl.ExprAsKeyword(attr.Expr) 221 222 switch { 223 case kw == "all": 224 r.Managed.IgnoreAllChanges = true 225 default: 226 exprs, listDiags := hcl.ExprList(attr.Expr) 227 diags = append(diags, listDiags...) 228 229 var ignoreAllRange hcl.Range 230 231 for _, expr := range exprs { 232 233 // our expr might be the literal string "*", which 234 // we accept as a deprecated way of saying "all". 235 if shimIsIgnoreChangesStar(expr) { 236 r.Managed.IgnoreAllChanges = true 237 ignoreAllRange = expr.Range() 238 diags = append(diags, &hcl.Diagnostic{ 239 Severity: hcl.DiagError, 240 Summary: "Invalid ignore_changes wildcard", 241 Detail: "The [\"*\"] form of ignore_changes wildcard is was deprecated and is now invalid. Use \"ignore_changes = all\" to ignore changes to all attributes.", 242 Subject: attr.Expr.Range().Ptr(), 243 }) 244 continue 245 } 246 247 expr, shimDiags := shimTraversalInString(expr, false) 248 diags = append(diags, shimDiags...) 249 250 traversal, travDiags := hcl.RelTraversalForExpr(expr) 251 diags = append(diags, travDiags...) 252 if len(traversal) != 0 { 253 r.Managed.IgnoreChanges = append(r.Managed.IgnoreChanges, traversal) 254 } 255 } 256 257 if r.Managed.IgnoreAllChanges && len(r.Managed.IgnoreChanges) != 0 { 258 diags = append(diags, &hcl.Diagnostic{ 259 Severity: hcl.DiagError, 260 Summary: "Invalid ignore_changes ruleset", 261 Detail: "Cannot mix wildcard string \"*\" with non-wildcard references.", 262 Subject: &ignoreAllRange, 263 Context: attr.Expr.Range().Ptr(), 264 }) 265 } 266 267 } 268 } 269 270 for _, block := range lcContent.Blocks { 271 switch block.Type { 272 case "precondition", "postcondition": 273 cr, moreDiags := decodeCheckRuleBlock(block, override) 274 diags = append(diags, moreDiags...) 275 276 moreDiags = cr.validateSelfReferences(block.Type, r.Addr()) 277 diags = append(diags, moreDiags...) 278 279 switch block.Type { 280 case "precondition": 281 r.Preconditions = append(r.Preconditions, cr) 282 case "postcondition": 283 r.Postconditions = append(r.Postconditions, cr) 284 } 285 default: 286 // The cases above should be exhaustive for all block types 287 // defined in the lifecycle schema, so this shouldn't happen. 288 panic(fmt.Sprintf("unexpected lifecycle sub-block type %q", block.Type)) 289 } 290 } 291 292 case "connection": 293 if seenConnection != nil { 294 diags = append(diags, &hcl.Diagnostic{ 295 Severity: hcl.DiagError, 296 Summary: "Duplicate connection block", 297 Detail: fmt.Sprintf("This resource already has a connection block at %s.", seenConnection.DefRange), 298 Subject: &block.DefRange, 299 }) 300 continue 301 } 302 seenConnection = block 303 304 r.Managed.Connection = &Connection{ 305 Config: block.Body, 306 DeclRange: block.DefRange, 307 } 308 309 case "provisioner": 310 pv, pvDiags := decodeProvisionerBlock(block) 311 diags = append(diags, pvDiags...) 312 if pv != nil { 313 r.Managed.Provisioners = append(r.Managed.Provisioners, pv) 314 } 315 316 case "_": 317 if seenEscapeBlock != nil { 318 diags = append(diags, &hcl.Diagnostic{ 319 Severity: hcl.DiagError, 320 Summary: "Duplicate escaping block", 321 Detail: fmt.Sprintf( 322 "The special block type \"_\" can be used to force particular arguments to be interpreted as resource-type-specific rather than as meta-arguments, but each resource block can have only one such block. The first escaping block was at %s.", 323 seenEscapeBlock.DefRange, 324 ), 325 Subject: &block.DefRange, 326 }) 327 continue 328 } 329 seenEscapeBlock = block 330 331 // When there's an escaping block its content merges with the 332 // existing config we extracted earlier, so later decoding 333 // will see a blend of both. 334 r.Config = hcl.MergeBodies([]hcl.Body{r.Config, block.Body}) 335 336 default: 337 // Any other block types are ones we've reserved for future use, 338 // so they get a generic message. 339 diags = append(diags, &hcl.Diagnostic{ 340 Severity: hcl.DiagError, 341 Summary: "Reserved block type name in resource block", 342 Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type), 343 Subject: &block.TypeRange, 344 }) 345 } 346 } 347 348 // Now we can validate the connection block references if there are any destroy provisioners. 349 // TODO: should we eliminate standalone connection blocks? 350 if r.Managed.Connection != nil { 351 for _, p := range r.Managed.Provisioners { 352 if p.When == ProvisionerWhenDestroy { 353 diags = append(diags, onlySelfRefs(r.Managed.Connection.Config)...) 354 break 355 } 356 } 357 } 358 359 return r, diags 360 } 361 362 func decodeDataBlock(block *hcl.Block, override, nested bool) (*Resource, hcl.Diagnostics) { 363 var diags hcl.Diagnostics 364 r := &Resource{ 365 Mode: addrs.DataResourceMode, 366 Type: block.Labels[0], 367 Name: block.Labels[1], 368 DeclRange: block.DefRange, 369 TypeRange: block.LabelRanges[0], 370 } 371 372 content, remain, moreDiags := block.Body.PartialContent(dataBlockSchema) 373 diags = append(diags, moreDiags...) 374 r.Config = remain 375 376 if !hclsyntax.ValidIdentifier(r.Type) { 377 diags = append(diags, &hcl.Diagnostic{ 378 Severity: hcl.DiagError, 379 Summary: "Invalid data source name", 380 Detail: badIdentifierDetail, 381 Subject: &block.LabelRanges[0], 382 }) 383 } 384 if !hclsyntax.ValidIdentifier(r.Name) { 385 diags = append(diags, &hcl.Diagnostic{ 386 Severity: hcl.DiagError, 387 Summary: "Invalid data resource name", 388 Detail: badIdentifierDetail, 389 Subject: &block.LabelRanges[1], 390 }) 391 } 392 393 if attr, exists := content.Attributes["count"]; exists && !nested { 394 r.Count = attr.Expr 395 } else if exists && nested { 396 // We don't allow count attributes in nested data blocks. 397 diags = append(diags, &hcl.Diagnostic{ 398 Severity: hcl.DiagError, 399 Summary: `Invalid "count" attribute`, 400 Detail: `The "count" and "for_each" meta-arguments are not supported within nested data blocks.`, 401 Subject: &attr.NameRange, 402 }) 403 } 404 405 if attr, exists := content.Attributes["for_each"]; exists && !nested { 406 r.ForEach = attr.Expr 407 // Cannot have count and for_each on the same data block 408 if r.Count != nil { 409 diags = append(diags, &hcl.Diagnostic{ 410 Severity: hcl.DiagError, 411 Summary: `Invalid combination of "count" and "for_each"`, 412 Detail: `The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used to be explicit about the number of resources to be created.`, 413 Subject: &attr.NameRange, 414 }) 415 } 416 } else if exists && nested { 417 // We don't allow for_each attributes in nested data blocks. 418 diags = append(diags, &hcl.Diagnostic{ 419 Severity: hcl.DiagError, 420 Summary: `Invalid "for_each" attribute`, 421 Detail: `The "count" and "for_each" meta-arguments are not supported within nested data blocks.`, 422 Subject: &attr.NameRange, 423 }) 424 } 425 426 if attr, exists := content.Attributes["provider"]; exists { 427 var providerDiags hcl.Diagnostics 428 r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider") 429 diags = append(diags, providerDiags...) 430 } 431 432 if attr, exists := content.Attributes["depends_on"]; exists { 433 deps, depsDiags := decodeDependsOn(attr) 434 diags = append(diags, depsDiags...) 435 r.DependsOn = append(r.DependsOn, deps...) 436 } 437 438 var seenEscapeBlock *hcl.Block 439 var seenLifecycle *hcl.Block 440 for _, block := range content.Blocks { 441 switch block.Type { 442 443 case "_": 444 if seenEscapeBlock != nil { 445 diags = append(diags, &hcl.Diagnostic{ 446 Severity: hcl.DiagError, 447 Summary: "Duplicate escaping block", 448 Detail: fmt.Sprintf( 449 "The special block type \"_\" can be used to force particular arguments to be interpreted as resource-type-specific rather than as meta-arguments, but each data block can have only one such block. The first escaping block was at %s.", 450 seenEscapeBlock.DefRange, 451 ), 452 Subject: &block.DefRange, 453 }) 454 continue 455 } 456 seenEscapeBlock = block 457 458 // When there's an escaping block its content merges with the 459 // existing config we extracted earlier, so later decoding 460 // will see a blend of both. 461 r.Config = hcl.MergeBodies([]hcl.Body{r.Config, block.Body}) 462 463 case "lifecycle": 464 if nested { 465 // We don't allow lifecycle arguments in nested data blocks, 466 // the lifecycle is managed by the parent block. 467 diags = append(diags, &hcl.Diagnostic{ 468 Severity: hcl.DiagError, 469 Summary: "Invalid lifecycle block", 470 Detail: `Nested data blocks do not support "lifecycle" blocks as the lifecycle is managed by the containing block.`, 471 Subject: block.DefRange.Ptr(), 472 }) 473 } 474 475 if seenLifecycle != nil { 476 diags = append(diags, &hcl.Diagnostic{ 477 Severity: hcl.DiagError, 478 Summary: "Duplicate lifecycle block", 479 Detail: fmt.Sprintf("This resource already has a lifecycle block at %s.", seenLifecycle.DefRange), 480 Subject: block.DefRange.Ptr(), 481 }) 482 continue 483 } 484 seenLifecycle = block 485 486 lcContent, lcDiags := block.Body.Content(resourceLifecycleBlockSchema) 487 diags = append(diags, lcDiags...) 488 489 // All of the attributes defined for resource lifecycle are for 490 // managed resources only, so we can emit a common error message 491 // for any given attributes that HCL accepted. 492 for name, attr := range lcContent.Attributes { 493 diags = append(diags, &hcl.Diagnostic{ 494 Severity: hcl.DiagError, 495 Summary: "Invalid data resource lifecycle argument", 496 Detail: fmt.Sprintf("The lifecycle argument %q is defined only for managed resources (\"resource\" blocks), and is not valid for data resources.", name), 497 Subject: attr.NameRange.Ptr(), 498 }) 499 } 500 501 for _, block := range lcContent.Blocks { 502 switch block.Type { 503 case "precondition", "postcondition": 504 cr, moreDiags := decodeCheckRuleBlock(block, override) 505 diags = append(diags, moreDiags...) 506 507 moreDiags = cr.validateSelfReferences(block.Type, r.Addr()) 508 diags = append(diags, moreDiags...) 509 510 switch block.Type { 511 case "precondition": 512 r.Preconditions = append(r.Preconditions, cr) 513 case "postcondition": 514 r.Postconditions = append(r.Postconditions, cr) 515 } 516 default: 517 // The cases above should be exhaustive for all block types 518 // defined in the lifecycle schema, so this shouldn't happen. 519 panic(fmt.Sprintf("unexpected lifecycle sub-block type %q", block.Type)) 520 } 521 } 522 523 default: 524 // Any other block types are ones we're reserving for future use, 525 // but don't have any defined meaning today. 526 diags = append(diags, &hcl.Diagnostic{ 527 Severity: hcl.DiagError, 528 Summary: "Reserved block type name in data block", 529 Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type), 530 Subject: block.TypeRange.Ptr(), 531 }) 532 } 533 } 534 535 return r, diags 536 } 537 538 // decodeReplaceTriggeredBy decodes and does basic validation of the 539 // replace_triggered_by expressions, ensuring they only contains references to 540 // a single resource, and the only extra variables are count.index or each.key. 541 func decodeReplaceTriggeredBy(expr hcl.Expression) ([]hcl.Expression, hcl.Diagnostics) { 542 // Since we are manually parsing the replace_triggered_by argument, we 543 // need to specially handle json configs, in which case the values will 544 // be json strings rather than hcl. To simplify parsing however we will 545 // decode the individual list elements, rather than the entire expression. 546 isJSON := hcljson.IsJSONExpression(expr) 547 548 exprs, diags := hcl.ExprList(expr) 549 550 for i, expr := range exprs { 551 if isJSON { 552 // We can abuse the hcl json api and rely on the fact that calling 553 // Value on a json expression with no EvalContext will return the 554 // raw string. We can then parse that as normal hcl syntax, and 555 // continue with the decoding. 556 v, ds := expr.Value(nil) 557 diags = diags.Extend(ds) 558 if diags.HasErrors() { 559 continue 560 } 561 562 expr, ds = hclsyntax.ParseExpression([]byte(v.AsString()), "", expr.Range().Start) 563 diags = diags.Extend(ds) 564 if diags.HasErrors() { 565 continue 566 } 567 // make sure to swap out the expression we're returning too 568 exprs[i] = expr 569 } 570 571 refs, refDiags := lang.ReferencesInExpr(addrs.ParseRef, expr) 572 for _, diag := range refDiags { 573 severity := hcl.DiagError 574 if diag.Severity() == tfdiags.Warning { 575 severity = hcl.DiagWarning 576 } 577 578 desc := diag.Description() 579 580 diags = append(diags, &hcl.Diagnostic{ 581 Severity: severity, 582 Summary: desc.Summary, 583 Detail: desc.Detail, 584 Subject: expr.Range().Ptr(), 585 }) 586 } 587 588 if refDiags.HasErrors() { 589 continue 590 } 591 592 resourceCount := 0 593 for _, ref := range refs { 594 switch sub := ref.Subject.(type) { 595 case addrs.Resource, addrs.ResourceInstance: 596 resourceCount++ 597 598 case addrs.ForEachAttr: 599 if sub.Name != "key" { 600 diags = append(diags, &hcl.Diagnostic{ 601 Severity: hcl.DiagError, 602 Summary: "Invalid each reference in replace_triggered_by expression", 603 Detail: "Only each.key may be used in replace_triggered_by.", 604 Subject: expr.Range().Ptr(), 605 }) 606 } 607 case addrs.CountAttr: 608 if sub.Name != "index" { 609 diags = append(diags, &hcl.Diagnostic{ 610 Severity: hcl.DiagError, 611 Summary: "Invalid count reference in replace_triggered_by expression", 612 Detail: "Only count.index may be used in replace_triggered_by.", 613 Subject: expr.Range().Ptr(), 614 }) 615 } 616 default: 617 // everything else should be simple traversals 618 diags = append(diags, &hcl.Diagnostic{ 619 Severity: hcl.DiagError, 620 Summary: "Invalid reference in replace_triggered_by expression", 621 Detail: "Only resources, count.index, and each.key may be used in replace_triggered_by.", 622 Subject: expr.Range().Ptr(), 623 }) 624 } 625 } 626 627 switch { 628 case resourceCount == 0: 629 diags = append(diags, &hcl.Diagnostic{ 630 Severity: hcl.DiagError, 631 Summary: "Invalid replace_triggered_by expression", 632 Detail: "Missing resource reference in replace_triggered_by expression.", 633 Subject: expr.Range().Ptr(), 634 }) 635 case resourceCount > 1: 636 diags = append(diags, &hcl.Diagnostic{ 637 Severity: hcl.DiagError, 638 Summary: "Invalid replace_triggered_by expression", 639 Detail: "Multiple resource references in replace_triggered_by expression.", 640 Subject: expr.Range().Ptr(), 641 }) 642 } 643 } 644 return exprs, diags 645 } 646 647 type ProviderConfigRef struct { 648 Name string 649 NameRange hcl.Range 650 Alias string 651 AliasRange *hcl.Range // nil if alias not set 652 653 // TODO: this may not be set in some cases, so it is not yet suitable for 654 // use outside of this package. We currently only use it for internal 655 // validation, but once we verify that this can be set in all cases, we can 656 // export this so providers don't need to be re-resolved. 657 // This same field is also added to the Provider struct. 658 providerType addrs.Provider 659 } 660 661 func decodeProviderConfigRef(expr hcl.Expression, argName string) (*ProviderConfigRef, hcl.Diagnostics) { 662 var diags hcl.Diagnostics 663 664 var shimDiags hcl.Diagnostics 665 expr, shimDiags = shimTraversalInString(expr, false) 666 diags = append(diags, shimDiags...) 667 668 traversal, travDiags := hcl.AbsTraversalForExpr(expr) 669 670 // AbsTraversalForExpr produces only generic errors, so we'll discard 671 // the errors given and produce our own with extra context. If we didn't 672 // get any errors then we might still have warnings, though. 673 if !travDiags.HasErrors() { 674 diags = append(diags, travDiags...) 675 } 676 677 if len(traversal) < 1 || len(traversal) > 2 { 678 // A provider reference was given as a string literal in the legacy 679 // configuration language and there are lots of examples out there 680 // showing that usage, so we'll sniff for that situation here and 681 // produce a specialized error message for it to help users find 682 // the new correct form. 683 if exprIsNativeQuotedString(expr) { 684 diags = append(diags, &hcl.Diagnostic{ 685 Severity: hcl.DiagError, 686 Summary: "Invalid provider configuration reference", 687 Detail: "A provider configuration reference must not be given in quotes.", 688 Subject: expr.Range().Ptr(), 689 }) 690 return nil, diags 691 } 692 693 diags = append(diags, &hcl.Diagnostic{ 694 Severity: hcl.DiagError, 695 Summary: "Invalid provider configuration reference", 696 Detail: fmt.Sprintf("The %s argument requires a provider type name, optionally followed by a period and then a configuration alias.", argName), 697 Subject: expr.Range().Ptr(), 698 }) 699 return nil, diags 700 } 701 702 // verify that the provider local name is normalized 703 name := traversal.RootName() 704 nameDiags := checkProviderNameNormalized(name, traversal[0].SourceRange()) 705 diags = append(diags, nameDiags...) 706 if diags.HasErrors() { 707 return nil, diags 708 } 709 710 ret := &ProviderConfigRef{ 711 Name: name, 712 NameRange: traversal[0].SourceRange(), 713 } 714 715 if len(traversal) > 1 { 716 aliasStep, ok := traversal[1].(hcl.TraverseAttr) 717 if !ok { 718 diags = append(diags, &hcl.Diagnostic{ 719 Severity: hcl.DiagError, 720 Summary: "Invalid provider configuration reference", 721 Detail: "Provider name must either stand alone or be followed by a period and then a configuration alias.", 722 Subject: traversal[1].SourceRange().Ptr(), 723 }) 724 return ret, diags 725 } 726 727 ret.Alias = aliasStep.Name 728 ret.AliasRange = aliasStep.SourceRange().Ptr() 729 } 730 731 return ret, diags 732 } 733 734 // Addr returns the provider config address corresponding to the receiving 735 // config reference. 736 // 737 // This is a trivial conversion, essentially just discarding the source 738 // location information and keeping just the addressing information. 739 func (r *ProviderConfigRef) Addr() addrs.LocalProviderConfig { 740 return addrs.LocalProviderConfig{ 741 LocalName: r.Name, 742 Alias: r.Alias, 743 } 744 } 745 746 func (r *ProviderConfigRef) String() string { 747 if r == nil { 748 return "<nil>" 749 } 750 if r.Alias != "" { 751 return fmt.Sprintf("%s.%s", r.Name, r.Alias) 752 } 753 return r.Name 754 } 755 756 var commonResourceAttributes = []hcl.AttributeSchema{ 757 { 758 Name: "count", 759 }, 760 { 761 Name: "for_each", 762 }, 763 { 764 Name: "provider", 765 }, 766 { 767 Name: "depends_on", 768 }, 769 } 770 771 // ResourceBlockSchema is the schema for a resource or data resource type within 772 // Terraform. 773 // 774 // This schema is public as it is required elsewhere in order to validate and 775 // use generated config. 776 var ResourceBlockSchema = &hcl.BodySchema{ 777 Attributes: commonResourceAttributes, 778 Blocks: []hcl.BlockHeaderSchema{ 779 {Type: "locals"}, // reserved for future use 780 {Type: "lifecycle"}, 781 {Type: "connection"}, 782 {Type: "provisioner", LabelNames: []string{"type"}}, 783 {Type: "_"}, // meta-argument escaping block 784 }, 785 } 786 787 var dataBlockSchema = &hcl.BodySchema{ 788 Attributes: commonResourceAttributes, 789 Blocks: []hcl.BlockHeaderSchema{ 790 {Type: "lifecycle"}, 791 {Type: "locals"}, // reserved for future use 792 {Type: "_"}, // meta-argument escaping block 793 }, 794 } 795 796 var resourceLifecycleBlockSchema = &hcl.BodySchema{ 797 // We tell HCL that these elements are all valid for both "resource" 798 // and "data" lifecycle blocks, but the rules are actually more restrictive 799 // than that. We deal with that after decoding so that we can return 800 // more specific error messages than HCL would typically return itself. 801 Attributes: []hcl.AttributeSchema{ 802 { 803 Name: "create_before_destroy", 804 }, 805 { 806 Name: "prevent_destroy", 807 }, 808 { 809 Name: "ignore_changes", 810 }, 811 { 812 Name: "replace_triggered_by", 813 }, 814 }, 815 Blocks: []hcl.BlockHeaderSchema{ 816 {Type: "precondition"}, 817 {Type: "postcondition"}, 818 }, 819 }