// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package states import ( "fmt" "reflect" "testing" "time" "github.com/go-test/deep" "github.com/google/go-cmp/cmp" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" ) func TestState(t *testing.T) { // This basic tests exercises the main mutation methods to construct // a state. It is not fully comprehensive, so other tests should visit // more esoteric codepaths. state := NewState() rootModule := state.RootModule() if rootModule == nil { t.Errorf("root module is nil; want valid object") } state.SetOutputValue( addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance), cty.StringVal("bar value"), false, ) state.SetOutputValue( addrs.OutputValue{Name: "secret"}.Absolute(addrs.RootModuleInstance), cty.StringVal("secret value"), true, ) rootModule.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "baz", }.Instance(addrs.IntKey(0)), &ResourceInstanceObjectSrc{ Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) // State silently ignores attempts to write to non-root outputs, because // historically we did track those here but these days we track them in // namedvals.State instead, and we're being gracious to existing callers // that might not know yet that they need to treat root module output // values in a special way. childModule := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey)) state.SetOutputValue(addrs.OutputValue{Name: "pizza"}.Absolute(childModule.Addr), cty.StringVal("hawaiian"), false) multiModA := state.EnsureModule(addrs.RootModuleInstance.Child("multi", addrs.StringKey("a"))) state.SetOutputValue(addrs.OutputValue{Name: "pizza"}.Absolute(multiModA.Addr), cty.StringVal("cheese"), false) multiModB := state.EnsureModule(addrs.RootModuleInstance.Child("multi", addrs.StringKey("b"))) state.SetOutputValue(addrs.OutputValue{Name: "pizza"}.Absolute(multiModB.Addr), cty.StringVal("sausage"), false) want := &State{ RootOutputValues: map[string]*OutputValue{ "bar": { Addr: addrs.AbsOutputValue{ OutputValue: addrs.OutputValue{ Name: "bar", }, }, Value: cty.StringVal("bar value"), Sensitive: false, }, "secret": { Addr: addrs.AbsOutputValue{ OutputValue: addrs.OutputValue{ Name: "secret", }, }, Value: cty.StringVal("secret value"), Sensitive: true, }, }, Modules: map[string]*Module{ "": { Addr: addrs.RootModuleInstance, Resources: map[string]*Resource{ "test_thing.baz": { Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "baz", }.Absolute(addrs.RootModuleInstance), Instances: map[addrs.InstanceKey]*ResourceInstance{ addrs.IntKey(0): { Current: &ResourceInstanceObjectSrc{ SchemaVersion: 1, Status: ObjectReady, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, Deposed: map[DeposedKey]*ResourceInstanceObjectSrc{}, }, }, ProviderConfig: addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, }, }, }, "module.child": { Addr: addrs.RootModuleInstance.Child("child", addrs.NoKey), Resources: map[string]*Resource{}, }, `module.multi["a"]`: { Addr: addrs.RootModuleInstance.Child("multi", addrs.StringKey("a")), Resources: map[string]*Resource{}, }, `module.multi["b"]`: { Addr: addrs.RootModuleInstance.Child("multi", addrs.StringKey("b")), Resources: map[string]*Resource{}, }, }, } { // Our structure goes deep, so we need to temporarily override the // deep package settings to ensure that we visit the full structure. oldDeepDepth := deep.MaxDepth oldDeepCompareUnexp := deep.CompareUnexportedFields deep.MaxDepth = 50 deep.CompareUnexportedFields = true defer func() { deep.MaxDepth = oldDeepDepth deep.CompareUnexportedFields = oldDeepCompareUnexp }() } if diff := cmp.Diff(want.String(), state.String()); diff != "" { t.Errorf("wrong result\n%s", diff) } } func TestStateDeepCopyObject(t *testing.T) { obj := &ResourceInstanceObject{ Value: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("id"), }), Private: []byte("private"), Status: ObjectReady, Dependencies: []addrs.ConfigResource{ { Module: addrs.RootModule, Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar", }, }, }, CreateBeforeDestroy: true, } objCopy := obj.DeepCopy() if !reflect.DeepEqual(obj, objCopy) { t.Fatalf("not equal\n%#v\n%#v", obj, objCopy) } } func TestStateDeepCopy(t *testing.T) { state := NewState() rootModule := state.RootModule() if rootModule == nil { t.Fatalf("root module is nil; want valid object") } state.SetOutputValue( addrs.OutputValue{Name: "bar"}.Absolute(rootModule.Addr), cty.StringVal("bar value"), false, ) state.SetOutputValue( addrs.OutputValue{Name: "secret"}.Absolute(rootModule.Addr), cty.StringVal("secret value"), true, ) rootModule.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "baz", }.Instance(addrs.IntKey(0)), &ResourceInstanceObjectSrc{ Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), Private: []byte("private data"), Dependencies: []addrs.ConfigResource{}, CreateBeforeDestroy: true, // these may or may not be copied, but should not affect equality of // the resources. decodeValueCache: cty.ObjectVal(map[string]cty.Value{ "woozles": cty.StringVal("confuzles"), }), decodeIdentityCache: cty.DynamicVal, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) rootModule.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "bar", }.Instance(addrs.IntKey(0)), &ResourceInstanceObjectSrc{ Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), // Sensitive path at "woozles" AttrSensitivePaths: []cty.Path{ cty.GetAttrPath("woozles"), }, Private: []byte("private data"), Dependencies: []addrs.ConfigResource{ { Module: addrs.RootModule, Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "baz", }, }, }, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey)) stateCopy := state.DeepCopy() if !state.Equal(stateCopy) { t.Fatalf("\nexpected:\n%q\ngot:\n%q\n", state, stateCopy) } // this is implied by the above, but has previously used a different // codepath for comparison. if !state.ManagedResourcesEqual(stateCopy) { t.Fatalf("\nexpected managed resources to be equal:\n%q\ngot:\n%q\n", state, stateCopy) } // remove the cached values and ensure equality still holds for _, mod := range stateCopy.Modules { for _, res := range mod.Resources { for _, inst := range res.Instances { inst.Current.decodeValueCache = cty.NilVal inst.Current.decodeIdentityCache = cty.NilVal } } } if !state.Equal(stateCopy) { t.Fatalf("\nexpected:\n%q\ngot:\n%q\n", state, stateCopy) } if !state.ManagedResourcesEqual(stateCopy) { t.Fatalf("\nexpected managed resources to be equal:\n%q\ngot:\n%q\n", state, stateCopy) } } func TestStateHasResourceInstanceObjects(t *testing.T) { providerConfig := addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("test/test"), } childModuleProviderConfig := addrs.AbsProviderConfig{ Module: addrs.RootModule.Child("child"), Provider: addrs.MustParseProviderSourceString("test/test"), } tests := map[string]struct { Setup func(ss *SyncState) Want bool }{ "empty": { func(ss *SyncState) {}, false, }, "one current, ready object in root module": { func(ss *SyncState) { ss.SetResourceInstanceCurrent( mustAbsResourceAddr("test.foo").Instance(addrs.NoKey), &ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{}`), Status: ObjectReady, }, providerConfig, ) }, true, }, "one current, ready object in child module": { func(ss *SyncState) { ss.SetResourceInstanceCurrent( mustAbsResourceAddr("module.child.test.foo").Instance(addrs.NoKey), &ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{}`), Status: ObjectReady, }, childModuleProviderConfig, ) }, true, }, "one current, tainted object in root module": { func(ss *SyncState) { ss.SetResourceInstanceCurrent( mustAbsResourceAddr("test.foo").Instance(addrs.NoKey), &ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{}`), Status: ObjectTainted, }, providerConfig, ) }, true, }, "one deposed, ready object in root module": { func(ss *SyncState) { ss.SetResourceInstanceDeposed( mustAbsResourceAddr("test.foo").Instance(addrs.NoKey), DeposedKey("uhoh"), &ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{}`), Status: ObjectTainted, }, providerConfig, ) }, true, }, "one empty resource husk in root module": { func(ss *SyncState) { // Current Terraform doesn't actually create resource husks // as part of its everyday work, so this is a "should never // happen" case but we'll test to make sure we're robust to // it anyway, because this was a historical bug blocking // "terraform workspace delete" and similar. ss.SetResourceInstanceCurrent( mustAbsResourceAddr("test.foo").Instance(addrs.NoKey), &ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{}`), Status: ObjectTainted, }, providerConfig, ) s := ss.Lock() delete(s.Modules[""].Resources["test.foo"].Instances, addrs.NoKey) ss.Unlock() done := make(chan struct{}) go func() { ss.Lock() ss.Unlock() close(done) }() select { case <-done: // OK: lock was released case <-time.After(500 * time.Millisecond): t.Fatalf("Unlock did not release SyncState lock (timed out acquiring lock again)") } }, false, }, "one current data resource object in root module": { func(ss *SyncState) { ss.SetResourceInstanceCurrent( mustAbsResourceAddr("data.test.foo").Instance(addrs.NoKey), &ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{}`), Status: ObjectReady, }, providerConfig, ) }, false, // data resources aren't managed resources, so they don't count }, } for name, test := range tests { t.Run(name, func(t *testing.T) { state := BuildState(test.Setup) got := state.HasManagedResourceInstanceObjects() if got != test.Want { t.Errorf("wrong result\nstate content: (using legacy state string format; might not be comprehensive)\n%s\n\ngot: %t\nwant: %t", state, got, test.Want) } }) } } func TestStateHasRootOutputValues(t *testing.T) { tests := map[string]struct { Setup func(ss *SyncState) Want bool }{ "empty": { func(ss *SyncState) {}, false, }, "one output value": { func(ss *SyncState) { ss.SetOutputValue( addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), cty.StringVal("bar value"), false, ) }, true, }, "one secret output value": { func(ss *SyncState) { ss.SetOutputValue( addrs.OutputValue{Name: "secret"}.Absolute(addrs.RootModuleInstance), cty.StringVal("secret value"), true, ) }, true, }, // The output value tests below are in other modules and do not persist between runs. // Terraform Core tracks them internally and does not expose them in any // artifacts that survive between executions. "one output value in child module": { func(ss *SyncState) { ss.SetOutputValue( addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey)), cty.StringVal("bar value"), false, ) }, false, }, "one output value in multi module": { func(ss *SyncState) { ss.state.EnsureModule(addrs.RootModuleInstance.Child("multi", addrs.StringKey("a"))) ss.SetOutputValue( addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance.Child("multi", addrs.StringKey("a"))), cty.StringVal("bar"), false, ) }, false, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { state := BuildState(test.Setup) got := state.HasRootOutputValues() if got != test.Want { t.Errorf("wrong result\nstate content: (using legacy state string format; might not be comprehensive)\n%s\n\ngot: %t\nwant: %t", state, got, test.Want) } }) } } func TestStateRootOutputValuesEqual(t *testing.T) { tests := map[string]struct { SetupA func(ss *SyncState) SetupB func(ss *SyncState) Want bool }{ "both empty": { func(ss *SyncState) {}, func(ss *SyncState) {}, true, }, "identical outputs": { func(ss *SyncState) { ss.SetOutputValue( addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), cty.StringVal("bar"), false, ) }, func(ss *SyncState) { ss.SetOutputValue( addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), cty.StringVal("bar"), false, ) }, true, }, "different values same key": { func(ss *SyncState) { ss.SetOutputValue( addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), cty.StringVal("bar"), false, ) }, func(ss *SyncState) { ss.SetOutputValue( addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), cty.StringVal("baz"), false, ) }, false, }, "different sensitivity same value": { func(ss *SyncState) { ss.SetOutputValue( addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), cty.StringVal("bar"), false, ) }, func(ss *SyncState) { ss.SetOutputValue( addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), cty.StringVal("bar"), true, ) }, false, }, "different keys same count": { func(ss *SyncState) { ss.SetOutputValue( addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), cty.StringVal("val"), false, ) }, func(ss *SyncState) { ss.SetOutputValue( addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance), cty.StringVal("val"), false, ) }, false, }, "different count": { func(ss *SyncState) { ss.SetOutputValue( addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), cty.StringVal("val"), false, ) }, func(ss *SyncState) { ss.SetOutputValue( addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), cty.StringVal("val"), false, ) ss.SetOutputValue( addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance), cty.StringVal("val2"), false, ) }, false, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { stateA := BuildState(test.SetupA) stateB := BuildState(test.SetupB) got := stateA.RootOutputValuesEqual(stateB) if got != test.Want { t.Errorf("wrong result for stateA.RootOutputValuesEqual(stateB)\ngot: %t\nwant: %t", got, test.Want) } }) } } func TestState_MoveAbsResource(t *testing.T) { // Set up a starter state for the embedded tests, which should start from a copy of this state. state := NewState() rootModule := state.RootModule() rootModule.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo", }.Instance(addrs.IntKey(0)), &ResourceInstanceObjectSrc{ Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Absolute(addrs.RootModuleInstance) t.Run("basic move", func(t *testing.T) { s := state.DeepCopy() dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "bar"}.Absolute(addrs.RootModuleInstance) s.MoveAbsResource(src, dst) if s.Empty() { t.Fatal("unexpected empty state") } if len(s.RootModule().Resources) != 1 { t.Fatalf("wrong number of resources in state; expected 1, found %d", len(state.RootModule().Resources)) } got := s.Resource(dst) if got.Addr.Resource != dst.Resource { t.Fatalf("dst resource not in state") } }) t.Run("move to new module", func(t *testing.T) { s := state.DeepCopy() dstModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("one")) dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "bar"}.Absolute(dstModule) s.MoveAbsResource(src, dst) if s.Empty() { t.Fatal("unexpected empty state") } if s.Module(dstModule) == nil { t.Fatalf("child module %s not in state", dstModule.String()) } if len(s.Module(dstModule).Resources) != 1 { t.Fatalf("wrong number of resources in state; expected 1, found %d", len(s.Module(dstModule).Resources)) } got := s.Resource(dst) if got.Addr.Resource != dst.Resource { t.Fatalf("dst resource not in state") } }) t.Run("from a child module to root", func(t *testing.T) { s := state.DeepCopy() srcModule := addrs.RootModuleInstance.Child("kinder", addrs.NoKey) cm := s.EnsureModule(srcModule) cm.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child", }.Instance(addrs.IntKey(0)), // Moving the AbsResouce moves all instances &ResourceInstanceObjectSrc{ Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) cm.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child", }.Instance(addrs.IntKey(1)), // Moving the AbsResouce moves all instances &ResourceInstanceObjectSrc{ Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(srcModule) dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(addrs.RootModuleInstance) s.MoveAbsResource(src, dst) if s.Empty() { t.Fatal("unexpected empty state") } // The child module should have been removed after removing its only resource if s.Module(srcModule) != nil { t.Fatalf("child module %s was not removed from state after mv", srcModule.String()) } if len(s.RootModule().Resources) != 2 { t.Fatalf("wrong number of resources in state; expected 2, found %d", len(s.RootModule().Resources)) } if len(s.Resource(dst).Instances) != 2 { t.Fatalf("wrong number of resource instances for dst, got %d expected 2", len(s.Resource(dst).Instances)) } got := s.Resource(dst) if got.Addr.Resource != dst.Resource { t.Fatalf("dst resource not in state") } }) t.Run("module to new module", func(t *testing.T) { s := NewState() srcModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("exists")) dstModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("new")) cm := s.EnsureModule(srcModule) cm.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child", }.Instance(addrs.NoKey), &ResourceInstanceObjectSrc{ Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(srcModule) dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(dstModule) s.MoveAbsResource(src, dst) if s.Empty() { t.Fatal("unexpected empty state") } // The child module should have been removed after removing its only resource if s.Module(srcModule) != nil { t.Fatalf("child module %s was not removed from state after mv", srcModule.String()) } gotMod := s.Module(dstModule) if len(gotMod.Resources) != 1 { t.Fatalf("wrong number of resources in state; expected 1, found %d", len(gotMod.Resources)) } got := s.Resource(dst) if got.Addr.Resource != dst.Resource { t.Fatalf("dst resource not in state") } }) t.Run("module to new module", func(t *testing.T) { s := NewState() srcModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("exists")) dstModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("new")) cm := s.EnsureModule(srcModule) cm.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child", }.Instance(addrs.NoKey), &ResourceInstanceObjectSrc{ Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(srcModule) dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(dstModule) s.MoveAbsResource(src, dst) if s.Empty() { t.Fatal("unexpected empty state") } // The child module should have been removed after removing its only resource if s.Module(srcModule) != nil { t.Fatalf("child module %s was not removed from state after mv", srcModule.String()) } gotMod := s.Module(dstModule) if len(gotMod.Resources) != 1 { t.Fatalf("wrong number of resources in state; expected 1, found %d", len(gotMod.Resources)) } got := s.Resource(dst) if got.Addr.Resource != dst.Resource { t.Fatalf("dst resource not in state") } }) } func TestState_MaybeMoveAbsResource(t *testing.T) { state := NewState() rootModule := state.RootModule() rootModule.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo", }.Instance(addrs.IntKey(0)), &ResourceInstanceObjectSrc{ Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Absolute(addrs.RootModuleInstance) dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "bar"}.Absolute(addrs.RootModuleInstance) // First move, success t.Run("first move", func(t *testing.T) { moved := state.MaybeMoveAbsResource(src, dst) if !moved { t.Fatal("wrong result") } }) // Trying to move a resource that doesn't exist in state to a resource which does exist should be a noop. t.Run("noop", func(t *testing.T) { moved := state.MaybeMoveAbsResource(src, dst) if moved { t.Fatal("wrong result") } }) } func TestState_MoveAbsResourceInstance(t *testing.T) { state := NewState() rootModule := state.RootModule() rootModule.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo", }.Instance(addrs.NoKey), &ResourceInstanceObjectSrc{ Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) // src resource from the state above src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) t.Run("resource to resource instance", func(t *testing.T) { s := state.DeepCopy() // For a little extra fun, move a resource to a resource instance: test_thing.foo to test_thing.foo[1] dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance) s.MoveAbsResourceInstance(src, dst) if s.Empty() { t.Fatal("unexpected empty state") } if len(s.RootModule().Resources) != 1 { t.Fatalf("wrong number of resources in state; expected 1, found %d", len(state.RootModule().Resources)) } got := s.ResourceInstance(dst) if got == nil { t.Fatalf("dst resource not in state") } }) t.Run("move to new module", func(t *testing.T) { s := state.DeepCopy() // test_thing.foo to module.kinder.test_thing.foo["baz"] dstModule := addrs.RootModuleInstance.Child("kinder", addrs.NoKey) dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.IntKey(1)).Absolute(dstModule) s.MoveAbsResourceInstance(src, dst) if s.Empty() { t.Fatal("unexpected empty state") } if s.Module(dstModule) == nil { t.Fatalf("child module %s not in state", dstModule.String()) } if len(s.Module(dstModule).Resources) != 1 { t.Fatalf("wrong number of resources in state; expected 1, found %d", len(s.Module(dstModule).Resources)) } got := s.ResourceInstance(dst) if got == nil { t.Fatalf("dst resource not in state") } }) } func TestState_MaybeMoveAbsResourceInstance(t *testing.T) { state := NewState() rootModule := state.RootModule() rootModule.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo", }.Instance(addrs.NoKey), &ResourceInstanceObjectSrc{ Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) // For a little extra fun, let's go from a resource to a resource instance: test_thing.foo to test_thing.bar[1] src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance) // First move, success t.Run("first move", func(t *testing.T) { moved := state.MaybeMoveAbsResourceInstance(src, dst) if !moved { t.Fatal("wrong result") } got := state.ResourceInstance(dst) if got == nil { t.Fatal("destination resource instance not in state") } }) // Moving a resource instance that doesn't exist in state to a resource which does exist should be a noop. t.Run("noop", func(t *testing.T) { moved := state.MaybeMoveAbsResourceInstance(src, dst) if moved { t.Fatal("wrong result") } }) } func TestState_MoveModuleInstance(t *testing.T) { state := NewState() srcModule := addrs.RootModuleInstance.Child("kinder", addrs.NoKey) m := state.EnsureModule(srcModule) m.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo", }.Instance(addrs.NoKey), &ResourceInstanceObjectSrc{ Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) dstModule := addrs.RootModuleInstance.Child("child", addrs.IntKey(3)) state.MoveModuleInstance(srcModule, dstModule) // srcModule should have been removed, dstModule should exist and have one resource if len(state.Modules) != 2 { // kinder[3] and root t.Fatalf("wrong number of modules in state. Expected 2, got %d", len(state.Modules)) } got := state.Module(dstModule) if got == nil { t.Fatal("dstModule not found") } gone := state.Module(srcModule) if gone != nil { t.Fatal("srcModule not removed from state") } r := got.Resource(mustAbsResourceAddr("test_thing.foo").Resource) if r.Addr.Module.String() != dstModule.String() { fmt.Println(r.Addr.Module.String()) t.Fatal("resource address was not updated") } } func TestState_MaybeMoveModuleInstance(t *testing.T) { state := NewState() src := addrs.RootModuleInstance.Child("child", addrs.StringKey("a")) cm := state.EnsureModule(src) cm.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo", }.Instance(addrs.NoKey), &ResourceInstanceObjectSrc{ Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) dst := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("b")) // First move, success t.Run("first move", func(t *testing.T) { moved := state.MaybeMoveModuleInstance(src, dst) if !moved { t.Fatal("wrong result") } }) // Second move, should be a noop t.Run("noop", func(t *testing.T) { moved := state.MaybeMoveModuleInstance(src, dst) if moved { t.Fatal("wrong result") } }) } func TestState_MoveModule(t *testing.T) { // For this test, add two module instances (kinder and kinder["a"]). // MoveModule(kinder) should move both instances. state := NewState() // starter state, should be copied by the subtests. srcModule := addrs.RootModule.Child("kinder") m := state.EnsureModule(srcModule.UnkeyedInstanceShim()) m.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo", }.Instance(addrs.NoKey), &ResourceInstanceObjectSrc{ Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) moduleInstance := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("a")) mi := state.EnsureModule(moduleInstance) mi.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo", }.Instance(addrs.NoKey), &ResourceInstanceObjectSrc{ Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) _, mc := srcModule.Call() src := mc.Absolute(addrs.RootModuleInstance.Child("kinder", addrs.NoKey)) t.Run("basic", func(t *testing.T) { s := state.DeepCopy() _, dstMC := addrs.RootModule.Child("child").Call() dst := dstMC.Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey)) s.MoveModule(src, dst) // srcModule should have been removed, dstModule should exist and have one resource if len(s.Modules) != 3 { // child, child["a"] and root t.Fatalf("wrong number of modules in state. Expected 3, got %d", len(s.Modules)) } got := s.Module(dst.Module) if got == nil { t.Fatal("dstModule not found") } got = s.Module(addrs.RootModuleInstance.Child("child", addrs.StringKey("a"))) if got == nil { t.Fatal("dstModule instance \"a\" not found") } gone := s.Module(srcModule.UnkeyedInstanceShim()) if gone != nil { t.Fatal("srcModule not removed from state") } }) t.Run("nested modules", func(t *testing.T) { s := state.DeepCopy() // add a child module to module.kinder mi := mustParseModuleInstanceStr(`module.kinder.module.grand[1]`) m := s.EnsureModule(mi) m.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo", }.Instance(addrs.NoKey), &ResourceInstanceObjectSrc{ Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) _, dstMC := addrs.RootModule.Child("child").Call() dst := dstMC.Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey)) s.MoveModule(src, dst) moved := s.Module(addrs.RootModuleInstance.Child("child", addrs.StringKey("a"))) if moved == nil { t.Fatal("dstModule not found") } // The nested module's relative address should also have been updated nested := s.Module(mustParseModuleInstanceStr(`module.child.module.grand[1]`)) if nested == nil { t.Fatal("nested child module of src wasn't moved") } }) } func TestState_ProviderAddrs(t *testing.T) { // 1) nil state var nilState *State if got := nilState.ProviderAddrs(); got != nil { t.Fatalf("nil state: expected nil, got %#v", got) } // 2) empty state empty := NewState() if got := empty.ProviderAddrs(); got != nil { t.Fatalf("empty state: expected nil, got %#v", got) } // 3) populated state s := NewState() rootAWS := addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("aws"), } rootGoogle := addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("google"), } childAWS := addrs.AbsProviderConfig{ Module: addrs.RootModule.Child("child"), Provider: addrs.NewDefaultProvider("aws"), } rm := s.RootModule() rm.SetResourceInstanceCurrent( addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.NoKey), &ResourceInstanceObjectSrc{Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{}`)}, rootAWS, ) rm.SetResourceInstanceCurrent( addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "bar"}.Instance(addrs.NoKey), &ResourceInstanceObjectSrc{Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{}`)}, rootAWS, ) rm.SetResourceInstanceCurrent( addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "baz"}.Instance(addrs.NoKey), &ResourceInstanceObjectSrc{Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{}`)}, rootGoogle, ) childMI := addrs.RootModuleInstance.Child("child", addrs.NoKey) cm := s.EnsureModule(childMI) cm.SetResourceInstanceCurrent( addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Instance(addrs.NoKey), &ResourceInstanceObjectSrc{Status: ObjectReady, SchemaVersion: 1, AttrsJSON: []byte(`{}`)}, childAWS, ) got := s.ProviderAddrs() expected := []addrs.AbsProviderConfig{childAWS, rootAWS, rootGoogle} if !reflect.DeepEqual(got, expected) { t.Fatalf("unexpected provider addrs\nexpected: %#v\ngot: %#v", expected, got) } } func mustParseModuleInstanceStr(str string) addrs.ModuleInstance { addr, diags := addrs.ParseModuleInstanceStr(str) if diags.HasErrors() { panic(diags.Err()) } return addr } func mustAbsResourceAddr(s string) addrs.AbsResource { addr, diags := addrs.ParseAbsResourceStr(s) if diags.HasErrors() { panic(diags.Err()) } return addr }