mirror of
https://github.com/hashicorp/terraform.git
synced 2026-02-19 02:39:17 -05:00
* feat: Update the `workspace new` subcommand to work with PSS, add E2E testing * refactor: Replace instances of `ioutil` with `os` while looking at the workspace command * docs: Update code comments in `workspace new` command * test: Update E2E test using PSS with workspace commands to assert state files are created by given commands * test: Include `workspace show` in happy path E2E test using PSS * fix: Allow DeleteState RPC to include the id of the state to delete * test: Include `workspace delete` in happy path E2E test using PSS * fix: Avoid assignment to nil map in mock provider during WriteStateBytes * test: Add integration test for workspace commands when using PSS We still need an E2E test for this, to ensure that the GRPC-related packages pass all the expected data between core and the provider. * test: Update test to reflect changes in the test fixture configuration * docs: Fix code comment * test: Change test to build its own Terraform binary with experiments enabled * refactor: Replace use of `newMeta` with reuse of Meta that we re-set the UI value on
228 lines
5.8 KiB
Go
228 lines
5.8 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package command
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/cli"
|
|
"github.com/hashicorp/terraform/internal/backend/local"
|
|
backendPluggable "github.com/hashicorp/terraform/internal/backend/pluggable"
|
|
"github.com/hashicorp/terraform/internal/command/arguments"
|
|
"github.com/hashicorp/terraform/internal/command/clistate"
|
|
"github.com/hashicorp/terraform/internal/command/views"
|
|
"github.com/hashicorp/terraform/internal/states"
|
|
"github.com/hashicorp/terraform/internal/states/statefile"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
"github.com/posener/complete"
|
|
)
|
|
|
|
type WorkspaceNewCommand struct {
|
|
Meta
|
|
LegacyName bool
|
|
}
|
|
|
|
func (c *WorkspaceNewCommand) Run(args []string) int {
|
|
args = c.Meta.process(args)
|
|
envCommandShowWarning(c.Ui, c.LegacyName)
|
|
|
|
var stateLock bool
|
|
var stateLockTimeout time.Duration
|
|
var statePath string
|
|
cmdFlags := c.Meta.defaultFlagSet("workspace new")
|
|
cmdFlags.BoolVar(&stateLock, "lock", true, "lock state")
|
|
cmdFlags.DurationVar(&stateLockTimeout, "lock-timeout", 0, "lock timeout")
|
|
cmdFlags.StringVar(&statePath, "state", "", "terraform state file")
|
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
|
if err := cmdFlags.Parse(args); err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
|
|
return 1
|
|
}
|
|
|
|
args = cmdFlags.Args()
|
|
if len(args) != 1 {
|
|
c.Ui.Error("Expected a single argument: NAME.\n")
|
|
return cli.RunResultHelp
|
|
}
|
|
|
|
workspace := args[0]
|
|
|
|
if !validWorkspaceName(workspace) {
|
|
c.Ui.Error(fmt.Sprintf(envInvalidName, workspace))
|
|
return 1
|
|
}
|
|
|
|
// You can't ask to create a workspace when you're overriding the
|
|
// workspace name to be something different.
|
|
if current, isOverridden := c.WorkspaceOverridden(); current != workspace && isOverridden {
|
|
c.Ui.Error(envIsOverriddenNewError)
|
|
return 1
|
|
}
|
|
|
|
configPath, err := ModulePath(args[1:])
|
|
if err != nil {
|
|
c.Ui.Error(err.Error())
|
|
return 1
|
|
}
|
|
|
|
var diags tfdiags.Diagnostics
|
|
|
|
// Load the backend
|
|
view := arguments.ViewHuman
|
|
b, diags := c.backend(configPath, view)
|
|
if diags.HasErrors() {
|
|
c.showDiagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
// This command will not write state
|
|
c.ignoreRemoteVersionConflict(b)
|
|
|
|
workspaces, wDiags := b.Workspaces()
|
|
if wDiags.HasErrors() {
|
|
c.Ui.Error(fmt.Sprintf("Failed to get configured named states: %s", wDiags.Err()))
|
|
return 1
|
|
}
|
|
c.showDiagnostics(diags) // output warnings, if any
|
|
|
|
for _, ws := range workspaces {
|
|
if workspace == ws {
|
|
c.Ui.Error(fmt.Sprintf(envExists, workspace))
|
|
return 1
|
|
}
|
|
}
|
|
|
|
// Create the new workspace
|
|
//
|
|
// In local, remote and remote-state backends obtaining a state manager
|
|
// creates an empty state file for the new workspace as a side-effect.
|
|
//
|
|
// The cloud backend also has logic in StateMgr for creating projects and
|
|
// workspaces if they don't already exist.
|
|
sMgr, sDiags := b.StateMgr(workspace)
|
|
if sDiags.HasErrors() {
|
|
c.Ui.Error(sDiags.Err().Error())
|
|
return 1
|
|
}
|
|
|
|
if l, ok := b.(*local.Local); ok {
|
|
if _, ok := l.Backend.(*backendPluggable.Pluggable); ok {
|
|
// Obtaining the state manager would have not created the state file as a side effect
|
|
// if a pluggable state store is in use.
|
|
//
|
|
// Instead, explicitly create the new workspace by saving an empty state file.
|
|
// We only do this when the backend in use is pluggable, to avoid impacting users
|
|
// of remote-state backends.
|
|
if err := sMgr.WriteState(states.NewState()); err != nil {
|
|
c.Ui.Error(err.Error())
|
|
return 1
|
|
}
|
|
if err := sMgr.PersistState(nil); err != nil {
|
|
c.Ui.Error(err.Error())
|
|
return 1
|
|
}
|
|
}
|
|
}
|
|
|
|
// now set the current workspace locally
|
|
if err := c.SetWorkspace(workspace); err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Error selecting new workspace: %s", err))
|
|
return 1
|
|
}
|
|
|
|
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
|
|
strings.TrimSpace(envCreated), workspace)))
|
|
|
|
if statePath == "" {
|
|
// if we're not loading a state, then we're done
|
|
return 0
|
|
}
|
|
|
|
// load the new Backend state
|
|
stateMgr, sDiags := b.StateMgr(workspace)
|
|
if sDiags.HasErrors() {
|
|
c.Ui.Error(sDiags.Err().Error())
|
|
return 1
|
|
}
|
|
|
|
if stateLock {
|
|
stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View))
|
|
if diags := stateLocker.Lock(stateMgr, "workspace-new"); diags.HasErrors() {
|
|
c.showDiagnostics(diags)
|
|
return 1
|
|
}
|
|
defer func() {
|
|
if diags := stateLocker.Unlock(); diags.HasErrors() {
|
|
c.showDiagnostics(diags)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// read the existing state file
|
|
f, err := os.Open(statePath)
|
|
if err != nil {
|
|
c.Ui.Error(err.Error())
|
|
return 1
|
|
}
|
|
|
|
stateFile, err := statefile.Read(f)
|
|
if err != nil {
|
|
c.Ui.Error(err.Error())
|
|
return 1
|
|
}
|
|
|
|
// save the existing state in the new Backend.
|
|
err = stateMgr.WriteState(stateFile.State)
|
|
if err != nil {
|
|
c.Ui.Error(err.Error())
|
|
return 1
|
|
}
|
|
err = stateMgr.PersistState(nil)
|
|
if err != nil {
|
|
c.Ui.Error(err.Error())
|
|
return 1
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
func (c *WorkspaceNewCommand) AutocompleteArgs() complete.Predictor {
|
|
return completePredictSequence{
|
|
complete.PredictAnything,
|
|
complete.PredictDirs(""),
|
|
}
|
|
}
|
|
|
|
func (c *WorkspaceNewCommand) AutocompleteFlags() complete.Flags {
|
|
return complete.Flags{
|
|
"-state": complete.PredictFiles("*.tfstate"),
|
|
}
|
|
}
|
|
|
|
func (c *WorkspaceNewCommand) Help() string {
|
|
helpText := `
|
|
Usage: terraform [global options] workspace new [OPTIONS] NAME
|
|
|
|
Create a new Terraform workspace.
|
|
|
|
Options:
|
|
|
|
-lock=false Don't hold a state lock during the operation. This is
|
|
dangerous if others might concurrently run commands
|
|
against the same workspace.
|
|
|
|
-lock-timeout=0s Duration to retry a state lock.
|
|
|
|
-state=path Copy an existing state file into the new workspace.
|
|
|
|
`
|
|
return strings.TrimSpace(helpText)
|
|
}
|
|
|
|
func (c *WorkspaceNewCommand) Synopsis() string {
|
|
return "Create a new workspace"
|
|
}
|