github.com/hashicorp/terraform-plugin-sdk@v1.17.2/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/hashicorp/terraform-plugin-sdk/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 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: r.ProviderConfigRef.Name, 74 Alias: r.ProviderConfigRef.Alias, 75 } 76 } 77 78 func decodeResourceBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) { 79 r := &Resource{ 80 Mode: addrs.ManagedResourceMode, 81 Type: block.Labels[0], 82 Name: block.Labels[1], 83 DeclRange: block.DefRange, 84 TypeRange: block.LabelRanges[0], 85 Managed: &ManagedResource{}, 86 } 87 88 content, remain, diags := block.Body.PartialContent(resourceBlockSchema) 89 r.Config = remain 90 91 if !hclsyntax.ValidIdentifier(r.Type) { 92 diags = append(diags, &hcl.Diagnostic{ 93 Severity: hcl.DiagError, 94 Summary: "Invalid resource type name", 95 Detail: badIdentifierDetail, 96 Subject: &block.LabelRanges[0], 97 }) 98 } 99 if !hclsyntax.ValidIdentifier(r.Name) { 100 diags = append(diags, &hcl.Diagnostic{ 101 Severity: hcl.DiagError, 102 Summary: "Invalid resource name", 103 Detail: badIdentifierDetail, 104 Subject: &block.LabelRanges[1], 105 }) 106 } 107 108 if attr, exists := content.Attributes["count"]; exists { 109 r.Count = attr.Expr 110 } 111 112 if attr, exists := content.Attributes["for_each"]; exists { 113 r.ForEach = attr.Expr 114 // Cannot have count and for_each on the same resource block 115 if r.Count != nil { 116 diags = append(diags, &hcl.Diagnostic{ 117 Severity: hcl.DiagError, 118 Summary: `Invalid combination of "count" and "for_each"`, 119 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.`, 120 Subject: &attr.NameRange, 121 }) 122 } 123 } 124 125 if attr, exists := content.Attributes["provider"]; exists { 126 var providerDiags hcl.Diagnostics 127 r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider") 128 diags = append(diags, providerDiags...) 129 } 130 131 if attr, exists := content.Attributes["depends_on"]; exists { 132 deps, depsDiags := decodeDependsOn(attr) 133 diags = append(diags, depsDiags...) 134 r.DependsOn = append(r.DependsOn, deps...) 135 } 136 137 var seenLifecycle *hcl.Block 138 var seenConnection *hcl.Block 139 for _, block := range content.Blocks { 140 switch block.Type { 141 case "lifecycle": 142 if seenLifecycle != nil { 143 diags = append(diags, &hcl.Diagnostic{ 144 Severity: hcl.DiagError, 145 Summary: "Duplicate lifecycle block", 146 Detail: fmt.Sprintf("This resource already has a lifecycle block at %s.", seenLifecycle.DefRange), 147 Subject: &block.DefRange, 148 }) 149 continue 150 } 151 seenLifecycle = block 152 153 lcContent, lcDiags := block.Body.Content(resourceLifecycleBlockSchema) 154 diags = append(diags, lcDiags...) 155 156 if attr, exists := lcContent.Attributes["create_before_destroy"]; exists { 157 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.Managed.CreateBeforeDestroy) 158 diags = append(diags, valDiags...) 159 r.Managed.CreateBeforeDestroySet = true 160 } 161 162 if attr, exists := lcContent.Attributes["prevent_destroy"]; exists { 163 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.Managed.PreventDestroy) 164 diags = append(diags, valDiags...) 165 r.Managed.PreventDestroySet = true 166 } 167 168 if attr, exists := lcContent.Attributes["ignore_changes"]; exists { 169 170 // ignore_changes can either be a list of relative traversals 171 // or it can be just the keyword "all" to ignore changes to this 172 // resource entirely. 173 // ignore_changes = [ami, instance_type] 174 // ignore_changes = all 175 // We also allow two legacy forms for compatibility with earlier 176 // versions: 177 // ignore_changes = ["ami", "instance_type"] 178 // ignore_changes = ["*"] 179 180 kw := hcl.ExprAsKeyword(attr.Expr) 181 182 switch { 183 case kw == "all": 184 r.Managed.IgnoreAllChanges = true 185 default: 186 exprs, listDiags := hcl.ExprList(attr.Expr) 187 diags = append(diags, listDiags...) 188 189 var ignoreAllRange hcl.Range 190 191 for _, expr := range exprs { 192 193 // our expr might be the literal string "*", which 194 // we accept as a deprecated way of saying "all". 195 if shimIsIgnoreChangesStar(expr) { 196 r.Managed.IgnoreAllChanges = true 197 ignoreAllRange = expr.Range() 198 diags = append(diags, &hcl.Diagnostic{ 199 Severity: hcl.DiagWarning, 200 Summary: "Deprecated ignore_changes wildcard", 201 Detail: "The [\"*\"] form of ignore_changes wildcard is deprecated. Use \"ignore_changes = all\" to ignore changes to all attributes.", 202 Subject: attr.Expr.Range().Ptr(), 203 }) 204 continue 205 } 206 207 expr, shimDiags := shimTraversalInString(expr, false) 208 diags = append(diags, shimDiags...) 209 210 traversal, travDiags := hcl.RelTraversalForExpr(expr) 211 diags = append(diags, travDiags...) 212 if len(traversal) != 0 { 213 r.Managed.IgnoreChanges = append(r.Managed.IgnoreChanges, traversal) 214 } 215 } 216 217 if r.Managed.IgnoreAllChanges && len(r.Managed.IgnoreChanges) != 0 { 218 diags = append(diags, &hcl.Diagnostic{ 219 Severity: hcl.DiagError, 220 Summary: "Invalid ignore_changes ruleset", 221 Detail: "Cannot mix wildcard string \"*\" with non-wildcard references.", 222 Subject: &ignoreAllRange, 223 Context: attr.Expr.Range().Ptr(), 224 }) 225 } 226 227 } 228 229 } 230 231 case "connection": 232 if seenConnection != nil { 233 diags = append(diags, &hcl.Diagnostic{ 234 Severity: hcl.DiagError, 235 Summary: "Duplicate connection block", 236 Detail: fmt.Sprintf("This resource already has a connection block at %s.", seenConnection.DefRange), 237 Subject: &block.DefRange, 238 }) 239 continue 240 } 241 seenConnection = block 242 243 r.Managed.Connection = &Connection{ 244 Config: block.Body, 245 DeclRange: block.DefRange, 246 } 247 248 case "provisioner": 249 pv, pvDiags := decodeProvisionerBlock(block) 250 diags = append(diags, pvDiags...) 251 if pv != nil { 252 r.Managed.Provisioners = append(r.Managed.Provisioners, pv) 253 } 254 255 default: 256 // Any other block types are ones we've reserved for future use, 257 // so they get a generic message. 258 diags = append(diags, &hcl.Diagnostic{ 259 Severity: hcl.DiagError, 260 Summary: "Reserved block type name in resource block", 261 Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type), 262 Subject: &block.TypeRange, 263 }) 264 } 265 } 266 267 return r, diags 268 } 269 270 func decodeDataBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) { 271 r := &Resource{ 272 Mode: addrs.DataResourceMode, 273 Type: block.Labels[0], 274 Name: block.Labels[1], 275 DeclRange: block.DefRange, 276 TypeRange: block.LabelRanges[0], 277 } 278 279 content, remain, diags := block.Body.PartialContent(dataBlockSchema) 280 r.Config = remain 281 282 if !hclsyntax.ValidIdentifier(r.Type) { 283 diags = append(diags, &hcl.Diagnostic{ 284 Severity: hcl.DiagError, 285 Summary: "Invalid data source name", 286 Detail: badIdentifierDetail, 287 Subject: &block.LabelRanges[0], 288 }) 289 } 290 if !hclsyntax.ValidIdentifier(r.Name) { 291 diags = append(diags, &hcl.Diagnostic{ 292 Severity: hcl.DiagError, 293 Summary: "Invalid data resource name", 294 Detail: badIdentifierDetail, 295 Subject: &block.LabelRanges[1], 296 }) 297 } 298 299 if attr, exists := content.Attributes["count"]; exists { 300 r.Count = attr.Expr 301 } 302 303 if attr, exists := content.Attributes["for_each"]; exists { 304 r.ForEach = attr.Expr 305 // Cannot have count and for_each on the same data block 306 if r.Count != nil { 307 diags = append(diags, &hcl.Diagnostic{ 308 Severity: hcl.DiagError, 309 Summary: `Invalid combination of "count" and "for_each"`, 310 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.`, 311 Subject: &attr.NameRange, 312 }) 313 } 314 } 315 316 if attr, exists := content.Attributes["provider"]; exists { 317 var providerDiags hcl.Diagnostics 318 r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider") 319 diags = append(diags, providerDiags...) 320 } 321 322 if attr, exists := content.Attributes["depends_on"]; exists { 323 deps, depsDiags := decodeDependsOn(attr) 324 diags = append(diags, depsDiags...) 325 r.DependsOn = append(r.DependsOn, deps...) 326 } 327 328 for _, block := range content.Blocks { 329 // All of the block types we accept are just reserved for future use, but some get a specialized error message. 330 switch block.Type { 331 case "lifecycle": 332 diags = append(diags, &hcl.Diagnostic{ 333 Severity: hcl.DiagError, 334 Summary: "Unsupported lifecycle block", 335 Detail: "Data resources do not have lifecycle settings, so a lifecycle block is not allowed.", 336 Subject: &block.DefRange, 337 }) 338 default: 339 diags = append(diags, &hcl.Diagnostic{ 340 Severity: hcl.DiagError, 341 Summary: "Reserved block type name in data 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 return r, diags 349 } 350 351 type ProviderConfigRef struct { 352 Name string 353 NameRange hcl.Range 354 Alias string 355 AliasRange *hcl.Range // nil if alias not set 356 } 357 358 func decodeProviderConfigRef(expr hcl.Expression, argName string) (*ProviderConfigRef, hcl.Diagnostics) { 359 var diags hcl.Diagnostics 360 361 var shimDiags hcl.Diagnostics 362 expr, shimDiags = shimTraversalInString(expr, false) 363 diags = append(diags, shimDiags...) 364 365 traversal, travDiags := hcl.AbsTraversalForExpr(expr) 366 367 // AbsTraversalForExpr produces only generic errors, so we'll discard 368 // the errors given and produce our own with extra context. If we didn't 369 // get any errors then we might still have warnings, though. 370 if !travDiags.HasErrors() { 371 diags = append(diags, travDiags...) 372 } 373 374 if len(traversal) < 1 || len(traversal) > 2 { 375 // A provider reference was given as a string literal in the legacy 376 // configuration language and there are lots of examples out there 377 // showing that usage, so we'll sniff for that situation here and 378 // produce a specialized error message for it to help users find 379 // the new correct form. 380 if exprIsNativeQuotedString(expr) { 381 diags = append(diags, &hcl.Diagnostic{ 382 Severity: hcl.DiagError, 383 Summary: "Invalid provider configuration reference", 384 Detail: "A provider configuration reference must not be given in quotes.", 385 Subject: expr.Range().Ptr(), 386 }) 387 return nil, diags 388 } 389 390 diags = append(diags, &hcl.Diagnostic{ 391 Severity: hcl.DiagError, 392 Summary: "Invalid provider configuration reference", 393 Detail: fmt.Sprintf("The %s argument requires a provider type name, optionally followed by a period and then a configuration alias.", argName), 394 Subject: expr.Range().Ptr(), 395 }) 396 return nil, diags 397 } 398 399 ret := &ProviderConfigRef{ 400 Name: traversal.RootName(), 401 NameRange: traversal[0].SourceRange(), 402 } 403 404 if len(traversal) > 1 { 405 aliasStep, ok := traversal[1].(hcl.TraverseAttr) 406 if !ok { 407 diags = append(diags, &hcl.Diagnostic{ 408 Severity: hcl.DiagError, 409 Summary: "Invalid provider configuration reference", 410 Detail: "Provider name must either stand alone or be followed by a period and then a configuration alias.", 411 Subject: traversal[1].SourceRange().Ptr(), 412 }) 413 return ret, diags 414 } 415 416 ret.Alias = aliasStep.Name 417 ret.AliasRange = aliasStep.SourceRange().Ptr() 418 } 419 420 return ret, diags 421 } 422 423 // Addr returns the provider config address corresponding to the receiving 424 // config reference. 425 // 426 // This is a trivial conversion, essentially just discarding the source 427 // location information and keeping just the addressing information. 428 func (r *ProviderConfigRef) Addr() addrs.ProviderConfig { 429 return addrs.ProviderConfig{ 430 Type: r.Name, 431 Alias: r.Alias, 432 } 433 } 434 435 func (r *ProviderConfigRef) String() string { 436 if r == nil { 437 return "<nil>" 438 } 439 if r.Alias != "" { 440 return fmt.Sprintf("%s.%s", r.Name, r.Alias) 441 } 442 return r.Name 443 } 444 445 var commonResourceAttributes = []hcl.AttributeSchema{ 446 { 447 Name: "count", 448 }, 449 { 450 Name: "for_each", 451 }, 452 { 453 Name: "provider", 454 }, 455 { 456 Name: "depends_on", 457 }, 458 } 459 460 var resourceBlockSchema = &hcl.BodySchema{ 461 Attributes: commonResourceAttributes, 462 Blocks: []hcl.BlockHeaderSchema{ 463 {Type: "locals"}, // reserved for future use 464 {Type: "lifecycle"}, 465 {Type: "connection"}, 466 {Type: "provisioner", LabelNames: []string{"type"}}, 467 }, 468 } 469 470 var dataBlockSchema = &hcl.BodySchema{ 471 Attributes: commonResourceAttributes, 472 Blocks: []hcl.BlockHeaderSchema{ 473 {Type: "lifecycle"}, // reserved for future use 474 {Type: "locals"}, // reserved for future use 475 }, 476 } 477 478 var resourceLifecycleBlockSchema = &hcl.BodySchema{ 479 Attributes: []hcl.AttributeSchema{ 480 { 481 Name: "create_before_destroy", 482 }, 483 { 484 Name: "prevent_destroy", 485 }, 486 { 487 Name: "ignore_changes", 488 }, 489 }, 490 }