terraform: Internals for `state rm` command

I decided to split this up from the terraform state rm command to make the diff easier to see. These changes will also be used for terraform state mv.

This adds a `Remove` method to the `*terraform.State` struct. It takes a list of addresses and removes the items matching that list. This leverages the `StateFilter` committed last week to make the view of the world consistent across address lookups.

There is a lot of test duplication here with StateFilter, but in Terraform style: we like it that way.
This commit is contained in:
Mitchell Hashimoto 2016-03-30 09:29:20 -07:00 committed by James Nugent
parent e133452663
commit a94b9fdc92
6 changed files with 435 additions and 21 deletions

View File

@ -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
}

View File

@ -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,
},
"",
},

View File

@ -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)

View File

@ -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(),

View File

@ -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"},

View File

@ -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