From 407be65cc8e520415744ceaa8a0181018199130b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 11 Apr 2016 17:03:25 -0700 Subject: [PATCH 01/23] terraform: ResourceAddress should output instance type if set --- terraform/resource_address.go | 14 +++++++++----- terraform/resource_address_test.go | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/terraform/resource_address.go b/terraform/resource_address.go index d6bb0522e..90f0eafd3 100644 --- a/terraform/resource_address.go +++ b/terraform/resource_address.go @@ -52,11 +52,15 @@ func (r *ResourceAddress) String() string { if r.Name != "" { name := r.Name - switch r.InstanceType { - case TypeDeposed: - name += ".deposed" - case TypeTainted: - name += ".tainted" + if r.InstanceTypeSet { + switch r.InstanceType { + case TypePrimary: + name += ".primary" + case TypeDeposed: + name += ".deposed" + case TypeTainted: + name += ".tainted" + } } if r.Index >= 0 { diff --git a/terraform/resource_address_test.go b/terraform/resource_address_test.go index 193c56c44..17dc92367 100644 --- a/terraform/resource_address_test.go +++ b/terraform/resource_address_test.go @@ -50,7 +50,7 @@ func TestParseResourceAddress(t *testing.T) { InstanceTypeSet: true, Index: 2, }, - "aws_instance.foo[2]", + "", }, "tainted": { "aws_instance.foo.tainted", From 25098f20c95c3a218506bbcda23ea7abca95e175 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 11 Apr 2016 17:20:37 -0700 Subject: [PATCH 02/23] terraform: State.Add works for module to module (new) --- terraform/state.go | 8 +- terraform/state_add.go | 204 ++++++++++++++++++++++++++++++++++++++++ terraform/state_test.go | 75 +++++++++++++++ 3 files changed, 281 insertions(+), 6 deletions(-) create mode 100644 terraform/state_add.go diff --git a/terraform/state.go b/terraform/state.go index fd853adf1..012221634 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -417,12 +417,8 @@ func (s *State) init() { if s.Version == 0 { s.Version = StateVersion } - if len(s.Modules) == 0 { - root := &ModuleState{ - Path: rootModulePath, - } - root.init() - s.Modules = []*ModuleState{root} + if s.ModuleByPath(rootModulePath) == nil { + s.AddModule(rootModulePath) } } diff --git a/terraform/state_add.go b/terraform/state_add.go new file mode 100644 index 000000000..ee6e266ba --- /dev/null +++ b/terraform/state_add.go @@ -0,0 +1,204 @@ +package terraform + +import ( + "fmt" +) + +// Add adds the item in the state at the given address. +// +// The item can be a ModuleState, ResourceState, or InstanceState. Depending +// on the item type, the address may or may not be valid. For example, a +// 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 full semantics of Add: +// +// ┌───────────────────────┬───────────────────────┬───────────────────────┐ +// │ Module Address │ Resource Address │ Instance Address │ +// ┌───────────────────────┼───────────────────────┼───────────────────────┼───────────────────────┤ +// │ ModuleState │ ✓ │ x │ x │ +// ├───────────────────────┼───────────────────────┼───────────────────────┼───────────────────────┤ +// │ ResourceState │ ✓ │ ✓ │ maybe* │ +// ├───────────────────────┼───────────────────────┼───────────────────────┼───────────────────────┤ +// │ Instance State │ ✓ │ ✓ │ ✓ │ +// └───────────────────────┴───────────────────────┴───────────────────────┴───────────────────────┘ +// +// *maybe - Resources can be added at an instance address only if the resource +// represents a single instance (primary). Example: +// "aws_instance.foo" can be moved to "aws_instance.bar.tainted" +// +func (s *State) Add(addrRaw string, raw interface{}) error { + // Parse the address + addr, err := ParseResourceAddress(addrRaw) + if err != nil { + return err + } + + // Determine the types + from := detectValueAddLoc(raw) + to := detectAddrAddLoc(addr) + + // Find the function to do this + fromMap, ok := stateAddFuncs[from] + if !ok { + return fmt.Errorf("invalid source to add to state: %T", raw) + } + f, ok := fromMap[to] + if !ok { + return fmt.Errorf("invalid destination: %s (%d)", addr, to) + } + + // Call the migrator + if err := f(s, addr, raw); err != nil { + return err + } + + // Prune the state + s.prune() + return nil +} + +func stateAddFunc_Module_Module(s *State, addr *ResourceAddress, raw interface{}) error { + src := raw.(*ModuleState) + + // TODO: outputs + // TODO: dependencies + + // Go through the resources perform an add for each of those + for k, v := range src.Resources { + resourceKey, err := ParseResourceStateKey(k) + if err != nil { + return err + } + + // Update the resource address for this + addrCopy := *addr + addrCopy.Type = resourceKey.Type + addrCopy.Name = resourceKey.Name + addrCopy.Index = resourceKey.Index + + // Perform an add + if err := s.Add(addrCopy.String(), v); err != nil { + return err + } + } + + return nil +} + +func stateAddFunc_Resource_Resource(s *State, addr *ResourceAddress, raw interface{}) error { + src := raw.(*ResourceState) + + // TODO: Dependencies + // TODO: Provider? + + // Move the primary + if src.Primary != nil { + addrCopy := *addr + addrCopy.InstanceType = TypePrimary + addrCopy.InstanceTypeSet = true + if err := s.Add(addrCopy.String(), src.Primary); err != nil { + return err + } + } + + // TODO: Move all tainted + // TODO: Move all deposed + + return nil +} + +func stateAddFunc_Instance_Instance(s *State, addr *ResourceAddress, raw interface{}) error { + src := raw.(*InstanceState).deepcopy() + + // Create the module up to this point + path := append([]string{"root"}, addr.Path...) + mod := s.ModuleByPath(path) + if mod == nil { + mod = s.AddModule(path) + } + + // Create the resources + resourceKey := (&ResourceStateKey{ + Name: addr.Name, + Type: addr.Type, + Index: addr.Index, + }).String() + resource, ok := mod.Resources[resourceKey] + if !ok { + resource = &ResourceState{Type: addr.Type} + resource.init() + mod.Resources[resourceKey] = resource + } + + // Depending on the instance type, set it + switch addr.InstanceType { + case TypePrimary: + resource.Primary = src + default: + return fmt.Errorf("can't move instance state to %s", addr.InstanceType) + } + + return nil +} + +// stateAddFunc is the type of function for adding an item to a state +type stateAddFunc func(s *State, addr *ResourceAddress, item interface{}) error + +// stateAddFuncs has the full matrix mapping of the state adders. +var stateAddFuncs map[stateAddLoc]map[stateAddLoc]stateAddFunc + +func init() { + stateAddFuncs = map[stateAddLoc]map[stateAddLoc]stateAddFunc{ + stateAddModule: { + stateAddModule: stateAddFunc_Module_Module, + }, + stateAddResource: { + stateAddResource: stateAddFunc_Resource_Resource, + }, + stateAddInstance: { + stateAddInstance: stateAddFunc_Instance_Instance, + }, + } +} + +// stateAddLoc is an enum to represent the location where state is being +// moved from/to. We use this for quick lookups in a function map. +type stateAddLoc uint + +const ( + stateAddInvalid stateAddLoc = iota + stateAddModule + stateAddResource + stateAddInstance +) + +// detectAddrAddLoc detects the state type for the given address. This +// function is specifically not unit tested since we consider the State.Add +// functionality to be comprehensive enough to cover this. +func detectAddrAddLoc(addr *ResourceAddress) stateAddLoc { + if addr.Name == "" { + return stateAddModule + } + + if !addr.InstanceTypeSet { + return stateAddResource + } + + return stateAddInstance +} + +// detectValueAddLoc determines the stateAddLoc value from the raw value +// that is some State structure. +func detectValueAddLoc(raw interface{}) stateAddLoc { + switch raw.(type) { + case *ModuleState: + return stateAddModule + case *ResourceState: + return stateAddResource + case *InstanceState: + return stateAddInstance + default: + return stateAddInvalid + } +} diff --git a/terraform/state_test.go b/terraform/state_test.go index a51b670b7..2ef0906b2 100644 --- a/terraform/state_test.go +++ b/terraform/state_test.go @@ -424,6 +424,81 @@ func TestStateIncrementSerialMaybe(t *testing.T) { } } +func TestStateAdd(t *testing.T) { + cases := map[string]struct { + Address string + Value interface{} + One, Two *State + }{ + "ModuleState => Module Addr (new)": { + "module.foo", + &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{}, + &State{ + Modules: []*ModuleState{ + &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", + }, + }, + }, + }, + }, + }, + }, + } + + for k, tc := range cases { + // Make sure they're both initialized as normal + tc.One.init() + tc.Two.init() + + // Add the value + if err := tc.One.Add(tc.Address, tc.Value); err != nil { + t.Fatalf("bad: %s\n\n%s", k, err) + } + + // Prune them both to be sure + tc.One.prune() + tc.Two.prune() + + // Verify equality + if !tc.One.Equal(tc.Two) { + t.Fatalf("Bad: %s\n\n%#v\n\n%#v", k, tc.One, tc.Two) + //t.Fatalf("Bad: %s\n\n%s\n\n%s", k, tc.One.String(), tc.Two.String()) + } + } +} + func TestStateRemove(t *testing.T) { cases := map[string]struct { Address string From bbc812d0353c96a8d72f232b320919f33242df5b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 11 Apr 2016 17:24:42 -0700 Subject: [PATCH 03/23] terraform: fix failing tests --- terraform/state_filter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/state_filter.go b/terraform/state_filter.go index 43cca6bc9..7a2937123 100644 --- a/terraform/state_filter.go +++ b/terraform/state_filter.go @@ -124,7 +124,7 @@ func (f *StateFilter) filterSingle(a *ResourceAddress) []*StateFilterResult { // Add the instances if r.Primary != nil { addr.InstanceType = TypePrimary - addr.InstanceTypeSet = true + addr.InstanceTypeSet = false results = append(results, &StateFilterResult{ Path: addr.Path, Address: addr.String(), From 30cf550fc5f55457dc9e9446a43a7460be470497 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 11 Apr 2016 17:40:23 -0700 Subject: [PATCH 04/23] terraform: can't move module to module that exists --- terraform/state_add.go | 6 ++++++ terraform/state_test.go | 38 ++++++++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/terraform/state_add.go b/terraform/state_add.go index ee6e266ba..9a485c0f8 100644 --- a/terraform/state_add.go +++ b/terraform/state_add.go @@ -61,6 +61,12 @@ func (s *State) Add(addrRaw string, raw interface{}) error { func stateAddFunc_Module_Module(s *State, addr *ResourceAddress, raw interface{}) error { src := raw.(*ModuleState) + // If the target module exists, it is an error + path := append([]string{"root"}, addr.Path...) + if s.ModuleByPath(path) != nil { + return fmt.Errorf("module target is not empty: %s", addr) + } + // TODO: outputs // TODO: dependencies diff --git a/terraform/state_test.go b/terraform/state_test.go index 2ef0906b2..d9a53b69f 100644 --- a/terraform/state_test.go +++ b/terraform/state_test.go @@ -426,11 +426,13 @@ func TestStateIncrementSerialMaybe(t *testing.T) { func TestStateAdd(t *testing.T) { cases := map[string]struct { + Err bool Address string Value interface{} One, Two *State }{ "ModuleState => Module Addr (new)": { + false, "module.foo", &ModuleState{ Path: rootModulePath, @@ -475,17 +477,45 @@ func TestStateAdd(t *testing.T) { }, }, }, + + "ModuleState => Module Addr (existing)": { + true, + "module.foo", + &ModuleState{}, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: []string{"root", "foo"}, + Resources: map[string]*ResourceState{ + "test_instance.baz": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + nil, + }, } for k, tc := range cases { // Make sure they're both initialized as normal tc.One.init() - tc.Two.init() + if tc.Two != nil { + tc.Two.init() + } // Add the value - if err := tc.One.Add(tc.Address, tc.Value); err != nil { + err := tc.One.Add(tc.Address, tc.Value) + if (err != nil) != tc.Err { t.Fatalf("bad: %s\n\n%s", k, err) } + if tc.Err { + continue + } // Prune them both to be sure tc.One.prune() @@ -493,8 +523,8 @@ func TestStateAdd(t *testing.T) { // Verify equality if !tc.One.Equal(tc.Two) { - t.Fatalf("Bad: %s\n\n%#v\n\n%#v", k, tc.One, tc.Two) - //t.Fatalf("Bad: %s\n\n%s\n\n%s", k, tc.One.String(), tc.Two.String()) + //t.Fatalf("Bad: %s\n\n%#v\n\n%#v", k, tc.One, tc.Two) + t.Fatalf("Bad: %s\n\n%s\n\n%s", k, tc.One.String(), tc.Two.String()) } } } From af3c3e4c602a39fdea16df9ba37e6c3792518a71 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 11 Apr 2016 18:05:55 -0700 Subject: [PATCH 05/23] terraform: Module copy copies outputs and dependencies --- terraform/state.go | 8 +++--- terraform/state_add.go | 8 +++--- terraform/state_test.go | 55 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 6 deletions(-) diff --git a/terraform/state.go b/terraform/state.go index 012221634..d2c61ac39 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -673,11 +673,13 @@ func (m *ModuleState) deepcopy() *ModuleState { return nil } n := &ModuleState{ - Path: make([]string, len(m.Path)), - Outputs: make(map[string]interface{}, len(m.Outputs)), - Resources: make(map[string]*ResourceState, len(m.Resources)), + Path: make([]string, len(m.Path)), + Outputs: make(map[string]interface{}, len(m.Outputs)), + Resources: make(map[string]*ResourceState, len(m.Resources)), + Dependencies: make([]string, len(m.Dependencies)), } copy(n.Path, m.Path) + copy(n.Dependencies, m.Dependencies) for k, v := range m.Outputs { n.Outputs[k] = v } diff --git a/terraform/state_add.go b/terraform/state_add.go index 9a485c0f8..74fa62255 100644 --- a/terraform/state_add.go +++ b/terraform/state_add.go @@ -59,7 +59,7 @@ func (s *State) Add(addrRaw string, raw interface{}) error { } func stateAddFunc_Module_Module(s *State, addr *ResourceAddress, raw interface{}) error { - src := raw.(*ModuleState) + src := raw.(*ModuleState).deepcopy() // If the target module exists, it is an error path := append([]string{"root"}, addr.Path...) @@ -67,8 +67,10 @@ func stateAddFunc_Module_Module(s *State, addr *ResourceAddress, raw interface{} return fmt.Errorf("module target is not empty: %s", addr) } - // TODO: outputs - // TODO: dependencies + // Create it and copy our outputs and dependencies + mod := s.AddModule(path) + mod.Outputs = src.Outputs + mod.Dependencies = src.Dependencies // Go through the resources perform an add for each of those for k, v := range src.Resources { diff --git a/terraform/state_test.go b/terraform/state_test.go index d9a53b69f..fdfd7a90a 100644 --- a/terraform/state_test.go +++ b/terraform/state_test.go @@ -478,6 +478,61 @@ func TestStateAdd(t *testing.T) { }, }, + "ModuleState w/ outputs and deps => Module Addr (new)": { + false, + "module.foo", + &ModuleState{ + Path: rootModulePath, + Outputs: map[string]interface{}{ + "foo": "bar", + }, + Dependencies: []string{"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{}, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: []string{"root", "foo"}, + Outputs: map[string]interface{}{ + "foo": "bar", + }, + Dependencies: []string{"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 => Module Addr (existing)": { true, "module.foo", From 2de43246076448aaf707f88127bdecb2729c8d8a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 11 Apr 2016 18:09:59 -0700 Subject: [PATCH 06/23] terraform: add stateadd to its own test file --- terraform/state_add_test.go | 165 ++++++++++++++++++++++++++++++++++++ terraform/state_test.go | 160 ---------------------------------- 2 files changed, 165 insertions(+), 160 deletions(-) create mode 100644 terraform/state_add_test.go diff --git a/terraform/state_add_test.go b/terraform/state_add_test.go new file mode 100644 index 000000000..50596af1f --- /dev/null +++ b/terraform/state_add_test.go @@ -0,0 +1,165 @@ +package terraform + +import ( + "testing" +) + +func TestStateAdd(t *testing.T) { + cases := map[string]struct { + Err bool + Address string + Value interface{} + One, Two *State + }{ + "ModuleState => Module Addr (new)": { + false, + "module.foo", + &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{}, + &State{ + Modules: []*ModuleState{ + &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 w/ outputs and deps => Module Addr (new)": { + false, + "module.foo", + &ModuleState{ + Path: rootModulePath, + Outputs: map[string]interface{}{ + "foo": "bar", + }, + Dependencies: []string{"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{}, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: []string{"root", "foo"}, + Outputs: map[string]interface{}{ + "foo": "bar", + }, + Dependencies: []string{"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 => Module Addr (existing)": { + true, + "module.foo", + &ModuleState{}, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: []string{"root", "foo"}, + Resources: map[string]*ResourceState{ + "test_instance.baz": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + nil, + }, + } + + for k, tc := range cases { + // Make sure they're both initialized as normal + tc.One.init() + if tc.Two != nil { + tc.Two.init() + } + + // Add the value + err := tc.One.Add(tc.Address, tc.Value) + if (err != nil) != tc.Err { + t.Fatalf("bad: %s\n\n%s", k, err) + } + if tc.Err { + continue + } + + // Prune them both to be sure + tc.One.prune() + tc.Two.prune() + + // Verify equality + if !tc.One.Equal(tc.Two) { + //t.Fatalf("Bad: %s\n\n%#v\n\n%#v", k, tc.One, tc.Two) + t.Fatalf("Bad: %s\n\n%s\n\n%s", k, tc.One.String(), tc.Two.String()) + } + } +} diff --git a/terraform/state_test.go b/terraform/state_test.go index fdfd7a90a..a51b670b7 100644 --- a/terraform/state_test.go +++ b/terraform/state_test.go @@ -424,166 +424,6 @@ func TestStateIncrementSerialMaybe(t *testing.T) { } } -func TestStateAdd(t *testing.T) { - cases := map[string]struct { - Err bool - Address string - Value interface{} - One, Two *State - }{ - "ModuleState => Module Addr (new)": { - false, - "module.foo", - &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{}, - &State{ - Modules: []*ModuleState{ - &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 w/ outputs and deps => Module Addr (new)": { - false, - "module.foo", - &ModuleState{ - Path: rootModulePath, - Outputs: map[string]interface{}{ - "foo": "bar", - }, - Dependencies: []string{"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{}, - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root", "foo"}, - Outputs: map[string]interface{}{ - "foo": "bar", - }, - Dependencies: []string{"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 => Module Addr (existing)": { - true, - "module.foo", - &ModuleState{}, - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root", "foo"}, - Resources: map[string]*ResourceState{ - "test_instance.baz": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - }, - nil, - }, - } - - for k, tc := range cases { - // Make sure they're both initialized as normal - tc.One.init() - if tc.Two != nil { - tc.Two.init() - } - - // Add the value - err := tc.One.Add(tc.Address, tc.Value) - if (err != nil) != tc.Err { - t.Fatalf("bad: %s\n\n%s", k, err) - } - if tc.Err { - continue - } - - // Prune them both to be sure - tc.One.prune() - tc.Two.prune() - - // Verify equality - if !tc.One.Equal(tc.Two) { - //t.Fatalf("Bad: %s\n\n%#v\n\n%#v", k, tc.One, tc.Two) - t.Fatalf("Bad: %s\n\n%s\n\n%s", k, tc.One.String(), tc.Two.String()) - } - } -} - func TestStateRemove(t *testing.T) { cases := map[string]struct { Address string From 21d7ffc3f31b9987ca3c3d2e10c58de4b54d7390 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 11 Apr 2016 18:22:11 -0700 Subject: [PATCH 07/23] terraform: add resource --- terraform/state_add.go | 65 +++++++++++++++++++++++++++++++++++++ terraform/state_add_test.go | 30 ++++++++++++++++- 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/terraform/state_add.go b/terraform/state_add.go index 74fa62255..b837e8f57 100644 --- a/terraform/state_add.go +++ b/terraform/state_add.go @@ -97,6 +97,10 @@ func stateAddFunc_Module_Module(s *State, addr *ResourceAddress, raw interface{} func stateAddFunc_Resource_Resource(s *State, addr *ResourceAddress, raw interface{}) error { src := raw.(*ResourceState) + // Initialize the resource + resource := stateAddInitAddr(s, addr).(*ResourceState) + resource.Type = src.Type + // TODO: Dependencies // TODO: Provider? @@ -210,3 +214,64 @@ func detectValueAddLoc(raw interface{}) stateAddLoc { return stateAddInvalid } } + +// stateAddInitAddr takes a ResourceAddress and creates the non-existing +// resources up to that point, returning the empty (or existing) interface +// at that address. +func stateAddInitAddr(s *State, addr *ResourceAddress) interface{} { + addType := detectAddrAddLoc(addr) + + // Get the module + path := append([]string{"root"}, addr.Path...) + mod := s.ModuleByPath(path) + if mod == nil { + mod = s.AddModule(path) + } + if addType == stateAddModule { + return mod + } + + // Add the resource + resourceKey := (&ResourceStateKey{ + Name: addr.Name, + Type: addr.Type, + Index: addr.Index, + }).String() + resource, ok := mod.Resources[resourceKey] + if !ok { + resource = &ResourceState{Type: addr.Type} + resource.init() + mod.Resources[resourceKey] = resource + } + if addType == stateAddResource { + return resource + } + + // Get the instance + var instance *InstanceState + switch addr.InstanceType { + case TypePrimary: + instance = resource.Primary + case TypeTainted: + idx := addr.Index + if addr.Index < 0 { + idx = 0 + } + if len(resource.Tainted) > idx { + instance = resource.Tainted[idx] + } + case TypeDeposed: + idx := addr.Index + if addr.Index < 0 { + idx = 0 + } + if len(resource.Deposed) > idx { + instance = resource.Deposed[idx] + } + } + if instance == nil { + instance = &InstanceState{} + } + + return instance +} diff --git a/terraform/state_add_test.go b/terraform/state_add_test.go index 50596af1f..9cd54457e 100644 --- a/terraform/state_add_test.go +++ b/terraform/state_add_test.go @@ -134,6 +134,34 @@ func TestStateAdd(t *testing.T) { }, nil, }, + + "ResourceState => Resource Addr (new)": { + false, + "aws_instance.foo", + &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + + &State{}, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: []string{"root"}, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + }, } for k, tc := range cases { @@ -158,7 +186,7 @@ func TestStateAdd(t *testing.T) { // Verify equality if !tc.One.Equal(tc.Two) { - //t.Fatalf("Bad: %s\n\n%#v\n\n%#v", k, tc.One, tc.Two) + // t.Fatalf("Bad: %s\n\n%#v\n\n%#v", k, tc.One, tc.Two) t.Fatalf("Bad: %s\n\n%s\n\n%s", k, tc.One.String(), tc.Two.String()) } } From c3240626450230943367680d36b3fbfbc5042706 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 11 Apr 2016 18:26:15 -0700 Subject: [PATCH 08/23] terraform: state add resource existing fails --- terraform/state_add.go | 20 +++++++++++++++----- terraform/state_add_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/terraform/state_add.go b/terraform/state_add.go index b837e8f57..445ba2ab8 100644 --- a/terraform/state_add.go +++ b/terraform/state_add.go @@ -98,7 +98,11 @@ func stateAddFunc_Resource_Resource(s *State, addr *ResourceAddress, raw interfa src := raw.(*ResourceState) // Initialize the resource - resource := stateAddInitAddr(s, addr).(*ResourceState) + resourceRaw, exists := stateAddInitAddr(s, addr) + if exists { + return fmt.Errorf("resource exists and not empty: %s", addr) + } + resource := resourceRaw.(*ResourceState) resource.Type = src.Type // TODO: Dependencies @@ -218,17 +222,19 @@ func detectValueAddLoc(raw interface{}) stateAddLoc { // stateAddInitAddr takes a ResourceAddress and creates the non-existing // resources up to that point, returning the empty (or existing) interface // at that address. -func stateAddInitAddr(s *State, addr *ResourceAddress) interface{} { +func stateAddInitAddr(s *State, addr *ResourceAddress) (interface{}, bool) { addType := detectAddrAddLoc(addr) // Get the module path := append([]string{"root"}, addr.Path...) + exists := true mod := s.ModuleByPath(path) if mod == nil { mod = s.AddModule(path) + exists = false } if addType == stateAddModule { - return mod + return mod, exists } // Add the resource @@ -237,17 +243,20 @@ func stateAddInitAddr(s *State, addr *ResourceAddress) interface{} { Type: addr.Type, Index: addr.Index, }).String() + exists = true resource, ok := mod.Resources[resourceKey] if !ok { resource = &ResourceState{Type: addr.Type} resource.init() mod.Resources[resourceKey] = resource + exists = false } if addType == stateAddResource { - return resource + return resource, exists } // Get the instance + exists = true var instance *InstanceState switch addr.InstanceType { case TypePrimary: @@ -271,7 +280,8 @@ func stateAddInitAddr(s *State, addr *ResourceAddress) interface{} { } if instance == nil { instance = &InstanceState{} + exists = false } - return instance + return instance, exists } diff --git a/terraform/state_add_test.go b/terraform/state_add_test.go index 9cd54457e..54944258f 100644 --- a/terraform/state_add_test.go +++ b/terraform/state_add_test.go @@ -162,6 +162,34 @@ func TestStateAdd(t *testing.T) { }, }, }, + + "ResourceState => Resource Addr (existing)": { + true, + "aws_instance.foo", + &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: []string{"root"}, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + nil, + }, } for k, tc := range cases { From cb060f906328101c4440cc92d97652f05e1dd163 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 11 Apr 2016 18:28:36 -0700 Subject: [PATCH 09/23] terraform: unify on init addr --- terraform/state_add.go | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/terraform/state_add.go b/terraform/state_add.go index 445ba2ab8..21a713e5d 100644 --- a/terraform/state_add.go +++ b/terraform/state_add.go @@ -127,30 +127,14 @@ func stateAddFunc_Resource_Resource(s *State, addr *ResourceAddress, raw interfa func stateAddFunc_Instance_Instance(s *State, addr *ResourceAddress, raw interface{}) error { src := raw.(*InstanceState).deepcopy() - // Create the module up to this point - path := append([]string{"root"}, addr.Path...) - mod := s.ModuleByPath(path) - if mod == nil { - mod = s.AddModule(path) - } - - // Create the resources - resourceKey := (&ResourceStateKey{ - Name: addr.Name, - Type: addr.Type, - Index: addr.Index, - }).String() - resource, ok := mod.Resources[resourceKey] - if !ok { - resource = &ResourceState{Type: addr.Type} - resource.init() - mod.Resources[resourceKey] = resource - } + // Create the instance + instanceRaw, _ := stateAddInitAddr(s, addr) + instance := instanceRaw.(*InstanceState) // Depending on the instance type, set it switch addr.InstanceType { case TypePrimary: - resource.Primary = src + *instance = *src default: return fmt.Errorf("can't move instance state to %s", addr.InstanceType) } From e65a7269361849ecdcd59d512352abb065d4871c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 11 Apr 2016 18:29:51 -0700 Subject: [PATCH 10/23] terraform: copy deps and provider for resource state --- terraform/state_add.go | 7 +++---- terraform/state_add_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/terraform/state_add.go b/terraform/state_add.go index 21a713e5d..4c6e0992f 100644 --- a/terraform/state_add.go +++ b/terraform/state_add.go @@ -95,7 +95,7 @@ func stateAddFunc_Module_Module(s *State, addr *ResourceAddress, raw interface{} } func stateAddFunc_Resource_Resource(s *State, addr *ResourceAddress, raw interface{}) error { - src := raw.(*ResourceState) + src := raw.(*ResourceState).deepcopy() // Initialize the resource resourceRaw, exists := stateAddInitAddr(s, addr) @@ -104,9 +104,8 @@ func stateAddFunc_Resource_Resource(s *State, addr *ResourceAddress, raw interfa } resource := resourceRaw.(*ResourceState) resource.Type = src.Type - - // TODO: Dependencies - // TODO: Provider? + resource.Dependencies = src.Dependencies + resource.Provider = src.Provider // Move the primary if src.Primary != nil { diff --git a/terraform/state_add_test.go b/terraform/state_add_test.go index 54944258f..f192c1ace 100644 --- a/terraform/state_add_test.go +++ b/terraform/state_add_test.go @@ -163,6 +163,38 @@ func TestStateAdd(t *testing.T) { }, }, + "ResourceState w/ deps, provider => Resource Addr (new)": { + false, + "aws_instance.foo", + &ResourceState{ + Type: "test_instance", + Provider: "foo", + Dependencies: []string{"bar"}, + Primary: &InstanceState{ + ID: "foo", + }, + }, + + &State{}, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: []string{"root"}, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "test_instance", + Provider: "foo", + Dependencies: []string{"bar"}, + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + }, + "ResourceState => Resource Addr (existing)": { true, "aws_instance.foo", From d3fcfcc027bd050469e95cf7f19e7d4c2dbba0ad Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 11 Apr 2016 18:41:48 -0700 Subject: [PATCH 11/23] terraform: moving resource to resource --- terraform/state_add.go | 38 ++++++++++++++++++++++--------------- terraform/state_add_test.go | 37 ++++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/terraform/state_add.go b/terraform/state_add.go index 4c6e0992f..9adf0df0c 100644 --- a/terraform/state_add.go +++ b/terraform/state_add.go @@ -117,8 +117,15 @@ func stateAddFunc_Resource_Resource(s *State, addr *ResourceAddress, raw interfa } } - // TODO: Move all tainted - // TODO: Move all deposed + // Move all tainted + if len(src.Tainted) > 0 { + resource.Tainted = src.Tainted + } + + // Move all deposed + if len(src.Deposed) > 0 { + resource.Deposed = src.Deposed + } return nil } @@ -130,13 +137,8 @@ func stateAddFunc_Instance_Instance(s *State, addr *ResourceAddress, raw interfa instanceRaw, _ := stateAddInitAddr(s, addr) instance := instanceRaw.(*InstanceState) - // Depending on the instance type, set it - switch addr.InstanceType { - case TypePrimary: - *instance = *src - default: - return fmt.Errorf("can't move instance state to %s", addr.InstanceType) - } + // Set it + *instance = *src return nil } @@ -240,10 +242,14 @@ func stateAddInitAddr(s *State, addr *ResourceAddress) (interface{}, bool) { // Get the instance exists = true - var instance *InstanceState + instance := &InstanceState{} switch addr.InstanceType { case TypePrimary: - instance = resource.Primary + if v := resource.Primary; v != nil { + instance = resource.Primary + } else { + exists = false + } case TypeTainted: idx := addr.Index if addr.Index < 0 { @@ -251,6 +257,9 @@ func stateAddInitAddr(s *State, addr *ResourceAddress) (interface{}, bool) { } if len(resource.Tainted) > idx { instance = resource.Tainted[idx] + } else { + resource.Tainted = append(resource.Tainted, instance) + exists = false } case TypeDeposed: idx := addr.Index @@ -259,12 +268,11 @@ func stateAddInitAddr(s *State, addr *ResourceAddress) (interface{}, bool) { } if len(resource.Deposed) > idx { instance = resource.Deposed[idx] + } else { + resource.Deposed = append(resource.Deposed, instance) + exists = false } } - if instance == nil { - instance = &InstanceState{} - exists = false - } return instance, exists } diff --git a/terraform/state_add_test.go b/terraform/state_add_test.go index f192c1ace..8b9a9678b 100644 --- a/terraform/state_add_test.go +++ b/terraform/state_add_test.go @@ -195,6 +195,39 @@ func TestStateAdd(t *testing.T) { }, }, + "ResourceState w/ tainted => Resource Addr (new)": { + false, + "aws_instance.foo", + &ResourceState{ + Type: "test_instance", + Tainted: []*InstanceState{ + &InstanceState{ + ID: "foo", + }, + }, + }, + + &State{}, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: []string{"root"}, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{}, + Tainted: []*InstanceState{ + &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + }, + }, + "ResourceState => Resource Addr (existing)": { true, "aws_instance.foo", @@ -246,8 +279,8 @@ func TestStateAdd(t *testing.T) { // Verify equality if !tc.One.Equal(tc.Two) { - // t.Fatalf("Bad: %s\n\n%#v\n\n%#v", k, tc.One, tc.Two) - t.Fatalf("Bad: %s\n\n%s\n\n%s", k, tc.One.String(), tc.Two.String()) + t.Fatalf("Bad: %s\n\n%#v\n\n%#v", k, tc.One, tc.Two) + //t.Fatalf("Bad: %s\n\n%s\n\n%s", k, tc.One.String(), tc.Two.String()) } } } From e497c265172eba84b79e55a8021ed1a0abd1f2d5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 12 Apr 2016 09:46:16 -0700 Subject: [PATCH 12/23] terraform: resource => module --- terraform/state_add.go | 39 ++++++++++++++++++++++++++---------- terraform/state_add_test.go | 40 +++++++++++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/terraform/state_add.go b/terraform/state_add.go index 9adf0df0c..79a67ced9 100644 --- a/terraform/state_add.go +++ b/terraform/state_add.go @@ -27,16 +27,22 @@ import ( // represents a single instance (primary). Example: // "aws_instance.foo" can be moved to "aws_instance.bar.tainted" // -func (s *State) Add(addrRaw string, raw interface{}) error { +func (s *State) Add(fromAddrRaw string, toAddrRaw string, raw interface{}) error { // Parse the address - addr, err := ParseResourceAddress(addrRaw) + toAddr, err := ParseResourceAddress(toAddrRaw) + if err != nil { + return err + } + + // Parse the from address + fromAddr, err := ParseResourceAddress(fromAddrRaw) if err != nil { return err } // Determine the types from := detectValueAddLoc(raw) - to := detectAddrAddLoc(addr) + to := detectAddrAddLoc(toAddr) // Find the function to do this fromMap, ok := stateAddFuncs[from] @@ -45,11 +51,11 @@ func (s *State) Add(addrRaw string, raw interface{}) error { } f, ok := fromMap[to] if !ok { - return fmt.Errorf("invalid destination: %s (%d)", addr, to) + return fmt.Errorf("invalid destination: %s (%d)", toAddr, to) } // Call the migrator - if err := f(s, addr, raw); err != nil { + if err := f(s, fromAddr, toAddr, raw); err != nil { return err } @@ -58,7 +64,7 @@ func (s *State) Add(addrRaw string, raw interface{}) error { return nil } -func stateAddFunc_Module_Module(s *State, addr *ResourceAddress, raw interface{}) error { +func stateAddFunc_Module_Module(s *State, fromAddr, addr *ResourceAddress, raw interface{}) error { src := raw.(*ModuleState).deepcopy() // If the target module exists, it is an error @@ -86,7 +92,7 @@ func stateAddFunc_Module_Module(s *State, addr *ResourceAddress, raw interface{} addrCopy.Index = resourceKey.Index // Perform an add - if err := s.Add(addrCopy.String(), v); err != nil { + if err := s.Add(fromAddr.String(), addrCopy.String(), v); err != nil { return err } } @@ -94,7 +100,17 @@ func stateAddFunc_Module_Module(s *State, addr *ResourceAddress, raw interface{} return nil } -func stateAddFunc_Resource_Resource(s *State, addr *ResourceAddress, raw interface{}) error { +func stateAddFunc_Resource_Module( + s *State, from, to *ResourceAddress, raw interface{}) error { + // Build the more specific to addr + addr := *to + addr.Type = from.Type + addr.Name = from.Name + + return s.Add(from.String(), addr.String(), raw) +} + +func stateAddFunc_Resource_Resource(s *State, fromAddr, addr *ResourceAddress, raw interface{}) error { src := raw.(*ResourceState).deepcopy() // Initialize the resource @@ -112,7 +128,7 @@ func stateAddFunc_Resource_Resource(s *State, addr *ResourceAddress, raw interfa addrCopy := *addr addrCopy.InstanceType = TypePrimary addrCopy.InstanceTypeSet = true - if err := s.Add(addrCopy.String(), src.Primary); err != nil { + if err := s.Add(fromAddr.String(), addrCopy.String(), src.Primary); err != nil { return err } } @@ -130,7 +146,7 @@ func stateAddFunc_Resource_Resource(s *State, addr *ResourceAddress, raw interfa return nil } -func stateAddFunc_Instance_Instance(s *State, addr *ResourceAddress, raw interface{}) error { +func stateAddFunc_Instance_Instance(s *State, fromAddr, addr *ResourceAddress, raw interface{}) error { src := raw.(*InstanceState).deepcopy() // Create the instance @@ -144,7 +160,7 @@ func stateAddFunc_Instance_Instance(s *State, addr *ResourceAddress, raw interfa } // stateAddFunc is the type of function for adding an item to a state -type stateAddFunc func(s *State, addr *ResourceAddress, item interface{}) error +type stateAddFunc func(s *State, from, to *ResourceAddress, item interface{}) error // stateAddFuncs has the full matrix mapping of the state adders. var stateAddFuncs map[stateAddLoc]map[stateAddLoc]stateAddFunc @@ -155,6 +171,7 @@ func init() { stateAddModule: stateAddFunc_Module_Module, }, stateAddResource: { + stateAddModule: stateAddFunc_Resource_Module, stateAddResource: stateAddFunc_Resource_Resource, }, stateAddInstance: { diff --git a/terraform/state_add_test.go b/terraform/state_add_test.go index 8b9a9678b..33cf1fa83 100644 --- a/terraform/state_add_test.go +++ b/terraform/state_add_test.go @@ -7,12 +7,13 @@ import ( func TestStateAdd(t *testing.T) { cases := map[string]struct { Err bool - Address string + From, To string Value interface{} One, Two *State }{ "ModuleState => Module Addr (new)": { false, + "", "module.foo", &ModuleState{ Path: rootModulePath, @@ -60,6 +61,7 @@ func TestStateAdd(t *testing.T) { "ModuleState w/ outputs and deps => Module Addr (new)": { false, + "", "module.foo", &ModuleState{ Path: rootModulePath, @@ -115,6 +117,7 @@ func TestStateAdd(t *testing.T) { "ModuleState => Module Addr (existing)": { true, + "", "module.foo", &ModuleState{}, &State{ @@ -137,6 +140,7 @@ func TestStateAdd(t *testing.T) { "ResourceState => Resource Addr (new)": { false, + "aws_instance.bar", "aws_instance.foo", &ResourceState{ Type: "test_instance", @@ -165,6 +169,7 @@ func TestStateAdd(t *testing.T) { "ResourceState w/ deps, provider => Resource Addr (new)": { false, + "aws_instance.bar", "aws_instance.foo", &ResourceState{ Type: "test_instance", @@ -197,6 +202,7 @@ func TestStateAdd(t *testing.T) { "ResourceState w/ tainted => Resource Addr (new)": { false, + "aws_instance.bar", "aws_instance.foo", &ResourceState{ Type: "test_instance", @@ -230,6 +236,7 @@ func TestStateAdd(t *testing.T) { "ResourceState => Resource Addr (existing)": { true, + "aws_instance.bar", "aws_instance.foo", &ResourceState{ Type: "test_instance", @@ -255,6 +262,35 @@ func TestStateAdd(t *testing.T) { }, nil, }, + + "ResourceState => Module (existing)": { + false, + "aws_instance.bar", + "module.foo", + &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + + &State{}, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: []string{"root", "foo"}, + Resources: map[string]*ResourceState{ + "aws_instance.bar": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + }, } for k, tc := range cases { @@ -265,7 +301,7 @@ func TestStateAdd(t *testing.T) { } // Add the value - err := tc.One.Add(tc.Address, tc.Value) + err := tc.One.Add(tc.From, tc.To, tc.Value) if (err != nil) != tc.Err { t.Fatalf("bad: %s\n\n%s", k, err) } From 4d268b6eca5e4d59e30447405fb5cee19d5e294b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 12 Apr 2016 09:51:43 -0700 Subject: [PATCH 13/23] terraform: instance => resource --- terraform/state_add.go | 10 ++++++++++ terraform/state_add_test.go | 28 +++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/terraform/state_add.go b/terraform/state_add.go index 79a67ced9..c7d45e039 100644 --- a/terraform/state_add.go +++ b/terraform/state_add.go @@ -159,6 +159,15 @@ func stateAddFunc_Instance_Instance(s *State, fromAddr, addr *ResourceAddress, r return nil } +func stateAddFunc_Instance_Resource( + s *State, from, to *ResourceAddress, raw interface{}) error { + addr := *to + addr.InstanceType = TypePrimary + addr.InstanceTypeSet = true + + return s.Add(from.String(), addr.String(), raw) +} + // stateAddFunc is the type of function for adding an item to a state type stateAddFunc func(s *State, from, to *ResourceAddress, item interface{}) error @@ -176,6 +185,7 @@ func init() { }, stateAddInstance: { stateAddInstance: stateAddFunc_Instance_Instance, + stateAddResource: stateAddFunc_Instance_Resource, }, } } diff --git a/terraform/state_add_test.go b/terraform/state_add_test.go index 33cf1fa83..b2000b244 100644 --- a/terraform/state_add_test.go +++ b/terraform/state_add_test.go @@ -263,7 +263,7 @@ func TestStateAdd(t *testing.T) { nil, }, - "ResourceState => Module (existing)": { + "ResourceState => Module (new)": { false, "aws_instance.bar", "module.foo", @@ -291,6 +291,32 @@ func TestStateAdd(t *testing.T) { }, }, }, + + "InstanceState => Resource (new)": { + false, + "aws_instance.bar.primary", + "aws_instance.baz", + &InstanceState{ + ID: "foo", + }, + + &State{}, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: []string{"root"}, + Resources: map[string]*ResourceState{ + "aws_instance.baz": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + }, } for k, tc := range cases { From 163f19fd000ddf71c3498d803462e931b28b2761 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 12 Apr 2016 09:55:17 -0700 Subject: [PATCH 14/23] terraform: instance => module --- terraform/state_add.go | 10 ++++++++++ terraform/state_add_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/terraform/state_add.go b/terraform/state_add.go index c7d45e039..6bd107004 100644 --- a/terraform/state_add.go +++ b/terraform/state_add.go @@ -159,6 +159,15 @@ func stateAddFunc_Instance_Instance(s *State, fromAddr, addr *ResourceAddress, r return nil } +func stateAddFunc_Instance_Module( + s *State, from, to *ResourceAddress, raw interface{}) error { + addr := *to + addr.Type = from.Type + addr.Name = from.Name + + return s.Add(from.String(), addr.String(), raw) +} + func stateAddFunc_Instance_Resource( s *State, from, to *ResourceAddress, raw interface{}) error { addr := *to @@ -185,6 +194,7 @@ func init() { }, stateAddInstance: { stateAddInstance: stateAddFunc_Instance_Instance, + stateAddModule: stateAddFunc_Instance_Module, stateAddResource: stateAddFunc_Instance_Resource, }, } diff --git a/terraform/state_add_test.go b/terraform/state_add_test.go index b2000b244..f1d5e3458 100644 --- a/terraform/state_add_test.go +++ b/terraform/state_add_test.go @@ -317,6 +317,32 @@ func TestStateAdd(t *testing.T) { }, }, }, + + "InstanceState => Module (new)": { + false, + "aws_instance.bar.primary", + "module.foo", + &InstanceState{ + ID: "foo", + }, + + &State{}, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: []string{"root", "foo"}, + Resources: map[string]*ResourceState{ + "aws_instance.bar": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + }, } for k, tc := range cases { From c4e5355a0277bfdaeedbe7bc114093a5ffd879bb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 12 Apr 2016 11:44:12 -0700 Subject: [PATCH 15/23] command/state mv --- command/state_mv.go | 126 +++++++++++++++++++++++++++++++++++++++ command/state_mv_test.go | 113 +++++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 command/state_mv.go create mode 100644 command/state_mv_test.go diff --git a/command/state_mv.go b/command/state_mv.go new file mode 100644 index 000000000..5b2a67b79 --- /dev/null +++ b/command/state_mv.go @@ -0,0 +1,126 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +// StateMvCommand is a Command implementation that shows a single resource. +type StateMvCommand struct { + Meta + StateMeta +} + +func (c *StateMvCommand) Run(args []string) int { + args = c.Meta.process(args, true) + + var backupPath string + cmdFlags := c.Meta.flagSet("state show") + cmdFlags.StringVar(&backupPath, "backup", "", "backup") + cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") + cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") + if err := cmdFlags.Parse(args); err != nil { + return cli.RunResultHelp + } + args = cmdFlags.Args() + if len(args) != 2 { + c.Ui.Error("Exactly two arguments expected.\n") + return cli.RunResultHelp + } + + state, err := c.StateMeta.State(&c.Meta) + if err != nil { + c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) + return cli.RunResultHelp + } + + stateReal := state.State() + if stateReal == nil { + c.Ui.Error(fmt.Sprintf(errStateNotFound)) + return 1 + } + + filter := &terraform.StateFilter{State: stateReal} + results, err := filter.Filter(args[0]) + if err != nil { + c.Ui.Error(fmt.Sprintf(errStateMv, err)) + return cli.RunResultHelp + } + + if err := stateReal.Remove(args[0]); err != nil { + c.Ui.Error(fmt.Sprintf(errStateMv, err)) + return 1 + } + + if err := stateReal.Add(args[0], args[1], results[0].Value); err != nil { + c.Ui.Error(fmt.Sprintf(errStateMv, err)) + return 1 + } + + if err := state.WriteState(stateReal); err != nil { + c.Ui.Error(fmt.Sprintf(errStateMvPersist, err)) + return 1 + } + + if err := state.PersistState(); err != nil { + c.Ui.Error(fmt.Sprintf(errStateMvPersist, err)) + return 1 + } + + c.Ui.Output(fmt.Sprintf( + "Moved %s to %s", args[0], args[1])) + return 0 +} + +func (c *StateMvCommand) Help() string { + helpText := ` +Usage: terraform state mv [options] ADDRESS ADDRESS + + Move an item in the state to another location within the same state. + + This command is useful for module refactors (moving items into a module) + or generally renaming of resources. + + This command creates a timestamped backup of the state on every invocation. + This can't be disabled. Due to the destructive nature of this command, + the backup is ensured by Terraform for safety reasons. + + This command can't currently move an item from one state file to a + completely new state file, but this functionality will come in an update. + +Options: + + -backup=PATH Path where Terraform should write the backup + state. This can't be disabled. If not set, Terraform + will write it to the same path as the statefile with + a backup extension. + + -state=PATH Path to a Terraform state file to use to look + up Terraform-managed resources. By default it will + use the state "terraform.tfstate" if it exists. + + -state-out=PATH Path to the destination state file to move the item + to. This defaults to the same statefile. This will + overwrite the destination state file. + +` + return strings.TrimSpace(helpText) +} + +func (c *StateMvCommand) Synopsis() string { + return "Move an item in the state" +} + +const errStateMv = `Error moving state: %[1]s + +Please ensure your addresses and state paths are valid. No +state was persisted. Your existing states are untouched.` + +const errStateMvPersist = `Error saving the state: %s + +The state wasn't saved properly. If the error happening after a partial +write occurred, a backup file will have been created. Otherwise, the state +is in the same state it was when the operation started.` diff --git a/command/state_mv_test.go b/command/state_mv_test.go new file mode 100644 index 000000000..06de1a6ca --- /dev/null +++ b/command/state_mv_test.go @@ -0,0 +1,113 @@ +package command + +import ( + "path/filepath" + "testing" + + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +func TestStateMv(t *testing.T) { + state := &terraform.State{ + Modules: []*terraform.ModuleState{ + &terraform.ModuleState{ + Path: []string{"root"}, + 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", + }, + }, + }, + + "test_instance.baz": &terraform.ResourceState{ + Type: "test_instance", + Primary: &terraform.InstanceState{ + ID: "foo", + Attributes: map[string]string{ + "foo": "value", + "bar": "value", + }, + }, + }, + }, + }, + }, + } + + statePath := testStateFile(t, state) + + p := testProvider() + ui := new(cli.MockUi) + c := &StateMvCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + "test_instance.foo", + "test_instance.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, statePath, testStateMvOutput) + + // Test we have backups + backups := testStateBackups(t, filepath.Dir(statePath)) + if len(backups) != 1 { + t.Fatalf("bad: %#v", backups) + } + testStateOutput(t, backups[0], testStateMvOutputOriginal) +} + +func TestStateMv_noState(t *testing.T) { + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + p := testProvider() + ui := new(cli.MockUi) + c := &StateMvCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{"from", "to"} + if code := c.Run(args); code != 1 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } +} + +const testStateMvOutputOriginal = ` +test_instance.baz: + ID = foo + bar = value + foo = value +test_instance.foo: + ID = bar + bar = value + foo = value +` + +const testStateMvOutput = ` +test_instance.bar: + ID = bar + bar = value + foo = value +test_instance.baz: + ID = foo + bar = value + foo = value +` From 235e860118358266af3ea7ff36fa1a71b2cab059 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 12 Apr 2016 14:17:59 -0700 Subject: [PATCH 16/23] command/state mv: handle -state-out to a different path --- command/state_mv.go | 77 +++++++++++++++++++++++++++++++++------- command/state_mv_test.go | 73 +++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 13 deletions(-) diff --git a/command/state_mv.go b/command/state_mv.go index 5b2a67b79..ec5597ad5 100644 --- a/command/state_mv.go +++ b/command/state_mv.go @@ -17,11 +17,13 @@ type StateMvCommand struct { func (c *StateMvCommand) Run(args []string) int { args = c.Meta.process(args, true) - var backupPath string + // We create two metas to track the two states + var meta1, meta2 Meta cmdFlags := c.Meta.flagSet("state show") - cmdFlags.StringVar(&backupPath, "backup", "", "backup") - cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") - cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") + cmdFlags.StringVar(&meta1.stateOutPath, "backup", "", "backup") + cmdFlags.StringVar(&meta1.statePath, "state", DefaultStateFilename, "path") + cmdFlags.StringVar(&meta2.stateOutPath, "backup-out", "", "backup") + cmdFlags.StringVar(&meta2.statePath, "state-out", "", "path") if err := cmdFlags.Parse(args); err != nil { return cli.RunResultHelp } @@ -31,45 +33,87 @@ func (c *StateMvCommand) Run(args []string) int { return cli.RunResultHelp } - state, err := c.StateMeta.State(&c.Meta) + // Copy the `-state` flag for output if we weren't given a custom one + if meta2.statePath == "" { + meta2.statePath = meta1.statePath + } + + // Read the from state + stateFrom, err := c.StateMeta.State(&meta1) if err != nil { c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) return cli.RunResultHelp } - stateReal := state.State() - if stateReal == nil { + stateFromReal := stateFrom.State() + if stateFromReal == nil { c.Ui.Error(fmt.Sprintf(errStateNotFound)) return 1 } - filter := &terraform.StateFilter{State: stateReal} + // Read the destination state + stateTo := stateFrom + stateToReal := stateFromReal + if meta2.statePath != meta1.statePath { + stateTo, err = c.StateMeta.State(&meta2) + if err != nil { + c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) + return cli.RunResultHelp + } + + stateToReal = stateTo.State() + if stateToReal == nil { + stateToReal = terraform.NewState() + } + } + + // Filter what we're moving + filter := &terraform.StateFilter{State: stateFromReal} results, err := filter.Filter(args[0]) if err != nil { c.Ui.Error(fmt.Sprintf(errStateMv, err)) return cli.RunResultHelp } + if len(results) == 0 { + c.Ui.Output(fmt.Sprintf("Item to move doesn't exist: %s", args[0])) + return 1 + } - if err := stateReal.Remove(args[0]); err != nil { + // Do the actual move + if err := stateFromReal.Remove(args[0]); err != nil { c.Ui.Error(fmt.Sprintf(errStateMv, err)) return 1 } - if err := stateReal.Add(args[0], args[1], results[0].Value); err != nil { + if err := stateToReal.Add(args[0], args[1], results[0].Value); err != nil { c.Ui.Error(fmt.Sprintf(errStateMv, err)) return 1 } - if err := state.WriteState(stateReal); err != nil { + // Write the new state + if err := stateTo.WriteState(stateToReal); err != nil { c.Ui.Error(fmt.Sprintf(errStateMvPersist, err)) return 1 } - if err := state.PersistState(); err != nil { + if err := stateTo.PersistState(); err != nil { c.Ui.Error(fmt.Sprintf(errStateMvPersist, err)) return 1 } + // Write the old state if it is different + if stateTo != stateFrom { + if err := stateFrom.WriteState(stateFromReal); err != nil { + c.Ui.Error(fmt.Sprintf(errStateMvPersist, err)) + return 1 + } + + if err := stateFrom.PersistState(); err != nil { + c.Ui.Error(fmt.Sprintf(errStateMvPersist, err)) + return 1 + } + } + c.Ui.Output(fmt.Sprintf( "Moved %s to %s", args[0], args[1])) return 0 @@ -93,11 +137,18 @@ Usage: terraform state mv [options] ADDRESS ADDRESS Options: - -backup=PATH Path where Terraform should write the backup + -backup=PATH Path where Terraform should write the backup for the original state. This can't be disabled. If not set, Terraform will write it to the same path as the statefile with a backup extension. + -backup-out=PATH Path where Terraform should write the backup for the destination + state. This can't be disabled. If not set, Terraform + will write it to the same path as the destination state + file with a backup extension. This only needs + to be specified if -state-out is set to a different path + than -state. + -state=PATH Path to a Terraform state file to use to look up Terraform-managed resources. By default it will use the state "terraform.tfstate" if it exists. diff --git a/command/state_mv_test.go b/command/state_mv_test.go index 06de1a6ca..e8b0391d8 100644 --- a/command/state_mv_test.go +++ b/command/state_mv_test.go @@ -71,6 +71,61 @@ func TestStateMv(t *testing.T) { testStateOutput(t, backups[0], testStateMvOutputOriginal) } +func TestStateMv_stateOutNew(t *testing.T) { + state := &terraform.State{ + Modules: []*terraform.ModuleState{ + &terraform.ModuleState{ + Path: []string{"root"}, + 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, + "test_instance.foo", + "test_instance.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, testStateMvOutput_stateOut) + testStateOutput(t, statePath, testStateMvOutput_stateOutSrc) + + // Test we have backups + backups := testStateBackups(t, filepath.Dir(statePath)) + if len(backups) != 1 { + t.Fatalf("bad: %#v", backups) + } + testStateOutput(t, backups[0], testStateMvOutput_stateOutOriginal) +} + func TestStateMv_noState(t *testing.T) { tmp, cwd := testCwd(t) defer testFixCwd(t, tmp, cwd) @@ -111,3 +166,21 @@ test_instance.baz: bar = value foo = value ` + +const testStateMvOutput_stateOut = ` +test_instance.bar: + ID = bar + bar = value + foo = value +` + +const testStateMvOutput_stateOutSrc = ` + +` + +const testStateMvOutput_stateOutOriginal = ` +test_instance.foo: + ID = bar + bar = value + foo = value +` From 32d0d29b56cb3fba5024004883cbe2ba6d05c28f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 12 Apr 2016 15:02:18 -0700 Subject: [PATCH 17/23] command: test for moving into another state file --- command/state_mv_test.go | 103 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/command/state_mv_test.go b/command/state_mv_test.go index e8b0391d8..accdb2e0f 100644 --- a/command/state_mv_test.go +++ b/command/state_mv_test.go @@ -126,6 +126,84 @@ func TestStateMv_stateOutNew(t *testing.T) { testStateOutput(t, backups[0], testStateMvOutput_stateOutOriginal) } +func TestStateMv_stateOutExisting(t *testing.T) { + stateSrc := &terraform.State{ + Modules: []*terraform.ModuleState{ + &terraform.ModuleState{ + Path: []string{"root"}, + 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, stateSrc) + + stateDst := &terraform.State{ + Modules: []*terraform.ModuleState{ + &terraform.ModuleState{ + Path: []string{"root"}, + Resources: map[string]*terraform.ResourceState{ + "test_instance.qux": &terraform.ResourceState{ + Type: "test_instance", + Primary: &terraform.InstanceState{ + ID: "bar", + }, + }, + }, + }, + }, + } + + stateOutPath := testStateFile(t, stateDst) + + p := testProvider() + ui := new(cli.MockUi) + c := &StateMvCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + "-state-out", stateOutPath, + "test_instance.foo", + "test_instance.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, testStateMvExisting_stateDst) + testStateOutput(t, statePath, testStateMvExisting_stateSrc) + + // Test we have backups + backups := testStateBackups(t, filepath.Dir(statePath)) + if len(backups) != 1 { + t.Fatalf("bad: %#v", backups) + } + testStateOutput(t, backups[0], testStateMvExisting_stateSrcOriginal) + + backups = testStateBackups(t, filepath.Dir(stateOutPath)) + if len(backups) != 1 { + t.Fatalf("bad: %#v", backups) + } + testStateOutput(t, backups[0], testStateMvExisting_stateDstOriginal) +} + func TestStateMv_noState(t *testing.T) { tmp, cwd := testCwd(t) defer testFixCwd(t, tmp, cwd) @@ -184,3 +262,28 @@ test_instance.foo: bar = value foo = value ` + +const testStateMvExisting_stateSrc = ` + +` + +const testStateMvExisting_stateDst = ` +test_instance.bar: + ID = bar + bar = value + foo = value +test_instance.qux: + ID = bar +` + +const testStateMvExisting_stateSrcOriginal = ` +test_instance.foo: + ID = bar + bar = value + foo = value +` + +const testStateMvExisting_stateDstOriginal = ` +test_instance.qux: + ID = bar +` From c966a70ff96c78970537f510c9f79045df36597c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 12 Apr 2016 15:03:03 -0700 Subject: [PATCH 18/23] command: update docs for state mv --- command/state_mv.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/command/state_mv.go b/command/state_mv.go index ec5597ad5..1c6e23fcc 100644 --- a/command/state_mv.go +++ b/command/state_mv.go @@ -125,15 +125,16 @@ Usage: terraform state mv [options] ADDRESS ADDRESS Move an item in the state to another location within the same state. - This command is useful for module refactors (moving items into a module) - or generally renaming of resources. + This command is useful for module refactors (moving items into a module), + configuration refactors (moving items to a completely different or new + state file), or generally renaming of resources. This command creates a timestamped backup of the state on every invocation. This can't be disabled. Due to the destructive nature of this command, the backup is ensured by Terraform for safety reasons. - This command can't currently move an item from one state file to a - completely new state file, but this functionality will come in an update. + If you're moving from one state file to a different state file, a backup + will be created for each state file. Options: From 04598baa2544ac20bec5c898fa7c940419a49582 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 12 Apr 2016 15:11:09 -0700 Subject: [PATCH 19/23] website: document state mv --- command/state_mv.go | 3 +- website/source/docs/commands/state/mv.html.md | 81 +++++++++++++++++++ website/source/layouts/commands-state.erb | 8 ++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 website/source/docs/commands/state/mv.html.md diff --git a/command/state_mv.go b/command/state_mv.go index 1c6e23fcc..25da66862 100644 --- a/command/state_mv.go +++ b/command/state_mv.go @@ -123,7 +123,8 @@ func (c *StateMvCommand) Help() string { helpText := ` Usage: terraform state mv [options] ADDRESS ADDRESS - Move an item in the state to another location within the same state. + Move an item in the state to another location or to a completely different + state file. This command is useful for module refactors (moving items into a module), configuration refactors (moving items to a completely different or new diff --git a/website/source/docs/commands/state/mv.html.md b/website/source/docs/commands/state/mv.html.md new file mode 100644 index 000000000..81a06ee25 --- /dev/null +++ b/website/source/docs/commands/state/mv.html.md @@ -0,0 +1,81 @@ +--- +layout: "commands-state" +page_title: "Command: state mv" +sidebar_current: "docs-state-sub-mv" +description: |- + The `terraform state rm` command removes items from the Terraform state. +--- + +# Command: state mv + +The `terraform state mv` command is used to move items in a +[Terraform state](/docs/state/index.html). This command can move +single resources, single instances of a resource, entire modules, and more. +This command can also move items to a completely different state file, +enabling efficient refactoring. + +## Usage + +Usage: `terraform state mv [options] SOURCE DESTINATION` + +This command will move an item matched by the address given to the +destination address. This command can also move to a destination address +in a completely different state file. + +This can be used for simple resource renaming, moving items to and from +a module, moving entire modules, and more. And because this command can also +move data to a completely new state, it can also be used for refactoring +one configuration into multiple separately managed Terraform configurations. + +This command will output a backup copy of the state prior to saving any +changes. The backup cannot be disabled. Due to the destructive nature +of this command, backups are required. + +If you're moving an item to a different state file, a backup will be created +for each state file. + +This command requires a source and destination address of the item to move. +Addresses are +in [resource addressing format](/docs/commands/state/addressing.html). + +The command-line flags are all optional. The list of available flags are: + +* `-backup=path` - Path to a backup file Defaults to the state path plus + a timestamp with the ".backup" extension. + +* `-backup-out=path` - Path to the backup file for the output state. + This is only necessary if `-state-out` is specified. + +* `-state=path` - Path to the state file. Defaults to "terraform.tfstate". + +* `-state-out=path` - Path to the state file to write to. If this isn't specified + the state specified by `-state` will be used. This can be + a new or existing path. + +## Example: Rename a Resource + +The example below renames a single resource: + +``` +$ terraform state mv aws_instance.foo aws_instance.bar +``` + +## Example: Move a Resource Into a Module + +The example below moves a resource into a module. The module will be +created if it doesn't exist. + +``` +$ terraform state mv aws_instance.foo module.web +``` + +## Example: Move a Module to Another State + +The example below moves a module into another state file. This removes +the module from the original state file and adds it to the destination. +The source and destination are the same meaning we're keeping the same name. + +``` +$ terraform state mv -state-out=other.tfstate \ + module.web module.web +``` diff --git a/website/source/layouts/commands-state.erb b/website/source/layouts/commands-state.erb index e182f0d2e..14cc64416 100644 --- a/website/source/layouts/commands-state.erb +++ b/website/source/layouts/commands-state.erb @@ -21,6 +21,14 @@ list + > + mv + + + > + rm + + > show From eaf3d608ed01a4ccfcb737a5791b590d5374ed53 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 12 Apr 2016 15:21:10 -0700 Subject: [PATCH 20/23] terraform: test moving a module to be nested --- terraform/state_add_test.go | 48 +++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/terraform/state_add_test.go b/terraform/state_add_test.go index f1d5e3458..6d2f32696 100644 --- a/terraform/state_add_test.go +++ b/terraform/state_add_test.go @@ -59,6 +59,54 @@ func TestStateAdd(t *testing.T) { }, }, + "ModuleState => Nested Module Addr (new)": { + false, + "", + "module.foo.module.bar", + &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{}, + &State{ + Modules: []*ModuleState{ + &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", + }, + }, + }, + }, + }, + }, + }, + "ModuleState w/ outputs and deps => Module Addr (new)": { false, "", From 9ddf73ad8138a2f2a090680504d9be0e1fc28a58 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 12 Apr 2016 15:21:51 -0700 Subject: [PATCH 21/23] website: update docs --- website/source/docs/commands/state/mv.html.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/website/source/docs/commands/state/mv.html.md b/website/source/docs/commands/state/mv.html.md index 81a06ee25..e145fbbff 100644 --- a/website/source/docs/commands/state/mv.html.md +++ b/website/source/docs/commands/state/mv.html.md @@ -69,6 +69,14 @@ created if it doesn't exist. $ terraform state mv aws_instance.foo module.web ``` +## Example: Move a Module Into a Module + +The example below moves a module into another module. + +``` +$ terraform state mv module.foo module.parent.module.foo +``` + ## Example: Move a Module to Another State The example below moves a module into another state file. This removes From f34ef1f92a8512d9ef1cae2c78ab025dd3e23b58 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 10 May 2016 17:03:58 -0700 Subject: [PATCH 22/23] command: compilation works This still isn't ready. But this gets tests passing and compilation working --- command/command_test.go | 16 +--------------- command/plan_test.go | 3 +++ command/state_meta.go | 6 ++++++ command/state_show.go | 2 +- command/state_test.go | 22 ++++++++++++++++++++++ 5 files changed, 33 insertions(+), 16 deletions(-) create mode 100644 command/state_test.go diff --git a/command/command_test.go b/command/command_test.go index 5d150bcb1..fcc8dfb25 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -224,21 +224,7 @@ func testProvider() *terraform.MockResourceProvider { } func testTempFile(t *testing.T) string { - tf, err := ioutil.TempFile("", "tf") - if err != nil { - t.Fatalf("err: %s", err) - } - - result := tf.Name() - - if err := tf.Close(); err != nil { - t.Fatalf("err: %s", err) - } - if err := os.Remove(result); err != nil { - t.Fatalf("err: %s", err) - } - - return result + return filepath.Join(testTempDir(t), "state.tfstate") } func testTempDir(t *testing.T) string { diff --git a/command/plan_test.go b/command/plan_test.go index 935793174..8736b8cc9 100644 --- a/command/plan_test.go +++ b/command/plan_test.go @@ -108,6 +108,9 @@ func TestPlan_destroy(t *testing.T) { } func TestPlan_noState(t *testing.T) { + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + p := testProvider() ui := new(cli.MockUi) c := &PlanCommand{ diff --git a/command/state_meta.go b/command/state_meta.go index f576004e3..c1af8b82e 100644 --- a/command/state_meta.go +++ b/command/state_meta.go @@ -3,12 +3,18 @@ package command import ( "errors" + "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" ) // StateMeta is the meta struct that should be embedded in state subcommands. type StateMeta struct{} +// State returns the state for this meta. +func (m *StateMeta) State(cm *Meta) (state.State, error) { + return cm.State() +} + // filterInstance filters a single instance out of filter results. func (c *StateMeta) filterInstance(rs []*terraform.StateFilterResult) (*terraform.StateFilterResult, error) { var result *terraform.StateFilterResult diff --git a/command/state_show.go b/command/state_show.go index 55bc30910..d9a4f1d08 100644 --- a/command/state_show.go +++ b/command/state_show.go @@ -26,7 +26,7 @@ func (c *StateShowCommand) Run(args []string) int { } args = cmdFlags.Args() - state, err := c.State() + state, err := c.Meta.State() if err != nil { c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) return cli.RunResultHelp diff --git a/command/state_test.go b/command/state_test.go new file mode 100644 index 000000000..7fe83e904 --- /dev/null +++ b/command/state_test.go @@ -0,0 +1,22 @@ +package command + +import ( + "path/filepath" + "sort" + "testing" +) + +// testStateBackups returns the list of backups in order of creation +// (oldest first) in the given directory. +func testStateBackups(t *testing.T, dir string) []string { + // Find all the backups + list, err := filepath.Glob(filepath.Join(dir, "*"+DefaultBackupExtension)) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Sort them which will put them naturally in the right order + sort.Strings(list) + + return list +} From d94f503501b19a85e8db673d9fa80f940b7d3d16 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 May 2016 09:16:48 -0700 Subject: [PATCH 23/23] command/state meta: State func --- command/state_meta.go | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/command/state_meta.go b/command/state_meta.go index c1af8b82e..7b6e13694 100644 --- a/command/state_meta.go +++ b/command/state_meta.go @@ -2,6 +2,8 @@ package command import ( "errors" + "fmt" + "time" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" @@ -10,9 +12,34 @@ import ( // StateMeta is the meta struct that should be embedded in state subcommands. type StateMeta struct{} -// State returns the state for this meta. -func (m *StateMeta) State(cm *Meta) (state.State, error) { - return cm.State() +// State returns the state for this meta. This is different then Meta.State +// in the way that backups are done. This configures backups to be timestamped +// rather than just the original state path plus a backup path. +func (c *StateMeta) State(m *Meta) (state.State, error) { + // Disable backups since we wrap it manually below + m.backupPath = "-" + + // Get the state (shouldn't be wrapped in a backup) + s, err := m.State() + if err != nil { + return nil, err + } + + // Determine the backup path. stateOutPath is set to the resulting + // file where state is written (cached in the case of remote state) + backupPath := fmt.Sprintf( + "%s.%d.%s", + m.stateOutPath, + time.Now().UTC().Unix(), + DefaultBackupExtension) + + // Wrap it for backups + s = &state.BackupState{ + Real: s, + Path: backupPath, + } + + return s, nil } // filterInstance filters a single instance out of filter results.