Skip to content

Commit 4420c4d

Browse files
authored
PSS: Update the workspace new subcommand to work with PSS, add E2E and integration tests for using workspace commands with PSS. (#37855)
* feat: Update the `workspace new` subcommand to work with PSS, add E2E testing * refactor: Replace instances of `ioutil` with `os` while looking at the workspace command * docs: Update code comments in `workspace new` command * test: Update E2E test using PSS with workspace commands to assert state files are created by given commands * test: Include `workspace show` in happy path E2E test using PSS * fix: Allow DeleteState RPC to include the id of the state to delete * test: Include `workspace delete` in happy path E2E test using PSS * fix: Avoid assignment to nil map in mock provider during WriteStateBytes * test: Add integration test for workspace commands when using PSS We still need an E2E test for this, to ensure that the GRPC-related packages pass all the expected data between core and the provider. * test: Update test to reflect changes in the test fixture configuration * docs: Fix code comment * test: Change test to build its own Terraform binary with experiments enabled * refactor: Replace use of `newMeta` with reuse of Meta that we re-set the UI value on
1 parent dc0eba3 commit 4420c4d

File tree

7 files changed

+308
-6
lines changed

7 files changed

+308
-6
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package e2etest
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"path"
10+
"path/filepath"
11+
"strings"
12+
"testing"
13+
14+
"github.com/hashicorp/terraform/internal/e2e"
15+
"github.com/hashicorp/terraform/internal/getproviders"
16+
)
17+
18+
func TestPrimary_stateStore_workspaceCmd(t *testing.T) {
19+
if !canRunGoBuild {
20+
// We're running in a separate-build-then-run context, so we can't
21+
// currently execute this test which depends on being able to build
22+
// new executable at runtime.
23+
//
24+
// (See the comment on canRunGoBuild's declaration for more information.)
25+
t.Skip("can't run without building a new provider executable")
26+
}
27+
28+
t.Setenv(e2e.TestExperimentFlag, "true")
29+
terraformBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform")
30+
31+
fixturePath := filepath.Join("testdata", "full-workflow-with-state-store-fs")
32+
tf := e2e.NewBinary(t, terraformBin, fixturePath)
33+
workspaceDirName := "states" // See workspace_dir value in the configuration
34+
35+
// In order to test integration with PSS we need a provider plugin implementing a state store.
36+
// Here will build the simple6 (built with protocol v6) provider, which implements PSS.
37+
simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6")
38+
simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider)
39+
40+
// Move the provider binaries into a directory that we will point terraform
41+
// to using the -plugin-dir cli flag.
42+
platform := getproviders.CurrentPlatform.String()
43+
hashiDir := "cache/registry.terraform.io/hashicorp/"
44+
if err := os.MkdirAll(tf.Path(hashiDir, "simple6/0.0.1/", platform), os.ModePerm); err != nil {
45+
t.Fatal(err)
46+
}
47+
if err := os.Rename(simple6ProviderExe, tf.Path(hashiDir, "simple6/0.0.1/", platform, "terraform-provider-simple6")); err != nil {
48+
t.Fatal(err)
49+
}
50+
51+
//// Init
52+
_, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color")
53+
if err != nil {
54+
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
55+
}
56+
fi, err := os.Stat(path.Join(tf.WorkDir(), workspaceDirName, "default", "terraform.tfstate"))
57+
if err != nil {
58+
t.Fatalf("failed to open default workspace's state file: %s", err)
59+
}
60+
if fi.Size() == 0 {
61+
t.Fatal("default workspace's state file should not have size 0 bytes")
62+
}
63+
64+
//// Create Workspace: terraform workspace new
65+
newWorkspace := "foobar"
66+
stdout, stderr, err := tf.Run("workspace", "new", newWorkspace, "-no-color")
67+
if err != nil {
68+
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
69+
}
70+
expectedMsg := fmt.Sprintf("Created and switched to workspace %q!", newWorkspace)
71+
if !strings.Contains(stdout, expectedMsg) {
72+
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout)
73+
}
74+
fi, err = os.Stat(path.Join(tf.WorkDir(), workspaceDirName, newWorkspace, "terraform.tfstate"))
75+
if err != nil {
76+
t.Fatalf("failed to open %s workspace's state file: %s", newWorkspace, err)
77+
}
78+
if fi.Size() == 0 {
79+
t.Fatalf("%s workspace's state file should not have size 0 bytes", newWorkspace)
80+
}
81+
82+
//// List Workspaces: : terraform workspace list
83+
stdout, stderr, err = tf.Run("workspace", "list", "-no-color")
84+
if err != nil {
85+
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
86+
}
87+
if !strings.Contains(stdout, newWorkspace) {
88+
t.Errorf("unexpected output, expected the new %q workspace to be listed present, but it's missing. Got:\n%s", newWorkspace, stdout)
89+
}
90+
91+
//// Select Workspace: terraform workspace select
92+
selectedWorkspace := "default"
93+
stdout, stderr, err = tf.Run("workspace", "select", selectedWorkspace, "-no-color")
94+
if err != nil {
95+
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
96+
}
97+
expectedMsg = fmt.Sprintf("Switched to workspace %q.", selectedWorkspace)
98+
if !strings.Contains(stdout, expectedMsg) {
99+
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout)
100+
}
101+
102+
//// Show Workspace: terraform workspace show
103+
stdout, stderr, err = tf.Run("workspace", "show", "-no-color")
104+
if err != nil {
105+
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
106+
}
107+
expectedMsg = fmt.Sprintf("%s\n", selectedWorkspace)
108+
if stdout != expectedMsg {
109+
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout)
110+
}
111+
112+
//// Delete Workspace: terraform workspace delete
113+
stdout, stderr, err = tf.Run("workspace", "delete", newWorkspace, "-no-color")
114+
if err != nil {
115+
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
116+
}
117+
expectedMsg = fmt.Sprintf("Deleted workspace %q!\n", newWorkspace)
118+
if stdout != expectedMsg {
119+
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout)
120+
}
121+
}

