Skip to content

Commit afe4abb

Browse files
committed
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.
1 parent 7bb8019 commit afe4abb

7 files changed

Lines changed: 163 additions & 0 deletions

File tree

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ type Resource struct {
8383
// to allow customized behavior
8484
type ResourceLifecycle struct {
8585
CreateBeforeDestroy bool `hcl:"create_before_destroy"`
86+
PreventDestroy bool `hcl:"prevent_destroy"`
8687
}
8788

8889
// Provisioner is a configured provisioner step on a resource.

terraform/context_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,112 @@ func TestContext2Plan_nil(t *testing.T) {
504504
}
505505
}
506506

507+
func TestContext2Plan_preventDestroy_bad(t *testing.T) {
508+
m := testModule(t, "plan-prevent-destroy-bad")
509+
p := testProvider("aws")
510+
p.DiffFn = testDiffFn
511+
ctx := testContext2(t, &ContextOpts{
512+
Module: m,
513+
Providers: map[string]ResourceProviderFactory{
514+
"aws": testProviderFuncFixed(p),
515+
},
516+
State: &State{
517+
Modules: []*ModuleState{
518+
&ModuleState{
519+
Path: rootModulePath,
520+
Resources: map[string]*ResourceState{
521+
"aws_instance.foo": &ResourceState{
522+
Type: "aws_instance",
523+
Primary: &InstanceState{
524+
ID: "i-abc123",
525+
},
526+
},
527+
},
528+
},
529+
},
530+
},
531+
})
532+
533+
plan, err := ctx.Plan()
534+
535+
expectedErr := "aws_instance.foo: plan would destroy"
536+
if !strings.Contains(fmt.Sprintf("%s", err), expectedErr) {
537+
t.Fatalf("expected err would contain %q\nerr: %s\nplan: %s",
538+
expectedErr, err, plan)
539+
}
540+
}
541+
542+
func TestContext2Plan_preventDestroy_good(t *testing.T) {
543+
m := testModule(t, "plan-prevent-destroy-good")
544+
p := testProvider("aws")
545+
p.DiffFn = testDiffFn
546+
ctx := testContext2(t, &ContextOpts{
547+
Module: m,
548+
Providers: map[string]ResourceProviderFactory{
549+
"aws": testProviderFuncFixed(p),
550+
},
551+
State: &State{
552+
Modules: []*ModuleState{
553+
&ModuleState{
554+
Path: rootModulePath,
555+
Resources: map[string]*ResourceState{
556+
"aws_instance.foo": &ResourceState{
557+
Type: "aws_instance",
558+
Primary: &InstanceState{
559+
ID: "i-abc123",
560+
},
561+
},
562+
},
563+
},
564+
},
565+
},
566+
})
567+
568+
plan, err := ctx.Plan()
569+
if err != nil {
570+
t.Fatalf("err: %s", err)
571+
}
572+
if !plan.Diff.Empty() {
573+
t.Fatalf("Expected empty plan, got %s", plan.String())
574+
}
575+
}
576+
577+
func TestContext2Plan_preventDestroy_destroyPlan(t *testing.T) {
578+
m := testModule(t, "plan-prevent-destroy-good")
579+
p := testProvider("aws")
580+
p.DiffFn = testDiffFn
581+
ctx := testContext2(t, &ContextOpts{
582+
Module: m,
583+
Providers: map[string]ResourceProviderFactory{
584+
"aws": testProviderFuncFixed(p),
585+
},
586+
State: &State{
587+
Modules: []*ModuleState{
588+
&ModuleState{
589+
Path: rootModulePath,
590+
Resources: map[string]*ResourceState{
591+
"aws_instance.foo": &ResourceState{
592+
Type: "aws_instance",
593+
Primary: &InstanceState{
594+
ID: "i-abc123",
595+
},
596+
},
597+
},
598+
},
599+
},
600+
},
601+
Destroy: true,
602+
})
603+
604+
plan, err := ctx.Plan()
605+
606+
expectedErr := "aws_instance.foo: plan would destroy"
607+
if !strings.Contains(fmt.Sprintf("%s", err), expectedErr) {
608+
t.Fatalf("expected err would contain %q\nerr: %s\nplan: %s",
609+
expectedErr, err, plan)
610+
}
611+
}
612+
507613
func TestContext2Plan_computed(t *testing.T) {
508614
m := testModule(t, "plan-computed")
509615
p := testProvider("aws")
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package terraform
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/hashicorp/terraform/config"
7+
)
8+
9+
// EvalPreventDestroy is an EvalNode implementation that returns an
10+
// error if a resource has PreventDestroy configured and the diff
11+
// would destroy the resource.
12+
type EvalCheckPreventDestroy struct {
13+
Resource *config.Resource
14+
Diff **InstanceDiff
15+
}
16+
17+
func (n *EvalCheckPreventDestroy) Eval(ctx EvalContext) (interface{}, error) {
18+
if n.Diff == nil || *n.Diff == nil || n.Resource == nil {
19+
return nil, nil
20+
}
21+
22+
diff := *n.Diff
23+
preventDestroy := n.Resource.Lifecycle.PreventDestroy
24+
25+
if diff.Destroy && preventDestroy {
26+
return nil, fmt.Errorf(preventDestroyErrStr, n.Resource.Id())
27+
}
28+
29+
return nil, nil
30+
}
31+
32+
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.`
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
resource "aws_instance" "foo" {
2+
require_new = "yes"
3+
4+
lifecycle {
5+
prevent_destroy = true
6+
}
7+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
resource "aws_instance" "foo" {
2+
lifecycle {
3+
prevent_destroy = true
4+
}
5+
}

terraform/transform_resource.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,10 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
263263
Output: &diff,
264264
OutputState: &state,
265265
},
266+
&EvalCheckPreventDestroy{
267+
Resource: n.Resource,
268+
Diff: &diff,
269+
},
266270
&EvalWriteState{
267271
Name: n.stateId(),
268272
ResourceType: n.Resource.Type,
@@ -295,6 +299,10 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
295299
State: &state,
296300
Output: &diff,
297301
},
302+
&EvalCheckPreventDestroy{
303+
Resource: n.Resource,
304+
Diff: &diff,
305+
},
298306
&EvalWriteDiff{
299307
Name: n.stateId(),
300308
Diff: &diff,

website/source/docs/configuration/resources.html.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ The `lifecycle` block allows the following keys to be set:
6464
instance is destroyed. As an example, this can be used to
6565
create an new DNS record before removing an old record.
6666

67+
* `prevent_destroy` (bool) - This flag provides extra protection against the
68+
destruction of a given resource. When this is set to `true`, any plan
69+
that includes a destroy of this resource will return an error message.
70+
6771
-------------
6872

6973
Within a resource, you can optionally have a **connection block**.

0 commit comments

Comments
 (0)