terraform/internal/lang/globalref/analyzer_meta_references.go

607 lines
21 KiB
Go

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
}