github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/workspace_command_test.go (about)

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