github.com/rvichery/terraform@v0.11.10/configs/resource.go (about) 1 package configs 2 3 import ( 4 "fmt" 5 6 "github.com/hashicorp/hcl2/gohcl" 7 "github.com/hashicorp/hcl2/hcl" 8 "github.com/hashicorp/hcl2/hcl/hclsyntax" 9 ) 10 11 // ManagedResource represents a "resource" block in a module or file. 12 type ManagedResource struct { 13 Name string 14 Type string 15 Config hcl.Body 16 Count hcl.Expression 17 ForEach hcl.Expression 18 19 ProviderConfigRef *ProviderConfigRef 20 21 DependsOn []hcl.Traversal 22 23 Connection *Connection 24 Provisioners []*Provisioner 25 26 CreateBeforeDestroy bool 27 PreventDestroy bool 28 IgnoreChanges []hcl.Traversal 29 IgnoreAllChanges bool 30 31 CreateBeforeDestroySet bool 32 PreventDestroySet bool 33 34 DeclRange hcl.Range 35 TypeRange hcl.Range 36 } 37 38 func (r *ManagedResource) moduleUniqueKey() string { 39 return fmt.Sprintf("%s.%s", r.Name, r.Type) 40 } 41 42 func decodeResourceBlock(block *hcl.Block) (*ManagedResource, hcl.Diagnostics) { 43 r := &ManagedResource{ 44 Type: block.Labels[0], 45 Name: block.Labels[1], 46 DeclRange: block.DefRange, 47 TypeRange: block.LabelRanges[0], 48 } 49 50 content, remain, diags := block.Body.PartialContent(resourceBlockSchema) 51 r.Config = remain 52 53 if !hclsyntax.ValidIdentifier(r.Type) { 54 diags = append(diags, &hcl.Diagnostic{ 55 Severity: hcl.DiagError, 56 Summary: "Invalid resource type name", 57 Detail: badIdentifierDetail, 58 Subject: &block.LabelRanges[0], 59 }) 60 } 61 if !hclsyntax.ValidIdentifier(r.Name) { 62 diags = append(diags, &hcl.Diagnostic{ 63 Severity: hcl.DiagError, 64 Summary: "Invalid resource name", 65 Detail: badIdentifierDetail, 66 Subject: &block.LabelRanges[1], 67 }) 68 } 69 70 if attr, exists := content.Attributes["count"]; exists { 71 r.Count = attr.Expr 72 } 73 74 if attr, exists := content.Attributes["for_each"]; exists { 75 r.Count = attr.Expr 76 } 77 78 if attr, exists := content.Attributes["provider"]; exists { 79 var providerDiags hcl.Diagnostics 80 r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr) 81 diags = append(diags, providerDiags...) 82 } 83 84 if attr, exists := content.Attributes["depends_on"]; exists { 85 deps, depsDiags := decodeDependsOn(attr) 86 diags = append(diags, depsDiags...) 87 r.DependsOn = append(r.DependsOn, deps...) 88 } 89 90 var seenLifecycle *hcl.Block 91 var seenConnection *hcl.Block 92 for _, block := range content.Blocks { 93 switch block.Type { 94 case "lifecycle": 95 if seenLifecycle != nil { 96 diags = append(diags, &hcl.Diagnostic{ 97 Severity: hcl.DiagError, 98 Summary: "Duplicate lifecycle block", 99 Detail: fmt.Sprintf("This resource already has a lifecycle block at %s.", seenLifecycle.DefRange), 100 Subject: &block.DefRange, 101 }) 102 continue 103 } 104 seenLifecycle = block 105 106 lcContent, lcDiags := block.Body.Content(resourceLifecycleBlockSchema) 107 diags = append(diags, lcDiags...) 108 109 if attr, exists := lcContent.Attributes["create_before_destroy"]; exists { 110 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.CreateBeforeDestroy) 111 diags = append(diags, valDiags...) 112 r.CreateBeforeDestroySet = true 113 } 114 115 if attr, exists := lcContent.Attributes["prevent_destroy"]; exists { 116 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.PreventDestroy) 117 diags = append(diags, valDiags...) 118 r.PreventDestroySet = true 119 } 120 121 if attr, exists := lcContent.Attributes["ignore_changes"]; exists { 122 123 // ignore_changes can either be a list of relative traversals 124 // or it can be just the keyword "all" to ignore changes to this 125 // resource entirely. 126 // ignore_changes = [ami, instance_type] 127 // ignore_changes = all 128 // We also allow two legacy forms for compatibility with earlier 129 // versions: 130 // ignore_changes = ["ami", "instance_type"] 131 // ignore_changes = ["*"] 132 133 kw := hcl.ExprAsKeyword(attr.Expr) 134 135 switch { 136 case kw == "all": 137 r.IgnoreAllChanges = true 138 default: 139 exprs, listDiags := hcl.ExprList(attr.Expr) 140 diags = append(diags, listDiags...) 141 142 var ignoreAllRange hcl.Range 143 144 for _, expr := range exprs { 145 146 // our expr might be the literal string "*", which 147 // we accept as a deprecated way of saying "all". 148 if shimIsIgnoreChangesStar(expr) { 149 r.IgnoreAllChanges = true 150 ignoreAllRange = expr.Range() 151 diags = append(diags, &hcl.Diagnostic{ 152 Severity: hcl.DiagWarning, 153 Summary: "Deprecated ignore_changes wildcard", 154 Detail: "The [\"*\"] form of ignore_changes wildcard is reprecated. Use \"ignore_changes = all\" to ignore changes to all attributes.", 155 Subject: attr.Expr.Range().Ptr(), 156 }) 157 continue 158 } 159 160 expr, shimDiags := shimTraversalInString(expr, false) 161 diags = append(diags, shimDiags...) 162 163 traversal, travDiags := hcl.RelTraversalForExpr(expr) 164 diags = append(diags, travDiags...) 165 if len(traversal) != 0 { 166 r.IgnoreChanges = append(r.IgnoreChanges, traversal) 167 } 168 } 169 170 if r.IgnoreAllChanges && len(r.IgnoreChanges) != 0 { 171 diags = append(diags, &hcl.Diagnostic{ 172 Severity: hcl.DiagError, 173 Summary: "Invalid ignore_changes ruleset", 174 Detail: "Cannot mix wildcard string \"*\" with non-wildcard references.", 175 Subject: &ignoreAllRange, 176 Context: attr.Expr.Range().Ptr(), 177 }) 178 } 179 180 } 181 182 } 183 184 case "connection": 185 if seenConnection != nil { 186 diags = append(diags, &hcl.Diagnostic{ 187 Severity: hcl.DiagError, 188 Summary: "Duplicate connection block", 189 Detail: fmt.Sprintf("This resource already has a connection block at %s.", seenConnection.DefRange), 190 Subject: &block.DefRange, 191 }) 192 continue 193 } 194 seenConnection = block 195 196 conn, connDiags := decodeConnectionBlock(block) 197 diags = append(diags, connDiags...) 198 r.Connection = conn 199 200 case "provisioner": 201 pv, pvDiags := decodeProvisionerBlock(block) 202 diags = append(diags, pvDiags...) 203 if pv != nil { 204 r.Provisioners = append(r.Provisioners, pv) 205 } 206 207 default: 208 // Should never happen, because the above cases should always be 209 // exhaustive for all the types specified in our schema. 210 continue 211 } 212 } 213 214 return r, diags 215 } 216 217 // DataResource represents a "data" block in a module or file. 218 type DataResource struct { 219 Name string 220 Type string 221 Config hcl.Body 222 Count hcl.Expression 223 ForEach hcl.Expression 224 225 ProviderConfigRef *ProviderConfigRef 226 227 DependsOn []hcl.Traversal 228 229 DeclRange hcl.Range 230 TypeRange hcl.Range 231 } 232 233 func (r *DataResource) moduleUniqueKey() string { 234 return fmt.Sprintf("data.%s.%s", r.Name, r.Type) 235 } 236 237 func decodeDataBlock(block *hcl.Block) (*DataResource, hcl.Diagnostics) { 238 r := &DataResource{ 239 Type: block.Labels[0], 240 Name: block.Labels[1], 241 DeclRange: block.DefRange, 242 TypeRange: block.LabelRanges[0], 243 } 244 245 content, remain, diags := block.Body.PartialContent(dataBlockSchema) 246 r.Config = remain 247 248 if !hclsyntax.ValidIdentifier(r.Type) { 249 diags = append(diags, &hcl.Diagnostic{ 250 Severity: hcl.DiagError, 251 Summary: "Invalid data source name", 252 Detail: badIdentifierDetail, 253 Subject: &block.LabelRanges[0], 254 }) 255 } 256 if !hclsyntax.ValidIdentifier(r.Name) { 257 diags = append(diags, &hcl.Diagnostic{ 258 Severity: hcl.DiagError, 259 Summary: "Invalid data resource name", 260 Detail: badIdentifierDetail, 261 Subject: &block.LabelRanges[1], 262 }) 263 } 264 265 if attr, exists := content.Attributes["count"]; exists { 266 r.Count = attr.Expr 267 } 268 269 if attr, exists := content.Attributes["for_each"]; exists { 270 r.Count = attr.Expr 271 } 272 273 if attr, exists := content.Attributes["provider"]; exists { 274 var providerDiags hcl.Diagnostics 275 r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr) 276 diags = append(diags, providerDiags...) 277 } 278 279 if attr, exists := content.Attributes["depends_on"]; exists { 280 deps, depsDiags := decodeDependsOn(attr) 281 diags = append(diags, depsDiags...) 282 r.DependsOn = append(r.DependsOn, deps...) 283 } 284 285 for _, block := range content.Blocks { 286 // Our schema only allows for "lifecycle" blocks, so we can assume 287 // that this is all we will see here. We don't have any lifecycle 288 // attributes for data resources currently, so we'll just produce 289 // an error. 290 diags = append(diags, &hcl.Diagnostic{ 291 Severity: hcl.DiagError, 292 Summary: "Unsupported lifecycle block", 293 Detail: "Data resources do not have lifecycle settings, so a lifecycle block is not allowed.", 294 Subject: &block.DefRange, 295 }) 296 break 297 } 298 299 return r, diags 300 } 301 302 type ProviderConfigRef struct { 303 Name string 304 NameRange hcl.Range 305 Alias string 306 AliasRange *hcl.Range // nil if alias not set 307 } 308 309 func decodeProviderConfigRef(attr *hcl.Attribute) (*ProviderConfigRef, hcl.Diagnostics) { 310 var diags hcl.Diagnostics 311 312 expr, shimDiags := shimTraversalInString(attr.Expr, false) 313 diags = append(diags, shimDiags...) 314 315 traversal, travDiags := hcl.AbsTraversalForExpr(expr) 316 317 // AbsTraversalForExpr produces only generic errors, so we'll discard 318 // the errors given and produce our own with extra context. If we didn't 319 // get any errors then we might still have warnings, though. 320 if !travDiags.HasErrors() { 321 diags = append(diags, travDiags...) 322 } 323 324 if len(traversal) < 1 && len(traversal) > 2 { 325 // A provider reference was given as a string literal in the legacy 326 // configuration language and there are lots of examples out there 327 // showing that usage, so we'll sniff for that situation here and 328 // produce a specialized error message for it to help users find 329 // the new correct form. 330 if exprIsNativeQuotedString(attr.Expr) { 331 diags = append(diags, &hcl.Diagnostic{ 332 Severity: hcl.DiagError, 333 Summary: "Invalid provider configuration reference", 334 Detail: "A provider configuration reference must not be given in quotes.", 335 Subject: expr.Range().Ptr(), 336 }) 337 return nil, diags 338 } 339 340 diags = append(diags, &hcl.Diagnostic{ 341 Severity: hcl.DiagError, 342 Summary: "Invalid provider configuration reference", 343 Detail: fmt.Sprintf("The %s argument requires a provider type name, optionally followed by a period and then a configuration alias.", attr.Name), 344 Subject: expr.Range().Ptr(), 345 }) 346 return nil, diags 347 } 348 349 ret := &ProviderConfigRef{ 350 Name: traversal.RootName(), 351 NameRange: traversal[0].SourceRange(), 352 } 353 354 if len(traversal) > 1 { 355 aliasStep, ok := traversal[1].(hcl.TraverseAttr) 356 if !ok { 357 diags = append(diags, &hcl.Diagnostic{ 358 Severity: hcl.DiagError, 359 Summary: "Invalid provider configuration reference", 360 Detail: "Provider name must either stand alone or be followed by a period and then a configuration alias.", 361 Subject: traversal[1].SourceRange().Ptr(), 362 }) 363 return ret, diags 364 } 365 366 ret.Alias = aliasStep.Name 367 ret.AliasRange = aliasStep.SourceRange().Ptr() 368 } 369 370 return ret, diags 371 } 372 373 var commonResourceAttributes = []hcl.AttributeSchema{ 374 { 375 Name: "count", 376 }, 377 { 378 Name: "for_each", 379 }, 380 { 381 Name: "provider", 382 }, 383 { 384 Name: "depends_on", 385 }, 386 } 387 388 var resourceBlockSchema = &hcl.BodySchema{ 389 Attributes: commonResourceAttributes, 390 Blocks: []hcl.BlockHeaderSchema{ 391 { 392 Type: "lifecycle", 393 }, 394 { 395 Type: "connection", 396 }, 397 { 398 Type: "provisioner", 399 LabelNames: []string{"type"}, 400 }, 401 }, 402 } 403 404 var dataBlockSchema = &hcl.BodySchema{ 405 Attributes: commonResourceAttributes, 406 Blocks: []hcl.BlockHeaderSchema{ 407 { 408 Type: "lifecycle", 409 }, 410 }, 411 } 412 413 var resourceLifecycleBlockSchema = &hcl.BodySchema{ 414 Attributes: []hcl.AttributeSchema{ 415 { 416 Name: "create_before_destroy", 417 }, 418 { 419 Name: "prevent_destroy", 420 }, 421 { 422 Name: "ignore_changes", 423 }, 424 }, 425 }