Smarter approach to .Equals on state objects for unordered lists (#3024)

Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
Christian Mesh
2025-07-14 09:26:24 -04:00
parent a1da9c1dcd
commit 7cbca7a9b4
3 changed files with 155 additions and 1 deletions

View File

@@ -6,6 +6,9 @@
package states
import (
"bytes"
"reflect"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
@@ -66,6 +69,96 @@ type ResourceInstanceObjectSrc struct {
CreateBeforeDestroy bool
}
// Compare two lists using an given element equal function, ignoring order and duplicates
func equalSlicesIgnoreOrder[S ~[]E, E any](a, b S, fn func(E, E) bool) bool {
if len(a) != len(b) {
return false
}
// Not sure if this is the most efficient approach, but it works
// First check if all elements in a existing in b
for _, v := range a {
found := false
for _, o := range b {
if fn(v, o) {
found = true
break
}
}
if !found {
return false
}
}
// Now check if all elements in b exist in a
// This is necessary just in case there are duplicate entries (there should not be).
for _, v := range b {
found := false
for _, o := range a {
if fn(v, o) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
func (os *ResourceInstanceObjectSrc) Equal(other *ResourceInstanceObjectSrc) bool {
if os == other {
return true
}
if os == nil || other == nil {
return false
}
if os.SchemaVersion != other.SchemaVersion {
return false
}
if !bytes.Equal(os.AttrsJSON, other.AttrsJSON) {
return false
}
if !reflect.DeepEqual(os.AttrsFlat, other.AttrsFlat) {
return false
}
// Ignore order/duplicates as that is the assumption in the rest of the codebase.
// Given that these are generated from maps, it is known that the order is not consistent.
if !equalSlicesIgnoreOrder(os.AttrSensitivePaths, other.AttrSensitivePaths, cty.PathValueMarks.Equal) {
return false
}
// Ignore order/duplicates as that is the assumption in the rest of the codebase.
// Given that these are generated from maps, it is known that the order is not consistent.
if !equalSlicesIgnoreOrder(os.TransientPathValueMarks, other.TransientPathValueMarks, cty.PathValueMarks.Equal) {
return false
}
if !bytes.Equal(os.Private, other.Private) {
return false
}
if os.Status != other.Status {
return false
}
// This represents a set of dependencies. They must all be resolved before executing and therefore the order does not matter.
if !equalSlicesIgnoreOrder(os.Dependencies, other.Dependencies, addrs.ConfigResource.Equal) {
return false
}
if os.CreateBeforeDestroy != other.CreateBeforeDestroy {
return false
}
return true
}
// Decode unmarshals the raw representation of the object attributes. Pass the
// implied type of the corresponding resource type schema for correct operation.
//

View File

@@ -59,6 +59,33 @@ func (rs *Resource) EnsureInstance(key addrs.InstanceKey) *ResourceInstance {
return ret
}
func (rs *Resource) Equal(other *Resource) bool {
if rs == other {
// Handles both pointers being nil
return true
}
if rs == nil || other == nil {
// Handles one pointer being nil
return false
}
if !rs.Addr.Equal(other.Addr) || rs.ProviderConfig.String() != other.ProviderConfig.String() {
return false
}
if len(rs.Instances) != len(other.Instances) {
return false
}
for key, inst := range rs.Instances {
if !inst.Equal(other.Instances[key]) {
return false
}
}
return true
}
// ResourceInstance represents the state of a particular instance of a resource.
type ResourceInstance struct {
// Current, if non-nil, is the remote object that is currently represented
@@ -100,6 +127,40 @@ func (i *ResourceInstance) HasAnyDeposed() bool {
return i != nil && len(i.Deposed) > 0
}
func (i *ResourceInstance) Equal(other *ResourceInstance) bool {
if i == other {
// Handles both pointers being nil
return true
}
if i == nil || other == nil {
// Handles one pointer being nil
return false
}
if !i.Current.Equal(other.Current) {
return false
}
if (i.ProviderKey == nil) != (other.ProviderKey == nil) {
return false
}
if i.ProviderKey != nil && i.ProviderKey.Value().Equals(other.ProviderKey.Value()).False() {
return false
}
if len(i.Deposed) != len(other.Deposed) {
return false
}
for key, dep := range i.Deposed {
if !dep.Equal(other.Deposed[key]) {
return false
}
}
return true
}
// HasObjects returns true if this resource has any objects at all, whether
// current or deposed.
func (i *ResourceInstance) HasObjects() bool {

View File

@@ -62,7 +62,7 @@ func sameManagedResources(s1, s2 *State) bool {
continue
}
otherRS := s2.Resource(addr)
if !reflect.DeepEqual(rs, otherRS) {
if !rs.Equal(otherRS) {
return false
}
}