github.com/opentofu/opentofu@v1.7.1/internal/command/workspace_command_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package command
     7  
     8  import (
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/mitchellh/cli"
    15  	"github.com/opentofu/opentofu/internal/addrs"
    16  	"github.com/opentofu/opentofu/internal/backend"
    17  	"github.com/opentofu/opentofu/internal/backend/local"
    18  	"github.com/opentofu/opentofu/internal/backend/remote-state/inmem"
    19  	"github.com/opentofu/opentofu/internal/encryption"
    20  	"github.com/opentofu/opentofu/internal/states"
    21  	"github.com/opentofu/opentofu/internal/states/statemgr"
    22  
    23  	legacy "github.com/opentofu/opentofu/internal/legacy/tofu"
    24  )
    25  
    26  func TestWorkspace_createAndChange(t *testing.T) {
    27  	// Create a temporary working directory that is empty
    28  	td := t.TempDir()
    29  	os.MkdirAll(td, 0755)
    30  	defer testChdir(t, td)()
    31  
    32  	newCmd := &WorkspaceNewCommand{}
    33  
    34  	current, _ := newCmd.Workspace()
    35  	if current != backend.DefaultStateName {
    36  		t.Fatal("current workspace should be 'default'")
    37  	}
    38  
    39  	args := []string{"test"}
    40  	ui := new(cli.MockUi)
    41  	view, _ := testView(t)
    42  	newCmd.Meta = Meta{Ui: ui, View: view}
    43  	if code := newCmd.Run(args); code != 0 {
    44  		t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
    45  	}
    46  
    47  	current, _ = newCmd.Workspace()
    48  	if current != "test" {
    49  		t.Fatalf("current workspace should be 'test', got %q", current)
    50  	}
    51  
    52  	selCmd := &WorkspaceSelectCommand{}
    53  	args = []string{backend.DefaultStateName}
    54  	ui = new(cli.MockUi)
    55  	selCmd.Meta = Meta{Ui: ui, View: view}
    56  	if code := selCmd.Run(args); code != 0 {
    57  		t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
    58  	}
    59  
    60  	current, _ = newCmd.Workspace()
    61  	if current != backend.DefaultStateName {
    62  		t.Fatal("current workspace should be 'default'")
    63  	}
    64  
    65  }
    66  
    67  // Create some workspaces and test the list output.
    68  // This also ensures we switch to the correct env after each call
    69  func TestWorkspace_createAndList(t *testing.T) {
    70  	// Create a temporary working directory that is empty
    71  	td := t.TempDir()
    72  	os.MkdirAll(td, 0755)
    73  	defer testChdir(t, td)()
    74  
    75  	// make sure a vars file doesn't interfere
    76  	err := os.WriteFile(
    77  		DefaultVarsFilename,
    78  		[]byte(`foo = "bar"`),
    79  		0644,
    80  	)
    81  	if err != nil {
    82  		t.Fatal(err)
    83  	}
    84  
    85  	envs := []string{"test_a", "test_b", "test_c"}
    86  
    87  	// create multiple workspaces
    88  	for _, env := range envs {
    89  		ui := new(cli.MockUi)
    90  		view, _ := testView(t)
    91  		newCmd := &WorkspaceNewCommand{
    92  			Meta: Meta{Ui: ui, View: view},
    93  		}
    94  		if code := newCmd.Run([]string{env}); code != 0 {
    95  			t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
    96  		}
    97  	}
    98  
    99  	listCmd := &WorkspaceListCommand{}
   100  	ui := new(cli.MockUi)
   101  	view, _ := testView(t)
   102  	listCmd.Meta = Meta{Ui: ui, View: view}
   103  
   104  	if code := listCmd.Run(nil); code != 0 {
   105  		t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
   106  	}
   107  
   108  	actual := strings.TrimSpace(ui.OutputWriter.String())
   109  	expected := "default\n  test_a\n  test_b\n* test_c"
   110  
   111  	if actual != expected {
   112  		t.Fatalf("\nexpected: %q\nactual:  %q", expected, actual)
   113  	}
   114  }
   115  
   116  // Create some workspaces and test the show output.
   117  func TestWorkspace_createAndShow(t *testing.T) {
   118  	// Create a temporary working directory that is empty
   119  	td := t.TempDir()
   120  	os.MkdirAll(td, 0755)
   121  	defer testChdir(t, td)()
   122  
   123  	// make sure a vars file doesn't interfere
   124  	err := os.WriteFile(
   125  		DefaultVarsFilename,
   126  		[]byte(`foo = "bar"`),
   127  		0644,
   128  	)
   129  	if err != nil {
   130  		t.Fatal(err)
   131  	}
   132  
   133  	// make sure current workspace show outputs "default"
   134  	showCmd := &WorkspaceShowCommand{}
   135  	ui := new(cli.MockUi)
   136  	view, _ := testView(t)
   137  	showCmd.Meta = Meta{Ui: ui, View: view}
   138  
   139  	if code := showCmd.Run(nil); code != 0 {
   140  		t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
   141  	}
   142  
   143  	actual := strings.TrimSpace(ui.OutputWriter.String())
   144  	expected := "default"
   145  
   146  	if actual != expected {
   147  		t.Fatalf("\nexpected: %q\nactual:  %q", expected, actual)
   148  	}
   149  
   150  	newCmd := &WorkspaceNewCommand{}
   151  
   152  	env := []string{"test_a"}
   153  
   154  	// create test_a workspace
   155  	ui = new(cli.MockUi)
   156  	newCmd.Meta = Meta{Ui: ui, View: view}
   157  	if code := newCmd.Run(env); code != 0 {
   158  		t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
   159  	}
   160  
   161  	selCmd := &WorkspaceSelectCommand{}
   162  	ui = new(cli.MockUi)
   163  	selCmd.Meta = Meta{Ui: ui, View: view}
   164  	if code := selCmd.Run(env); code != 0 {
   165  		t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
   166  	}
   167  
   168  	showCmd = &WorkspaceShowCommand{}
   169  	ui = new(cli.MockUi)
   170  	showCmd.Meta = Meta{Ui: ui, View: view}
   171  
   172  	if code := showCmd.Run(nil); code != 0 {
   173  		t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
   174  	}
   175  
   176  	actual = strings.TrimSpace(ui.OutputWriter.String())
   177  	expected = "test_a"
   178  
   179  	if actual != expected {
   180  		t.Fatalf("\nexpected: %q\nactual:  %q", expected, actual)
   181  	}
   182  }
   183  
   184  // Don't allow names that aren't URL safe
   185  func TestWorkspace_createInvalid(t *testing.T) {
   186  	// Create a temporary working directory that is empty
   187  	td := t.TempDir()
   188  	os.MkdirAll(td, 0755)
   189  	defer testChdir(t, td)()
   190  
   191  	envs := []string{"test_a*", "test_b/foo", "../../../test_c", "好_d"}
   192  
   193  	// create multiple workspaces
   194  	for _, env := range envs {
   195  		ui := new(cli.MockUi)
   196  		view, _ := testView(t)
   197  		newCmd := &WorkspaceNewCommand{
   198  			Meta: Meta{Ui: ui, View: view},
   199  		}
   200  		if code := newCmd.Run([]string{env}); code == 0 {
   201  			t.Fatalf("expected failure: \n%s", ui.OutputWriter)
   202  		}
   203  	}
   204  
   205  	// list workspaces to make sure none were created
   206  	listCmd := &WorkspaceListCommand{}
   207  	ui := new(cli.MockUi)
   208  	view, _ := testView(t)
   209  	listCmd.Meta = Meta{Ui: ui, View: view}
   210  
   211  	if code := listCmd.Run(nil); code != 0 {
   212  		t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
   213  	}
   214  
   215  	actual := strings.TrimSpace(ui.OutputWriter.String())
   216  	expected := "* default"
   217  
   218  	if actual != expected {
   219  		t.Fatalf("\nexpected: %q\nactual:  %q", expected, actual)
   220  	}
   221  }
   222  
   223  func TestWorkspace_createWithState(t *testing.T) {
   224  	td := t.TempDir()
   225  	testCopyDir(t, testFixturePath("inmem-backend"), td)
   226  	defer testChdir(t, td)()
   227  	defer inmem.Reset()
   228  
   229  	// init the backend
   230  	ui := new(cli.MockUi)
   231  	view, _ := testView(t)
   232  	initCmd := &InitCommand{
   233  		Meta: Meta{Ui: ui, View: view},
   234  	}
   235  	if code := initCmd.Run([]string{}); code != 0 {
   236  		t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
   237  	}
   238  
   239  	originalState := states.BuildState(func(s *states.SyncState) {
   240  		s.SetResourceInstanceCurrent(
   241  			addrs.Resource{
   242  				Mode: addrs.ManagedResourceMode,
   243  				Type: "test_instance",
   244  				Name: "foo",
   245  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   246  			&states.ResourceInstanceObjectSrc{
   247  				AttrsJSON: []byte(`{"id":"bar"}`),
   248  				Status:    states.ObjectReady,
   249  			},
   250  			addrs.AbsProviderConfig{
   251  				Provider: addrs.NewDefaultProvider("test"),
   252  				Module:   addrs.RootModule,
   253  			},
   254  		)
   255  	})
   256  
   257  	err := statemgr.WriteAndPersist(statemgr.NewFilesystem("test.tfstate", encryption.StateEncryptionDisabled()), originalState, nil)
   258  	if err != nil {
   259  		t.Fatal(err)
   260  	}
   261  
   262  	workspace := "test_workspace"
   263  
   264  	args := []string{"-state", "test.tfstate", workspace}
   265  	ui = new(cli.MockUi)
   266  	newCmd := &WorkspaceNewCommand{
   267  		Meta: Meta{Ui: ui, View: view},
   268  	}
   269  	if code := newCmd.Run(args); code != 0 {
   270  		t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
   271  	}
   272  
   273  	newPath := filepath.Join(local.DefaultWorkspaceDir, "test", DefaultStateFilename)
   274  	envState := statemgr.NewFilesystem(newPath, encryption.StateEncryptionDisabled())
   275  	err = envState.RefreshState()
   276  	if err != nil {
   277  		t.Fatal(err)
   278  	}
   279  
   280  	b := backend.TestBackendConfig(t, inmem.New(encryption.StateEncryptionDisabled()), nil)
   281  	sMgr, err := b.StateMgr(workspace)
   282  	if err != nil {
   283  		t.Fatal(err)
   284  	}
   285  
   286  	newState := sMgr.State()
   287  
   288  	if got, want := newState.String(), originalState.String(); got != want {
   289  		t.Fatalf("states not equal\ngot: %s\nwant: %s", got, want)
   290  	}
   291  }
   292  
   293  func TestWorkspace_delete(t *testing.T) {
   294  	td := t.TempDir()
   295  	os.MkdirAll(td, 0755)
   296  	defer testChdir(t, td)()
   297  
   298  	// create the workspace directories
   299  	if err := os.MkdirAll(filepath.Join(local.DefaultWorkspaceDir, "test"), 0755); err != nil {
   300  		t.Fatal(err)
   301  	}
   302  
   303  	// create the workspace file
   304  	if err := os.MkdirAll(DefaultDataDir, 0755); err != nil {
   305  		t.Fatal(err)
   306  	}
   307  	if err := os.WriteFile(filepath.Join(DefaultDataDir, local.DefaultWorkspaceFile), []byte("test"), 0644); err != nil {
   308  		t.Fatal(err)
   309  	}
   310  
   311  	ui := new(cli.MockUi)
   312  	view, _ := testView(t)
   313  	delCmd := &WorkspaceDeleteCommand{
   314  		Meta: Meta{Ui: ui, View: view},
   315  	}
   316  
   317  	current, _ := delCmd.Workspace()
   318  	if current != "test" {
   319  		t.Fatal("wrong workspace:", current)
   320  	}
   321  
   322  	// we can't delete our current workspace
   323  	args := []string{"test"}
   324  	if code := delCmd.Run(args); code == 0 {
   325  		t.Fatal("expected error deleting current workspace")
   326  	}
   327  
   328  	// change back to default
   329  	if err := delCmd.SetWorkspace(backend.DefaultStateName); err != nil {
   330  		t.Fatal(err)
   331  	}
   332  
   333  	// try the delete again
   334  	ui = new(cli.MockUi)
   335  	delCmd.Meta.Ui = ui
   336  	if code := delCmd.Run(args); code != 0 {
   337  		t.Fatalf("error deleting workspace: %s", ui.ErrorWriter)
   338  	}
   339  
   340  	current, _ = delCmd.Workspace()
   341  	if current != backend.DefaultStateName {
   342  		t.Fatalf("wrong workspace: %q", current)
   343  	}
   344  }
   345  
   346  func TestWorkspace_deleteInvalid(t *testing.T) {
   347  	td := t.TempDir()
   348  	os.MkdirAll(td, 0755)
   349  	defer testChdir(t, td)()
   350  
   351  	// choose an invalid workspace name
   352  	workspace := "test workspace"
   353  	path := filepath.Join(local.DefaultWorkspaceDir, workspace)
   354  
   355  	// create the workspace directories
   356  	if err := os.MkdirAll(path, 0755); err != nil {
   357  		t.Fatal(err)
   358  	}
   359  
   360  	ui := new(cli.MockUi)
   361  	view, _ := testView(t)
   362  	delCmd := &WorkspaceDeleteCommand{
   363  		Meta: Meta{Ui: ui, View: view},
   364  	}
   365  
   366  	// delete the workspace
   367  	if code := delCmd.Run([]string{workspace}); code != 0 {
   368  		t.Fatalf("error deleting workspace: %s", ui.ErrorWriter)
   369  	}
   370  
   371  	if _, err := os.Stat(path); err == nil {
   372  		t.Fatalf("should have deleted workspace, but %s still exists", path)
   373  	} else if !os.IsNotExist(err) {
   374  		t.Fatalf("unexpected error for workspace path: %s", err)
   375  	}
   376  }
   377  
   378  func TestWorkspace_deleteWithState(t *testing.T) {
   379  	td := t.TempDir()
   380  	os.MkdirAll(td, 0755)
   381  	defer testChdir(t, td)()
   382  
   383  	// create the workspace directories
   384  	if err := os.MkdirAll(filepath.Join(local.DefaultWorkspaceDir, "test"), 0755); err != nil {
   385  		t.Fatal(err)
   386  	}
   387  
   388  	// create a non-empty state
   389  	originalState := &legacy.State{
   390  		Modules: []*legacy.ModuleState{
   391  			{
   392  				Path: []string{"root"},
   393  				Resources: map[string]*legacy.ResourceState{
   394  					"test_instance.foo": {
   395  						Type: "test_instance",
   396  						Primary: &legacy.InstanceState{
   397  							ID: "bar",
   398  						},
   399  					},
   400  				},
   401  			},
   402  		},
   403  	}
   404  
   405  	f, err := os.Create(filepath.Join(local.DefaultWorkspaceDir, "test", "terraform.tfstate"))
   406  	if err != nil {
   407  		t.Fatal(err)
   408  	}
   409  	defer f.Close()
   410  	if err := legacy.WriteState(originalState, f); err != nil {
   411  		t.Fatal(err)
   412  	}
   413  
   414  	ui := cli.NewMockUi()
   415  	view, _ := testView(t)
   416  	delCmd := &WorkspaceDeleteCommand{
   417  		Meta: Meta{Ui: ui, View: view},
   418  	}
   419  	args := []string{"test"}
   420  	if code := delCmd.Run(args); code == 0 {
   421  		t.Fatalf("expected failure without -force.\noutput: %s", ui.OutputWriter)
   422  	}
   423  	gotStderr := ui.ErrorWriter.String()
   424  	if want, got := `Workspace "test" is currently tracking the following resource instances`, gotStderr; !strings.Contains(got, want) {
   425  		t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, got)
   426  	}
   427  	if want, got := `- test_instance.foo`, gotStderr; !strings.Contains(got, want) {
   428  		t.Errorf("error message doesn't mention the remaining instance\nwant substring: %s\ngot:\n%s", want, got)
   429  	}
   430  
   431  	ui = new(cli.MockUi)
   432  	delCmd.Meta.Ui = ui
   433  
   434  	args = []string{"-force", "test"}
   435  	if code := delCmd.Run(args); code != 0 {
   436  		t.Fatalf("failure: %s", ui.ErrorWriter)
   437  	}
   438  
   439  	if _, err := os.Stat(filepath.Join(local.DefaultWorkspaceDir, "test")); !os.IsNotExist(err) {
   440  		t.Fatal("env 'test' still exists!")
   441  	}
   442  }
   443  
   444  func TestWorkspace_selectWithOrCreate(t *testing.T) {
   445  	// Create a temporary working directory that is empty
   446  	td := t.TempDir()
   447  	os.MkdirAll(td, 0755)
   448  	defer testChdir(t, td)()
   449  
   450  	selectCmd := &WorkspaceSelectCommand{}
   451  
   452  	current, _ := selectCmd.Workspace()
   453  	if current != backend.DefaultStateName {
   454  		t.Fatal("current workspace should be 'default'")
   455  	}
   456  
   457  	args := []string{"-or-create", "test"}
   458  	ui := new(cli.MockUi)
   459  	view, _ := testView(t)
   460  	selectCmd.Meta = Meta{Ui: ui, View: view}
   461  	if code := selectCmd.Run(args); code != 0 {
   462  		t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
   463  	}
   464  
   465  	current, _ = selectCmd.Workspace()
   466  	if current != "test" {
   467  		t.Fatalf("current workspace should be 'test', got %q", current)
   468  	}
   469  
   470  }