Retaining resources during destruction - New flag -suppress-forget-errors (#3588)
Some checks are pending
build / Build for freebsd_386 (push) Waiting to run
build / Build for linux_386 (push) Waiting to run
build / Build for openbsd_386 (push) Waiting to run
build / Build for windows_386 (push) Waiting to run
build / Build for freebsd_amd64 (push) Waiting to run
build / Build for linux_amd64 (push) Waiting to run
build / Build for openbsd_amd64 (push) Waiting to run
build / Build for solaris_amd64 (push) Waiting to run
build / Build for windows_amd64 (push) Waiting to run
build / Build for freebsd_arm (push) Waiting to run
build / Build for linux_arm (push) Waiting to run
build / Build for linux_arm64 (push) Waiting to run
build / Build for darwin_amd64 (push) Waiting to run
build / Build for darwin_arm64 (push) Waiting to run
build / End-to-end Tests for linux_386 (push) Waiting to run
build / End-to-end Tests for windows_386 (push) Waiting to run
build / End-to-end Tests for darwin_amd64 (push) Waiting to run
build / End-to-end Tests for linux_amd64 (push) Waiting to run
build / End-to-end Tests for windows_amd64 (push) Waiting to run
Quick Checks / List files changed for pull request (push) Waiting to run
Quick Checks / Unit tests for linux_386 (push) Blocked by required conditions
Quick Checks / Unit tests for linux_amd64 (push) Blocked by required conditions
Quick Checks / Unit tests for windows_amd64 (push) Blocked by required conditions
Quick Checks / Unit tests for linux_arm (push) Blocked by required conditions
Quick Checks / Unit tests for darwin_arm64 (push) Blocked by required conditions
Quick Checks / Unit tests for linux_arm64 (push) Blocked by required conditions
Quick Checks / Race Tests (push) Blocked by required conditions
Quick Checks / End-to-end Tests (push) Blocked by required conditions
Quick Checks / Code Consistency Checks (push) Blocked by required conditions
Quick Checks / License Checks (push) Waiting to run
Website checks / List files changed for pull request (push) Waiting to run
Website checks / Build (push) Blocked by required conditions
Website checks / Test Installation Instructions (push) Blocked by required conditions

Signed-off-by: Ilia Gogotchuri <ilia.gogotchuri0@gmail.com>
This commit is contained in:
Ilia Gogotchuri 2025-12-16 15:41:03 +04:00 committed by GitHub
parent 0256de5c4d
commit 1eacb9a046
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 149 additions and 19 deletions

View file

@ -10,6 +10,7 @@ ENHANCEMENTS:
- `prevent_destroy` arguments in the `lifecycle` block for managed resources can now use references to other symbols in the same module, such as to a module's input variables. ([#3474](https://github.com/opentofu/opentofu/issues/3474), [#3507](https://github.com/opentofu/opentofu/issues/3507))
- New `lifecycle` meta-argument `destroy` for altering resource destruction behavior. When set to `false` OpenTofu will not retain resources when they are planned for destruction. ([#3409](https://github.com/opentofu/opentofu/pull/3409))
- New `-suppress-forget-errors` flag for the `tofu destroy` command to suppress errors and exit with a zero status code when resources are forgotten during destroy operations. ([#3588](https://github.com/opentofu/opentofu/issues/3588))
- OpenTofu now uses the `BROWSER` environment variable when launching a web browser on Unix platforms, as long as it's set to a single command that can accept a URL to open as its first and only argument. ([#3456](https://github.com/opentofu/opentofu/issues/3456))
- Improve performance around provider checking and schema management. ([#2730](https://github.com/opentofu/opentofu/pull/2730))
- `tofu init` now fetches providers and their metadata in parallel. Depending on provider size and network properties, this can reduce provider installation and checking time. ([#2729](https://github.com/opentofu/opentofu/pull/2729))

View file

@ -297,7 +297,9 @@ type Operation struct {
// Injected by the command creating the operation (plan/apply/refresh/etc...)
Variables map[string]UnparsedVariableValue
RootCall configs.StaticModuleCall
// SuppressForgetErrorsDuringDestroy suppresses the error that occurs when a
// destroy operation completes successfully but leaves forgotten instances behind.
SuppressForgetErrorsDuringDestroy bool
// Some operations use root module variables only opportunistically or
// don't need them at all. If this flag is set, the backend must treat
// all variables as optional and provide an unknown value for any required

View file

@ -213,6 +213,11 @@ func (b *Local) localRunDirect(ctx context.Context, op *backend.Operation, run *
}
run.PlanOpts = planOpts
// Set ApplyOpts for direct runs to pass through the CLI flag
run.ApplyOpts = &tofu.ApplyOpts{
SuppressForgetErrorsDuringDestroy: op.SuppressForgetErrorsDuringDestroy,
}
// For a "direct" local run, the input state is the most recently stored
// snapshot, from the previous run.
state := s.State()
@ -282,7 +287,10 @@ func (b *Local) localRunForPlanFile(ctx context.Context, op *backend.Operation,
diags = diags.Append(undeclaredDiags)
declaredVars, declaredDiags := backend.ParseDeclaredVariableValues(op.Variables, config.Module.Variables)
diags = diags.Append(declaredDiags)
run.ApplyOpts = &tofu.ApplyOpts{SetVariables: declaredVars}
run.ApplyOpts = &tofu.ApplyOpts{
SetVariables: declaredVars,
SuppressForgetErrorsDuringDestroy: op.SuppressForgetErrorsDuringDestroy,
}
// NOTE: We're intentionally comparing the current locks with the
// configuration snapshot, rather than the lock snapshot in the plan file,

View file

@ -112,7 +112,7 @@ func (c *ApplyCommand) Run(rawArgs []string) int {
}
// Build the operation request
opReq, opDiags := c.OperationRequest(ctx, be, view, args.ViewType, planFile, args.Operation, args.AutoApprove, enc)
opReq, opDiags := c.OperationRequest(ctx, be, view, args, planFile, enc)
diags = diags.Append(opDiags)
// Before we delegate to the backend, we'll print any warning diagnostics
@ -254,10 +254,8 @@ func (c *ApplyCommand) OperationRequest(
ctx context.Context,
be backend.Enhanced,
view views.Apply,
viewType arguments.ViewType,
applyArgs *arguments.Apply,
planFile *planfile.WrappedPlanFile,
args *arguments.Operation,
autoApprove bool,
enc encryption.Encryption,
) (*backend.Operation, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
@ -268,16 +266,17 @@ func (c *ApplyCommand) OperationRequest(
diags = diags.Append(c.providerDevOverrideRuntimeWarnings())
// Build the operation
opReq := c.Operation(ctx, be, viewType, enc)
opReq.AutoApprove = autoApprove
opReq := c.Operation(ctx, be, applyArgs.ViewType, enc)
opReq.AutoApprove = applyArgs.AutoApprove
opReq.SuppressForgetErrorsDuringDestroy = applyArgs.SuppressForgetErrorsDuringDestroy
opReq.ConfigDir = "."
opReq.PlanMode = args.PlanMode
opReq.PlanMode = applyArgs.Operation.PlanMode
opReq.Hooks = view.Hooks()
opReq.PlanFile = planFile
opReq.PlanRefresh = args.Refresh
opReq.Targets = args.Targets
opReq.Excludes = args.Excludes
opReq.ForceReplace = args.ForceReplace
opReq.PlanRefresh = applyArgs.Operation.Refresh
opReq.Targets = applyArgs.Operation.Targets
opReq.Excludes = applyArgs.Operation.Excludes
opReq.ForceReplace = applyArgs.Operation.ForceReplace
opReq.Type = backend.OperationTypeApply
opReq.View = view.Operation()
@ -385,6 +384,10 @@ Options:
-show-sensitive If specified, sensitive values will be displayed.
-suppress-forget-errors Suppress the error that occurs when a destroy
operation completes successfully but leaves
forgotten instances behind.
-var 'foo=bar' Set a variable in the OpenTofu configuration.
This flag can be set multiple times.
@ -424,6 +427,12 @@ Usage: tofu [global options] destroy [options]
This command is a convenience alias for:
tofu apply -destroy
Options:
-suppress-forget-errors Suppress the error that occurs when a destroy
operation completes successfully but leaves
forgotten instances behind.
This command also accepts many of the plan-customization options accepted by
the tofu plan command. For more information on those options, run:
tofu plan -help

View file

@ -460,6 +460,76 @@ func TestApply_destroySkipInConfigAndState(t *testing.T) {
}
}
// TestApply_destroySkipWithSuppressFlag tests that the -suppress-forget-errors
// flag suppresses the error when destroy mode leaves forgotten instances behind.
func TestApply_destroySkipWithSuppressFlag(t *testing.T) {
// Create a temporary working directory that is empty
td := t.TempDir()
testCopyDir(t, testFixturePath("skip-destroy"), td)
t.Chdir(td)
// Create some existing state with SkipDestroy set
originalState := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"id":"baz"}`),
Status: states.ObjectReady,
SkipDestroy: true,
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
addrs.NoKey,
)
})
statePath := testStateFile(t, originalState)
p := applyFixtureProvider()
view, done := testView(t)
c := &ApplyCommand{
Destroy: true,
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
},
}
// with the suppress flag, the destroy should succeed even with forgotten instances
args := []string{
"-suppress-forget-errors",
"-state", statePath,
}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Log(output.Stdout())
t.Fatalf("expected success with -suppress-forget-errors, but got: %d\n\n%s", code, output.Stderr())
}
if _, err := os.Stat(statePath); err != nil {
t.Fatalf("err: %s", err)
}
state := testStateRead(t, statePath)
if state == nil {
t.Fatal("state should not be nil")
}
// state should be empty after the destroy
actualStr := strings.TrimSpace(state.String())
expectedStr := strings.TrimSpace(testApplyDestroyStr)
if actualStr != expectedStr {
t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr)
}
}
// In this case, the user has removed skip-destroy from config, but it's still set in state.
// We will plan a new state first, which will remove the skip-destroy attribute from state and then proceed to destroy the resource
func TestApply_destroySkipInStateNotInConfig(t *testing.T) {

View file

@ -34,6 +34,10 @@ type Apply struct {
// ShowSensitive is used to display the value of variables marked as sensitive.
ShowSensitive bool
// SuppressForgetErrorsDuringDestroy suppresses the error that occurs when a
// destroy operation completes successfully but leaves forgotten instances behind.
SuppressForgetErrorsDuringDestroy bool
}
// ParseApply processes CLI arguments, returning an Apply value and errors.
@ -51,6 +55,7 @@ func ParseApply(args []string) (*Apply, tfdiags.Diagnostics) {
cmdFlags.BoolVar(&apply.AutoApprove, "auto-approve", false, "auto-approve")
cmdFlags.BoolVar(&apply.InputEnabled, "input", true, "input")
cmdFlags.BoolVar(&apply.ShowSensitive, "show-sensitive", false, "displays sensitive values")
cmdFlags.BoolVar(&apply.SuppressForgetErrorsDuringDestroy, "suppress-forget-errors", false, "suppress errors in destroy mode due to resources being forgotten")
var json bool
cmdFlags.BoolVar(&json, "json", false, "json")

View file

@ -37,6 +37,10 @@ type ApplyOpts struct {
// in Context#mergePlanAndApplyVariables, the merging of this with the plan variable values
// follows the same logic and rules of the validation mentioned above.
SetVariables InputValues
// SuppressForgetErrorsDuringDestroy suppresses the error that would otherwise
// be raised when a destroy operation completes with forgotten instances remaining.
SuppressForgetErrorsDuringDestroy bool
}
// Apply performs the actions described by the given Plan object and returns
@ -150,11 +154,15 @@ func (c *Context) Apply(ctx context.Context, plan *plans.Plan, config *configs.C
// Even though this was the intended outcome, some automations may depend on the success of destroy operation
// to indicate the complete removal of resources
if forgetCount > 0 {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Destroy was successful but left behind forgotten instances",
"As requested, OpenTofu has not deleted some remote objects that are no longer managed by this configuration. Those objects continue to exist in their remote system and so may continue to incur charges. Refer to the original plan for more information.",
))
suppressError := opts != nil && opts.SuppressForgetErrorsDuringDestroy
if !suppressError {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Destroy was successful but left behind forgotten instances",
`As requested, OpenTofu has not deleted some remote objects that are no longer managed by this configuration. Those objects continue to exist in their remote system and so may continue to incur charges. Refer to the original plan for more information.
To suppress this error for the future 'destroy' runs, you can add the CLI flag "-suppress-forget-errors".`,
))
}
}
}

View file

@ -39,6 +39,7 @@ type skipDestroyTestCase struct {
runApply bool
expectApplyError bool
expectEmptyState bool
applyOpts *ApplyOpts
}
func setupSkipTestState(t *testing.T, instances []skipStateInstance) *states.State {
@ -128,7 +129,7 @@ func runSkipDestroyTestCase(t *testing.T, tc skipDestroyTestCase) {
verifySkipPlanChanges(t, plan, tc.expectedChanges)
if tc.runApply {
appliedState, applyDiags := ctx.Apply(t.Context(), plan, m, nil)
appliedState, applyDiags := ctx.Apply(t.Context(), plan, m, tc.applyOpts)
if tc.expectApplyError {
if !applyDiags.HasErrors() {
@ -367,6 +368,30 @@ func TestSkipDestroy_DestroyMode_ErrorOnForgotten(t *testing.T) {
expectApplyError: false,
expectEmptyState: true,
},
{
// Identical to the first test and no error because of the suppress flag below
name: "NoErrorOnForgotten_WithSuppressFlag",
config: `
resource "aws_instance" "foo" {
lifecycle {
destroy = false
}
}
`,
stateInstances: []skipStateInstance{
{addr: "aws_instance.foo", skipDestroy: true},
},
planMode: plans.DestroyMode,
expectedChanges: []skipExpectedChange{
{addr: "aws_instance.foo", action: plans.Forget},
},
runApply: true,
expectApplyError: false,
expectEmptyState: true,
applyOpts: &ApplyOpts{
SuppressForgetErrorsDuringDestroy: true,
},
},
}
for _, tc := range tc {

View file

@ -59,6 +59,8 @@ When resources are forgotten:
This exit code behavior might be important for automation and CI/CD pipelines, as it
signals that the destroy operation did not complete as a typical destroy would.
In case you want `tofu destroy` to not emit errors and exit with zero status code when resources are forgotten,
you can use the `-suppress-forget-errors` flag.
:::warning
The `destroy` attribute is persisted in the state file, even when resources are removed from the configuration.