github.com/opentofu/opentofu@v1.7.1/internal/lang/globalref/analyzer_contributing_resources.go (about)

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