mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-02 12:17:39 -06:00
cdb80f68a8
Fix checksum issue with remote state If we read a state file with "null" objects in a module and they become initialized to an empty map the state file may be written out with empty objects rather than "null", changing the checksum. If we can detect this, increment the serial number to prevent a conflict in atlas. Our fakeAtlas test server now needs to decode the state directly rather than using the ReadState function, so as to be able to read the state unaltered. The terraform.State data structures have initialization spread out throughout the package. More thoroughly initialize State during ReadState, and add a call to init() during WriteState as another normalization safeguard. Expose State.init through an exported Init() method, so that a new State can be completely realized outside of the terraform package. Additionally, the internal init now completely walks all internal state structures ensuring that all maps and slices are initialized. While it was mentioned before that the `init()` methods are problematic with too many call sites, expanding this out better exposes the entry points that will need to be refactored later for improved concurrency handling. The State structures had a mix of `omitempty` fields. Remove omitempty for all maps and slices as part of this normalization process. Make Lineage mandatory, which is now explicitly set in some tests.
152 lines
3.6 KiB
Go
152 lines
3.6 KiB
Go
package state
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/terraform/terraform"
|
|
)
|
|
|
|
// TestState is a helper for testing state implementations. It is expected
|
|
// that the given implementation is pre-loaded with the TestStateInitial
|
|
// state.
|
|
func TestState(t *testing.T, s interface{}) {
|
|
reader, ok := s.(StateReader)
|
|
if !ok {
|
|
t.Fatalf("must at least be a StateReader")
|
|
}
|
|
|
|
// If it implements refresh, refresh
|
|
if rs, ok := s.(StateRefresher); ok {
|
|
if err := rs.RefreshState(); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
}
|
|
|
|
// current will track our current state
|
|
current := TestStateInitial()
|
|
|
|
// Check that the initial state is correct
|
|
if state := reader.State(); !current.Equal(state) {
|
|
t.Fatalf("not initial:\n%#v\n\n%#v", state, current)
|
|
}
|
|
|
|
// Write a new state and verify that we have it
|
|
if ws, ok := s.(StateWriter); ok {
|
|
current.AddModuleState(&terraform.ModuleState{
|
|
Path: []string{"root"},
|
|
Outputs: map[string]*terraform.OutputState{
|
|
"bar": &terraform.OutputState{
|
|
Type: "string",
|
|
Sensitive: false,
|
|
Value: "baz",
|
|
},
|
|
},
|
|
})
|
|
|
|
if err := ws.WriteState(current); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
if actual := reader.State(); !actual.Equal(current) {
|
|
t.Fatalf("bad:\n%#v\n\n%#v", actual, current)
|
|
}
|
|
}
|
|
|
|
// Test persistence
|
|
if ps, ok := s.(StatePersister); ok {
|
|
if err := ps.PersistState(); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
// Refresh if we got it
|
|
if rs, ok := s.(StateRefresher); ok {
|
|
if err := rs.RefreshState(); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
}
|
|
|
|
// Just set the serials the same... Then compare.
|
|
actual := reader.State()
|
|
if !actual.Equal(current) {
|
|
t.Fatalf("bad: %#v\n\n%#v", actual, current)
|
|
}
|
|
}
|
|
|
|
// If we can write and persist then verify that the serial
|
|
// is only implemented on change.
|
|
writer, writeOk := s.(StateWriter)
|
|
persister, persistOk := s.(StatePersister)
|
|
if writeOk && persistOk {
|
|
// Same serial
|
|
serial := current.Serial
|
|
if err := writer.WriteState(current); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
if err := persister.PersistState(); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
if reader.State().Serial != serial {
|
|
t.Fatalf("bad: expected %d, got %d", serial, reader.State().Serial)
|
|
}
|
|
|
|
// Change the serial
|
|
currentCopy := *current
|
|
current = ¤tCopy
|
|
current.Modules = []*terraform.ModuleState{
|
|
&terraform.ModuleState{
|
|
Path: []string{"root", "somewhere"},
|
|
Outputs: map[string]*terraform.OutputState{
|
|
"serialCheck": &terraform.OutputState{
|
|
Type: "string",
|
|
Sensitive: false,
|
|
Value: "true",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if err := writer.WriteState(current); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
if err := persister.PersistState(); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
if reader.State().Serial <= serial {
|
|
t.Fatalf("bad: expected %d, got %d", serial, reader.State().Serial)
|
|
}
|
|
|
|
// Check that State() returns a copy by modifying the copy and comparing
|
|
// to the current state.
|
|
stateCopy := reader.State()
|
|
stateCopy.Serial++
|
|
if reflect.DeepEqual(stateCopy, current) {
|
|
t.Fatal("State() should return a copy")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestStateInitial is the initial state that a State should have
|
|
// for TestState.
|
|
func TestStateInitial() *terraform.State {
|
|
initial := &terraform.State{
|
|
Modules: []*terraform.ModuleState{
|
|
&terraform.ModuleState{
|
|
Path: []string{"root", "child"},
|
|
Outputs: map[string]*terraform.OutputState{
|
|
"foo": &terraform.OutputState{
|
|
Type: "string",
|
|
Sensitive: false,
|
|
Value: "bar",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
initial.Init()
|
|
|
|
return initial
|
|
}
|