terraform/state/testing.go

157 lines
4.0 KiB
Go
Raw Normal View History

2015-02-21 20:52:55 +01:00
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
2015-02-21 21:25:10 +01:00
current := TestStateInitial()
2015-02-21 20:52:55 +01:00
// Check that the initial state is correct
2015-02-24 06:36:35 +01:00
if state := reader.State(); !current.Equal(state) {
t.Fatalf("not initial:\n%#v\n\n%#v", state, current)
2015-02-21 20:52:55 +01:00
}
state: more robust handling of state Serial Previously we relied on a constellation of coincidences for everything to work out correctly with state serials. In particular, callers needed to be very careful about mutating states (or not) because many different bits of code shared pointers to the same objects. Here we move to a model where all of the state managers always use distinct instances of state, copied when WriteState is called. This means that they are truly a snapshot of the state as it was at that call, even if the caller goes on mutating the state that was passed. We also adjust the handling of serials so that the state managers ignore any serials in incoming states and instead just treat each Persist as the next version after what was most recently Refreshed. (An exception exists for when nothing has been refreshed, e.g. because we are writing a state to a location for the first time. In that case we _do_ trust the caller, since the given state is either a new state or it's a copy of something we're migrating from elsewhere with its state and lineage intact.) The intent here is to allow the rest of Terraform to not worry about serials and state identity, and instead just treat the state as a mutable structure. We'll just snapshot it occasionally, when WriteState is called, and deal with serials _only_ at persist time. This is intended as a more robust version of #15423, which was a quick hotfix to an issue that resulted from our previous slopping handling of state serials but arguably makes the problem worse by depending on an additional coincidental behavior of the local backend's apply implementation.
2017-07-05 21:34:30 +02:00
// Now we've proven that the state we're starting with is an initial
// state, we'll complete our work here with that state, since otherwise
// further writes would violate the invariant that we only try to write
// states that share the same lineage as what was initially written.
current = reader.State()
2015-02-21 20:52:55 +01:00
// Write a new state and verify that we have it
if ws, ok := s.(StateWriter); ok {
current.AddModuleState(&terraform.ModuleState{
2015-02-21 20:52:55 +01:00
Path: []string{"root"},
Outputs: map[string]*terraform.OutputState{
"bar": &terraform.OutputState{
Type: "string",
Sensitive: false,
Value: "baz",
},
2015-02-21 20:52:55 +01:00
},
})
if err := ws.WriteState(current); err != nil {
t.Fatalf("err: %s", err)
}
2015-02-24 06:36:35 +01:00
if actual := reader.State(); !actual.Equal(current) {
t.Fatalf("bad:\n%#v\n\n%#v", actual, current)
2015-02-21 20:52:55 +01:00
}
}
// 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)
}
}
2015-02-21 21:25:10 +01:00
// Just set the serials the same... Then compare.
actual := reader.State()
2015-02-24 06:36:35 +01:00
if !actual.Equal(current) {
2015-02-21 21:25:10 +01:00
t.Fatalf("bad: %#v\n\n%#v", actual, current)
2015-02-21 20:52:55 +01:00
}
}
2015-02-24 06:26:33 +01:00
// If we can write and persist then verify that the serial
state: more robust handling of state Serial Previously we relied on a constellation of coincidences for everything to work out correctly with state serials. In particular, callers needed to be very careful about mutating states (or not) because many different bits of code shared pointers to the same objects. Here we move to a model where all of the state managers always use distinct instances of state, copied when WriteState is called. This means that they are truly a snapshot of the state as it was at that call, even if the caller goes on mutating the state that was passed. We also adjust the handling of serials so that the state managers ignore any serials in incoming states and instead just treat each Persist as the next version after what was most recently Refreshed. (An exception exists for when nothing has been refreshed, e.g. because we are writing a state to a location for the first time. In that case we _do_ trust the caller, since the given state is either a new state or it's a copy of something we're migrating from elsewhere with its state and lineage intact.) The intent here is to allow the rest of Terraform to not worry about serials and state identity, and instead just treat the state as a mutable structure. We'll just snapshot it occasionally, when WriteState is called, and deal with serials _only_ at persist time. This is intended as a more robust version of #15423, which was a quick hotfix to an issue that resulted from our previous slopping handling of state serials but arguably makes the problem worse by depending on an additional coincidental behavior of the local backend's apply implementation.
2017-07-05 21:34:30 +02:00
// is only incremented on change.
2015-02-24 06:26:33 +01:00
writer, writeOk := s.(StateWriter)
persister, persistOk := s.(StatePersister)
if writeOk && persistOk {
// Same serial
state: more robust handling of state Serial Previously we relied on a constellation of coincidences for everything to work out correctly with state serials. In particular, callers needed to be very careful about mutating states (or not) because many different bits of code shared pointers to the same objects. Here we move to a model where all of the state managers always use distinct instances of state, copied when WriteState is called. This means that they are truly a snapshot of the state as it was at that call, even if the caller goes on mutating the state that was passed. We also adjust the handling of serials so that the state managers ignore any serials in incoming states and instead just treat each Persist as the next version after what was most recently Refreshed. (An exception exists for when nothing has been refreshed, e.g. because we are writing a state to a location for the first time. In that case we _do_ trust the caller, since the given state is either a new state or it's a copy of something we're migrating from elsewhere with its state and lineage intact.) The intent here is to allow the rest of Terraform to not worry about serials and state identity, and instead just treat the state as a mutable structure. We'll just snapshot it occasionally, when WriteState is called, and deal with serials _only_ at persist time. This is intended as a more robust version of #15423, which was a quick hotfix to an issue that resulted from our previous slopping handling of state serials but arguably makes the problem worse by depending on an additional coincidental behavior of the local backend's apply implementation.
2017-07-05 21:34:30 +02:00
serial := reader.State().Serial
2015-02-24 06:26:33 +01:00
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 {
state: more robust handling of state Serial Previously we relied on a constellation of coincidences for everything to work out correctly with state serials. In particular, callers needed to be very careful about mutating states (or not) because many different bits of code shared pointers to the same objects. Here we move to a model where all of the state managers always use distinct instances of state, copied when WriteState is called. This means that they are truly a snapshot of the state as it was at that call, even if the caller goes on mutating the state that was passed. We also adjust the handling of serials so that the state managers ignore any serials in incoming states and instead just treat each Persist as the next version after what was most recently Refreshed. (An exception exists for when nothing has been refreshed, e.g. because we are writing a state to a location for the first time. In that case we _do_ trust the caller, since the given state is either a new state or it's a copy of something we're migrating from elsewhere with its state and lineage intact.) The intent here is to allow the rest of Terraform to not worry about serials and state identity, and instead just treat the state as a mutable structure. We'll just snapshot it occasionally, when WriteState is called, and deal with serials _only_ at persist time. This is intended as a more robust version of #15423, which was a quick hotfix to an issue that resulted from our previous slopping handling of state serials but arguably makes the problem worse by depending on an additional coincidental behavior of the local backend's apply implementation.
2017-07-05 21:34:30 +02:00
t.Fatalf("serial changed after persisting with no changes: got %d, want %d", reader.State().Serial, serial)
2015-02-24 06:26:33 +01:00
}
// Change the serial
current = current.DeepCopy()
2015-02-24 06:26:33 +01:00
current.Modules = []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root", "somewhere"},
Outputs: map[string]*terraform.OutputState{
"serialCheck": &terraform.OutputState{
Type: "string",
Sensitive: false,
Value: "true",
},
},
2015-02-24 06:26:33 +01:00
},
}
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 {
state: more robust handling of state Serial Previously we relied on a constellation of coincidences for everything to work out correctly with state serials. In particular, callers needed to be very careful about mutating states (or not) because many different bits of code shared pointers to the same objects. Here we move to a model where all of the state managers always use distinct instances of state, copied when WriteState is called. This means that they are truly a snapshot of the state as it was at that call, even if the caller goes on mutating the state that was passed. We also adjust the handling of serials so that the state managers ignore any serials in incoming states and instead just treat each Persist as the next version after what was most recently Refreshed. (An exception exists for when nothing has been refreshed, e.g. because we are writing a state to a location for the first time. In that case we _do_ trust the caller, since the given state is either a new state or it's a copy of something we're migrating from elsewhere with its state and lineage intact.) The intent here is to allow the rest of Terraform to not worry about serials and state identity, and instead just treat the state as a mutable structure. We'll just snapshot it occasionally, when WriteState is called, and deal with serials _only_ at persist time. This is intended as a more robust version of #15423, which was a quick hotfix to an issue that resulted from our previous slopping handling of state serials but arguably makes the problem worse by depending on an additional coincidental behavior of the local backend's apply implementation.
2017-07-05 21:34:30 +02:00
t.Fatalf("serial incorrect after persisting with changes: got %d, want > %d", reader.State().Serial, serial)
2015-02-24 06:26:33 +01:00
}
2015-02-24 06:36:35 +01:00
// 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) {
2015-02-24 06:36:35 +01:00
t.Fatal("State() should return a copy")
}
2015-02-24 06:26:33 +01:00
}
2015-02-21 20:52:55 +01:00
}
2015-02-21 21:25:10 +01:00
// 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",
},
2015-02-21 21:25:10 +01:00
},
},
},
}
initial.Init()
2015-02-21 21:25:10 +01:00
return initial
}