From 2d76ec13d66b2400ff74dc0a356b702d243c7166 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 31 Oct 2025 18:31:46 +0000 Subject: [PATCH 01/16] feat: Update the `workspace new` subcommand to work with PSS, add E2E testing --- .../e2etest/pluggable_state_store_test.go | 89 +++++++++++++++++++ internal/command/workspace_new.go | 29 +++++- 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 internal/command/e2etest/pluggable_state_store_test.go diff --git a/internal/command/e2etest/pluggable_state_store_test.go b/internal/command/e2etest/pluggable_state_store_test.go new file mode 100644 index 000000000000..84b27335fbc0 --- /dev/null +++ b/internal/command/e2etest/pluggable_state_store_test.go @@ -0,0 +1,89 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package e2etest + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/hashicorp/terraform/internal/e2e" + "github.com/hashicorp/terraform/internal/getproviders" +) + +func TestPrimary_stateStore_workspaceCmd(t *testing.T) { + if v := os.Getenv("TF_TEST_EXPERIMENTS"); v == "" { + t.Skip("can't run without enabling experiments in the executable terraform binary, enable with TF_TEST_EXPERIMENTS=1") + } + + if !canRunGoBuild { + // We're running in a separate-build-then-run context, so we can't + // currently execute this test which depends on being able to build + // new executable at runtime. + // + // (See the comment on canRunGoBuild's declaration for more information.) + t.Skip("can't run without building a new provider executable") + } + t.Parallel() + + tf := e2e.NewBinary(t, terraformBin, "testdata/full-workflow-with-state-store-fs") + + // In order to test integration with PSS we need a provider plugin implementing a state store. + // Here will build the simple6 (built with protocol v6) provider, which implements PSS. + simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6") + simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider) + + // Move the provider binaries into a directory that we will point terraform + // to using the -plugin-dir cli flag. + platform := getproviders.CurrentPlatform.String() + hashiDir := "cache/registry.terraform.io/hashicorp/" + if err := os.MkdirAll(tf.Path(hashiDir, "simple6/0.0.1/", platform), os.ModePerm); err != nil { + t.Fatal(err) + } + if err := os.Rename(simple6ProviderExe, tf.Path(hashiDir, "simple6/0.0.1/", platform, "terraform-provider-simple6")); err != nil { + t.Fatal(err) + } + + //// Init + _, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color") + if err != nil { + t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) + } + + //// Create Workspace + newWorkspace := "foobar" + stdout, stderr, err := tf.Run("workspace", "new", newWorkspace, "-no-color") + if err != nil { + t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) + } + + expectedMsg := fmt.Sprintf("Created and switched to workspace %q!", newWorkspace) + if !strings.Contains(stdout, expectedMsg) { + t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) + } + + //// List Workspaces + stdout, stderr, err = tf.Run("workspace", "list", "-no-color") + if err != nil { + t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) + } + + if !strings.Contains(stdout, newWorkspace) { + t.Errorf("unexpected output, expected the new %q workspace to be listed present, but it's missing. Got:\n%s", newWorkspace, stdout) + } + + //// Select Workspace + selectedWorkspace := "default" + stdout, stderr, err = tf.Run("workspace", "select", selectedWorkspace, "-no-color") + if err != nil { + t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) + } + + expectedMsg = fmt.Sprintf("Switched to workspace %q.", selectedWorkspace) + if !strings.Contains(stdout, expectedMsg) { + t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) + } +} diff --git a/internal/command/workspace_new.go b/internal/command/workspace_new.go index d36c76efef61..f1113c4d79a5 100644 --- a/internal/command/workspace_new.go +++ b/internal/command/workspace_new.go @@ -10,9 +10,12 @@ import ( "time" "github.com/hashicorp/cli" + "github.com/hashicorp/terraform/internal/backend/local" + backendPluggable "github.com/hashicorp/terraform/internal/backend/pluggable" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" + "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/posener/complete" @@ -93,12 +96,36 @@ func (c *WorkspaceNewCommand) Run(args []string) int { } } - _, sDiags := b.StateMgr(workspace) + // Create the new workspace + // + // In remote-state backends, obtaining a state manager + // creates an empty state file for the new workspace as a + // side-effect. + sMgr, sDiags := b.StateMgr(workspace) if sDiags.HasErrors() { c.Ui.Error(sDiags.Err().Error()) return 1 } + if l, ok := b.(*local.Local); ok { + if _, ok := l.Backend.(*backendPluggable.Pluggable); ok { + // Obtaining the state manager will not have created the state file as a side effect + // if a pluggable state store is in use. + // + // Instead, explicitly create the new workspace by saving an empty state file. + // We only do this when the backend in use is pluggable, to avoid impacting users + // of remote-state backends. + if err := sMgr.WriteState(states.NewState()); err != nil { + c.Ui.Error(err.Error()) + return 1 + } + if err := sMgr.PersistState(nil); err != nil { + c.Ui.Error(err.Error()) + return 1 + } + } + } + // now set the current workspace locally if err := c.SetWorkspace(workspace); err != nil { c.Ui.Error(fmt.Sprintf("Error selecting new workspace: %s", err)) From 38010e54f507ae06086156cbf9d1355f3bd450d1 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 3 Nov 2025 09:52:03 +0000 Subject: [PATCH 02/16] refactor: Replace instances of `ioutil` with `os` while looking at the workspace command --- internal/command/workspace_command_test.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/command/workspace_command_test.go b/internal/command/workspace_command_test.go index 5e35ccef413c..25ba835f4511 100644 --- a/internal/command/workspace_command_test.go +++ b/internal/command/workspace_command_test.go @@ -4,7 +4,6 @@ package command import ( - "io/ioutil" "os" "path/filepath" "strings" @@ -114,7 +113,7 @@ func TestWorkspace_createAndList(t *testing.T) { t.Chdir(td) // make sure a vars file doesn't interfere - err := ioutil.WriteFile( + err := os.WriteFile( DefaultVarsFilename, []byte(`foo = "bar"`), 0644, @@ -162,7 +161,7 @@ func TestWorkspace_createAndShow(t *testing.T) { t.Chdir(td) // make sure a vars file doesn't interfere - err := ioutil.WriteFile( + err := os.WriteFile( DefaultVarsFilename, []byte(`foo = "bar"`), 0644, @@ -345,7 +344,7 @@ func TestWorkspace_delete(t *testing.T) { 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 { + if err := os.WriteFile(filepath.Join(DefaultDataDir, local.DefaultWorkspaceFile), []byte("test"), 0644); err != nil { t.Fatal(err) } From a6f6932404ec1df7151e979ee4dd0329a66a9aae Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 3 Nov 2025 11:30:45 +0000 Subject: [PATCH 03/16] docs: Update code comments in `workspace new` command --- internal/command/workspace_new.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/command/workspace_new.go b/internal/command/workspace_new.go index f1113c4d79a5..5ce346a2de08 100644 --- a/internal/command/workspace_new.go +++ b/internal/command/workspace_new.go @@ -98,9 +98,11 @@ func (c *WorkspaceNewCommand) Run(args []string) int { // Create the new workspace // - // In remote-state backends, obtaining a state manager - // creates an empty state file for the new workspace as a - // side-effect. + // In local, remote and remote-state backends obtaining a state manager + // creates an empty state file for the new workspace as a side-effect. + // + // The cloud backend also has logic in StateMgr for creating projects and + // workspaces if they don't already exist. sMgr, sDiags := b.StateMgr(workspace) if sDiags.HasErrors() { c.Ui.Error(sDiags.Err().Error()) From 7f9bfe27b18a1cc3ba7131c5001dec6e40e30fc0 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Thu, 6 Nov 2025 15:42:51 +0000 Subject: [PATCH 04/16] test: Update E2E test using PSS with workspace commands to assert state files are created by given commands --- .../e2etest/pluggable_state_store_test.go | 19 ++++++++++++++++--- .../full-workflow-with-state-store-fs/main.tf | 2 ++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/internal/command/e2etest/pluggable_state_store_test.go b/internal/command/e2etest/pluggable_state_store_test.go index 84b27335fbc0..c0b512d14390 100644 --- a/internal/command/e2etest/pluggable_state_store_test.go +++ b/internal/command/e2etest/pluggable_state_store_test.go @@ -6,6 +6,7 @@ package e2etest import ( "fmt" "os" + "path" "path/filepath" "strings" "testing" @@ -30,6 +31,7 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) { t.Parallel() tf := e2e.NewBinary(t, terraformBin, "testdata/full-workflow-with-state-store-fs") + workspaceDirName := "states" // see test fixture value for workspace_dir // In order to test integration with PSS we need a provider plugin implementing a state store. // Here will build the simple6 (built with protocol v6) provider, which implements PSS. @@ -52,6 +54,13 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } + fi, err := os.Stat(path.Join(tf.WorkDir(), workspaceDirName, "default", "terraform.tfstate")) + if err != nil { + t.Fatalf("failed to open default workspace's state file: %s", err) + } + if fi.Size() == 0 { + t.Fatal("default workspace's state file should not have size 0 bytes") + } //// Create Workspace newWorkspace := "foobar" @@ -59,18 +68,23 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } - expectedMsg := fmt.Sprintf("Created and switched to workspace %q!", newWorkspace) if !strings.Contains(stdout, expectedMsg) { t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) } + fi, err = os.Stat(path.Join(tf.WorkDir(), workspaceDirName, newWorkspace, "terraform.tfstate")) + if err != nil { + t.Fatalf("failed to open %s workspace's state file: %s", newWorkspace, err) + } + if fi.Size() == 0 { + t.Fatalf("%s workspace's state file should not have size 0 bytes", newWorkspace) + } //// List Workspaces stdout, stderr, err = tf.Run("workspace", "list", "-no-color") if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } - if !strings.Contains(stdout, newWorkspace) { t.Errorf("unexpected output, expected the new %q workspace to be listed present, but it's missing. Got:\n%s", newWorkspace, stdout) } @@ -81,7 +95,6 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } - expectedMsg = fmt.Sprintf("Switched to workspace %q.", selectedWorkspace) if !strings.Contains(stdout, expectedMsg) { t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) diff --git a/internal/command/e2etest/testdata/full-workflow-with-state-store-fs/main.tf b/internal/command/e2etest/testdata/full-workflow-with-state-store-fs/main.tf index d2f5c9b4446f..d2c773a6fca9 100644 --- a/internal/command/e2etest/testdata/full-workflow-with-state-store-fs/main.tf +++ b/internal/command/e2etest/testdata/full-workflow-with-state-store-fs/main.tf @@ -7,6 +7,8 @@ terraform { state_store "simple6_fs" { provider "simple6" {} + + workspace_dir = "states" } } From e3877b9e9cd2239e72682f2de016c5af43aa3b01 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Thu, 6 Nov 2025 17:05:19 +0000 Subject: [PATCH 05/16] test: Include `workspace show` in happy path E2E test using PSS --- .../e2etest/pluggable_state_store_test.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/internal/command/e2etest/pluggable_state_store_test.go b/internal/command/e2etest/pluggable_state_store_test.go index c0b512d14390..74eddff70098 100644 --- a/internal/command/e2etest/pluggable_state_store_test.go +++ b/internal/command/e2etest/pluggable_state_store_test.go @@ -62,7 +62,7 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) { t.Fatal("default workspace's state file should not have size 0 bytes") } - //// Create Workspace + //// Create Workspace: terraform workspace new newWorkspace := "foobar" stdout, stderr, err := tf.Run("workspace", "new", newWorkspace, "-no-color") if err != nil { @@ -80,7 +80,7 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) { t.Fatalf("%s workspace's state file should not have size 0 bytes", newWorkspace) } - //// List Workspaces + //// List Workspaces: : terraform workspace list stdout, stderr, err = tf.Run("workspace", "list", "-no-color") if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) @@ -89,7 +89,7 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) { t.Errorf("unexpected output, expected the new %q workspace to be listed present, but it's missing. Got:\n%s", newWorkspace, stdout) } - //// Select Workspace + //// Select Workspace: terraform workspace select selectedWorkspace := "default" stdout, stderr, err = tf.Run("workspace", "select", selectedWorkspace, "-no-color") if err != nil { @@ -99,4 +99,14 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) { if !strings.Contains(stdout, expectedMsg) { t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) } + + //// Show Workspace: terraform workspace show + stdout, stderr, err = tf.Run("workspace", "show", "-no-color") + if err != nil { + t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) + } + expectedMsg = fmt.Sprintf("%s\n", selectedWorkspace) + if stdout != expectedMsg { + t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) + } } From 0df7988b4137326c3e920ae7efb7bc47e771cd57 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 7 Nov 2025 11:23:55 +0000 Subject: [PATCH 06/16] fix: Allow DeleteState RPC to include the id of the state to delete --- internal/plugin6/grpc_provider.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/plugin6/grpc_provider.go b/internal/plugin6/grpc_provider.go index 0339cefeeea2..2f517f363af1 100644 --- a/internal/plugin6/grpc_provider.go +++ b/internal/plugin6/grpc_provider.go @@ -1841,6 +1841,7 @@ func (p *GRPCProvider) DeleteState(r providers.DeleteStateRequest) (resp provide protoReq := &proto6.DeleteState_Request{ TypeName: r.TypeName, + StateId: r.StateId, } schema := p.GetProviderSchema() From a3bd37185918197f3328c29c8f3b240d8d54825b Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 7 Nov 2025 11:24:08 +0000 Subject: [PATCH 07/16] test: Include `workspace delete` in happy path E2E test using PSS --- internal/command/e2etest/pluggable_state_store_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internal/command/e2etest/pluggable_state_store_test.go b/internal/command/e2etest/pluggable_state_store_test.go index 74eddff70098..330f6f1bdac8 100644 --- a/internal/command/e2etest/pluggable_state_store_test.go +++ b/internal/command/e2etest/pluggable_state_store_test.go @@ -109,4 +109,14 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) { if stdout != expectedMsg { t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) } + + //// Delete Workspace: terraform workspace delete + stdout, stderr, err = tf.Run("workspace", "delete", newWorkspace, "-no-color") + if err != nil { + t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) + } + expectedMsg = fmt.Sprintf("Deleted workspace %q!\n", newWorkspace) + if stdout != expectedMsg { + t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) + } } From f1169242f65fd1439daa93dda4141cc3353132df Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 7 Nov 2025 17:28:34 +0000 Subject: [PATCH 08/16] fix: Avoid assignment to nil map in mock provider during WriteStateBytes --- internal/providers/testing/provider_mock.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/providers/testing/provider_mock.go b/internal/providers/testing/provider_mock.go index 8aa55051e3a2..e164593fc60c 100644 --- a/internal/providers/testing/provider_mock.go +++ b/internal/providers/testing/provider_mock.go @@ -361,6 +361,9 @@ func (p *MockProvider) WriteStateBytes(r providers.WriteStateBytesRequest) (resp // If we haven't already, record in the mock that // the matching workspace exists + if p.MockStates == nil { + p.MockStates = make(map[string]interface{}) + } p.MockStates[r.StateId] = true return p.WriteStateBytesResponse From d7b78a6143308cc7788dbdd24a2817bb15d0bbfe Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 7 Nov 2025 17:28:37 +0000 Subject: [PATCH 09/16] 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. --- internal/command/workspace_command_test.go | 149 +++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/internal/command/workspace_command_test.go b/internal/command/workspace_command_test.go index 25ba835f4511..937e70f31a1b 100644 --- a/internal/command/workspace_command_test.go +++ b/internal/command/workspace_command_test.go @@ -4,6 +4,7 @@ package command import ( + "fmt" "os" "path/filepath" "strings" @@ -15,11 +16,159 @@ import ( "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/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/states/statemgr" ) +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) + newMeta := func(provider providers.Interface) (meta Meta, ui *cli.MockUi, close func()) { + // Assumes the mocked provider is hashicorp/test + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.2.3"}, + }) + + 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, + } + return meta, ui, close + } + + //// Init + meta, ui, close := newMeta(mock) + defer close() + 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" + + meta, ui, close = newMeta(mock) + defer close() + 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 + meta, ui, close = newMeta(mock) + defer close() + 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 + meta, ui, close = newMeta(mock) + defer close() + 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 + meta, ui, close = newMeta(mock) + defer close() + 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 + meta, ui, close = newMeta(mock) + defer close() + 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) + } +} + func TestWorkspace_createAndChange(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() From 7bc3ea8dc955ac08b867e747821a30c37232a9a5 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 7 Nov 2025 17:37:00 +0000 Subject: [PATCH 10/16] test: Update test to reflect changes in the test fixture configuration --- internal/command/e2etest/primary_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/command/e2etest/primary_test.go b/internal/command/e2etest/primary_test.go index 786b33d8866c..ebffa9c37d75 100644 --- a/internal/command/e2etest/primary_test.go +++ b/internal/command/e2etest/primary_test.go @@ -4,6 +4,7 @@ package e2etest import ( + "fmt" "os" "path/filepath" "reflect" @@ -250,6 +251,7 @@ func TestPrimary_stateStore(t *testing.T) { fixturePath := filepath.Join("testdata", "full-workflow-with-state-store-fs") tf := e2e.NewBinary(t, terraformBin, fixturePath) + workspaceDirName := "states" // See workspace_dir value in the configuration // In order to test integration with PSS we need a provider plugin implementing a state store. // Here will build the simple6 (built with protocol v6) provider, which implements PSS. @@ -291,7 +293,7 @@ func TestPrimary_stateStore(t *testing.T) { } // Check the statefile saved by the fs state store. - path := "terraform.tfstate.d/default/terraform.tfstate" + path := fmt.Sprintf("%s/default/terraform.tfstate", workspaceDirName) f, err := tf.OpenFile(path) if err != nil { t.Fatalf("unexpected error opening state file %s: %s\nstderr:\n%s", path, err, stderr) From b8f908894164cb94fd2d42bd0bf95211576686f3 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 7 Nov 2025 18:05:30 +0000 Subject: [PATCH 11/16] docs: Fix code comment --- internal/command/workspace_new.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/command/workspace_new.go b/internal/command/workspace_new.go index 5ce346a2de08..d217fd38e945 100644 --- a/internal/command/workspace_new.go +++ b/internal/command/workspace_new.go @@ -111,7 +111,7 @@ func (c *WorkspaceNewCommand) Run(args []string) int { if l, ok := b.(*local.Local); ok { if _, ok := l.Backend.(*backendPluggable.Pluggable); ok { - // Obtaining the state manager will not have created the state file as a side effect + // Obtaining the state manager would have not created the state file as a side effect // if a pluggable state store is in use. // // Instead, explicitly create the new workspace by saving an empty state file. From ae65335c6527b01b571a220d9275893aa656d9b7 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Tue, 11 Nov 2025 15:47:47 +0000 Subject: [PATCH 12/16] test: Change test to build its own Terraform binary with experiments enabled --- .../command/e2etest/pluggable_state_store_test.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/internal/command/e2etest/pluggable_state_store_test.go b/internal/command/e2etest/pluggable_state_store_test.go index 330f6f1bdac8..a8f59183b988 100644 --- a/internal/command/e2etest/pluggable_state_store_test.go +++ b/internal/command/e2etest/pluggable_state_store_test.go @@ -16,10 +16,6 @@ import ( ) func TestPrimary_stateStore_workspaceCmd(t *testing.T) { - if v := os.Getenv("TF_TEST_EXPERIMENTS"); v == "" { - t.Skip("can't run without enabling experiments in the executable terraform binary, enable with TF_TEST_EXPERIMENTS=1") - } - if !canRunGoBuild { // We're running in a separate-build-then-run context, so we can't // currently execute this test which depends on being able to build @@ -28,10 +24,13 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) { // (See the comment on canRunGoBuild's declaration for more information.) t.Skip("can't run without building a new provider executable") } - t.Parallel() - tf := e2e.NewBinary(t, terraformBin, "testdata/full-workflow-with-state-store-fs") - workspaceDirName := "states" // see test fixture value for workspace_dir + t.Setenv(e2e.TestExperimentFlag, "true") + terraformBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform") + + fixturePath := filepath.Join("testdata", "full-workflow-with-state-store-fs") + tf := e2e.NewBinary(t, terraformBin, fixturePath) + workspaceDirName := "states" // See workspace_dir value in the configuration // In order to test integration with PSS we need a provider plugin implementing a state store. // Here will build the simple6 (built with protocol v6) provider, which implements PSS. From 8c7fc7804425caadfdd557c6df12df240646abb8 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Tue, 11 Nov 2025 17:11:14 +0000 Subject: [PATCH 13/16] test: Add E2E tests for `state list` and `state show` commands --- .../e2etest/pluggable_state_store_test.go | 78 +++++++++++++++++++ .../.terraform.lock.hcl | 6 ++ .../hashicorp/simple6/0.0.1/.gitkeep | 0 .../.terraform/terraform.tfstate | 16 ++++ .../main.tf | 25 ++++++ .../states/default/terraform.tfstate | 40 ++++++++++ 6 files changed, 165 insertions(+) create mode 100644 internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/.terraform.lock.hcl create mode 100644 internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/.terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/.gitkeep create mode 100644 internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/.terraform/terraform.tfstate create mode 100644 internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/main.tf create mode 100644 internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/states/default/terraform.tfstate diff --git a/internal/command/e2etest/pluggable_state_store_test.go b/internal/command/e2etest/pluggable_state_store_test.go index a8f59183b988..09f802ce8cca 100644 --- a/internal/command/e2etest/pluggable_state_store_test.go +++ b/internal/command/e2etest/pluggable_state_store_test.go @@ -11,10 +11,12 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform/internal/e2e" "github.com/hashicorp/terraform/internal/getproviders" ) +// Tests using `terraform workspace` commands in combination with pluggable state storage. func TestPrimary_stateStore_workspaceCmd(t *testing.T) { if !canRunGoBuild { // We're running in a separate-build-then-run context, so we can't @@ -119,3 +121,79 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) { t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) } } + +// Tests using `terraform state` subcommands in combination with pluggable state storage: +// > `terraform state show` +// > `terraform state list` +func TestPrimary_stateStore_stateCmds(t *testing.T) { + + if !canRunGoBuild { + // We're running in a separate-build-then-run context, so we can't + // currently execute this test which depends on being able to build + // new executable at runtime. + // + // (See the comment on canRunGoBuild's declaration for more information.) + t.Skip("can't run without building a new provider executable") + } + + t.Setenv(e2e.TestExperimentFlag, "true") + tfBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform") + + fixturePath := filepath.Join("testdata", "initialized-directory-with-state-store-fs") + tf := e2e.NewBinary(t, tfBin, fixturePath) + + workspaceDirName := "states" // see test fixture value for workspace_dir + + // In order to test integration with PSS we need a provider plugin implementing a state store. + // Here will build the simple6 (built with protocol v6) provider, which implements PSS. + simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6") + simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider) + + // Move the provider binaries into the correct directory .terraform/providers/ directory + // that will contain provider binaries in an initialized working directory. + platform := getproviders.CurrentPlatform.String() + if err := os.MkdirAll(tf.Path(".terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/", platform), os.ModePerm); err != nil { + t.Fatal(err) + } + if err := os.Rename(simple6ProviderExe, tf.Path(".terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/", platform, "terraform-provider-simple6")); err != nil { + t.Fatal(err) + } + + // Assert that the test starts with the default state present from test fixtures + defaultStateId := "default" + fi, err := os.Stat(path.Join(tf.WorkDir(), workspaceDirName, defaultStateId, "terraform.tfstate")) + if err != nil { + t.Fatalf("failed to open default workspace's state file: %s", err) + } + if fi.Size() == 0 { + t.Fatal("default workspace's state file should not have size 0 bytes") + } + + //// List State: terraform state list + expectedResourceAddr := "terraform_data.my-data" + stdout, stderr, err := tf.Run("state", "list", "-no-color") + if err != nil { + t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) + } + expectedMsg := expectedResourceAddr + "\n" // This is the only resource instance in the test fixture state + if stdout != expectedMsg { + t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) + } + + //// Show State: terraform state show + stdout, stderr, err = tf.Run("state", "show", expectedResourceAddr, "-no-color") + if err != nil { + t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) + } + // show displays the state for the specified resource + expectedMsg = `# terraform_data.my-data: +resource "terraform_data" "my-data" { + id = "d71fb368-2ba1-fb4c-5bd9-6a2b7f05d60c" + input = "hello world" + output = "hello world" +} +` + if diff := cmp.Diff(stdout, expectedMsg); diff != "" { + t.Errorf("wrong result, diff:\n%s", diff) + } +} diff --git a/internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/.terraform.lock.hcl b/internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/.terraform.lock.hcl new file mode 100644 index 000000000000..7a0db0a25a93 --- /dev/null +++ b/internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/.terraform.lock.hcl @@ -0,0 +1,6 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/simple6" { + version = "0.0.1" +} diff --git a/internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/.terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/.gitkeep b/internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/.terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/.terraform/terraform.tfstate b/internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/.terraform/terraform.tfstate new file mode 100644 index 000000000000..e297b792ce7b --- /dev/null +++ b/internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/.terraform/terraform.tfstate @@ -0,0 +1,16 @@ +{ + "version": 3, + "terraform_version": "1.15.0", + "state_store": { + "type": "simple6_fs", + "provider": { + "version": "0.0.1", + "source": "registry.terraform.io/hashicorp/simple6", + "config": {} + }, + "config": { + "workspace_dir": "states" + }, + "hash": 3942813381 + } +} \ No newline at end of file diff --git a/internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/main.tf b/internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/main.tf new file mode 100644 index 000000000000..d2c773a6fca9 --- /dev/null +++ b/internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/main.tf @@ -0,0 +1,25 @@ +terraform { + required_providers { + simple6 = { + source = "registry.terraform.io/hashicorp/simple6" + } + } + + state_store "simple6_fs" { + provider "simple6" {} + + workspace_dir = "states" + } +} + +variable "name" { + default = "world" +} + +resource "terraform_data" "my-data" { + input = "hello ${var.name}" +} + +output "greeting" { + value = resource.terraform_data.my-data.output +} diff --git a/internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/states/default/terraform.tfstate b/internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/states/default/terraform.tfstate new file mode 100644 index 000000000000..4feaaed87a08 --- /dev/null +++ b/internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/states/default/terraform.tfstate @@ -0,0 +1,40 @@ +{ + "version": 4, + "terraform_version": "1.15.0", + "serial": 1, + "lineage": "9e13d881-e480-7a63-d47a-b4f5224e6743", + "outputs": { + "greeting": { + "value": "hello world", + "type": "string" + } + }, + "resources": [ + { + "mode": "managed", + "type": "terraform_data", + "name": "my-data", + "provider": "provider[\"terraform.io/builtin/terraform\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "d71fb368-2ba1-fb4c-5bd9-6a2b7f05d60c", + "input": { + "value": "hello world", + "type": "string" + }, + "output": { + "value": "hello world", + "type": "string" + }, + "triggers_replace": null + }, + "sensitive_attributes": [], + "identity_schema_version": 0 + } + ] + } + ], + "check_results": null +} \ No newline at end of file From 7a97fa861d26d37bc3138910c4b1840002904c48 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 12 Nov 2025 14:10:29 +0000 Subject: [PATCH 14/16] test: Add E2E test demonstrating `output` command used with PSS --- .../e2etest/pluggable_state_store_test.go | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/internal/command/e2etest/pluggable_state_store_test.go b/internal/command/e2etest/pluggable_state_store_test.go index 09f802ce8cca..9114da364ffc 100644 --- a/internal/command/e2etest/pluggable_state_store_test.go +++ b/internal/command/e2etest/pluggable_state_store_test.go @@ -197,3 +197,72 @@ resource "terraform_data" "my-data" { t.Errorf("wrong result, diff:\n%s", diff) } } + +// Tests using the `terraform output` command in combination with pluggable state storage: +// > `terraform output` +// > `terraform output ` +func TestPrimary_stateStore_outputCmd(t *testing.T) { + + if !canRunGoBuild { + // We're running in a separate-build-then-run context, so we can't + // currently execute this test which depends on being able to build + // new executable at runtime. + // + // (See the comment on canRunGoBuild's declaration for more information.) + t.Skip("can't run without building a new provider executable") + } + + t.Setenv(e2e.TestExperimentFlag, "true") + tfBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform") + + fixturePath := filepath.Join("testdata", "initialized-directory-with-state-store-fs") + tf := e2e.NewBinary(t, tfBin, fixturePath) + + workspaceDirName := "states" // see test fixture value for workspace_dir + + // In order to test integration with PSS we need a provider plugin implementing a state store. + // Here will build the simple6 (built with protocol v6) provider, which implements PSS. + simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6") + simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider) + + // Move the provider binaries into the correct .terraform/providers/ directory + // that will contain provider binaries in an initialized working directory. + platform := getproviders.CurrentPlatform.String() + if err := os.MkdirAll(tf.Path(".terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/", platform), os.ModePerm); err != nil { + t.Fatal(err) + } + if err := os.Rename(simple6ProviderExe, tf.Path(".terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/", platform, "terraform-provider-simple6")); err != nil { + t.Fatal(err) + } + + // Assert that the test starts with the default state present from test fixtures + defaultStateId := "default" + fi, err := os.Stat(path.Join(tf.WorkDir(), workspaceDirName, defaultStateId, "terraform.tfstate")) + if err != nil { + t.Fatalf("failed to open default workspace's state file: %s", err) + } + if fi.Size() == 0 { + t.Fatal("default workspace's state file should not have size 0 bytes") + } + + //// List all outputs: terraform output + stdout, stderr, err := tf.Run("output", "-no-color") + if err != nil { + t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) + } + expectedMsg := "greeting = \"hello world\"\n" // See the test fixture files + if stdout != expectedMsg { + t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) + } + + //// View a specific output: terraform output + outputName := "greeting" + stdout, stderr, err = tf.Run("output", outputName, "-no-color") + if err != nil { + t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) + } + expectedMsg = "\"hello world\"\n" // Only the value is outputted, no name present + if stdout != expectedMsg { + t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) + } +} From fab00aae3af1a8e58b9a491b1b8fa4fcf14d1808 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 12 Nov 2025 14:11:01 +0000 Subject: [PATCH 15/16] test: Add E2E test demonstrating `show` command used with PSS --- .../e2etest/pluggable_state_store_test.go | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/internal/command/e2etest/pluggable_state_store_test.go b/internal/command/e2etest/pluggable_state_store_test.go index 9114da364ffc..227f196c454f 100644 --- a/internal/command/e2etest/pluggable_state_store_test.go +++ b/internal/command/e2etest/pluggable_state_store_test.go @@ -266,3 +266,86 @@ func TestPrimary_stateStore_outputCmd(t *testing.T) { t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) } } + +// Tests using the `terraform show` command in combination with pluggable state storage +// > `terraform show` +// > `terraform show ` +// > `terraform show ` // TODO +func TestPrimary_stateStore_showCmd(t *testing.T) { + + if !canRunGoBuild { + // We're running in a separate-build-then-run context, so we can't + // currently execute this test which depends on being able to build + // new executable at runtime. + // + // (See the comment on canRunGoBuild's declaration for more information.) + t.Skip("can't run without building a new provider executable") + } + + t.Setenv(e2e.TestExperimentFlag, "true") + tfBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform") + + fixturePath := filepath.Join("testdata", "initialized-directory-with-state-store-fs") + tf := e2e.NewBinary(t, tfBin, fixturePath) + + workspaceDirName := "states" // see test fixture value for workspace_dir + + // In order to test integration with PSS we need a provider plugin implementing a state store. + // Here will build the simple6 (built with protocol v6) provider, which implements PSS. + simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6") + simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider) + + // Move the provider binaries into the correct .terraform/providers/ directory + // that will contain provider binaries in an initialized working directory. + platform := getproviders.CurrentPlatform.String() + if err := os.MkdirAll(tf.Path(".terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/", platform), os.ModePerm); err != nil { + t.Fatal(err) + } + if err := os.Rename(simple6ProviderExe, tf.Path(".terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/", platform, "terraform-provider-simple6")); err != nil { + t.Fatal(err) + } + + // Assert that the test starts with the default state present from test fixtures + defaultStateId := "default" + fi, err := os.Stat(path.Join(tf.WorkDir(), workspaceDirName, defaultStateId, "terraform.tfstate")) + if err != nil { + t.Fatalf("failed to open default workspace's state file: %s", err) + } + if fi.Size() == 0 { + t.Fatal("default workspace's state file should not have size 0 bytes") + } + + //// Show state: terraform state + stdout, stderr, err := tf.Run("show", "-no-color") + if err != nil { + t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) + } + expectedMsg := `# terraform_data.my-data: +resource "terraform_data" "my-data" { + id = "d71fb368-2ba1-fb4c-5bd9-6a2b7f05d60c" + input = "hello world" + output = "hello world" +} + + +Outputs: + +greeting = "hello world" +` // See the test fixture folder's state file + + if diff := cmp.Diff(stdout, expectedMsg); diff != "" { + t.Errorf("wrong result, diff:\n%s", diff) + } + + //// Show state: terraform show + path := fmt.Sprintf("./%s/%s/terraform.tfstate", workspaceDirName, defaultStateId) + stdout, stderr, err = tf.Run("show", path, "-no-color") + if err != nil { + t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) + } + if diff := cmp.Diff(stdout, expectedMsg); diff != "" { + t.Errorf("wrong result, diff:\n%s", diff) + } + + // TODO: Show plan file: terraform show +} From e9ebdc8f320f7e3f3f6736c2271494ef14b0bf55 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 12 Nov 2025 14:11:18 +0000 Subject: [PATCH 16/16] docs: Fix code comment --- internal/command/e2etest/pluggable_state_store_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/command/e2etest/pluggable_state_store_test.go b/internal/command/e2etest/pluggable_state_store_test.go index 227f196c454f..e014800ca9ab 100644 --- a/internal/command/e2etest/pluggable_state_store_test.go +++ b/internal/command/e2etest/pluggable_state_store_test.go @@ -149,7 +149,7 @@ func TestPrimary_stateStore_stateCmds(t *testing.T) { simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6") simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider) - // Move the provider binaries into the correct directory .terraform/providers/ directory + // Move the provider binaries into the correct .terraform/providers/ directory // that will contain provider binaries in an initialized working directory. platform := getproviders.CurrentPlatform.String() if err := os.MkdirAll(tf.Path(".terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/", platform), os.ModePerm); err != nil {