mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 01:41:48 -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.
184 lines
4.1 KiB
Go
184 lines
4.1 KiB
Go
package terraform
|
|
|
|
import (
|
|
"bytes"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
|
)
|
|
|
|
// TestReadUpgradeStateV1toV3 tests the state upgrade process from the V1 state
|
|
// to the current version, and needs editing each time. This means it tests the
|
|
// entire pipeline of upgrades (which migrate version to version).
|
|
func TestReadUpgradeStateV1toV3(t *testing.T) {
|
|
// ReadState should transparently detect the old version but will upgrade
|
|
// it on Write.
|
|
actual, err := ReadState(strings.NewReader(testV1State))
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
if err := WriteState(actual, buf); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
if actual.Version != 3 {
|
|
t.Fatalf("bad: State version not incremented; is %d", actual.Version)
|
|
}
|
|
|
|
roundTripped, err := ReadState(buf)
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(actual, roundTripped) {
|
|
t.Logf("actual:\n%#v", actual)
|
|
t.Fatalf("roundTripped:\n%#v", roundTripped)
|
|
}
|
|
}
|
|
|
|
func TestReadUpgradeStateV1toV3_outputs(t *testing.T) {
|
|
// ReadState should transparently detect the old version but will upgrade
|
|
// it on Write.
|
|
actual, err := ReadState(strings.NewReader(testV1StateWithOutputs))
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
if err := WriteState(actual, buf); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
if actual.Version != 3 {
|
|
t.Fatalf("bad: State version not incremented; is %d", actual.Version)
|
|
}
|
|
|
|
roundTripped, err := ReadState(buf)
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(actual, roundTripped) {
|
|
spew.Config.DisableMethods = true
|
|
t.Fatalf("bad:\n%s\n\nround tripped:\n%s\n", spew.Sdump(actual), spew.Sdump(roundTripped))
|
|
spew.Config.DisableMethods = false
|
|
}
|
|
}
|
|
|
|
// Upgrading the state should not lose empty module Outputs and Resources maps
|
|
// during upgrade. The init for a new module initializes new maps, so we may not
|
|
// be expecting to check for a nil map.
|
|
func TestReadUpgradeStateV1toV3_emptyState(t *testing.T) {
|
|
// ReadState should transparently detect the old version but will upgrade
|
|
// it on Write.
|
|
orig, err := ReadStateV1([]byte(testV1EmptyState))
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
stateV2, err := upgradeStateV1ToV2(orig)
|
|
for _, m := range stateV2.Modules {
|
|
if m.Resources == nil {
|
|
t.Fatal("V1 to V2 upgrade lost module.Resources")
|
|
}
|
|
if m.Outputs == nil {
|
|
t.Fatal("V1 to V2 upgrade lost module.Outputs")
|
|
}
|
|
}
|
|
|
|
stateV3, err := upgradeStateV2ToV3(stateV2)
|
|
for _, m := range stateV3.Modules {
|
|
if m.Resources == nil {
|
|
t.Fatal("V2 to V3 upgrade lost module.Resources")
|
|
}
|
|
if m.Outputs == nil {
|
|
t.Fatal("V2 to V3 upgrade lost module.Outputs")
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
const testV1EmptyState = `{
|
|
"version": 1,
|
|
"serial": 0,
|
|
"modules": [
|
|
{
|
|
"path": [
|
|
"root"
|
|
],
|
|
"outputs": {},
|
|
"resources": {}
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
const testV1State = `{
|
|
"version": 1,
|
|
"serial": 9,
|
|
"remote": {
|
|
"type": "http",
|
|
"config": {
|
|
"url": "http://my-cool-server.com/"
|
|
}
|
|
},
|
|
"modules": [
|
|
{
|
|
"path": [
|
|
"root"
|
|
],
|
|
"outputs": null,
|
|
"resources": {
|
|
"foo": {
|
|
"type": "",
|
|
"primary": {
|
|
"id": "bar"
|
|
}
|
|
}
|
|
},
|
|
"depends_on": [
|
|
"aws_instance.bar"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
const testV1StateWithOutputs = `{
|
|
"version": 1,
|
|
"serial": 9,
|
|
"remote": {
|
|
"type": "http",
|
|
"config": {
|
|
"url": "http://my-cool-server.com/"
|
|
}
|
|
},
|
|
"modules": [
|
|
{
|
|
"path": [
|
|
"root"
|
|
],
|
|
"outputs": {
|
|
"foo": "bar",
|
|
"baz": "foo"
|
|
},
|
|
"resources": {
|
|
"foo": {
|
|
"type": "",
|
|
"primary": {
|
|
"id": "bar"
|
|
}
|
|
}
|
|
},
|
|
"depends_on": [
|
|
"aws_instance.bar"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
`
|