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  }