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 }