opentofu/internal/states/state_test.go
Martin Atkins bee7403f3e command/workspace_delete: Allow deleting a workspace with empty husks
Previously we would reject attempts to delete a workspace if its state
contained any resources at all, even if none of the resources had any
resource instance objects associated with it.

Nowadays there isn't any situation where the normal Terraform workflow
will leave behind resource husks, and so this isn't as problematic as it
might've been in the v0.12 era, but nonetheless what we actually care
about for this check is whether there might be any remote objects that
this state is tracking, and for that it's more precise to look for
non-nil resource instance objects, rather than whole resources.

This also includes some adjustments to our error messaging to give more
information about the problem and to use terminology more consistent with
how we currently talk about this situation in our documentation and
elsewhere in the UI.

We were also using the old State.HasResources method as part of some of
our tests. I considered preserving it to avoid changing the behavior of
those tests, but the new check seemed close enough to the intent of those
tests that it wasn't worth maintaining this method that wouldn't be used
in any main code anymore. I've therefore updated those tests to use
the new HasResourceInstanceObjects method instead.
2021-10-13 13:54:11 -07:00

1009 lines
29 KiB
Go

package states
import (
"fmt"
"reflect"
"testing"
"github.com/go-test/deep"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/lang/marks"
)
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")
}
rootModule.SetLocalValue("foo", cty.StringVal("foo value"))
rootModule.SetOutputValue("bar", cty.StringVal("bar value"), false)
rootModule.SetOutputValue("secret", 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,
},
)
childModule := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
childModule.SetOutputValue("pizza", cty.StringVal("hawaiian"), false)
multiModA := state.EnsureModule(addrs.RootModuleInstance.Child("multi", addrs.StringKey("a")))
multiModA.SetOutputValue("pizza", cty.StringVal("cheese"), false)
multiModB := state.EnsureModule(addrs.RootModuleInstance.Child("multi", addrs.StringKey("b")))
multiModB.SetOutputValue("pizza", cty.StringVal("sausage"), false)
want := &State{
Modules: map[string]*Module{
"": {
Addr: addrs.RootModuleInstance,
LocalValues: map[string]cty.Value{
"foo": cty.StringVal("foo value"),
},
OutputValues: 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,
},
},
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),
LocalValues: map[string]cty.Value{},
OutputValues: map[string]*OutputValue{
"pizza": {
Addr: addrs.AbsOutputValue{
Module: addrs.RootModuleInstance.Child("child", addrs.NoKey),
OutputValue: addrs.OutputValue{
Name: "pizza",
},
},
Value: cty.StringVal("hawaiian"),
Sensitive: false,
},
},
Resources: map[string]*Resource{},
},
`module.multi["a"]`: {
Addr: addrs.RootModuleInstance.Child("multi", addrs.StringKey("a")),
LocalValues: map[string]cty.Value{},
OutputValues: map[string]*OutputValue{
"pizza": {
Addr: addrs.AbsOutputValue{
Module: addrs.RootModuleInstance.Child("multi", addrs.StringKey("a")),
OutputValue: addrs.OutputValue{
Name: "pizza",
},
},
Value: cty.StringVal("cheese"),
Sensitive: false,
},
},
Resources: map[string]*Resource{},
},
`module.multi["b"]`: {
Addr: addrs.RootModuleInstance.Child("multi", addrs.StringKey("b")),
LocalValues: map[string]cty.Value{},
OutputValues: map[string]*OutputValue{
"pizza": {
Addr: addrs.AbsOutputValue{
Module: addrs.RootModuleInstance.Child("multi", addrs.StringKey("b")),
OutputValue: addrs.OutputValue{
Name: "pizza",
},
},
Value: cty.StringVal("sausage"),
Sensitive: false,
},
},
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
}()
}
for _, problem := range deep.Equal(state, want) {
t.Error(problem)
}
expectedOutputs := map[string]string{
`module.multi["a"].output.pizza`: "cheese",
`module.multi["b"].output.pizza`: "sausage",
}
for _, o := range state.ModuleOutputs(addrs.RootModuleInstance, addrs.ModuleCall{Name: "multi"}) {
addr := o.Addr.String()
expected := expectedOutputs[addr]
delete(expectedOutputs, addr)
if expected != o.Value.AsString() {
t.Fatalf("expected %q:%q, got %q", addr, expected, o.Value.AsString())
}
}
for addr, o := range expectedOutputs {
t.Fatalf("missing output %q:%q", addr, o)
}
}
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.Errorf("root module is nil; want valid object")
}
rootModule.SetLocalValue("foo", cty.StringVal("foo value"))
rootModule.SetOutputValue("bar", cty.StringVal("bar value"), false)
rootModule.SetOutputValue("secret", 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,
},
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.PathValueMarks{
{
Path: cty.Path{cty.GetAttrStep{Name: "woozles"}},
Marks: cty.NewValueMarks(marks.Sensitive),
},
},
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,
},
)
childModule := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
childModule.SetOutputValue("pizza", cty.StringVal("hawaiian"), false)
stateCopy := state.DeepCopy()
if !state.Equal(stateCopy) {
t.Fatalf("\nexpected:\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()
},
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 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 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
}