github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/lang/globalref/analyzer_meta_references.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package globalref 5 6 import ( 7 "github.com/hashicorp/hcl/v2" 8 "github.com/zclconf/go-cty/cty" 9 "github.com/zclconf/go-cty/cty/convert" 10 "github.com/zclconf/go-cty/cty/gocty" 11 12 "github.com/terramate-io/tf/addrs" 13 "github.com/terramate-io/tf/configs/configschema" 14 "github.com/terramate-io/tf/lang" 15 ) 16 17 // MetaReferences inspects the configuration to find the references contained 18 // within the most specific object that the given address refers to. 19 // 20 // This finds only the direct references in that object, not any indirect 21 // references from those. This is a building block for some other Analyzer 22 // functions that can walk through multiple levels of reference. 23 // 24 // If the given reference refers to something that doesn't exist in the 25 // configuration we're analyzing then MetaReferences will return no 26 // meta-references at all, which is indistinguishable from an existing 27 // object that doesn't refer to anything. 28 func (a *Analyzer) MetaReferences(ref Reference) []Reference { 29 // This function is aiming to encapsulate the fact that a reference 30 // is actually quite a complex notion which includes both a specific 31 // object the reference is to, where each distinct object type has 32 // a very different representation in the configuration, and then 33 // also potentially an attribute or block within the definition of that 34 // object. Our goal is to make all of these different situations appear 35 // mostly the same to the caller, in that all of them can be reduced to 36 // a set of references regardless of which expression or expressions we 37 // derive those from. 38 39 moduleAddr := ref.ModuleAddr() 40 remaining := ref.LocalRef.Remaining 41 42 // Our first task then is to select an appropriate implementation based 43 // on which address type the reference refers to. 44 switch targetAddr := ref.LocalRef.Subject.(type) { 45 case addrs.InputVariable: 46 return a.metaReferencesInputVariable(moduleAddr, targetAddr, remaining) 47 case addrs.LocalValue: 48 return a.metaReferencesLocalValue(moduleAddr, targetAddr, remaining) 49 case addrs.ModuleCallInstanceOutput: 50 return a.metaReferencesOutputValue(moduleAddr, targetAddr, remaining) 51 case addrs.ModuleCallInstance: 52 return a.metaReferencesModuleCall(moduleAddr, targetAddr, remaining) 53 case addrs.ModuleCall: 54 // TODO: It isn't really correct to say that a reference to a module 55 // call is a reference to its no-key instance. Really what we want to 56 // say here is that it's a reference to _all_ instances, or to an 57 // instance with an unknown key, but we don't have any representation 58 // of that. For the moment it's pretty immaterial since most of our 59 // other analysis ignores instance keys anyway, but maybe we'll revisit 60 // this latter to distingish these two cases better. 61 return a.metaReferencesModuleCall(moduleAddr, targetAddr.Instance(addrs.NoKey), remaining) 62 case addrs.CountAttr, addrs.ForEachAttr: 63 if resourceAddr, ok := ref.ResourceInstance(); ok { 64 return a.metaReferencesCountOrEach(resourceAddr.ContainingResource()) 65 } 66 return nil 67 case addrs.ResourceInstance: 68 return a.metaReferencesResourceInstance(moduleAddr, targetAddr, remaining) 69 case addrs.Resource: 70 // TODO: It isn't really correct to say that a reference to a resource 71 // is a reference to its no-key instance. Really what we want to say 72 // here is that it's a reference to _all_ instances, or to an instance 73 // with an unknown key, but we don't have any representation of that. 74 // For the moment it's pretty immaterial since most of our other 75 // analysis ignores instance keys anyway, but maybe we'll revisit this 76 // latter to distingish these two cases better. 77 return a.metaReferencesResourceInstance(moduleAddr, targetAddr.Instance(addrs.NoKey), remaining) 78 default: 79 // For anything we don't explicitly support we'll just return no 80 // references. This includes the reference types that don't really 81 // refer to configuration objects at all, like "path.module", 82 // and so which cannot possibly generate any references. 83 return nil 84 } 85 } 86 87 func (a *Analyzer) metaReferencesInputVariable(calleeAddr addrs.ModuleInstance, addr addrs.InputVariable, remain hcl.Traversal) []Reference { 88 if calleeAddr.IsRoot() { 89 // A root module variable definition can never refer to anything, 90 // because it conceptually exists outside of any module. 91 return nil 92 } 93 94 callerAddr, callAddr := calleeAddr.Call() 95 96 // We need to find the module call inside the caller module. 97 callerCfg := a.ModuleConfig(callerAddr) 98 if callerCfg == nil { 99 return nil 100 } 101 call := callerCfg.ModuleCalls[callAddr.Name] 102 if call == nil { 103 return nil 104 } 105 106 // Now we need to look for an attribute matching the variable name inside 107 // the module block body. 108 body := call.Config 109 schema := &hcl.BodySchema{ 110 Attributes: []hcl.AttributeSchema{ 111 {Name: addr.Name}, 112 }, 113 } 114 // We don't check for errors here because we'll make a best effort to 115 // analyze whatever partial result HCL is able to extract. 116 content, _, _ := body.PartialContent(schema) 117 attr := content.Attributes[addr.Name] 118 if attr == nil { 119 return nil 120 } 121 refs, _ := lang.ReferencesInExpr(addrs.ParseRef, attr.Expr) 122 return absoluteRefs(callerAddr, refs) 123 } 124 125 func (a *Analyzer) metaReferencesOutputValue(callerAddr addrs.ModuleInstance, addr addrs.ModuleCallInstanceOutput, remain hcl.Traversal) []Reference { 126 calleeAddr := callerAddr.Child(addr.Call.Call.Name, addr.Call.Key) 127 128 // We need to find the output value declaration inside the callee module. 129 calleeCfg := a.ModuleConfig(calleeAddr) 130 if calleeCfg == nil { 131 return nil 132 } 133 134 oc := calleeCfg.Outputs[addr.Name] 135 if oc == nil { 136 return nil 137 } 138 139 // We don't check for errors here because we'll make a best effort to 140 // analyze whatever partial result HCL is able to extract. 141 refs, _ := lang.ReferencesInExpr(addrs.ParseRef, oc.Expr) 142 return absoluteRefs(calleeAddr, refs) 143 } 144 145 func (a *Analyzer) metaReferencesLocalValue(moduleAddr addrs.ModuleInstance, addr addrs.LocalValue, remain hcl.Traversal) []Reference { 146 modCfg := a.ModuleConfig(moduleAddr) 147 if modCfg == nil { 148 return nil 149 } 150 151 local := modCfg.Locals[addr.Name] 152 if local == nil { 153 return nil 154 } 155 156 // We don't check for errors here because we'll make a best effort to 157 // analyze whatever partial result HCL is able to extract. 158 refs, _ := lang.ReferencesInExpr(addrs.ParseRef, local.Expr) 159 return absoluteRefs(moduleAddr, refs) 160 } 161 162 func (a *Analyzer) metaReferencesModuleCall(callerAddr addrs.ModuleInstance, addr addrs.ModuleCallInstance, remain hcl.Traversal) []Reference { 163 calleeAddr := callerAddr.Child(addr.Call.Name, addr.Key) 164 165 // What we're really doing here is just rolling up all of the references 166 // from all of this module's output values. 167 calleeCfg := a.ModuleConfig(calleeAddr) 168 if calleeCfg == nil { 169 return nil 170 } 171 172 var ret []Reference 173 for name := range calleeCfg.Outputs { 174 outputAddr := addrs.ModuleCallInstanceOutput{ 175 Call: addr, 176 Name: name, 177 } 178 moreRefs := a.metaReferencesOutputValue(callerAddr, outputAddr, nil) 179 ret = append(ret, moreRefs...) 180 } 181 return ret 182 } 183 184 func (a *Analyzer) metaReferencesCountOrEach(resourceAddr addrs.AbsResource) []Reference { 185 return a.ReferencesFromResourceRepetition(resourceAddr) 186 } 187 188 func (a *Analyzer) metaReferencesResourceInstance(moduleAddr addrs.ModuleInstance, addr addrs.ResourceInstance, remain hcl.Traversal) []Reference { 189 modCfg := a.ModuleConfig(moduleAddr) 190 if modCfg == nil { 191 return nil 192 } 193 194 rc := modCfg.ResourceByAddr(addr.Resource) 195 if rc == nil { 196 return nil 197 } 198 199 // In valid cases we should have the schema for this resource type 200 // available. In invalid cases we might be dealing with partial information, 201 // and so the schema might be nil so we won't be able to return reference 202 // information for this particular situation. 203 providerSchema, ok := a.providerSchemas[rc.Provider] 204 if !ok { 205 return nil 206 } 207 208 resourceTypeSchema, _ := providerSchema.SchemaForResourceAddr(addr.Resource) 209 if resourceTypeSchema == nil { 210 return nil 211 } 212 213 // When analyzing the resource configuration to look for references, we'll 214 // make a best effort to narrow down to only a particular sub-portion of 215 // the configuration by following the remaining traversal steps. In the 216 // ideal case this will lead us to a specific expression, but as a 217 // compromise it might lead us to some nested blocks where at least we 218 // can limit our searching only to those. 219 bodies := []hcl.Body{rc.Config} 220 var exprs []hcl.Expression 221 schema := resourceTypeSchema 222 var steppingThrough *configschema.NestedBlock 223 var steppingThroughType string 224 nextStep := func(newBodies []hcl.Body, newExprs []hcl.Expression) { 225 // We append exprs but replace bodies because exprs represent extra 226 // expressions we collected on the path, such as dynamic block for_each, 227 // which can potentially contribute to the final evalcontext, but 228 // bodies never contribute any values themselves, and instead just 229 // narrow down where we're searching. 230 bodies = newBodies 231 exprs = append(exprs, newExprs...) 232 steppingThrough = nil 233 steppingThroughType = "" 234 // Caller must also update "schema" if necessary. 235 } 236 traverseInBlock := func(name string) ([]hcl.Body, []hcl.Expression) { 237 if attr := schema.Attributes[name]; attr != nil { 238 // When we reach a specific attribute we can't traverse any deeper, because attributes are the leaves of the schema. 239 schema = nil 240 return traverseAttr(bodies, name) 241 } else if blockType := schema.BlockTypes[name]; blockType != nil { 242 // We need to take a different action here depending on 243 // the nesting mode of the block type. Some require us 244 // to traverse in two steps in order to select a specific 245 // child block, while others we can just step through 246 // directly. 247 switch blockType.Nesting { 248 case configschema.NestingSingle, configschema.NestingGroup: 249 // There should be only zero or one blocks of this 250 // type, so we can traverse in only one step. 251 schema = &blockType.Block 252 return traverseNestedBlockSingle(bodies, name) 253 case configschema.NestingMap, configschema.NestingList, configschema.NestingSet: 254 steppingThrough = blockType 255 return bodies, exprs // Preserve current selections for the second step 256 default: 257 // The above should be exhaustive, but just in case 258 // we add something new in future we'll bail out 259 // here and conservatively return everything under 260 // the current traversal point. 261 schema = nil 262 return nil, nil 263 } 264 } 265 266 // We'll get here if the given name isn't in the schema at all. If so, 267 // there's nothing else to be done here. 268 schema = nil 269 return nil, nil 270 } 271 Steps: 272 for _, step := range remain { 273 // If we filter out all of our bodies before we finish traversing then 274 // we know we won't find anything else, because all of our subsequent 275 // traversal steps won't have any bodies to search. 276 if len(bodies) == 0 { 277 return nil 278 } 279 // If we no longer have a schema then that suggests we've 280 // traversed as deep as what the schema covers (e.g. we reached 281 // a specific attribute) and so we'll stop early, assuming that 282 // any remaining steps are traversals into an attribute expression 283 // result. 284 if schema == nil { 285 break 286 } 287 288 switch step := step.(type) { 289 290 case hcl.TraverseAttr: 291 switch { 292 case steppingThrough != nil: 293 // If we're stepping through a NestingMap block then 294 // it's valid to use attribute syntax to select one of 295 // the blocks by its label. Other nesting types require 296 // TraverseIndex, so can never be valid. 297 if steppingThrough.Nesting != configschema.NestingMap { 298 nextStep(nil, nil) // bail out 299 continue 300 } 301 nextStep(traverseNestedBlockMap(bodies, steppingThroughType, step.Name)) 302 schema = &steppingThrough.Block 303 default: 304 nextStep(traverseInBlock(step.Name)) 305 if schema == nil { 306 // traverseInBlock determined that we've traversed as 307 // deep as we can with reference to schema, so we'll 308 // stop here and just process whatever's selected. 309 break Steps 310 } 311 } 312 case hcl.TraverseIndex: 313 switch { 314 case steppingThrough != nil: 315 switch steppingThrough.Nesting { 316 case configschema.NestingMap: 317 keyVal, err := convert.Convert(step.Key, cty.String) 318 if err != nil { // Invalid traversal, so can't have any refs 319 nextStep(nil, nil) // bail out 320 continue 321 } 322 nextStep(traverseNestedBlockMap(bodies, steppingThroughType, keyVal.AsString())) 323 schema = &steppingThrough.Block 324 case configschema.NestingList: 325 idxVal, err := convert.Convert(step.Key, cty.Number) 326 if err != nil { // Invalid traversal, so can't have any refs 327 nextStep(nil, nil) // bail out 328 continue 329 } 330 var idx int 331 err = gocty.FromCtyValue(idxVal, &idx) 332 if err != nil { // Invalid traversal, so can't have any refs 333 nextStep(nil, nil) // bail out 334 continue 335 } 336 nextStep(traverseNestedBlockList(bodies, steppingThroughType, idx)) 337 schema = &steppingThrough.Block 338 default: 339 // Note that NestingSet ends up in here because we don't 340 // actually allow traversing into set-backed block types, 341 // and so such a reference would be invalid. 342 nextStep(nil, nil) // bail out 343 continue 344 } 345 default: 346 // When indexing the contents of a block directly we always 347 // interpret the key as a string representing an attribute 348 // name. 349 nameVal, err := convert.Convert(step.Key, cty.String) 350 if err != nil { // Invalid traversal, so can't have any refs 351 nextStep(nil, nil) // bail out 352 continue 353 } 354 nextStep(traverseInBlock(nameVal.AsString())) 355 if schema == nil { 356 // traverseInBlock determined that we've traversed as 357 // deep as we can with reference to schema, so we'll 358 // stop here and just process whatever's selected. 359 break Steps 360 } 361 } 362 default: 363 // We shouldn't get here, because the above cases are exhaustive 364 // for all of the relative traversal types, but we'll be robust in 365 // case HCL adds more in future and just pretend the traversal 366 // ended a bit early if so. 367 break Steps 368 } 369 } 370 371 if steppingThrough != nil { 372 // If we ended in the middle of "stepping through" then we'll conservatively 373 // use the bodies of _all_ nested blocks of the type we were stepping 374 // through, because the recipient of this value could refer to any 375 // of them dynamically. 376 var labelNames []string 377 if steppingThrough.Nesting == configschema.NestingMap { 378 labelNames = []string{"key"} 379 } 380 blocks := findBlocksInBodies(bodies, steppingThroughType, labelNames) 381 for _, block := range blocks { 382 bodies, exprs = blockParts(block) 383 } 384 } 385 386 if len(bodies) == 0 && len(exprs) == 0 { 387 return nil 388 } 389 390 var refs []*addrs.Reference 391 for _, expr := range exprs { 392 moreRefs, _ := lang.ReferencesInExpr(addrs.ParseRef, expr) 393 refs = append(refs, moreRefs...) 394 } 395 if schema != nil { 396 for _, body := range bodies { 397 moreRefs, _ := lang.ReferencesInBlock(addrs.ParseRef, body, schema) 398 refs = append(refs, moreRefs...) 399 } 400 } 401 return absoluteRefs(addr.Absolute(moduleAddr), refs) 402 } 403 404 func traverseAttr(bodies []hcl.Body, name string) ([]hcl.Body, []hcl.Expression) { 405 if len(bodies) == 0 { 406 return nil, nil 407 } 408 schema := &hcl.BodySchema{ 409 Attributes: []hcl.AttributeSchema{ 410 {Name: name}, 411 }, 412 } 413 // We can find at most one expression per body, because attribute names 414 // are always unique within a body. 415 retExprs := make([]hcl.Expression, 0, len(bodies)) 416 for _, body := range bodies { 417 content, _, _ := body.PartialContent(schema) 418 if attr := content.Attributes[name]; attr != nil && attr.Expr != nil { 419 retExprs = append(retExprs, attr.Expr) 420 } 421 } 422 return nil, retExprs 423 } 424 425 func traverseNestedBlockSingle(bodies []hcl.Body, typeName string) ([]hcl.Body, []hcl.Expression) { 426 if len(bodies) == 0 { 427 return nil, nil 428 } 429 430 blocks := findBlocksInBodies(bodies, typeName, nil) 431 var retBodies []hcl.Body 432 var retExprs []hcl.Expression 433 for _, block := range blocks { 434 moreBodies, moreExprs := blockParts(block) 435 retBodies = append(retBodies, moreBodies...) 436 retExprs = append(retExprs, moreExprs...) 437 } 438 return retBodies, retExprs 439 } 440 441 func traverseNestedBlockMap(bodies []hcl.Body, typeName string, key string) ([]hcl.Body, []hcl.Expression) { 442 if len(bodies) == 0 { 443 return nil, nil 444 } 445 446 blocks := findBlocksInBodies(bodies, typeName, []string{"key"}) 447 var retBodies []hcl.Body 448 var retExprs []hcl.Expression 449 for _, block := range blocks { 450 switch block.Type { 451 case "dynamic": 452 // For dynamic blocks we allow the key to be chosen dynamically 453 // and so we'll just conservatively include all dynamic block 454 // bodies. However, we need to also look for references in some 455 // arguments of the dynamic block itself. 456 argExprs, contentBody := dynamicBlockParts(block.Body) 457 retExprs = append(retExprs, argExprs...) 458 if contentBody != nil { 459 retBodies = append(retBodies, contentBody) 460 } 461 case typeName: 462 if len(block.Labels) == 1 && block.Labels[0] == key && block.Body != nil { 463 retBodies = append(retBodies, block.Body) 464 } 465 } 466 } 467 return retBodies, retExprs 468 } 469 470 func traverseNestedBlockList(bodies []hcl.Body, typeName string, idx int) ([]hcl.Body, []hcl.Expression) { 471 if len(bodies) == 0 { 472 return nil, nil 473 } 474 475 schema := &hcl.BodySchema{ 476 Blocks: []hcl.BlockHeaderSchema{ 477 {Type: typeName, LabelNames: nil}, 478 {Type: "dynamic", LabelNames: []string{"type"}}, 479 }, 480 } 481 var retBodies []hcl.Body 482 var retExprs []hcl.Expression 483 for _, body := range bodies { 484 content, _, _ := body.PartialContent(schema) 485 blocks := content.Blocks 486 487 // A tricky aspect of this scenario is that if there are any "dynamic" 488 // blocks then we can't statically predict how many concrete blocks they 489 // will generate, and so consequently we can't predict the indices of 490 // any statically-defined blocks that might appear after them. 491 firstDynamic := -1 // -1 means "no dynamic blocks" 492 for i, block := range blocks { 493 if block.Type == "dynamic" { 494 firstDynamic = i 495 break 496 } 497 } 498 499 switch { 500 case firstDynamic >= 0 && idx >= firstDynamic: 501 // This is the unfortunate case where the selection could be 502 // any of the blocks from firstDynamic onwards, and so we 503 // need to conservatively include all of them in our result. 504 for _, block := range blocks[firstDynamic:] { 505 moreBodies, moreExprs := blockParts(block) 506 retBodies = append(retBodies, moreBodies...) 507 retExprs = append(retExprs, moreExprs...) 508 } 509 default: 510 // This is the happier case where we can select just a single 511 // static block based on idx. Note that this one is guaranteed 512 // to never be dynamic but we're using blockParts here just 513 // for consistency. 514 moreBodies, moreExprs := blockParts(blocks[idx]) 515 retBodies = append(retBodies, moreBodies...) 516 retExprs = append(retExprs, moreExprs...) 517 } 518 } 519 520 return retBodies, retExprs 521 } 522 523 func findBlocksInBodies(bodies []hcl.Body, typeName string, labelNames []string) []*hcl.Block { 524 // We need to look for both static blocks of the given type, and any 525 // dynamic blocks whose label gives the expected type name. 526 schema := &hcl.BodySchema{ 527 Blocks: []hcl.BlockHeaderSchema{ 528 {Type: typeName, LabelNames: labelNames}, 529 {Type: "dynamic", LabelNames: []string{"type"}}, 530 }, 531 } 532 var blocks []*hcl.Block 533 for _, body := range bodies { 534 // We ignore errors here because we'll just make a best effort to analyze 535 // whatever partial result HCL returns in that case. 536 content, _, _ := body.PartialContent(schema) 537 538 for _, block := range content.Blocks { 539 switch block.Type { 540 case "dynamic": 541 if len(block.Labels) != 1 { // Invalid 542 continue 543 } 544 if block.Labels[0] == typeName { 545 blocks = append(blocks, block) 546 } 547 case typeName: 548 blocks = append(blocks, block) 549 } 550 } 551 } 552 553 // NOTE: The caller still needs to check for dynamic vs. static in order 554 // to do further processing. The callers above all aim to encapsulate 555 // that. 556 return blocks 557 } 558 559 func blockParts(block *hcl.Block) ([]hcl.Body, []hcl.Expression) { 560 switch block.Type { 561 case "dynamic": 562 exprs, contentBody := dynamicBlockParts(block.Body) 563 var bodies []hcl.Body 564 if contentBody != nil { 565 bodies = []hcl.Body{contentBody} 566 } 567 return bodies, exprs 568 default: 569 if block.Body == nil { 570 return nil, nil 571 } 572 return []hcl.Body{block.Body}, nil 573 } 574 } 575 576 func dynamicBlockParts(body hcl.Body) ([]hcl.Expression, hcl.Body) { 577 if body == nil { 578 return nil, nil 579 } 580 581 // This is a subset of the "dynamic" block schema defined by the HCL 582 // dynblock extension, covering only the two arguments that are allowed 583 // to be arbitrary expressions possibly referring elsewhere. 584 schema := &hcl.BodySchema{ 585 Attributes: []hcl.AttributeSchema{ 586 {Name: "for_each"}, 587 {Name: "labels"}, 588 }, 589 Blocks: []hcl.BlockHeaderSchema{ 590 {Type: "content"}, 591 }, 592 } 593 content, _, _ := body.PartialContent(schema) 594 var exprs []hcl.Expression 595 if len(content.Attributes) != 0 { 596 exprs = make([]hcl.Expression, 0, len(content.Attributes)) 597 } 598 for _, attr := range content.Attributes { 599 if attr.Expr != nil { 600 exprs = append(exprs, attr.Expr) 601 } 602 } 603 var contentBody hcl.Body 604 for _, block := range content.Blocks { 605 if block != nil && block.Type == "content" && block.Body != nil { 606 contentBody = block.Body 607 } 608 } 609 return exprs, contentBody 610 }