// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "io/ioutil" "os" "path/filepath" "strings" "testing" "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/states" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/mitchellh/cli" legacy "github.com/hashicorp/terraform/internal/legacy/terraform" ) func TestWorkspace_createAndChange(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() os.MkdirAll(td, 0755) defer testChdir(t, 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'") } } // 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) defer testChdir(t, td)() // make sure a vars file doesn't interfere err := ioutil.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) defer testChdir(t, td)() // make sure a vars file doesn't interfere err := ioutil.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) defer testChdir(t, 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) defer testChdir(t, 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, err := b.StateMgr(workspace) if err != nil { t.Fatal(err) } 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) defer testChdir(t, 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 := ioutil.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) } } func TestWorkspace_deleteInvalid(t *testing.T) { td := t.TempDir() os.MkdirAll(td, 0755) defer testChdir(t, 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_deleteWithState(t *testing.T) { td := t.TempDir() os.MkdirAll(td, 0755) defer testChdir(t, 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 := &legacy.State{ Modules: []*legacy.ModuleState{ { Path: []string{"root"}, Resources: map[string]*legacy.ResourceState{ "test_instance.foo": { Type: "test_instance", Primary: &legacy.InstanceState{ ID: "bar", }, }, }, }, }, } f, err := os.Create(filepath.Join(local.DefaultWorkspaceDir, "test", "terraform.tfstate")) if err != nil { t.Fatal(err) } defer f.Close() if err := legacy.WriteState(originalState, 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_selectWithOrCreate(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() os.MkdirAll(td, 0755) defer testChdir(t, 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) } }