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.
This commit is contained in:
parent
aa2bd0945b
commit
15ea04af8a
|
@ -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) {
|
||||
|
@ -4075,10 +4068,9 @@ func TestContext2Apply_outputOrphanModule(t *testing.T) {
|
|||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
expected = "<no state>"
|
||||
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:
|
||||
<no state>
|
||||
`)
|
||||
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(`
|
||||
<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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{},
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -713,11 +713,6 @@ const testTerraformApplyDestroyStr = `
|
|||
<no state>
|
||||
`
|
||||
|
||||
const testTerraformApplyDestroyNestedModuleStr = `
|
||||
module.child.subchild:
|
||||
<no state>
|
||||
`
|
||||
|
||||
const testTerraformApplyErrorStr = `
|
||||
aws_instance.bar:
|
||||
ID = bar
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue