From 68b07a766a0f40d0d0e5ee30e14ab2e527c9d2ad Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 8 Nov 2017 11:10:32 -0500 Subject: [PATCH 01/11] add failing test for orphaned modules outputs When an entire module is removed from the config, that module's outputs are not removed from the state. --- terraform/context_apply_test.go | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index 094029044..04f8b1db2 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -4036,7 +4036,7 @@ func TestContext2Apply_outputOrphanModule(t *testing.T) { "aws": testProviderFuncFixed(p), }, ), - State: state, + State: state.DeepCopy(), }) if _, err := ctx.Plan(); err != nil { @@ -4051,7 +4051,34 @@ func TestContext2Apply_outputOrphanModule(t *testing.T) { actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyOutputOrphanModuleStr) if actual != expected { - t.Fatalf("bad: \n%s", actual) + t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual) + } + + // now apply with no module in the config, which should remove the + // remaining output + ctx = testContext2(t, &ContextOpts{ + Module: module.NewEmptyTree(), + ProviderResolver: ResourceProviderResolverFixed( + map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + ), + State: state.DeepCopy(), + }) + + if _, err := ctx.Plan(); err != nil { + t.Fatalf("err: %s", err) + } + + state, err = ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + expected = "" + actual = strings.TrimSpace(state.String()) + if actual != expected { + t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual) } } From 9283568dca068ea6dd897411ca1125f2d29c4af9 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 8 Nov 2017 11:11:26 -0500 Subject: [PATCH 02/11] add OrphanOutputTransformer to the plan graph make sure orphaned outputs appear in the plan as well --- terraform/graph_builder_plan.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/terraform/graph_builder_plan.go b/terraform/graph_builder_plan.go index 5d625e051..564fecb1f 100644 --- a/terraform/graph_builder_plan.go +++ b/terraform/graph_builder_plan.go @@ -84,6 +84,12 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { Module: b.Module, }, + // Create orphan output nodes + &OrphanOutputTransformer{ + Module: b.Module, + State: b.State, + }, + // Attach the configuration to any resources &AttachResourceConfigTransformer{Module: b.Module}, From aa2bd0945b52ed524011dc5c876ed66e4d2e09b6 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 8 Nov 2017 14:01:54 -0500 Subject: [PATCH 03/11] properly find orphaned outputs You can't find orphans by walking the config, because by definition orphans aren't in the config. Leaving the broken test for when empty modules are removed from the state as well. --- terraform/state.go | 31 +++++++++++++++++-- terraform/transform_orphan_output.go | 45 +++++++++++----------------- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/terraform/state.go b/terraform/state.go index 89a404847..1818364b6 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -1089,7 +1089,7 @@ func (m *ModuleState) Orphans(c *config.Config) []string { defer m.Unlock() keys := make(map[string]struct{}) - for k, _ := range m.Resources { + for k := range m.Resources { keys[k] = struct{}{} } @@ -1097,7 +1097,7 @@ func (m *ModuleState) Orphans(c *config.Config) []string { for _, r := range c.Resources { delete(keys, r.Id()) - for k, _ := range keys { + for k := range keys { if strings.HasPrefix(k, r.Id()+".") { delete(keys, k) } @@ -1106,7 +1106,32 @@ func (m *ModuleState) Orphans(c *config.Config) []string { } result := make([]string, 0, len(keys)) - for k, _ := range keys { + for k := range keys { + result = append(result, k) + } + + return result +} + +// OrphanOutputs returns a list of outputs that are in the State but aren't +// present in the configuration itself. +func (m *ModuleState) OrphanOutputs(c *config.Config) []string { + m.Lock() + defer m.Unlock() + + keys := make(map[string]struct{}) + for k := range m.Outputs { + keys[k] = struct{}{} + } + + if c != nil { + for _, o := range c.Outputs { + delete(keys, o.Name) + } + } + + result := make([]string, 0, len(keys)) + for k := range keys { result = append(result, k) } diff --git a/terraform/transform_orphan_output.go b/terraform/transform_orphan_output.go index 49568d5bc..f27472dc2 100644 --- a/terraform/transform_orphan_output.go +++ b/terraform/transform_orphan_output.go @@ -21,43 +21,32 @@ func (t *OrphanOutputTransformer) Transform(g *Graph) error { return nil } - return t.transform(g, t.Module) -} - -func (t *OrphanOutputTransformer) transform(g *Graph, m *module.Tree) error { - // Get our configuration, and recurse into children - var c *config.Config - if m != nil { - c = m.Config() - for _, child := range m.Children() { - if err := t.transform(g, child); err != nil { - return err - } + for _, ms := range t.State.Modules { + if err := t.transform(g, ms); err != nil { + return err } } + return nil +} - // Get the state. If there is no state, then we have no orphans! - path := normalizeModulePath(m.Path()) - state := t.State.ModuleByPath(path) - if state == nil { +func (t *OrphanOutputTransformer) transform(g *Graph, ms *ModuleState) error { + if ms == nil { return nil } - // Make a map of the valid outputs - valid := make(map[string]struct{}) - for _, o := range c.Outputs { - valid[o.Name] = struct{}{} + path := normalizeModulePath(ms.Path) + + // Get the config for this path, which is nil if the entire module has been + // removed. + var c *config.Config + if m := t.Module.Child(path[1:]); m != nil { + c = m.Config() } - // Go through the outputs and find the ones that aren't in our config. - for n, _ := range state.Outputs { - // If it is in the valid map, then ignore - if _, ok := valid[n]; ok { - continue - } - - // Orphan! + // add all the orphaned outputs to the graph + for _, n := range ms.OrphanOutputs(c) { g.Add(&NodeOutputOrphan{OutputName: n, PathValue: path}) + } return nil From 15ea04af8a70a80ebe1ea37815cf0784fc693760 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 8 Nov 2017 18:49:26 -0500 Subject: [PATCH 04/11] remove modules from state Remove the module entry from the state if a module is no longer in the configuration. Modules are not removed if there are any existing resources with the module path as a prefix. The only time this should be the case is if a module was removed in the config, but the apply didn't target that module. Create a NodeModuleRemoved and an associated EvalDeleteModule to track the module in the graph then remove it from the state. The NodeModuleRemoved dependencies are simply any other node which contains the module path as a prefix in its path. This could have probably been done much easier as a step in pruning the state, but modules are going to have to be promoted to full graph nodes anyway in order to support count. --- terraform/context_apply_test.go | 38 ++++---------- terraform/eval_state.go | 31 ----------- terraform/graph_builder_apply.go | 3 ++ terraform/node_module_removed.go | 71 ++++++++++++++++++++++++++ terraform/state.go | 4 ++ terraform/terraform_test.go | 5 -- terraform/transform_removed_modules.go | 55 ++++++++++++++++++++ 7 files changed, 142 insertions(+), 65 deletions(-) create mode 100644 terraform/node_module_removed.go create mode 100644 terraform/transform_removed_modules.go diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index 04f8b1db2..4c9ec48f6 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -342,11 +342,7 @@ func TestContext2Apply_resourceDependsOnModuleStateOnly(t *testing.T) { t.Fatal("should check") } - checkStateString(t, state, ` - -module.child: - - `) + checkStateString(t, state, "") } } @@ -2698,10 +2694,7 @@ func TestContext2Apply_moduleOrphanInheritAlias(t *testing.T) { t.Fatal("must call configure") } - checkStateString(t, state, ` -module.child: - - `) + checkStateString(t, state, "") } func TestContext2Apply_moduleOrphanProvider(t *testing.T) { @@ -4075,10 +4068,9 @@ func TestContext2Apply_outputOrphanModule(t *testing.T) { t.Fatalf("err: %s", err) } - expected = "" actual = strings.TrimSpace(state.String()) - if actual != expected { - t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual) + if actual != "" { + t.Fatalf("expected no state, got:\n%s", actual) } } @@ -6127,9 +6119,8 @@ func TestContext2Apply_destroyNestedModule(t *testing.T) { // Test that things were destroyed actual := strings.TrimSpace(state.String()) - expected := strings.TrimSpace(testTerraformApplyDestroyNestedModuleStr) - if actual != expected { - t.Fatalf("bad: \n%s", actual) + if actual != "" { + t.Fatalf("expected no state, got: %s", actual) } } @@ -6177,12 +6168,8 @@ func TestContext2Apply_destroyDeeplyNestedModule(t *testing.T) { // Test that things were destroyed actual := strings.TrimSpace(state.String()) - expected := strings.TrimSpace(` -module.child.subchild.subsubchild: - - `) - if actual != expected { - t.Fatalf("bad: \n%s", actual) + if actual != "" { + t.Fatalf("epected no state, got: %s", actual) } } @@ -9107,14 +9094,7 @@ func TestContext2Apply_destroyWithProviders(t *testing.T) { got := strings.TrimSpace(state.String()) - // This should fail once modules are removed from the state entirely. - want := strings.TrimSpace(` - -module.child: - -module.mod.removed: - `) - + want := strings.TrimSpace("") if got != want { t.Fatalf("wrong final state\ngot:\n%s\nwant:\n%s", got, want) } diff --git a/terraform/eval_state.go b/terraform/eval_state.go index 1f67e3d86..11826907c 100644 --- a/terraform/eval_state.go +++ b/terraform/eval_state.go @@ -214,37 +214,6 @@ func writeInstanceToState( return nil, nil } -// EvalClearPrimaryState is an EvalNode implementation that clears the primary -// instance from a resource state. -type EvalClearPrimaryState struct { - Name string -} - -func (n *EvalClearPrimaryState) Eval(ctx EvalContext) (interface{}, error) { - state, lock := ctx.State() - - // Get a read lock so we can access this instance - lock.RLock() - defer lock.RUnlock() - - // Look for the module state. If we don't have one, then it doesn't matter. - mod := state.ModuleByPath(ctx.Path()) - if mod == nil { - return nil, nil - } - - // Look for the resource state. If we don't have one, then it is okay. - rs := mod.Resources[n.Name] - if rs == nil { - return nil, nil - } - - // Clear primary from the resource state - rs.Primary = nil - - return nil, nil -} - // EvalDeposeState is an EvalNode implementation that takes the primary // out of a state and makes it Deposed. This is done at the beginning of // create-before-destroy calls so that the create can create while preserving diff --git a/terraform/graph_builder_apply.go b/terraform/graph_builder_apply.go index 614da2c85..2f1a988be 100644 --- a/terraform/graph_builder_apply.go +++ b/terraform/graph_builder_apply.go @@ -133,6 +133,9 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { &CloseProviderTransformer{}, &CloseProvisionerTransformer{}, + // Remove modules no longer present in the config + &RemovedModuleTransformer{Module: b.Module, State: b.State}, + // Single root &RootTransformer{}, } diff --git a/terraform/node_module_removed.go b/terraform/node_module_removed.go new file mode 100644 index 000000000..36f36ef18 --- /dev/null +++ b/terraform/node_module_removed.go @@ -0,0 +1,71 @@ +package terraform + +import ( + "fmt" + "log" + "reflect" +) + +// NodeModuleRemoved represents a module that is no longer in the +// config. +type NodeModuleRemoved struct { + PathValue []string +} + +func (n *NodeModuleRemoved) Name() string { + return fmt.Sprintf("%s (removed)", modulePrefixStr(n.PathValue)) +} + +// GraphNodeSubPath +func (n *NodeModuleRemoved) Path() []string { + return n.PathValue +} + +// GraphNodeEvalable +func (n *NodeModuleRemoved) EvalTree() EvalNode { + return &EvalOpFilter{ + Ops: []walkOperation{walkRefresh, walkApply, walkDestroy}, + Node: &EvalDeleteModule{ + PathValue: n.PathValue, + }, + } +} + +// EvalDeleteModule is an EvalNode implementation that removes an empty module +// entry from the state. +type EvalDeleteModule struct { + PathValue []string +} + +func (n *EvalDeleteModule) Eval(ctx EvalContext) (interface{}, error) { + state, lock := ctx.State() + if state == nil { + return nil, nil + } + + // Get a write lock so we can access this instance + lock.Lock() + defer lock.Unlock() + + // Make sure we have a clean state + // Destroyed resources aren't deleted, they're written with an ID of "". + state.prune() + + // find the module and delete it + for i, m := range state.Modules { + if reflect.DeepEqual(m.Path, n.PathValue) { + if !m.Empty() { + // a targeted apply may leave module resources even without a config, + // so just log this and return. + log.Printf("[DEBUG] cannot remove module %s, not empty", modulePrefixStr(n.PathValue)) + break + } + tail := len(state.Modules) - 1 + state.Modules[i] = state.Modules[tail] + state.Modules = state.Modules[:tail] + break + } + } + + return nil, nil +} diff --git a/terraform/state.go b/terraform/state.go index 1818364b6..8773745fd 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -1339,6 +1339,10 @@ func (m *ModuleState) String() string { return buf.String() } +func (m *ModuleState) Empty() bool { + return len(m.Locals) == 0 && len(m.Outputs) == 0 && len(m.Resources) == 0 +} + // ResourceStateKey is a structured representation of the key used for the // ModuleState.Resources mapping type ResourceStateKey struct { diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 0a84e34e6..2deb44153 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -713,11 +713,6 @@ const testTerraformApplyDestroyStr = ` ` -const testTerraformApplyDestroyNestedModuleStr = ` -module.child.subchild: - -` - const testTerraformApplyErrorStr = ` aws_instance.bar: ID = bar diff --git a/terraform/transform_removed_modules.go b/terraform/transform_removed_modules.go new file mode 100644 index 000000000..08816f10f --- /dev/null +++ b/terraform/transform_removed_modules.go @@ -0,0 +1,55 @@ +package terraform + +import ( + "log" + "strings" + + "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/dag" +) + +type RemovedModuleTransformer struct { + Module *module.Tree // root module + State *State +} + +func (t *RemovedModuleTransformer) Transform(g *Graph) error { + // nothing to remove if there's no state! + if t.State == nil { + return nil + } + + // get a map of all nodes by path, so we can connect anything that might be + // in the module + refMap := map[string][]dag.Vertex{} + for _, v := range g.Vertices() { + if pn, ok := v.(GraphNodeSubPath); ok { + path := normalizeModulePath(pn.Path())[1:] + p := modulePrefixStr(path) + refMap[p] = append(refMap[p], v) + } + } + + for _, m := range t.State.Modules { + c := t.Module.Child(m.Path[1:]) + if c != nil { + continue + } + + log.Printf("[DEBUG] module %s no longer in config\n", modulePrefixStr(m.Path)) + + node := &NodeModuleRemoved{PathValue: m.Path} + g.Add(node) + + // connect this to anything that contains the module's path + refPath := modulePrefixStr(m.Path) + for p, nodes := range refMap { + if strings.HasPrefix(p, refPath) { + for _, parent := range nodes { + g.Connect(dag.BasicEdge(node, parent)) + } + } + } + } + return nil +} From ccc9b1d76786dcde9218364cd5cb922bf20289a0 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 8 Nov 2017 22:10:43 -0500 Subject: [PATCH 05/11] replacing orphaned with removed --- terraform/state.go | 4 ++-- terraform/transform_orphan_output.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/terraform/state.go b/terraform/state.go index 8773745fd..5bc2f8a04 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -1113,9 +1113,9 @@ func (m *ModuleState) Orphans(c *config.Config) []string { return result } -// OrphanOutputs returns a list of outputs that are in the State but aren't +// RemovedOutputs returns a list of outputs that are in the State but aren't // present in the configuration itself. -func (m *ModuleState) OrphanOutputs(c *config.Config) []string { +func (m *ModuleState) RemovedOutputs(c *config.Config) []string { m.Lock() defer m.Unlock() diff --git a/terraform/transform_orphan_output.go b/terraform/transform_orphan_output.go index f27472dc2..aea2bd0ed 100644 --- a/terraform/transform_orphan_output.go +++ b/terraform/transform_orphan_output.go @@ -44,7 +44,7 @@ func (t *OrphanOutputTransformer) transform(g *Graph, ms *ModuleState) error { } // add all the orphaned outputs to the graph - for _, n := range ms.OrphanOutputs(c) { + for _, n := range ms.RemovedOutputs(c) { g.Add(&NodeOutputOrphan{OutputName: n, PathValue: path}) } From 14cc654b1674d2c3eb6be9716f621fb6b5d04dee Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 8 Nov 2017 22:12:35 -0500 Subject: [PATCH 06/11] preserve order when removing module from state --- terraform/node_module_removed.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/terraform/node_module_removed.go b/terraform/node_module_removed.go index 36f36ef18..ce5056929 100644 --- a/terraform/node_module_removed.go +++ b/terraform/node_module_removed.go @@ -60,9 +60,7 @@ func (n *EvalDeleteModule) Eval(ctx EvalContext) (interface{}, error) { log.Printf("[DEBUG] cannot remove module %s, not empty", modulePrefixStr(n.PathValue)) break } - tail := len(state.Modules) - 1 - state.Modules[i] = state.Modules[tail] - state.Modules = state.Modules[:tail] + state.Modules = append(state.Modules[:i], state.Modules[i+1:]...) break } } From 5915d883d2280c6a8a346a9f670a8fab16e07b92 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 9 Nov 2017 10:30:55 -0500 Subject: [PATCH 07/11] make NodeModuleRemoved a GraphNodeReferencer This was the node can be automatically connected by the ReferenceTransformer. --- terraform/node_module_removed.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/terraform/node_module_removed.go b/terraform/node_module_removed.go index ce5056929..bb3e5ee1e 100644 --- a/terraform/node_module_removed.go +++ b/terraform/node_module_removed.go @@ -31,6 +31,14 @@ func (n *NodeModuleRemoved) EvalTree() EvalNode { } } +func (n *NodeModuleRemoved) ReferenceGlobal() bool { + return true +} + +func (n *NodeModuleRemoved) References() []string { + return []string{modulePrefixStr(n.PathValue)} +} + // EvalDeleteModule is an EvalNode implementation that removes an empty module // entry from the state. type EvalDeleteModule struct { From 3916f3a5a3685f2f9584d454e11ad0cbdc75ddd6 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 9 Nov 2017 10:32:01 -0500 Subject: [PATCH 08/11] only add nodes in RemovedModuleTransformer Let the ReferenceTransformer connect them once it's fixed. --- terraform/transform_removed_modules.go | 29 +++----------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/terraform/transform_removed_modules.go b/terraform/transform_removed_modules.go index 08816f10f..2e05edbaa 100644 --- a/terraform/transform_removed_modules.go +++ b/terraform/transform_removed_modules.go @@ -2,12 +2,12 @@ package terraform import ( "log" - "strings" "github.com/hashicorp/terraform/config/module" - "github.com/hashicorp/terraform/dag" ) +// RemoveModuleTransformer implements GraphTransformer to add nodes indicating +// when a module was removed from the configuration. type RemovedModuleTransformer struct { Module *module.Tree // root module State *State @@ -19,17 +19,6 @@ func (t *RemovedModuleTransformer) Transform(g *Graph) error { return nil } - // get a map of all nodes by path, so we can connect anything that might be - // in the module - refMap := map[string][]dag.Vertex{} - for _, v := range g.Vertices() { - if pn, ok := v.(GraphNodeSubPath); ok { - path := normalizeModulePath(pn.Path())[1:] - p := modulePrefixStr(path) - refMap[p] = append(refMap[p], v) - } - } - for _, m := range t.State.Modules { c := t.Module.Child(m.Path[1:]) if c != nil { @@ -37,19 +26,7 @@ func (t *RemovedModuleTransformer) Transform(g *Graph) error { } log.Printf("[DEBUG] module %s no longer in config\n", modulePrefixStr(m.Path)) - - node := &NodeModuleRemoved{PathValue: m.Path} - g.Add(node) - - // connect this to anything that contains the module's path - refPath := modulePrefixStr(m.Path) - for p, nodes := range refMap { - if strings.HasPrefix(p, refPath) { - for _, parent := range nodes { - g.Connect(dag.BasicEdge(node, parent)) - } - } - } + g.Add(&NodeModuleRemoved{PathValue: m.Path}) } return nil } From 7e4dcdb9f0ece525e4cb53cb3dd0753a5e93fe7c Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 9 Nov 2017 10:34:56 -0500 Subject: [PATCH 09/11] run RemovedModuleTransformer before References Also add RemovedModuleTransformer to the plan graph for parity. --- terraform/graph_builder_apply.go | 6 +++--- terraform/graph_builder_plan.go | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/terraform/graph_builder_apply.go b/terraform/graph_builder_apply.go index 2f1a988be..1f826e1d9 100644 --- a/terraform/graph_builder_apply.go +++ b/terraform/graph_builder_apply.go @@ -113,6 +113,9 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { // Add module variables &ModuleVariableTransformer{Module: b.Module}, + // Remove modules no longer present in the config + &RemovedModuleTransformer{Module: b.Module, State: b.State}, + // Connect references so ordering is correct &ReferenceTransformer{}, @@ -133,9 +136,6 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { &CloseProviderTransformer{}, &CloseProvisionerTransformer{}, - // Remove modules no longer present in the config - &RemovedModuleTransformer{Module: b.Module, State: b.State}, - // Single root &RootTransformer{}, } diff --git a/terraform/graph_builder_plan.go b/terraform/graph_builder_plan.go index 564fecb1f..f8dd0fc93 100644 --- a/terraform/graph_builder_plan.go +++ b/terraform/graph_builder_plan.go @@ -115,6 +115,9 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { Module: b.Module, }, + // Remove modules no longer present in the config + &RemovedModuleTransformer{Module: b.Module, State: b.State}, + // Connect so that the references are ready for targeting. We'll // have to connect again later for providers and so on. &ReferenceTransformer{}, From e0ad3300c61c5581dae3d1b781c4a1b418891575 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 9 Nov 2017 10:36:42 -0500 Subject: [PATCH 10/11] fix References used by the ReferenceTransformer There was a bug where all references would be discarded in the case when a self-reference was encountered. Since a module references all descendants by it's own path, it returns a self-reference by definition. --- terraform/transform_reference.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/terraform/transform_reference.go b/terraform/transform_reference.go index 2560e5ad6..85a82a651 100644 --- a/terraform/transform_reference.go +++ b/terraform/transform_reference.go @@ -127,6 +127,7 @@ func (m *ReferenceMap) References(v dag.Vertex) ([]dag.Vertex, []string) { var matches []dag.Vertex var missing []string prefix := m.prefix(v) + for _, ns := range rn.References() { found := false for _, n := range strings.Split(ns, "/") { @@ -139,19 +140,14 @@ func (m *ReferenceMap) References(v dag.Vertex) ([]dag.Vertex, []string) { // Mark that we found a match found = true - // Make sure this isn't a self reference, which isn't included - selfRef := false for _, p := range parents { + // don't include self-references if p == v { - selfRef = true - break + continue } - } - if selfRef { - continue + matches = append(matches, p) } - matches = append(matches, parents...) break } From e3ea3150aea21d55065f741f90a58e20bd38b9f9 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 9 Nov 2017 10:52:46 -0500 Subject: [PATCH 11/11] make NodeOutputOrphan referenceable The removed output need to be referencable so if its parent module is also being remove, the removal happens in the correct order. --- terraform/node_output_orphan.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/terraform/node_output_orphan.go b/terraform/node_output_orphan.go index 636a15df1..0fd1554a9 100644 --- a/terraform/node_output_orphan.go +++ b/terraform/node_output_orphan.go @@ -19,6 +19,11 @@ func (n *NodeOutputOrphan) Name() string { return result } +// GraphNodeReferenceable +func (n *NodeOutputOrphan) ReferenceableName() []string { + return []string{"output." + n.OutputName} +} + // GraphNodeSubPath func (n *NodeOutputOrphan) Path() []string { return n.PathValue