github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/jsonformat/differ/set.go (about)

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