diff --git a/config/config.go b/config/config.go index ed4aaaf88..68c2fca1b 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 6ff63d813..a1cc68c4f 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 000000000..7cab76d10 --- /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 000000000..19077c1a6 --- /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 000000000..a88b9e3e1 --- /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 7a968885a..316883637 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 c74f030c9..4e612428e 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**.