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 }