2023-05-02 11:33:06 -04:00
|
|
|
// Copyright IBM Corp. 2014, 2026
|
2023-08-10 18:43:27 -04:00
|
|
|
// SPDX-License-Identifier: BUSL-1.1
|
2023-05-02 11:33:06 -04:00
|
|
|
|
2017-02-16 18:29:19 -05:00
|
|
|
package command
|
|
|
|
|
|
|
|
|
|
import (
|
2025-11-19 07:50:43 -05:00
|
|
|
"fmt"
|
2017-02-16 18:29:19 -05:00
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
|
2023-12-20 06:04:10 -05:00
|
|
|
"github.com/hashicorp/cli"
|
2024-03-07 20:46:07 -05:00
|
|
|
|
2021-05-17 15:00:50 -04:00
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
2021-05-17 11:42:17 -04:00
|
|
|
"github.com/hashicorp/terraform/internal/backend"
|
|
|
|
|
"github.com/hashicorp/terraform/internal/backend/local"
|
|
|
|
|
"github.com/hashicorp/terraform/internal/backend/remote-state/inmem"
|
2026-02-18 04:27:17 -05:00
|
|
|
"github.com/hashicorp/terraform/internal/command/arguments"
|
2025-11-19 07:50:43 -05:00
|
|
|
"github.com/hashicorp/terraform/internal/providers"
|
2021-05-17 15:43:35 -04:00
|
|
|
"github.com/hashicorp/terraform/internal/states"
|
2024-03-07 20:46:07 -05:00
|
|
|
"github.com/hashicorp/terraform/internal/states/statefile"
|
2021-05-17 15:43:35 -04:00
|
|
|
"github.com/hashicorp/terraform/internal/states/statemgr"
|
2017-02-16 18:29:19 -05:00
|
|
|
)
|
|
|
|
|
|
2025-11-19 07:50:43 -05:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 10:15:04 -05:00
|
|
|
// 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,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-30 18:06:13 -04:00
|
|
|
func TestWorkspace_createAndChange(t *testing.T) {
|
2017-02-16 18:29:19 -05:00
|
|
|
// Create a temporary working directory that is empty
|
2022-04-08 12:34:16 -04:00
|
|
|
td := t.TempDir()
|
2017-02-16 18:29:19 -05:00
|
|
|
os.MkdirAll(td, 0755)
|
2025-07-16 11:04:10 -04:00
|
|
|
t.Chdir(td)
|
2017-02-16 18:29:19 -05:00
|
|
|
|
2017-05-30 18:06:13 -04:00
|
|
|
newCmd := &WorkspaceNewCommand{}
|
2017-02-16 18:29:19 -05:00
|
|
|
|
2020-06-16 12:23:15 -04:00
|
|
|
current, _ := newCmd.Workspace()
|
2017-02-16 18:29:19 -05:00
|
|
|
if current != backend.DefaultStateName {
|
2017-05-30 18:06:13 -04:00
|
|
|
t.Fatal("current workspace should be 'default'")
|
2017-02-16 18:29:19 -05:00
|
|
|
}
|
|
|
|
|
|
2017-02-23 13:13:28 -05:00
|
|
|
args := []string{"test"}
|
2017-02-16 18:29:19 -05:00
|
|
|
ui := new(cli.MockUi)
|
2021-02-16 07:19:22 -05:00
|
|
|
view, _ := testView(t)
|
|
|
|
|
newCmd.Meta = Meta{Ui: ui, View: view}
|
2017-02-23 13:13:28 -05:00
|
|
|
if code := newCmd.Run(args); code != 0 {
|
2017-02-16 18:29:19 -05:00
|
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-16 12:23:15 -04:00
|
|
|
current, _ = newCmd.Workspace()
|
2017-02-16 18:29:19 -05:00
|
|
|
if current != "test" {
|
2017-05-30 18:06:13 -04:00
|
|
|
t.Fatalf("current workspace should be 'test', got %q", current)
|
2017-02-16 18:29:19 -05:00
|
|
|
}
|
|
|
|
|
|
2017-05-30 18:06:13 -04:00
|
|
|
selCmd := &WorkspaceSelectCommand{}
|
2017-02-16 18:29:19 -05:00
|
|
|
args = []string{backend.DefaultStateName}
|
|
|
|
|
ui = new(cli.MockUi)
|
2021-02-16 07:19:22 -05:00
|
|
|
selCmd.Meta = Meta{Ui: ui, View: view}
|
2017-02-23 13:13:28 -05:00
|
|
|
if code := selCmd.Run(args); code != 0 {
|
2017-02-16 18:29:19 -05:00
|
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-16 12:23:15 -04:00
|
|
|
current, _ = newCmd.Workspace()
|
2017-02-16 18:29:19 -05:00
|
|
|
if current != backend.DefaultStateName {
|
2017-05-30 18:06:13 -04:00
|
|
|
t.Fatal("current workspace should be 'default'")
|
2017-02-16 18:29:19 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-26 07:26:58 -04:00
|
|
|
func TestWorkspace_cannotCreateOrSelectEmptyStringWorkspace(t *testing.T) {
|
|
|
|
|
// Create a temporary working directory that is empty
|
|
|
|
|
td := t.TempDir()
|
|
|
|
|
os.MkdirAll(td, 0755)
|
2025-07-16 11:04:10 -04:00
|
|
|
t.Chdir(td)
|
2025-06-26 07:26:58 -04:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-30 18:06:13 -04:00
|
|
|
// Create some workspaces and test the list output.
|
2017-02-16 18:29:19 -05:00
|
|
|
// This also ensures we switch to the correct env after each call
|
2017-05-30 18:06:13 -04:00
|
|
|
func TestWorkspace_createAndList(t *testing.T) {
|
2017-02-16 18:29:19 -05:00
|
|
|
// Create a temporary working directory that is empty
|
2022-04-08 12:34:16 -04:00
|
|
|
td := t.TempDir()
|
2017-02-16 18:29:19 -05:00
|
|
|
os.MkdirAll(td, 0755)
|
2025-07-16 11:04:10 -04:00
|
|
|
t.Chdir(td)
|
2017-02-16 18:29:19 -05:00
|
|
|
|
2017-03-03 18:19:56 -05:00
|
|
|
// make sure a vars file doesn't interfere
|
2025-11-19 07:50:43 -05:00
|
|
|
err := os.WriteFile(
|
2026-02-18 04:27:17 -05:00
|
|
|
arguments.DefaultVarsFilename,
|
2017-03-03 18:19:56 -05:00
|
|
|
[]byte(`foo = "bar"`),
|
|
|
|
|
0644,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-16 18:29:19 -05:00
|
|
|
envs := []string{"test_a", "test_b", "test_c"}
|
|
|
|
|
|
2017-05-30 18:06:13 -04:00
|
|
|
// create multiple workspaces
|
2017-02-16 18:29:19 -05:00
|
|
|
for _, env := range envs {
|
|
|
|
|
ui := new(cli.MockUi)
|
2021-02-16 07:19:22 -05:00
|
|
|
view, _ := testView(t)
|
2017-07-06 13:49:26 -04:00
|
|
|
newCmd := &WorkspaceNewCommand{
|
2021-02-16 07:19:22 -05:00
|
|
|
Meta: Meta{Ui: ui, View: view},
|
2017-07-06 13:49:26 -04:00
|
|
|
}
|
2017-02-23 13:13:28 -05:00
|
|
|
if code := newCmd.Run([]string{env}); code != 0 {
|
2017-02-16 18:29:19 -05:00
|
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-30 18:06:13 -04:00
|
|
|
listCmd := &WorkspaceListCommand{}
|
2017-02-16 18:29:19 -05:00
|
|
|
ui := new(cli.MockUi)
|
2021-02-16 07:19:22 -05:00
|
|
|
view, _ := testView(t)
|
|
|
|
|
listCmd.Meta = Meta{Ui: ui, View: view}
|
2017-02-16 18:29:19 -05:00
|
|
|
|
2017-02-23 13:13:28 -05:00
|
|
|
if code := listCmd.Run(nil); code != 0 {
|
2017-02-16 18:29:19 -05:00
|
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
actual := strings.TrimSpace(ui.OutputWriter.String())
|
2017-02-23 13:13:28 -05:00
|
|
|
expected := "default\n test_a\n test_b\n* test_c"
|
|
|
|
|
|
2017-02-16 18:29:19 -05:00
|
|
|
if actual != expected {
|
2017-07-05 17:35:46 -04:00
|
|
|
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
|
2022-04-08 12:34:16 -04:00
|
|
|
td := t.TempDir()
|
2017-07-05 17:35:46 -04:00
|
|
|
os.MkdirAll(td, 0755)
|
2025-07-16 11:04:10 -04:00
|
|
|
t.Chdir(td)
|
2017-07-05 17:35:46 -04:00
|
|
|
|
|
|
|
|
// make sure a vars file doesn't interfere
|
2025-11-19 07:50:43 -05:00
|
|
|
err := os.WriteFile(
|
2026-02-18 04:27:17 -05:00
|
|
|
arguments.DefaultVarsFilename,
|
2017-07-05 17:35:46 -04:00
|
|
|
[]byte(`foo = "bar"`),
|
|
|
|
|
0644,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// make sure current workspace show outputs "default"
|
|
|
|
|
showCmd := &WorkspaceShowCommand{}
|
|
|
|
|
ui := new(cli.MockUi)
|
2021-02-16 07:19:22 -05:00
|
|
|
view, _ := testView(t)
|
|
|
|
|
showCmd.Meta = Meta{Ui: ui, View: view}
|
2017-07-05 17:35:46 -04:00
|
|
|
|
|
|
|
|
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)
|
2021-02-16 07:19:22 -05:00
|
|
|
newCmd.Meta = Meta{Ui: ui, View: view}
|
2017-07-05 17:35:46 -04:00
|
|
|
if code := newCmd.Run(env); code != 0 {
|
|
|
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
selCmd := &WorkspaceSelectCommand{}
|
|
|
|
|
ui = new(cli.MockUi)
|
2021-02-16 07:19:22 -05:00
|
|
|
selCmd.Meta = Meta{Ui: ui, View: view}
|
2017-07-05 17:35:46 -04:00
|
|
|
if code := selCmd.Run(env); code != 0 {
|
|
|
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showCmd = &WorkspaceShowCommand{}
|
|
|
|
|
ui = new(cli.MockUi)
|
2021-02-16 07:19:22 -05:00
|
|
|
showCmd.Meta = Meta{Ui: ui, View: view}
|
2017-07-05 17:35:46 -04:00
|
|
|
|
|
|
|
|
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)
|
2017-02-16 18:29:19 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-27 16:16:09 -04:00
|
|
|
// Don't allow names that aren't URL safe
|
2017-05-30 18:06:13 -04:00
|
|
|
func TestWorkspace_createInvalid(t *testing.T) {
|
2017-03-27 16:16:09 -04:00
|
|
|
// Create a temporary working directory that is empty
|
2022-04-08 12:34:16 -04:00
|
|
|
td := t.TempDir()
|
2017-03-27 16:16:09 -04:00
|
|
|
os.MkdirAll(td, 0755)
|
2025-07-16 11:04:10 -04:00
|
|
|
t.Chdir(td)
|
2017-03-27 16:16:09 -04:00
|
|
|
|
|
|
|
|
envs := []string{"test_a*", "test_b/foo", "../../../test_c", "好_d"}
|
|
|
|
|
|
2017-05-30 18:06:13 -04:00
|
|
|
// create multiple workspaces
|
2017-03-27 16:16:09 -04:00
|
|
|
for _, env := range envs {
|
|
|
|
|
ui := new(cli.MockUi)
|
2021-02-16 07:19:22 -05:00
|
|
|
view, _ := testView(t)
|
2017-07-06 13:49:26 -04:00
|
|
|
newCmd := &WorkspaceNewCommand{
|
2021-02-16 07:19:22 -05:00
|
|
|
Meta: Meta{Ui: ui, View: view},
|
2017-07-06 13:49:26 -04:00
|
|
|
}
|
2017-03-27 16:16:09 -04:00
|
|
|
if code := newCmd.Run([]string{env}); code == 0 {
|
|
|
|
|
t.Fatalf("expected failure: \n%s", ui.OutputWriter)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-30 18:06:13 -04:00
|
|
|
// list workspaces to make sure none were created
|
|
|
|
|
listCmd := &WorkspaceListCommand{}
|
2017-03-27 16:16:09 -04:00
|
|
|
ui := new(cli.MockUi)
|
2021-02-16 07:19:22 -05:00
|
|
|
view, _ := testView(t)
|
|
|
|
|
listCmd.Meta = Meta{Ui: ui, View: view}
|
2017-03-27 16:16:09 -04:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-30 18:06:13 -04:00
|
|
|
func TestWorkspace_createWithState(t *testing.T) {
|
2022-04-08 12:34:16 -04:00
|
|
|
td := t.TempDir()
|
2020-10-07 12:48:25 -04:00
|
|
|
testCopyDir(t, testFixturePath("inmem-backend"), td)
|
2025-07-16 11:04:10 -04:00
|
|
|
t.Chdir(td)
|
2017-08-01 16:41:34 -04:00
|
|
|
defer inmem.Reset()
|
|
|
|
|
|
|
|
|
|
// init the backend
|
|
|
|
|
ui := new(cli.MockUi)
|
2021-02-16 07:19:22 -05:00
|
|
|
view, _ := testView(t)
|
2017-08-01 16:41:34 -04:00
|
|
|
initCmd := &InitCommand{
|
2021-02-16 07:19:22 -05:00
|
|
|
Meta: Meta{Ui: ui, View: view},
|
2017-08-01 16:41:34 -04:00
|
|
|
}
|
|
|
|
|
if code := initCmd.Run([]string{}); code != 0 {
|
|
|
|
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
|
|
|
|
}
|
2017-02-16 18:29:19 -05:00
|
|
|
|
terraform: Ugly huge change to weave in new State and Plan types
Due to how often the state and plan types are referenced throughout
Terraform, there isn't a great way to switch them out gradually. As a
consequence, this huge commit gets us from the old world to a _compilable_
new world, but still has a large number of known test failures due to
key functionality being stubbed out.
The stubs here are for anything that interacts with providers, since we
now need to do the follow-up work to similarly replace the old
terraform.ResourceProvider interface with its replacement in the new
"providers" package. That work, along with work to fix the remaining
failing tests, will follow in subsequent commits.
The aim here was to replace all references to terraform.State and its
downstream types with states.State, terraform.Plan with plans.Plan,
state.State with statemgr.State, and switch to the new implementations of
the state and plan file formats. However, due to the number of times those
types are used, this also ended up affecting numerous other parts of core
such as terraform.Hook, the backend.Backend interface, and most of the CLI
commands.
Just as with 5861dbf3fc49b19587a31816eb06f511ab861bb4 before, I apologize
in advance to the person who inevitably just found this huge commit while
spelunking through the commit history.
2018-08-14 17:24:45 -04:00
|
|
|
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,
|
2017-02-16 18:29:19 -05:00
|
|
|
},
|
2020-02-13 15:32:58 -05:00
|
|
|
addrs.AbsProviderConfig{
|
2020-10-05 08:33:49 -04:00
|
|
|
Provider: addrs.NewDefaultProvider("test"),
|
2020-03-11 14:19:52 -04:00
|
|
|
Module: addrs.RootModule,
|
2020-02-13 15:32:58 -05:00
|
|
|
},
|
terraform: Ugly huge change to weave in new State and Plan types
Due to how often the state and plan types are referenced throughout
Terraform, there isn't a great way to switch them out gradually. As a
consequence, this huge commit gets us from the old world to a _compilable_
new world, but still has a large number of known test failures due to
key functionality being stubbed out.
The stubs here are for anything that interacts with providers, since we
now need to do the follow-up work to similarly replace the old
terraform.ResourceProvider interface with its replacement in the new
"providers" package. That work, along with work to fix the remaining
failing tests, will follow in subsequent commits.
The aim here was to replace all references to terraform.State and its
downstream types with states.State, terraform.Plan with plans.Plan,
state.State with statemgr.State, and switch to the new implementations of
the state and plan file formats. However, due to the number of times those
types are used, this also ended up affecting numerous other parts of core
such as terraform.Hook, the backend.Backend interface, and most of the CLI
commands.
Just as with 5861dbf3fc49b19587a31816eb06f511ab861bb4 before, I apologize
in advance to the person who inevitably just found this huge commit while
spelunking through the commit history.
2018-08-14 17:24:45 -04:00
|
|
|
)
|
|
|
|
|
})
|
2017-02-16 18:29:19 -05:00
|
|
|
|
terraform: Ugly huge change to weave in new State and Plan types
Due to how often the state and plan types are referenced throughout
Terraform, there isn't a great way to switch them out gradually. As a
consequence, this huge commit gets us from the old world to a _compilable_
new world, but still has a large number of known test failures due to
key functionality being stubbed out.
The stubs here are for anything that interacts with providers, since we
now need to do the follow-up work to similarly replace the old
terraform.ResourceProvider interface with its replacement in the new
"providers" package. That work, along with work to fix the remaining
failing tests, will follow in subsequent commits.
The aim here was to replace all references to terraform.State and its
downstream types with states.State, terraform.Plan with plans.Plan,
state.State with statemgr.State, and switch to the new implementations of
the state and plan file formats. However, due to the number of times those
types are used, this also ended up affecting numerous other parts of core
such as terraform.Hook, the backend.Backend interface, and most of the CLI
commands.
Just as with 5861dbf3fc49b19587a31816eb06f511ab861bb4 before, I apologize
in advance to the person who inevitably just found this huge commit while
spelunking through the commit history.
2018-08-14 17:24:45 -04:00
|
|
|
err := statemgr.NewFilesystem("test.tfstate").WriteState(originalState)
|
2017-02-16 18:29:19 -05:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-01 18:13:04 -04:00
|
|
|
workspace := "test_workspace"
|
|
|
|
|
|
|
|
|
|
args := []string{"-state", "test.tfstate", workspace}
|
2017-08-01 16:41:34 -04:00
|
|
|
ui = new(cli.MockUi)
|
2017-05-30 18:06:13 -04:00
|
|
|
newCmd := &WorkspaceNewCommand{
|
2021-02-16 07:19:22 -05:00
|
|
|
Meta: Meta{Ui: ui, View: view},
|
2017-02-16 18:29:19 -05:00
|
|
|
}
|
2017-02-23 13:13:28 -05:00
|
|
|
if code := newCmd.Run(args); code != 0 {
|
2017-02-16 18:29:19 -05:00
|
|
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-30 20:13:43 -04:00
|
|
|
newPath := filepath.Join(local.DefaultWorkspaceDir, "test", DefaultStateFilename)
|
2020-08-11 11:43:01 -04:00
|
|
|
envState := statemgr.NewFilesystem(newPath)
|
2017-02-16 18:29:19 -05:00
|
|
|
err = envState.RefreshState()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-01 18:13:04 -04:00
|
|
|
b := backend.TestBackendConfig(t, inmem.New(), nil)
|
2025-09-04 06:14:35 -04:00
|
|
|
sMgr, sDiags := b.StateMgr(workspace)
|
|
|
|
|
if sDiags.HasErrors() {
|
|
|
|
|
t.Fatal(sDiags)
|
2017-08-01 18:13:04 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newState := sMgr.State()
|
|
|
|
|
|
2018-11-07 13:49:10 -05:00
|
|
|
if got, want := newState.String(), originalState.String(); got != want {
|
|
|
|
|
t.Fatalf("states not equal\ngot: %s\nwant: %s", got, want)
|
2017-02-16 18:29:19 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-30 18:06:13 -04:00
|
|
|
func TestWorkspace_delete(t *testing.T) {
|
2022-04-08 12:34:16 -04:00
|
|
|
td := t.TempDir()
|
2017-02-16 18:29:19 -05:00
|
|
|
os.MkdirAll(td, 0755)
|
2025-07-16 11:04:10 -04:00
|
|
|
t.Chdir(td)
|
2017-02-16 18:29:19 -05:00
|
|
|
|
2017-05-30 18:06:13 -04:00
|
|
|
// create the workspace directories
|
2017-05-30 20:13:43 -04:00
|
|
|
if err := os.MkdirAll(filepath.Join(local.DefaultWorkspaceDir, "test"), 0755); err != nil {
|
2017-02-16 18:29:19 -05:00
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-30 18:06:13 -04:00
|
|
|
// create the workspace file
|
2017-02-16 18:29:19 -05:00
|
|
|
if err := os.MkdirAll(DefaultDataDir, 0755); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2025-11-19 07:50:43 -05:00
|
|
|
if err := os.WriteFile(filepath.Join(DefaultDataDir, local.DefaultWorkspaceFile), []byte("test"), 0644); err != nil {
|
2017-02-16 18:29:19 -05:00
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-28 13:13:03 -05:00
|
|
|
ui := new(cli.MockUi)
|
2021-02-16 07:19:22 -05:00
|
|
|
view, _ := testView(t)
|
2017-05-30 18:06:13 -04:00
|
|
|
delCmd := &WorkspaceDeleteCommand{
|
2021-02-16 07:19:22 -05:00
|
|
|
Meta: Meta{Ui: ui, View: view},
|
2017-02-16 18:29:19 -05:00
|
|
|
}
|
|
|
|
|
|
2020-06-16 12:23:15 -04:00
|
|
|
current, _ := delCmd.Workspace()
|
2017-02-16 18:29:19 -05:00
|
|
|
if current != "test" {
|
2017-05-30 18:06:13 -04:00
|
|
|
t.Fatal("wrong workspace:", current)
|
2017-02-16 18:29:19 -05:00
|
|
|
}
|
|
|
|
|
|
2017-05-30 18:06:13 -04:00
|
|
|
// we can't delete our current workspace
|
2017-02-23 13:13:28 -05:00
|
|
|
args := []string{"test"}
|
2017-02-28 13:13:03 -05:00
|
|
|
if code := delCmd.Run(args); code == 0 {
|
2017-05-30 18:06:13 -04:00
|
|
|
t.Fatal("expected error deleting current workspace")
|
2017-02-16 18:29:19 -05:00
|
|
|
}
|
|
|
|
|
|
2017-02-28 13:13:03 -05:00
|
|
|
// change back to default
|
2017-05-30 20:13:43 -04:00
|
|
|
if err := delCmd.SetWorkspace(backend.DefaultStateName); err != nil {
|
2017-02-16 18:29:19 -05:00
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-28 13:13:03 -05:00
|
|
|
// try the delete again
|
|
|
|
|
ui = new(cli.MockUi)
|
|
|
|
|
delCmd.Meta.Ui = ui
|
|
|
|
|
if code := delCmd.Run(args); code != 0 {
|
2017-05-30 18:06:13 -04:00
|
|
|
t.Fatalf("error deleting workspace: %s", ui.ErrorWriter)
|
2017-02-28 13:13:03 -05:00
|
|
|
}
|
|
|
|
|
|
2020-06-16 12:23:15 -04:00
|
|
|
current, _ = delCmd.Workspace()
|
2017-02-16 18:29:19 -05:00
|
|
|
if current != backend.DefaultStateName {
|
2017-05-30 18:06:13 -04:00
|
|
|
t.Fatalf("wrong workspace: %q", current)
|
2017-02-16 18:29:19 -05:00
|
|
|
}
|
|
|
|
|
}
|
2020-06-18 10:03:30 -04:00
|
|
|
|
2025-06-26 10:36:29 -04:00
|
|
|
// TestWorkspace_deleteInvalid shows that if a workspace with an invalid name
|
|
|
|
|
// has been created, Terraform allows users to delete it.
|
2020-06-18 10:03:30 -04:00
|
|
|
func TestWorkspace_deleteInvalid(t *testing.T) {
|
2022-04-08 12:34:16 -04:00
|
|
|
td := t.TempDir()
|
2020-06-18 10:03:30 -04:00
|
|
|
os.MkdirAll(td, 0755)
|
2025-07-16 11:04:10 -04:00
|
|
|
t.Chdir(td)
|
2020-06-18 10:03:30 -04:00
|
|
|
|
|
|
|
|
// 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)
|
2021-02-16 07:19:22 -05:00
|
|
|
view, _ := testView(t)
|
2020-06-18 10:03:30 -04:00
|
|
|
delCmd := &WorkspaceDeleteCommand{
|
2021-02-16 07:19:22 -05:00
|
|
|
Meta: Meta{Ui: ui, View: view},
|
2020-06-18 10:03:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-26 10:36:29 -04:00
|
|
|
func TestWorkspace_deleteRejectsEmptyString(t *testing.T) {
|
|
|
|
|
td := t.TempDir()
|
|
|
|
|
os.MkdirAll(td, 0755)
|
2025-07-16 11:04:10 -04:00
|
|
|
t.Chdir(td)
|
2025-06-26 10:36:29 -04:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-30 18:06:13 -04:00
|
|
|
func TestWorkspace_deleteWithState(t *testing.T) {
|
2022-04-08 12:34:16 -04:00
|
|
|
td := t.TempDir()
|
2017-02-16 18:29:19 -05:00
|
|
|
os.MkdirAll(td, 0755)
|
2025-07-16 11:04:10 -04:00
|
|
|
t.Chdir(td)
|
2017-02-16 18:29:19 -05:00
|
|
|
|
2017-05-30 18:06:13 -04:00
|
|
|
// create the workspace directories
|
2017-05-30 20:13:43 -04:00
|
|
|
if err := os.MkdirAll(filepath.Join(local.DefaultWorkspaceDir, "test"), 0755); err != nil {
|
2017-02-16 18:29:19 -05:00
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// create a non-empty state
|
2024-03-07 20:46:07 -05:00
|
|
|
originalState := states.BuildState(func(ss *states.SyncState) {
|
|
|
|
|
ss.SetResourceInstanceCurrent(
|
|
|
|
|
addrs.AbsResourceInstance{
|
|
|
|
|
Resource: addrs.ResourceInstance{
|
|
|
|
|
Resource: addrs.Resource{
|
|
|
|
|
Mode: addrs.ManagedResourceMode,
|
2017-02-16 18:29:19 -05:00
|
|
|
Type: "test_instance",
|
2024-03-07 20:46:07 -05:00
|
|
|
Name: "foo",
|
2017-02-16 18:29:19 -05:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2024-03-07 20:46:07 -05:00
|
|
|
&states.ResourceInstanceObjectSrc{
|
|
|
|
|
AttrsJSON: []byte("{}"),
|
|
|
|
|
Status: states.ObjectReady,
|
|
|
|
|
},
|
|
|
|
|
addrs.AbsProviderConfig{
|
|
|
|
|
Provider: addrs.NewBuiltInProvider("test"),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
originalStateFile := &statefile.File{
|
|
|
|
|
Serial: 1,
|
|
|
|
|
Lineage: "whatever",
|
|
|
|
|
State: originalState,
|
2017-02-16 18:29:19 -05:00
|
|
|
}
|
|
|
|
|
|
2020-08-11 11:43:01 -04:00
|
|
|
f, err := os.Create(filepath.Join(local.DefaultWorkspaceDir, "test", "terraform.tfstate"))
|
2017-02-16 18:29:19 -05:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2020-08-11 11:43:01 -04:00
|
|
|
defer f.Close()
|
2024-03-07 20:46:07 -05:00
|
|
|
if err := statefile.Write(originalStateFile, f); err != nil {
|
2020-08-11 11:43:01 -04:00
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2017-02-16 18:29:19 -05:00
|
|
|
|
2021-10-13 15:21:23 -04:00
|
|
|
ui := cli.NewMockUi()
|
2021-02-16 07:19:22 -05:00
|
|
|
view, _ := testView(t)
|
2017-05-30 18:06:13 -04:00
|
|
|
delCmd := &WorkspaceDeleteCommand{
|
2021-02-16 07:19:22 -05:00
|
|
|
Meta: Meta{Ui: ui, View: view},
|
2017-02-16 18:29:19 -05:00
|
|
|
}
|
2017-02-23 13:13:28 -05:00
|
|
|
args := []string{"test"}
|
|
|
|
|
if code := delCmd.Run(args); code == 0 {
|
2017-02-16 18:29:19 -05:00
|
|
|
t.Fatalf("expected failure without -force.\noutput: %s", ui.OutputWriter)
|
|
|
|
|
}
|
2021-10-13 15:21:23 -04:00
|
|
|
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)
|
|
|
|
|
}
|
2017-02-16 18:29:19 -05:00
|
|
|
|
|
|
|
|
ui = new(cli.MockUi)
|
2017-02-23 13:13:28 -05:00
|
|
|
delCmd.Meta.Ui = ui
|
2017-02-16 18:29:19 -05:00
|
|
|
|
2017-02-23 13:13:28 -05:00
|
|
|
args = []string{"-force", "test"}
|
|
|
|
|
if code := delCmd.Run(args); code != 0 {
|
2017-02-16 18:29:19 -05:00
|
|
|
t.Fatalf("failure: %s", ui.ErrorWriter)
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-30 20:13:43 -04:00
|
|
|
if _, err := os.Stat(filepath.Join(local.DefaultWorkspaceDir, "test")); !os.IsNotExist(err) {
|
2017-02-16 18:29:19 -05:00
|
|
|
t.Fatal("env 'test' still exists!")
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-08-12 09:14:52 -04:00
|
|
|
|
2025-06-27 14:17:43 -04:00
|
|
|
func TestWorkspace_cannotDeleteDefaultWorkspace(t *testing.T) {
|
|
|
|
|
td := t.TempDir()
|
|
|
|
|
os.MkdirAll(td, 0755)
|
2025-07-16 11:04:10 -04:00
|
|
|
t.Chdir(td)
|
2025-06-27 14:17:43 -04:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-12 09:14:52 -04:00
|
|
|
func TestWorkspace_selectWithOrCreate(t *testing.T) {
|
|
|
|
|
// Create a temporary working directory that is empty
|
|
|
|
|
td := t.TempDir()
|
|
|
|
|
os.MkdirAll(td, 0755)
|
2025-07-16 11:04:10 -04:00
|
|
|
t.Chdir(td)
|
2022-08-12 09:14:52 -04:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-01-23 10:33:46 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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(),
|
|
|
|
|
)
|
|
|
|
|
}
|
2022-08-12 09:14:52 -04:00
|
|
|
|
2026-01-23 10:33:46 -05:00
|
|
|
// 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(),
|
|
|
|
|
)
|
|
|
|
|
}
|
2022-08-12 09:14:52 -04:00
|
|
|
}
|
2025-06-26 07:26:58 -04:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|