github.com/opentofu/opentofu@v1.7.1/internal/command/workspace_new.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 "os" 11 "strings" 12 "time" 13 14 "github.com/mitchellh/cli" 15 "github.com/posener/complete" 16 17 "github.com/opentofu/opentofu/internal/command/arguments" 18 "github.com/opentofu/opentofu/internal/command/clistate" 19 "github.com/opentofu/opentofu/internal/command/views" 20 "github.com/opentofu/opentofu/internal/encryption" 21 "github.com/opentofu/opentofu/internal/states/statefile" 22 "github.com/opentofu/opentofu/internal/tfdiags" 23 ) 24 25 type WorkspaceNewCommand struct { 26 Meta 27 LegacyName bool 28 } 29 30 func (c *WorkspaceNewCommand) Run(args []string) int { 31 args = c.Meta.process(args) 32 envCommandShowWarning(c.Ui, c.LegacyName) 33 34 var stateLock bool 35 var stateLockTimeout time.Duration 36 var statePath string 37 cmdFlags := c.Meta.defaultFlagSet("workspace new") 38 cmdFlags.BoolVar(&stateLock, "lock", true, "lock state") 39 cmdFlags.DurationVar(&stateLockTimeout, "lock-timeout", 0, "lock timeout") 40 cmdFlags.StringVar(&statePath, "state", "", "tofu state file") 41 cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } 42 if err := cmdFlags.Parse(args); err != nil { 43 c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) 44 return 1 45 } 46 47 args = cmdFlags.Args() 48 if len(args) != 1 { 49 c.Ui.Error("Expected a single argument: NAME.\n") 50 return cli.RunResultHelp 51 } 52 53 workspace := args[0] 54 55 if !validWorkspaceName(workspace) { 56 c.Ui.Error(fmt.Sprintf(envInvalidName, workspace)) 57 return 1 58 } 59 60 // You can't ask to create a workspace when you're overriding the 61 // workspace name to be something different. 62 if current, isOverridden := c.WorkspaceOverridden(); current != workspace && isOverridden { 63 c.Ui.Error(envIsOverriddenNewError) 64 return 1 65 } 66 67 configPath, err := modulePath(args[1:]) 68 if err != nil { 69 c.Ui.Error(err.Error()) 70 return 1 71 } 72 73 var diags tfdiags.Diagnostics 74 75 backendConfig, backendDiags := c.loadBackendConfig(configPath) 76 diags = diags.Append(backendDiags) 77 if diags.HasErrors() { 78 c.showDiagnostics(diags) 79 return 1 80 } 81 82 // Load the encryption configuration 83 enc, encDiags := c.EncryptionFromPath(configPath) 84 diags = diags.Append(encDiags) 85 if encDiags.HasErrors() { 86 c.showDiagnostics(diags) 87 return 1 88 } 89 90 // Load the backend 91 b, backendDiags := c.Backend(&BackendOpts{ 92 Config: backendConfig, 93 }, enc.State()) 94 diags = diags.Append(backendDiags) 95 if backendDiags.HasErrors() { 96 c.showDiagnostics(diags) 97 return 1 98 } 99 100 // This command will not write state 101 c.ignoreRemoteVersionConflict(b) 102 103 workspaces, err := b.Workspaces() 104 if err != nil { 105 c.Ui.Error(fmt.Sprintf("Failed to get configured named states: %s", err)) 106 return 1 107 } 108 for _, ws := range workspaces { 109 if workspace == ws { 110 c.Ui.Error(fmt.Sprintf(envExists, workspace)) 111 return 1 112 } 113 } 114 115 _, err = b.StateMgr(workspace) 116 if err != nil { 117 c.Ui.Error(err.Error()) 118 return 1 119 } 120 121 // now set the current workspace locally 122 if err := c.SetWorkspace(workspace); err != nil { 123 c.Ui.Error(fmt.Sprintf("Error selecting new workspace: %s", err)) 124 return 1 125 } 126 127 c.Ui.Output(c.Colorize().Color(fmt.Sprintf( 128 strings.TrimSpace(envCreated), workspace))) 129 130 if statePath == "" { 131 // if we're not loading a state, then we're done 132 return 0 133 } 134 135 // load the new Backend state 136 stateMgr, err := b.StateMgr(workspace) 137 if err != nil { 138 c.Ui.Error(err.Error()) 139 return 1 140 } 141 142 if stateLock { 143 stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View)) 144 if diags := stateLocker.Lock(stateMgr, "workspace-new"); diags.HasErrors() { 145 c.showDiagnostics(diags) 146 return 1 147 } 148 defer func() { 149 if diags := stateLocker.Unlock(); diags.HasErrors() { 150 c.showDiagnostics(diags) 151 } 152 }() 153 } 154 155 // read the existing state file 156 f, err := os.Open(statePath) 157 if err != nil { 158 c.Ui.Error(err.Error()) 159 return 1 160 } 161 162 stateFile, err := statefile.Read(f, encryption.StateEncryptionDisabled()) // Assume given statefile is not encrypted 163 if err != nil { 164 c.Ui.Error(err.Error()) 165 return 1 166 } 167 168 // save the existing state in the new Backend. 169 err = stateMgr.WriteState(stateFile.State) 170 if err != nil { 171 c.Ui.Error(err.Error()) 172 return 1 173 } 174 err = stateMgr.PersistState(nil) 175 if err != nil { 176 c.Ui.Error(err.Error()) 177 return 1 178 } 179 180 return 0 181 } 182 183 func (c *WorkspaceNewCommand) AutocompleteArgs() complete.Predictor { 184 return completePredictSequence{ 185 complete.PredictAnything, 186 complete.PredictDirs(""), 187 } 188 } 189 190 func (c *WorkspaceNewCommand) AutocompleteFlags() complete.Flags { 191 return complete.Flags{ 192 "-state": complete.PredictFiles("*.tfstate"), 193 } 194 } 195 196 func (c *WorkspaceNewCommand) Help() string { 197 helpText := ` 198 Usage: tofu [global options] workspace new [OPTIONS] NAME 199 200 Create a new OpenTofu workspace. 201 202 Options: 203 204 -lock=false Don't hold a state lock during the operation. This is 205 dangerous if others might concurrently run commands 206 against the same workspace. 207 208 -lock-timeout=0s Duration to retry a state lock. 209 210 -state=path Copy an existing state file into the new workspace. 211 212 ` 213 return strings.TrimSpace(helpText) 214 } 215 216 func (c *WorkspaceNewCommand) Synopsis() string { 217 return "Create a new workspace" 218 }