// Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2023 HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package differ import ( "reflect" "github.com/zclconf/go-cty/cty" "github.com/opentofu/opentofu/internal/command/jsonformat/collections" "github.com/opentofu/opentofu/internal/command/jsonformat/computed" "github.com/opentofu/opentofu/internal/command/jsonformat/computed/renderers" "github.com/opentofu/opentofu/internal/command/jsonformat/structured" "github.com/opentofu/opentofu/internal/command/jsonformat/structured/attribute_path" "github.com/opentofu/opentofu/internal/command/jsonprovider" "github.com/opentofu/opentofu/internal/plans" ) func computeAttributeDiffAsSet(change structured.Change, elementType cty.Type) computed.Diff { var elements []computed.Diff current := change.GetDefaultActionForIteration() processSet(change, func(value structured.Change) { element := ComputeDiffForType(value, elementType) elements = append(elements, element) current = collections.CompareActions(current, element.Action) }) return computed.NewDiff(renderers.Set(elements), current, change.ReplacePaths.Matches()) } func computeAttributeDiffAsNestedSet(change structured.Change, attributes map[string]*jsonprovider.Attribute) computed.Diff { var elements []computed.Diff current := change.GetDefaultActionForIteration() processSet(change, func(value structured.Change) { element := computeDiffForNestedAttribute(value, &jsonprovider.NestedType{ Attributes: attributes, NestingMode: "single", }) elements = append(elements, element) current = collections.CompareActions(current, element.Action) }) return computed.NewDiff(renderers.NestedSet(elements), current, change.ReplacePaths.Matches()) } func computeBlockDiffsAsSet(change structured.Change, block *jsonprovider.Block) ([]computed.Diff, plans.Action) { var elements []computed.Diff current := change.GetDefaultActionForIteration() processSet(change, func(value structured.Change) { element := ComputeDiffForBlock(value, block) elements = append(elements, element) current = collections.CompareActions(current, element.Action) }) return elements, current } func processSet(change structured.Change, process func(value structured.Change)) { sliceValue := change.AsSlice() foundInBefore := make(map[int]int) foundInAfter := make(map[int]int) // O(n^2) operation here to find matching pairs in the set, so we can make // the display look pretty. There might be a better way to do this, so look // here for potential optimisations. for ix := 0; ix < len(sliceValue.Before); ix++ { matched := false for jx := 0; jx < len(sliceValue.After); jx++ { if _, ok := foundInAfter[jx]; ok { // We've already found a match for this after value. continue } child := sliceValue.GetChild(ix, jx) if reflect.DeepEqual(child.Before, child.After) && child.IsBeforeSensitive() == child.IsAfterSensitive() && !child.IsUnknown() { matched = true foundInBefore[ix] = jx foundInAfter[jx] = ix } } if !matched { foundInBefore[ix] = -1 } } clearRelevantStatus := func(change structured.Change) structured.Change { // It's actually really difficult to render the diffs when some indices // within a slice are relevant and others aren't. To make this simpler // we just treat all children of a relevant list or set as also // relevant. // // Interestingly the tofu plan builder also agrees with this, and // never sets relevant attributes beneath lists or sets. We're just // going to enforce this logic here as well. If the collection is // relevant (decided elsewhere), then every element in the collection is // also relevant. To be clear, in practice even if we didn't do the // following explicitly the effect would be the same. It's just nicer // for us to be clear about the behaviour we expect. // // What makes this difficult is the fact that the beforeIx and afterIx // can be different, and it's quite difficult to work out which one is // the relevant one. For nested lists, block lists, and tuples it's much // easier because we always process the same indices in the before and // after. change.RelevantAttributes = attribute_path.AlwaysMatcher() return change } // Now everything in before should be a key in foundInBefore and a value // in foundInAfter. If a key is mapped to -1 in foundInBefore it means it // does not have an equivalent in foundInAfter and so has been deleted. // Everything in foundInAfter has a matching value in foundInBefore, but // some values in after may not be in foundInAfter. This means these values // are newly created. for ix := 0; ix < len(sliceValue.Before); ix++ { if jx := foundInBefore[ix]; jx >= 0 { child := clearRelevantStatus(sliceValue.GetChild(ix, jx)) process(child) continue } child := clearRelevantStatus(sliceValue.GetChild(ix, len(sliceValue.After))) process(child) } for jx := 0; jx < len(sliceValue.After); jx++ { if _, ok := foundInAfter[jx]; ok { // Then this value was handled in the previous for loop. continue } child := clearRelevantStatus(sliceValue.GetChild(len(sliceValue.Before), jx)) process(child) } }