mirror of
https://github.com/hashicorp/terraform.git
synced 2026-02-18 18:29:44 -05:00
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>
This commit is contained in:
parent
8ab5ded5b9
commit
a5aa6cc5b7
3 changed files with 110 additions and 1 deletions
5
.changes/v1.15/BUG FIXES-20260214-120000.yaml
Normal file
5
.changes/v1.15/BUG FIXES-20260214-120000.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
kind: BUG FIXES
|
||||
body: 'states: fixed a bug that caused Terraform to be unable to identify when two states had different output values. This may have caused issues in specific circumstances like backend migrations.'
|
||||
time: 2026-02-14T12:00:00.000000+00:00
|
||||
custom:
|
||||
Issue: "38181"
|
||||
|
|
@ -101,7 +101,7 @@ func (s *State) RootOutputValuesEqual(s2 *State) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
for k, v1 := range s2.RootOutputValues {
|
||||
for k, v1 := range s.RootOutputValues {
|
||||
v2, ok := s2.RootOutputValues[k]
|
||||
if !ok || !v1.Equal(v2) {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -474,6 +474,110 @@ func TestStateHasRootOutputValues(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
func TestStateRootOutputValuesEqual(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
SetupA func(ss *SyncState)
|
||||
SetupB func(ss *SyncState)
|
||||
Want bool
|
||||
}{
|
||||
"both empty": {
|
||||
func(ss *SyncState) {},
|
||||
func(ss *SyncState) {},
|
||||
true,
|
||||
},
|
||||
"identical outputs": {
|
||||
func(ss *SyncState) {
|
||||
ss.SetOutputValue(
|
||||
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
|
||||
cty.StringVal("bar"), false,
|
||||
)
|
||||
},
|
||||
func(ss *SyncState) {
|
||||
ss.SetOutputValue(
|
||||
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
|
||||
cty.StringVal("bar"), false,
|
||||
)
|
||||
},
|
||||
true,
|
||||
},
|
||||
"different values same key": {
|
||||
func(ss *SyncState) {
|
||||
ss.SetOutputValue(
|
||||
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
|
||||
cty.StringVal("bar"), false,
|
||||
)
|
||||
},
|
||||
func(ss *SyncState) {
|
||||
ss.SetOutputValue(
|
||||
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
|
||||
cty.StringVal("baz"), false,
|
||||
)
|
||||
},
|
||||
false,
|
||||
},
|
||||
"different sensitivity same value": {
|
||||
func(ss *SyncState) {
|
||||
ss.SetOutputValue(
|
||||
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
|
||||
cty.StringVal("bar"), false,
|
||||
)
|
||||
},
|
||||
func(ss *SyncState) {
|
||||
ss.SetOutputValue(
|
||||
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
|
||||
cty.StringVal("bar"), true,
|
||||
)
|
||||
},
|
||||
false,
|
||||
},
|
||||
"different keys same count": {
|
||||
func(ss *SyncState) {
|
||||
ss.SetOutputValue(
|
||||
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
|
||||
cty.StringVal("val"), false,
|
||||
)
|
||||
},
|
||||
func(ss *SyncState) {
|
||||
ss.SetOutputValue(
|
||||
addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance),
|
||||
cty.StringVal("val"), false,
|
||||
)
|
||||
},
|
||||
false,
|
||||
},
|
||||
"different count": {
|
||||
func(ss *SyncState) {
|
||||
ss.SetOutputValue(
|
||||
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
|
||||
cty.StringVal("val"), false,
|
||||
)
|
||||
},
|
||||
func(ss *SyncState) {
|
||||
ss.SetOutputValue(
|
||||
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
|
||||
cty.StringVal("val"), false,
|
||||
)
|
||||
ss.SetOutputValue(
|
||||
addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance),
|
||||
cty.StringVal("val2"), false,
|
||||
)
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
stateA := BuildState(test.SetupA)
|
||||
stateB := BuildState(test.SetupB)
|
||||
got := stateA.RootOutputValuesEqual(stateB)
|
||||
if got != test.Want {
|
||||
t.Errorf("wrong result for stateA.RootOutputValuesEqual(stateB)\ngot: %t\nwant: %t", got, test.Want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestState_MoveAbsResource(t *testing.T) {
|
||||
// Set up a starter state for the embedded tests, which should start from a copy of this state.
|
||||
state := NewState()
|
||||
|
|
|
|||
Loading…
Reference in a new issue