terraform/internal/states/state_equal.go
Varun Chawla a5aa6cc5b7
states: fix RootOutputValuesEqual comparing s2 to itself (#38181)
* states: fix RootOutputValuesEqual comparing s2 to itself

RootOutputValuesEqual had a copy-paste bug where it iterated over
s2.RootOutputValues instead of s.RootOutputValues, effectively
comparing s2 against itself rather than comparing the receiver (s)
against the argument (s2). This meant the function would always
return true as long as both states had the same number of output
values, regardless of whether the actual values differed.

This bug was introduced in #37886 and affects refresh-only plan mode,
where RootOutputValuesEqual is used to determine if root output values
changed during refresh, which controls whether the plan is considered
applyable.

* add changelog entry for RootOutputValuesEqual fix

* Update changelog wording per reviewer suggestion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:15:37 +00:00

112 lines
2.7 KiB
Go

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package states
// Equal returns true if the receiver is functionally equivalent to other,
// including any ephemeral portions of the state that would not be included
// if the state were saved to files.
//
// To test only the persistent portions of two states for equality, instead
// use statefile.StatesMarshalEqual.
func (s *State) Equal(other *State) bool {
if s == other {
return true
}
if s == nil || other == nil {
return false
}
if !s.RootOutputValuesEqual(other) {
return false
}
if !s.CheckResults.Equal(other.CheckResults) {
return false
}
return s.ManagedResourcesEqual(other)
}
// ManagedResourcesEqual returns true if all of the managed resources tracked
// in the receiver are functionally equivalent to the same tracked in the
// other given state.
//
// This is a more constrained version of Equal that disregards other
// differences, including but not limited to changes to data resources and
// changes to output values.
func (s *State) ManagedResourcesEqual(other *State) bool {
// First, some accommodations for situations where one of the objects is
// nil, for robustness since we sometimes use a nil state to represent
// a prior state being entirely absent.
if s == other {
// covers both states being nil, or both states being the exact same
// object.
return true
}
// Managed resources are technically equal if one state is nil while the
// other has no resources.
if s == nil {
return !other.HasManagedResourceInstanceObjects()
}
if other == nil {
return !s.HasManagedResourceInstanceObjects()
}
// If we get here then both states are non-nil.
if len(s.Modules) != len(other.Modules) {
return false
}
for key, sMod := range s.Modules {
otherMod, ok := other.Modules[key]
if !ok {
return false
}
// Something else is wrong if the addresses don't match, but they are
// definitely not equal
if !sMod.Addr.Equal(otherMod.Addr) {
return false
}
if len(sMod.Resources) != len(otherMod.Resources) {
return false
}
for key, sRes := range sMod.Resources {
otherRes, ok := otherMod.Resources[key]
if !ok {
return false
}
if !sRes.Equal(otherRes) {
return false
}
}
}
return true
}
// RootOutputValuesEqual returns true if the root output values tracked in the
// receiver are functionally equivalent to the same tracked in the other given
// state.
func (s *State) RootOutputValuesEqual(s2 *State) bool {
if s == nil && s2 == nil {
return true
}
if len(s.RootOutputValues) != len(s2.RootOutputValues) {
return false
}
for k, v1 := range s.RootOutputValues {
v2, ok := s2.RootOutputValues[k]
if !ok || !v1.Equal(v2) {
return false
}
}
return true
}