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 }