Skip to content

Commit 84c2e2f

Browse files
author
Liam Cervante
authored
terraform test: override prevent_destroy meta attribute (#37364)
1 parent ed76d19 commit 84c2e2f

File tree

8 files changed

+52
-5
lines changed

8 files changed

+52
-5
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: ENHANCEMENTS
2+
body: 'terraform test: ignore prevent_destroy attribute during when cleaning up tests"'
3+
time: 2025-07-23T14:14:20.602923+02:00
4+
custom:
5+
Issue: "37364"

internal/command/test_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,10 @@ func TestTest_Runs(t *testing.T) {
393393
expectedOut: []string{"test_resource.two will be destroyed"},
394394
code: 0,
395395
},
396+
"prevent-destroy": {
397+
expectedOut: []string{"1 passed, 0 failed."},
398+
code: 0,
399+
},
396400
}
397401
for name, tc := range tcs {
398402
t.Run(name, func(t *testing.T) {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
resource "test_resource" "resource" {
3+
lifecycle {
4+
// we should still be able to destroy this during tests.
5+
prevent_destroy = true
6+
}
7+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
run "test" {}

internal/moduletest/graph/node_state_cleanup.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,12 @@ func (n *NodeStateCleanup) destroy(ctx *EvalContext, runNode *NodeTestRun, waite
130130
setVariables, _, _ := runNode.FilterVariablesToModule(variables)
131131

132132
planOpts := &terraform.PlanOpts{
133-
Mode: plans.DestroyMode,
134-
SetVariables: setVariables,
135-
Overrides: mocking.PackageOverrides(run.Config, file.Config, mocks),
136-
ExternalProviders: providers,
133+
Mode: plans.DestroyMode,
134+
SetVariables: setVariables,
135+
Overrides: mocking.PackageOverrides(run.Config, file.Config, mocks),
136+
ExternalProviders: providers,
137+
SkipRefresh: true,
138+
OverridePreventDestroy: true,
137139
}
138140

139141
tfCtx, _ := terraform.NewContext(n.opts.ContextOpts)

internal/terraform/context_plan.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@ type PlanOpts struct {
139139
// Query is a boolean that indicates whether the plan is being
140140
// generated for a query operation.
141141
Query bool
142+
143+
// OverridePreventDestroy will override any prevent_destroy attributes
144+
// allowing Terraform to destroy resources even if the prevent_destroy
145+
// attribute is set. This can only be set during a destroy plan, and should
146+
// only be set during the test command.
147+
OverridePreventDestroy bool
142148
}
143149

144150
// Plan generates an execution plan by comparing the given configuration
@@ -513,6 +519,7 @@ func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State
513519
refreshOpts := *opts
514520
refreshOpts.Mode = plans.NormalMode
515521
refreshOpts.PreDestroyRefresh = true
522+
refreshOpts.OverridePreventDestroy = false
516523

517524
// FIXME: A normal plan is required here to refresh the state, because
518525
// 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,
912919
externalProviderConfigs = opts.ExternalProviders
913920
}
914921

922+
if opts != nil && opts.OverridePreventDestroy && opts.Mode != plans.DestroyMode {
923+
panic("you can only set OverridePreventDestroy during destroy operations.")
924+
}
925+
915926
switch mode := opts.Mode; mode {
916927
case plans.NormalMode:
917928
// 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,
969980
Operation: walkPlanDestroy,
970981
Overrides: opts.Overrides,
971982
SkipGraphValidation: c.graphOpts.SkipGraphValidation,
983+
overridePreventDestroy: opts.OverridePreventDestroy,
972984
}).Build(addrs.RootModuleInstance)
973985
return graph, walkPlanDestroy, diags
974986
default:

internal/terraform/graph_builder_plan.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ type PlanGraphBuilder struct {
114114
// If true, the graph builder will generate a query plan instead of a
115115
// normal plan. This is used for the "terraform query" command.
116116
queryPlan bool
117+
118+
// overridePreventDestroy is only applicable during destroy operations, and
119+
// allows Terraform to ignore the configuration attribute prevent_destroy
120+
// to destroy resources regardless.
121+
overridePreventDestroy bool
117122
}
118123

119124
// See GraphBuilder
@@ -141,6 +146,10 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
141146
panic("invalid plan operation: " + b.Operation.String())
142147
}
143148

149+
if b.overridePreventDestroy && b.Operation != walkPlanDestroy {
150+
panic("overridePreventDestroy can only be set during walkPlanDestroy operations")
151+
}
152+
144153
steps := []GraphTransformer{
145154
// Creates all the resources represented in the config
146155
&ConfigTransformer{
@@ -336,6 +345,7 @@ func (b *PlanGraphBuilder) initDestroy() {
336345
b.initPlan()
337346

338347
b.ConcreteResourceInstance = func(a *NodeAbstractResourceInstance) dag.Vertex {
348+
a.overridePreventDestroy = b.overridePreventDestroy
339349
return &NodePlanDestroyableResourceInstance{
340350
NodeAbstractResourceInstance: a,
341351
skipRefresh: b.skipRefresh,

internal/terraform/node_resource_abstract_instance.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ type NodeAbstractResourceInstance struct {
4646

4747
preDestroyRefresh bool
4848

49+
// overridePreventDestroy is set during test cleanup operations to allow
50+
// tests to clean up any created infrastructure regardless of this setting
51+
// in the configuration.
52+
overridePreventDestroy bool
53+
4954
// During import (or query) we may generate configuration for a resource, which needs
5055
// to be stored in the final change.
5156
generatedConfigHCL string
@@ -185,7 +190,7 @@ func (n *NodeAbstractResourceInstance) checkPreventDestroy(change *plans.Resourc
185190
return nil
186191
}
187192

188-
preventDestroy := n.Config.Managed.PreventDestroy
193+
preventDestroy := n.Config.Managed.PreventDestroy && !n.overridePreventDestroy
189194

190195
if (change.Action == plans.Delete || change.Action.IsReplace()) && preventDestroy {
191196
var diags tfdiags.Diagnostics

0 commit comments

Comments
 (0)