// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package command import ( "context" "strings" "time" backendInit "github.com/hashicorp/terraform/internal/backend/init" "github.com/hashicorp/terraform/internal/backend/local" "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/moduletest" "github.com/hashicorp/terraform/internal/tfdiags" ) // TestCleanupCommand is a command that cleans up left-over resources created // during Terraform test runs. It basically runs the test command in cleanup mode. type TestCleanupCommand struct { Meta } func (c *TestCleanupCommand) Help() string { helpText := ` Usage: terraform [global options] test cleanup [options] Cleans up left-over resources in states that were created during Terraform test runs. By default, this command ignores the skip_cleanup attributes in the manifest file. Use the -repair flag to override this behavior, which will ensure that resources that were intentionally left-over are exempt from cleanup. Options: -repair Overrides the skip_cleanup attribute in the manifest file and attempts to clean up all resources. -no-color If specified, output won't contain any color. -verbose Print detailed output during the cleanup process. ` return strings.TrimSpace(helpText) } func (c *TestCleanupCommand) Synopsis() string { return "Clean up left-over resources created during Terraform test runs" } func (c *TestCleanupCommand) Run(rawArgs []string) int { setup, diags := c.setupTestExecution(moduletest.CleanupMode, "test cleanup", rawArgs) if diags.HasErrors() { return 1 } args := setup.Args view := setup.View config := setup.Config variables := setup.Variables testVariables := setup.TestVariables opts := setup.Opts // We have two levels of interrupt here. A 'stop' and a 'cancel'. A 'stop' // is a soft request to stop. We'll finish the current test, do the tidy up, // but then skip all remaining tests and run blocks. A 'cancel' is a hard // request to stop now. We'll cancel the current operation immediately // even if it's a delete operation, and we won't clean up any infrastructure // if we're halfway through a test. We'll print details explaining what was // stopped so the user can do their best to recover from it. runningCtx, done := context.WithCancel(context.Background()) stopCtx, stop := context.WithCancel(runningCtx) cancelCtx, cancel := context.WithCancel(context.Background()) runner := &local.TestSuiteRunner{ BackendFactory: backendInit.Backend, Config: config, // The GlobalVariables are loaded from the // main configuration directory // The GlobalTestVariables are loaded from the // test directory GlobalVariables: variables, GlobalTestVariables: testVariables, TestingDirectory: args.TestDirectory, Opts: opts, View: view, Stopped: false, Cancelled: false, StoppedCtx: stopCtx, CancelledCtx: cancelCtx, Filter: args.Filter, Verbose: args.Verbose, Repair: args.Repair, CommandMode: moduletest.CleanupMode, } var testDiags tfdiags.Diagnostics go func() { defer logging.PanicHandler() defer done() defer stop() defer cancel() _, testDiags = runner.Test(c.Meta.AllowExperimentalFeatures) }() // Wait for the operation to complete, or for an interrupt to occur. select { case <-c.ShutdownCh: // Nice request to be cancelled. view.Interrupted() runner.Stop() stop() select { case <-c.ShutdownCh: // The user pressed it again, now we have to get it to stop as // fast as possible. view.FatalInterrupt() runner.Cancel() cancel() waitTime := 5 * time.Second // We'll wait 5 seconds for this operation to finish now, regardless // of whether it finishes successfully or not. select { case <-runningCtx.Done(): case <-time.After(waitTime): } case <-runningCtx.Done(): // The application finished nicely after the request was stopped. } case <-runningCtx.Done(): // tests finished normally with no interrupts. } view.Diagnostics(nil, nil, testDiags) return 0 }