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:
parent
e133452663
commit
a94b9fdc92
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
"",
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue