From 4a8c2d0958f392e33be4ae1e50fdfb6893b7425d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jan 2017 19:55:32 -0800 Subject: [PATCH] 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 052caa1887..a9b1bc101e 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 b2c9ced7bb..fee44d37a6 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 0000000000..6f39fc0b85 --- /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 0000000000..e756487f14 --- /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" + } +}