delete outputs during destroy

Now that outputs are always evaluated, we still need a way to remove
them from state when they are destroyed.

Previously, outputs were removed during destroy from the same
"Applyable" node type that evaluates them. Now that we need to possibly
both evaluate and remove output during an apply, we add a new node -
NodeDestroyableOutput.

This new node is added to the graph by the DestroyOutputTransformer,
which make the new destroy node depend on all descendants of the output
node.  This ensures that the output remains in the state as long as
everything which may interpolate the output still exists.
This commit is contained in:
James Bardin 2018-01-29 19:17:31 -05:00
parent 08139557f8
commit d31fe5ab9d
5 changed files with 104 additions and 20 deletions

View File

@ -2590,24 +2590,14 @@ func TestContext2Apply_moduleDestroyOrder(t *testing.T) {
&ModuleState{ &ModuleState{
Path: rootModulePath, Path: rootModulePath,
Resources: map[string]*ResourceState{ Resources: map[string]*ResourceState{
"aws_instance.b": &ResourceState{ "aws_instance.b": resourceState("aws_instance", "b"),
Type: "aws_instance",
Primary: &InstanceState{
ID: "b",
},
},
}, },
}, },
&ModuleState{ &ModuleState{
Path: []string{"root", "child"}, Path: []string{"root", "child"},
Resources: map[string]*ResourceState{ Resources: map[string]*ResourceState{
"aws_instance.a": &ResourceState{ "aws_instance.a": resourceState("aws_instance", "a"),
Type: "aws_instance",
Primary: &InstanceState{
ID: "a",
},
},
}, },
Outputs: map[string]*OutputState{ Outputs: map[string]*OutputState{
"a_output": &OutputState{ "a_output": &OutputState{

View File

@ -126,6 +126,11 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
&DestroyValueReferenceTransformer{}, &DestroyValueReferenceTransformer{},
), ),
GraphTransformIf(
func() bool { return b.Destroy },
&DestroyOutputTransformer{},
),
// Add the node to fix the state count boundaries // Add the node to fix the state count boundaries
&CountBoundaryTransformer{}, &CountBoundaryTransformer{},

View File

@ -93,3 +93,55 @@ func (n *NodeApplyableOutput) EvalTree() EvalNode {
}, },
} }
} }
// NodeDestroyableOutput represents an output that is "destroybale":
// its application will remove the output from the state.
type NodeDestroyableOutput struct {
PathValue []string
Config *config.Output // Config is the output in the config
}
func (n *NodeDestroyableOutput) Name() string {
result := fmt.Sprintf("output.%s (destroy)", n.Config.Name)
if len(n.PathValue) > 1 {
result = fmt.Sprintf("%s.%s", modulePrefixStr(n.PathValue), result)
}
return result
}
// GraphNodeSubPath
func (n *NodeDestroyableOutput) Path() []string {
return n.PathValue
}
// RemovableIfNotTargeted
func (n *NodeDestroyableOutput) RemoveIfNotTargeted() bool {
// We need to add this so that this node will be removed if
// it isn't targeted or a dependency of a target.
return true
}
// GraphNodeReferencer
func (n *NodeDestroyableOutput) References() []string {
var result []string
result = append(result, n.Config.DependsOn...)
result = append(result, ReferencesFromConfig(n.Config.RawConfig)...)
for _, v := range result {
split := strings.Split(v, "/")
for i, s := range split {
split[i] = s + ".destroy"
}
result = append(result, strings.Join(split, "/"))
}
return result
}
// GraphNodeEvalable
func (n *NodeDestroyableOutput) EvalTree() EvalNode {
return &EvalDeleteOutput{
Name: n.Config.Name,
}
}

View File

@ -1,7 +1,10 @@
package terraform package terraform
import ( import (
"log"
"github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/dag"
) )
// OutputTransformer is a GraphTransformer that adds all the outputs // OutputTransformer is a GraphTransformer that adds all the outputs
@ -41,11 +44,6 @@ func (t *OutputTransformer) transform(g *Graph, m *module.Tree) error {
// Add all outputs here // Add all outputs here
for _, o := range os { for _, o := range os {
// Build the node.
//
// NOTE: For now this is just an "applyable" output. As we build
// new graph builders for the other operations I suspect we'll
// find a way to parameterize this, require new transforms, etc.
node := &NodeApplyableOutput{ node := &NodeApplyableOutput{
PathValue: normalizeModulePath(m.Path()), PathValue: normalizeModulePath(m.Path()),
Config: o, Config: o,
@ -57,3 +55,41 @@ func (t *OutputTransformer) transform(g *Graph, m *module.Tree) error {
return nil return nil
} }
// DestroyOutputTransformer is a GraphTransformer that adds nodes to delete
// outputs during destroy. We need to do this to ensure that no stale outputs
// are ever left in the state.
type DestroyOutputTransformer struct {
}
func (t *DestroyOutputTransformer) Transform(g *Graph) error {
for _, v := range g.Vertices() {
output, ok := v.(*NodeApplyableOutput)
if !ok {
continue
}
// create the destroy node for this output
node := &NodeDestroyableOutput{
PathValue: output.PathValue,
Config: output.Config,
}
log.Printf("[TRACE] creating %s", node.Name())
g.Add(node)
deps, err := g.Descendents(v)
if err != nil {
return err
}
// the destroy node must depend on the eval node
deps.Add(v)
for _, d := range deps.List() {
log.Printf("[TRACE] %s depends on %s", node.Name(), dag.VertexName(d))
g.Connect(dag.BasicEdge(node, d))
}
}
return nil
}

View File

@ -96,11 +96,12 @@ func (t *DestroyValueReferenceTransformer) Transform(g *Graph) error {
// reverse any outgoing edges so that the value is evaluated first. // reverse any outgoing edges so that the value is evaluated first.
for _, e := range g.EdgesFrom(v) { for _, e := range g.EdgesFrom(v) {
target := e.Target() target := e.Target()
switch target.(type) {
case *NodeApplyableOutput, *NodeLocal: // only destroy nodes will be evaluated in reverse
// don't reverse other values if _, ok := target.(GraphNodeDestroyer); !ok {
continue continue
} }
log.Printf("[TRACE] output dep: %s", dag.VertexName(target)) log.Printf("[TRACE] output dep: %s", dag.VertexName(target))
g.RemoveEdge(e) g.RemoveEdge(e)