diff --git a/.changes/v1.14/ENHANCEMENTS-20250723-141420.yaml b/.changes/v1.14/ENHANCEMENTS-20250723-141420.yaml new file mode 100644 index 0000000000..7edfaa2280 --- /dev/null +++ b/.changes/v1.14/ENHANCEMENTS-20250723-141420.yaml @@ -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" diff --git a/internal/command/test_test.go b/internal/command/test_test.go index e2055c892c..9970547ff2 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -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) { diff --git a/internal/command/testdata/test/prevent-destroy/main.tf b/internal/command/testdata/test/prevent-destroy/main.tf new file mode 100644 index 0000000000..c7deb71c62 --- /dev/null +++ b/internal/command/testdata/test/prevent-destroy/main.tf @@ -0,0 +1,7 @@ + +resource "test_resource" "resource" { + lifecycle { + // we should still be able to destroy this during tests. + prevent_destroy = true + } +} diff --git a/internal/command/testdata/test/prevent-destroy/main.tftest.hcl b/internal/command/testdata/test/prevent-destroy/main.tftest.hcl new file mode 100644 index 0000000000..d3995cae55 --- /dev/null +++ b/internal/command/testdata/test/prevent-destroy/main.tftest.hcl @@ -0,0 +1,2 @@ + +run "test" {} diff --git a/internal/moduletest/graph/node_state_cleanup.go b/internal/moduletest/graph/node_state_cleanup.go index 47dbf09e06..15df73af60 100644 --- a/internal/moduletest/graph/node_state_cleanup.go +++ b/internal/moduletest/graph/node_state_cleanup.go @@ -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) diff --git a/internal/terraform/context_plan.go b/internal/terraform/context_plan.go index 678d292054..94aa7d94eb 100644 --- a/internal/terraform/context_plan.go +++ b/internal/terraform/context_plan.go @@ -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: diff --git a/internal/terraform/graph_builder_plan.go b/internal/terraform/graph_builder_plan.go index ee6829e433..982821fabf 100644 --- a/internal/terraform/graph_builder_plan.go +++ b/internal/terraform/graph_builder_plan.go @@ -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, diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index 511b2558f9..816928c0a8 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -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