terraform: create before destroy

This commit is contained in:
Mitchell Hashimoto 2015-02-13 15:57:37 -08:00
parent 5c2639bd10
commit f7f1f17b49
11 changed files with 277 additions and 25 deletions

View File

@ -3369,7 +3369,6 @@ func TestContext2Apply_provisionerFail(t *testing.T) {
} }
} }
/*
func TestContext2Apply_provisionerFail_createBeforeDestroy(t *testing.T) { func TestContext2Apply_provisionerFail_createBeforeDestroy(t *testing.T) {
m := testModule(t, "apply-provisioner-fail-create-before") m := testModule(t, "apply-provisioner-fail-create-before")
p := testProvider("aws") p := testProvider("aws")

View File

@ -126,3 +126,99 @@ func (n *EvalWriteState) Eval(
func (n *EvalWriteState) Type() EvalType { func (n *EvalWriteState) Type() EvalType {
return EvalTypeNull return EvalTypeNull
} }
// EvalDeposeState is an EvalNode implementation that reads the
// InstanceState for a specific resource out of the state.
type EvalDeposeState struct {
Name string
}
func (n *EvalDeposeState) Args() ([]EvalNode, []EvalType) {
return nil, nil
}
// TODO: test
func (n *EvalDeposeState) Eval(
ctx EvalContext, args []interface{}) (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
}
// If we don't have a primary, we have nothing to depose
if rs.Primary == nil {
return nil, nil
}
// Depose to the tainted
rs.Tainted = append(rs.Tainted, rs.Primary)
rs.Primary = nil
return nil, nil
}
func (n *EvalDeposeState) Type() EvalType {
return EvalTypeNull
}
// EvalUndeposeState is an EvalNode implementation that reads the
// InstanceState for a specific resource out of the state.
type EvalUndeposeState struct {
Name string
}
func (n *EvalUndeposeState) Args() ([]EvalNode, []EvalType) {
return nil, nil
}
// TODO: test
func (n *EvalUndeposeState) Eval(
ctx EvalContext, args []interface{}) (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
}
// If we don't have any tainted, then we don't have anything to do
if len(rs.Tainted) == 0 {
return nil, nil
}
// Undepose to the tainted
idx := len(rs.Tainted) - 1
rs.Primary = rs.Tainted[idx]
rs.Tainted[idx] = nil
rs.Tainted = rs.Tainted[:idx]
return nil, nil
}
func (n *EvalUndeposeState) Type() EvalType {
return EvalTypeNull
}

View File

@ -81,7 +81,6 @@ func (b *BuiltinGraphBuilder) Steps() []GraphTransformer {
// Create all our resources from the configuration and state // Create all our resources from the configuration and state
&ConfigTransformer{Module: b.Root}, &ConfigTransformer{Module: b.Root},
&OrphanTransformer{State: b.State, Module: b.Root}, &OrphanTransformer{State: b.State, Module: b.Root},
&TaintedTransformer{State: b.State},
// Provider-related transformations // Provider-related transformations
&MissingProviderTransformer{Providers: b.Providers}, &MissingProviderTransformer{Providers: b.Providers},
@ -105,6 +104,7 @@ func (b *BuiltinGraphBuilder) Steps() []GraphTransformer {
// Create the destruction nodes // Create the destruction nodes
&DestroyTransformer{}, &DestroyTransformer{},
&CreateBeforeDestroyTransformer{},
&PruneDestroyTransformer{Diff: b.Diff}, &PruneDestroyTransformer{Diff: b.Diff},
// Make sure we create one root // Make sure we create one root

View File

@ -177,7 +177,7 @@ type GraphNodeConfigResource struct {
// that logically this node is where it would happen. // that logically this node is where it would happen.
Destroy bool Destroy bool
destroyNode dag.Vertex destroyNode GraphNodeDestroy
} }
func (n *GraphNodeConfigResource) DependableName() []string { func (n *GraphNodeConfigResource) DependableName() []string {
@ -248,6 +248,11 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
State: state, State: state,
View: n.Resource.Id(), View: n.Resource.Id(),
}) })
steps = append(steps, &TaintedTransformer{
State: state,
View: n.Resource.Id(),
})
} }
// Always end with the root being added // Always end with the root being added
@ -288,7 +293,7 @@ func (n *GraphNodeConfigResource) ProvisionedBy() []string {
} }
// GraphNodeDestroyable // GraphNodeDestroyable
func (n *GraphNodeConfigResource) DestroyNode() dag.Vertex { func (n *GraphNodeConfigResource) DestroyNode() GraphNodeDestroy {
// If we're already a destroy node, then don't do anything // If we're already a destroy node, then don't do anything
if n.Destroy { if n.Destroy {
return nil return nil
@ -300,13 +305,36 @@ func (n *GraphNodeConfigResource) DestroyNode() dag.Vertex {
} }
// Just make a copy that is set to destroy // Just make a copy that is set to destroy
result := *n result := &graphNodeResourceDestroy{
GraphNodeConfigResource: *n,
Original: n,
}
result.Destroy = true result.Destroy = true
n.destroyNode = &result n.destroyNode = result
return n.destroyNode return n.destroyNode
} }
// graphNodeResourceDestroy represents the logical destruction of a
// resource. This node doesn't mean it will be destroyed for sure, but
// instead that if a destroy were to happen, it must happen at this point.
type graphNodeResourceDestroy struct {
GraphNodeConfigResource
Original *GraphNodeConfigResource
}
func (n *graphNodeResourceDestroy) CreateBeforeDestroy() bool {
return n.Original.Resource.Lifecycle.CreateBeforeDestroy
}
func (n *graphNodeResourceDestroy) CreateNode() dag.Vertex {
return n.Original
}
func (n *graphNodeResourceDestroy) DiffId() string {
return ""
}
// graphNodeModuleExpanded represents a module where the graph has // graphNodeModuleExpanded represents a module where the graph has
// been expanded. It stores the graph of the module as well as a reference // been expanded. It stores the graph of the module as well as a reference
// to the map of variables. // to the map of variables.

View File

@ -663,7 +663,7 @@ func graphAddDiff(g *depgraph.Graph, gDiff *Diff, d *ModuleDiff) error {
} }
// Set the ReplacePrimary flag on the new instance so that // Set the ReplacePrimary flag on the new instance so that
// it will become the new primary, and Diposed flag on the // it will become the new primary, and Deposed flag on the
// existing instance so that it will step down // existing instance so that it will step down
rn.Resource.Flags |= FlagReplacePrimary rn.Resource.Flags |= FlagReplacePrimary
newNode.Resource.Flags |= FlagDeposed newNode.Resource.Flags |= FlagDeposed

View File

@ -0,0 +1,9 @@
resource "aws_instance" "web" {
lifecycle {
create_before_destroy = true
}
}
resource "aws_load_balancer" "lb" {
member = "${aws_instance.web.id}"
}

View File

@ -14,14 +14,24 @@ type GraphNodeDestroyable interface {
// DestroyNode returns the node used for the destroy. This should // DestroyNode returns the node used for the destroy. This should
// return the same node every time so that it can be used later for // return the same node every time so that it can be used later for
// lookups as well. // lookups as well.
DestroyNode() dag.Vertex DestroyNode() GraphNodeDestroy
} }
// GraphNodeDestroyer is the interface that must implemented by // GraphNodeDestroy is the interface that must implemented by
// nodes that destroy. // nodes that destroy.
type GraphNodeDestroyer interface { type GraphNodeDestroy interface {
dag.Vertex dag.Vertex
// CreateBeforeDestroy is called to check whether this node
// should be created before it is destroyed. The CreateBeforeDestroy
// transformer uses this information to setup the graph.
CreateBeforeDestroy() bool
// CreateNode returns the node used for the create side of this
// destroy. This must already exist within the graph.
CreateNode() dag.Vertex
// Not used right now
DiffId() string DiffId() string
} }
@ -94,6 +104,52 @@ func (t *DestroyTransformer) Transform(g *Graph) error {
return nil return nil
} }
// CreateBeforeDestroyTransformer is a GraphTransformer that modifies
// the destroys of some nodes so that the creation happens before the
// destroy.
type CreateBeforeDestroyTransformer struct{}
func (t *CreateBeforeDestroyTransformer) Transform(g *Graph) error {
for _, v := range g.Vertices() {
// We only care to use the destroy nodes
dn, ok := v.(GraphNodeDestroy)
if !ok {
continue
}
// If the node doesn't need to create before destroy, then continue
if !dn.CreateBeforeDestroy() {
continue
}
// Get the creation side of this node
cn := dn.CreateNode()
// Take all the things which depend on the web creation and
// make them dependencies on the destruction. Clarifying this
// with an example: if you have a web server and a load balancer
// and the load balancer depends on the web server, then when we
// do a create before destroy, we want to make sure the steps are:
//
// 1.) Create new web server
// 2.) Update load balancer
// 3.) Delete old web server
//
// This ensures that.
for _, sourceRaw := range g.UpEdges(cn).List() {
source := sourceRaw.(dag.Vertex)
g.Connect(dag.BasicEdge(dn, source))
}
// Swap the edge so that the destroy depends on the creation
// happening...
g.Connect(dag.BasicEdge(dn, cn))
g.RemoveEdge(dag.BasicEdge(cn, dn))
}
return nil
}
// PruneDestroyTransformer is a GraphTransformer that removes the destroy // PruneDestroyTransformer is a GraphTransformer that removes the destroy
// nodes that aren't in the diff. // nodes that aren't in the diff.
type PruneDestroyTransformer struct { type PruneDestroyTransformer struct {
@ -108,7 +164,7 @@ func (t *PruneDestroyTransformer) Transform(g *Graph) error {
for _, v := range g.Vertices() { for _, v := range g.Vertices() {
// If it is not a destroyer, we don't care // If it is not a destroyer, we don't care
dn, ok := v.(GraphNodeDestroyer) dn, ok := v.(GraphNodeDestroy)
if !ok { if !ok {
continue continue
} }

View File

@ -30,6 +30,38 @@ func TestDestroyTransformer(t *testing.T) {
} }
} }
func TestCreateBeforeDestroyTransformer(t *testing.T) {
mod := testModule(t, "transform-create-before-destroy-basic")
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
tf := &DestroyTransformer{}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
tf := &CreateBeforeDestroyTransformer{}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformCreateBeforeDestroyBasicStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
const testTransformDestroyBasicStr = ` const testTransformDestroyBasicStr = `
aws_instance.bar aws_instance.bar
aws_instance.bar (destroy) aws_instance.bar (destroy)
@ -40,3 +72,15 @@ aws_instance.foo
aws_instance.foo (destroy) aws_instance.foo (destroy)
aws_instance.bar (destroy) aws_instance.bar (destroy)
` `
const testTransformCreateBeforeDestroyBasicStr = `
aws_instance.web
aws_instance.web (destroy)
aws_instance.web
aws_load_balancer.lb
aws_load_balancer.lb (destroy)
aws_load_balancer.lb
aws_instance.web
aws_load_balancer.lb (destroy)
aws_load_balancer.lb (destroy)
`

View File

@ -251,6 +251,15 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
Node: EvalNoop{}, Node: EvalNoop{},
}, },
&EvalIf{
If: func(ctx EvalContext) (bool, error) {
return n.Resource.Lifecycle.CreateBeforeDestroy, nil
},
Node: &EvalDeposeState{
Name: n.stateId(),
},
},
&EvalDiff{ &EvalDiff{
Info: info, Info: info,
Config: interpolateNode, Config: interpolateNode,
@ -304,6 +313,15 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
Tainted: &tainted, Tainted: &tainted,
Error: &err, Error: &err,
}, },
&EvalIf{
If: func(ctx EvalContext) (bool, error) {
return n.Resource.Lifecycle.CreateBeforeDestroy &&
tainted, nil
},
Node: &EvalUndeposeState{
Name: n.stateId(),
},
},
&EvalWriteState{ &EvalWriteState{
Name: n.stateId(), Name: n.stateId(),
ResourceType: n.Resource.Type, ResourceType: n.Resource.Type,
@ -311,7 +329,7 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
State: &state, State: &state,
Tainted: &tainted, Tainted: &tainted,
TaintedIndex: -1, TaintedIndex: -1,
TaintedClearPrimary: true, TaintedClearPrimary: !n.Resource.Lifecycle.CreateBeforeDestroy,
}, },
&EvalApplyPost{ &EvalApplyPost{
Info: info, Info: info,
@ -391,6 +409,8 @@ func (n *graphNodeExpandedResourceDestroy) EvalTree() EvalNode {
&EvalReadState{ &EvalReadState{
Name: n.stateId(), Name: n.stateId(),
Output: &state, Output: &state,
Tainted: n.Resource.Lifecycle.CreateBeforeDestroy,
TaintedIndex: -1,
}, },
&EvalApply{ &EvalApply{
Info: info, Info: info,

View File

@ -10,6 +10,10 @@ type TaintedTransformer struct {
// State is the global state. We'll automatically find the correct // State is the global state. We'll automatically find the correct
// ModuleState based on the Graph.Path that is being transformed. // ModuleState based on the Graph.Path that is being transformed.
State *State State *State
// View, if non-empty, is the ModuleState.View used around the state
// to find tainted resources.
View string
} }
func (t *TaintedTransformer) Transform(g *Graph) error { func (t *TaintedTransformer) Transform(g *Graph) error {
@ -20,6 +24,11 @@ func (t *TaintedTransformer) Transform(g *Graph) error {
return nil return nil
} }
// If we have a view, apply it now
if t.View != "" {
state = state.View(t.View)
}
// Go through all the resources in our state to look for tainted resources // Go through all the resources in our state to look for tainted resources
for k, rs := range state.Resources { for k, rs := range state.Resources {
// If we have no tainted resources, then move on // If we have no tainted resources, then move on
@ -31,16 +40,14 @@ func (t *TaintedTransformer) Transform(g *Graph) error {
// Add the graph node and make the connection from any untainted // Add the graph node and make the connection from any untainted
// resources with this name to the tainted resource, so that // resources with this name to the tainted resource, so that
// the tainted resource gets destroyed first. // the tainted resource gets destroyed first.
g.ConnectFrom(k, g.Add(&graphNodeTaintedResource{ g.Add(&graphNodeTaintedResource{
Index: i, Index: i,
ResourceName: k, ResourceName: k,
ResourceType: rs.Type, ResourceType: rs.Type,
})) })
} }
} }
// TODO: Any other dependencies?
return nil return nil
} }
@ -51,10 +58,6 @@ type graphNodeTaintedResource struct {
ResourceType string ResourceType string
} }
func (n *graphNodeTaintedResource) DependentOn() []string {
return []string{n.ResourceName}
}
func (n *graphNodeTaintedResource) Name() string { func (n *graphNodeTaintedResource) Name() string {
return fmt.Sprintf("%s (tainted #%d)", n.ResourceName, n.Index+1) return fmt.Sprintf("%s (tainted #%d)", n.ResourceName, n.Index+1)
} }
@ -94,7 +97,6 @@ func (n *graphNodeTaintedResource) EvalTree() EvalNode {
&EvalWriteState{ &EvalWriteState{
Name: n.ResourceName, Name: n.ResourceName,
ResourceType: n.ResourceType, ResourceType: n.ResourceType,
Dependencies: n.DependentOn(),
State: &state, State: &state,
Tainted: &tainted, Tainted: &tainted,
TaintedIndex: n.Index, TaintedIndex: n.Index,
@ -139,7 +141,6 @@ func (n *graphNodeTaintedResource) EvalTree() EvalNode {
&EvalWriteState{ &EvalWriteState{
Name: n.ResourceName, Name: n.ResourceName,
ResourceType: n.ResourceType, ResourceType: n.ResourceType,
Dependencies: n.DependentOn(),
State: &state, State: &state,
Tainted: &tainted, Tainted: &tainted,
TaintedIndex: n.Index, TaintedIndex: n.Index,

View File

@ -60,6 +60,5 @@ func TestGraphNodeTaintedResource_ProvidedBy(t *testing.T) {
const testTransformTaintedBasicStr = ` const testTransformTaintedBasicStr = `
aws_instance.web aws_instance.web
aws_instance.web (tainted #1)
aws_instance.web (tainted #1) aws_instance.web (tainted #1)
` `