terraform/internal/command/workspace_command_test.go
Sarah French 8e59c3296a
Some checks failed
build / Determine intended Terraform version (push) Has been cancelled
build / Determine Go toolchain version (push) Has been cancelled
Quick Checks / Unit Tests (push) Has been cancelled
Quick Checks / Race Tests (push) Has been cancelled
Quick Checks / End-to-end Tests (push) Has been cancelled
Quick Checks / Code Consistency Checks (push) Has been cancelled
build / Generate release metadata (push) Has been cancelled
build / Build for freebsd_386 (push) Has been cancelled
build / Build for linux_386 (push) Has been cancelled
build / Build for openbsd_386 (push) Has been cancelled
build / Build for windows_386 (push) Has been cancelled
build / Build for darwin_amd64 (push) Has been cancelled
build / Build for freebsd_amd64 (push) Has been cancelled
build / Build for linux_amd64 (push) Has been cancelled
build / Build for openbsd_amd64 (push) Has been cancelled
build / Build for solaris_amd64 (push) Has been cancelled
build / Build for windows_amd64 (push) Has been cancelled
build / Build for freebsd_arm (push) Has been cancelled
build / Build for linux_arm (push) Has been cancelled
build / Build for darwin_arm64 (push) Has been cancelled
build / Build for linux_arm64 (push) Has been cancelled
build / Build for windows_arm64 (push) Has been cancelled
build / Build Docker image for linux_386 (push) Has been cancelled
build / Build Docker image for linux_amd64 (push) Has been cancelled
build / Build Docker image for linux_arm (push) Has been cancelled
build / Build Docker image for linux_arm64 (push) Has been cancelled
build / Build e2etest for linux_386 (push) Has been cancelled
build / Build e2etest for windows_386 (push) Has been cancelled
build / Build e2etest for darwin_amd64 (push) Has been cancelled
build / Build e2etest for linux_amd64 (push) Has been cancelled
build / Build e2etest for windows_amd64 (push) Has been cancelled
build / Build e2etest for linux_arm (push) Has been cancelled
build / Build e2etest for darwin_arm64 (push) Has been cancelled
build / Build e2etest for linux_arm64 (push) Has been cancelled
build / Run e2e test for linux_386 (push) Has been cancelled
build / Run e2e test for windows_386 (push) Has been cancelled
build / Run e2e test for darwin_amd64 (push) Has been cancelled
build / Run e2e test for linux_amd64 (push) Has been cancelled
build / Run e2e test for windows_amd64 (push) Has been cancelled
build / Run e2e test for linux_arm (push) Has been cancelled
build / Run e2e test for linux_arm64 (push) Has been cancelled
build / Run terraform-exec test for linux amd64 (push) Has been cancelled
PSS: Address edge case in workspace list when no state files/workspace artefacts exist (#38094)
* test: Add test demonstrating how the `workspace list` command behaves when no workspaces are reported from a pluggable state store.

This scenario isn't possible when using backends. See the code comment for the test for more information.

* feat: Make Terraform warn when no workspaces exist, including guidance to users for how to create the workspace.

This change is not a breaking change because all backends report that the "default" workspace always exists. This new warning can only be viewed in specific circumstances by users of pluggable state storage.
2026-01-28 15:15:04 +00:00

1000 lines
28 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/hashicorp/cli"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/backend/local"
"github.com/hashicorp/terraform/internal/backend/remote-state/inmem"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/states/statefile"
"github.com/hashicorp/terraform/internal/states/statemgr"
)
func TestWorkspace_allCommands_pluggableStateStore(t *testing.T) {
// Create a temporary working directory with pluggable state storage in the config
td := t.TempDir()
testCopyDir(t, testFixturePath("state-store-new"), td)
t.Chdir(td)
mock := testStateStoreMockWithChunkNegotiation(t, 1000)
// Assumes the mocked provider is hashicorp/test
providerSource, close := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.2.3"},
})
defer close()
ui := new(cli.MockUi)
view, _ := testView(t)
meta := Meta{
AllowExperimentalFeatures: true,
Ui: ui,
View: view,
testingOverrides: &testingOverrides{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): providers.FactoryFixed(mock),
},
},
ProviderSource: providerSource,
}
//// Init
intCmd := &InitCommand{
Meta: meta,
}
args := []string{"-enable-pluggable-state-storage-experiment"} // Needed to test init changes for PSS project
code := intCmd.Run(args)
if code != 0 {
t.Fatalf("bad: %d\n\n%s\n%s", code, ui.ErrorWriter, ui.OutputWriter)
}
// We expect a state to have been created for the default workspace
if _, ok := mock.MockStates["default"]; !ok {
t.Fatal("expected the default workspace to exist, but it didn't")
}
//// Create Workspace
newWorkspace := "foobar"
ui = new(cli.MockUi)
meta.Ui = ui
newCmd := &WorkspaceNewCommand{
Meta: meta,
}
current, _ := newCmd.Workspace()
if current != backend.DefaultStateName {
t.Fatal("before creating any custom workspaces, the current workspace should be 'default'")
}
args = []string{newWorkspace}
code = newCmd.Run(args)
if code != 0 {
t.Fatalf("bad: %d\n\n%s\n%s", code, ui.ErrorWriter, ui.OutputWriter)
}
expectedMsg := fmt.Sprintf("Created and switched to workspace %q!", newWorkspace)
if !strings.Contains(ui.OutputWriter.String(), expectedMsg) {
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, ui.OutputWriter)
}
// We expect a state to have been created for the new custom workspace
if _, ok := mock.MockStates[newWorkspace]; !ok {
t.Fatalf("expected the %s workspace to exist, but it didn't", newWorkspace)
}
current, _ = newCmd.Workspace()
if current != newWorkspace {
t.Fatalf("current workspace should be %q, got %q", newWorkspace, current)
}
//// List Workspaces
ui = new(cli.MockUi)
meta.Ui = ui
listCmd := &WorkspaceListCommand{
Meta: meta,
}
args = []string{}
code = listCmd.Run(args)
if code != 0 {
t.Fatalf("bad: %d\n\n%s\n%s", code, ui.ErrorWriter, ui.OutputWriter)
}
if !strings.Contains(ui.OutputWriter.String(), newWorkspace) {
t.Errorf("unexpected output, expected the new %q workspace to be listed present, but it's missing. Got:\n%s", newWorkspace, ui.OutputWriter)
}
//// Select Workspace
ui = new(cli.MockUi)
meta.Ui = ui
selCmd := &WorkspaceSelectCommand{
Meta: meta,
}
selectedWorkspace := backend.DefaultStateName
args = []string{selectedWorkspace}
code = selCmd.Run(args)
if code != 0 {
t.Fatalf("bad: %d\n\n%s\n%s", code, ui.ErrorWriter, ui.OutputWriter)
}
expectedMsg = fmt.Sprintf("Switched to workspace %q.", selectedWorkspace)
if !strings.Contains(ui.OutputWriter.String(), expectedMsg) {
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, ui.OutputWriter)
}
//// Show Workspace
ui = new(cli.MockUi)
meta.Ui = ui
showCmd := &WorkspaceShowCommand{
Meta: meta,
}
args = []string{}
code = showCmd.Run(args)
if code != 0 {
t.Fatalf("bad: %d\n\n%s\n%s", code, ui.ErrorWriter, ui.OutputWriter)
}
expectedMsg = fmt.Sprintf("%s\n", selectedWorkspace)
if !strings.Contains(ui.OutputWriter.String(), expectedMsg) {
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, ui.OutputWriter)
}
current, _ = newCmd.Workspace()
if current != backend.DefaultStateName {
t.Fatal("current workspace should be 'default'")
}
//// Delete Workspace
ui = new(cli.MockUi)
meta.Ui = ui
deleteCmd := &WorkspaceDeleteCommand{
Meta: meta,
}
args = []string{newWorkspace}
code = deleteCmd.Run(args)
if code != 0 {
t.Fatalf("bad: %d\n\n%s\n%s", code, ui.ErrorWriter, ui.OutputWriter)
}
expectedMsg = fmt.Sprintf("Deleted workspace %q!\n", newWorkspace)
if !strings.Contains(ui.OutputWriter.String(), expectedMsg) {
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, ui.OutputWriter)
}
}
// Test how the workspace list command behaves when zero workspaces are present.
//
// Historically, the backends built into the Terraform binary would always report that the default workspace exists,
// even when there were no artefacts representing that workspace. All backends were implemented to do this, therefore
// it was impossible for the `workspace list` command to report that no workspaces existed.
//
// After the introduction of pluggable state storage we can't rely on all implementations to include that behaviour.
// Instead, we only report workspaces as existing based on the existence of state files/artefacts. Similarly, we've
// changed how new workspace artefacts are created. Previously the "default" workspace's state file was only created
// after the first apply, and custom workspaces' state files were created as a side-effect of obtaining a state manager
// during `workspace new`. Now the `workspace new` command explicitly writes an empty state file as part of creating a
// new workspace. The "default" workspace is a special case, and now an empty state file is created during init when
// that workspace is selected. These changes together allow Terraform to only report a workspace's existence based on
// the existence of artefacts.
//
// Users will only experience `workspace list` returning no workspaces if they either:
// 1. Have "default" selected and run `workspace list` before running `init`
// the necessary `workspace new` command to make that workspace.
// 2. Have a custom workspace selected that isn't created yet. This could happen if a user sets `TF_WORKSPACE`
// (or manually edits .terraform/environment) before they run `workspace new`.
func TestWorkspace_list_noReturnedWorkspaces(t *testing.T) {
// Create a temporary working directory with pluggable state storage in the config
td := t.TempDir()
testCopyDir(t, testFixturePath("state-store-unchanged"), td)
t.Chdir(td)
mock := testStateStoreMockWithChunkNegotiation(t, 1000)
// Assumes the mocked provider is hashicorp/test
providerSource, close := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.2.3"},
})
defer close()
ui := new(cli.MockUi)
view, _ := testView(t)
meta := Meta{
AllowExperimentalFeatures: true,
Ui: ui,
View: view,
testingOverrides: &testingOverrides{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): providers.FactoryFixed(mock),
},
},
ProviderSource: providerSource,
}
// What happens if no workspaces are returned from a pluggable state storage implementation?
// (and there are no error diagnostics)
mock.GetStatesResponse = &providers.GetStatesResponse{
States: []string{},
Diagnostics: nil,
}
listCmd := &WorkspaceListCommand{
Meta: meta,
}
args := []string{}
if code := listCmd.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
// Users see a warning that the selected workspace doesn't exist yet
expectedWarningMessages := []string{
"Warning: Terraform cannot find any existing workspaces.",
"The \"default\" workspace is selected in your working directory.",
"init",
}
for _, msg := range expectedWarningMessages {
if !strings.Contains(ui.ErrorWriter.String(), msg) {
t.Fatalf("expected stderr output to include: %s\ngot: %s",
msg,
ui.ErrorWriter,
)
}
}
// No other output is present
if ui.OutputWriter.String() != "" {
t.Fatalf("unexpected stdout: %s",
ui.OutputWriter,
)
}
}
func TestWorkspace_createAndChange(t *testing.T) {
// Create a temporary working directory that is empty
td := t.TempDir()
os.MkdirAll(td, 0755)
t.Chdir(td)
newCmd := &WorkspaceNewCommand{}
current, _ := newCmd.Workspace()
if current != backend.DefaultStateName {
t.Fatal("current workspace should be 'default'")
}
args := []string{"test"}
ui := new(cli.MockUi)
view, _ := testView(t)
newCmd.Meta = Meta{Ui: ui, View: view}
if code := newCmd.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
current, _ = newCmd.Workspace()
if current != "test" {
t.Fatalf("current workspace should be 'test', got %q", current)
}
selCmd := &WorkspaceSelectCommand{}
args = []string{backend.DefaultStateName}
ui = new(cli.MockUi)
selCmd.Meta = Meta{Ui: ui, View: view}
if code := selCmd.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
current, _ = newCmd.Workspace()
if current != backend.DefaultStateName {
t.Fatal("current workspace should be 'default'")
}
}
func TestWorkspace_cannotCreateOrSelectEmptyStringWorkspace(t *testing.T) {
// Create a temporary working directory that is empty
td := t.TempDir()
os.MkdirAll(td, 0755)
t.Chdir(td)
newCmd := &WorkspaceNewCommand{}
current, _ := newCmd.Workspace()
if current != backend.DefaultStateName {
t.Fatal("current workspace should be 'default'")
}
args := []string{""}
ui := cli.NewMockUi()
view, _ := testView(t)
newCmd.Meta = Meta{Ui: ui, View: view}
if code := newCmd.Run(args); code != 1 {
t.Fatalf("expected failure when trying to create the \"\" workspace.\noutput: %s", ui.OutputWriter)
}
gotStderr := ui.ErrorWriter.String()
if want, got := `The workspace name "" is not allowed`, gotStderr; !strings.Contains(got, want) {
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, got)
}
ui = cli.NewMockUi()
selectCmd := &WorkspaceSelectCommand{
Meta: Meta{
Ui: ui,
View: view,
},
}
if code := selectCmd.Run(args); code != 1 {
t.Fatalf("expected failure when trying to select the the \"\" workspace.\noutput: %s", ui.OutputWriter)
}
gotStderr = ui.ErrorWriter.String()
if want, got := `The workspace name "" is not allowed`, gotStderr; !strings.Contains(got, want) {
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, got)
}
}
// Create some workspaces and test the list output.
// This also ensures we switch to the correct env after each call
func TestWorkspace_createAndList(t *testing.T) {
// Create a temporary working directory that is empty
td := t.TempDir()
os.MkdirAll(td, 0755)
t.Chdir(td)
// make sure a vars file doesn't interfere
err := os.WriteFile(
DefaultVarsFilename,
[]byte(`foo = "bar"`),
0644,
)
if err != nil {
t.Fatal(err)
}
envs := []string{"test_a", "test_b", "test_c"}
// create multiple workspaces
for _, env := range envs {
ui := new(cli.MockUi)
view, _ := testView(t)
newCmd := &WorkspaceNewCommand{
Meta: Meta{Ui: ui, View: view},
}
if code := newCmd.Run([]string{env}); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
}
listCmd := &WorkspaceListCommand{}
ui := new(cli.MockUi)
view, _ := testView(t)
listCmd.Meta = Meta{Ui: ui, View: view}
if code := listCmd.Run(nil); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
actual := strings.TrimSpace(ui.OutputWriter.String())
expected := "default\n test_a\n test_b\n* test_c"
if actual != expected {
t.Fatalf("\nexpected: %q\nactual: %q", expected, actual)
}
}
// Create some workspaces and test the show output.
func TestWorkspace_createAndShow(t *testing.T) {
// Create a temporary working directory that is empty
td := t.TempDir()
os.MkdirAll(td, 0755)
t.Chdir(td)
// make sure a vars file doesn't interfere
err := os.WriteFile(
DefaultVarsFilename,
[]byte(`foo = "bar"`),
0644,
)
if err != nil {
t.Fatal(err)
}
// make sure current workspace show outputs "default"
showCmd := &WorkspaceShowCommand{}
ui := new(cli.MockUi)
view, _ := testView(t)
showCmd.Meta = Meta{Ui: ui, View: view}
if code := showCmd.Run(nil); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
actual := strings.TrimSpace(ui.OutputWriter.String())
expected := "default"
if actual != expected {
t.Fatalf("\nexpected: %q\nactual: %q", expected, actual)
}
newCmd := &WorkspaceNewCommand{}
env := []string{"test_a"}
// create test_a workspace
ui = new(cli.MockUi)
newCmd.Meta = Meta{Ui: ui, View: view}
if code := newCmd.Run(env); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
selCmd := &WorkspaceSelectCommand{}
ui = new(cli.MockUi)
selCmd.Meta = Meta{Ui: ui, View: view}
if code := selCmd.Run(env); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
showCmd = &WorkspaceShowCommand{}
ui = new(cli.MockUi)
showCmd.Meta = Meta{Ui: ui, View: view}
if code := showCmd.Run(nil); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
actual = strings.TrimSpace(ui.OutputWriter.String())
expected = "test_a"
if actual != expected {
t.Fatalf("\nexpected: %q\nactual: %q", expected, actual)
}
}
// Don't allow names that aren't URL safe
func TestWorkspace_createInvalid(t *testing.T) {
// Create a temporary working directory that is empty
td := t.TempDir()
os.MkdirAll(td, 0755)
t.Chdir(td)
envs := []string{"test_a*", "test_b/foo", "../../../test_c", "好_d"}
// create multiple workspaces
for _, env := range envs {
ui := new(cli.MockUi)
view, _ := testView(t)
newCmd := &WorkspaceNewCommand{
Meta: Meta{Ui: ui, View: view},
}
if code := newCmd.Run([]string{env}); code == 0 {
t.Fatalf("expected failure: \n%s", ui.OutputWriter)
}
}
// list workspaces to make sure none were created
listCmd := &WorkspaceListCommand{}
ui := new(cli.MockUi)
view, _ := testView(t)
listCmd.Meta = Meta{Ui: ui, View: view}
if code := listCmd.Run(nil); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
actual := strings.TrimSpace(ui.OutputWriter.String())
expected := "* default"
if actual != expected {
t.Fatalf("\nexpected: %q\nactual: %q", expected, actual)
}
}
func TestWorkspace_createWithState(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("inmem-backend"), td)
t.Chdir(td)
defer inmem.Reset()
// init the backend
ui := new(cli.MockUi)
view, _ := testView(t)
initCmd := &InitCommand{
Meta: Meta{Ui: ui, View: view},
}
if code := initCmd.Run([]string{}); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
originalState := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"id":"bar"}`),
Status: states.ObjectReady,
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
})
err := statemgr.NewFilesystem("test.tfstate").WriteState(originalState)
if err != nil {
t.Fatal(err)
}
workspace := "test_workspace"
args := []string{"-state", "test.tfstate", workspace}
ui = new(cli.MockUi)
newCmd := &WorkspaceNewCommand{
Meta: Meta{Ui: ui, View: view},
}
if code := newCmd.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
newPath := filepath.Join(local.DefaultWorkspaceDir, "test", DefaultStateFilename)
envState := statemgr.NewFilesystem(newPath)
err = envState.RefreshState()
if err != nil {
t.Fatal(err)
}
b := backend.TestBackendConfig(t, inmem.New(), nil)
sMgr, sDiags := b.StateMgr(workspace)
if sDiags.HasErrors() {
t.Fatal(sDiags)
}
newState := sMgr.State()
if got, want := newState.String(), originalState.String(); got != want {
t.Fatalf("states not equal\ngot: %s\nwant: %s", got, want)
}
}
func TestWorkspace_delete(t *testing.T) {
td := t.TempDir()
os.MkdirAll(td, 0755)
t.Chdir(td)
// create the workspace directories
if err := os.MkdirAll(filepath.Join(local.DefaultWorkspaceDir, "test"), 0755); err != nil {
t.Fatal(err)
}
// create the workspace file
if err := os.MkdirAll(DefaultDataDir, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(DefaultDataDir, local.DefaultWorkspaceFile), []byte("test"), 0644); err != nil {
t.Fatal(err)
}
ui := new(cli.MockUi)
view, _ := testView(t)
delCmd := &WorkspaceDeleteCommand{
Meta: Meta{Ui: ui, View: view},
}
current, _ := delCmd.Workspace()
if current != "test" {
t.Fatal("wrong workspace:", current)
}
// we can't delete our current workspace
args := []string{"test"}
if code := delCmd.Run(args); code == 0 {
t.Fatal("expected error deleting current workspace")
}
// change back to default
if err := delCmd.SetWorkspace(backend.DefaultStateName); err != nil {
t.Fatal(err)
}
// try the delete again
ui = new(cli.MockUi)
delCmd.Meta.Ui = ui
if code := delCmd.Run(args); code != 0 {
t.Fatalf("error deleting workspace: %s", ui.ErrorWriter)
}
current, _ = delCmd.Workspace()
if current != backend.DefaultStateName {
t.Fatalf("wrong workspace: %q", current)
}
}
// TestWorkspace_deleteInvalid shows that if a workspace with an invalid name
// has been created, Terraform allows users to delete it.
func TestWorkspace_deleteInvalid(t *testing.T) {
td := t.TempDir()
os.MkdirAll(td, 0755)
t.Chdir(td)
// choose an invalid workspace name
workspace := "test workspace"
path := filepath.Join(local.DefaultWorkspaceDir, workspace)
// create the workspace directories
if err := os.MkdirAll(path, 0755); err != nil {
t.Fatal(err)
}
ui := new(cli.MockUi)
view, _ := testView(t)
delCmd := &WorkspaceDeleteCommand{
Meta: Meta{Ui: ui, View: view},
}
// delete the workspace
if code := delCmd.Run([]string{workspace}); code != 0 {
t.Fatalf("error deleting workspace: %s", ui.ErrorWriter)
}
if _, err := os.Stat(path); err == nil {
t.Fatalf("should have deleted workspace, but %s still exists", path)
} else if !os.IsNotExist(err) {
t.Fatalf("unexpected error for workspace path: %s", err)
}
}
func TestWorkspace_deleteRejectsEmptyString(t *testing.T) {
td := t.TempDir()
os.MkdirAll(td, 0755)
t.Chdir(td)
// Empty string identifier for workspace
workspace := ""
path := filepath.Join(local.DefaultWorkspaceDir, workspace)
// create the workspace directories
if err := os.MkdirAll(path, 0755); err != nil {
t.Fatal(err)
}
ui := cli.NewMockUi()
view, _ := testView(t)
delCmd := &WorkspaceDeleteCommand{
Meta: Meta{Ui: ui, View: view},
}
// delete the workspace
if code := delCmd.Run([]string{workspace}); code != cli.RunResultHelp {
t.Fatalf("expected code %d but got %d. Output: %s", cli.RunResultHelp, code, ui.OutputWriter)
}
if !strings.Contains(string(ui.ErrorWriter.Bytes()), "got an empty string") {
t.Fatalf("expected error to include \"got an empty string\" but was missing, got: %s", ui.ErrorWriter)
}
}
func TestWorkspace_deleteWithState(t *testing.T) {
td := t.TempDir()
os.MkdirAll(td, 0755)
t.Chdir(td)
// create the workspace directories
if err := os.MkdirAll(filepath.Join(local.DefaultWorkspaceDir, "test"), 0755); err != nil {
t.Fatal(err)
}
// create a non-empty state
originalState := states.BuildState(func(ss *states.SyncState) {
ss.SetResourceInstanceCurrent(
addrs.AbsResourceInstance{
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
},
},
},
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte("{}"),
Status: states.ObjectReady,
},
addrs.AbsProviderConfig{
Provider: addrs.NewBuiltInProvider("test"),
},
)
})
originalStateFile := &statefile.File{
Serial: 1,
Lineage: "whatever",
State: originalState,
}
f, err := os.Create(filepath.Join(local.DefaultWorkspaceDir, "test", "terraform.tfstate"))
if err != nil {
t.Fatal(err)
}
defer f.Close()
if err := statefile.Write(originalStateFile, f); err != nil {
t.Fatal(err)
}
ui := cli.NewMockUi()
view, _ := testView(t)
delCmd := &WorkspaceDeleteCommand{
Meta: Meta{Ui: ui, View: view},
}
args := []string{"test"}
if code := delCmd.Run(args); code == 0 {
t.Fatalf("expected failure without -force.\noutput: %s", ui.OutputWriter)
}
gotStderr := ui.ErrorWriter.String()
if want, got := `Workspace "test" is currently tracking the following resource instances`, gotStderr; !strings.Contains(got, want) {
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, got)
}
if want, got := `- test_instance.foo`, gotStderr; !strings.Contains(got, want) {
t.Errorf("error message doesn't mention the remaining instance\nwant substring: %s\ngot:\n%s", want, got)
}
ui = new(cli.MockUi)
delCmd.Meta.Ui = ui
args = []string{"-force", "test"}
if code := delCmd.Run(args); code != 0 {
t.Fatalf("failure: %s", ui.ErrorWriter)
}
if _, err := os.Stat(filepath.Join(local.DefaultWorkspaceDir, "test")); !os.IsNotExist(err) {
t.Fatal("env 'test' still exists!")
}
}
func TestWorkspace_cannotDeleteDefaultWorkspace(t *testing.T) {
td := t.TempDir()
os.MkdirAll(td, 0755)
t.Chdir(td)
// Create an empty default state, i.e. create default workspace.
originalStateFile := &statefile.File{
Serial: 1,
Lineage: "whatever",
State: states.NewState(),
}
f, err := os.Create(filepath.Join(local.DefaultStateFilename))
if err != nil {
t.Fatal(err)
}
defer f.Close()
if err := statefile.Write(originalStateFile, f); err != nil {
t.Fatal(err)
}
// Create a non-default workspace
if err := os.MkdirAll(filepath.Join(local.DefaultWorkspaceDir, "test"), 0755); err != nil {
t.Fatal(err)
}
// Select the non-default "test" workspace
selectCmd := &WorkspaceSelectCommand{}
args := []string{"test"}
ui := cli.NewMockUi()
view, _ := testView(t)
selectCmd.Meta = Meta{Ui: ui, View: view}
if code := selectCmd.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
// Assert there is a default and "test" workspace, and "test" is selected
listCmd := &WorkspaceListCommand{}
ui = cli.NewMockUi()
listCmd.Meta = Meta{Ui: ui, View: view}
if code := listCmd.Run(nil); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
actual := strings.TrimSpace(ui.OutputWriter.String())
expected := "default\n* test"
if actual != expected {
t.Fatalf("\nexpected: %q\nactual: %q", expected, actual)
}
// Attempt to delete the default workspace (not forced)
ui = cli.NewMockUi()
delCmd := &WorkspaceDeleteCommand{
Meta: Meta{Ui: ui, View: view},
}
args = []string{"default"}
if code := delCmd.Run(args); code != 1 {
t.Fatalf("expected failure when trying to delete the default workspace.\noutput: %s", ui.OutputWriter)
}
// User should be prevented from deleting the default workspace despite:
// * the state being empty
// * default not being the selected workspace
gotStderr := ui.ErrorWriter.String()
if want, got := `Cannot delete the default workspace`, gotStderr; !strings.Contains(got, want) {
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, got)
}
// Attempt to force delete the default workspace
ui = cli.NewMockUi()
delCmd = &WorkspaceDeleteCommand{
Meta: Meta{Ui: ui, View: view},
}
args = []string{"-force", "default"}
if code := delCmd.Run(args); code != 1 {
t.Fatalf("expected failure when trying to delete the default workspace.\noutput: %s", ui.OutputWriter)
}
// Outcome should be the same even when forcing
gotStderr = ui.ErrorWriter.String()
if want, got := `Cannot delete the default workspace`, gotStderr; !strings.Contains(got, want) {
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, got)
}
}
func TestWorkspace_selectWithOrCreate(t *testing.T) {
// Create a temporary working directory that is empty
td := t.TempDir()
os.MkdirAll(td, 0755)
t.Chdir(td)
selectCmd := &WorkspaceSelectCommand{}
current, _ := selectCmd.Workspace()
if current != backend.DefaultStateName {
t.Fatal("current workspace should be 'default'")
}
args := []string{"-or-create", "test"}
ui := new(cli.MockUi)
view, _ := testView(t)
selectCmd.Meta = Meta{Ui: ui, View: view}
if code := selectCmd.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
current, _ = selectCmd.Workspace()
if current != "test" {
t.Fatalf("current workspace should be 'test', got %q", current)
}
}
// Test that the old `env` subcommands raise a deprecation warning
//
// Test covers:
// - `terraform env new`
// - `terraform env select`
// - `terraform env list`
// - `terraform env delete`
//
// Note: there is no `env` equivalent of `terraform workspace show`.
func TestWorkspace_envCommandDeprecationWarnings(t *testing.T) {
// We're asserting the warning below is returned whenever a legacy `env` command
// is executed. Commands are made to be legacy via LegacyName: true
expectedWarning := `Warning: the "terraform env" family of commands is deprecated`
// Create a temporary working directory to make workspaces in
td := t.TempDir()
os.MkdirAll(td, 0755)
t.Chdir(td)
newCmd := &WorkspaceNewCommand{}
current, _ := newCmd.Workspace()
if current != backend.DefaultStateName {
t.Fatal("current workspace should be 'default'")
}
// Assert `terraform env new "foobar"` returns expected deprecation warning
ui := new(cli.MockUi)
view, _ := testView(t)
newCmd = &WorkspaceNewCommand{
Meta: Meta{Ui: ui, View: view},
LegacyName: true,
}
newWorkspace := "foobar"
args := []string{newWorkspace}
if code := newCmd.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
if !strings.Contains(ui.ErrorWriter.String(), expectedWarning) {
t.Fatalf("expected the command to return a warning, but it was missing.\nwanted: %s\ngot: %s",
expectedWarning,
ui.ErrorWriter.String(),
)
}
// Assert `terraform env select "default"` returns expected deprecation warning
ui = new(cli.MockUi)
view, _ = testView(t)
selectCmd := &WorkspaceSelectCommand{
Meta: Meta{Ui: ui, View: view},
LegacyName: true,
}
defaultWorkspace := "default"
args = []string{defaultWorkspace}
if code := selectCmd.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
if !strings.Contains(ui.ErrorWriter.String(), expectedWarning) {
t.Fatalf("expected the command to return a warning, but it was missing.\nwanted: %s\ngot: %s",
expectedWarning,
ui.ErrorWriter.String(),
)
}
// Assert `terraform env list` returns expected deprecation warning
ui = new(cli.MockUi)
view, _ = testView(t)
listCmd := &WorkspaceListCommand{
Meta: Meta{Ui: ui, View: view},
LegacyName: true,
}
args = []string{}
if code := listCmd.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
if !strings.Contains(ui.ErrorWriter.String(), expectedWarning) {
t.Fatalf("expected the command to return a warning, but it was missing.\nwanted: %s\ngot: %s",
expectedWarning,
ui.ErrorWriter.String(),
)
}
// Assert `terraform env delete` returns expected deprecation warning
ui = new(cli.MockUi)
view, _ = testView(t)
deleteCmd := &WorkspaceDeleteCommand{
Meta: Meta{Ui: ui, View: view},
LegacyName: true,
}
args = []string{newWorkspace}
if code := deleteCmd.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
if !strings.Contains(ui.ErrorWriter.String(), expectedWarning) {
t.Fatalf("expected the command to return a warning, but it was missing.\nwanted: %s\ngot: %s",
expectedWarning,
ui.ErrorWriter.String(),
)
}
}
func TestValidWorkspaceName(t *testing.T) {
cases := map[string]struct {
input string
valid bool
}{
"foobar": {
input: "foobar",
valid: true,
},
"valid symbols": {
input: "-._~@:",
valid: true,
},
"includes space": {
input: "two words",
valid: false,
},
"empty string": {
input: "",
valid: false,
},
}
for tn, tc := range cases {
t.Run(tn, func(t *testing.T) {
valid := validWorkspaceName(tc.input)
if valid != tc.valid {
t.Fatalf("unexpected output when processing input %q. Wanted %v got %v", tc.input, tc.valid, valid)
}
})
}
}