From e76fdb92e7774885a7dfd5395a0ef59bc12277f8 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Fri, 5 Feb 2016 12:19:10 -0600 Subject: [PATCH] core: fix bug detecting deeply nested module orphans 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 #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 #3114 (the Provisioner side of it was fixed in #4877) --- terraform/context_plan_test.go | 85 +++++++++++++++++++ terraform/state.go | 17 +++- terraform/state_test.go | 25 ++++++ .../plan-modules-remove-provisioners/main.tf | 5 ++ .../parent/child/main.tf | 2 + .../parent/main.tf | 7 ++ 6 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 terraform/test-fixtures/plan-modules-remove-provisioners/main.tf create mode 100644 terraform/test-fixtures/plan-modules-remove-provisioners/parent/child/main.tf create mode 100644 terraform/test-fixtures/plan-modules-remove-provisioners/parent/main.tf diff --git a/terraform/context_plan_test.go b/terraform/context_plan_test.go index c667109be..efdf9c53a 100644 --- a/terraform/context_plan_test.go +++ b/terraform/context_plan_test.go @@ -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 diff --git a/terraform/state.go b/terraform/state.go index a20fd7ba4..cd9218cb4 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -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 diff --git a/terraform/state_test.go b/terraform/state_test.go index c3bfb18df..181360138 100644 --- a/terraform/state_test.go +++ b/terraform/state_test.go @@ -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 diff --git a/terraform/test-fixtures/plan-modules-remove-provisioners/main.tf b/terraform/test-fixtures/plan-modules-remove-provisioners/main.tf new file mode 100644 index 000000000..ce9a38866 --- /dev/null +++ b/terraform/test-fixtures/plan-modules-remove-provisioners/main.tf @@ -0,0 +1,5 @@ +resource "aws_instance" "top" {} + +# module "test" { +# source = "./parent" +# } diff --git a/terraform/test-fixtures/plan-modules-remove-provisioners/parent/child/main.tf b/terraform/test-fixtures/plan-modules-remove-provisioners/parent/child/main.tf new file mode 100644 index 000000000..b626e60c8 --- /dev/null +++ b/terraform/test-fixtures/plan-modules-remove-provisioners/parent/child/main.tf @@ -0,0 +1,2 @@ +resource "aws_instance" "foo" { +} diff --git a/terraform/test-fixtures/plan-modules-remove-provisioners/parent/main.tf b/terraform/test-fixtures/plan-modules-remove-provisioners/parent/main.tf new file mode 100644 index 000000000..fbc1aa09c --- /dev/null +++ b/terraform/test-fixtures/plan-modules-remove-provisioners/parent/main.tf @@ -0,0 +1,7 @@ +module "childone" { + source = "./child" +} + +module "childtwo" { + source = "./child" +}