github.com/opentofu/opentofu@v1.7.1/internal/command/workspace_delete.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package command
     7  
     8  import (
     9  	"fmt"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/mitchellh/cli"
    14  	"github.com/posener/complete"
    15  
    16  	"github.com/opentofu/opentofu/internal/command/arguments"
    17  	"github.com/opentofu/opentofu/internal/command/clistate"
    18  	"github.com/opentofu/opentofu/internal/command/views"
    19  	"github.com/opentofu/opentofu/internal/states"
    20  	"github.com/opentofu/opentofu/internal/tfdiags"
    21  )
    22  
    23  type WorkspaceDeleteCommand struct {
    24  	Meta
    25  	LegacyName bool
    26  }
    27  
    28  func (c *WorkspaceDeleteCommand) Run(args []string) int {
    29  	args = c.Meta.process(args)
    30  	envCommandShowWarning(c.Ui, c.LegacyName)
    31  
    32  	var force bool
    33  	var stateLock bool
    34  	var stateLockTimeout time.Duration
    35  	cmdFlags := c.Meta.defaultFlagSet("workspace delete")
    36  	cmdFlags.BoolVar(&force, "force", false, "force removal of a non-empty workspace")
    37  	cmdFlags.BoolVar(&stateLock, "lock", true, "lock state")
    38  	cmdFlags.DurationVar(&stateLockTimeout, "lock-timeout", 0, "lock timeout")
    39  	cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
    40  	if err := cmdFlags.Parse(args); err != nil {
    41  		c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
    42  		return 1
    43  	}
    44  
    45  	args = cmdFlags.Args()
    46  	if len(args) != 1 {
    47  		c.Ui.Error("Expected a single argument: NAME.\n")
    48  		return cli.RunResultHelp
    49  	}
    50  
    51  	configPath, err := modulePath(args[1:])
    52  	if err != nil {
    53  		c.Ui.Error(err.Error())
    54  		return 1
    55  	}
    56  
    57  	var diags tfdiags.Diagnostics
    58  
    59  	backendConfig, backendDiags := c.loadBackendConfig(configPath)
    60  	diags = diags.Append(backendDiags)
    61  	if diags.HasErrors() {
    62  		c.showDiagnostics(diags)
    63  		return 1
    64  	}
    65  
    66  	// Load the encryption configuration
    67  	enc, encDiags := c.EncryptionFromPath(configPath)
    68  	diags = diags.Append(encDiags)
    69  	if encDiags.HasErrors() {
    70  		c.showDiagnostics(diags)
    71  		return 1
    72  	}
    73  
    74  	// Load the backend
    75  	b, backendDiags := c.Backend(&BackendOpts{
    76  		Config: backendConfig,
    77  	}, enc.State())
    78  	diags = diags.Append(backendDiags)
    79  	if backendDiags.HasErrors() {
    80  		c.showDiagnostics(diags)
    81  		return 1
    82  	}
    83  
    84  	// This command will not write state
    85  	c.ignoreRemoteVersionConflict(b)
    86  
    87  	workspaces, err := b.Workspaces()
    88  	if err != nil {
    89  		c.Ui.Error(err.Error())
    90  		return 1
    91  	}
    92  
    93  	workspace := args[0]
    94  	exists := false
    95  	for _, ws := range workspaces {
    96  		if workspace == ws {
    97  			exists = true
    98  			break
    99  		}
   100  	}
   101  
   102  	if !exists {
   103  		c.Ui.Error(fmt.Sprintf(strings.TrimSpace(envDoesNotExist), workspace))
   104  		return 1
   105  	}
   106  
   107  	currentWorkspace, err := c.Workspace()
   108  	if err != nil {
   109  		c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
   110  		return 1
   111  	}
   112  	if workspace == currentWorkspace {
   113  		c.Ui.Error(fmt.Sprintf(strings.TrimSpace(envDelCurrent), workspace))
   114  		return 1
   115  	}
   116  
   117  	// we need the actual state to see if it's empty
   118  	stateMgr, err := b.StateMgr(workspace)
   119  	if err != nil {
   120  		c.Ui.Error(err.Error())
   121  		return 1
   122  	}
   123  
   124  	var stateLocker clistate.Locker
   125  	if stateLock {
   126  		stateLocker = clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View))
   127  		if diags := stateLocker.Lock(stateMgr, "state-replace-provider"); diags.HasErrors() {
   128  			c.showDiagnostics(diags)
   129  			return 1
   130  		}
   131  	} else {
   132  		stateLocker = clistate.NewNoopLocker()
   133  	}
   134  
   135  	if err := stateMgr.RefreshState(); err != nil {
   136  		// We need to release the lock before exit
   137  		stateLocker.Unlock()
   138  		c.Ui.Error(err.Error())
   139  		return 1
   140  	}
   141  
   142  	hasResources := stateMgr.State().HasManagedResourceInstanceObjects()
   143  
   144  	if hasResources && !force {
   145  		// We'll collect a list of what's being managed here as extra context
   146  		// for the message.
   147  		var buf strings.Builder
   148  		for _, obj := range stateMgr.State().AllResourceInstanceObjectAddrs() {
   149  			if obj.DeposedKey == states.NotDeposed {
   150  				fmt.Fprintf(&buf, "\n  - %s", obj.Instance.String())
   151  			} else {
   152  				fmt.Fprintf(&buf, "\n  - %s (deposed object %s)", obj.Instance.String(), obj.DeposedKey)
   153  			}
   154  		}
   155  
   156  		// We need to release the lock before exit
   157  		stateLocker.Unlock()
   158  
   159  		diags = diags.Append(tfdiags.Sourceless(
   160  			tfdiags.Error,
   161  			"Workspace is not empty",
   162  			fmt.Sprintf(
   163  				"Workspace %q is currently tracking the following resource instances:%s\n\nDeleting this workspace would cause OpenTofu to lose track of any associated remote objects, which would then require you to delete them manually outside of OpenTofu. You should destroy these objects with OpenTofu before deleting the workspace.\n\nIf you want to delete this workspace anyway, and have OpenTofu forget about these managed objects, use the -force option to disable this safety check.",
   164  				workspace, buf.String(),
   165  			),
   166  		))
   167  		c.showDiagnostics(diags)
   168  		return 1
   169  	}
   170  
   171  	// We need to release the lock just before deleting the state, in case
   172  	// the backend can't remove the resource while holding the lock. This
   173  	// is currently true for Windows local files.
   174  	//
   175  	// TODO: While there is little safety in locking while deleting the
   176  	// state, it might be nice to be able to coordinate processes around
   177  	// state deletion, i.e. in a CI environment. Adding Delete() as a
   178  	// required method of States would allow the removal of the resource to
   179  	// be delegated from the Backend to the State itself.
   180  	stateLocker.Unlock()
   181  
   182  	err = b.DeleteWorkspace(workspace, force)
   183  	if err != nil {
   184  		c.Ui.Error(err.Error())
   185  		return 1
   186  	}
   187  
   188  	c.Ui.Output(
   189  		c.Colorize().Color(
   190  			fmt.Sprintf(envDeleted, workspace),
   191  		),
   192  	)
   193  
   194  	if hasResources {
   195  		c.Ui.Output(
   196  			c.Colorize().Color(
   197  				fmt.Sprintf(envWarnNotEmpty, workspace),
   198  			),
   199  		)
   200  	}
   201  
   202  	return 0
   203  }
   204  
   205  func (c *WorkspaceDeleteCommand) AutocompleteArgs() complete.Predictor {
   206  	return completePredictSequence{
   207  		c.completePredictWorkspaceName(),
   208  		complete.PredictDirs(""),
   209  	}
   210  }
   211  
   212  func (c *WorkspaceDeleteCommand) AutocompleteFlags() complete.Flags {
   213  	return complete.Flags{
   214  		"-force": complete.PredictNothing,
   215  	}
   216  }
   217  
   218  func (c *WorkspaceDeleteCommand) Help() string {
   219  	helpText := `
   220  Usage: tofu [global options] workspace delete [OPTIONS] NAME
   221  
   222    Delete a OpenTofu workspace
   223  
   224  
   225  Options:
   226  
   227    -force             Remove a workspace even if it is managing resources.
   228                       OpenTofu can no longer track or manage the workspace's
   229                       infrastructure.
   230  
   231    -lock=false        Don't hold a state lock during the operation. This is
   232                       dangerous if others might concurrently run commands
   233                       against the same workspace.
   234  
   235    -lock-timeout=0s   Duration to retry a state lock.
   236  
   237  `
   238  	return strings.TrimSpace(helpText)
   239  }
   240  
   241  func (c *WorkspaceDeleteCommand) Synopsis() string {
   242  	return "Delete a workspace"
   243  }