mirror of
https://github.com/opentofu/opentofu.git
synced 2025-12-18 15:46:08 -05:00
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
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:
parent
0256de5c4d
commit
1eacb9a046
9 changed files with 149 additions and 19 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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".`,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue