github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/lang/globalref/analyzer_contributing_resources.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package globalref
     5  
     6  import (
     7  	"sort"
     8  
     9  	"github.com/terramate-io/tf/addrs"
    10  )
    11  
    12  // ContributingResources analyzes all of the given references and
    13  // for each one tries to walk backwards through any named values to find all
    14  // resources whose values contributed either directly or indirectly to any of
    15  // them.
    16  //
    17  // This is a wrapper around ContributingResourceReferences which simplifies
    18  // the result to only include distinct resource addresses, not full references.
    19  // If the configuration includes several different references to different
    20  // parts of a resource, ContributingResources will not preserve that detail.
    21  func (a *Analyzer) ContributingResources(refs ...Reference) []addrs.AbsResource {
    22  	retRefs := a.ContributingResourceReferences(refs...)
    23  	if len(retRefs) == 0 {
    24  		return nil
    25  	}
    26  
    27  	uniq := make(map[string]addrs.AbsResource, len(refs))
    28  	for _, ref := range retRefs {
    29  		if addr, ok := resourceForAddr(ref.LocalRef.Subject); ok {
    30  			moduleAddr := ref.ModuleAddr()
    31  			absAddr := addr.Absolute(moduleAddr)
    32  			uniq[absAddr.String()] = absAddr
    33  		}
    34  	}
    35  	ret := make([]addrs.AbsResource, 0, len(uniq))
    36  	for _, addr := range uniq {
    37  		ret = append(ret, addr)
    38  	}
    39  	sort.Slice(ret, func(i, j int) bool {
    40  		// We only have a sorting function for resource _instances_, but
    41  		// it'll do well enough if we just pretend we have no-key instances.
    42  		return ret[i].Instance(addrs.NoKey).Less(ret[j].Instance(addrs.NoKey))
    43  	})
    44  	return ret
    45  }
    46  
    47  // ContributingResourceReferences analyzes all of the given references and
    48  // for each one tries to walk backwards through any named values to find all
    49  // references to resource attributes that contributed either directly or
    50  // indirectly to any of them.
    51  //
    52  // This is a global operation that can be potentially quite expensive for
    53  // complex configurations.
    54  func (a *Analyzer) ContributingResourceReferences(refs ...Reference) []Reference {
    55  	// Our methodology here is to keep digging through MetaReferences
    56  	// until we've visited everything we encounter directly or indirectly,
    57  	// and keep track of any resources we find along the way.
    58  
    59  	// We'll aggregate our result here, using the string representations of
    60  	// the resources as keys to avoid returning the same one more than once.
    61  	found := make(map[referenceAddrKey]Reference)
    62  
    63  	// We might encounter the same object multiple times as we walk,
    64  	// but we won't learn anything more by traversing them again and so we'll
    65  	// just skip them instead.
    66  	visitedObjects := make(map[referenceAddrKey]struct{})
    67  
    68  	// A queue of objects we still need to visit.
    69  	// Note that if we find multiple references to the same object then we'll
    70  	// just arbitrary choose any one of them, because for our purposes here
    71  	// it's immaterial which reference we actually followed.
    72  	pendingObjects := make(map[referenceAddrKey]Reference)
    73  
    74  	// Initial state: identify any directly-mentioned resources and
    75  	// queue up any named values we refer to.
    76  	for _, ref := range refs {
    77  		if _, ok := resourceForAddr(ref.LocalRef.Subject); ok {
    78  			found[ref.addrKey()] = ref
    79  		}
    80  		pendingObjects[ref.addrKey()] = ref
    81  	}
    82  
    83  	for len(pendingObjects) > 0 {
    84  		// Note: we modify this map while we're iterating over it, which means
    85  		// that anything we add might be either visited within a later
    86  		// iteration of the inner loop or in a later iteration of the outer
    87  		// loop, but we get the correct result either way because we keep
    88  		// working until we've fully depleted the queue.
    89  		for key, ref := range pendingObjects {
    90  			delete(pendingObjects, key)
    91  
    92  			// We do this _before_ the visit below just in case this is an
    93  			// invalid config with a self-referential local value, in which
    94  			// case we'll just silently ignore the self reference for our
    95  			// purposes here, and thus still eventually converge (albeit
    96  			// with an incomplete answer).
    97  			visitedObjects[key] = struct{}{}
    98  
    99  			moreRefs := a.MetaReferences(ref)
   100  			for _, newRef := range moreRefs {
   101  				if _, ok := resourceForAddr(newRef.LocalRef.Subject); ok {
   102  					found[newRef.addrKey()] = newRef
   103  				}
   104  
   105  				newKey := newRef.addrKey()
   106  				if _, visited := visitedObjects[newKey]; !visited {
   107  					pendingObjects[newKey] = newRef
   108  				}
   109  			}
   110  		}
   111  	}
   112  
   113  	if len(found) == 0 {
   114  		return nil
   115  	}
   116  
   117  	ret := make([]Reference, 0, len(found))
   118  	for _, ref := range found {
   119  		ret = append(ret, ref)
   120  	}
   121  	return ret
   122  }
   123  
   124  func resourceForAddr(addr addrs.Referenceable) (addrs.Resource, bool) {
   125  	switch addr := addr.(type) {
   126  	case addrs.Resource:
   127  		return addr, true
   128  	case addrs.ResourceInstance:
   129  		return addr.Resource, true
   130  	default:
   131  		return addrs.Resource{}, false
   132  	}
   133  }