github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/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/hashicorp/terraform/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 24 DependsOn []hcl.Traversal 25 26 // Managed is populated only for Mode = addrs.ManagedResourceMode, 27 // containing the additional fields that apply to managed resources. 28 // For all other resource modes, this field is nil. 29 Managed *ManagedResource 30 31 DeclRange hcl.Range 32 TypeRange hcl.Range 33 } 34 35 // ManagedResource represents a "resource" block in a module or file. 36 type ManagedResource struct { 37 Connection *Connection 38 Provisioners []*Provisioner 39 40 CreateBeforeDestroy bool 41 PreventDestroy bool 42 IgnoreChanges []hcl.Traversal 43 IgnoreAllChanges bool 44 45 CreateBeforeDestroySet bool 46 PreventDestroySet bool 47 } 48 49 func (r *Resource) moduleUniqueKey() string { 50 return r.Addr().String() 51 } 52 53 // Addr returns a resource address for the receiver that is relative to the 54 // resource's containing module. 55 func (r *Resource) Addr() addrs.Resource { 56 return addrs.Resource{ 57 Mode: r.Mode, 58 Type: r.Type, 59 Name: r.Name, 60 } 61 } 62 63 // ProviderConfigAddr returns the address for the provider configuration 64 // that should be used for this resource. This function implements the 65 // default behavior of extracting the type from the resource type name if 66 // an explicit "provider" argument was not provided. 67 func (r *Resource) ProviderConfigAddr() addrs.ProviderConfig { 68 if r.ProviderConfigRef == nil { 69 return r.Addr().DefaultProviderConfig() 70 } 71 72 return addrs.ProviderConfig{ 73 Type: addrs.NewLegacyProvider(r.ProviderConfigRef.Name), 74 Alias: r.ProviderConfigRef.Alias, 75 } 76 } 77 78 func decodeResourceBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) { 79 var diags hcl.Diagnostics 80 r := &Resource{ 81 Mode: addrs.ManagedResourceMode, 82 Type: block.Labels[0], 83 Name: block.Labels[1], 84 DeclRange: block.DefRange, 85 TypeRange: block.LabelRanges[0], 86 Managed: &ManagedResource{}, 87 } 88 89 // Produce deprecation messages for any pre-0.12-style 90 // single-interpolation-only expressions. We do this up front here because 91 // then we can also catch instances inside special blocks like "connection", 92 // before PartialContent extracts them. 93 moreDiags := warnForDeprecatedInterpolationsInBody(block.Body) 94 diags = append(diags, moreDiags...) 95 96 content, remain, moreDiags := block.Body.PartialContent(resourceBlockSchema) 97 diags = append(diags, moreDiags...) 98 r.Config = remain 99 100 if !hclsyntax.ValidIdentifier(r.Type) { 101 diags = append(diags, &hcl.Diagnostic{ 102 Severity: hcl.DiagError, 103 Summary: "Invalid resource type name", 104 Detail: badIdentifierDetail, 105 Subject: &block.LabelRanges[0], 106 }) 107 } 108 if !hclsyntax.ValidIdentifier(r.Name) { 109 diags = append(diags, &hcl.Diagnostic{ 110 Severity: hcl.DiagError, 111 Summary: "Invalid resource name", 112 Detail: badIdentifierDetail, 113 Subject: &block.LabelRanges[1], 114 }) 115 } 116 117 if attr, exists := content.Attributes["count"]; exists { 118 r.Count = attr.Expr 119 } 120 121 if attr, exists := content.Attributes["for_each"]; exists { 122 r.ForEach = attr.Expr 123 // Cannot have count and for_each on the same resource block 124 if r.Count != nil { 125 diags = append(diags, &hcl.Diagnostic{ 126 Severity: hcl.DiagError, 127 Summary: `Invalid combination of "count" and "for_each"`, 128 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.`, 129 Subject: &attr.NameRange, 130 }) 131 } 132 } 133 134 if attr, exists := content.Attributes["provider"]; exists { 135 var providerDiags hcl.Diagnostics 136 r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider") 137 diags = append(diags, providerDiags...) 138 } 139 140 if attr, exists := content.Attributes["depends_on"]; exists { 141 deps, depsDiags := decodeDependsOn(attr) 142 diags = append(diags, depsDiags...) 143 r.DependsOn = append(r.DependsOn, deps...) 144 } 145 146 var seenLifecycle *hcl.Block 147 var seenConnection *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.DiagWarning, 209 Summary: "Deprecated ignore_changes wildcard", 210 Detail: "The [\"*\"] form of ignore_changes wildcard is deprecated. 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 default: 265 // Any other block types are ones we've reserved for future use, 266 // so they get a generic message. 267 diags = append(diags, &hcl.Diagnostic{ 268 Severity: hcl.DiagError, 269 Summary: "Reserved block type name in resource block", 270 Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type), 271 Subject: &block.TypeRange, 272 }) 273 } 274 } 275 276 // Now we can validate the connection block references if there are any destroy provisioners. 277 // TODO: should we eliminate standalone connection blocks? 278 if r.Managed.Connection != nil { 279 for _, p := range r.Managed.Provisioners { 280 if p.When == ProvisionerWhenDestroy { 281 diags = append(diags, onlySelfRefs(r.Managed.Connection.Config)...) 282 break 283 } 284 } 285 } 286 287 return r, diags 288 } 289 290 func decodeDataBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) { 291 r := &Resource{ 292 Mode: addrs.DataResourceMode, 293 Type: block.Labels[0], 294 Name: block.Labels[1], 295 DeclRange: block.DefRange, 296 TypeRange: block.LabelRanges[0], 297 } 298 299 content, remain, diags := block.Body.PartialContent(dataBlockSchema) 300 r.Config = remain 301 302 if !hclsyntax.ValidIdentifier(r.Type) { 303 diags = append(diags, &hcl.Diagnostic{ 304 Severity: hcl.DiagError, 305 Summary: "Invalid data source name", 306 Detail: badIdentifierDetail, 307 Subject: &block.LabelRanges[0], 308 }) 309 } 310 if !hclsyntax.ValidIdentifier(r.Name) { 311 diags = append(diags, &hcl.Diagnostic{ 312 Severity: hcl.DiagError, 313 Summary: "Invalid data resource name", 314 Detail: badIdentifierDetail, 315 Subject: &block.LabelRanges[1], 316 }) 317 } 318 319 if attr, exists := content.Attributes["count"]; exists { 320 r.Count = attr.Expr 321 } 322 323 if attr, exists := content.Attributes["for_each"]; exists { 324 r.ForEach = attr.Expr 325 // Cannot have count and for_each on the same data block 326 if r.Count != nil { 327 diags = append(diags, &hcl.Diagnostic{ 328 Severity: hcl.DiagError, 329 Summary: `Invalid combination of "count" and "for_each"`, 330 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.`, 331 Subject: &attr.NameRange, 332 }) 333 } 334 } 335 336 if attr, exists := content.Attributes["provider"]; exists { 337 var providerDiags hcl.Diagnostics 338 r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider") 339 diags = append(diags, providerDiags...) 340 } 341 342 if attr, exists := content.Attributes["depends_on"]; exists { 343 deps, depsDiags := decodeDependsOn(attr) 344 diags = append(diags, depsDiags...) 345 r.DependsOn = append(r.DependsOn, deps...) 346 } 347 348 for _, block := range content.Blocks { 349 // All of the block types we accept are just reserved for future use, but some get a specialized error message. 350 switch block.Type { 351 case "lifecycle": 352 diags = append(diags, &hcl.Diagnostic{ 353 Severity: hcl.DiagError, 354 Summary: "Unsupported lifecycle block", 355 Detail: "Data resources do not have lifecycle settings, so a lifecycle block is not allowed.", 356 Subject: &block.DefRange, 357 }) 358 default: 359 diags = append(diags, &hcl.Diagnostic{ 360 Severity: hcl.DiagError, 361 Summary: "Reserved block type name in data block", 362 Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type), 363 Subject: &block.TypeRange, 364 }) 365 } 366 } 367 368 return r, diags 369 } 370 371 type ProviderConfigRef struct { 372 Name string 373 NameRange hcl.Range 374 Alias string 375 AliasRange *hcl.Range // nil if alias not set 376 } 377 378 func decodeProviderConfigRef(expr hcl.Expression, argName string) (*ProviderConfigRef, hcl.Diagnostics) { 379 var diags hcl.Diagnostics 380 381 var shimDiags hcl.Diagnostics 382 expr, shimDiags = shimTraversalInString(expr, false) 383 diags = append(diags, shimDiags...) 384 385 traversal, travDiags := hcl.AbsTraversalForExpr(expr) 386 387 // AbsTraversalForExpr produces only generic errors, so we'll discard 388 // the errors given and produce our own with extra context. If we didn't 389 // get any errors then we might still have warnings, though. 390 if !travDiags.HasErrors() { 391 diags = append(diags, travDiags...) 392 } 393 394 if len(traversal) < 1 || len(traversal) > 2 { 395 // A provider reference was given as a string literal in the legacy 396 // configuration language and there are lots of examples out there 397 // showing that usage, so we'll sniff for that situation here and 398 // produce a specialized error message for it to help users find 399 // the new correct form. 400 if exprIsNativeQuotedString(expr) { 401 diags = append(diags, &hcl.Diagnostic{ 402 Severity: hcl.DiagError, 403 Summary: "Invalid provider configuration reference", 404 Detail: "A provider configuration reference must not be given in quotes.", 405 Subject: expr.Range().Ptr(), 406 }) 407 return nil, diags 408 } 409 410 diags = append(diags, &hcl.Diagnostic{ 411 Severity: hcl.DiagError, 412 Summary: "Invalid provider configuration reference", 413 Detail: fmt.Sprintf("The %s argument requires a provider type name, optionally followed by a period and then a configuration alias.", argName), 414 Subject: expr.Range().Ptr(), 415 }) 416 return nil, diags 417 } 418 419 ret := &ProviderConfigRef{ 420 Name: traversal.RootName(), 421 NameRange: traversal[0].SourceRange(), 422 } 423 424 if len(traversal) > 1 { 425 aliasStep, ok := traversal[1].(hcl.TraverseAttr) 426 if !ok { 427 diags = append(diags, &hcl.Diagnostic{ 428 Severity: hcl.DiagError, 429 Summary: "Invalid provider configuration reference", 430 Detail: "Provider name must either stand alone or be followed by a period and then a configuration alias.", 431 Subject: traversal[1].SourceRange().Ptr(), 432 }) 433 return ret, diags 434 } 435 436 ret.Alias = aliasStep.Name 437 ret.AliasRange = aliasStep.SourceRange().Ptr() 438 } 439 440 return ret, diags 441 } 442 443 // Addr returns the provider config address corresponding to the receiving 444 // config reference. 445 // 446 // This is a trivial conversion, essentially just discarding the source 447 // location information and keeping just the addressing information. 448 func (r *ProviderConfigRef) Addr() addrs.ProviderConfig { 449 return addrs.ProviderConfig{ 450 Type: addrs.NewLegacyProvider(r.Name), 451 Alias: r.Alias, 452 } 453 } 454 455 func (r *ProviderConfigRef) String() string { 456 if r == nil { 457 return "<nil>" 458 } 459 if r.Alias != "" { 460 return fmt.Sprintf("%s.%s", r.Name, r.Alias) 461 } 462 return r.Name 463 } 464 465 var commonResourceAttributes = []hcl.AttributeSchema{ 466 { 467 Name: "count", 468 }, 469 { 470 Name: "for_each", 471 }, 472 { 473 Name: "provider", 474 }, 475 { 476 Name: "depends_on", 477 }, 478 } 479 480 var resourceBlockSchema = &hcl.BodySchema{ 481 Attributes: commonResourceAttributes, 482 Blocks: []hcl.BlockHeaderSchema{ 483 {Type: "locals"}, // reserved for future use 484 {Type: "lifecycle"}, 485 {Type: "connection"}, 486 {Type: "provisioner", LabelNames: []string{"type"}}, 487 }, 488 } 489 490 var dataBlockSchema = &hcl.BodySchema{ 491 Attributes: commonResourceAttributes, 492 Blocks: []hcl.BlockHeaderSchema{ 493 {Type: "lifecycle"}, // reserved for future use 494 {Type: "locals"}, // reserved for future use 495 }, 496 } 497 498 var resourceLifecycleBlockSchema = &hcl.BodySchema{ 499 Attributes: []hcl.AttributeSchema{ 500 { 501 Name: "create_before_destroy", 502 }, 503 { 504 Name: "prevent_destroy", 505 }, 506 { 507 Name: "ignore_changes", 508 }, 509 }, 510 }