// Copyright IBM Corp. 2014, 2026 // 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/command/arguments" "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( arguments.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( arguments.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) } }) } }