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 }