package terraform import ( "bytes" "encoding/json" "fmt" "os" "reflect" "sort" "strings" "testing" "github.com/davecgh/go-spew/spew" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs/hcl2shim" ) func TestStateValidate(t *testing.T) { cases := map[string]struct { In *State Err bool }{ "empty state": { &State{}, false, }, "multiple modules": { &State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root", "foo"}, }, &ModuleState{ Path: []string{"root", "foo"}, }, }, }, true, }, } for name, tc := range cases { // Init the state tc.In.init() err := tc.In.Validate() if (err != nil) != tc.Err { t.Fatalf("%s: err: %s", name, err) } } } func TestStateAddModule(t *testing.T) { cases := []struct { In []addrs.ModuleInstance Out [][]string }{ { []addrs.ModuleInstance{ addrs.RootModuleInstance, addrs.RootModuleInstance.Child("child", addrs.NoKey), }, [][]string{ []string{"root"}, []string{"root", "child"}, }, }, { []addrs.ModuleInstance{ addrs.RootModuleInstance.Child("foo", addrs.NoKey).Child("bar", addrs.NoKey), addrs.RootModuleInstance.Child("foo", addrs.NoKey), addrs.RootModuleInstance, addrs.RootModuleInstance.Child("bar", addrs.NoKey), }, [][]string{ []string{"root"}, []string{"root", "bar"}, []string{"root", "foo"}, []string{"root", "foo", "bar"}, }, }, // Same last element, different middle element { []addrs.ModuleInstance{ addrs.RootModuleInstance.Child("foo", addrs.NoKey).Child("bar", addrs.NoKey), // This one should sort after... addrs.RootModuleInstance.Child("foo", addrs.NoKey), addrs.RootModuleInstance, addrs.RootModuleInstance.Child("bar", addrs.NoKey).Child("bar", addrs.NoKey), // ...this one. addrs.RootModuleInstance.Child("bar", addrs.NoKey), }, [][]string{ []string{"root"}, []string{"root", "bar"}, []string{"root", "foo"}, []string{"root", "bar", "bar"}, []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("wrong result\ninput: %sgot: %#v\nwant: %#v", spew.Sdump(tc.In), actual, tc.Out) } } } func TestStateOutputTypeRoundTrip(t *testing.T) { state := &State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root"}, Outputs: map[string]*OutputState{ "string_output": &OutputState{ Value: "String Value", Type: "string", }, }, }, }, } state.init() buf := new(bytes.Buffer) if err := WriteState(state, buf); err != nil { t.Fatalf("err: %s", err) } roundTripped, err := ReadState(buf) if err != nil { t.Fatalf("err: %s", err) } if !reflect.DeepEqual(state, roundTripped) { t.Logf("expected:\n%#v", state) t.Fatalf("got:\n%#v", roundTripped) } } func TestStateDeepCopy(t *testing.T) { cases := []struct { State *State }{ // Nil {nil}, // Version { &State{Version: 5}, }, // TFVersion { &State{TFVersion: "5"}, }, // Modules { &State{ Version: 6, Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "test_instance.foo": &ResourceState{ Primary: &InstanceState{ Meta: map[string]interface{}{}, }, }, }, }, }, }, }, // Deposed // The nil values shouldn't be there if the State was properly init'ed, // but the Copy should still work anyway. { &State{ Version: 6, Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "test_instance.foo": &ResourceState{ Primary: &InstanceState{ Meta: map[string]interface{}{}, }, Deposed: []*InstanceState{ {ID: "test"}, nil, }, }, }, }, }, }, }, } for i, tc := range cases { t.Run(fmt.Sprintf("copy-%d", i), func(t *testing.T) { actual := tc.State.DeepCopy() expected := tc.State if !reflect.DeepEqual(actual, expected) { t.Fatalf("Expected: %#v\nRecevied: %#v\n", expected, actual) } }) } } func TestStateEqual(t *testing.T) { cases := []struct { Name string Result bool One, Two *State }{ // Nils { "one nil", false, nil, &State{Version: 2}, }, { "both nil", true, nil, nil, }, // Different versions { "different state versions", false, &State{Version: 5}, &State{Version: 2}, }, // Different modules { "different module states", false, &State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root"}, }, }, }, &State{}, }, { "same module states", true, &State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root"}, }, }, }, &State{ Modules: []*ModuleState{ &ModuleState{ Path: []string{"root"}, }, }, }, }, // Meta differs { "differing meta values with primitives", false, &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "test_instance.foo": &ResourceState{ Primary: &InstanceState{ Meta: map[string]interface{}{ "schema_version": "1", }, }, }, }, }, }, }, &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "test_instance.foo": &ResourceState{ Primary: &InstanceState{ Meta: map[string]interface{}{ "schema_version": "2", }, }, }, }, }, }, }, }, // Meta with complex types { "same meta with complex types", true, &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "test_instance.foo": &ResourceState{ Primary: &InstanceState{ Meta: map[string]interface{}{ "timeouts": map[string]interface{}{ "create": 42, "read": "27", }, }, }, }, }, }, }, }, &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "test_instance.foo": &ResourceState{ Primary: &InstanceState{ Meta: map[string]interface{}{ "timeouts": map[string]interface{}{ "create": 42, "read": "27", }, }, }, }, }, }, }, }, }, // Meta with complex types that have been altered during serialization { "same meta with complex types that have been json-ified", true, &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "test_instance.foo": &ResourceState{ Primary: &InstanceState{ Meta: map[string]interface{}{ "timeouts": map[string]interface{}{ "create": int(42), "read": "27", }, }, }, }, }, }, }, }, &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "test_instance.foo": &ResourceState{ Primary: &InstanceState{ Meta: map[string]interface{}{ "timeouts": map[string]interface{}{ "create": float64(42), "read": "27", }, }, }, }, }, }, }, }, }, } for i, tc := range cases { t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { 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 TestStateCompareAges(t *testing.T) { cases := []struct { Result StateAgeComparison Err bool One, Two *State }{ { StateAgeEqual, false, &State{ Lineage: "1", Serial: 2, }, &State{ Lineage: "1", Serial: 2, }, }, { StateAgeReceiverOlder, false, &State{ Lineage: "1", Serial: 2, }, &State{ Lineage: "1", Serial: 3, }, }, { StateAgeReceiverNewer, false, &State{ Lineage: "1", Serial: 3, }, &State{ Lineage: "1", Serial: 2, }, }, { StateAgeEqual, true, &State{ Lineage: "1", Serial: 2, }, &State{ Lineage: "2", Serial: 2, }, }, { StateAgeEqual, true, &State{ Lineage: "1", Serial: 3, }, &State{ Lineage: "2", Serial: 2, }, }, } for i, tc := range cases { result, err := tc.One.CompareAges(tc.Two) if err != nil && !tc.Err { t.Errorf( "%d: got error, but want success\n\n%s\n\n%s", i, tc.One, tc.Two, ) continue } if err == nil && tc.Err { t.Errorf( "%d: got success, but want error\n\n%s\n\n%s", i, tc.One, tc.Two, ) continue } if result != tc.Result { t.Errorf( "%d: got result %d, but want %d\n\n%s\n\n%s", i, result, tc.Result, tc.One, tc.Two, ) continue } } } func TestStateSameLineage(t *testing.T) { cases := []struct { Result bool One, Two *State }{ { true, &State{ Lineage: "1", }, &State{ Lineage: "1", }, }, { // Empty lineage is compatible with all true, &State{ Lineage: "", }, &State{ Lineage: "1", }, }, { // Empty lineage is compatible with all true, &State{ Lineage: "1", }, &State{ Lineage: "", }, }, { false, &State{ Lineage: "1", }, &State{ Lineage: "2", }, }, } for i, tc := range cases { result := tc.One.SameLineage(tc.Two) if result != tc.Result { t.Errorf( "%d: got %v, but want %v\n\n%s\n\n%s", i, result, tc.Result, tc.One, tc.Two, ) continue } } } func TestStateMarshalEqual(t *testing.T) { tests := map[string]struct { S1, S2 *State Want bool }{ "both nil": { nil, nil, true, }, "first zero, second nil": { &State{}, nil, false, }, "first nil, second zero": { nil, &State{}, false, }, "both zero": { // These are not equal because they both implicitly init with // different lineage. &State{}, &State{}, false, }, "both set, same lineage": { &State{ Lineage: "abc123", }, &State{ Lineage: "abc123", }, true, }, "both set, same lineage, different serial": { &State{ Lineage: "abc123", Serial: 1, }, &State{ Lineage: "abc123", Serial: 2, }, false, }, "both set, same lineage, same serial, same resources": { &State{ Lineage: "abc123", Serial: 1, Modules: []*ModuleState{ { Path: []string{"root"}, Resources: map[string]*ResourceState{ "foo_bar.baz": {}, }, }, }, }, &State{ Lineage: "abc123", Serial: 1, Modules: []*ModuleState{ { Path: []string{"root"}, Resources: map[string]*ResourceState{ "foo_bar.baz": {}, }, }, }, }, true, }, "both set, same lineage, same serial, different resources": { &State{ Lineage: "abc123", Serial: 1, Modules: []*ModuleState{ { Path: []string{"root"}, Resources: map[string]*ResourceState{ "foo_bar.baz": {}, }, }, }, }, &State{ Lineage: "abc123", Serial: 1, Modules: []*ModuleState{ { Path: []string{"root"}, Resources: map[string]*ResourceState{ "pizza_crust.tasty": {}, }, }, }, }, false, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { got := test.S1.MarshalEqual(test.S2) if got != test.Want { t.Errorf("wrong result %#v; want %#v", got, test.Want) s1Buf := &bytes.Buffer{} s2Buf := &bytes.Buffer{} _ = WriteState(test.S1, s1Buf) _ = WriteState(test.S2, s2Buf) t.Logf("\nState 1: %s\nState 2: %s", s1Buf.Bytes(), s2Buf.Bytes()) } }) } } func TestStateRemove(t *testing.T) { cases := map[string]struct { Address string One, Two *State }{ "simple resource": { "test_instance.foo", &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "test_instance.foo": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, "test_instance.bar": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, }, }, }, }, &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "test_instance.bar": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, }, }, }, }, }, "single instance": { "test_instance.foo.primary", &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "test_instance.foo": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, }, }, }, }, &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{}, }, }, }, }, "single instance in multi-count": { "test_instance.foo[0]", &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "test_instance.foo.0": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, "test_instance.foo.1": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, }, }, }, }, &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "test_instance.foo.1": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, }, }, }, }, }, "single resource, multi-count": { "test_instance.foo", &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "test_instance.foo.0": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, "test_instance.foo.1": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, }, }, }, }, &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{}, }, }, }, }, "full module": { "module.foo", &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "test_instance.foo": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, }, }, &ModuleState{ Path: []string{"root", "foo"}, Resources: map[string]*ResourceState{ "test_instance.foo": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, "test_instance.bar": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, }, }, }, }, &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "test_instance.foo": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, }, }, }, }, }, "module and children": { "module.foo", &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "test_instance.foo": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, }, }, &ModuleState{ Path: []string{"root", "foo"}, Resources: map[string]*ResourceState{ "test_instance.foo": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, "test_instance.bar": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, }, }, &ModuleState{ Path: []string{"root", "foo", "bar"}, Resources: map[string]*ResourceState{ "test_instance.foo": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, "test_instance.bar": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, }, }, }, }, &State{ Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Resources: map[string]*ResourceState{ "test_instance.foo": &ResourceState{ Type: "test_instance", Primary: &InstanceState{ ID: "foo", }, }, }, }, }, }, }, } for k, tc := range cases { if err := tc.One.Remove(tc.Address); err != nil { t.Fatalf("bad: %s\n\n%s", k, err) } if !tc.One.Equal(tc.Two) { t.Fatalf("Bad: %s\n\n%s\n\n%s", k, tc.One.String(), tc.Two.String()) } } } 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{ Primary: &InstanceState{ ID: "foo", }, }, &ResourceState{ Primary: &InstanceState{ ID: "foo", Tainted: true, }, }, }, { true, &ResourceState{ Primary: &InstanceState{ ID: "foo", Tainted: true, }, }, &ResourceState{ Primary: &InstanceState{ ID: "foo", Tainted: true, }, }, }, } 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, not tainted": { &ResourceState{ Primary: &InstanceState{ID: "foo"}, }, &ResourceState{ Primary: &InstanceState{ ID: "foo", Tainted: true, }, }, }, "primary, tainted": { &ResourceState{ Primary: &InstanceState{ ID: "foo", Tainted: true, }, }, &ResourceState{ Primary: &InstanceState{ ID: "foo", Tainted: true, }, }, }, } 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 TestResourceStateUntaint(t *testing.T) { cases := map[string]struct { Input *ResourceState ExpectedOutput *ResourceState }{ "no primary, err": { Input: &ResourceState{}, ExpectedOutput: &ResourceState{}, }, "primary, not tainted": { Input: &ResourceState{ Primary: &InstanceState{ID: "foo"}, }, ExpectedOutput: &ResourceState{ Primary: &InstanceState{ID: "foo"}, }, }, "primary, tainted": { Input: &ResourceState{ Primary: &InstanceState{ ID: "foo", Tainted: true, }, }, ExpectedOutput: &ResourceState{ Primary: &InstanceState{ID: "foo"}, }, }, } for k, tc := range cases { tc.Input.Untaint() if !reflect.DeepEqual(tc.Input, tc.ExpectedOutput) { t.Fatalf( "Failure: %s\n\nExpected: %#v\n\nGot: %#v", k, tc.ExpectedOutput, 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 TestStateHasResources(t *testing.T) { cases := []struct { In *State Result bool }{ { nil, false, }, { &State{}, false, }, { &State{ Remote: &RemoteState{Type: "foo"}, }, false, }, { &State{ Modules: []*ModuleState{ &ModuleState{}, }, }, false, }, { &State{ Modules: []*ModuleState{ &ModuleState{}, &ModuleState{}, }, }, false, }, { &State{ Modules: []*ModuleState{ &ModuleState{}, &ModuleState{ Resources: map[string]*ResourceState{ "foo.foo": &ResourceState{}, }, }, }, }, true, }, } for i, tc := range cases { if tc.In.HasResources() != tc.Result { t.Fatalf("bad %d %#v:\n\n%#v", i, tc.Result, tc.In) } } } func TestStateFromFutureTerraform(t *testing.T) { cases := []struct { In string Result bool }{ { "", false, }, { "0.1", false, }, { "999.15.1", true, }, } for _, tc := range cases { state := &State{TFVersion: tc.In} actual := state.FromFutureTerraform() if actual != tc.Result { t.Fatalf("%s: bad: %v", tc.In, actual) } } } 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": hcl2shim.UnknownVariableValue, } if !reflect.DeepEqual(expected, is2.Attributes) { t.Fatalf("bad: %#v", is2.Attributes) } } // GH-12183. This tests that a list with a computed set generates the // right partial state. This never failed but is put here for completion // of the test case for GH-12183. func TestInstanceState_MergeDiff_computedSet(t *testing.T) { is := InstanceState{} diff := &InstanceDiff{ Attributes: map[string]*ResourceAttrDiff{ "config.#": &ResourceAttrDiff{ Old: "0", New: "1", RequiresNew: true, }, "config.0.name": &ResourceAttrDiff{ Old: "", New: "hello", }, "config.0.rules.#": &ResourceAttrDiff{ Old: "", NewComputed: true, }, }, } is2 := is.MergeDiff(diff) expected := map[string]string{ "config.#": "1", "config.0.name": "hello", "config.0.rules.#": hcl2shim.UnknownVariableValue, } if !reflect.DeepEqual(expected, is2.Attributes) { t.Fatalf("bad: %#v", is2.Attributes) } } func TestInstanceState_MergeDiff_nil(t *testing.T) { var is *InstanceState 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 TestReadWriteState(t *testing.T) { state := &State{ Serial: 9, Lineage: "5d1ad1a1-4027-4665-a908-dbe6adff11d8", 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", }, }, }, }, }, }, }, } state.init() 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) } 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{} mod.Resources["foo"].Primary.Ephemeral.init() if !reflect.DeepEqual(actual, state) { t.Logf("expected:\n%#v", state) t.Fatalf("got:\n%#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(), "does not support state version") { t.Fatalf("err: %v", err) } } func TestReadStateEmptyOrNilFile(t *testing.T) { var emptyState bytes.Buffer _, err := ReadState(&emptyState) if err != ErrNoState { t.Fatal("expected ErrNostate, got", err) } var nilFile *os.File _, err = ReadState(nilFile) if err != ErrNoState { t.Fatal("expected ErrNostate, got", err) } } func TestReadStateTFVersion(t *testing.T) { type tfVersion struct { Version int `json:"version"` TFVersion string `json:"terraform_version"` } cases := []struct { Written string Read string Err bool }{ { "0.0.0", "0.0.0", false, }, { "", "", false, }, { "bad", "", true, }, } for _, tc := range cases { buf, err := json.Marshal(&tfVersion{ Version: 2, TFVersion: tc.Written, }) if err != nil { t.Fatalf("err: %v", err) } s, err := ReadState(bytes.NewReader(buf)) if (err != nil) != tc.Err { t.Fatalf("%s: err: %s", tc.Written, err) } if err != nil { continue } if s.TFVersion != tc.Read { t.Fatalf("%s: bad: %s", tc.Written, s.TFVersion) } } } func TestWriteStateTFVersion(t *testing.T) { cases := []struct { Write string Read string Err bool }{ { "0.0.0", "0.0.0", false, }, { "", "", false, }, { "bad", "", true, }, } for _, tc := range cases { var buf bytes.Buffer err := WriteState(&State{TFVersion: tc.Write}, &buf) if (err != nil) != tc.Err { t.Fatalf("%s: err: %s", tc.Write, err) } if err != nil { continue } s, err := ReadState(&buf) if err != nil { t.Fatalf("%s: err: %s", tc.Write, err) } if s.TFVersion != tc.Read { t.Fatalf("%s: bad: %s", tc.Write, s.TFVersion) } } } func TestParseResourceStateKey(t *testing.T) { cases := []struct { Input string Expected *ResourceStateKey ExpectedErr bool }{ { Input: "aws_instance.foo.3", Expected: &ResourceStateKey{ Mode: ManagedResourceMode, Type: "aws_instance", Name: "foo", Index: 3, }, }, { Input: "aws_instance.foo.0", Expected: &ResourceStateKey{ Mode: ManagedResourceMode, Type: "aws_instance", Name: "foo", Index: 0, }, }, { Input: "aws_instance.foo", Expected: &ResourceStateKey{ Mode: ManagedResourceMode, Type: "aws_instance", Name: "foo", Index: -1, }, }, { Input: "data.aws_ami.foo", Expected: &ResourceStateKey{ Mode: DataResourceMode, Type: "aws_ami", Name: "foo", Index: -1, }, }, { Input: "aws_instance.foo.malformed", ExpectedErr: true, }, { Input: "aws_instance.foo.malformedwithnumber.123", ExpectedErr: true, }, { Input: "malformed", ExpectedErr: true, }, } for _, tc := range cases { rsk, err := ParseResourceStateKey(tc.Input) if rsk != nil && tc.Expected != nil && !rsk.Equal(tc.Expected) { t.Fatalf("%s: expected %s, got %s", tc.Input, tc.Expected, rsk) } if (err != nil) != tc.ExpectedErr { t.Fatalf("%s: expected err: %t, got %s", tc.Input, tc.ExpectedErr, err) } } } func TestReadState_prune(t *testing.T) { state := &State{ Modules: []*ModuleState{ &ModuleState{Path: rootModulePath}, nil, }, } state.init() buf := new(bytes.Buffer) if err := WriteState(state, buf); err != nil { t.Fatalf("err: %s", err) } actual, err := ReadState(buf) if err != nil { t.Fatalf("err: %s", err) } expected := &State{ Version: state.Version, Lineage: state.Lineage, } expected.init() if !reflect.DeepEqual(actual, expected) { t.Fatalf("got:\n%#v", actual) } } func TestReadState_pruneDependencies(t *testing.T) { state := &State{ Serial: 9, Lineage: "5d1ad1a1-4027-4665-a908-dbe6adff11d8", Remote: &RemoteState{ Type: "http", Config: map[string]string{ "url": "http://my-cool-server.com/", }, }, Modules: []*ModuleState{ &ModuleState{ Path: rootModulePath, Dependencies: []string{ "aws_instance.bar", "aws_instance.bar", }, Resources: map[string]*ResourceState{ "foo": &ResourceState{ Dependencies: []string{ "aws_instance.baz", "aws_instance.baz", }, Primary: &InstanceState{ ID: "bar", }, }, }, }, }, } state.init() buf := new(bytes.Buffer) if err := WriteState(state, buf); err != nil { t.Fatalf("err: %s", err) } actual, err := ReadState(buf) if err != nil { t.Fatalf("err: %s", err) } // make sure the duplicate Dependencies are filtered modDeps := actual.Modules[0].Dependencies resourceDeps := actual.Modules[0].Resources["foo"].Dependencies if len(modDeps) > 1 || modDeps[0] != "aws_instance.bar" { t.Fatalf("expected 1 module depends_on entry, got %q", modDeps) } if len(resourceDeps) > 1 || resourceDeps[0] != "aws_instance.baz" { t.Fatalf("expected 1 resource depends_on entry, got %q", resourceDeps) } } func TestReadState_bigHash(t *testing.T) { expected := uint64(14885267135666261723) s := strings.NewReader(`{"version": 3, "backend":{"hash":14885267135666261723}}`) actual, err := ReadState(s) if err != nil { t.Fatal(err) } if actual.Backend.Hash != expected { t.Fatalf("expected backend hash %d, got %d", expected, actual.Backend.Hash) } } func TestResourceNameSort(t *testing.T) { names := []string{ "a", "b", "a.0", "a.c", "a.d", "c", "a.b.0", "a.b.1", "a.b.10", "a.b.2", } sort.Sort(resourceNameSort(names)) expected := []string{ "a", "a.0", "a.b.0", "a.b.1", "a.b.2", "a.b.10", "a.c", "a.d", "b", "c", } if !reflect.DeepEqual(names, expected) { t.Fatalf("got: %q\nexpected: %q\n", names, expected) } }