Skip to content

Commit

Permalink
core: fix bug detecting deeply nested module orphans
Browse files Browse the repository at this point in the history
Context:

As part of building up a Plan, Terraform needs to detect "orphaned"
resources--resources which are present in the state but not in the
config. This happens when config for those resources is removed by the
user, making it Terraform's responsibility to destroy them.

Both state and config are organized by Module into a logical tree, so
the process of finding orphans involves checking for orphaned Resources
in the current module and for orphaned Modules, which themselves will
have all their Resources marked as orphans.

Bug:

In hashicorp#3114 a problem was exposed where, given a module tree that looked
like this:

```
root
 |
 +-- parent (empty, except for sub-modules)
       |
       +-- child1 (1 resource)
       |
       +-- child2 (1 resource)
```

If `parent` was removed, a bunch of error messages would occur during
the plan. The root cause of this was duplicate orphans appearing for the
resources in child1 and child2.

Fix:

This turned out to be a bug in orphaned module detection. When looking
for deeply nested orphaned modules, root.parent was getting added twice
as an orphaned module to the graph.

Here, we add an additional check to prevent a double add, which
addresses this scenario properly.

Fixes hashicorp#3114 (the Provisioner side of it was fixed in hashicorp#4877)
  • Loading branch information
phinze authored and joshmyers committed Feb 18, 2016
1 parent 093d6de commit 9031352
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 1 deletion.
85 changes: 85 additions & 0 deletions terraform/context_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,91 @@ func TestContext2Plan_moduleOrphans(t *testing.T) {
}
}

// https://github.com/hashicorp/terraform/issues/3114
func TestContext2Plan_moduleOrphansWithProvisioner(t *testing.T) {
m := testModule(t, "plan-modules-remove-provisioners")
p := testProvider("aws")
pr := testProvisioner()
p.DiffFn = testDiffFn
s := &State{
Modules: []*ModuleState{
&ModuleState{
Path: []string{"root"},
Resources: map[string]*ResourceState{
"aws_instance.top": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "top",
},
},
},
},
&ModuleState{
Path: []string{"root", "parent", "childone"},
Resources: map[string]*ResourceState{
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "baz",
},
},
},
},
&ModuleState{
Path: []string{"root", "parent", "childtwo"},
Resources: map[string]*ResourceState{
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "baz",
},
},
},
},
},
}
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
Provisioners: map[string]ResourceProvisionerFactory{
"shell": testProvisionerFuncFixed(pr),
},
State: s,
})

plan, err := ctx.Plan()
if err != nil {
t.Fatalf("err: %s", err)
}

actual := strings.TrimSpace(plan.String())
expected := strings.TrimSpace(`
DIFF:
module.parent.childone:
DESTROY: aws_instance.foo
module.parent.childtwo:
DESTROY: aws_instance.foo
STATE:
aws_instance.top:
ID = top
module.parent.childone:
aws_instance.foo:
ID = baz
module.parent.childtwo:
aws_instance.foo:
ID = baz
`)
if actual != expected {
t.Fatalf("bad:\n%s", actual)
}
}

func TestContext2Plan_moduleProviderInherit(t *testing.T) {
var l sync.Mutex
var calls []string
Expand Down
17 changes: 16 additions & 1 deletion terraform/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,23 @@ func (s *State) ModuleOrphans(path []string, c *config.Config) [][]string {
continue
}

orphanPath := m.Path[:len(path)+1]

// Don't double-add if we've already added this orphan (which can happen if
// there are multiple nested sub-modules that get orphaned together).
alreadyAdded := false
for _, o := range orphans {
if reflect.DeepEqual(o, orphanPath) {
alreadyAdded = true
break
}
}
if alreadyAdded {
continue
}

// Add this orphan
orphans = append(orphans, m.Path[:len(path)+1])
orphans = append(orphans, orphanPath)
}

return orphans
Expand Down
25 changes: 25 additions & 0 deletions terraform/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,31 @@ func TestStateModuleOrphans_nilConfig(t *testing.T) {
}
}

func TestStateModuleOrphans_deepNestedNilConfig(t *testing.T) {
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
},
&ModuleState{
Path: []string{RootModuleName, "parent", "childfoo"},
},
&ModuleState{
Path: []string{RootModuleName, "parent", "childbar"},
},
},
}

actual := state.ModuleOrphans(RootModulePath, nil)
expected := [][]string{
[]string{RootModuleName, "parent"},
}

if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}

func TestStateEqual(t *testing.T) {
cases := []struct {
Result bool
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
resource "aws_instance" "top" {}

# module "test" {
# source = "./parent"
# }
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
resource "aws_instance" "foo" {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module "childone" {
source = "./child"
}

module "childtwo" {
source = "./child"
}

0 comments on commit 9031352

Please sign in to comment.