Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/v1.14/ENHANCEMENTS-20250723-141420.yaml
Original file line number Diff line number Diff line change
@@ -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"
4 changes: 4 additions & 0 deletions internal/command/test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
7 changes: 7 additions & 0 deletions internal/command/testdata/test/prevent-destroy/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

resource "test_resource" "resource" {
lifecycle {
// we should still be able to destroy this during tests.
prevent_destroy = true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

run "test" {}
10 changes: 6 additions & 4 deletions internal/moduletest/graph/node_state_cleanup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions internal/terraform/context_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
10 changes: 10 additions & 0 deletions internal/terraform/graph_builder_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion internal/terraform/node_resource_abstract_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down