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:
parent
7bb8019ce9
commit
afe4abb637
|
@ -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.
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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.`
|
|
@ -0,0 +1,7 @@
|
||||||
|
resource "aws_instance" "foo" {
|
||||||
|
require_new = "yes"
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
prevent_destroy = true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
resource "aws_instance" "foo" {
|
||||||
|
lifecycle {
|
||||||
|
prevent_destroy = true
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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**.
|
||||||
|
|
Loading…
Reference in New Issue