From 928fce71f72c3821508b935d166504a04d91dbd6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jan 2017 18:10:21 -0800 Subject: [PATCH 01/10] config: parse "when" and "on_failure" on provisioners --- config/config.go | 18 ++++++++- config/config_string.go | 11 ++++- config/config_test.go | 7 ++++ config/loader_hcl.go | 36 ++++++++++++++++- config/loader_test.go | 27 +++++++++++++ config/provisioner_enums.go | 40 +++++++++++++++++++ config/test-fixtures/provisioners-destroy.tf | 14 +++++++ .../validate-basic-provisioners/main.tf | 14 +++++++ 8 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 config/provisioner_enums.go create mode 100644 config/test-fixtures/provisioners-destroy.tf create mode 100644 config/test-fixtures/validate-basic-provisioners/main.tf diff --git a/config/config.go b/config/config.go index e1543a1af..724a78144 100644 --- a/config/config.go +++ b/config/config.go @@ -136,6 +136,9 @@ type Provisioner struct { Type string RawConfig *RawConfig ConnInfo *RawConfig + + When ProvisionerWhen + OnFailure ProvisionerOnFailure } // Copy returns a copy of this Provisioner @@ -144,6 +147,8 @@ func (p *Provisioner) Copy() *Provisioner { Type: p.Type, RawConfig: p.RawConfig.Copy(), ConnInfo: p.ConnInfo.Copy(), + When: p.When, + OnFailure: p.OnFailure, } } @@ -553,7 +558,7 @@ func (c *Config) Validate() error { // Validate DependsOn errs = append(errs, c.validateDependsOn(n, r.DependsOn, resources, modules)...) - // Verify provisioners don't contain any splats + // Verify provisioners for _, p := range r.Provisioners { // This validation checks that there are now splat variables // referencing ourself. This currently is not allowed. @@ -585,6 +590,17 @@ func (c *Config) Validate() error { break } } + + // Check for invalid when/onFailure values, though this should be + // picked up by the loader we check here just in case. + if p.When == ProvisionerWhenInvalid { + errs = append(errs, fmt.Errorf( + "%s: provisioner 'when' value is invalid", n)) + } + if p.OnFailure == ProvisionerOnFailureInvalid { + errs = append(errs, fmt.Errorf( + "%s: provisioner 'on_failure' value is invalid", n)) + } } // Verify ignore_changes contains valid entries diff --git a/config/config_string.go b/config/config_string.go index f11290e87..a5ef7d5cd 100644 --- a/config/config_string.go +++ b/config/config_string.go @@ -214,7 +214,16 @@ func resourcesStr(rs []*Resource) string { if len(r.Provisioners) > 0 { result += fmt.Sprintf(" provisioners\n") for _, p := range r.Provisioners { - result += fmt.Sprintf(" %s\n", p.Type) + when := "" + if p.When != ProvisionerWhenCreate { + when = fmt.Sprintf(" (%s)", p.When.String()) + } + + result += fmt.Sprintf(" %s%s\n", p.Type, when) + + if p.OnFailure != ProvisionerOnFailureFail { + result += fmt.Sprintf(" on_failure = %s\n", p.OnFailure.String()) + } ks := make([]string, 0, len(p.RawConfig.Raw)) for k, _ := range p.RawConfig.Raw { diff --git a/config/config_test.go b/config/config_test.go index c73ed6100..95acd28b7 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -168,6 +168,13 @@ func TestConfigValidate_table(t *testing.T) { true, "data sources cannot have", }, + + { + "basic provisioners", + "validate-basic-provisioners", + false, + "", + }, } for i, tc := range cases { diff --git a/config/loader_hcl.go b/config/loader_hcl.go index 88f6fd0cf..dfadce9d6 100644 --- a/config/loader_hcl.go +++ b/config/loader_hcl.go @@ -849,8 +849,40 @@ func loadProvisionersHcl(list *ast.ObjectList, connInfo map[string]interface{}) return nil, err } - // Delete the "connection" section, handle separately + // Parse the "when" value + when := ProvisionerWhenCreate + if v, ok := config["when"]; ok { + switch v { + case "create": + when = ProvisionerWhenCreate + case "destroy": + when = ProvisionerWhenDestroy + default: + return nil, fmt.Errorf( + "position %s: 'provisioner' when must be 'create' or 'destroy'", + item.Pos()) + } + } + + // Parse the "on_failure" value + onFailure := ProvisionerOnFailureFail + if v, ok := config["on_failure"]; ok { + switch v { + case "continue": + onFailure = ProvisionerOnFailureContinue + case "fail": + onFailure = ProvisionerOnFailureFail + default: + return nil, fmt.Errorf( + "position %s: 'provisioner' on_failure must be 'continue' or 'fail'", + item.Pos()) + } + } + + // Delete fields we special case delete(config, "connection") + delete(config, "when") + delete(config, "on_failure") rawConfig, err := NewRawConfig(config) if err != nil { @@ -889,6 +921,8 @@ func loadProvisionersHcl(list *ast.ObjectList, connInfo map[string]interface{}) Type: n, RawConfig: rawConfig, ConnInfo: connRaw, + When: when, + OnFailure: onFailure, }) } diff --git a/config/loader_test.go b/config/loader_test.go index 1cdc4e561..80c0d8d1f 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -629,6 +629,22 @@ func TestLoadFile_provisioners(t *testing.T) { } } +func TestLoadFile_provisionersDestroy(t *testing.T) { + c, err := LoadFile(filepath.Join(fixtureDir, "provisioners-destroy.tf")) + if err != nil { + t.Fatalf("err: %s", err) + } + + if c == nil { + t.Fatal("config should not be nil") + } + + actual := resourcesStr(c.Resources) + if actual != strings.TrimSpace(provisionerDestroyResourcesStr) { + t.Fatalf("bad:\n%s", actual) + } +} + func TestLoadFile_unnamedOutput(t *testing.T) { _, err := LoadFile(filepath.Join(fixtureDir, "output-unnamed.tf")) if err == nil { @@ -1126,6 +1142,17 @@ aws_instance.web (x1) user: var.foo ` +const provisionerDestroyResourcesStr = ` +aws_instance.web (x1) + provisioners + shell + shell (destroy) + path + shell (destroy) + on_failure = continue + path +` + const connectionResourcesStr = ` aws_instance.web (x1) ami diff --git a/config/provisioner_enums.go b/config/provisioner_enums.go new file mode 100644 index 000000000..00fd43fce --- /dev/null +++ b/config/provisioner_enums.go @@ -0,0 +1,40 @@ +package config + +// ProvisionerWhen is an enum for valid values for when to run provisioners. +type ProvisionerWhen int + +const ( + ProvisionerWhenInvalid ProvisionerWhen = iota + ProvisionerWhenCreate + ProvisionerWhenDestroy +) + +var provisionerWhenStrs = map[ProvisionerWhen]string{ + ProvisionerWhenInvalid: "invalid", + ProvisionerWhenCreate: "create", + ProvisionerWhenDestroy: "destroy", +} + +func (v ProvisionerWhen) String() string { + return provisionerWhenStrs[v] +} + +// ProvisionerOnFailure is an enum for valid values for on_failure options +// for provisioners. +type ProvisionerOnFailure int + +const ( + ProvisionerOnFailureInvalid ProvisionerOnFailure = iota + ProvisionerOnFailureContinue + ProvisionerOnFailureFail +) + +var provisionerOnFailureStrs = map[ProvisionerOnFailure]string{ + ProvisionerOnFailureInvalid: "invalid", + ProvisionerOnFailureContinue: "continue", + ProvisionerOnFailureFail: "fail", +} + +func (v ProvisionerOnFailure) String() string { + return provisionerOnFailureStrs[v] +} diff --git a/config/test-fixtures/provisioners-destroy.tf b/config/test-fixtures/provisioners-destroy.tf new file mode 100644 index 000000000..0ad4f557b --- /dev/null +++ b/config/test-fixtures/provisioners-destroy.tf @@ -0,0 +1,14 @@ +resource "aws_instance" "web" { + provisioner "shell" {} + + provisioner "shell" { + path = "foo" + when = "destroy" + } + + provisioner "shell" { + path = "foo" + when = "destroy" + on_failure = "continue" + } +} diff --git a/config/test-fixtures/validate-basic-provisioners/main.tf b/config/test-fixtures/validate-basic-provisioners/main.tf new file mode 100644 index 000000000..0ad4f557b --- /dev/null +++ b/config/test-fixtures/validate-basic-provisioners/main.tf @@ -0,0 +1,14 @@ +resource "aws_instance" "web" { + provisioner "shell" {} + + provisioner "shell" { + path = "foo" + when = "destroy" + } + + provisioner "shell" { + path = "foo" + when = "destroy" + on_failure = "continue" + } +} From e9f6c9c429a92ea0644ae3640fc31872d971128c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jan 2017 18:07:51 -0800 Subject: [PATCH 02/10] terraform: run destroy provisioners on destroy --- terraform/context_apply_test.go | 138 ++++++++++++++++++ terraform/eval_apply.go | 36 ++++- terraform/graph_builder_apply.go | 9 +- terraform/graph_builder_apply_test.go | 72 +++++++++ terraform/graph_test.go | 26 ++++ terraform/node_resource_apply.go | 1 + terraform/node_resource_destroy.go | 32 ++++ .../apply-provisioner-destroy/main.tf | 12 ++ .../graph-builder-apply-provisioner/main.tf | 3 + 9 files changed, 317 insertions(+), 12 deletions(-) create mode 100644 terraform/test-fixtures/apply-provisioner-destroy/main.tf create mode 100644 terraform/test-fixtures/graph-builder-apply-provisioner/main.tf diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index 77b0579fb..423ff1eee 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -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, ``) + + // 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) { m := testModule(t, "apply-provisioner-resource-ref") p := testProvider("aws") diff --git a/terraform/eval_apply.go b/terraform/eval_apply.go index f8a42ed47..b554f50ce 100644 --- a/terraform/eval_apply.go +++ b/terraform/eval_apply.go @@ -140,18 +140,22 @@ type EvalApplyProvisioners struct { InterpResource *Resource CreateNew *bool Error *error + + // When is the type of provisioner to run at this point + When config.ProvisionerWhen } // TODO: test func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) { 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 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 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 we have one, otherwise we just output it. - err := n.apply(ctx) + err := n.apply(ctx, provs) if err != nil { // Provisioning failed, so mark the resource as tainted state.Tainted = true @@ -201,7 +205,29 @@ func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) { 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 // Store the original connection info, restore later @@ -210,7 +236,7 @@ func (n *EvalApplyProvisioners) apply(ctx EvalContext) error { state.Ephemeral.ConnInfo = origConnInfo }() - for _, prov := range n.Resource.Provisioners { + for _, prov := range provs { // Get the provisioner provisioner := ctx.Provisioner(prov.Type) diff --git a/terraform/graph_builder_apply.go b/terraform/graph_builder_apply.go index 75de53757..4eeaab3e2 100644 --- a/terraform/graph_builder_apply.go +++ b/terraform/graph_builder_apply.go @@ -96,13 +96,8 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { ), // Provisioner-related transformations - GraphTransformIf( - func() bool { return !b.Destroy }, - GraphTransformMulti( - &MissingProvisionerTransformer{Provisioners: b.Provisioners}, - &ProvisionerTransformer{}, - ), - ), + &MissingProvisionerTransformer{Provisioners: b.Provisioners}, + &ProvisionerTransformer{}, // Add root variables &RootVariableTransformer{Module: b.Module}, diff --git a/terraform/graph_builder_apply_test.go b/terraform/graph_builder_apply_test.go index f6420154e..659f8e141 100644 --- a/terraform/graph_builder_apply_test.go +++ b/terraform/graph_builder_apply_test.go @@ -232,6 +232,78 @@ func TestApplyGraphBuilder_moduleDestroy(t *testing.T) { "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 = ` aws_instance.create provider.aws diff --git a/terraform/graph_test.go b/terraform/graph_test.go index 3d5ae9aab..fc9fc25dd 100644 --- a/terraform/graph_test.go +++ b/terraform/graph_test.go @@ -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 // A (dag.VertexName value) happens before node B. func testGraphHappensBefore(t *testing.T, g *Graph, A, B string) { diff --git a/terraform/node_resource_apply.go b/terraform/node_resource_apply.go index a16683642..07b33a055 100644 --- a/terraform/node_resource_apply.go +++ b/terraform/node_resource_apply.go @@ -321,6 +321,7 @@ func (n *NodeApplyableResource) evalTreeManagedResource( InterpResource: resource, CreateNew: &createNew, Error: &err, + When: config.ProvisionerWhenCreate, }, &EvalIf{ If: func(ctx EvalContext) (bool, error) { diff --git a/terraform/node_resource_destroy.go b/terraform/node_resource_destroy.go index 6b4a48b6b..df1e9f404 100644 --- a/terraform/node_resource_destroy.go +++ b/terraform/node_resource_destroy.go @@ -107,6 +107,17 @@ func (n *NodeDestroyResource) EvalTree() EvalNode { 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 rs := n.ResourceState if rs == nil { @@ -160,6 +171,27 @@ func (n *NodeDestroyResource) EvalTree() EvalNode { &EvalRequireState{ 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. &EvalIf{ If: func(ctx EvalContext) (bool, error) { diff --git a/terraform/test-fixtures/apply-provisioner-destroy/main.tf b/terraform/test-fixtures/apply-provisioner-destroy/main.tf new file mode 100644 index 000000000..686a1b040 --- /dev/null +++ b/terraform/test-fixtures/apply-provisioner-destroy/main.tf @@ -0,0 +1,12 @@ +resource "aws_instance" "foo" { + foo = "bar" + + provisioner "shell" { + foo = "create" + } + + provisioner "shell" { + foo = "destroy" + when = "destroy" + } +} diff --git a/terraform/test-fixtures/graph-builder-apply-provisioner/main.tf b/terraform/test-fixtures/graph-builder-apply-provisioner/main.tf new file mode 100644 index 000000000..948622156 --- /dev/null +++ b/terraform/test-fixtures/graph-builder-apply-provisioner/main.tf @@ -0,0 +1,3 @@ +resource "null_resource" "foo" { + provisioner "local" {} +} From 85cb3a16b0c00b6aeff835c1822376545e8685bf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jan 2017 18:26:22 -0800 Subject: [PATCH 03/10] terraform: on destroy prov failure, don't taint and preserve state --- terraform/context_apply_test.go | 59 ++++++++++++++++++++++++++++++ terraform/eval_apply.go | 13 +++++-- terraform/node_resource_destroy.go | 14 +++++++ 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index 423ff1eee..052caa188 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -4058,6 +4058,65 @@ func TestContext2Apply_provisionerDestroy(t *testing.T) { } } +// Verify that on destroy provisioner failure, nothing happens to the instance +func TestContext2Apply_provisionerDestroyFail(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 { + return fmt.Errorf("provisioner error") + } + + 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.Fatal("should error") + } + + checkStateString(t, state, ` +aws_instance.foo: + ID = bar + `) + + // 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") diff --git a/terraform/eval_apply.go b/terraform/eval_apply.go index b554f50ce..b2c9ced7b 100644 --- a/terraform/eval_apply.go +++ b/terraform/eval_apply.go @@ -160,9 +160,13 @@ func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) { return nil, nil } + // taint tells us whether to enable tainting. + taint := n.When == config.ProvisionerWhenCreate + if n.Error != nil && *n.Error != nil { - // We're already errored creating, so mark as tainted and continue - state.Tainted = true + if taint { + state.Tainted = true + } // We're already tainted, so just return out return nil, nil @@ -182,8 +186,9 @@ func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) { // if we have one, otherwise we just output it. err := n.apply(ctx, provs) if err != nil { - // Provisioning failed, so mark the resource as tainted - state.Tainted = true + if taint { + state.Tainted = true + } if n.Error != nil { *n.Error = multierror.Append(*n.Error, err) diff --git a/terraform/node_resource_destroy.go b/terraform/node_resource_destroy.go index df1e9f404..9fe2b1716 100644 --- a/terraform/node_resource_destroy.go +++ b/terraform/node_resource_destroy.go @@ -192,6 +192,20 @@ func (n *NodeDestroyResource) EvalTree() EvalNode { }, }, + // If we have a provisioning error, then we just call + // the post-apply hook now. + &EvalIf{ + If: func(ctx EvalContext) (bool, error) { + return err != nil, nil + }, + + Then: &EvalApplyPost{ + Info: info, + State: &state, + Error: &err, + }, + }, + // Make sure we handle data sources properly. &EvalIf{ If: func(ctx EvalContext) (bool, error) { From 4a8c2d0958f392e33be4ae1e50fdfb6893b7425d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jan 2017 19:55:32 -0800 Subject: [PATCH 04/10] terraform: on_failure for provisioners --- terraform/context_apply_test.go | 144 ++++++++++++++++++ terraform/eval_apply.go | 14 +- .../main.tf | 15 ++ .../apply-provisioner-destroy-fail/main.tf | 14 ++ 4 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 terraform/test-fixtures/apply-provisioner-destroy-continue/main.tf create mode 100644 terraform/test-fixtures/apply-provisioner-destroy-fail/main.tf diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index 052caa188..a9b1bc101 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -4117,6 +4117,150 @@ aws_instance.foo: } } +// Verify that on destroy provisioner failure with "continue" that +// we continue to the next provisioner. +func TestContext2Apply_provisionerDestroyFailContinue(t *testing.T) { + m := testModule(t, "apply-provisioner-destroy-continue") + p := testProvider("aws") + pr := testProvisioner() + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + + var calls []string + pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error { + val, ok := c.Config["foo"] + if !ok { + t.Fatalf("bad value for foo: %v %#v", val, c) + } + + calls = append(calls, val.(string)) + return fmt.Errorf("provisioner error") + } + + 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, ``) + + // Verify apply was invoked + if !pr.ApplyCalled { + t.Fatalf("provisioner not invoked") + } + + expected := []string{"one", "two"} + if !reflect.DeepEqual(calls, expected) { + t.Fatalf("bad: %#v", calls) + } +} + +// Verify that on destroy provisioner failure with "continue" that +// we continue to the next provisioner. But if the next provisioner defines +// to fail, then we fail after running it. +func TestContext2Apply_provisionerDestroyFailContinueFail(t *testing.T) { + m := testModule(t, "apply-provisioner-destroy-fail") + p := testProvider("aws") + pr := testProvisioner() + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + + var calls []string + pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error { + val, ok := c.Config["foo"] + if !ok { + t.Fatalf("bad value for foo: %v %#v", val, c) + } + + calls = append(calls, val.(string)) + return fmt.Errorf("provisioner error") + } + + 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.Fatal("should error") + } + + checkStateString(t, state, ` +aws_instance.foo: + ID = bar + `) + + // Verify apply was invoked + if !pr.ApplyCalled { + t.Fatalf("provisioner not invoked") + } + + expected := []string{"one", "two"} + if !reflect.DeepEqual(calls, expected) { + t.Fatalf("bad: %#v", calls) + } +} + // Verify destroy provisioners are not run for tainted instances. func TestContext2Apply_provisionerDestroyTainted(t *testing.T) { m := testModule(t, "apply-provisioner-destroy") diff --git a/terraform/eval_apply.go b/terraform/eval_apply.go index b2c9ced7b..fee44d37a 100644 --- a/terraform/eval_apply.go +++ b/terraform/eval_apply.go @@ -306,8 +306,18 @@ func (n *EvalApplyProvisioners) apply(ctx EvalContext, provs []*config.Provision // Invoke the Provisioner output := CallbackUIOutput{OutputFn: outputFn} - if err := provisioner.Apply(&output, state, provConfig); err != nil { - return err + applyErr := provisioner.Apply(&output, state, provConfig) + if applyErr != nil { + // Determine failure behavior + switch prov.OnFailure { + case config.ProvisionerOnFailureContinue: + log.Printf( + "[INFO] apply: %s [%s]: error during provision, continue requested", + n.Info.Id, prov.Type) + + case config.ProvisionerOnFailureFail: + return applyErr + } } { diff --git a/terraform/test-fixtures/apply-provisioner-destroy-continue/main.tf b/terraform/test-fixtures/apply-provisioner-destroy-continue/main.tf new file mode 100644 index 000000000..6f39fc0b8 --- /dev/null +++ b/terraform/test-fixtures/apply-provisioner-destroy-continue/main.tf @@ -0,0 +1,15 @@ +resource "aws_instance" "foo" { + foo = "bar" + + provisioner "shell" { + foo = "one" + when = "destroy" + on_failure = "continue" + } + + provisioner "shell" { + foo = "two" + when = "destroy" + on_failure = "continue" + } +} diff --git a/terraform/test-fixtures/apply-provisioner-destroy-fail/main.tf b/terraform/test-fixtures/apply-provisioner-destroy-fail/main.tf new file mode 100644 index 000000000..e756487f1 --- /dev/null +++ b/terraform/test-fixtures/apply-provisioner-destroy-fail/main.tf @@ -0,0 +1,14 @@ +resource "aws_instance" "foo" { + foo = "bar" + + provisioner "shell" { + foo = "one" + when = "destroy" + on_failure = "continue" + } + + provisioner "shell" { + foo = "two" + when = "destroy" + } +} From b56ee1a1699c5853cb747d6bd5ba2bbf6dc0f8f0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jan 2017 20:05:28 -0800 Subject: [PATCH 05/10] terraform: test on_failure with non-destroy provisioners --- terraform/context_apply_test.go | 45 +++++++++++++++++++ .../apply-provisioner-fail-continue/main.tf | 7 +++ 2 files changed, 52 insertions(+) create mode 100644 terraform/test-fixtures/apply-provisioner-fail-continue/main.tf diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index a9b1bc101..0a483fe61 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -3998,6 +3998,51 @@ aws_instance.web: `) } +// Verify that a normal provisioner with on_failure "continue" set won't +// taint the resource and continues executing. +func TestContext2Apply_provisionerFailContinue(t *testing.T) { + m := testModule(t, "apply-provisioner-fail-continue") + p := testProvider("aws") + pr := testProvisioner() + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + + pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error { + return fmt.Errorf("provisioner error") + } + + ctx := testContext2(t, &ContextOpts{ + Module: m, + 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") + } +} + func TestContext2Apply_provisionerDestroy(t *testing.T) { m := testModule(t, "apply-provisioner-destroy") p := testProvider("aws") diff --git a/terraform/test-fixtures/apply-provisioner-fail-continue/main.tf b/terraform/test-fixtures/apply-provisioner-fail-continue/main.tf new file mode 100644 index 000000000..39587984e --- /dev/null +++ b/terraform/test-fixtures/apply-provisioner-fail-continue/main.tf @@ -0,0 +1,7 @@ +resource "aws_instance" "foo" { + foo = "bar" + + provisioner "shell" { + on_failure = "continue" + } +} From f40fdde708bacccff8a26d0577b0a6da65276fa6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jan 2017 20:21:12 -0800 Subject: [PATCH 06/10] terraform: PostProvision hook gets the error from the provision step --- terraform/context_apply_test.go | 40 +++++++++++++++++++++++++++++++++ terraform/eval_apply.go | 18 ++++++++------- terraform/hook.go | 4 ++-- terraform/hook_mock.go | 4 +++- terraform/hook_stop.go | 2 +- 5 files changed, 56 insertions(+), 12 deletions(-) diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index 0a483fe61..a96ac1a98 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -4043,6 +4043,46 @@ aws_instance.foo: } } +// Verify that a normal provisioner with on_failure "continue" records +// the error with the hook. +func TestContext2Apply_provisionerFailContinueHook(t *testing.T) { + h := new(MockHook) + m := testModule(t, "apply-provisioner-fail-continue") + p := testProvider("aws") + pr := testProvisioner() + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error { + return fmt.Errorf("provisioner error") + } + + ctx := testContext2(t, &ContextOpts{ + Module: m, + Hooks: []Hook{h}, + 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) + } + + if _, err := ctx.Apply(); err != nil { + t.Fatalf("err: %s", err) + } + + if !h.PostProvisionCalled { + t.Fatal("PostProvision not called") + } + if h.PostProvisionErrorArg == nil { + t.Fatal("should have error") + } +} + func TestContext2Apply_provisionerDestroy(t *testing.T) { m := testModule(t, "apply-provisioner-destroy") p := testProvider("aws") diff --git a/terraform/eval_apply.go b/terraform/eval_apply.go index fee44d37a..21c9ff832 100644 --- a/terraform/eval_apply.go +++ b/terraform/eval_apply.go @@ -307,6 +307,13 @@ func (n *EvalApplyProvisioners) apply(ctx EvalContext, provs []*config.Provision // Invoke the Provisioner output := CallbackUIOutput{OutputFn: outputFn} applyErr := provisioner.Apply(&output, state, provConfig) + + // Call post hook + hookErr := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PostProvision(n.Info, prov.Type, applyErr) + }) + + // Handle the error before we deal with the hook if applyErr != nil { // Determine failure behavior switch prov.OnFailure { @@ -320,14 +327,9 @@ func (n *EvalApplyProvisioners) apply(ctx EvalContext, provs []*config.Provision } } - { - // Call post hook - err := ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostProvision(n.Info, prov.Type) - }) - if err != nil { - return err - } + // Deal with the hook + if hookErr != nil { + return hookErr } } diff --git a/terraform/hook.go b/terraform/hook.go index 81a68842f..ab11e8ee0 100644 --- a/terraform/hook.go +++ b/terraform/hook.go @@ -42,7 +42,7 @@ type Hook interface { PreProvisionResource(*InstanceInfo, *InstanceState) (HookAction, error) PostProvisionResource(*InstanceInfo, *InstanceState) (HookAction, error) PreProvision(*InstanceInfo, string) (HookAction, error) - PostProvision(*InstanceInfo, string) (HookAction, error) + PostProvision(*InstanceInfo, string, error) (HookAction, error) ProvisionOutput(*InstanceInfo, string, string) // PreRefresh and PostRefresh are called before and after a single @@ -92,7 +92,7 @@ func (*NilHook) PreProvision(*InstanceInfo, string) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PostProvision(*InstanceInfo, string) (HookAction, error) { +func (*NilHook) PostProvision(*InstanceInfo, string, error) (HookAction, error) { return HookActionContinue, nil } diff --git a/terraform/hook_mock.go b/terraform/hook_mock.go index b0bb94e57..0e4640067 100644 --- a/terraform/hook_mock.go +++ b/terraform/hook_mock.go @@ -55,6 +55,7 @@ type MockHook struct { PostProvisionCalled bool PostProvisionInfo *InstanceInfo PostProvisionProvisionerId string + PostProvisionErrorArg error PostProvisionReturn HookAction PostProvisionError error @@ -170,13 +171,14 @@ func (h *MockHook) PreProvision(n *InstanceInfo, provId string) (HookAction, err return h.PreProvisionReturn, h.PreProvisionError } -func (h *MockHook) PostProvision(n *InstanceInfo, provId string) (HookAction, error) { +func (h *MockHook) PostProvision(n *InstanceInfo, provId string, err error) (HookAction, error) { h.Lock() defer h.Unlock() h.PostProvisionCalled = true h.PostProvisionInfo = n h.PostProvisionProvisionerId = provId + h.PostProvisionErrorArg = err return h.PostProvisionReturn, h.PostProvisionError } diff --git a/terraform/hook_stop.go b/terraform/hook_stop.go index 4c9bbb7b3..104d0098a 100644 --- a/terraform/hook_stop.go +++ b/terraform/hook_stop.go @@ -38,7 +38,7 @@ func (h *stopHook) PreProvision(*InstanceInfo, string) (HookAction, error) { return h.hook() } -func (h *stopHook) PostProvision(*InstanceInfo, string) (HookAction, error) { +func (h *stopHook) PostProvision(*InstanceInfo, string, error) (HookAction, error) { return h.hook() } From d3df7874d58057e6fc445c937196450146d19ec0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jan 2017 20:36:53 -0800 Subject: [PATCH 07/10] terraform: introduce EvalApplyPre so that PreApply is called even for destroy provisioners. --- terraform/eval_apply.go | 41 ++++++++++++++++++++++-------- terraform/node_resource_apply.go | 6 +++++ terraform/node_resource_destroy.go | 7 +++++ terraform/terraform_test.go | 4 +++ terraform/transform_deposed.go | 6 +++++ terraform/transform_orphan.go | 5 ++++ terraform/transform_resource.go | 5 ++++ 7 files changed, 64 insertions(+), 10 deletions(-) diff --git a/terraform/eval_apply.go b/terraform/eval_apply.go index 21c9ff832..2f6a4973e 100644 --- a/terraform/eval_apply.go +++ b/terraform/eval_apply.go @@ -52,16 +52,6 @@ func (n *EvalApply) Eval(ctx EvalContext) (interface{}, error) { *n.CreateNew = state.ID == "" && !diff.GetDestroy() || diff.RequiresNew() } - { - // Call pre-apply hook - err := ctx.Hook(func(h Hook) (HookAction, error) { - return h.PreApply(n.Info, state, diff) - }) - if err != nil { - return nil, err - } - } - // With the completed diff, apply! log.Printf("[DEBUG] apply: %s: executing Apply", n.Info.Id) state, err := provider.Apply(n.Info, state, diff) @@ -104,6 +94,37 @@ func (n *EvalApply) Eval(ctx EvalContext) (interface{}, error) { return nil, nil } +// EvalApplyPre is an EvalNode implementation that does the pre-Apply work +type EvalApplyPre struct { + Info *InstanceInfo + State **InstanceState + Diff **InstanceDiff +} + +// TODO: test +func (n *EvalApplyPre) Eval(ctx EvalContext) (interface{}, error) { + state := *n.State + diff := *n.Diff + + // If the state is nil, make it non-nil + if state == nil { + state = new(InstanceState) + } + state.init() + + { + // Call post-apply hook + err := ctx.Hook(func(h Hook) (HookAction, error) { + return h.PreApply(n.Info, state, diff) + }) + if err != nil { + return nil, err + } + } + + return nil, nil +} + // EvalApplyPost is an EvalNode implementation that does the post-Apply work type EvalApplyPost struct { Info *InstanceInfo diff --git a/terraform/node_resource_apply.go b/terraform/node_resource_apply.go index 07b33a055..b5ce47f6c 100644 --- a/terraform/node_resource_apply.go +++ b/terraform/node_resource_apply.go @@ -298,6 +298,12 @@ func (n *NodeApplyableResource) evalTreeManagedResource( Name: stateId, Output: &state, }, + // Call pre-apply hook + &EvalApplyPre{ + Info: info, + State: &state, + Diff: &diffApply, + }, &EvalApply{ Info: info, State: &state, diff --git a/terraform/node_resource_destroy.go b/terraform/node_resource_destroy.go index 9fe2b1716..0bf424a39 100644 --- a/terraform/node_resource_destroy.go +++ b/terraform/node_resource_destroy.go @@ -172,6 +172,13 @@ func (n *NodeDestroyResource) EvalTree() EvalNode { State: &state, }, + // Call pre-apply hook + &EvalApplyPre{ + Info: info, + State: &state, + Diff: &diffApply, + }, + // Run destroy provisioners if not tainted &EvalIf{ If: func(ctx EvalContext) (bool, error) { diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 142123e53..59c2ffa98 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -179,6 +179,10 @@ func (h *HookRecordApplyOrder) PreApply( info *InstanceInfo, s *InstanceState, d *InstanceDiff) (HookAction, error) { + if d.Empty() { + return HookActionContinue, nil + } + if h.Active { h.l.Lock() defer h.l.Unlock() diff --git a/terraform/transform_deposed.go b/terraform/transform_deposed.go index 456e8bfd3..2148cef47 100644 --- a/terraform/transform_deposed.go +++ b/terraform/transform_deposed.go @@ -127,6 +127,12 @@ func (n *graphNodeDeposedResource) EvalTree() EvalNode { State: &state, Output: &diff, }, + // Call pre-apply hook + &EvalApplyPre{ + Info: info, + State: &state, + Diff: &diff, + }, &EvalApply{ Info: info, State: &state, diff --git a/terraform/transform_orphan.go b/terraform/transform_orphan.go index 83a4f6d57..1b288b39d 100644 --- a/terraform/transform_orphan.go +++ b/terraform/transform_orphan.go @@ -321,6 +321,11 @@ func (n *graphNodeOrphanResource) managedResourceEvalNodes(info *InstanceInfo) [ Name: n.ResourceKey.String(), Output: &state, }, + &EvalApplyPre{ + Info: info, + State: &state, + Diff: &diff, + }, &EvalApply{ Info: info, State: &state, diff --git a/terraform/transform_resource.go b/terraform/transform_resource.go index 00628c7c3..743032377 100644 --- a/terraform/transform_resource.go +++ b/terraform/transform_resource.go @@ -500,6 +500,11 @@ func (n *graphNodeExpandedResource) managedResourceEvalNodes(resource *Resource, Name: n.stateId(), Output: &state, }, + &EvalApplyPre{ + Info: info, + State: &state, + Diff: &diffApply, + }, &EvalApply{ Info: info, State: &state, From a50003d9f61d54149d2f3a7310a6f15f85eb59bc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jan 2017 20:45:06 -0800 Subject: [PATCH 08/10] terraform: fix compilation by complying with interface --- terraform/debug.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/debug.go b/terraform/debug.go index 168bbd55f..265339f63 100644 --- a/terraform/debug.go +++ b/terraform/debug.go @@ -413,7 +413,7 @@ func (*DebugHook) PreProvision(ii *InstanceInfo, s string) (HookAction, error) { return HookActionContinue, nil } -func (*DebugHook) PostProvision(ii *InstanceInfo, s string) (HookAction, error) { +func (*DebugHook) PostProvision(ii *InstanceInfo, s string, err error) (HookAction, error) { if dbug == nil { return HookActionContinue, nil } From 3a1140951bcedada4d376316133ee061015acf0c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jan 2017 23:21:29 -0800 Subject: [PATCH 09/10] website: document destroy provisioners --- .../docs/configuration/resources.html.md | 3 + .../docs/provisioners/index.html.markdown | 101 ++++++++++++++++-- 2 files changed, 98 insertions(+), 6 deletions(-) diff --git a/website/source/docs/configuration/resources.html.md b/website/source/docs/configuration/resources.html.md index 68454f41a..0a89dded5 100644 --- a/website/source/docs/configuration/resources.html.md +++ b/website/source/docs/configuration/resources.html.md @@ -295,6 +295,9 @@ where `PROVISIONER` is: provisioner NAME { CONFIG ... + [when = "create"|"destroy"] + [on_failure = "continue"|"fail"] + [CONNECTION] } ``` diff --git a/website/source/docs/provisioners/index.html.markdown b/website/source/docs/provisioners/index.html.markdown index 07d37feea..738631585 100644 --- a/website/source/docs/provisioners/index.html.markdown +++ b/website/source/docs/provisioners/index.html.markdown @@ -3,15 +3,104 @@ layout: "docs" page_title: "Provisioners" sidebar_current: "docs-provisioners" description: |- - When a resource is initially created, provisioners can be executed to initialize that resource. This can be used to add resources to an inventory management system, run a configuration management tool, bootstrap the resource into a cluster, etc. + Provisioners are used to execute scripts on a local or remote machine as part of resource creation or destruction. --- # Provisioners -When a resource is initially created, provisioners can be executed to -initialize that resource. This can be used to add resources to an inventory -management system, run a configuration management tool, bootstrap the -resource into a cluster, etc. +Provisioners are used to execute scripts on a local or remote machine +as part of resource creation or destruction. Provisioners can be used to +bootstrap a resource, cleanup before destroy, run configuration management, etc. -Use the navigation to the left to read about the available provisioners. +Provisioners are added directly to any resource: +``` +resource "aws_instance" "web" { + # ... + + provisioner "local-exec" { + command = "echo ${self.private_ip_address} > file.txt" + } +} +``` + +For provisioners other than local execution, you must specify +[connection settings](/docs/provisioners/connection.html) so Terraform knows +how to communicate with the resource. + +## Creation-Time Provisioners + +Provisioners by default run when the resource they are defined within is +created. Creation-time provisioners are only run during _creation_, not +during updating or any other lifecycle. They are meant as a means to perform +bootstrapping of a system. + +If a creation-time provisioner fails, the resource is marked as **tainted**. +A tainted resource will be planned for destruction and recreation upon the +next `terraform apply`. Terraform does this because a failed provisioner +can leave a resource in a semi-configured state. Because Terraform cannot +reason about what the provisioner does, the only way to ensure proper creation +of a resource is to recreate it. This is tainting. + +You can change this behavior by setting the `on_failure` attribute, +which is covered in detail below. + +## Destroy-Time Provisioners + +If `when = "destroy"` is specified, the provisioner will run when the +resource it is defined within is _destroyed_. + +Destroy provisioners are run before the resource is destroyed. If they +fail, Terraform will error and rerun the provisioners again on the next +`terraform apply`. Due to this behavior, care should be taken for destroy +provisioners to be safe to run multiple times. + +## Multiple Provisioners + +Multiple provisioners can be specified within a resource. Multiple provisioners +are executed in the order they're defined in the configuration file. + +You may also mix and match creation and destruction provisioners. Only +the provisioners that are valid for a given operation will be run. Those +valid provisioners will be run in the order they're defined in the configuration +file. + +Example of multiple provisioners: + +``` +resource "aws_instance" "web" { + # ... + + provisioner "local-exec" { + command = "echo first" + } + + provisioner "local-exec" { + command = "echo second" + } +} +``` + +## Failure Behavior + +By default, provisioners that fail will also cause the Terraform apply +itself to error. The `on_failure` setting can be used to change this. The +allowed values are: + + * `"continue"` - Ignore the error and continue with creation or destruction. + + * `"fail"` - Error (the default behavior). If this is a creation provisioner, + taint the resource. + +Example: + +``` +resource "aws_instance" "web" { + # ... + + provisioner "local-exec" { + command = "echo ${self.private_ip_address} > file.txt" + on_failure = "continue" + } +} +``` From 2055885a0fb52ff6a49766e936b7bc193cc6b059 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jan 2017 23:23:55 -0800 Subject: [PATCH 10/10] website: add destroy provisioners to getting started --- terraform/debug_test.go | 2 +- .../source/intro/getting-started/provision.html.md | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/terraform/debug_test.go b/terraform/debug_test.go index 927632350..b80111fc7 100644 --- a/terraform/debug_test.go +++ b/terraform/debug_test.go @@ -156,7 +156,7 @@ func TestDebugHook_nilArgs(t *testing.T) { h.PostApply(nil, nil, nil) h.PostDiff(nil, nil) h.PostImportState(nil, nil) - h.PostProvision(nil, "") + h.PostProvision(nil, "", nil) h.PostProvisionResource(nil, nil) h.PostRefresh(nil, nil) h.PostStateUpdate(nil) diff --git a/website/source/intro/getting-started/provision.html.md b/website/source/intro/getting-started/provision.html.md index 78d79f00d..f78c3a412 100644 --- a/website/source/intro/getting-started/provision.html.md +++ b/website/source/intro/getting-started/provision.html.md @@ -100,6 +100,20 @@ If you create an execution plan with a tainted resource, however, the plan will clearly state that the resource will be destroyed because it is tainted. +## Destroy Provisioners + +Provisioners can also be defined that run only during a destroy +operation. These are useful for performing system cleanup, extracting +data, etc. + +For many resources, using built-in cleanup mechanisms is recommended +if possible (such as init scripts), but provisioners can be used if +necessary. + +The getting started guide won't show any destroy provisioner examples. +If you need to use destroy provisioners, please +[see the provisioner documentation](/docs/provisioners). + ## Next Provisioning is important for being able to bootstrap instances.