github.com/opentofu/opentofu@v1.7.1/internal/command/jsonformat/differ/set.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 differ
     7  
     8  import (
     9  	"reflect"
    10  
    11  	"github.com/zclconf/go-cty/cty"
    12  
    13  	"github.com/opentofu/opentofu/internal/command/jsonformat/collections"
    14  	"github.com/opentofu/opentofu/internal/command/jsonformat/computed"
    15  	"github.com/opentofu/opentofu/internal/command/jsonformat/computed/renderers"
    16  	"github.com/opentofu/opentofu/internal/command/jsonformat/structured"
    17  	"github.com/opentofu/opentofu/internal/command/jsonformat/structured/attribute_path"
    18  	"github.com/opentofu/opentofu/internal/command/jsonprovider"
    19  	"github.com/opentofu/opentofu/internal/plans"
    20  )
    21  
    22  func computeAttributeDiffAsSet(change structured.Change, elementType cty.Type) computed.Diff {
    23  	var elements []computed.Diff
    24  	current := change.GetDefaultActionForIteration()
    25  	processSet(change, func(value structured.Change) {
    26  		element := ComputeDiffForType(value, elementType)
    27  		elements = append(elements, element)
    28  		current = collections.CompareActions(current, element.Action)
    29  	})
    30  	return computed.NewDiff(renderers.Set(elements), current, change.ReplacePaths.Matches())
    31  }
    32  
    33  func computeAttributeDiffAsNestedSet(change structured.Change, attributes map[string]*jsonprovider.Attribute) computed.Diff {
    34  	var elements []computed.Diff
    35  	current := change.GetDefaultActionForIteration()
    36  	processSet(change, func(value structured.Change) {
    37  		element := computeDiffForNestedAttribute(value, &jsonprovider.NestedType{
    38  			Attributes:  attributes,
    39  			NestingMode: "single",
    40  		})
    41  		elements = append(elements, element)
    42  		current = collections.CompareActions(current, element.Action)
    43  	})
    44  	return computed.NewDiff(renderers.NestedSet(elements), current, change.ReplacePaths.Matches())
    45  }
    46  
    47  func computeBlockDiffsAsSet(change structured.Change, block *jsonprovider.Block) ([]computed.Diff, plans.Action) {
    48  	var elements []computed.Diff
    49  	current := change.GetDefaultActionForIteration()
    50  	processSet(change, func(value structured.Change) {
    51  		element := ComputeDiffForBlock(value, block)
    52  		elements = append(elements, element)
    53  		current = collections.CompareActions(current, element.Action)
    54  	})
    55  	return elements, current
    56  }
    57  
    58  func processSet(change structured.Change, process func(value structured.Change)) {
    59  	sliceValue := change.AsSlice()
    60  
    61  	foundInBefore := make(map[int]int)
    62  	foundInAfter := make(map[int]int)
    63  
    64  	// O(n^2) operation here to find matching pairs in the set, so we can make
    65  	// the display look pretty. There might be a better way to do this, so look
    66  	// here for potential optimisations.
    67  
    68  	for ix := 0; ix < len(sliceValue.Before); ix++ {
    69  		matched := false
    70  		for jx := 0; jx < len(sliceValue.After); jx++ {
    71  			if _, ok := foundInAfter[jx]; ok {
    72  				// We've already found a match for this after value.
    73  				continue
    74  			}
    75  
    76  			child := sliceValue.GetChild(ix, jx)
    77  			if reflect.DeepEqual(child.Before, child.After) && child.IsBeforeSensitive() == child.IsAfterSensitive() && !child.IsUnknown() {
    78  				matched = true
    79  				foundInBefore[ix] = jx
    80  				foundInAfter[jx] = ix
    81  			}
    82  		}
    83  
    84  		if !matched {
    85  			foundInBefore[ix] = -1
    86  		}
    87  	}
    88  
    89  	clearRelevantStatus := func(change structured.Change) structured.Change {
    90  		// It's actually really difficult to render the diffs when some indices
    91  		// within a slice are relevant and others aren't. To make this simpler
    92  		// we just treat all children of a relevant list or set as also
    93  		// relevant.
    94  		//
    95  		// Interestingly the tofu plan builder also agrees with this, and
    96  		// never sets relevant attributes beneath lists or sets. We're just
    97  		// going to enforce this logic here as well. If the collection is
    98  		// relevant (decided elsewhere), then every element in the collection is
    99  		// also relevant. To be clear, in practice even if we didn't do the
   100  		// following explicitly the effect would be the same. It's just nicer
   101  		// for us to be clear about the behaviour we expect.
   102  		//
   103  		// What makes this difficult is the fact that the beforeIx and afterIx
   104  		// can be different, and it's quite difficult to work out which one is
   105  		// the relevant one. For nested lists, block lists, and tuples it's much
   106  		// easier because we always process the same indices in the before and
   107  		// after.
   108  		change.RelevantAttributes = attribute_path.AlwaysMatcher()
   109  		return change
   110  	}
   111  
   112  	// Now everything in before should be a key in foundInBefore and a value
   113  	// in foundInAfter. If a key is mapped to -1 in foundInBefore it means it
   114  	// does not have an equivalent in foundInAfter and so has been deleted.
   115  	// Everything in foundInAfter has a matching value in foundInBefore, but
   116  	// some values in after may not be in foundInAfter. This means these values
   117  	// are newly created.
   118  
   119  	for ix := 0; ix < len(sliceValue.Before); ix++ {
   120  		if jx := foundInBefore[ix]; jx >= 0 {
   121  			child := clearRelevantStatus(sliceValue.GetChild(ix, jx))
   122  			process(child)
   123  			continue
   124  		}
   125  		child := clearRelevantStatus(sliceValue.GetChild(ix, len(sliceValue.After)))
   126  		process(child)
   127  	}
   128  
   129  	for jx := 0; jx < len(sliceValue.After); jx++ {
   130  		if _, ok := foundInAfter[jx]; ok {
   131  			// Then this value was handled in the previous for loop.
   132  			continue
   133  		}
   134  		child := clearRelevantStatus(sliceValue.GetChild(len(sliceValue.Before), jx))
   135  		process(child)
   136  	}
   137  }