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..09f802ce8cca --- /dev/null +++ b/internal/command/e2etest/pluggable_state_store_test.go @@ -0,0 +1,199 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package e2etest + +import ( + "fmt" + "os" + "path" + "path/filepath" + "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 + // 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") + 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. + 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) + } + 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: terraform workspace new + 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) + } + 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: : 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) + } + 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: terraform workspace select + 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) + } + + //// 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) + } + + //// 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) + } +} + +// 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/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) 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" } } 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 diff --git a/internal/command/workspace_command_test.go b/internal/command/workspace_command_test.go index 5e35ccef413c..937e70f31a1b 100644 --- a/internal/command/workspace_command_test.go +++ b/internal/command/workspace_command_test.go @@ -4,7 +4,7 @@ package command import ( - "io/ioutil" + "fmt" "os" "path/filepath" "strings" @@ -16,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() @@ -114,7 +262,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 +310,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 +493,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) } diff --git a/internal/command/workspace_new.go b/internal/command/workspace_new.go index d36c76efef61..d217fd38e945 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,38 @@ func (c *WorkspaceNewCommand) Run(args []string) int { } } - _, sDiags := b.StateMgr(workspace) + // Create the new workspace + // + // 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()) return 1 } + if l, ok := b.(*local.Local); ok { + if _, ok := l.Backend.(*backendPluggable.Pluggable); ok { + // 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. + // 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)) 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() 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