github.com/muratcelep/terraform@v1.1.0-beta2-not-internal-4/not-internal/configs/resource.go (about) 1 package configs 2 3 import ( 4 "fmt" 5 6 "github.com/hashicorp/hcl/v2" 7 "github.com/hashicorp/hcl/v2/gohcl" 8 "github.com/hashicorp/hcl/v2/hclsyntax" 9 10 "github.com/muratcelep/terraform/not-internal/addrs" 11 ) 12 13 // Resource represents a "resource" or "data" block in a module or file. 14 type Resource struct { 15 Mode addrs.ResourceMode 16 Name string 17 Type string 18 Config hcl.Body 19 Count hcl.Expression 20 ForEach hcl.Expression 21 22 ProviderConfigRef *ProviderConfigRef 23 Provider addrs.Provider 24 25 DependsOn []hcl.Traversal 26 27 // Managed is populated only for Mode = addrs.ManagedResourceMode, 28 // containing the additional fields that apply to managed resources. 29 // For all other resource modes, this field is nil. 30 Managed *ManagedResource 31 32 DeclRange hcl.Range 33 TypeRange hcl.Range 34 } 35 36 // ManagedResource represents a "resource" block in a module or file. 37 type ManagedResource struct { 38 Connection *Connection 39 Provisioners []*Provisioner 40 41 CreateBeforeDestroy bool 42 PreventDestroy bool 43 IgnoreChanges []hcl.Traversal 44 IgnoreAllChanges bool 45 46 CreateBeforeDestroySet bool 47 PreventDestroySet bool 48 } 49 50 func (r *Resource) moduleUniqueKey() string { 51 return r.Addr().String() 52 } 53 54 // Addr returns a resource address for the receiver that is relative to the 55 // resource's containing module. 56 func (r *Resource) Addr() addrs.Resource { 57 return addrs.Resource{ 58 Mode: r.Mode, 59 Type: r.Type, 60 Name: r.Name, 61 } 62 } 63 64 // ProviderConfigAddr returns the address for the provider configuration that 65 // should be used for this resource. This function returns a default provider 66 // config addr if an explicit "provider" argument was not provided. 67 func (r *Resource) ProviderConfigAddr() addrs.LocalProviderConfig { 68 if r.ProviderConfigRef == nil { 69 // If no specific "provider" argument is given, we want to look up the 70 // provider config where the local name matches the implied provider 71 // from the resource type. This may be different from the resource's 72 // provider type. 73 return addrs.LocalProviderConfig{ 74 LocalName: r.Addr().ImpliedProvider(), 75 } 76 } 77 78 return addrs.LocalProviderConfig{ 79 LocalName: r.ProviderConfigRef.Name, 80 Alias: r.ProviderConfigRef.Alias, 81 } 82 } 83 84 func decodeResourceBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) { 85 var diags hcl.Diagnostics 86 r := &Resource{ 87 Mode: addrs.ManagedResourceMode, 88 Type: block.Labels[0], 89 Name: block.Labels[1], 90 DeclRange: block.DefRange, 91 TypeRange: block.LabelRanges[0], 92 Managed: &ManagedResource{}, 93 } 94 95 content, remain, moreDiags := block.Body.PartialContent(resourceBlockSchema) 96 diags = append(diags, moreDiags...) 97 r.Config = remain 98 99 if !hclsyntax.ValidIdentifier(r.Type) { 100 diags = append(diags, &hcl.Diagnostic{ 101 Severity: hcl.DiagError, 102 Summary: "Invalid resource type name", 103 Detail: badIdentifierDetail, 104 Subject: &block.LabelRanges[0], 105 }) 106 } 107 if !hclsyntax.ValidIdentifier(r.Name) { 108 diags = append(diags, &hcl.Diagnostic{ 109 Severity: hcl.DiagError, 110 Summary: "Invalid resource name", 111 Detail: badIdentifierDetail, 112 Subject: &block.LabelRanges[1], 113 }) 114 } 115 116 if attr, exists := content.Attributes["count"]; exists { 117 r.Count = attr.Expr 118 } 119 120 if attr, exists := content.Attributes["for_each"]; exists { 121 r.ForEach = attr.Expr 122 // Cannot have count and for_each on the same resource block 123 if r.Count != nil { 124 diags = append(diags, &hcl.Diagnostic{ 125 Severity: hcl.DiagError, 126 Summary: `Invalid combination of "count" and "for_each"`, 127 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.`, 128 Subject: &attr.NameRange, 129 }) 130 } 131 } 132 133 if attr, exists := content.Attributes["provider"]; exists { 134 var providerDiags hcl.Diagnostics 135 r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider") 136 diags = append(diags, providerDiags...) 137 } 138 139 if attr, exists := content.Attributes["depends_on"]; exists { 140 deps, depsDiags := decodeDependsOn(attr) 141 diags = append(diags, depsDiags...) 142 r.DependsOn = append(r.DependsOn, deps...) 143 } 144 145 var seenLifecycle *hcl.Block 146 var seenConnection *hcl.Block 147 var seenEscapeBlock *hcl.Block 148 for _, block := range content.Blocks { 149 switch block.Type { 150 case "lifecycle": 151 if seenLifecycle != nil { 152 diags = append(diags, &hcl.Diagnostic{ 153 Severity: hcl.DiagError, 154 Summary: "Duplicate lifecycle block", 155 Detail: fmt.Sprintf("This resource already has a lifecycle block at %s.", seenLifecycle.DefRange), 156 Subject: &block.DefRange, 157 }) 158 continue 159 } 160 seenLifecycle = block 161 162 lcContent, lcDiags := block.Body.Content(resourceLifecycleBlockSchema) 163 diags = append(diags, lcDiags...) 164 165 if attr, exists := lcContent.Attributes["create_before_destroy"]; exists { 166 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.Managed.CreateBeforeDestroy) 167 diags = append(diags, valDiags...) 168 r.Managed.CreateBeforeDestroySet = true 169 } 170 171 if attr, exists := lcContent.Attributes["prevent_destroy"]; exists { 172 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.Managed.PreventDestroy) 173 diags = append(diags, valDiags...) 174 r.Managed.PreventDestroySet = true 175 } 176 177 if attr, exists := lcContent.Attributes["ignore_changes"]; exists { 178 179 // ignore_changes can either be a list of relative traversals 180 // or it can be just the keyword "all" to ignore changes to this 181 // resource entirely. 182 // ignore_changes = [ami, instance_type] 183 // ignore_changes = all 184 // We also allow two legacy forms for compatibility with earlier 185 // versions: 186 // ignore_changes = ["ami", "instance_type"] 187 // ignore_changes = ["*"] 188 189 kw := hcl.ExprAsKeyword(attr.Expr) 190 191 switch { 192 case kw == "all": 193 r.Managed.IgnoreAllChanges = true 194 default: 195 exprs, listDiags := hcl.ExprList(attr.Expr) 196 diags = append(diags, listDiags...) 197 198 var ignoreAllRange hcl.Range 199 200 for _, expr := range exprs { 201 202 // our expr might be the literal string "*", which 203 // we accept as a deprecated way of saying "all". 204 if shimIsIgnoreChangesStar(expr) { 205 r.Managed.IgnoreAllChanges = true 206 ignoreAllRange = expr.Range() 207 diags = append(diags, &hcl.Diagnostic{ 208 Severity: hcl.DiagError, 209 Summary: "Invalid ignore_changes wildcard", 210 Detail: "The [\"*\"] form of ignore_changes wildcard is was deprecated and is now invalid. Use \"ignore_changes = all\" to ignore changes to all attributes.", 211 Subject: attr.Expr.Range().Ptr(), 212 }) 213 continue 214 } 215 216 expr, shimDiags := shimTraversalInString(expr, false) 217 diags = append(diags, shimDiags...) 218 219 traversal, travDiags := hcl.RelTraversalForExpr(expr) 220 diags = append(diags, travDiags...) 221 if len(traversal) != 0 { 222 r.Managed.IgnoreChanges = append(r.Managed.IgnoreChanges, traversal) 223 } 224 } 225 226 if r.Managed.IgnoreAllChanges && len(r.Managed.IgnoreChanges) != 0 { 227 diags = append(diags, &hcl.Diagnostic{ 228 Severity: hcl.DiagError, 229 Summary: "Invalid ignore_changes ruleset", 230 Detail: "Cannot mix wildcard string \"*\" with non-wildcard references.", 231 Subject: &ignoreAllRange, 232 Context: attr.Expr.Range().Ptr(), 233 }) 234 } 235 236 } 237 238 } 239 240 case "connection": 241 if seenConnection != nil { 242 diags = append(diags, &hcl.Diagnostic{ 243 Severity: hcl.DiagError, 244 Summary: "Duplicate connection block", 245 Detail: fmt.Sprintf("This resource already has a connection block at %s.", seenConnection.DefRange), 246 Subject: &block.DefRange, 247 }) 248 continue 249 } 250 seenConnection = block 251 252 r.Managed.Connection = &Connection{ 253 Config: block.Body, 254 DeclRange: block.DefRange, 255 } 256 257 case "provisioner": 258 pv, pvDiags := decodeProvisionerBlock(block) 259 diags = append(diags, pvDiags...) 260 if pv != nil { 261 r.Managed.Provisioners = append(r.Managed.Provisioners, pv) 262 } 263 264 case "_": 265 if seenEscapeBlock != nil { 266 diags = append(diags, &hcl.Diagnostic{ 267 Severity: hcl.DiagError, 268 Summary: "Duplicate escaping block", 269 Detail: fmt.Sprintf( 270 "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.", 271 seenEscapeBlock.DefRange, 272 ), 273 Subject: &block.DefRange, 274 }) 275 continue 276 } 277 seenEscapeBlock = block 278 279 // When there's an escaping block its content merges with the 280 // existing config we extracted earlier, so later decoding 281 // will see a blend of both. 282 r.Config = hcl.MergeBodies([]hcl.Body{r.Config, block.Body}) 283 284 default: 285 // Any other block types are ones we've reserved for future use, 286 // so they get a generic message. 287 diags = append(diags, &hcl.Diagnostic{ 288 Severity: hcl.DiagError, 289 Summary: "Reserved block type name in resource block", 290 Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type), 291 Subject: &block.TypeRange, 292 }) 293 } 294 } 295 296 // Now we can validate the connection block references if there are any destroy provisioners. 297 // TODO: should we eliminate standalone connection blocks? 298 if r.Managed.Connection != nil { 299 for _, p := range r.Managed.Provisioners { 300 if p.When == ProvisionerWhenDestroy { 301 diags = append(diags, onlySelfRefs(r.Managed.Connection.Config)...) 302 break 303 } 304 } 305 } 306 307 return r, diags 308 } 309 310 func decodeDataBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) { 311 var diags hcl.Diagnostics 312 r := &Resource{ 313 Mode: addrs.DataResourceMode, 314 Type: block.Labels[0], 315 Name: block.Labels[1], 316 DeclRange: block.DefRange, 317 TypeRange: block.LabelRanges[0], 318 } 319 320 content, remain, moreDiags := block.Body.PartialContent(dataBlockSchema) 321 diags = append(diags, moreDiags...) 322 r.Config = remain 323 324 if !hclsyntax.ValidIdentifier(r.Type) { 325 diags = append(diags, &hcl.Diagnostic{ 326 Severity: hcl.DiagError, 327 Summary: "Invalid data source name", 328 Detail: badIdentifierDetail, 329 Subject: &block.LabelRanges[0], 330 }) 331 } 332 if !hclsyntax.ValidIdentifier(r.Name) { 333 diags = append(diags, &hcl.Diagnostic{ 334 Severity: hcl.DiagError, 335 Summary: "Invalid data resource name", 336 Detail: badIdentifierDetail, 337 Subject: &block.LabelRanges[1], 338 }) 339 } 340 341 if attr, exists := content.Attributes["count"]; exists { 342 r.Count = attr.Expr 343 } 344 345 if attr, exists := content.Attributes["for_each"]; exists { 346 r.ForEach = attr.Expr 347 // Cannot have count and for_each on the same data block 348 if r.Count != nil { 349 diags = append(diags, &hcl.Diagnostic{ 350 Severity: hcl.DiagError, 351 Summary: `Invalid combination of "count" and "for_each"`, 352 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.`, 353 Subject: &attr.NameRange, 354 }) 355 } 356 } 357 358 if attr, exists := content.Attributes["provider"]; exists { 359 var providerDiags hcl.Diagnostics 360 r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider") 361 diags = append(diags, providerDiags...) 362 } 363 364 if attr, exists := content.Attributes["depends_on"]; exists { 365 deps, depsDiags := decodeDependsOn(attr) 366 diags = append(diags, depsDiags...) 367 r.DependsOn = append(r.DependsOn, deps...) 368 } 369 370 var seenEscapeBlock *hcl.Block 371 for _, block := range content.Blocks { 372 switch block.Type { 373 374 case "_": 375 if seenEscapeBlock != nil { 376 diags = append(diags, &hcl.Diagnostic{ 377 Severity: hcl.DiagError, 378 Summary: "Duplicate escaping block", 379 Detail: fmt.Sprintf( 380 "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.", 381 seenEscapeBlock.DefRange, 382 ), 383 Subject: &block.DefRange, 384 }) 385 continue 386 } 387 seenEscapeBlock = block 388 389 // When there's an escaping block its content merges with the 390 // existing config we extracted earlier, so later decoding 391 // will see a blend of both. 392 r.Config = hcl.MergeBodies([]hcl.Body{r.Config, block.Body}) 393 394 // The rest of these are just here to reserve block type names for future use. 395 case "lifecycle": 396 diags = append(diags, &hcl.Diagnostic{ 397 Severity: hcl.DiagError, 398 Summary: "Unsupported lifecycle block", 399 Detail: "Data resources do not have lifecycle settings, so a lifecycle block is not allowed.", 400 Subject: &block.DefRange, 401 }) 402 403 default: 404 diags = append(diags, &hcl.Diagnostic{ 405 Severity: hcl.DiagError, 406 Summary: "Reserved block type name in data block", 407 Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type), 408 Subject: &block.TypeRange, 409 }) 410 } 411 } 412 413 return r, diags 414 } 415 416 type ProviderConfigRef struct { 417 Name string 418 NameRange hcl.Range 419 Alias string 420 AliasRange *hcl.Range // nil if alias not set 421 422 // TODO: this may not be set in some cases, so it is not yet suitable for 423 // use outside of this package. We currently only use it for not-internal 424 // validation, but once we verify that this can be set in all cases, we can 425 // export this so providers don't need to be re-resolved. 426 // This same field is also added to the Provider struct. 427 providerType addrs.Provider 428 } 429 430 func decodeProviderConfigRef(expr hcl.Expression, argName string) (*ProviderConfigRef, hcl.Diagnostics) { 431 var diags hcl.Diagnostics 432 433 var shimDiags hcl.Diagnostics 434 expr, shimDiags = shimTraversalInString(expr, false) 435 diags = append(diags, shimDiags...) 436 437 traversal, travDiags := hcl.AbsTraversalForExpr(expr) 438 439 // AbsTraversalForExpr produces only generic errors, so we'll discard 440 // the errors given and produce our own with extra context. If we didn't 441 // get any errors then we might still have warnings, though. 442 if !travDiags.HasErrors() { 443 diags = append(diags, travDiags...) 444 } 445 446 if len(traversal) < 1 || len(traversal) > 2 { 447 // A provider reference was given as a string literal in the legacy 448 // configuration language and there are lots of examples out there 449 // showing that usage, so we'll sniff for that situation here and 450 // produce a specialized error message for it to help users find 451 // the new correct form. 452 if exprIsNativeQuotedString(expr) { 453 diags = append(diags, &hcl.Diagnostic{ 454 Severity: hcl.DiagError, 455 Summary: "Invalid provider configuration reference", 456 Detail: "A provider configuration reference must not be given in quotes.", 457 Subject: expr.Range().Ptr(), 458 }) 459 return nil, diags 460 } 461 462 diags = append(diags, &hcl.Diagnostic{ 463 Severity: hcl.DiagError, 464 Summary: "Invalid provider configuration reference", 465 Detail: fmt.Sprintf("The %s argument requires a provider type name, optionally followed by a period and then a configuration alias.", argName), 466 Subject: expr.Range().Ptr(), 467 }) 468 return nil, diags 469 } 470 471 // verify that the provider local name is normalized 472 name := traversal.RootName() 473 nameDiags := checkProviderNameNormalized(name, traversal[0].SourceRange()) 474 diags = append(diags, nameDiags...) 475 if diags.HasErrors() { 476 return nil, diags 477 } 478 479 ret := &ProviderConfigRef{ 480 Name: name, 481 NameRange: traversal[0].SourceRange(), 482 } 483 484 if len(traversal) > 1 { 485 aliasStep, ok := traversal[1].(hcl.TraverseAttr) 486 if !ok { 487 diags = append(diags, &hcl.Diagnostic{ 488 Severity: hcl.DiagError, 489 Summary: "Invalid provider configuration reference", 490 Detail: "Provider name must either stand alone or be followed by a period and then a configuration alias.", 491 Subject: traversal[1].SourceRange().Ptr(), 492 }) 493 return ret, diags 494 } 495 496 ret.Alias = aliasStep.Name 497 ret.AliasRange = aliasStep.SourceRange().Ptr() 498 } 499 500 return ret, diags 501 } 502 503 // Addr returns the provider config address corresponding to the receiving 504 // config reference. 505 // 506 // This is a trivial conversion, essentially just discarding the source 507 // location information and keeping just the addressing information. 508 func (r *ProviderConfigRef) Addr() addrs.LocalProviderConfig { 509 return addrs.LocalProviderConfig{ 510 LocalName: r.Name, 511 Alias: r.Alias, 512 } 513 } 514 515 func (r *ProviderConfigRef) String() string { 516 if r == nil { 517 return "<nil>" 518 } 519 if r.Alias != "" { 520 return fmt.Sprintf("%s.%s", r.Name, r.Alias) 521 } 522 return r.Name 523 } 524 525 var commonResourceAttributes = []hcl.AttributeSchema{ 526 { 527 Name: "count", 528 }, 529 { 530 Name: "for_each", 531 }, 532 { 533 Name: "provider", 534 }, 535 { 536 Name: "depends_on", 537 }, 538 } 539 540 var resourceBlockSchema = &hcl.BodySchema{ 541 Attributes: commonResourceAttributes, 542 Blocks: []hcl.BlockHeaderSchema{ 543 {Type: "locals"}, // reserved for future use 544 {Type: "lifecycle"}, 545 {Type: "connection"}, 546 {Type: "provisioner", LabelNames: []string{"type"}}, 547 {Type: "_"}, // meta-argument escaping block 548 }, 549 } 550 551 var dataBlockSchema = &hcl.BodySchema{ 552 Attributes: commonResourceAttributes, 553 Blocks: []hcl.BlockHeaderSchema{ 554 {Type: "lifecycle"}, // reserved for future use 555 {Type: "locals"}, // reserved for future use 556 {Type: "_"}, // meta-argument escaping block 557 }, 558 } 559 560 var resourceLifecycleBlockSchema = &hcl.BodySchema{ 561 Attributes: []hcl.AttributeSchema{ 562 { 563 Name: "create_before_destroy", 564 }, 565 { 566 Name: "prevent_destroy", 567 }, 568 { 569 Name: "ignore_changes", 570 }, 571 }, 572 }