From 0548a422bf2a51c464a2199695b043be297ae47d Mon Sep 17 00:00:00 2001 From: Sarah French Date: Tue, 11 Nov 2025 17:11:14 +0000 Subject: [PATCH 01/15] 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 4a3ed2b20b1af34ff2059076a26319fa6946624a Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 21 Nov 2025 17:41:05 +0000 Subject: [PATCH 02/15] test: Update `mockPluggableStateStorageProvider` to log a warning during tests where the values in `MockStates` aren't compatible with the `ReadStateBytesFn` default function. Make existing test set an appropriate value in `MockStates`. --- internal/command/autocomplete_test.go | 4 +-- internal/command/init_test.go | 42 +++++++++++++++------------ 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/internal/command/autocomplete_test.go b/internal/command/autocomplete_test.go index c13be9fbb06d..97d84a30c3d0 100644 --- a/internal/command/autocomplete_test.go +++ b/internal/command/autocomplete_test.go @@ -42,7 +42,7 @@ func TestMetaCompletePredictWorkspaceName(t *testing.T) { t.Chdir(td) // Set up pluggable state store provider mock - mockProvider := mockPluggableStateStorageProvider() + mockProvider := mockPluggableStateStorageProvider(t) // Mock the existence of workspaces mockProvider.MockStates = map[string]interface{}{ "default": true, @@ -89,7 +89,7 @@ func TestMetaCompletePredictWorkspaceName(t *testing.T) { t.Chdir(td) // Set up pluggable state store provider mock - mockProvider := mockPluggableStateStorageProvider() + mockProvider := mockPluggableStateStorageProvider(t) // No workspaces exist in the mock mockProvider.MockStates = map[string]interface{}{} mockProviderAddress := addrs.NewDefaultProvider("test") diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 829b73734b27..b245863acd46 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -3238,7 +3238,7 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { testCopyDir(t, testFixturePath("init-with-state-store"), td) t.Chdir(td) - mockProvider := mockPluggableStateStorageProvider() + mockProvider := mockPluggableStateStorageProvider(t) mockProviderAddress := addrs.NewDefaultProvider("test") providerSource, close := newMockProviderSource(t, map[string][]string{ // The test fixture config has no version constraints, so the latest version will @@ -3325,7 +3325,7 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { testCopyDir(t, testFixturePath("init-with-state-store"), td) t.Chdir(td) - mockProvider := mockPluggableStateStorageProvider() + mockProvider := mockPluggableStateStorageProvider(t) mockProviderAddress := addrs.NewDefaultProvider("test") providerSource, close := newMockProviderSource(t, map[string][]string{ "hashicorp/test": {"1.0.0"}, @@ -3374,7 +3374,7 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { testCopyDir(t, testFixturePath("init-with-state-store"), td) t.Chdir(td) - mockProvider := mockPluggableStateStorageProvider() + mockProvider := mockPluggableStateStorageProvider(t) mockProviderAddress := addrs.NewDefaultProvider("test") providerSource, close := newMockProviderSource(t, map[string][]string{ "hashicorp/test": {"1.0.0"}, @@ -3429,7 +3429,7 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { customWorkspace := "my-custom-workspace" t.Setenv(WorkspaceNameEnvVar, customWorkspace) - mockProvider := mockPluggableStateStorageProvider() + mockProvider := mockPluggableStateStorageProvider(t) mockProviderAddress := addrs.NewDefaultProvider("test") providerSource, close := newMockProviderSource(t, map[string][]string{ "hashicorp/test": {"1.0.0"}, @@ -3492,7 +3492,7 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { testCopyDir(t, testFixturePath("init-with-state-store"), td) t.Chdir(td) - mockProvider := mockPluggableStateStorageProvider() + mockProvider := mockPluggableStateStorageProvider(t) mockProvider.GetStatesResponse = &providers.GetStatesResponse{ States: []string{ "foobar1", @@ -3583,7 +3583,7 @@ func TestInit_stateStore_configUnchanged(t *testing.T) { testCopyDir(t, testFixturePath("state-store-unchanged"), td) t.Chdir(td) - mockProvider := mockPluggableStateStorageProvider() + mockProvider := mockPluggableStateStorageProvider(t) // If the working directory was previously initialized successfully then at least // one workspace is guaranteed to exist when a user is re-running init with no config // changes since last init. So this test says `default` exists. @@ -3670,11 +3670,11 @@ func TestInit_stateStore_configChanges(t *testing.T) { testCopyDir(t, testFixturePath("state-store-changed/store-config"), td) t.Chdir(td) - mockProvider := mockPluggableStateStorageProvider() + mockProvider := mockPluggableStateStorageProvider(t) // The previous init implied by this test scenario would have created this. mockProvider.GetStatesResponse = &providers.GetStatesResponse{States: []string{"default"}} - mockProvider.MockStates = map[string]interface{}{"default": true} + mockProvider.MockStates = map[string]interface{}{"default": []byte(`{"version": 4,"terraform_version":"1.15.0","serial": 1,"lineage": "","outputs": {},"resources": [],"checks":[]}`)} mockProviderAddress := addrs.NewDefaultProvider("test") providerSource, close := newMockProviderSource(t, map[string][]string{ @@ -3758,7 +3758,7 @@ func TestInit_stateStore_configChanges(t *testing.T) { testCopyDir(t, testFixturePath("state-store-changed/store-config"), td) t.Chdir(td) - mockProvider := mockPluggableStateStorageProvider() + mockProvider := mockPluggableStateStorageProvider(t) mockProvider.GetStatesResponse = &providers.GetStatesResponse{States: []string{"default"}} // The previous init implied by this test scenario would have created the default workspace. mockProviderAddress := addrs.NewDefaultProvider("test") providerSource, close := newMockProviderSource(t, map[string][]string{ @@ -3808,7 +3808,7 @@ func TestInit_stateStore_configChanges(t *testing.T) { testCopyDir(t, testFixturePath("state-store-changed/provider-config"), td) t.Chdir(td) - mockProvider := mockPluggableStateStorageProvider() + mockProvider := mockPluggableStateStorageProvider(t) mockProvider.GetStatesResponse = &providers.GetStatesResponse{States: []string{"default"}} // The previous init implied by this test scenario would have created the default workspace. mockProviderAddress := addrs.NewDefaultProvider("test") providerSource, close := newMockProviderSource(t, map[string][]string{ @@ -3857,7 +3857,7 @@ func TestInit_stateStore_configChanges(t *testing.T) { testCopyDir(t, testFixturePath("state-store-changed/state-store-type"), td) t.Chdir(td) - mockProvider := mockPluggableStateStorageProvider() + mockProvider := mockPluggableStateStorageProvider(t) storeName := "test_store" otherStoreName := "test_otherstore" // Make the provider report that it contains a 2nd storage implementation with the above name @@ -3909,11 +3909,11 @@ func TestInit_stateStore_configChanges(t *testing.T) { testCopyDir(t, testFixturePath("state-store-changed/provider-used"), td) t.Chdir(td) - mockProvider := mockPluggableStateStorageProvider() + mockProvider := mockPluggableStateStorageProvider(t) mockProvider.GetStatesResponse = &providers.GetStatesResponse{States: []string{"default"}} // The previous init implied by this test scenario would have created the default workspace. // Make a mock that implies its name is test2 based on returned schemas - mockProvider2 := mockPluggableStateStorageProvider() + mockProvider2 := mockPluggableStateStorageProvider(t) mockProvider2.GetProviderSchemaResponse.StateStores["test2_store"] = mockProvider.GetProviderSchemaResponse.StateStores["test_store"] delete(mockProvider2.GetProviderSchemaResponse.StateStores, "test_store") @@ -3974,7 +3974,7 @@ func TestInit_stateStore_providerUpgrade(t *testing.T) { testCopyDir(t, testFixturePath("state-store-changed/provider-upgraded"), td) t.Chdir(td) - mockProvider := mockPluggableStateStorageProvider() + mockProvider := mockPluggableStateStorageProvider(t) mockProviderAddress := addrs.NewDefaultProvider("test") providerSource, close := newMockProviderSource(t, map[string][]string{ "hashicorp/test": {"1.2.3", "9.9.9"}, // 1.2.3 is the version used in the backend state file, 9.9.9 is the version being upgraded to @@ -4023,7 +4023,7 @@ func TestInit_stateStore_unset(t *testing.T) { testCopyDir(t, testFixturePath("init-state-store"), td) t.Chdir(td) - mockProvider := mockPluggableStateStorageProvider() + mockProvider := mockPluggableStateStorageProvider(t) storeName := "test_store" otherStoreName := "test_otherstore" // Make the provider report that it contains a 2nd storage implementation with the above name @@ -4121,7 +4121,7 @@ func TestInit_stateStore_unset_withoutProviderRequirements(t *testing.T) { testCopyDir(t, testFixturePath("init-state-store"), td) t.Chdir(td) - mockProvider := mockPluggableStateStorageProvider() + mockProvider := mockPluggableStateStorageProvider(t) storeName := "test_store" otherStoreName := "test_otherstore" // Make the provider report that it contains a 2nd storage implementation with the above name @@ -4355,7 +4355,7 @@ func expectedPackageInstallPath(name, version string, exe bool) string { )) } -func mockPluggableStateStorageProvider() *testing_provider.MockProvider { +func mockPluggableStateStorageProvider(t *testing.T) *testing_provider.MockProvider { // Create a mock provider to use for PSS // Get mock provider factory to be used during init // @@ -4412,8 +4412,14 @@ func mockPluggableStateStorageProvider() *testing_provider.MockProvider { mock.ReadStateBytesFn = func(req providers.ReadStateBytesRequest) providers.ReadStateBytesResponse { state := []byte{} if v, exist := mock.MockStates[req.StateId]; exist { - if s, ok := v.([]byte); ok { + s, ok := v.([]byte) + if ok { state = s + } else { + // Test setup is incorrect if this happens + t.Logf("Warning: The mock provider is set up to return state bytes from the MockStates map, but the mock encountered a value that wasn't a slice of bytes: %#v", + mock.MockStates, + ) } } return providers.ReadStateBytesResponse{ From 16704492a9c7e33c5a774a5b90060b3e55333cce Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 21 Nov 2025 17:44:30 +0000 Subject: [PATCH 03/15] test: Update `mockPluggableStateStorageProvider` helper to include a resource schema --- internal/command/init_test.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/internal/command/init_test.go b/internal/command/init_test.go index b245863acd46..99a035bcfa99 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -4371,8 +4371,17 @@ func mockPluggableStateStorageProvider(t *testing.T) *testing_provider.MockProvi }, }, }, - DataSources: map[string]providers.Schema{}, - ResourceTypes: map[string]providers.Schema{}, + DataSources: map[string]providers.Schema{}, + ResourceTypes: map[string]providers.Schema{ + "test_instance": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "input": {Type: cty.String, Optional: true}, + "id": {Type: cty.String, Computed: true}, + }, + }, + }, + }, ListResourceTypes: map[string]providers.Schema{}, StateStores: map[string]providers.Schema{ pssName: { From 2874ba37e9ecce1293c8bf688a40c6b85c996496 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 21 Nov 2025 17:59:43 +0000 Subject: [PATCH 04/15] test: Add command-level test for `state list` showing integration with pluggable state storage code. --- internal/command/state_list_test.go | 82 +++++++++++++++++++ .../.terraform.lock.hcl | 6 ++ .../hashicorp/simple6/0.0.1/.gitkeep | 0 .../.terraform/terraform.tfstate | 19 +++++ .../testdata/state-list-state-store/main.tf | 25 ++++++ .../state-list-state-store/terraform.tfstate | 40 +++++++++ 6 files changed, 172 insertions(+) create mode 100644 internal/command/testdata/state-list-state-store/.terraform.lock.hcl create mode 100644 internal/command/testdata/state-list-state-store/.terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/.gitkeep create mode 100644 internal/command/testdata/state-list-state-store/.terraform/terraform.tfstate create mode 100644 internal/command/testdata/state-list-state-store/main.tf create mode 100644 internal/command/testdata/state-list-state-store/terraform.tfstate diff --git a/internal/command/state_list_test.go b/internal/command/state_list_test.go index c667c11754c8..494e21dbf978 100644 --- a/internal/command/state_list_test.go +++ b/internal/command/state_list_test.go @@ -4,10 +4,15 @@ package command import ( + "bytes" "strings" "testing" "github.com/hashicorp/cli" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statefile" ) func TestStateList(t *testing.T) { @@ -153,6 +158,83 @@ func TestStateList_backendCustomState(t *testing.T) { } } +// Tests using `terraform state list` subcommand in combination with pluggable state storage +// +// Note: Whereas other tests in this file use the local backend and require a state file in the test fixures, +// with pluggable state storage we can define the state via the mocked provider. +func TestStateList_stateStore(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("state-list-state-store"), td) + t.Chdir(td) + + // Get bytes describing a state containing a resource + state := states.NewState() + rootModule := state.RootModule() + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "ami": "bar", + "network_interface": [{ + "device_index": 0, + "description": "Main network interface" + }] + }`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + var stateBuf bytes.Buffer + if err := statefile.Write(statefile.New(state, "", 1), &stateBuf); err != nil { + t.Fatal(err) + } + + // Create a mock that contains a persisted "default" state that uses the bytes from above. + mockProvider := mockPluggableStateStorageProvider(t) + mockProvider.MockStates = map[string]interface{}{ + "default": stateBuf.Bytes(), + } + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + ui := cli.NewMockUi() + c := &StateListCommand{ + Meta: Meta{ + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: providerSource, + Ui: ui, + }, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // Test that outputs were displayed + expected := "test_instance.foo\n" + actual := ui.OutputWriter.String() + if actual != expected { + t.Fatalf("Expected:\n%q\n\nTo equal: %q", actual, expected) + } +} + func TestStateList_backendOverrideState(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() diff --git a/internal/command/testdata/state-list-state-store/.terraform.lock.hcl b/internal/command/testdata/state-list-state-store/.terraform.lock.hcl new file mode 100644 index 000000000000..e5c03757a7fa --- /dev/null +++ b/internal/command/testdata/state-list-state-store/.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/test" { + version = "1.2.3" +} diff --git a/internal/command/testdata/state-list-state-store/.terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/.gitkeep b/internal/command/testdata/state-list-state-store/.terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/internal/command/testdata/state-list-state-store/.terraform/terraform.tfstate b/internal/command/testdata/state-list-state-store/.terraform/terraform.tfstate new file mode 100644 index 000000000000..4f96aa73ee7d --- /dev/null +++ b/internal/command/testdata/state-list-state-store/.terraform/terraform.tfstate @@ -0,0 +1,19 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "state_store": { + "type": "test_store", + "config": { + "value": "foobar" + }, + "provider": { + "version": "1.2.3", + "source": "registry.terraform.io/hashicorp/test", + "config": { + "region": null + } + }, + "hash": 4158988729 + } +} \ No newline at end of file diff --git a/internal/command/testdata/state-list-state-store/main.tf b/internal/command/testdata/state-list-state-store/main.tf new file mode 100644 index 000000000000..84f54d6c1a4a --- /dev/null +++ b/internal/command/testdata/state-list-state-store/main.tf @@ -0,0 +1,25 @@ +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } + + state_store "test_store" { + provider "test" {} + + value = "foobar" + } +} + +variable "name" { + default = "world" +} + +resource "test_instance" "my-data" { + input = "hello ${var.name}" +} + +output "greeting" { + value = resource.terraform_data.my-data.output +} diff --git a/internal/command/testdata/state-list-state-store/terraform.tfstate b/internal/command/testdata/state-list-state-store/terraform.tfstate new file mode 100644 index 000000000000..4feaaed87a08 --- /dev/null +++ b/internal/command/testdata/state-list-state-store/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 fb446c0279c32ccf05d839b153ab2e7dae07a45c Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 21 Nov 2025 17:59:55 +0000 Subject: [PATCH 05/15] test: Add command-level test for `state show` showing integration with pluggable state storage code. --- internal/command/state_show_test.go | 82 +++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/internal/command/state_show_test.go b/internal/command/state_show_test.go index e978bed164e2..c345454c6266 100644 --- a/internal/command/state_show_test.go +++ b/internal/command/state_show_test.go @@ -4,13 +4,16 @@ package command import ( + "bytes" "strings" "testing" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/terminal" "github.com/zclconf/go-cty/cty" ) @@ -269,6 +272,85 @@ func TestStateShow_configured_provider(t *testing.T) { } } +// Tests using `terraform state show` subcommand in combination with pluggable state storage +// +// Note: Whereas other tests in this file use the local backend and require a state file in the test fixures, +// with pluggable state storage we can define the state via the mocked provider. +func TestStateShow_stateStore(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("state-list-state-store"), td) + t.Chdir(td) + + // Get bytes describing a state containing a resource + state := states.NewState() + rootModule := state.RootModule() + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "input": "foobar" + }`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + var stateBuf bytes.Buffer + if err := statefile.Write(statefile.New(state, "", 1), &stateBuf); err != nil { + t.Fatalf("error during test setup: %s", err) + } + + // Create a mock that contains a persisted "default" state that uses the bytes from above. + mockProvider := mockPluggableStateStorageProvider(t) + mockProvider.MockStates = map[string]interface{}{ + "default": stateBuf.Bytes(), + } + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + ui := cli.NewMockUi() + streams, done := terminal.StreamsForTesting(t) + c := &StateShowCommand{ + Meta: Meta{ + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: providerSource, + Ui: ui, + Streams: streams, + }, + } + + // `terraform show` command specifying a given resource addr + expectedResourceAddr := "test_instance.foo" + args := []string{expectedResourceAddr} + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) + } + + // Test that outputs were displayed + expected := "# test_instance.foo:\nresource \"test_instance\" \"foo\" {\n input = \"foobar\"\n}\n" + actual := output.Stdout() + if actual != expected { + t.Fatalf("Expected:\n%q\n\nTo equal: %q", actual, expected) + } +} + const testStateShowOutput = ` # test_instance.foo: resource "test_instance" "foo" { From db2b662204bd17aeedf55daedb834ae5fe36504c Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 21 Nov 2025 18:48:43 +0000 Subject: [PATCH 06/15] test: Add command-level test for `state pull` showing integration with pluggable state storage code. --- internal/command/state_pull_test.go | 83 +++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/internal/command/state_pull_test.go b/internal/command/state_pull_test.go index a4079152aa29..55d2983ea68a 100644 --- a/internal/command/state_pull_test.go +++ b/internal/command/state_pull_test.go @@ -10,6 +10,11 @@ import ( "testing" "github.com/hashicorp/cli" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statefile" + "github.com/hashicorp/terraform/internal/terminal" ) func TestStatePull(t *testing.T) { @@ -43,6 +48,84 @@ func TestStatePull(t *testing.T) { } } +// Tests using `terraform state pull` subcommand in combination with pluggable state storage +// +// Note: Whereas other tests in this file use the local backend and require a state file in the test fixures, +// with pluggable state storage we can define the state via the mocked provider. +func TestStatePull_stateStore(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("state-list-state-store"), td) + t.Chdir(td) + + // Get bytes describing a state containing a resource + state := states.NewState() + rootModule := state.RootModule() + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "input": "foobar" + }`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + var stateBuf bytes.Buffer + if err := statefile.Write(statefile.New(state, "", 1), &stateBuf); err != nil { + t.Fatalf("error during test setup: %s", err) + } + stateBytes := stateBuf.Bytes() + + // Create a mock that contains a persisted "default" state that uses the bytes from above. + mockProvider := mockPluggableStateStorageProvider(t) + mockProvider.MockStates = map[string]interface{}{ + "default": stateBytes, + } + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + ui := cli.NewMockUi() + streams, _ := terminal.StreamsForTesting(t) + c := &StatePullCommand{ + Meta: Meta{ + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: providerSource, + Ui: ui, + Streams: streams, + }, + } + + // `terraform show` command specifying a given resource addr + expectedResourceAddr := "test_instance.foo" + args := []string{expectedResourceAddr} + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // Test that the state in the output matches the original state + actual := ui.OutputWriter.Bytes() + if bytes.Equal(actual, stateBytes) { + t.Fatalf("expected:\n%s\n\nto include: %q", actual, stateBytes) + } +} + func TestStatePull_noState(t *testing.T) { tmp := t.TempDir() t.Chdir(tmp) From ac088bf81e8173b2ed745e18d6e08ad6514a1443 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 21 Nov 2025 19:07:56 +0000 Subject: [PATCH 07/15] test: Add command-level test for `state identities` showing integration with pluggable state storage code. --- internal/command/state_identities_test.go | 74 +++++++++++++++++++ .../.terraform.lock.hcl | 6 ++ .../.terraform/terraform.tfstate | 19 +++++ .../state-identities-state-store/main.tf | 13 ++++ 4 files changed, 112 insertions(+) create mode 100644 internal/command/testdata/state-identities-state-store/.terraform.lock.hcl create mode 100644 internal/command/testdata/state-identities-state-store/.terraform/terraform.tfstate create mode 100644 internal/command/testdata/state-identities-state-store/main.tf diff --git a/internal/command/state_identities_test.go b/internal/command/state_identities_test.go index 64dfb5c1593c..b6d7c59c6528 100644 --- a/internal/command/state_identities_test.go +++ b/internal/command/state_identities_test.go @@ -4,6 +4,7 @@ package command import ( + "bytes" "encoding/json" "os" "path/filepath" @@ -11,6 +12,9 @@ import ( "testing" "github.com/hashicorp/cli" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states/statefile" ) func TestStateIdentities(t *testing.T) { @@ -423,3 +427,73 @@ func TestStateIdentities_modules(t *testing.T) { }) } + +func TestStateIdentities_stateStore(t *testing.T) { + // We need configuration present to force pluggable state storage to be used + td := t.TempDir() + testCopyDir(t, testFixturePath("state-identities-state-store"), td) + t.Chdir(td) + + // Get a state file, that contains identity information,as bytes + state := testStateWithIdentity() + var stateBuf bytes.Buffer + if err := statefile.Write(statefile.New(state, "", 1), &stateBuf); err != nil { + t.Fatalf("error during test setup: %s", err) + } + stateBytes := stateBuf.Bytes() + + // Create a mock that contains a persisted "default" state that uses the bytes from above. + mockProvider := mockPluggableStateStorageProvider(t) + mockProvider.MockStates = map[string]interface{}{ + "default": stateBytes, + } + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + ui := cli.NewMockUi() + c := &StateIdentitiesCommand{ + Meta: Meta{ + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: providerSource, + Ui: ui, + }, + } + + args := []string{"-json"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // Test that outputs were displayed + expected := `{ + "test_instance.bar": { + "id": "my-bar-id" + }, + "test_instance.foo": { + "id": "my-foo-id" + } +} +` + actual := ui.OutputWriter.String() + + // Normalize JSON strings + var expectedJSON, actualJSON map[string]interface{} + if err := json.Unmarshal([]byte(expected), &expectedJSON); err != nil { + t.Fatalf("Failed to unmarshal expected JSON: %s", err) + } + if err := json.Unmarshal([]byte(actual), &actualJSON); err != nil { + t.Fatalf("Failed to unmarshal actual JSON: %s", err) + } + + if !reflect.DeepEqual(expectedJSON, actualJSON) { + t.Fatalf("Expected:\n%q\n\nTo equal: %q", expected, actual) + } +} diff --git a/internal/command/testdata/state-identities-state-store/.terraform.lock.hcl b/internal/command/testdata/state-identities-state-store/.terraform.lock.hcl new file mode 100644 index 000000000000..e5c03757a7fa --- /dev/null +++ b/internal/command/testdata/state-identities-state-store/.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/test" { + version = "1.2.3" +} diff --git a/internal/command/testdata/state-identities-state-store/.terraform/terraform.tfstate b/internal/command/testdata/state-identities-state-store/.terraform/terraform.tfstate new file mode 100644 index 000000000000..4f96aa73ee7d --- /dev/null +++ b/internal/command/testdata/state-identities-state-store/.terraform/terraform.tfstate @@ -0,0 +1,19 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "state_store": { + "type": "test_store", + "config": { + "value": "foobar" + }, + "provider": { + "version": "1.2.3", + "source": "registry.terraform.io/hashicorp/test", + "config": { + "region": null + } + }, + "hash": 4158988729 + } +} \ No newline at end of file diff --git a/internal/command/testdata/state-identities-state-store/main.tf b/internal/command/testdata/state-identities-state-store/main.tf new file mode 100644 index 000000000000..34b58fdc0e2e --- /dev/null +++ b/internal/command/testdata/state-identities-state-store/main.tf @@ -0,0 +1,13 @@ +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } + + state_store "test_store" { + provider "test" {} + + value = "foobar" + } +} From 85b4a834ac69db48711886138009c210ff89e924 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 24 Nov 2025 11:49:01 +0000 Subject: [PATCH 08/15] test: Add command-level test for `state rm` showing integration with pluggable state storage code. --- internal/command/state_rm_test.go | 106 ++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/internal/command/state_rm_test.go b/internal/command/state_rm_test.go index 7aece89f7d4a..c940168ee0c9 100644 --- a/internal/command/state_rm_test.go +++ b/internal/command/state_rm_test.go @@ -4,6 +4,7 @@ package command import ( + "bytes" "os" "path/filepath" "strings" @@ -12,7 +13,9 @@ import ( "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statefile" ) func TestStateRm(t *testing.T) { @@ -82,6 +85,109 @@ func TestStateRm(t *testing.T) { testStateOutput(t, backups[0], testStateRmOutputOriginal) } +func TestStateRm_stateStore(t *testing.T) { + // Create a temporary working directory + td := t.TempDir() + testCopyDir(t, testFixturePath("state-list-state-store"), td) + t.Chdir(td) + + // Get bytes describing a state containing resources + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }) + var stateBuf bytes.Buffer + if err := statefile.Write(statefile.New(state, "", 1), &stateBuf); err != nil { + t.Fatalf("error during test setup: %s", err) + } + + // Create a mock that contains a persisted "default" state that uses the bytes from above. + mockProvider := mockPluggableStateStorageProvider(t) + mockProvider.MockStates = map[string]interface{}{ + "default": stateBuf.Bytes(), + } + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + // Make the mock assert that the removed resource is not present when the new state is persisted + keptResource := "test_instance.bar" + removedResource := "test_instance.foo" + mockProvider.WriteStateBytesFn = func(req providers.WriteStateBytesRequest) providers.WriteStateBytesResponse { + r := bytes.NewReader(req.Bytes) + file, err := statefile.Read(r) + if err != nil { + t.Fatal(err) + } + + root := file.State.Modules[""] + if _, ok := root.Resources[keptResource]; !ok { + t.Fatalf("expected the new state to keep the %s resource, but it couldn't be found", keptResource) + } + if _, ok := root.Resources[removedResource]; ok { + t.Fatalf("expected the %s resource to be removed from the state, but it is present", removedResource) + } + return providers.WriteStateBytesResponse{} + } + + ui := new(cli.MockUi) + view, _ := testView(t) + c := &StateRmCommand{ + StateMeta{ + Meta: Meta{ + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: providerSource, + Ui: ui, + View: view, + }, + }, + } + + args := []string{ + removedResource, + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // See the mock definition above for logic that asserts what the new state will look like after removing the resource. +} + func TestStateRmNotChildModule(t *testing.T) { state := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( From cebf4a5ea504f7ca6b7320ef9ed65da48f9377cf Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 24 Nov 2025 12:58:03 +0000 Subject: [PATCH 09/15] test: Add command-level test for `state mv` showing integration with pluggable state storage code. --- internal/command/state_mv_test.go | 135 ++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/internal/command/state_mv_test.go b/internal/command/state_mv_test.go index 9d6c2aa9034b..a3788faf79ac 100644 --- a/internal/command/state_mv_test.go +++ b/internal/command/state_mv_test.go @@ -4,6 +4,8 @@ package command import ( + "bytes" + "encoding/json" "fmt" "os" "path/filepath" @@ -14,7 +16,9 @@ import ( "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statefile" ) func TestStateMv(t *testing.T) { @@ -153,6 +157,137 @@ func TestStateMv(t *testing.T) { } +func TestStateMv_stateStore(t *testing.T) { + // Create a temporary working directory + td := t.TempDir() + testCopyDir(t, testFixturePath("state-list-state-store"), td) + t.Chdir(td) + + // Get bytes describing a state containing resources + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "baz", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"baz","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }) + var stateBuf bytes.Buffer + if err := statefile.Write(statefile.New(state, "", 1), &stateBuf); err != nil { + t.Fatalf("error during test setup: %s", err) + } + + // Create a mock that contains a persisted "default" state that uses the bytes from above. + mockProvider := mockPluggableStateStorageProvider(t) + mockProvider.MockStates = map[string]interface{}{ + "default": stateBuf.Bytes(), + } + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + // Make the mock assert that the resource has been moved when the new state is persisted + oldAddr := "test_instance.foo" + newAddr := "test_instance.bar" + mockProvider.WriteStateBytesFn = func(req providers.WriteStateBytesRequest) providers.WriteStateBytesResponse { + r := bytes.NewReader(req.Bytes) + file, err := statefile.Read(r) + if err != nil { + t.Fatal(err) + } + + root := file.State.Modules[""] + if _, ok := root.Resources[oldAddr]; ok { + t.Fatalf("expected the new state to have moved the %s resource to the new addr %s, but the old addr is still present", + newAddr, + oldAddr, + ) + } + resource, ok := root.Resources[newAddr] + if !ok { + t.Fatalf("expected the moved resource to be at addr %s, but it isn't present", newAddr) + } + + // Check that the moved resource has the same state. + var key addrs.InstanceKey + type attrsJson struct { + Id string `json:"id"` + Foo string `json:"foo"` + Bar string `json:"bar"` + } + var data attrsJson + attrs := resource.Instances[key].Current.AttrsJSON + err = json.Unmarshal(attrs, &data) + if err != nil { + t.Fatal(err) + } + expectedData := attrsJson{ + Id: "foo", + Foo: "value", + Bar: "value", + } + if diff := cmp.Diff(expectedData, data); diff != "" { + t.Fatalf("the state of the moved resource doesn't match the original state:\nDiff = %s", diff) + } + + return providers.WriteStateBytesResponse{} + } + + ui := new(cli.MockUi) + view, _ := testView(t) + c := &StateMvCommand{ + StateMeta{ + Meta: Meta{ + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: providerSource, + Ui: ui, + View: view, + }, + }, + } + + args := []string{ + oldAddr, + newAddr, + } + if code := c.Run(args); code != 0 { + t.Fatalf("return code: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // See the mock definition above for logic that asserts what the new state will look like after moving the resource. +} + func TestStateMv_backupAndBackupOutOptionsWithNonLocalBackend(t *testing.T) { state := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( From 38abe77a3e7ce1722cf9f904e6b892f164d0ca66 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 24 Nov 2025 13:44:53 +0000 Subject: [PATCH 10/15] test: Add command-level test for `state push` showing integration with pluggable state storage code. --- internal/command/state_push_test.go | 52 +++++++++++++++++++ .../.terraform.lock.hcl | 6 +++ .../.terraform/terraform.tfstate | 19 +++++++ .../state-push-state-store-good/main.tf | 13 +++++ .../replace.tfstate | 23 ++++++++ 5 files changed, 113 insertions(+) create mode 100644 internal/command/testdata/state-push-state-store-good/.terraform.lock.hcl create mode 100644 internal/command/testdata/state-push-state-store-good/.terraform/terraform.tfstate create mode 100644 internal/command/testdata/state-push-state-store-good/main.tf create mode 100644 internal/command/testdata/state-push-state-store-good/replace.tfstate diff --git a/internal/command/state_push_test.go b/internal/command/state_push_test.go index fb4475c7a9ec..9356e9342ca3 100644 --- a/internal/command/state_push_test.go +++ b/internal/command/state_push_test.go @@ -9,9 +9,12 @@ import ( "testing" "github.com/hashicorp/cli" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/backend" "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" ) func TestStatePush_empty(t *testing.T) { @@ -44,6 +47,55 @@ func TestStatePush_empty(t *testing.T) { } } +func TestStatePush_stateStore(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("state-push-state-store-good"), td) + t.Chdir(td) + + expected := testStateRead(t, "replace.tfstate") + + // Create a mock that doesn't have any internal states. + mockProvider := mockPluggableStateStorageProvider(t) + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + ui := new(cli.MockUi) + view, _ := testView(t) + c := &StatePushCommand{ + Meta: Meta{ + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: providerSource, + Ui: ui, + View: view, + }, + } + + args := []string{"replace.tfstate"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // Access the pushed state from the mock's internal store + r := bytes.NewReader(mockProvider.MockStates["default"].([]byte)) + actual, err := statefile.Read(r) + if err != nil { + t.Fatal(err) + } + + if !actual.State.Equal(expected) { + t.Fatalf("bad: %#v", actual) + } +} + func TestStatePush_lockedState(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() diff --git a/internal/command/testdata/state-push-state-store-good/.terraform.lock.hcl b/internal/command/testdata/state-push-state-store-good/.terraform.lock.hcl new file mode 100644 index 000000000000..e5c03757a7fa --- /dev/null +++ b/internal/command/testdata/state-push-state-store-good/.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/test" { + version = "1.2.3" +} diff --git a/internal/command/testdata/state-push-state-store-good/.terraform/terraform.tfstate b/internal/command/testdata/state-push-state-store-good/.terraform/terraform.tfstate new file mode 100644 index 000000000000..4f96aa73ee7d --- /dev/null +++ b/internal/command/testdata/state-push-state-store-good/.terraform/terraform.tfstate @@ -0,0 +1,19 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "state_store": { + "type": "test_store", + "config": { + "value": "foobar" + }, + "provider": { + "version": "1.2.3", + "source": "registry.terraform.io/hashicorp/test", + "config": { + "region": null + } + }, + "hash": 4158988729 + } +} \ No newline at end of file diff --git a/internal/command/testdata/state-push-state-store-good/main.tf b/internal/command/testdata/state-push-state-store-good/main.tf new file mode 100644 index 000000000000..34b58fdc0e2e --- /dev/null +++ b/internal/command/testdata/state-push-state-store-good/main.tf @@ -0,0 +1,13 @@ +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } + + state_store "test_store" { + provider "test" {} + + value = "foobar" + } +} diff --git a/internal/command/testdata/state-push-state-store-good/replace.tfstate b/internal/command/testdata/state-push-state-store-good/replace.tfstate new file mode 100644 index 000000000000..9921bc076254 --- /dev/null +++ b/internal/command/testdata/state-push-state-store-good/replace.tfstate @@ -0,0 +1,23 @@ +{ + "version": 4, + "serial": 0, + "lineage": "hello", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "null_resource", + "name": "b", + "provider": "provider.null", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "9051675049789185374", + "triggers": null + } + } + ] + } + ] +} From 6567d88ec72b04003281b0d8d0bb1d751cee9010 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 24 Nov 2025 15:50:33 +0000 Subject: [PATCH 11/15] test: Add command-level test for `state replace-provider` showing integration with pluggable state storage code. --- .../command/state_replace_provider_test.go | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/internal/command/state_replace_provider_test.go b/internal/command/state_replace_provider_test.go index 06508fd9a435..6abd706240e5 100644 --- a/internal/command/state_replace_provider_test.go +++ b/internal/command/state_replace_provider_test.go @@ -12,7 +12,9 @@ import ( "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statefile" ) func TestStateReplaceProvider(t *testing.T) { @@ -285,6 +287,104 @@ func TestStateReplaceProvider(t *testing.T) { }) } +func TestStateReplaceProvider_stateStore(t *testing.T) { + // Create a temporary working directory + td := t.TempDir() + testCopyDir(t, testFixturePath("state-list-state-store"), td) + t.Chdir(td) + + // Get bytes describing a state containing resources + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "baz", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"baz","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }) + var stateBuf bytes.Buffer + if err := statefile.Write(statefile.New(state, "", 1), &stateBuf); err != nil { + t.Fatalf("error during test setup: %s", err) + } + + // Create a mock that contains a persisted "default" state that uses the bytes from above. + mockProvider := mockPluggableStateStorageProvider(t) + mockProvider.MockStates = map[string]interface{}{ + "default": stateBuf.Bytes(), + } + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + ui := new(cli.MockUi) + view, _ := testView(t) + c := &StateReplaceProviderCommand{ + StateMeta{ + Meta: Meta{ + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: providerSource, + Ui: ui, + View: view, + }, + }, + } + + inputBuf := &bytes.Buffer{} + ui.InputReader = inputBuf + inputBuf.WriteString("yes\n") + + args := []string{ + "hashicorp/test", + "testing-corp/test", + } + if code := c.Run(args); code != 0 { + t.Fatalf("return code: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // For the two resources in the mocked state, we expect them both to be changed. + expectedOutputMsgs := []string{ + "- registry.terraform.io/hashicorp/test\n + registry.terraform.io/testing-corp/test\n", + "Successfully replaced provider for 2 resources.", + } + for _, msg := range expectedOutputMsgs { + if !strings.Contains(ui.OutputWriter.String(), msg) { + t.Fatalf("expected command output to include %q but it's not present in the output:\nOutput = %s", + msg, ui.OutputWriter.String()) + } + } +} + func TestStateReplaceProvider_docs(t *testing.T) { c := &StateReplaceProviderCommand{} From deda57eff19c36bed82f56bd3a7f74d413d699ea Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 24 Nov 2025 16:48:22 +0000 Subject: [PATCH 12/15] test: Change shared test fixture to not be named after a specific command under test. This test fixure is reused across tests that need the config to define a state store but otherwise rely on the mock provider to set up the test scenario. --- internal/command/output_test.go | 35 +++++++++++++++++++ internal/command/state_list_test.go | 2 +- internal/command/state_mv_test.go | 2 +- internal/command/state_pull_test.go | 2 +- .../command/state_replace_provider_test.go | 2 +- internal/command/state_rm_test.go | 2 +- internal/command/state_show_test.go | 2 +- .../.terraform.lock.hcl | 0 .../.terraform/terraform.tfstate | 0 .../main.tf | 0 .../terraform.tfstate | 0 .../hashicorp/simple6/0.0.1/.gitkeep | 0 12 files changed, 41 insertions(+), 6 deletions(-) rename internal/command/testdata/{state-list-state-store => state-commands-state-store}/.terraform.lock.hcl (100%) rename internal/command/testdata/{state-list-state-store => state-commands-state-store}/.terraform/terraform.tfstate (100%) rename internal/command/testdata/{state-list-state-store => state-commands-state-store}/main.tf (100%) rename internal/command/testdata/{state-list-state-store => state-commands-state-store}/terraform.tfstate (100%) delete mode 100644 internal/command/testdata/state-list-state-store/.terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/.gitkeep diff --git a/internal/command/output_test.go b/internal/command/output_test.go index 28bbc6d1f2ac..60cd244e7cd2 100644 --- a/internal/command/output_test.go +++ b/internal/command/output_test.go @@ -50,6 +50,41 @@ func TestOutput(t *testing.T) { } } +func TestOutput_stateStore(t *testing.T) { + originalState := states.BuildState(func(s *states.SyncState) { + s.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), + false, + ) + }) + + statePath := testStateFile(t, originalState) + + view, done := testView(t) + c := &OutputCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + View: view, + }, + } + + args := []string{ + "-state", statePath, + "foo", + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", output.Stderr()) + } + + actual := strings.TrimSpace(output.Stdout()) + if actual != `"bar"` { + t.Fatalf("bad: %#v", actual) + } +} + func TestOutput_json(t *testing.T) { originalState := states.BuildState(func(s *states.SyncState) { s.SetOutputValue( diff --git a/internal/command/state_list_test.go b/internal/command/state_list_test.go index 494e21dbf978..7b6046f8dd0d 100644 --- a/internal/command/state_list_test.go +++ b/internal/command/state_list_test.go @@ -165,7 +165,7 @@ func TestStateList_backendCustomState(t *testing.T) { func TestStateList_stateStore(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() - testCopyDir(t, testFixturePath("state-list-state-store"), td) + testCopyDir(t, testFixturePath("state-commands-state-store"), td) t.Chdir(td) // Get bytes describing a state containing a resource diff --git a/internal/command/state_mv_test.go b/internal/command/state_mv_test.go index a3788faf79ac..82157f97df21 100644 --- a/internal/command/state_mv_test.go +++ b/internal/command/state_mv_test.go @@ -160,7 +160,7 @@ func TestStateMv(t *testing.T) { func TestStateMv_stateStore(t *testing.T) { // Create a temporary working directory td := t.TempDir() - testCopyDir(t, testFixturePath("state-list-state-store"), td) + testCopyDir(t, testFixturePath("state-commands-state-store"), td) t.Chdir(td) // Get bytes describing a state containing resources diff --git a/internal/command/state_pull_test.go b/internal/command/state_pull_test.go index 55d2983ea68a..a46aed77aa4d 100644 --- a/internal/command/state_pull_test.go +++ b/internal/command/state_pull_test.go @@ -55,7 +55,7 @@ func TestStatePull(t *testing.T) { func TestStatePull_stateStore(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() - testCopyDir(t, testFixturePath("state-list-state-store"), td) + testCopyDir(t, testFixturePath("state-commands-state-store"), td) t.Chdir(td) // Get bytes describing a state containing a resource diff --git a/internal/command/state_replace_provider_test.go b/internal/command/state_replace_provider_test.go index 6abd706240e5..e040eb1f67ae 100644 --- a/internal/command/state_replace_provider_test.go +++ b/internal/command/state_replace_provider_test.go @@ -290,7 +290,7 @@ func TestStateReplaceProvider(t *testing.T) { func TestStateReplaceProvider_stateStore(t *testing.T) { // Create a temporary working directory td := t.TempDir() - testCopyDir(t, testFixturePath("state-list-state-store"), td) + testCopyDir(t, testFixturePath("state-commands-state-store"), td) t.Chdir(td) // Get bytes describing a state containing resources diff --git a/internal/command/state_rm_test.go b/internal/command/state_rm_test.go index c940168ee0c9..dcf69ba268ab 100644 --- a/internal/command/state_rm_test.go +++ b/internal/command/state_rm_test.go @@ -88,7 +88,7 @@ func TestStateRm(t *testing.T) { func TestStateRm_stateStore(t *testing.T) { // Create a temporary working directory td := t.TempDir() - testCopyDir(t, testFixturePath("state-list-state-store"), td) + testCopyDir(t, testFixturePath("state-commands-state-store"), td) t.Chdir(td) // Get bytes describing a state containing resources diff --git a/internal/command/state_show_test.go b/internal/command/state_show_test.go index c345454c6266..3f7b74801b34 100644 --- a/internal/command/state_show_test.go +++ b/internal/command/state_show_test.go @@ -279,7 +279,7 @@ func TestStateShow_configured_provider(t *testing.T) { func TestStateShow_stateStore(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() - testCopyDir(t, testFixturePath("state-list-state-store"), td) + testCopyDir(t, testFixturePath("state-commands-state-store"), td) t.Chdir(td) // Get bytes describing a state containing a resource diff --git a/internal/command/testdata/state-list-state-store/.terraform.lock.hcl b/internal/command/testdata/state-commands-state-store/.terraform.lock.hcl similarity index 100% rename from internal/command/testdata/state-list-state-store/.terraform.lock.hcl rename to internal/command/testdata/state-commands-state-store/.terraform.lock.hcl diff --git a/internal/command/testdata/state-list-state-store/.terraform/terraform.tfstate b/internal/command/testdata/state-commands-state-store/.terraform/terraform.tfstate similarity index 100% rename from internal/command/testdata/state-list-state-store/.terraform/terraform.tfstate rename to internal/command/testdata/state-commands-state-store/.terraform/terraform.tfstate diff --git a/internal/command/testdata/state-list-state-store/main.tf b/internal/command/testdata/state-commands-state-store/main.tf similarity index 100% rename from internal/command/testdata/state-list-state-store/main.tf rename to internal/command/testdata/state-commands-state-store/main.tf diff --git a/internal/command/testdata/state-list-state-store/terraform.tfstate b/internal/command/testdata/state-commands-state-store/terraform.tfstate similarity index 100% rename from internal/command/testdata/state-list-state-store/terraform.tfstate rename to internal/command/testdata/state-commands-state-store/terraform.tfstate diff --git a/internal/command/testdata/state-list-state-store/.terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/.gitkeep b/internal/command/testdata/state-list-state-store/.terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 From a6b6c1a7749dee10c7c24a64948c4bb44ed33c5e Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 24 Nov 2025 16:50:54 +0000 Subject: [PATCH 13/15] test: Update test to use shared test fixture --- internal/command/state_identities_test.go | 2 +- .../.terraform.lock.hcl | 6 ------ .../.terraform/terraform.tfstate | 19 ------------------- .../state-identities-state-store/main.tf | 13 ------------- 4 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 internal/command/testdata/state-identities-state-store/.terraform.lock.hcl delete mode 100644 internal/command/testdata/state-identities-state-store/.terraform/terraform.tfstate delete mode 100644 internal/command/testdata/state-identities-state-store/main.tf diff --git a/internal/command/state_identities_test.go b/internal/command/state_identities_test.go index b6d7c59c6528..0073fb3ccb68 100644 --- a/internal/command/state_identities_test.go +++ b/internal/command/state_identities_test.go @@ -431,7 +431,7 @@ func TestStateIdentities_modules(t *testing.T) { func TestStateIdentities_stateStore(t *testing.T) { // We need configuration present to force pluggable state storage to be used td := t.TempDir() - testCopyDir(t, testFixturePath("state-identities-state-store"), td) + testCopyDir(t, testFixturePath("state-commands-state-store"), td) t.Chdir(td) // Get a state file, that contains identity information,as bytes diff --git a/internal/command/testdata/state-identities-state-store/.terraform.lock.hcl b/internal/command/testdata/state-identities-state-store/.terraform.lock.hcl deleted file mode 100644 index e5c03757a7fa..000000000000 --- a/internal/command/testdata/state-identities-state-store/.terraform.lock.hcl +++ /dev/null @@ -1,6 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/test" { - version = "1.2.3" -} diff --git a/internal/command/testdata/state-identities-state-store/.terraform/terraform.tfstate b/internal/command/testdata/state-identities-state-store/.terraform/terraform.tfstate deleted file mode 100644 index 4f96aa73ee7d..000000000000 --- a/internal/command/testdata/state-identities-state-store/.terraform/terraform.tfstate +++ /dev/null @@ -1,19 +0,0 @@ -{ - "version": 3, - "serial": 0, - "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", - "state_store": { - "type": "test_store", - "config": { - "value": "foobar" - }, - "provider": { - "version": "1.2.3", - "source": "registry.terraform.io/hashicorp/test", - "config": { - "region": null - } - }, - "hash": 4158988729 - } -} \ No newline at end of file diff --git a/internal/command/testdata/state-identities-state-store/main.tf b/internal/command/testdata/state-identities-state-store/main.tf deleted file mode 100644 index 34b58fdc0e2e..000000000000 --- a/internal/command/testdata/state-identities-state-store/main.tf +++ /dev/null @@ -1,13 +0,0 @@ -terraform { - required_providers { - test = { - source = "registry.terraform.io/hashicorp/test" - } - } - - state_store "test_store" { - provider "test" {} - - value = "foobar" - } -} From ea418bcaffee365cd9f56d1ce0d21db1e18cb4b5 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 24 Nov 2025 17:01:11 +0000 Subject: [PATCH 14/15] test: Remove redundant test fixture The internal/command/testdata/state-commands-state-store and internal/command/testdata/state-store-unchanged test fixtures are the same. --- internal/command/state_identities_test.go | 2 +- internal/command/state_list_test.go | 2 +- internal/command/state_mv_test.go | 2 +- internal/command/state_pull_test.go | 2 +- .../command/state_replace_provider_test.go | 2 +- internal/command/state_rm_test.go | 2 +- internal/command/state_show_test.go | 2 +- .../.terraform.lock.hcl | 6 --- .../.terraform/terraform.tfstate | 19 --------- .../state-commands-state-store/main.tf | 25 ------------ .../terraform.tfstate | 40 ------------------- 11 files changed, 7 insertions(+), 97 deletions(-) delete mode 100644 internal/command/testdata/state-commands-state-store/.terraform.lock.hcl delete mode 100644 internal/command/testdata/state-commands-state-store/.terraform/terraform.tfstate delete mode 100644 internal/command/testdata/state-commands-state-store/main.tf delete mode 100644 internal/command/testdata/state-commands-state-store/terraform.tfstate diff --git a/internal/command/state_identities_test.go b/internal/command/state_identities_test.go index 0073fb3ccb68..6a9aae85877b 100644 --- a/internal/command/state_identities_test.go +++ b/internal/command/state_identities_test.go @@ -431,7 +431,7 @@ func TestStateIdentities_modules(t *testing.T) { func TestStateIdentities_stateStore(t *testing.T) { // We need configuration present to force pluggable state storage to be used td := t.TempDir() - testCopyDir(t, testFixturePath("state-commands-state-store"), td) + testCopyDir(t, testFixturePath("state-store-unchanged"), td) t.Chdir(td) // Get a state file, that contains identity information,as bytes diff --git a/internal/command/state_list_test.go b/internal/command/state_list_test.go index 7b6046f8dd0d..ce065f0a96e2 100644 --- a/internal/command/state_list_test.go +++ b/internal/command/state_list_test.go @@ -165,7 +165,7 @@ func TestStateList_backendCustomState(t *testing.T) { func TestStateList_stateStore(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() - testCopyDir(t, testFixturePath("state-commands-state-store"), td) + testCopyDir(t, testFixturePath("state-store-unchanged"), td) t.Chdir(td) // Get bytes describing a state containing a resource diff --git a/internal/command/state_mv_test.go b/internal/command/state_mv_test.go index 82157f97df21..07f4cbc0dd51 100644 --- a/internal/command/state_mv_test.go +++ b/internal/command/state_mv_test.go @@ -160,7 +160,7 @@ func TestStateMv(t *testing.T) { func TestStateMv_stateStore(t *testing.T) { // Create a temporary working directory td := t.TempDir() - testCopyDir(t, testFixturePath("state-commands-state-store"), td) + testCopyDir(t, testFixturePath("state-store-unchanged"), td) t.Chdir(td) // Get bytes describing a state containing resources diff --git a/internal/command/state_pull_test.go b/internal/command/state_pull_test.go index a46aed77aa4d..e0e2c06548b8 100644 --- a/internal/command/state_pull_test.go +++ b/internal/command/state_pull_test.go @@ -55,7 +55,7 @@ func TestStatePull(t *testing.T) { func TestStatePull_stateStore(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() - testCopyDir(t, testFixturePath("state-commands-state-store"), td) + testCopyDir(t, testFixturePath("state-store-unchanged"), td) t.Chdir(td) // Get bytes describing a state containing a resource diff --git a/internal/command/state_replace_provider_test.go b/internal/command/state_replace_provider_test.go index e040eb1f67ae..65e900fe73c0 100644 --- a/internal/command/state_replace_provider_test.go +++ b/internal/command/state_replace_provider_test.go @@ -290,7 +290,7 @@ func TestStateReplaceProvider(t *testing.T) { func TestStateReplaceProvider_stateStore(t *testing.T) { // Create a temporary working directory td := t.TempDir() - testCopyDir(t, testFixturePath("state-commands-state-store"), td) + testCopyDir(t, testFixturePath("state-store-unchanged"), td) t.Chdir(td) // Get bytes describing a state containing resources diff --git a/internal/command/state_rm_test.go b/internal/command/state_rm_test.go index dcf69ba268ab..90ab21cb2a4e 100644 --- a/internal/command/state_rm_test.go +++ b/internal/command/state_rm_test.go @@ -88,7 +88,7 @@ func TestStateRm(t *testing.T) { func TestStateRm_stateStore(t *testing.T) { // Create a temporary working directory td := t.TempDir() - testCopyDir(t, testFixturePath("state-commands-state-store"), td) + testCopyDir(t, testFixturePath("state-store-unchanged"), td) t.Chdir(td) // Get bytes describing a state containing resources diff --git a/internal/command/state_show_test.go b/internal/command/state_show_test.go index 3f7b74801b34..2dedddeea452 100644 --- a/internal/command/state_show_test.go +++ b/internal/command/state_show_test.go @@ -279,7 +279,7 @@ func TestStateShow_configured_provider(t *testing.T) { func TestStateShow_stateStore(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() - testCopyDir(t, testFixturePath("state-commands-state-store"), td) + testCopyDir(t, testFixturePath("state-store-unchanged"), td) t.Chdir(td) // Get bytes describing a state containing a resource diff --git a/internal/command/testdata/state-commands-state-store/.terraform.lock.hcl b/internal/command/testdata/state-commands-state-store/.terraform.lock.hcl deleted file mode 100644 index e5c03757a7fa..000000000000 --- a/internal/command/testdata/state-commands-state-store/.terraform.lock.hcl +++ /dev/null @@ -1,6 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/test" { - version = "1.2.3" -} diff --git a/internal/command/testdata/state-commands-state-store/.terraform/terraform.tfstate b/internal/command/testdata/state-commands-state-store/.terraform/terraform.tfstate deleted file mode 100644 index 4f96aa73ee7d..000000000000 --- a/internal/command/testdata/state-commands-state-store/.terraform/terraform.tfstate +++ /dev/null @@ -1,19 +0,0 @@ -{ - "version": 3, - "serial": 0, - "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", - "state_store": { - "type": "test_store", - "config": { - "value": "foobar" - }, - "provider": { - "version": "1.2.3", - "source": "registry.terraform.io/hashicorp/test", - "config": { - "region": null - } - }, - "hash": 4158988729 - } -} \ No newline at end of file diff --git a/internal/command/testdata/state-commands-state-store/main.tf b/internal/command/testdata/state-commands-state-store/main.tf deleted file mode 100644 index 84f54d6c1a4a..000000000000 --- a/internal/command/testdata/state-commands-state-store/main.tf +++ /dev/null @@ -1,25 +0,0 @@ -terraform { - required_providers { - test = { - source = "registry.terraform.io/hashicorp/test" - } - } - - state_store "test_store" { - provider "test" {} - - value = "foobar" - } -} - -variable "name" { - default = "world" -} - -resource "test_instance" "my-data" { - input = "hello ${var.name}" -} - -output "greeting" { - value = resource.terraform_data.my-data.output -} diff --git a/internal/command/testdata/state-commands-state-store/terraform.tfstate b/internal/command/testdata/state-commands-state-store/terraform.tfstate deleted file mode 100644 index 4feaaed87a08..000000000000 --- a/internal/command/testdata/state-commands-state-store/terraform.tfstate +++ /dev/null @@ -1,40 +0,0 @@ -{ - "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 bd6f62eb12b507acb486140d9ab6d9fdde076bfb Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 26 Nov 2025 15:03:07 +0000 Subject: [PATCH 15/15] fix: Re-add logic for setting chunk size in the context of E2E tests using grpcwrap package This was removed, incorrectly, in https://github.com/hashicorp/terraform/pull/37899 --- internal/grpcwrap/provider6.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/grpcwrap/provider6.go b/internal/grpcwrap/provider6.go index 2f0d0f7395f3..35a6679860da 100644 --- a/internal/grpcwrap/provider6.go +++ b/internal/grpcwrap/provider6.go @@ -982,6 +982,10 @@ func (p *provider6) ConfigureStateStore(ctx context.Context, req *tfplugin6.Conf return resp, nil } + // If this isn't present, chunk size is 0 and downstream code + // acts like there is no state at all. + p.chunkSize = configureResp.Capabilities.ChunkSize + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, configureResp.Diagnostics) resp.Capabilities = &tfplugin6.StateStoreServerCapabilities{ ChunkSize: configureResp.Capabilities.ChunkSize,