internal/command/e2etest/primary_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package e2etest
55

66
import (
7+
"fmt"
78
"os"
89
"path/filepath"
910
"reflect"
@@ -250,6 +251,7 @@ func TestPrimary_stateStore(t *testing.T) {
250251

251252
fixturePath := filepath.Join("testdata", "full-workflow-with-state-store-fs")
252253
tf := e2e.NewBinary(t, terraformBin, fixturePath)
254+
workspaceDirName := "states" // See workspace_dir value in the configuration
253255

254256
// In order to test integration with PSS we need a provider plugin implementing a state store.
255257
// Here will build the simple6 (built with protocol v6) provider, which implements PSS.
@@ -291,7 +293,7 @@ func TestPrimary_stateStore(t *testing.T) {
291293
}
292294

293295
// Check the statefile saved by the fs state store.
294-
path := "terraform.tfstate.d/default/terraform.tfstate"
296+
path := fmt.Sprintf("%s/default/terraform.tfstate", workspaceDirName)
295297
f, err := tf.OpenFile(path)
296298
if err != nil {
297299
t.Fatalf("unexpected error opening state file %s: %s\nstderr:\n%s", path, err, stderr)

internal/command/e2etest/testdata/full-workflow-with-state-store-fs/main.tf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ terraform {
77

88
state_store "simple6_fs" {
99
provider "simple6" {}
10+
11+
workspace_dir = "states"
1012
}
1113
}
1214

internal/command/workspace_command_test.go

Lines changed: 148 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
package command
55

66
import (
7-
"io/ioutil"
7+
"fmt"
88
"os"
99
"path/filepath"
1010
"strings"
@@ -16,11 +16,155 @@ import (
1616
"github.com/hashicorp/terraform/internal/backend"
1717
"github.com/hashicorp/terraform/internal/backend/local"
1818
"github.com/hashicorp/terraform/internal/backend/remote-state/inmem"
19+
"github.com/hashicorp/terraform/internal/providers"
1920
"github.com/hashicorp/terraform/internal/states"
2021
"github.com/hashicorp/terraform/internal/states/statefile"
2122
"github.com/hashicorp/terraform/internal/states/statemgr"
2223
)
2324

25+
func TestWorkspace_allCommands_pluggableStateStore(t *testing.T) {
26+
// Create a temporary working directory with pluggable state storage in the config
27+
td := t.TempDir()
28+
testCopyDir(t, testFixturePath("state-store-new"), td)
29+
t.Chdir(td)
30+
31+
mock := testStateStoreMockWithChunkNegotiation(t, 1000)
32+
33+
// Assumes the mocked provider is hashicorp/test
34+
providerSource, close := newMockProviderSource(t, map[string][]string{
35+
"hashicorp/test": {"1.2.3"},
36+
})
37+
defer close()
38+
39+
ui := new(cli.MockUi)
40+
view, _ := testView(t)
41+
meta := Meta{
42+
AllowExperimentalFeatures: true,
43+
Ui: ui,
44+
View: view,
45+
testingOverrides: &testingOverrides{
46+
Providers: map[addrs.Provider]providers.Factory{
47+
addrs.NewDefaultProvider("test"): providers.FactoryFixed(mock),
48+
},
49+
},
50+
ProviderSource: providerSource,
51+
}
52+
53+
//// Init
54+
intCmd := &InitCommand{
55+
Meta: meta,
56+
}
57+
args := []string{"-enable-pluggable-state-storage-experiment"} // Needed to test init changes for PSS project
58+
code := intCmd.Run(args)
59+
if code != 0 {
60+
t.Fatalf("bad: %d\n\n%s\n%s", code, ui.ErrorWriter, ui.OutputWriter)
61+
}
62+
// We expect a state to have been created for the default workspace
63+
if _, ok := mock.MockStates["default"]; !ok {
64+
t.Fatal("expected the default workspace to exist, but it didn't")
65+
}
66+
67+
//// Create Workspace
68+
newWorkspace := "foobar"
69+
ui = new(cli.MockUi)
70+
meta.Ui = ui
71+
newCmd := &WorkspaceNewCommand{
72+
Meta: meta,
73+
}
74+
75+
current, _ := newCmd.Workspace()
76+
if current != backend.DefaultStateName {
77+
t.Fatal("before creating any custom workspaces, the current workspace should be 'default'")
78+
}
79+
80+
args = []string{newWorkspace}
81+
code = newCmd.Run(args)
82+
if code != 0 {
83+
t.Fatalf("bad: %d\n\n%s\n%s", code, ui.ErrorWriter, ui.OutputWriter)
84+
}
85+
expectedMsg := fmt.Sprintf("Created and switched to workspace %q!", newWorkspace)
86+
if !strings.Contains(ui.OutputWriter.String(), expectedMsg) {
87+
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, ui.OutputWriter)
88+
}
89+
// We expect a state to have been created for the new custom workspace
90+
if _, ok := mock.MockStates[newWorkspace]; !ok {
91+
t.Fatalf("expected the %s workspace to exist, but it didn't", newWorkspace)
92+
}
93+
current, _ = newCmd.Workspace()
94+
if current != newWorkspace {
95+
t.Fatalf("current workspace should be %q, got %q", newWorkspace, current)
96+
}
97+
98+
//// List Workspaces
99+
ui = new(cli.MockUi)
100+
meta.Ui = ui
101+
listCmd := &WorkspaceListCommand{
102+
Meta: meta,
103+
}
104+
args = []string{}
105+
code = listCmd.Run(args)
106+
if code != 0 {
107+
t.Fatalf("bad: %d\n\n%s\n%s", code, ui.ErrorWriter, ui.OutputWriter)
108+
}
109+
if !strings.Contains(ui.OutputWriter.String(), newWorkspace) {
110+
t.Errorf("unexpected output, expected the new %q workspace to be listed present, but it's missing. Got:\n%s", newWorkspace, ui.OutputWriter)
111+
}
112+
113+
//// Select Workspace
114+
ui = new(cli.MockUi)
115+
meta.Ui = ui
116+
selCmd := &WorkspaceSelectCommand{
117+
Meta: meta,
118+
}
119+
selectedWorkspace := backend.DefaultStateName
120+
args = []string{selectedWorkspace}
121+
code = selCmd.Run(args)
122+
if code != 0 {
123+
t.Fatalf("bad: %d\n\n%s\n%s", code, ui.ErrorWriter, ui.OutputWriter)
124+
}
125+
expectedMsg = fmt.Sprintf("Switched to workspace %q.", selectedWorkspace)
126+
if !strings.Contains(ui.OutputWriter.String(), expectedMsg) {
127+
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, ui.OutputWriter)
128+
}
129+
130+
//// Show Workspace
131+
ui = new(cli.MockUi)
132+
meta.Ui = ui
133+
showCmd := &WorkspaceShowCommand{
134+
Meta: meta,
135+
}
136+
args = []string{}
137+
code = showCmd.Run(args)
138+
if code != 0 {
139+
t.Fatalf("bad: %d\n\n%s\n%s", code, ui.ErrorWriter, ui.OutputWriter)
140+
}
141+
expectedMsg = fmt.Sprintf("%s\n", selectedWorkspace)
142+
if !strings.Contains(ui.OutputWriter.String(), expectedMsg) {
143+
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, ui.OutputWriter)
144+
}
145+
146+
current, _ = newCmd.Workspace()
147+
if current != backend.DefaultStateName {
148+
t.Fatal("current workspace should be 'default'")
149+
}
150+
151+
//// Delete Workspace
152+
ui = new(cli.MockUi)
153+
meta.Ui = ui
154+
deleteCmd := &WorkspaceDeleteCommand{
155+
Meta: meta,
156+
}
157+
args = []string{newWorkspace}
158+
code = deleteCmd.Run(args)
159+
if code != 0 {
160+
t.Fatalf("bad: %d\n\n%s\n%s", code, ui.ErrorWriter, ui.OutputWriter)
161+
}
162+
expectedMsg = fmt.Sprintf("Deleted workspace %q!\n", newWorkspace)
163+
if !strings.Contains(ui.OutputWriter.String(), expectedMsg) {
164+
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, ui.OutputWriter)
165+
}
166+
}
167+
24168
func TestWorkspace_createAndChange(t *testing.T) {
25169
// Create a temporary working directory that is empty
26170
td := t.TempDir()
@@ -114,7 +258,7 @@ func TestWorkspace_createAndList(t *testing.T) {
114258
t.Chdir(td)
115259

116260
// make sure a vars file doesn't interfere
117-
err := ioutil.WriteFile(
261+
err := os.WriteFile(
118262
DefaultVarsFilename,
119263
[]byte(`foo = "bar"`),
120264
0644,
@@ -162,7 +306,7 @@ func TestWorkspace_createAndShow(t *testing.T) {
162306
t.Chdir(td)
163307

164308
// make sure a vars file doesn't interfere
165-
err := ioutil.WriteFile(
309+
err := os.WriteFile(
166310
DefaultVarsFilename,
167311
[]byte(`foo = "bar"`),
168312
0644,
@@ -345,7 +489,7 @@ func TestWorkspace_delete(t *testing.T) {
345489
if err := os.MkdirAll(DefaultDataDir, 0755); err != nil {
346490
t.Fatal(err)
347491
}
348-
if err := ioutil.WriteFile(filepath.Join(DefaultDataDir, local.DefaultWorkspaceFile), []byte("test"), 0644); err != nil {
492+
if err := os.WriteFile(filepath.Join(DefaultDataDir, local.DefaultWorkspaceFile), []byte("test"), 0644); err != nil {
349493
t.Fatal(err)
350494
}
351495

0 commit comments

Comments
 (0)