diff --git a/.changes/v1.15/BUG FIXES-20260214-120000.yaml b/.changes/v1.15/BUG FIXES-20260214-120000.yaml new file mode 100644 index 0000000000..f4f4a12a54 --- /dev/null +++ b/.changes/v1.15/BUG FIXES-20260214-120000.yaml @@ -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" diff --git a/internal/states/state_equal.go b/internal/states/state_equal.go index 97414311c0..6ed37ff257 100644 --- a/internal/states/state_equal.go +++ b/internal/states/state_equal.go @@ -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 diff --git a/internal/states/state_test.go b/internal/states/state_test.go index dae25f6cd5..458a29f874 100644 --- a/internal/states/state_test.go +++ b/internal/states/state_test.go @@ -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()