Merge pull request #16599 from hashicorp/jbardin/orphaned-module-outputs

Remove modules and module outputs from state
This commit is contained in:
James Bardin 2017-11-09 12:33:35 -05:00 committed by GitHub
commit ca191a3b2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 214 additions and 103 deletions

View File

@ -342,11 +342,7 @@ func TestContext2Apply_resourceDependsOnModuleStateOnly(t *testing.T) {
t.Fatal("should check")
}
checkStateString(t, state, `
<no state>
module.child:
<no state>
`)
checkStateString(t, state, "<no state>")
}
}
@ -2698,10 +2694,7 @@ func TestContext2Apply_moduleOrphanInheritAlias(t *testing.T) {
t.Fatal("must call configure")
}
checkStateString(t, state, `
module.child:
<no state>
`)
checkStateString(t, state, "")
}
func TestContext2Apply_moduleOrphanProvider(t *testing.T) {
@ -4036,7 +4029,7 @@ func TestContext2Apply_outputOrphanModule(t *testing.T) {
"aws": testProviderFuncFixed(p),
},
),
State: state,
State: state.DeepCopy(),
})
if _, err := ctx.Plan(); err != nil {
@ -4051,7 +4044,33 @@ 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)
}
actual = strings.TrimSpace(state.String())
if actual != "" {
t.Fatalf("expected no state, got:\n%s", actual)
}
}
@ -6100,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)
}
}
@ -6150,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:
<no state>
`)
if actual != expected {
t.Fatalf("bad: \n%s", actual)
if actual != "" {
t.Fatalf("epected no state, got: %s", actual)
}
}
@ -9080,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(`
<no state>
module.child:
<no state>
module.mod.removed:
<no state>`)
want := strings.TrimSpace("<no state>")
if got != want {
t.Fatalf("wrong final state\ngot:\n%s\nwant:\n%s", got, want)
}

View File

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

View File

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

View File

@ -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},
@ -109,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{},

View File

@ -0,0 +1,77 @@
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,
},
}
}
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 {
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
}
state.Modules = append(state.Modules[:i], state.Modules[i+1:]...)
break
}
}
return nil, nil
}

View File

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

View File

@ -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
}
// RemovedOutputs returns a list of outputs that are in the State but aren't
// present in the configuration itself.
func (m *ModuleState) RemovedOutputs(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)
}
@ -1314,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 {

View File

@ -713,11 +713,6 @@ const testTerraformApplyDestroyStr = `
<no state>
`
const testTerraformApplyDestroyNestedModuleStr = `
module.child.subchild:
<no state>
`
const testTerraformApplyErrorStr = `
aws_instance.bar:
ID = bar

View File

@ -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 {
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.RemovedOutputs(c) {
g.Add(&NodeOutputOrphan{OutputName: n, PathValue: path})
}
return nil

View File

@ -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
}
}
if selfRef {
continue
}
matches = append(matches, p)
}
matches = append(matches, parents...)
break
}

View File

@ -0,0 +1,32 @@
package terraform
import (
"log"
"github.com/hashicorp/terraform/config/module"
)
// 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
}
func (t *RemovedModuleTransformer) Transform(g *Graph) error {
// nothing to remove if there's no state!
if t.State == nil {
return nil
}
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))
g.Add(&NodeModuleRemoved{PathValue: m.Path})
}
return nil
}