terraform: run destroy provisioners on destroy
This commit is contained in:
parent
928fce71f7
commit
e9f6c9c429
|
@ -3998,6 +3998,144 @@ aws_instance.web:
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContext2Apply_provisionerDestroy(t *testing.T) {
|
||||||
|
m := testModule(t, "apply-provisioner-destroy")
|
||||||
|
p := testProvider("aws")
|
||||||
|
pr := testProvisioner()
|
||||||
|
p.ApplyFn = testApplyFn
|
||||||
|
p.DiffFn = testDiffFn
|
||||||
|
pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error {
|
||||||
|
val, ok := c.Config["foo"]
|
||||||
|
if !ok || val != "destroy" {
|
||||||
|
t.Fatalf("bad value for foo: %v %#v", val, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
state := &State{
|
||||||
|
Modules: []*ModuleState{
|
||||||
|
&ModuleState{
|
||||||
|
Path: rootModulePath,
|
||||||
|
Resources: map[string]*ResourceState{
|
||||||
|
"aws_instance.foo": &ResourceState{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Primary: &InstanceState{
|
||||||
|
ID: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Module: m,
|
||||||
|
State: state,
|
||||||
|
Destroy: true,
|
||||||
|
Providers: map[string]ResourceProviderFactory{
|
||||||
|
"aws": testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
Provisioners: map[string]ResourceProvisionerFactory{
|
||||||
|
"shell": testProvisionerFuncFixed(pr),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if _, err := ctx.Plan(); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := ctx.Apply()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkStateString(t, state, `<no state>`)
|
||||||
|
|
||||||
|
// Verify apply was invoked
|
||||||
|
if !pr.ApplyCalled {
|
||||||
|
t.Fatalf("provisioner not invoked")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify destroy provisioners are not run for tainted instances.
|
||||||
|
func TestContext2Apply_provisionerDestroyTainted(t *testing.T) {
|
||||||
|
m := testModule(t, "apply-provisioner-destroy")
|
||||||
|
p := testProvider("aws")
|
||||||
|
pr := testProvisioner()
|
||||||
|
p.ApplyFn = testApplyFn
|
||||||
|
p.DiffFn = testDiffFn
|
||||||
|
|
||||||
|
destroyCalled := false
|
||||||
|
pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error {
|
||||||
|
expected := "create"
|
||||||
|
if rs.ID == "bar" {
|
||||||
|
destroyCalled = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
val, ok := c.Config["foo"]
|
||||||
|
if !ok || val != expected {
|
||||||
|
t.Fatalf("bad value for foo: %v %#v", val, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
state := &State{
|
||||||
|
Modules: []*ModuleState{
|
||||||
|
&ModuleState{
|
||||||
|
Path: rootModulePath,
|
||||||
|
Resources: map[string]*ResourceState{
|
||||||
|
"aws_instance.foo": &ResourceState{
|
||||||
|
Type: "aws_instance",
|
||||||
|
Primary: &InstanceState{
|
||||||
|
ID: "bar",
|
||||||
|
Tainted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Module: m,
|
||||||
|
State: state,
|
||||||
|
Providers: map[string]ResourceProviderFactory{
|
||||||
|
"aws": testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
Provisioners: map[string]ResourceProvisionerFactory{
|
||||||
|
"shell": testProvisionerFuncFixed(pr),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if _, err := ctx.Plan(); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := ctx.Apply()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkStateString(t, state, `
|
||||||
|
aws_instance.foo:
|
||||||
|
ID = foo
|
||||||
|
foo = bar
|
||||||
|
type = aws_instance
|
||||||
|
`)
|
||||||
|
|
||||||
|
// Verify apply was invoked
|
||||||
|
if !pr.ApplyCalled {
|
||||||
|
t.Fatalf("provisioner not invoked")
|
||||||
|
}
|
||||||
|
|
||||||
|
if destroyCalled {
|
||||||
|
t.Fatal("destroy should not be called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestContext2Apply_provisionerResourceRef(t *testing.T) {
|
func TestContext2Apply_provisionerResourceRef(t *testing.T) {
|
||||||
m := testModule(t, "apply-provisioner-resource-ref")
|
m := testModule(t, "apply-provisioner-resource-ref")
|
||||||
p := testProvider("aws")
|
p := testProvider("aws")
|
||||||
|
|
|
@ -140,18 +140,22 @@ type EvalApplyProvisioners struct {
|
||||||
InterpResource *Resource
|
InterpResource *Resource
|
||||||
CreateNew *bool
|
CreateNew *bool
|
||||||
Error *error
|
Error *error
|
||||||
|
|
||||||
|
// When is the type of provisioner to run at this point
|
||||||
|
When config.ProvisionerWhen
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: test
|
// TODO: test
|
||||||
func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) {
|
func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) {
|
||||||
state := *n.State
|
state := *n.State
|
||||||
|
|
||||||
if !*n.CreateNew {
|
if n.CreateNew != nil && !*n.CreateNew {
|
||||||
// If we're not creating a new resource, then don't run provisioners
|
// If we're not creating a new resource, then don't run provisioners
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(n.Resource.Provisioners) == 0 {
|
provs := n.filterProvisioners()
|
||||||
|
if len(provs) == 0 {
|
||||||
// We have no provisioners, so don't do anything
|
// We have no provisioners, so don't do anything
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
@ -176,7 +180,7 @@ func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) {
|
||||||
|
|
||||||
// If there are no errors, then we append it to our output error
|
// If there are no errors, then we append it to our output error
|
||||||
// if we have one, otherwise we just output it.
|
// if we have one, otherwise we just output it.
|
||||||
err := n.apply(ctx)
|
err := n.apply(ctx, provs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Provisioning failed, so mark the resource as tainted
|
// Provisioning failed, so mark the resource as tainted
|
||||||
state.Tainted = true
|
state.Tainted = true
|
||||||
|
@ -201,7 +205,29 @@ func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *EvalApplyProvisioners) apply(ctx EvalContext) error {
|
// filterProvisioners filters the provisioners on the resource to only
|
||||||
|
// the provisioners specified by the "when" option.
|
||||||
|
func (n *EvalApplyProvisioners) filterProvisioners() []*config.Provisioner {
|
||||||
|
// Fast path the zero case
|
||||||
|
if n.Resource == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(n.Resource.Provisioners) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]*config.Provisioner, 0, len(n.Resource.Provisioners))
|
||||||
|
for _, p := range n.Resource.Provisioners {
|
||||||
|
if p.When == n.When {
|
||||||
|
result = append(result, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *EvalApplyProvisioners) apply(ctx EvalContext, provs []*config.Provisioner) error {
|
||||||
state := *n.State
|
state := *n.State
|
||||||
|
|
||||||
// Store the original connection info, restore later
|
// Store the original connection info, restore later
|
||||||
|
@ -210,7 +236,7 @@ func (n *EvalApplyProvisioners) apply(ctx EvalContext) error {
|
||||||
state.Ephemeral.ConnInfo = origConnInfo
|
state.Ephemeral.ConnInfo = origConnInfo
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for _, prov := range n.Resource.Provisioners {
|
for _, prov := range provs {
|
||||||
// Get the provisioner
|
// Get the provisioner
|
||||||
provisioner := ctx.Provisioner(prov.Type)
|
provisioner := ctx.Provisioner(prov.Type)
|
||||||
|
|
||||||
|
|
|
@ -96,13 +96,8 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
|
||||||
),
|
),
|
||||||
|
|
||||||
// Provisioner-related transformations
|
// Provisioner-related transformations
|
||||||
GraphTransformIf(
|
&MissingProvisionerTransformer{Provisioners: b.Provisioners},
|
||||||
func() bool { return !b.Destroy },
|
&ProvisionerTransformer{},
|
||||||
GraphTransformMulti(
|
|
||||||
&MissingProvisionerTransformer{Provisioners: b.Provisioners},
|
|
||||||
&ProvisionerTransformer{},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Add root variables
|
// Add root variables
|
||||||
&RootVariableTransformer{Module: b.Module},
|
&RootVariableTransformer{Module: b.Module},
|
||||||
|
|
|
@ -232,6 +232,78 @@ func TestApplyGraphBuilder_moduleDestroy(t *testing.T) {
|
||||||
"module.A.null_resource.foo (destroy)")
|
"module.A.null_resource.foo (destroy)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyGraphBuilder_provisioner(t *testing.T) {
|
||||||
|
diff := &Diff{
|
||||||
|
Modules: []*ModuleDiff{
|
||||||
|
&ModuleDiff{
|
||||||
|
Path: []string{"root"},
|
||||||
|
Resources: map[string]*InstanceDiff{
|
||||||
|
"null_resource.foo": &InstanceDiff{
|
||||||
|
Attributes: map[string]*ResourceAttrDiff{
|
||||||
|
"name": &ResourceAttrDiff{
|
||||||
|
Old: "",
|
||||||
|
New: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
b := &ApplyGraphBuilder{
|
||||||
|
Module: testModule(t, "graph-builder-apply-provisioner"),
|
||||||
|
Diff: diff,
|
||||||
|
Providers: []string{"null"},
|
||||||
|
Provisioners: []string{"local"},
|
||||||
|
}
|
||||||
|
|
||||||
|
g, err := b.Build(RootModulePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testGraphContains(t, g, "provisioner.local")
|
||||||
|
testGraphHappensBefore(
|
||||||
|
t, g,
|
||||||
|
"provisioner.local",
|
||||||
|
"null_resource.foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyGraphBuilder_provisionerDestroy(t *testing.T) {
|
||||||
|
diff := &Diff{
|
||||||
|
Modules: []*ModuleDiff{
|
||||||
|
&ModuleDiff{
|
||||||
|
Path: []string{"root"},
|
||||||
|
Resources: map[string]*InstanceDiff{
|
||||||
|
"null_resource.foo": &InstanceDiff{
|
||||||
|
Destroy: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
b := &ApplyGraphBuilder{
|
||||||
|
Destroy: true,
|
||||||
|
Module: testModule(t, "graph-builder-apply-provisioner"),
|
||||||
|
Diff: diff,
|
||||||
|
Providers: []string{"null"},
|
||||||
|
Provisioners: []string{"local"},
|
||||||
|
}
|
||||||
|
|
||||||
|
g, err := b.Build(RootModulePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testGraphContains(t, g, "provisioner.local")
|
||||||
|
testGraphHappensBefore(
|
||||||
|
t, g,
|
||||||
|
"provisioner.local",
|
||||||
|
"null_resource.foo (destroy)")
|
||||||
|
}
|
||||||
|
|
||||||
const testApplyGraphBuilderStr = `
|
const testApplyGraphBuilderStr = `
|
||||||
aws_instance.create
|
aws_instance.create
|
||||||
provider.aws
|
provider.aws
|
||||||
|
|
|
@ -88,6 +88,32 @@ func TestGraphWalk_panicWrap(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testGraphContains is an assertion helper that tests that a node is
|
||||||
|
// contained in the graph.
|
||||||
|
func testGraphContains(t *testing.T, g *Graph, name string) {
|
||||||
|
for _, v := range g.Vertices() {
|
||||||
|
if dag.VertexName(v) == name {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatalf(
|
||||||
|
"Expected %q in:\n\n%s",
|
||||||
|
name, g.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// testGraphnotContains is an assertion helper that tests that a node is
|
||||||
|
// NOT contained in the graph.
|
||||||
|
func testGraphNotContains(t *testing.T, g *Graph, name string) {
|
||||||
|
for _, v := range g.Vertices() {
|
||||||
|
if dag.VertexName(v) == name {
|
||||||
|
t.Fatalf(
|
||||||
|
"Expected %q to NOT be in:\n\n%s",
|
||||||
|
name, g.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// testGraphHappensBefore is an assertion helper that tests that node
|
// testGraphHappensBefore is an assertion helper that tests that node
|
||||||
// A (dag.VertexName value) happens before node B.
|
// A (dag.VertexName value) happens before node B.
|
||||||
func testGraphHappensBefore(t *testing.T, g *Graph, A, B string) {
|
func testGraphHappensBefore(t *testing.T, g *Graph, A, B string) {
|
||||||
|
|
|
@ -321,6 +321,7 @@ func (n *NodeApplyableResource) evalTreeManagedResource(
|
||||||
InterpResource: resource,
|
InterpResource: resource,
|
||||||
CreateNew: &createNew,
|
CreateNew: &createNew,
|
||||||
Error: &err,
|
Error: &err,
|
||||||
|
When: config.ProvisionerWhenCreate,
|
||||||
},
|
},
|
||||||
&EvalIf{
|
&EvalIf{
|
||||||
If: func(ctx EvalContext) (bool, error) {
|
If: func(ctx EvalContext) (bool, error) {
|
||||||
|
|
|
@ -107,6 +107,17 @@ func (n *NodeDestroyResource) EvalTree() EvalNode {
|
||||||
uniqueExtra: "destroy",
|
uniqueExtra: "destroy",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build the resource for eval
|
||||||
|
addr := n.Addr
|
||||||
|
resource := &Resource{
|
||||||
|
Name: addr.Name,
|
||||||
|
Type: addr.Type,
|
||||||
|
CountIndex: addr.Index,
|
||||||
|
}
|
||||||
|
if resource.CountIndex < 0 {
|
||||||
|
resource.CountIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
// Get our state
|
// Get our state
|
||||||
rs := n.ResourceState
|
rs := n.ResourceState
|
||||||
if rs == nil {
|
if rs == nil {
|
||||||
|
@ -160,6 +171,27 @@ func (n *NodeDestroyResource) EvalTree() EvalNode {
|
||||||
&EvalRequireState{
|
&EvalRequireState{
|
||||||
State: &state,
|
State: &state,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Run destroy provisioners if not tainted
|
||||||
|
&EvalIf{
|
||||||
|
If: func(ctx EvalContext) (bool, error) {
|
||||||
|
if state != nil && state.Tainted {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
|
||||||
|
Then: &EvalApplyProvisioners{
|
||||||
|
Info: info,
|
||||||
|
State: &state,
|
||||||
|
Resource: n.Config,
|
||||||
|
InterpResource: resource,
|
||||||
|
Error: &err,
|
||||||
|
When: config.ProvisionerWhenDestroy,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// Make sure we handle data sources properly.
|
// Make sure we handle data sources properly.
|
||||||
&EvalIf{
|
&EvalIf{
|
||||||
If: func(ctx EvalContext) (bool, error) {
|
If: func(ctx EvalContext) (bool, error) {
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
resource "aws_instance" "foo" {
|
||||||
|
foo = "bar"
|
||||||
|
|
||||||
|
provisioner "shell" {
|
||||||
|
foo = "create"
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "shell" {
|
||||||
|
foo = "destroy"
|
||||||
|
when = "destroy"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
resource "null_resource" "foo" {
|
||||||
|
provisioner "local" {}
|
||||||
|
}
|
Loading…
Reference in New Issue