terraform test: override prevent_destroy meta attribute (#37364)

pull/37370/head^2
Liam Cervante 9 months ago committed by GitHub
parent ed76d19043
commit 84c2e2f9e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
kind: ENHANCEMENTS
body: 'terraform test: ignore prevent_destroy attribute during when cleaning up tests"'
time: 2025-07-23T14:14:20.602923+02:00
custom:
Issue: "37364"

@ -393,6 +393,10 @@ func TestTest_Runs(t *testing.T) {
expectedOut: []string{"test_resource.two will be destroyed"},
code: 0,
},
"prevent-destroy": {
expectedOut: []string{"1 passed, 0 failed."},
code: 0,
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {

@ -0,0 +1,7 @@
resource "test_resource" "resource" {
lifecycle {
// we should still be able to destroy this during tests.
prevent_destroy = true
}
}

@ -130,10 +130,12 @@ func (n *NodeStateCleanup) destroy(ctx *EvalContext, runNode *NodeTestRun, waite
setVariables, _, _ := runNode.FilterVariablesToModule(variables)
planOpts := &terraform.PlanOpts{
Mode: plans.DestroyMode,
SetVariables: setVariables,
Overrides: mocking.PackageOverrides(run.Config, file.Config, mocks),
ExternalProviders: providers,
Mode: plans.DestroyMode,
SetVariables: setVariables,
Overrides: mocking.PackageOverrides(run.Config, file.Config, mocks),
ExternalProviders: providers,
SkipRefresh: true,
OverridePreventDestroy: true,
}
tfCtx, _ := terraform.NewContext(n.opts.ContextOpts)

@ -139,6 +139,12 @@ type PlanOpts struct {
// Query is a boolean that indicates whether the plan is being
// generated for a query operation.
Query bool
// OverridePreventDestroy will override any prevent_destroy attributes
// allowing Terraform to destroy resources even if the prevent_destroy
// attribute is set. This can only be set during a destroy plan, and should
// only be set during the test command.
OverridePreventDestroy bool
}
// Plan generates an execution plan by comparing the given configuration
@ -513,6 +519,7 @@ func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State
refreshOpts := *opts
refreshOpts.Mode = plans.NormalMode
refreshOpts.PreDestroyRefresh = true
refreshOpts.OverridePreventDestroy = false
// FIXME: A normal plan is required here to refresh the state, because
// the state and configuration may not match during a destroy, and a
@ -912,6 +919,10 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State,
externalProviderConfigs = opts.ExternalProviders
}
if opts != nil && opts.OverridePreventDestroy && opts.Mode != plans.DestroyMode {
panic("you can only set OverridePreventDestroy during destroy operations.")
}
switch mode := opts.Mode; mode {
case plans.NormalMode:
// In Normal mode we need to pay attention to import and removed blocks
@ -969,6 +980,7 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State,
Operation: walkPlanDestroy,
Overrides: opts.Overrides,
SkipGraphValidation: c.graphOpts.SkipGraphValidation,
overridePreventDestroy: opts.OverridePreventDestroy,
}).Build(addrs.RootModuleInstance)
return graph, walkPlanDestroy, diags
default:

@ -114,6 +114,11 @@ type PlanGraphBuilder struct {
// If true, the graph builder will generate a query plan instead of a
// normal plan. This is used for the "terraform query" command.
queryPlan bool
// overridePreventDestroy is only applicable during destroy operations, and
// allows Terraform to ignore the configuration attribute prevent_destroy
// to destroy resources regardless.
overridePreventDestroy bool
}
// See GraphBuilder
@ -141,6 +146,10 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
panic("invalid plan operation: " + b.Operation.String())
}
if b.overridePreventDestroy && b.Operation != walkPlanDestroy {
panic("overridePreventDestroy can only be set during walkPlanDestroy operations")
}
steps := []GraphTransformer{
// Creates all the resources represented in the config
&ConfigTransformer{
@ -336,6 +345,7 @@ func (b *PlanGraphBuilder) initDestroy() {
b.initPlan()
b.ConcreteResourceInstance = func(a *NodeAbstractResourceInstance) dag.Vertex {
a.overridePreventDestroy = b.overridePreventDestroy
return &NodePlanDestroyableResourceInstance{
NodeAbstractResourceInstance: a,
skipRefresh: b.skipRefresh,

@ -46,6 +46,11 @@ type NodeAbstractResourceInstance struct {
preDestroyRefresh bool
// overridePreventDestroy is set during test cleanup operations to allow
// tests to clean up any created infrastructure regardless of this setting
// in the configuration.
overridePreventDestroy bool
// During import (or query) we may generate configuration for a resource, which needs
// to be stored in the final change.
generatedConfigHCL string
@ -185,7 +190,7 @@ func (n *NodeAbstractResourceInstance) checkPreventDestroy(change *plans.Resourc
return nil
}
preventDestroy := n.Config.Managed.PreventDestroy
preventDestroy := n.Config.Managed.PreventDestroy && !n.overridePreventDestroy
if (change.Action == plans.Delete || change.Action.IsReplace()) && preventDestroy {
var diags tfdiags.Diagnostics

Loading…
Cancel
Save