Ensure better state normalization
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.
This commit is contained in:
parent
97fcd5f4cc
commit
cdb80f68a8
|
@ -1181,6 +1181,7 @@ func TestApply_backup(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
originalState.Init()
|
||||
|
||||
statePath := testStateFile(t, originalState)
|
||||
backupPath := testTempFile(t)
|
||||
|
|
|
@ -131,7 +131,7 @@ func testReadPlan(t *testing.T, path string) *terraform.Plan {
|
|||
|
||||
// testState returns a test State structure that we use for a lot of tests.
|
||||
func testState() *terraform.State {
|
||||
return &terraform.State{
|
||||
state := &terraform.State{
|
||||
Version: 2,
|
||||
Modules: []*terraform.ModuleState{
|
||||
&terraform.ModuleState{
|
||||
|
@ -148,6 +148,8 @@ func testState() *terraform.State {
|
|||
},
|
||||
},
|
||||
}
|
||||
state.Init()
|
||||
return state
|
||||
}
|
||||
|
||||
func testStateFile(t *testing.T, s *terraform.State) string {
|
||||
|
|
|
@ -173,7 +173,7 @@ func TestRefresh_defaultState(t *testing.T) {
|
|||
}
|
||||
|
||||
p.RefreshFn = nil
|
||||
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
|
||||
p.RefreshReturn = newInstanceState("yes")
|
||||
|
||||
args := []string{
|
||||
testFixturePath("refresh"),
|
||||
|
@ -200,7 +200,8 @@ func TestRefresh_defaultState(t *testing.T) {
|
|||
actual := newState.RootModule().Resources["test_instance.foo"].Primary
|
||||
expected := p.RefreshReturn
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Fatalf("bad: %#v", actual)
|
||||
t.Logf("expected:\n%#v", expected)
|
||||
t.Fatalf("bad:\n%#v", actual)
|
||||
}
|
||||
|
||||
f, err = os.Open(statePath + DefaultBackupExtension)
|
||||
|
@ -347,7 +348,7 @@ func TestRefresh_outPath(t *testing.T) {
|
|||
}
|
||||
|
||||
p.RefreshFn = nil
|
||||
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
|
||||
p.RefreshReturn = newInstanceState("yes")
|
||||
|
||||
args := []string{
|
||||
"-state", statePath,
|
||||
|
@ -577,7 +578,7 @@ func TestRefresh_backup(t *testing.T) {
|
|||
}
|
||||
|
||||
p.RefreshFn = nil
|
||||
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
|
||||
p.RefreshReturn = newInstanceState("yes")
|
||||
|
||||
args := []string{
|
||||
"-state", statePath,
|
||||
|
@ -662,7 +663,7 @@ func TestRefresh_disableBackup(t *testing.T) {
|
|||
}
|
||||
|
||||
p.RefreshFn = nil
|
||||
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
|
||||
p.RefreshReturn = newInstanceState("yes")
|
||||
|
||||
args := []string{
|
||||
"-state", statePath,
|
||||
|
@ -742,6 +743,20 @@ func TestRefresh_displaysOutputs(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// When creating an InstaneState for direct comparison to one contained in
|
||||
// terraform.State, all fields must be inintialized (duplicating the
|
||||
// InstanceState.init() method)
|
||||
func newInstanceState(id string) *terraform.InstanceState {
|
||||
return &terraform.InstanceState{
|
||||
ID: id,
|
||||
Attributes: make(map[string]string),
|
||||
Ephemeral: terraform.EphemeralState{
|
||||
ConnInfo: make(map[string]string),
|
||||
},
|
||||
Meta: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
const refreshVarFile = `
|
||||
foo = "bar"
|
||||
`
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"crypto/md5"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
|
@ -161,6 +162,9 @@ func TestAtlasClient_LegitimateConflict(t *testing.T) {
|
|||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
terraform.WriteState(state, &buf)
|
||||
|
||||
// Changing the state but not the serial. Should generate a conflict.
|
||||
state.RootModule().Outputs["drift"] = &terraform.OutputState{
|
||||
Type: "string",
|
||||
|
@ -244,7 +248,10 @@ func (f *fakeAtlas) Server() *httptest.Server {
|
|||
}
|
||||
|
||||
func (f *fakeAtlas) CurrentState() *terraform.State {
|
||||
currentState, err := terraform.ReadState(bytes.NewReader(f.state))
|
||||
// we read the state manually here, because terraform may alter state
|
||||
// during read
|
||||
currentState := &terraform.State{}
|
||||
err := json.Unmarshal(f.state, currentState)
|
||||
if err != nil {
|
||||
f.t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
@ -288,10 +295,15 @@ func (f *fakeAtlas) handler(resp http.ResponseWriter, req *http.Request) {
|
|||
var buf bytes.Buffer
|
||||
buf.ReadFrom(req.Body)
|
||||
sum := md5.Sum(buf.Bytes())
|
||||
state, err := terraform.ReadState(&buf)
|
||||
|
||||
// we read the state manually here, because terraform may alter state
|
||||
// during read
|
||||
state := &terraform.State{}
|
||||
err := json.Unmarshal(buf.Bytes(), state)
|
||||
if err != nil {
|
||||
f.t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
conflict := f.CurrentSerial() == state.Serial && f.CurrentSum() != sum
|
||||
conflict = conflict || f.alwaysConflict
|
||||
if conflict {
|
||||
|
@ -351,7 +363,8 @@ var testStateModuleOrderChange = []byte(
|
|||
var testStateSimple = []byte(
|
||||
`{
|
||||
"version": 3,
|
||||
"serial": 1,
|
||||
"serial": 2,
|
||||
"lineage": "c00ad9ac-9b35-42fe-846e-b06f0ef877e9",
|
||||
"modules": [
|
||||
{
|
||||
"path": [
|
||||
|
@ -364,7 +377,8 @@ var testStateSimple = []byte(
|
|||
"value": "bar"
|
||||
}
|
||||
},
|
||||
"resources": null
|
||||
"resources": {},
|
||||
"depends_on": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
|
@ -29,12 +28,12 @@ func TestState(t *testing.T, s interface{}) {
|
|||
|
||||
// Check that the initial state is correct
|
||||
if state := reader.State(); !current.Equal(state) {
|
||||
t.Fatalf("not initial: %#v\n\n%#v", state, current)
|
||||
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.Modules = append(current.Modules, &terraform.ModuleState{
|
||||
current.AddModuleState(&terraform.ModuleState{
|
||||
Path: []string{"root"},
|
||||
Outputs: map[string]*terraform.OutputState{
|
||||
"bar": &terraform.OutputState{
|
||||
|
@ -50,7 +49,7 @@ func TestState(t *testing.T, s interface{}) {
|
|||
}
|
||||
|
||||
if actual := reader.State(); !actual.Equal(current) {
|
||||
t.Fatalf("bad: %#v\n\n%#v", actual, current)
|
||||
t.Fatalf("bad:\n%#v\n\n%#v", actual, current)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,7 +145,7 @@ func TestStateInitial() *terraform.State {
|
|||
},
|
||||
}
|
||||
|
||||
var scratch bytes.Buffer
|
||||
terraform.WriteState(initial, &scratch)
|
||||
initial.Init()
|
||||
|
||||
return initial
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ type State struct {
|
|||
// Apart from the guarantee that collisions between two lineages
|
||||
// are very unlikely, this value is opaque and external callers
|
||||
// should only compare lineage strings byte-for-byte for equality.
|
||||
Lineage string `json:"lineage,omitempty"`
|
||||
Lineage string `json:"lineage"`
|
||||
|
||||
// Remote is used to track the metadata required to
|
||||
// pull and push state files from a remote storage endpoint.
|
||||
|
@ -113,7 +113,13 @@ func (s *State) Children(path []string) []*ModuleState {
|
|||
// This should be the preferred method to add module states since it
|
||||
// allows us to optimize lookups later as well as control sorting.
|
||||
func (s *State) AddModule(path []string) *ModuleState {
|
||||
m := &ModuleState{Path: path}
|
||||
// check if the module exists first
|
||||
m := s.ModuleByPath(path)
|
||||
if m != nil {
|
||||
return m
|
||||
}
|
||||
|
||||
m = &ModuleState{Path: path}
|
||||
m.init()
|
||||
s.Modules = append(s.Modules, m)
|
||||
s.sort()
|
||||
|
@ -498,6 +504,10 @@ func (s *State) FromFutureTerraform() bool {
|
|||
return SemVersion.LessThan(v)
|
||||
}
|
||||
|
||||
func (s *State) Init() {
|
||||
s.init()
|
||||
}
|
||||
|
||||
func (s *State) init() {
|
||||
if s.Version == 0 {
|
||||
s.Version = StateVersion
|
||||
|
@ -506,6 +516,14 @@ func (s *State) init() {
|
|||
s.AddModule(rootModulePath)
|
||||
}
|
||||
s.EnsureHasLineage()
|
||||
|
||||
for _, mod := range s.Modules {
|
||||
mod.init()
|
||||
}
|
||||
|
||||
if s.Remote != nil {
|
||||
s.Remote.init()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) EnsureHasLineage() {
|
||||
|
@ -517,6 +535,21 @@ func (s *State) EnsureHasLineage() {
|
|||
}
|
||||
}
|
||||
|
||||
// AddModuleState insert this module state and override any existing ModuleState
|
||||
func (s *State) AddModuleState(mod *ModuleState) {
|
||||
mod.init()
|
||||
|
||||
for i, m := range s.Modules {
|
||||
if reflect.DeepEqual(m.Path, mod.Path) {
|
||||
s.Modules[i] = mod
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s.Modules = append(s.Modules, mod)
|
||||
s.sort()
|
||||
}
|
||||
|
||||
// prune is used to remove any resources that are no longer required
|
||||
func (s *State) prune() {
|
||||
if s == nil {
|
||||
|
@ -586,6 +619,12 @@ type RemoteState struct {
|
|||
Config map[string]string `json:"config"`
|
||||
}
|
||||
|
||||
func (r *RemoteState) init() {
|
||||
if r.Config == nil {
|
||||
r.Config = make(map[string]string)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RemoteState) deepcopy() *RemoteState {
|
||||
confCopy := make(map[string]string, len(r.Config))
|
||||
for k, v := range r.Config {
|
||||
|
@ -713,7 +752,7 @@ type ModuleState struct {
|
|||
// Terraform. If Terraform doesn't find a matching ID in the
|
||||
// overall state, then it assumes it isn't managed and doesn't
|
||||
// worry about it.
|
||||
Dependencies []string `json:"depends_on,omitempty"`
|
||||
Dependencies []string `json:"depends_on"`
|
||||
}
|
||||
|
||||
// Equal tests whether one module state is equal to another.
|
||||
|
@ -817,12 +856,23 @@ func (m *ModuleState) View(id string) *ModuleState {
|
|||
}
|
||||
|
||||
func (m *ModuleState) init() {
|
||||
if m.Path == nil {
|
||||
m.Path = []string{}
|
||||
}
|
||||
if m.Outputs == nil {
|
||||
m.Outputs = make(map[string]*OutputState)
|
||||
}
|
||||
if m.Resources == nil {
|
||||
m.Resources = make(map[string]*ResourceState)
|
||||
}
|
||||
|
||||
if m.Dependencies == nil {
|
||||
m.Dependencies = make([]string, 0)
|
||||
}
|
||||
|
||||
for _, rs := range m.Resources {
|
||||
rs.init()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ModuleState) deepcopy() *ModuleState {
|
||||
|
@ -1095,7 +1145,7 @@ type ResourceState struct {
|
|||
// Terraform. If Terraform doesn't find a matching ID in the
|
||||
// overall state, then it assumes it isn't managed and doesn't
|
||||
// worry about it.
|
||||
Dependencies []string `json:"depends_on,omitempty"`
|
||||
Dependencies []string `json:"depends_on"`
|
||||
|
||||
// Primary is the current active instance for this resource.
|
||||
// It can be replaced but only after a successful creation.
|
||||
|
@ -1113,13 +1163,13 @@ type ResourceState struct {
|
|||
//
|
||||
// An instance will remain in the Deposed list until it is successfully
|
||||
// destroyed and purged.
|
||||
Deposed []*InstanceState `json:"deposed,omitempty"`
|
||||
Deposed []*InstanceState `json:"deposed"`
|
||||
|
||||
// Provider is used when a resource is connected to a provider with an alias.
|
||||
// If this string is empty, the resource is connected to the default provider,
|
||||
// e.g. "aws_instance" goes with the "aws" provider.
|
||||
// If the resource block contained a "provider" key, that value will be set here.
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
|
||||
// Equal tests whether two ResourceStates are equal.
|
||||
|
@ -1171,6 +1221,18 @@ func (r *ResourceState) init() {
|
|||
r.Primary = &InstanceState{}
|
||||
}
|
||||
r.Primary.init()
|
||||
|
||||
if r.Dependencies == nil {
|
||||
r.Dependencies = []string{}
|
||||
}
|
||||
|
||||
if r.Deposed == nil {
|
||||
r.Deposed = make([]*InstanceState, 0)
|
||||
}
|
||||
|
||||
for _, dep := range r.Deposed {
|
||||
dep.init()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ResourceState) deepcopy() *ResourceState {
|
||||
|
@ -1222,7 +1284,7 @@ type InstanceState struct {
|
|||
// Attributes are basic information about the resource. Any keys here
|
||||
// are accessible in variable format within Terraform configurations:
|
||||
// ${resourcetype.name.attribute}.
|
||||
Attributes map[string]string `json:"attributes,omitempty"`
|
||||
Attributes map[string]string `json:"attributes"`
|
||||
|
||||
// Ephemeral is used to store any state associated with this instance
|
||||
// that is necessary for the Terraform run to complete, but is not
|
||||
|
@ -1232,10 +1294,10 @@ type InstanceState struct {
|
|||
// Meta is a simple K/V map that is persisted to the State but otherwise
|
||||
// ignored by Terraform core. It's meant to be used for accounting by
|
||||
// external client code.
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
Meta map[string]string `json:"meta"`
|
||||
|
||||
// Tainted is used to mark a resource for recreation.
|
||||
Tainted bool `json:"tainted,omitempty"`
|
||||
Tainted bool `json:"tainted"`
|
||||
}
|
||||
|
||||
func (i *InstanceState) init() {
|
||||
|
@ -1498,6 +1560,7 @@ func ReadState(src io.Reader) (*State, error) {
|
|||
return nil, fmt.Errorf("Terraform %s does not support state version %d, please update.",
|
||||
SemVersion.String(), versionIdentifier.Version)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func ReadStateV1(jsonBytes []byte) (*stateV1, error) {
|
||||
|
@ -1543,6 +1606,9 @@ func ReadStateV2(jsonBytes []byte) (*State, error) {
|
|||
// Sort it
|
||||
state.sort()
|
||||
|
||||
// catch any unitialized fields in the state
|
||||
state.init()
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
|
@ -1575,6 +1641,23 @@ func ReadStateV3(jsonBytes []byte) (*State, error) {
|
|||
// Sort it
|
||||
state.sort()
|
||||
|
||||
// catch any unitialized fields in the state
|
||||
state.init()
|
||||
|
||||
// Now we write the state back out to detect any changes in normaliztion.
|
||||
// If our state is now written out differently, bump the serial number to
|
||||
// prevent conflicts.
|
||||
var buf bytes.Buffer
|
||||
err := WriteState(state, &buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !bytes.Equal(jsonBytes, buf.Bytes()) {
|
||||
log.Println("[INFO] state modified during read or write. incrementing serial number")
|
||||
state.Serial++
|
||||
}
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
|
@ -1583,6 +1666,9 @@ func WriteState(d *State, dst io.Writer) error {
|
|||
// Make sure it is sorted
|
||||
d.sort()
|
||||
|
||||
// make sure we have no uninitialized fields
|
||||
d.init()
|
||||
|
||||
// Ensure the version is set
|
||||
d.Version = StateVersion
|
||||
|
||||
|
|
|
@ -90,6 +90,7 @@ func TestStateOutputTypeRoundTrip(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
state.init()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := WriteState(state, buf); err != nil {
|
||||
|
@ -102,7 +103,8 @@ func TestStateOutputTypeRoundTrip(t *testing.T) {
|
|||
}
|
||||
|
||||
if !reflect.DeepEqual(state, roundTripped) {
|
||||
t.Fatalf("bad: %#v", roundTripped)
|
||||
t.Logf("expected:\n%#v", state)
|
||||
t.Fatalf("got:\n%#v", roundTripped)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -121,6 +123,8 @@ func TestStateModuleOrphans(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
state.init()
|
||||
|
||||
config := testModule(t, "state-module-orphans").Config()
|
||||
actual := state.ModuleOrphans(RootModulePath, config)
|
||||
expected := [][]string{
|
||||
|
@ -144,6 +148,8 @@ func TestStateModuleOrphans_nested(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
state.init()
|
||||
|
||||
actual := state.ModuleOrphans(RootModulePath, nil)
|
||||
expected := [][]string{
|
||||
[]string{RootModuleName, "foo"},
|
||||
|
@ -169,6 +175,8 @@ func TestStateModuleOrphans_nilConfig(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
state.init()
|
||||
|
||||
actual := state.ModuleOrphans(RootModulePath, nil)
|
||||
expected := [][]string{
|
||||
[]string{RootModuleName, "foo"},
|
||||
|
@ -195,6 +203,8 @@ func TestStateModuleOrphans_deepNestedNilConfig(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
state.init()
|
||||
|
||||
actual := state.ModuleOrphans(RootModulePath, nil)
|
||||
expected := [][]string{
|
||||
[]string{RootModuleName, "parent"},
|
||||
|
@ -1280,6 +1290,7 @@ func TestInstanceState_MergeDiff_nilDiff(t *testing.T) {
|
|||
func TestReadWriteState(t *testing.T) {
|
||||
state := &State{
|
||||
Serial: 9,
|
||||
Lineage: "5d1ad1a1-4027-4665-a908-dbe6adff11d8",
|
||||
Remote: &RemoteState{
|
||||
Type: "http",
|
||||
Config: map[string]string{
|
||||
|
@ -1309,6 +1320,7 @@ func TestReadWriteState(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
state.init()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := WriteState(state, buf); err != nil {
|
||||
|
@ -1328,9 +1340,11 @@ func TestReadWriteState(t *testing.T) {
|
|||
// ReadState should not restore sensitive information!
|
||||
mod := state.RootModule()
|
||||
mod.Resources["foo"].Primary.Ephemeral = EphemeralState{}
|
||||
mod.Resources["foo"].Primary.Ephemeral.init()
|
||||
|
||||
if !reflect.DeepEqual(actual, state) {
|
||||
t.Fatalf("bad: %#v", actual)
|
||||
t.Logf("expected:\n%#v", state)
|
||||
t.Fatalf("got:\n%#v", actual)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ func upgradeStateV1ToV2(old *stateV1) (*State, error) {
|
|||
}
|
||||
|
||||
newState.sort()
|
||||
newState.init()
|
||||
|
||||
return newState, nil
|
||||
}
|
||||
|
|
|
@ -35,7 +35,8 @@ func TestReadUpgradeStateV1toV3(t *testing.T) {
|
|||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, roundTripped) {
|
||||
t.Fatalf("bad: %#v", actual)
|
||||
t.Logf("actual:\n%#v", actual)
|
||||
t.Fatalf("roundTripped:\n%#v", roundTripped)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue