terraform/internal/lang/globalref/analyzer_contributing_resou...

131 lines
4.5 KiB
Go
Raw Normal View History

lang/globalref: Global reference analysis utilities Our existing functionality for dealing with references generally only has to concern itself with one level of references at a time, and only within one module, because we use it to draw a dependency graph which then ends up reflecting the broader context. However, there are some situations where it's handy to be able to ask questions about the indirect contributions to a particular expression in the configuration, particularly for additional hints in the user interface where we're just providing some extra context rather than changing behavior. This new "globalref" package therefore aims to be the home for algorithms for use-cases like this. It introduces its own special "Reference" type that wraps addrs.Reference to annotate it also with the usually-implied context about where the references would be evaluated. With that building block we can therefore ask questions whose answers might involve discussing references in multiple packages at once, such as "which resources directly or indirectly contribute to this expression?", including indirect hops through input variables or output values which would therefore change the evaluation context. The current implementations of this are around mapping references onto the static configuration expressions that they refer to, which is a pretty broad and conservative approach that unfortunately therefore loses accuracy when confronted with complex expressions that might take dynamic actions on the contents of an object. My hunch is that this'll be good enough to get some initial small use-cases solved, though there's plenty room for improvement in accuracy. It's somewhat ironic that this sort of "what is this value built from?" question is the use-case I had in mind when I designed the "marks" feature in cty, yet we've ended up putting it to an unexpected but still valid use in Terraform for sensitivity analysis and our currently handling of that isn't really tight enough to permit other concurrent uses of marks for other use-cases. I expect we can address that later and so maybe we'll try for a more accurate version of these analyses at a later date, but my hunch is that this'll be good enough for us to still get some good use out of it in the near future, particular related to helping understand where unknown values came from and in tailoring our refresh results in plan output to deemphasize detected changes that couldn't possibly have contributed to the proposed plan.
2021-06-09 21:11:44 +02:00
package globalref
import (
"sort"
"github.com/hashicorp/terraform/internal/addrs"
)
// ContributingResources analyzes all of the given references and
// for each one tries to walk backwards through any named values to find all
// resources whose values contributed either directly or indirectly to any of
// them.
//
// This is a wrapper around ContributingResourceReferences which simplifies
// the result to only include distinct resource addresses, not full references.
// If the configuration includes several different references to different
// parts of a resource, ContributingResources will not preserve that detail.
func (a *Analyzer) ContributingResources(refs ...Reference) []addrs.AbsResource {
retRefs := a.ContributingResourceReferences(refs...)
if len(retRefs) == 0 {
return nil
}
uniq := make(map[string]addrs.AbsResource, len(refs))
for _, ref := range retRefs {
if addr, ok := resourceForAddr(ref.LocalRef.Subject); ok {
moduleAddr := ref.ModuleAddr()
absAddr := addr.Absolute(moduleAddr)
uniq[absAddr.String()] = absAddr
}
}
ret := make([]addrs.AbsResource, 0, len(uniq))
for _, addr := range uniq {
ret = append(ret, addr)
}
sort.Slice(ret, func(i, j int) bool {
// We only have a sorting function for resource _instances_, but
// it'll do well enough if we just pretend we have no-key instances.
return ret[i].Instance(addrs.NoKey).Less(ret[j].Instance(addrs.NoKey))
})
return ret
}
// ContributingResourceReferences analyzes all of the given references and
// for each one tries to walk backwards through any named values to find all
// references to resource attributes that contributed either directly or
// indirectly to any of them.
//
// This is a global operation that can be potentially quite expensive for
// complex configurations.
func (a *Analyzer) ContributingResourceReferences(refs ...Reference) []Reference {
// Our methodology here is to keep digging through MetaReferences
// until we've visited everything we encounter directly or indirectly,
// and keep track of any resources we find along the way.
// We'll aggregate our result here, using the string representations of
// the resources as keys to avoid returning the same one more than once.
found := make(map[referenceAddrKey]Reference)
// We might encounter the same object multiple times as we walk,
// but we won't learn anything more by traversing them again and so we'll
// just skip them instead.
visitedObjects := make(map[referenceAddrKey]struct{})
// A queue of objects we still need to visit.
// Note that if we find multiple references to the same object then we'll
// just arbitrary choose any one of them, because for our purposes here
// it's immaterial which reference we actually followed.
pendingObjects := make(map[referenceAddrKey]Reference)
// Initial state: identify any directly-mentioned resources and
// queue up any named values we refer to.
for _, ref := range refs {
if _, ok := resourceForAddr(ref.LocalRef.Subject); ok {
found[ref.addrKey()] = ref
}
pendingObjects[ref.addrKey()] = ref
}
for len(pendingObjects) > 0 {
// Note: we modify this map while we're iterating over it, which means
// that anything we add might be either visited within a later
// iteration of the inner loop or in a later iteration of the outer
// loop, but we get the correct result either way because we keep
// working until we've fully depleted the queue.
for key, ref := range pendingObjects {
delete(pendingObjects, key)
// We do this _before_ the visit below just in case this is an
// invalid config with a self-referential local value, in which
// case we'll just silently ignore the self reference for our
// purposes here, and thus still eventually converge (albeit
// with an incomplete answer).
visitedObjects[key] = struct{}{}
moreRefs := a.MetaReferences(ref)
for _, newRef := range moreRefs {
if _, ok := resourceForAddr(newRef.LocalRef.Subject); ok {
found[newRef.addrKey()] = newRef
}
newKey := newRef.addrKey()
if _, visited := visitedObjects[newKey]; !visited {
pendingObjects[newKey] = newRef
}
}
}
}
if len(found) == 0 {
return nil
}
ret := make([]Reference, 0, len(found))
for _, ref := range found {
ret = append(ret, ref)
}
return ret
}
func resourceForAddr(addr addrs.Referenceable) (addrs.Resource, bool) {
switch addr := addr.(type) {
case addrs.Resource:
return addr, true
case addrs.ResourceInstance:
return addr.Resource, true
default:
return addrs.Resource{}, false
}
}