diff --git a/terraform/resource_address.go b/terraform/resource_address.go index 46b47aa2c..d6bb0522e 100644 --- a/terraform/resource_address.go +++ b/terraform/resource_address.go @@ -18,9 +18,10 @@ type ResourceAddress struct { // Addresses a specific resource that occurs in a list Index int - InstanceType InstanceType - Name string - Type string + InstanceType InstanceType + InstanceTypeSet bool + Name string + Type string } // Copy returns a copy of this ResourceAddress @@ -83,11 +84,12 @@ func ParseResourceAddress(s string) (*ResourceAddress, error) { path := ParseResourcePath(matches["path"]) return &ResourceAddress{ - Path: path, - Index: resourceIndex, - InstanceType: instanceType, - Name: matches["name"], - Type: matches["type"], + Path: path, + Index: resourceIndex, + InstanceType: instanceType, + InstanceTypeSet: matches["instance_type"] != "", + Name: matches["name"], + Type: matches["type"], }, nil } diff --git a/terraform/resource_address_test.go b/terraform/resource_address_test.go index 6fee98d1f..193c56c44 100644 --- a/terraform/resource_address_test.go +++ b/terraform/resource_address_test.go @@ -44,30 +44,33 @@ func TestParseResourceAddress(t *testing.T) { "explicit primary, explicit index": { "aws_instance.foo.primary[2]", &ResourceAddress{ - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 2, + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + InstanceTypeSet: true, + Index: 2, }, "aws_instance.foo[2]", }, "tainted": { "aws_instance.foo.tainted", &ResourceAddress{ - Type: "aws_instance", - Name: "foo", - InstanceType: TypeTainted, - Index: -1, + Type: "aws_instance", + Name: "foo", + InstanceType: TypeTainted, + InstanceTypeSet: true, + Index: -1, }, "", }, "deposed": { "aws_instance.foo.deposed", &ResourceAddress{ - Type: "aws_instance", - Name: "foo", - InstanceType: TypeDeposed, - Index: -1, + Type: "aws_instance", + Name: "foo", + InstanceType: TypeDeposed, + InstanceTypeSet: true, + Index: -1, }, "", }, diff --git a/terraform/state.go b/terraform/state.go index d63313278..0a2c9e5bf 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -198,6 +198,122 @@ func (s *State) IsRemote() bool { return true } +// Remove removes the item in the state at the given address, returning +// any errors that may have occurred. +// +// If the address references a module state or resource, it will delete +// all children as well. To check what will be deleted, use a StateFilter +// first. +func (s *State) Remove(addr ...string) error { + // Filter out what we need to delete + filter := &StateFilter{State: s} + results, err := filter.Filter(addr...) + if err != nil { + return err + } + + // If we have no results, just exit early, we're not going to do anything. + // While what happens below is fairly fast, this is an important early + // exit since the prune below might modify the state more and we don't + // want to modify the state if we don't have to. + if len(results) == 0 { + return nil + } + + // Go through each result and grab what we need + removed := make(map[interface{}]struct{}) + for _, r := range results { + // Convert the path to our own type + path := append([]string{"root"}, r.Path...) + + // If we removed this already, then ignore + if _, ok := removed[r.Value]; ok { + continue + } + + // If we removed the parent already, then ignore + if r.Parent != nil { + if _, ok := removed[r.Parent.Value]; ok { + continue + } + } + + // Add this to the removed list + removed[r.Value] = struct{}{} + + switch v := r.Value.(type) { + case *ModuleState: + s.removeModule(path, v) + case *ResourceState: + s.removeResource(path, v) + case *InstanceState: + s.removeInstance(path, r.Parent.Value.(*ResourceState), v) + default: + return fmt.Errorf("unknown type to delete: %T", r.Value) + } + } + + // Prune since the removal functions often do the bare minimum to + // remove a thing and may leave around dangling empty modules, resources, + // etc. Prune will clean that all up. + s.prune() + + return nil +} + +func (s *State) removeModule(path []string, v *ModuleState) { + for i, m := range s.Modules { + if m == v { + s.Modules, s.Modules[len(s.Modules)-1] = append(s.Modules[:i], s.Modules[i+1:]...), nil + return + } + } +} + +func (s *State) removeResource(path []string, v *ResourceState) { + // Get the module this resource lives in. If it doesn't exist, we're done. + mod := s.ModuleByPath(path) + if mod == nil { + return + } + + // Find this resource. This is a O(N) lookup when if we had the key + // it could be O(1) but even with thousands of resources this shouldn't + // matter right now. We can easily up performance here when the time comes. + for k, r := range mod.Resources { + if r == v { + // Found it + delete(mod.Resources, k) + return + } + } +} + +func (s *State) removeInstance(path []string, r *ResourceState, v *InstanceState) { + // Go through the resource and find the instance that matches this + // (if any) and remove it. + + // Check primary + if r.Primary == v { + r.Primary = nil + return + } + + // Check lists + lists := [][]*InstanceState{r.Tainted, r.Deposed} + for _, is := range lists { + for i, instance := range is { + if instance == v { + // Found it, remove it + is, is[len(is)-1] = append(is[:i], is[i+1:]...), nil + + // Done + return + } + } + } +} + // RootModule returns the ModuleState for the root module func (s *State) RootModule() *ModuleState { root := s.ModuleByPath(rootModulePath) diff --git a/terraform/state_filter.go b/terraform/state_filter.go index 8b1f523c7..74fe501c1 100644 --- a/terraform/state_filter.go +++ b/terraform/state_filter.go @@ -113,11 +113,14 @@ func (f *StateFilter) filterSingle(a *ResourceAddress) []*StateFilterResult { Address: addr.String(), Value: r, } - results = append(results, resourceResult) + if !a.InstanceTypeSet { + results = append(results, resourceResult) + } // Add the instances if r.Primary != nil { addr.InstanceType = TypePrimary + addr.InstanceTypeSet = true results = append(results, &StateFilterResult{ Path: addr.Path, Address: addr.String(), @@ -129,6 +132,7 @@ func (f *StateFilter) filterSingle(a *ResourceAddress) []*StateFilterResult { for _, instance := range r.Tainted { if f.relevant(a, instance) { addr.InstanceType = TypeTainted + addr.InstanceTypeSet = true results = append(results, &StateFilterResult{ Path: addr.Path, Address: addr.String(), @@ -141,6 +145,7 @@ func (f *StateFilter) filterSingle(a *ResourceAddress) []*StateFilterResult { for _, instance := range r.Deposed { if f.relevant(a, instance) { addr.InstanceType = TypeDeposed + addr.InstanceTypeSet = true results = append(results, &StateFilterResult{ Path: addr.Path, Address: addr.String(), diff --git a/terraform/state_filter_test.go b/terraform/state_filter_test.go index 1f19dc1f1..d9b1db8d5 100644 --- a/terraform/state_filter_test.go +++ b/terraform/state_filter_test.go @@ -29,6 +29,23 @@ func TestStateFilterFilter(t *testing.T) { }, }, + "single resource": { + "small.tfstate", + []string{"aws_key_pair.onprem"}, + []string{ + "*terraform.ResourceState: aws_key_pair.onprem", + "*terraform.InstanceState: aws_key_pair.onprem", + }, + }, + + "single instance": { + "small.tfstate", + []string{"aws_key_pair.onprem.primary"}, + []string{ + "*terraform.InstanceState: aws_key_pair.onprem", + }, + }, + "module filter": { "complete.tfstate", []string{"module.bootstrap"}, diff --git a/terraform/state_test.go b/terraform/state_test.go index f145652a4..b66aa28a2 100644 --- a/terraform/state_test.go +++ b/terraform/state_test.go @@ -358,6 +358,277 @@ func TestStateIncrementSerialMaybe(t *testing.T) { } } +func TestStateRemove(t *testing.T) { + cases := map[string]struct { + Address string + One, Two *State + }{ + "simple resource": { + "test_instance.foo", + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "test_instance.foo": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{}, + }, + }, + }, + }, + + "single instance": { + "test_instance.foo.primary", + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "test_instance.foo": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{}, + }, + }, + }, + }, + + "single instance in multi-count": { + "test_instance.foo[0]", + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "test_instance.foo.0": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + + "test_instance.foo.1": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "test_instance.foo.1": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + }, + + "single resource, multi-count": { + "test_instance.foo", + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "test_instance.foo.0": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + + "test_instance.foo.1": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{}, + }, + }, + }, + }, + + "full module": { + "module.foo", + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "test_instance.foo": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + + &ModuleState{ + Path: []string{"root", "foo"}, + Resources: map[string]*ResourceState{ + "test_instance.foo": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + + "test_instance.bar": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "test_instance.foo": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + }, + + "module and children": { + "module.foo", + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "test_instance.foo": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + + &ModuleState{ + Path: []string{"root", "foo"}, + Resources: map[string]*ResourceState{ + "test_instance.foo": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + + "test_instance.bar": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + + &ModuleState{ + Path: []string{"root", "foo", "bar"}, + Resources: map[string]*ResourceState{ + "test_instance.foo": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + + "test_instance.bar": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "test_instance.foo": &ResourceState{ + Type: "test_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + }, + }, + } + + for k, tc := range cases { + if err := tc.One.Remove(tc.Address); err != nil { + t.Fatalf("bad: %s\n\n%s", k, err) + } + + if !tc.One.Equal(tc.Two) { + t.Fatalf("Bad: %s\n\n%s\n\n%s", k, tc.One.String(), tc.Two.String()) + } + } +} + func TestResourceStateEqual(t *testing.T) { cases := []struct { Result bool