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.
This commit is contained in:
Paul Hinze 2015-04-16 17:57:18 -05:00
parent 7bb8019ce9
commit afe4abb637
7 changed files with 163 additions and 0 deletions

View File

@ -83,6 +83,7 @@ type Resource struct {
// to allow customized behavior // to allow customized behavior
type ResourceLifecycle struct { type ResourceLifecycle struct {
CreateBeforeDestroy bool `hcl:"create_before_destroy"` CreateBeforeDestroy bool `hcl:"create_before_destroy"`
PreventDestroy bool `hcl:"prevent_destroy"`
} }
// Provisioner is a configured provisioner step on a resource. // Provisioner is a configured provisioner step on a resource.

View File

@ -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) { func TestContext2Plan_computed(t *testing.T) {
m := testModule(t, "plan-computed") m := testModule(t, "plan-computed")
p := testProvider("aws") p := testProvider("aws")

View File

@ -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.`

View File

@ -0,0 +1,7 @@
resource "aws_instance" "foo" {
require_new = "yes"
lifecycle {
prevent_destroy = true
}
}

View File

@ -0,0 +1,5 @@
resource "aws_instance" "foo" {
lifecycle {
prevent_destroy = true
}
}

View File

@ -263,6 +263,10 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
Output: &diff, Output: &diff,
OutputState: &state, OutputState: &state,
}, },
&EvalCheckPreventDestroy{
Resource: n.Resource,
Diff: &diff,
},
&EvalWriteState{ &EvalWriteState{
Name: n.stateId(), Name: n.stateId(),
ResourceType: n.Resource.Type, ResourceType: n.Resource.Type,
@ -295,6 +299,10 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
State: &state, State: &state,
Output: &diff, Output: &diff,
}, },
&EvalCheckPreventDestroy{
Resource: n.Resource,
Diff: &diff,
},
&EvalWriteDiff{ &EvalWriteDiff{
Name: n.stateId(), Name: n.stateId(),
Diff: &diff, Diff: &diff,

View File

@ -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 instance is destroyed. As an example, this can be used to
create an new DNS record before removing an old record. 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**. Within a resource, you can optionally have a **connection block**.