From afe4abb6374e1fdcbf45cb09ba800f562cf89329 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Thu, 16 Apr 2015 17:57:18 -0500 Subject: [PATCH] core: add prevent_destroy lifecycle flag When the `prevent_destroy` flag is set on a resource, any plan that would destroy that resource instead returns an error. This has the effect of preventing the resource from being unexpectedly destroyed by Terraform until the flag is removed from the config. --- config/config.go | 1 + terraform/context_test.go | 106 ++++++++++++++++++ terraform/eval_check_prevent_destroy.go | 32 ++++++ .../plan-prevent-destroy-bad/main.tf | 7 ++ .../plan-prevent-destroy-good/main.tf | 5 + terraform/transform_resource.go | 8 ++ .../docs/configuration/resources.html.md | 4 + 7 files changed, 163 insertions(+) create mode 100644 terraform/eval_check_prevent_destroy.go create mode 100644 terraform/test-fixtures/plan-prevent-destroy-bad/main.tf create mode 100644 terraform/test-fixtures/plan-prevent-destroy-good/main.tf diff --git a/config/config.go b/config/config.go index ed4aaaf887..68c2fca1b0 100644 --- a/config/config.go +++ b/config/config.go @@ -83,6 +83,7 @@ type Resource struct { // to allow customized behavior type ResourceLifecycle struct { CreateBeforeDestroy bool `hcl:"create_before_destroy"` + PreventDestroy bool `hcl:"prevent_destroy"` } // Provisioner is a configured provisioner step on a resource. diff --git a/terraform/context_test.go b/terraform/context_test.go index 6ff63d813a..a1cc68c4f0 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -504,6 +504,112 @@ func TestContext2Plan_nil(t *testing.T) { } } +func TestContext2Plan_preventDestroy_bad(t *testing.T) { + m := testModule(t, "plan-prevent-destroy-bad") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "i-abc123", + }, + }, + }, + }, + }, + }, + }) + + plan, err := ctx.Plan() + + expectedErr := "aws_instance.foo: plan would destroy" + if !strings.Contains(fmt.Sprintf("%s", err), expectedErr) { + t.Fatalf("expected err would contain %q\nerr: %s\nplan: %s", + expectedErr, err, plan) + } +} + +func TestContext2Plan_preventDestroy_good(t *testing.T) { + m := testModule(t, "plan-prevent-destroy-good") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "i-abc123", + }, + }, + }, + }, + }, + }, + }) + + plan, err := ctx.Plan() + if err != nil { + t.Fatalf("err: %s", err) + } + if !plan.Diff.Empty() { + t.Fatalf("Expected empty plan, got %s", plan.String()) + } +} + +func TestContext2Plan_preventDestroy_destroyPlan(t *testing.T) { + m := testModule(t, "plan-prevent-destroy-good") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "i-abc123", + }, + }, + }, + }, + }, + }, + Destroy: true, + }) + + plan, err := ctx.Plan() + + expectedErr := "aws_instance.foo: plan would destroy" + if !strings.Contains(fmt.Sprintf("%s", err), expectedErr) { + t.Fatalf("expected err would contain %q\nerr: %s\nplan: %s", + expectedErr, err, plan) + } +} + func TestContext2Plan_computed(t *testing.T) { m := testModule(t, "plan-computed") p := testProvider("aws") diff --git a/terraform/eval_check_prevent_destroy.go b/terraform/eval_check_prevent_destroy.go new file mode 100644 index 0000000000..7cab76d10d --- /dev/null +++ b/terraform/eval_check_prevent_destroy.go @@ -0,0 +1,32 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" +) + +// EvalPreventDestroy is an EvalNode implementation that returns an +// error if a resource has PreventDestroy configured and the diff +// would destroy the resource. +type EvalCheckPreventDestroy struct { + Resource *config.Resource + Diff **InstanceDiff +} + +func (n *EvalCheckPreventDestroy) Eval(ctx EvalContext) (interface{}, error) { + if n.Diff == nil || *n.Diff == nil || n.Resource == nil { + return nil, nil + } + + diff := *n.Diff + preventDestroy := n.Resource.Lifecycle.PreventDestroy + + if diff.Destroy && preventDestroy { + return nil, fmt.Errorf(preventDestroyErrStr, n.Resource.Id()) + } + + return nil, nil +} + +const preventDestroyErrStr = `%s: plan would destroy, but resource has prevent_destroy set. To avoid this error, either disable prevent_destroy, or change your config so the plan does not destroy this resource.` diff --git a/terraform/test-fixtures/plan-prevent-destroy-bad/main.tf b/terraform/test-fixtures/plan-prevent-destroy-bad/main.tf new file mode 100644 index 0000000000..19077c1a65 --- /dev/null +++ b/terraform/test-fixtures/plan-prevent-destroy-bad/main.tf @@ -0,0 +1,7 @@ +resource "aws_instance" "foo" { + require_new = "yes" + + lifecycle { + prevent_destroy = true + } +} diff --git a/terraform/test-fixtures/plan-prevent-destroy-good/main.tf b/terraform/test-fixtures/plan-prevent-destroy-good/main.tf new file mode 100644 index 0000000000..a88b9e3e10 --- /dev/null +++ b/terraform/test-fixtures/plan-prevent-destroy-good/main.tf @@ -0,0 +1,5 @@ +resource "aws_instance" "foo" { + lifecycle { + prevent_destroy = true + } +} diff --git a/terraform/transform_resource.go b/terraform/transform_resource.go index 7a968885ac..3168836373 100644 --- a/terraform/transform_resource.go +++ b/terraform/transform_resource.go @@ -263,6 +263,10 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode { Output: &diff, OutputState: &state, }, + &EvalCheckPreventDestroy{ + Resource: n.Resource, + Diff: &diff, + }, &EvalWriteState{ Name: n.stateId(), ResourceType: n.Resource.Type, @@ -295,6 +299,10 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode { State: &state, Output: &diff, }, + &EvalCheckPreventDestroy{ + Resource: n.Resource, + Diff: &diff, + }, &EvalWriteDiff{ Name: n.stateId(), Diff: &diff, diff --git a/website/source/docs/configuration/resources.html.md b/website/source/docs/configuration/resources.html.md index c74f030c96..4e612428e7 100644 --- a/website/source/docs/configuration/resources.html.md +++ b/website/source/docs/configuration/resources.html.md @@ -64,6 +64,10 @@ The `lifecycle` block allows the following keys to be set: instance is destroyed. As an example, this can be used to create an new DNS record before removing an old record. + * `prevent_destroy` (bool) - This flag provides extra protection against the + destruction of a given resource. When this is set to `true`, any plan + that includes a destroy of this resource will return an error message. + ------------- Within a resource, you can optionally have a **connection block**.