From a44c8b87601908a32d6f7b967d1804a8aebd9416 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 18 Aug 2016 15:05:42 -0400 Subject: [PATCH] terraform: state mv tests --- command/state_mv_test.go | 116 ++++++++++++++++++++++++++++++++++++ terraform/state.go | 6 ++ terraform/state_add.go | 44 +++++++++++++- terraform/state_add_test.go | 79 ++++++++++++++++++++++++ 4 files changed, 244 insertions(+), 1 deletion(-) diff --git a/command/state_mv_test.go b/command/state_mv_test.go index accdb2e0f..4cca8fbda 100644 --- a/command/state_mv_test.go +++ b/command/state_mv_test.go @@ -223,6 +223,87 @@ func TestStateMv_noState(t *testing.T) { } } +func TestStateMv_stateOutNew_nestedModule(t *testing.T) { + state := &terraform.State{ + Modules: []*terraform.ModuleState{ + &terraform.ModuleState{ + Path: []string{"root"}, + Resources: map[string]*terraform.ResourceState{}, + }, + + &terraform.ModuleState{ + Path: []string{"root", "foo"}, + Resources: map[string]*terraform.ResourceState{}, + }, + + &terraform.ModuleState{ + Path: []string{"root", "foo", "child1"}, + Resources: map[string]*terraform.ResourceState{ + "test_instance.foo": &terraform.ResourceState{ + Type: "test_instance", + Primary: &terraform.InstanceState{ + ID: "bar", + Attributes: map[string]string{ + "foo": "value", + "bar": "value", + }, + }, + }, + }, + }, + + &terraform.ModuleState{ + Path: []string{"root", "foo", "child2"}, + Resources: map[string]*terraform.ResourceState{ + "test_instance.foo": &terraform.ResourceState{ + Type: "test_instance", + Primary: &terraform.InstanceState{ + ID: "bar", + Attributes: map[string]string{ + "foo": "value", + "bar": "value", + }, + }, + }, + }, + }, + }, + } + + statePath := testStateFile(t, state) + stateOutPath := statePath + ".out" + + p := testProvider() + ui := new(cli.MockUi) + c := &StateMvCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + "-state-out", stateOutPath, + "module.foo", + "module.bar", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // Test it is correct + testStateOutput(t, stateOutPath, testStateMvNestedModule_stateOut) + testStateOutput(t, statePath, testStateMvNestedModule_stateOutSrc) + + // Test we have backups + backups := testStateBackups(t, filepath.Dir(statePath)) + if len(backups) != 1 { + t.Fatalf("bad: %#v", backups) + } + testStateOutput(t, backups[0], testStateMvNestedModule_stateOutOriginal) +} + const testStateMvOutputOriginal = ` test_instance.baz: ID = foo @@ -245,6 +326,41 @@ test_instance.baz: foo = value ` +const testStateMvNestedModule_stateOut = ` +module.bar: + +module.bar.child1: + test_instance.foo: + ID = bar + bar = value + foo = value +module.bar.child2: + test_instance.foo: + ID = bar + bar = value + foo = value +` + +const testStateMvNestedModule_stateOutSrc = ` + +` + +const testStateMvNestedModule_stateOutOriginal = ` + +module.foo: + +module.foo.child1: + test_instance.foo: + ID = bar + bar = value + foo = value +module.foo.child2: + test_instance.foo: + ID = bar + bar = value + foo = value +` + const testStateMvOutput_stateOut = ` test_instance.bar: ID = bar diff --git a/terraform/state.go b/terraform/state.go index d6dc17194..05d124e06 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -808,6 +808,12 @@ func (m *ModuleState) IsRoot() bool { return reflect.DeepEqual(m.Path, rootModulePath) } +// IsDescendent returns true if other is a descendent of this module. +func (m *ModuleState) IsDescendent(other *ModuleState) bool { + i := len(m.Path) + return len(other.Path) > i && reflect.DeepEqual(other.Path[:i], m.Path) +} + // Orphans returns a list of keys of resources that are in the State // but aren't present in the configuration itself. Hence, these keys // represent the state of resources that are orphans. diff --git a/terraform/state_add.go b/terraform/state_add.go index 83643a042..34dd72e54 100644 --- a/terraform/state_add.go +++ b/terraform/state_add.go @@ -11,6 +11,11 @@ import ( // module cannot be moved to a resource address, however a resource can be // moved to a module address (it retains the same name, under that resource). // +// The item can also be a []*ModuleState, which is the case for nested +// modules. In this case, Add will expect the zero-index to be the top-most +// module to add and will only nest children from there. For semantics, this +// is equivalent to module => module. +// // The full semantics of Add: // // ┌───────────────────────┬───────────────────────┬───────────────────────┐ @@ -65,7 +70,26 @@ func (s *State) Add(fromAddrRaw string, toAddrRaw string, raw interface{}) error } func stateAddFunc_Module_Module(s *State, fromAddr, addr *ResourceAddress, raw interface{}) error { - src := raw.(*ModuleState).deepcopy() + // raw can be either *ModuleState or []*ModuleState. The former means + // we're moving just one module. The latter means we're moving a module + // and children. + root := raw + var rest []*ModuleState + if list, ok := raw.([]*ModuleState); ok { + // We need at least one item + if len(list) == 0 { + return fmt.Errorf("module move with no value to: %s", addr) + } + + // The first item is always the root + root = list[0] + if len(list) > 1 { + rest = list[1:] + } + } + + // Get the actual module state + src := root.(*ModuleState).deepcopy() // If the target module exists, it is an error path := append([]string{"root"}, addr.Path...) @@ -97,6 +121,22 @@ func stateAddFunc_Module_Module(s *State, fromAddr, addr *ResourceAddress, raw i } } + // Add all the children if we have them + for _, item := range rest { + // If item isn't a descendent of our root, then ignore it + if !src.IsDescendent(item) { + continue + } + + // It is! Strip the leading prefix and attach that to our address + extra := item.Path[len(src.Path)+1:] + addrCopy := addr.Copy() + addrCopy.Path = append(addrCopy.Path, extra) + + // Add it + s.Add(fromAddr.String(), addrCopy.String(), item) + } + return nil } @@ -227,6 +267,8 @@ func detectValueAddLoc(raw interface{}) stateAddLoc { switch raw.(type) { case *ModuleState: return stateAddModule + case []*ModuleState: + return stateAddModule case *ResourceState: return stateAddResource case *InstanceState: diff --git a/terraform/state_add_test.go b/terraform/state_add_test.go index c69e99056..0ace9bf86 100644 --- a/terraform/state_add_test.go +++ b/terraform/state_add_test.go @@ -194,6 +194,85 @@ func TestStateAdd(t *testing.T) { nil, }, + "ModuleState with children => Module Addr (new)": { + false, + "module.foo", + "module.bar", + + []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{}, + }, + + &ModuleState{ + Path: []string{"root", "foo", "child1"}, + Resources: map[string]*ResourceState{ + "test_instance.foo": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + + &ModuleState{ + Path: []string{"root", "foo", "child2"}, + Resources: map[string]*ResourceState{ + "test_instance.foo": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + + // Should be ignored + &ModuleState{ + Path: []string{"root", "bar", "child2"}, + Resources: map[string]*ResourceState{ + "test_instance.foo": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + + &State{}, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: []string{"root", "bar", "child1"}, + Resources: map[string]*ResourceState{ + "test_instance.foo": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + + &ModuleState{ + Path: []string{"root", "bar", "child2"}, + Resources: map[string]*ResourceState{ + "test_instance.foo": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + }, + "ResourceState => Resource Addr (new)": { false, "aws_instance.bar",