terraform/terraform/state_test.go

787 lines
14 KiB
Go

package terraform
import (
"bytes"
"encoding/json"
"reflect"
"strings"
"testing"
"github.com/hashicorp/terraform/config"
)
func TestStateAddModule(t *testing.T) {
cases := []struct {
In [][]string
Out [][]string
}{
{
[][]string{
[]string{"root"},
[]string{"root", "child"},
},
[][]string{
[]string{"root"},
[]string{"root", "child"},
},
},
{
[][]string{
[]string{"root", "foo", "bar"},
[]string{"root", "foo"},
[]string{"root"},
[]string{"root", "bar"},
},
[][]string{
[]string{"root"},
[]string{"root", "bar"},
[]string{"root", "foo"},
[]string{"root", "foo", "bar"},
},
},
}
for _, tc := range cases {
s := new(State)
for _, p := range tc.In {
s.AddModule(p)
}
actual := make([][]string, 0, len(tc.In))
for _, m := range s.Modules {
actual = append(actual, m.Path)
}
if !reflect.DeepEqual(actual, tc.Out) {
t.Fatalf("In: %#v\n\nOut: %#v", tc.In, actual)
}
}
}
func TestStateModuleOrphans(t *testing.T) {
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
},
&ModuleState{
Path: []string{RootModuleName, "foo"},
},
&ModuleState{
Path: []string{RootModuleName, "bar"},
},
},
}
config := testModule(t, "state-module-orphans").Config()
actual := state.ModuleOrphans(RootModulePath, config)
expected := [][]string{
[]string{RootModuleName, "foo"},
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestStateModuleOrphans_nilConfig(t *testing.T) {
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
},
&ModuleState{
Path: []string{RootModuleName, "foo"},
},
&ModuleState{
Path: []string{RootModuleName, "bar"},
},
},
}
actual := state.ModuleOrphans(RootModulePath, nil)
expected := [][]string{
[]string{RootModuleName, "foo"},
[]string{RootModuleName, "bar"},
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestStateEqual(t *testing.T) {
cases := []struct {
Result bool
One, Two *State
}{
// Nils
{
false,
nil,
&State{Version: 2},
},
{
true,
nil,
nil,
},
// Different versions
{
false,
&State{Version: 5},
&State{Version: 2},
},
// Different modules
{
false,
&State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
},
},
},
&State{},
},
{
true,
&State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
},
},
},
&State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
},
},
},
},
}
for i, tc := range cases {
if tc.One.Equal(tc.Two) != tc.Result {
t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String())
}
if tc.Two.Equal(tc.One) != tc.Result {
t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String())
}
}
}
func TestStateIncrementSerialMaybe(t *testing.T) {
cases := map[string]struct {
S1, S2 *State
Serial int64
}{
"S2 is nil": {
&State{},
nil,
0,
},
"S2 is identical": {
&State{},
&State{},
0,
},
"S2 is different": {
&State{},
&State{
Modules: []*ModuleState{
&ModuleState{Path: rootModulePath},
},
},
1,
},
"S1 serial is higher": {
&State{Serial: 5},
&State{
Serial: 3,
Modules: []*ModuleState{
&ModuleState{Path: rootModulePath},
},
},
5,
},
}
for name, tc := range cases {
tc.S1.IncrementSerialMaybe(tc.S2)
if tc.S1.Serial != tc.Serial {
t.Fatalf("Bad: %s\nGot: %d", name, tc.S1.Serial)
}
}
}
func TestResourceStateEqual(t *testing.T) {
cases := []struct {
Result bool
One, Two *ResourceState
}{
// Different types
{
false,
&ResourceState{Type: "foo"},
&ResourceState{Type: "bar"},
},
// Different dependencies
{
false,
&ResourceState{Dependencies: []string{"foo"}},
&ResourceState{Dependencies: []string{"bar"}},
},
{
false,
&ResourceState{Dependencies: []string{"foo", "bar"}},
&ResourceState{Dependencies: []string{"foo"}},
},
{
true,
&ResourceState{Dependencies: []string{"bar", "foo"}},
&ResourceState{Dependencies: []string{"foo", "bar"}},
},
// Different primaries
{
false,
&ResourceState{Primary: nil},
&ResourceState{Primary: &InstanceState{ID: "foo"}},
},
{
true,
&ResourceState{Primary: &InstanceState{ID: "foo"}},
&ResourceState{Primary: &InstanceState{ID: "foo"}},
},
// Different tainted
{
false,
&ResourceState{
Tainted: nil,
},
&ResourceState{
Tainted: []*InstanceState{
&InstanceState{ID: "foo"},
},
},
},
{
true,
&ResourceState{
Tainted: []*InstanceState{
&InstanceState{ID: "foo"},
},
},
&ResourceState{
Tainted: []*InstanceState{
&InstanceState{ID: "foo"},
},
},
},
{
true,
&ResourceState{
Tainted: []*InstanceState{
&InstanceState{ID: "foo"},
nil,
},
},
&ResourceState{
Tainted: []*InstanceState{
&InstanceState{ID: "foo"},
},
},
},
}
for i, tc := range cases {
if tc.One.Equal(tc.Two) != tc.Result {
t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String())
}
if tc.Two.Equal(tc.One) != tc.Result {
t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String())
}
}
}
func TestResourceStateTaint(t *testing.T) {
cases := map[string]struct {
Input *ResourceState
Output *ResourceState
}{
"no primary": {
&ResourceState{},
&ResourceState{},
},
"primary, no tainted": {
&ResourceState{
Primary: &InstanceState{ID: "foo"},
},
&ResourceState{
Tainted: []*InstanceState{
&InstanceState{ID: "foo"},
},
},
},
"primary, with tainted": {
&ResourceState{
Primary: &InstanceState{ID: "foo"},
Tainted: []*InstanceState{
&InstanceState{ID: "bar"},
},
},
&ResourceState{
Tainted: []*InstanceState{
&InstanceState{ID: "bar"},
&InstanceState{ID: "foo"},
},
},
},
}
for k, tc := range cases {
tc.Input.Taint()
if !reflect.DeepEqual(tc.Input, tc.Output) {
t.Fatalf(
"Failure: %s\n\nExpected: %#v\n\nGot: %#v",
k, tc.Output, tc.Input)
}
}
}
func TestInstanceStateEmpty(t *testing.T) {
cases := map[string]struct {
In *InstanceState
Result bool
}{
"nil is empty": {
nil,
true,
},
"non-nil but without ID is empty": {
&InstanceState{},
true,
},
"with ID is not empty": {
&InstanceState{
ID: "i-abc123",
},
false,
},
}
for tn, tc := range cases {
if tc.In.Empty() != tc.Result {
t.Fatalf("%q expected %#v to be empty: %#v", tn, tc.In, tc.Result)
}
}
}
func TestInstanceStateEqual(t *testing.T) {
cases := []struct {
Result bool
One, Two *InstanceState
}{
// Nils
{
false,
nil,
&InstanceState{},
},
{
false,
&InstanceState{},
nil,
},
// Different IDs
{
false,
&InstanceState{ID: "foo"},
&InstanceState{ID: "bar"},
},
// Different Attributes
{
false,
&InstanceState{Attributes: map[string]string{"foo": "bar"}},
&InstanceState{Attributes: map[string]string{"foo": "baz"}},
},
// Different Attribute keys
{
false,
&InstanceState{Attributes: map[string]string{"foo": "bar"}},
&InstanceState{Attributes: map[string]string{"bar": "baz"}},
},
{
false,
&InstanceState{Attributes: map[string]string{"bar": "baz"}},
&InstanceState{Attributes: map[string]string{"foo": "bar"}},
},
}
for i, tc := range cases {
if tc.One.Equal(tc.Two) != tc.Result {
t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String())
}
}
}
func TestStateEmpty(t *testing.T) {
cases := []struct {
In *State
Result bool
}{
{
nil,
true,
},
{
&State{},
true,
},
{
&State{
Remote: &RemoteState{Type: "foo"},
},
true,
},
{
&State{
Modules: []*ModuleState{
&ModuleState{},
},
},
false,
},
}
for i, tc := range cases {
if tc.In.Empty() != tc.Result {
t.Fatalf("bad %d %#v:\n\n%#v", i, tc.Result, tc.In)
}
}
}
func TestStateIsRemote(t *testing.T) {
cases := []struct {
In *State
Result bool
}{
{
nil,
false,
},
{
&State{},
false,
},
{
&State{
Remote: &RemoteState{Type: "foo"},
},
true,
},
}
for i, tc := range cases {
if tc.In.IsRemote() != tc.Result {
t.Fatalf("bad %d %#v:\n\n%#v", i, tc.Result, tc.In)
}
}
}
func TestInstanceState_MergeDiff(t *testing.T) {
is := InstanceState{
ID: "foo",
Attributes: map[string]string{
"foo": "bar",
"port": "8000",
},
}
diff := &InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "bar",
New: "baz",
},
"bar": &ResourceAttrDiff{
Old: "",
New: "foo",
},
"baz": &ResourceAttrDiff{
Old: "",
New: "foo",
NewComputed: true,
},
"port": &ResourceAttrDiff{
NewRemoved: true,
},
},
}
is2 := is.MergeDiff(diff)
expected := map[string]string{
"foo": "baz",
"bar": "foo",
"baz": config.UnknownVariableValue,
}
if !reflect.DeepEqual(expected, is2.Attributes) {
t.Fatalf("bad: %#v", is2.Attributes)
}
}
func TestInstanceState_MergeDiff_nil(t *testing.T) {
var is *InstanceState = nil
diff := &InstanceDiff{
Attributes: map[string]*ResourceAttrDiff{
"foo": &ResourceAttrDiff{
Old: "",
New: "baz",
},
},
}
is2 := is.MergeDiff(diff)
expected := map[string]string{
"foo": "baz",
}
if !reflect.DeepEqual(expected, is2.Attributes) {
t.Fatalf("bad: %#v", is2.Attributes)
}
}
func TestInstanceState_MergeDiff_nilDiff(t *testing.T) {
is := InstanceState{
ID: "foo",
Attributes: map[string]string{
"foo": "bar",
},
}
is2 := is.MergeDiff(nil)
expected := map[string]string{
"foo": "bar",
}
if !reflect.DeepEqual(expected, is2.Attributes) {
t.Fatalf("bad: %#v", is2.Attributes)
}
}
func TestReadUpgradeState(t *testing.T) {
state := &StateV1{
Resources: map[string]*ResourceStateV1{
"foo": &ResourceStateV1{
ID: "bar",
},
},
}
buf := new(bytes.Buffer)
if err := testWriteStateV1(state, buf); err != nil {
t.Fatalf("err: %s", err)
}
// ReadState should transparently detect the old
// version and upgrade up so the latest.
actual, err := ReadState(buf)
if err != nil {
t.Fatalf("err: %s", err)
}
upgraded, err := upgradeV1State(state)
if err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(actual, upgraded) {
t.Fatalf("bad: %#v", actual)
}
}
func TestReadWriteState(t *testing.T) {
state := &State{
Serial: 9,
Remote: &RemoteState{
Type: "http",
Config: map[string]string{
"url": "http://my-cool-server.com/",
},
},
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Dependencies: []string{
"aws_instance.bar",
},
Resources: map[string]*ResourceState{
"foo": &ResourceState{
Primary: &InstanceState{
ID: "bar",
Ephemeral: EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "root",
"password": "supersecret",
},
},
},
},
},
},
},
}
// Checksum before the write
chksum := checksumStruct(t, state)
buf := new(bytes.Buffer)
if err := WriteState(state, buf); err != nil {
t.Fatalf("err: %s", err)
}
// Verify that the version and serial are set
if state.Version != StateVersion {
t.Fatalf("bad version number: %d", state.Version)
}
// Checksum after the write
chksumAfter := checksumStruct(t, state)
if chksumAfter != chksum {
t.Fatalf("structure changed during serialization!")
}
actual, err := ReadState(buf)
if err != nil {
t.Fatalf("err: %s", err)
}
// ReadState should not restore sensitive information!
mod := state.RootModule()
mod.Resources["foo"].Primary.Ephemeral = EphemeralState{}
if !reflect.DeepEqual(actual, state) {
t.Fatalf("bad: %#v", actual)
}
}
func TestReadStateNewVersion(t *testing.T) {
type out struct {
Version int
}
buf, err := json.Marshal(&out{StateVersion + 1})
if err != nil {
t.Fatalf("err: %v", err)
}
s, err := ReadState(bytes.NewReader(buf))
if s != nil {
t.Fatalf("unexpected: %#v", s)
}
if !strings.Contains(err.Error(), "not supported") {
t.Fatalf("err: %v", err)
}
}
func TestUpgradeV1State(t *testing.T) {
old := &StateV1{
Outputs: map[string]string{
"ip": "127.0.0.1",
},
Resources: map[string]*ResourceStateV1{
"foo": &ResourceStateV1{
Type: "test_resource",
ID: "bar",
Attributes: map[string]string{
"key": "val",
},
},
"bar": &ResourceStateV1{
Type: "test_resource",
ID: "1234",
Attributes: map[string]string{
"a": "b",
},
},
},
Tainted: map[string]struct{}{
"bar": struct{}{},
},
}
state, err := upgradeV1State(old)
if err != nil {
t.Fatalf("err: %v", err)
}
if len(state.Modules) != 1 {
t.Fatalf("should only have root module: %#v", state.Modules)
}
root := state.RootModule()
if len(root.Outputs) != 1 {
t.Fatalf("bad outputs: %v", root.Outputs)
}
if root.Outputs["ip"] != "127.0.0.1" {
t.Fatalf("bad outputs: %v", root.Outputs)
}
if len(root.Resources) != 2 {
t.Fatalf("bad resources: %v", root.Resources)
}
foo := root.Resources["foo"]
if foo.Type != "test_resource" {
t.Fatalf("bad: %#v", foo)
}
if foo.Primary == nil || foo.Primary.ID != "bar" ||
foo.Primary.Attributes["key"] != "val" {
t.Fatalf("bad: %#v", foo)
}
if len(foo.Tainted) > 0 {
t.Fatalf("bad: %#v", foo)
}
bar := root.Resources["bar"]
if bar.Type != "test_resource" {
t.Fatalf("bad: %#v", bar)
}
if bar.Primary != nil {
t.Fatalf("bad: %#v", bar)
}
if len(bar.Tainted) != 1 {
t.Fatalf("bad: %#v", bar)
}
bt := bar.Tainted[0]
if bt.ID != "1234" || bt.Attributes["a"] != "b" {
t.Fatalf("bad: %#v", bt)
}
}