| package globalref |
| |
| import ( |
| "github.com/hashicorp/hcl/v2" |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/configs/configschema" |
| "github.com/hashicorp/terraform/internal/lang" |
| "github.com/zclconf/go-cty/cty" |
| "github.com/zclconf/go-cty/cty/convert" |
| "github.com/zclconf/go-cty/cty/gocty" |
| ) |
| |
| // MetaReferences inspects the configuration to find the references contained |
| // within the most specific object that the given address refers to. |
| // |
| // This finds only the direct references in that object, not any indirect |
| // references from those. This is a building block for some other Analyzer |
| // functions that can walk through multiple levels of reference. |
| // |
| // If the given reference refers to something that doesn't exist in the |
| // configuration we're analyzing then MetaReferences will return no |
| // meta-references at all, which is indistinguishable from an existing |
| // object that doesn't refer to anything. |
| func (a *Analyzer) MetaReferences(ref Reference) []Reference { |
| // This function is aiming to encapsulate the fact that a reference |
| // is actually quite a complex notion which includes both a specific |
| // object the reference is to, where each distinct object type has |
| // a very different representation in the configuration, and then |
| // also potentially an attribute or block within the definition of that |
| // object. Our goal is to make all of these different situations appear |
| // mostly the same to the caller, in that all of them can be reduced to |
| // a set of references regardless of which expression or expressions we |
| // derive those from. |
| |
| moduleAddr := ref.ModuleAddr() |
| remaining := ref.LocalRef.Remaining |
| |
| // Our first task then is to select an appropriate implementation based |
| // on which address type the reference refers to. |
| switch targetAddr := ref.LocalRef.Subject.(type) { |
| case addrs.InputVariable: |
| return a.metaReferencesInputVariable(moduleAddr, targetAddr, remaining) |
| case addrs.LocalValue: |
| return a.metaReferencesLocalValue(moduleAddr, targetAddr, remaining) |
| case addrs.ModuleCallInstanceOutput: |
| return a.metaReferencesOutputValue(moduleAddr, targetAddr, remaining) |
| case addrs.ModuleCallInstance: |
| return a.metaReferencesModuleCall(moduleAddr, targetAddr, remaining) |
| case addrs.ModuleCall: |
| // TODO: It isn't really correct to say that a reference to a module |
| // call is a reference to its no-key instance. Really what we want to |
| // say here is that it's a reference to _all_ instances, or to an |
| // instance with an unknown key, but we don't have any representation |
| // of that. For the moment it's pretty immaterial since most of our |
| // other analysis ignores instance keys anyway, but maybe we'll revisit |
| // this latter to distingish these two cases better. |
| return a.metaReferencesModuleCall(moduleAddr, targetAddr.Instance(addrs.NoKey), remaining) |
| case addrs.CountAttr, addrs.ForEachAttr: |
| if resourceAddr, ok := ref.ResourceInstance(); ok { |
| return a.metaReferencesCountOrEach(resourceAddr.ContainingResource()) |
| } |
| return nil |
| case addrs.ResourceInstance: |
| return a.metaReferencesResourceInstance(moduleAddr, targetAddr, remaining) |
| case addrs.Resource: |
| // TODO: It isn't really correct to say that a reference to a resource |
| // is a reference to its no-key instance. Really what we want to say |
| // here is that it's a reference to _all_ instances, or to an instance |
| // with an unknown key, but we don't have any representation of that. |
| // For the moment it's pretty immaterial since most of our other |
| // analysis ignores instance keys anyway, but maybe we'll revisit this |
| // latter to distingish these two cases better. |
| return a.metaReferencesResourceInstance(moduleAddr, targetAddr.Instance(addrs.NoKey), remaining) |
| default: |
| // For anything we don't explicitly support we'll just return no |
| // references. This includes the reference types that don't really |
| // refer to configuration objects at all, like "path.module", |
| // and so which cannot possibly generate any references. |
| return nil |
| } |
| } |
| |
| func (a *Analyzer) metaReferencesInputVariable(calleeAddr addrs.ModuleInstance, addr addrs.InputVariable, remain hcl.Traversal) []Reference { |
| if calleeAddr.IsRoot() { |
| // A root module variable definition can never refer to anything, |
| // because it conceptually exists outside of any module. |
| return nil |
| } |
| |
| callerAddr, callAddr := calleeAddr.Call() |
| |
| // We need to find the module call inside the caller module. |
| callerCfg := a.ModuleConfig(callerAddr) |
| if callerCfg == nil { |
| return nil |
| } |
| call := callerCfg.ModuleCalls[callAddr.Name] |
| if call == nil { |
| return nil |
| } |
| |
| // Now we need to look for an attribute matching the variable name inside |
| // the module block body. |
| body := call.Config |
| schema := &hcl.BodySchema{ |
| Attributes: []hcl.AttributeSchema{ |
| {Name: addr.Name}, |
| }, |
| } |
| // We don't check for errors here because we'll make a best effort to |
| // analyze whatever partial result HCL is able to extract. |
| content, _, _ := body.PartialContent(schema) |
| attr := content.Attributes[addr.Name] |
| if attr == nil { |
| return nil |
| } |
| refs, _ := lang.ReferencesInExpr(attr.Expr) |
| return absoluteRefs(callerAddr, refs) |
| } |
| |
| func (a *Analyzer) metaReferencesOutputValue(callerAddr addrs.ModuleInstance, addr addrs.ModuleCallInstanceOutput, remain hcl.Traversal) []Reference { |
| calleeAddr := callerAddr.Child(addr.Call.Call.Name, addr.Call.Key) |
| |
| // We need to find the output value declaration inside the callee module. |
| calleeCfg := a.ModuleConfig(calleeAddr) |
| if calleeCfg == nil { |
| return nil |
| } |
| |
| oc := calleeCfg.Outputs[addr.Name] |
| if oc == nil { |
| return nil |
| } |
| |
| // We don't check for errors here because we'll make a best effort to |
| // analyze whatever partial result HCL is able to extract. |
| refs, _ := lang.ReferencesInExpr(oc.Expr) |
| return absoluteRefs(calleeAddr, refs) |
| } |
| |
| func (a *Analyzer) metaReferencesLocalValue(moduleAddr addrs.ModuleInstance, addr addrs.LocalValue, remain hcl.Traversal) []Reference { |
| modCfg := a.ModuleConfig(moduleAddr) |
| if modCfg == nil { |
| return nil |
| } |
| |
| local := modCfg.Locals[addr.Name] |
| if local == nil { |
| return nil |
| } |
| |
| // We don't check for errors here because we'll make a best effort to |
| // analyze whatever partial result HCL is able to extract. |
| refs, _ := lang.ReferencesInExpr(local.Expr) |
| return absoluteRefs(moduleAddr, refs) |
| } |
| |
| func (a *Analyzer) metaReferencesModuleCall(callerAddr addrs.ModuleInstance, addr addrs.ModuleCallInstance, remain hcl.Traversal) []Reference { |
| calleeAddr := callerAddr.Child(addr.Call.Name, addr.Key) |
| |
| // What we're really doing here is just rolling up all of the references |
| // from all of this module's output values. |
| calleeCfg := a.ModuleConfig(calleeAddr) |
| if calleeCfg == nil { |
| return nil |
| } |
| |
| var ret []Reference |
| for name := range calleeCfg.Outputs { |
| outputAddr := addrs.ModuleCallInstanceOutput{ |
| Call: addr, |
| Name: name, |
| } |
| moreRefs := a.metaReferencesOutputValue(callerAddr, outputAddr, nil) |
| ret = append(ret, moreRefs...) |
| } |
| return ret |
| } |
| |
| func (a *Analyzer) metaReferencesCountOrEach(resourceAddr addrs.AbsResource) []Reference { |
| return a.ReferencesFromResourceRepetition(resourceAddr) |
| } |
| |
| func (a *Analyzer) metaReferencesResourceInstance(moduleAddr addrs.ModuleInstance, addr addrs.ResourceInstance, remain hcl.Traversal) []Reference { |
| modCfg := a.ModuleConfig(moduleAddr) |
| if modCfg == nil { |
| return nil |
| } |
| |
| rc := modCfg.ResourceByAddr(addr.Resource) |
| if rc == nil { |
| return nil |
| } |
| |
| // In valid cases we should have the schema for this resource type |
| // available. In invalid cases we might be dealing with partial information, |
| // and so the schema might be nil so we won't be able to return reference |
| // information for this particular situation. |
| providerSchema := a.providerSchemas[rc.Provider] |
| if providerSchema == nil { |
| return nil |
| } |
| |
| resourceTypeSchema, _ := providerSchema.SchemaForResourceAddr(addr.Resource) |
| if resourceTypeSchema == nil { |
| return nil |
| } |
| |
| // When analyzing the resource configuration to look for references, we'll |
| // make a best effort to narrow down to only a particular sub-portion of |
| // the configuration by following the remaining traversal steps. In the |
| // ideal case this will lead us to a specific expression, but as a |
| // compromise it might lead us to some nested blocks where at least we |
| // can limit our searching only to those. |
| bodies := []hcl.Body{rc.Config} |
| var exprs []hcl.Expression |
| schema := resourceTypeSchema |
| var steppingThrough *configschema.NestedBlock |
| var steppingThroughType string |
| nextStep := func(newBodies []hcl.Body, newExprs []hcl.Expression) { |
| // We append exprs but replace bodies because exprs represent extra |
| // expressions we collected on the path, such as dynamic block for_each, |
| // which can potentially contribute to the final evalcontext, but |
| // bodies never contribute any values themselves, and instead just |
| // narrow down where we're searching. |
| bodies = newBodies |
| exprs = append(exprs, newExprs...) |
| steppingThrough = nil |
| steppingThroughType = "" |
| // Caller must also update "schema" if necessary. |
| } |
| traverseInBlock := func(name string) ([]hcl.Body, []hcl.Expression) { |
| if attr := schema.Attributes[name]; attr != nil { |
| // When we reach a specific attribute we can't traverse any deeper, because attributes are the leaves of the schema. |
| schema = nil |
| return traverseAttr(bodies, name) |
| } else if blockType := schema.BlockTypes[name]; blockType != nil { |
| // We need to take a different action here depending on |
| // the nesting mode of the block type. Some require us |
| // to traverse in two steps in order to select a specific |
| // child block, while others we can just step through |
| // directly. |
| switch blockType.Nesting { |
| case configschema.NestingSingle, configschema.NestingGroup: |
| // There should be only zero or one blocks of this |
| // type, so we can traverse in only one step. |
| schema = &blockType.Block |
| return traverseNestedBlockSingle(bodies, name) |
| case configschema.NestingMap, configschema.NestingList, configschema.NestingSet: |
| steppingThrough = blockType |
| return bodies, exprs // Preserve current selections for the second step |
| default: |
| // The above should be exhaustive, but just in case |
| // we add something new in future we'll bail out |
| // here and conservatively return everything under |
| // the current traversal point. |
| schema = nil |
| return nil, nil |
| } |
| } |
| |
| // We'll get here if the given name isn't in the schema at all. If so, |
| // there's nothing else to be done here. |
| schema = nil |
| return nil, nil |
| } |
| Steps: |
| for _, step := range remain { |
| // If we filter out all of our bodies before we finish traversing then |
| // we know we won't find anything else, because all of our subsequent |
| // traversal steps won't have any bodies to search. |
| if len(bodies) == 0 { |
| return nil |
| } |
| // If we no longer have a schema then that suggests we've |
| // traversed as deep as what the schema covers (e.g. we reached |
| // a specific attribute) and so we'll stop early, assuming that |
| // any remaining steps are traversals into an attribute expression |
| // result. |
| if schema == nil { |
| break |
| } |
| |
| switch step := step.(type) { |
| |
| case hcl.TraverseAttr: |
| switch { |
| case steppingThrough != nil: |
| // If we're stepping through a NestingMap block then |
| // it's valid to use attribute syntax to select one of |
| // the blocks by its label. Other nesting types require |
| // TraverseIndex, so can never be valid. |
| if steppingThrough.Nesting != configschema.NestingMap { |
| nextStep(nil, nil) // bail out |
| continue |
| } |
| nextStep(traverseNestedBlockMap(bodies, steppingThroughType, step.Name)) |
| schema = &steppingThrough.Block |
| default: |
| nextStep(traverseInBlock(step.Name)) |
| if schema == nil { |
| // traverseInBlock determined that we've traversed as |
| // deep as we can with reference to schema, so we'll |
| // stop here and just process whatever's selected. |
| break Steps |
| } |
| } |
| case hcl.TraverseIndex: |
| switch { |
| case steppingThrough != nil: |
| switch steppingThrough.Nesting { |
| case configschema.NestingMap: |
| keyVal, err := convert.Convert(step.Key, cty.String) |
| if err != nil { // Invalid traversal, so can't have any refs |
| nextStep(nil, nil) // bail out |
| continue |
| } |
| nextStep(traverseNestedBlockMap(bodies, steppingThroughType, keyVal.AsString())) |
| schema = &steppingThrough.Block |
| case configschema.NestingList: |
| idxVal, err := convert.Convert(step.Key, cty.Number) |
| if err != nil { // Invalid traversal, so can't have any refs |
| nextStep(nil, nil) // bail out |
| continue |
| } |
| var idx int |
| err = gocty.FromCtyValue(idxVal, &idx) |
| if err != nil { // Invalid traversal, so can't have any refs |
| nextStep(nil, nil) // bail out |
| continue |
| } |
| nextStep(traverseNestedBlockList(bodies, steppingThroughType, idx)) |
| schema = &steppingThrough.Block |
| default: |
| // Note that NestingSet ends up in here because we don't |
| // actually allow traversing into set-backed block types, |
| // and so such a reference would be invalid. |
| nextStep(nil, nil) // bail out |
| continue |
| } |
| default: |
| // When indexing the contents of a block directly we always |
| // interpret the key as a string representing an attribute |
| // name. |
| nameVal, err := convert.Convert(step.Key, cty.String) |
| if err != nil { // Invalid traversal, so can't have any refs |
| nextStep(nil, nil) // bail out |
| continue |
| } |
| nextStep(traverseInBlock(nameVal.AsString())) |
| if schema == nil { |
| // traverseInBlock determined that we've traversed as |
| // deep as we can with reference to schema, so we'll |
| // stop here and just process whatever's selected. |
| break Steps |
| } |
| } |
| default: |
| // We shouldn't get here, because the above cases are exhaustive |
| // for all of the relative traversal types, but we'll be robust in |
| // case HCL adds more in future and just pretend the traversal |
| // ended a bit early if so. |
| break Steps |
| } |
| } |
| |
| if steppingThrough != nil { |
| // If we ended in the middle of "stepping through" then we'll conservatively |
| // use the bodies of _all_ nested blocks of the type we were stepping |
| // through, because the recipient of this value could refer to any |
| // of them dynamically. |
| var labelNames []string |
| if steppingThrough.Nesting == configschema.NestingMap { |
| labelNames = []string{"key"} |
| } |
| blocks := findBlocksInBodies(bodies, steppingThroughType, labelNames) |
| for _, block := range blocks { |
| bodies, exprs = blockParts(block) |
| } |
| } |
| |
| if len(bodies) == 0 && len(exprs) == 0 { |
| return nil |
| } |
| |
| var refs []*addrs.Reference |
| for _, expr := range exprs { |
| moreRefs, _ := lang.ReferencesInExpr(expr) |
| refs = append(refs, moreRefs...) |
| } |
| if schema != nil { |
| for _, body := range bodies { |
| moreRefs, _ := lang.ReferencesInBlock(body, schema) |
| refs = append(refs, moreRefs...) |
| } |
| } |
| return absoluteRefs(addr.Absolute(moduleAddr), refs) |
| } |
| |
| func traverseAttr(bodies []hcl.Body, name string) ([]hcl.Body, []hcl.Expression) { |
| if len(bodies) == 0 { |
| return nil, nil |
| } |
| schema := &hcl.BodySchema{ |
| Attributes: []hcl.AttributeSchema{ |
| {Name: name}, |
| }, |
| } |
| // We can find at most one expression per body, because attribute names |
| // are always unique within a body. |
| retExprs := make([]hcl.Expression, 0, len(bodies)) |
| for _, body := range bodies { |
| content, _, _ := body.PartialContent(schema) |
| if attr := content.Attributes[name]; attr != nil && attr.Expr != nil { |
| retExprs = append(retExprs, attr.Expr) |
| } |
| } |
| return nil, retExprs |
| } |
| |
| func traverseNestedBlockSingle(bodies []hcl.Body, typeName string) ([]hcl.Body, []hcl.Expression) { |
| if len(bodies) == 0 { |
| return nil, nil |
| } |
| |
| blocks := findBlocksInBodies(bodies, typeName, nil) |
| var retBodies []hcl.Body |
| var retExprs []hcl.Expression |
| for _, block := range blocks { |
| moreBodies, moreExprs := blockParts(block) |
| retBodies = append(retBodies, moreBodies...) |
| retExprs = append(retExprs, moreExprs...) |
| } |
| return retBodies, retExprs |
| } |
| |
| func traverseNestedBlockMap(bodies []hcl.Body, typeName string, key string) ([]hcl.Body, []hcl.Expression) { |
| if len(bodies) == 0 { |
| return nil, nil |
| } |
| |
| blocks := findBlocksInBodies(bodies, typeName, []string{"key"}) |
| var retBodies []hcl.Body |
| var retExprs []hcl.Expression |
| for _, block := range blocks { |
| switch block.Type { |
| case "dynamic": |
| // For dynamic blocks we allow the key to be chosen dynamically |
| // and so we'll just conservatively include all dynamic block |
| // bodies. However, we need to also look for references in some |
| // arguments of the dynamic block itself. |
| argExprs, contentBody := dynamicBlockParts(block.Body) |
| retExprs = append(retExprs, argExprs...) |
| if contentBody != nil { |
| retBodies = append(retBodies, contentBody) |
| } |
| case typeName: |
| if len(block.Labels) == 1 && block.Labels[0] == key && block.Body != nil { |
| retBodies = append(retBodies, block.Body) |
| } |
| } |
| } |
| return retBodies, retExprs |
| } |
| |
| func traverseNestedBlockList(bodies []hcl.Body, typeName string, idx int) ([]hcl.Body, []hcl.Expression) { |
| if len(bodies) == 0 { |
| return nil, nil |
| } |
| |
| schema := &hcl.BodySchema{ |
| Blocks: []hcl.BlockHeaderSchema{ |
| {Type: typeName, LabelNames: nil}, |
| {Type: "dynamic", LabelNames: []string{"type"}}, |
| }, |
| } |
| var retBodies []hcl.Body |
| var retExprs []hcl.Expression |
| for _, body := range bodies { |
| content, _, _ := body.PartialContent(schema) |
| blocks := content.Blocks |
| |
| // A tricky aspect of this scenario is that if there are any "dynamic" |
| // blocks then we can't statically predict how many concrete blocks they |
| // will generate, and so consequently we can't predict the indices of |
| // any statically-defined blocks that might appear after them. |
| firstDynamic := -1 // -1 means "no dynamic blocks" |
| for i, block := range blocks { |
| if block.Type == "dynamic" { |
| firstDynamic = i |
| break |
| } |
| } |
| |
| switch { |
| case firstDynamic >= 0 && idx >= firstDynamic: |
| // This is the unfortunate case where the selection could be |
| // any of the blocks from firstDynamic onwards, and so we |
| // need to conservatively include all of them in our result. |
| for _, block := range blocks[firstDynamic:] { |
| moreBodies, moreExprs := blockParts(block) |
| retBodies = append(retBodies, moreBodies...) |
| retExprs = append(retExprs, moreExprs...) |
| } |
| default: |
| // This is the happier case where we can select just a single |
| // static block based on idx. Note that this one is guaranteed |
| // to never be dynamic but we're using blockParts here just |
| // for consistency. |
| moreBodies, moreExprs := blockParts(blocks[idx]) |
| retBodies = append(retBodies, moreBodies...) |
| retExprs = append(retExprs, moreExprs...) |
| } |
| } |
| |
| return retBodies, retExprs |
| } |
| |
| func findBlocksInBodies(bodies []hcl.Body, typeName string, labelNames []string) []*hcl.Block { |
| // We need to look for both static blocks of the given type, and any |
| // dynamic blocks whose label gives the expected type name. |
| schema := &hcl.BodySchema{ |
| Blocks: []hcl.BlockHeaderSchema{ |
| {Type: typeName, LabelNames: labelNames}, |
| {Type: "dynamic", LabelNames: []string{"type"}}, |
| }, |
| } |
| var blocks []*hcl.Block |
| for _, body := range bodies { |
| // We ignore errors here because we'll just make a best effort to analyze |
| // whatever partial result HCL returns in that case. |
| content, _, _ := body.PartialContent(schema) |
| |
| for _, block := range content.Blocks { |
| switch block.Type { |
| case "dynamic": |
| if len(block.Labels) != 1 { // Invalid |
| continue |
| } |
| if block.Labels[0] == typeName { |
| blocks = append(blocks, block) |
| } |
| case typeName: |
| blocks = append(blocks, block) |
| } |
| } |
| } |
| |
| // NOTE: The caller still needs to check for dynamic vs. static in order |
| // to do further processing. The callers above all aim to encapsulate |
| // that. |
| return blocks |
| } |
| |
| func blockParts(block *hcl.Block) ([]hcl.Body, []hcl.Expression) { |
| switch block.Type { |
| case "dynamic": |
| exprs, contentBody := dynamicBlockParts(block.Body) |
| var bodies []hcl.Body |
| if contentBody != nil { |
| bodies = []hcl.Body{contentBody} |
| } |
| return bodies, exprs |
| default: |
| if block.Body == nil { |
| return nil, nil |
| } |
| return []hcl.Body{block.Body}, nil |
| } |
| } |
| |
| func dynamicBlockParts(body hcl.Body) ([]hcl.Expression, hcl.Body) { |
| if body == nil { |
| return nil, nil |
| } |
| |
| // This is a subset of the "dynamic" block schema defined by the HCL |
| // dynblock extension, covering only the two arguments that are allowed |
| // to be arbitrary expressions possibly referring elsewhere. |
| schema := &hcl.BodySchema{ |
| Attributes: []hcl.AttributeSchema{ |
| {Name: "for_each"}, |
| {Name: "labels"}, |
| }, |
| Blocks: []hcl.BlockHeaderSchema{ |
| {Type: "content"}, |
| }, |
| } |
| content, _, _ := body.PartialContent(schema) |
| var exprs []hcl.Expression |
| if len(content.Attributes) != 0 { |
| exprs = make([]hcl.Expression, 0, len(content.Attributes)) |
| } |
| for _, attr := range content.Attributes { |
| if attr.Expr != nil { |
| exprs = append(exprs, attr.Expr) |
| } |
| } |
| var contentBody hcl.Body |
| for _, block := range content.Blocks { |
| if block != nil && block.Type == "content" && block.Body != nil { |
| contentBody = block.Body |
| } |
| } |
| return exprs, contentBody |
| } |