From d291ddccdc024e7059acf60cdce2fc64a42fc5e2 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Tue, 2 Dec 2025 14:14:58 +0000 Subject: [PATCH 1/5] test: Add E2E test for using pluggable state storage with the `providers` command Note: I've excluded the `terraform providers locks` and `terraform providers mirror` commands as they don't interact with backends. --- .../e2etest/pluggable_state_store_test.go | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/internal/command/e2etest/pluggable_state_store_test.go b/internal/command/e2etest/pluggable_state_store_test.go index 92238bd0fd3b..35cba411b1ef 100644 --- a/internal/command/e2etest/pluggable_state_store_test.go +++ b/internal/command/e2etest/pluggable_state_store_test.go @@ -198,3 +198,89 @@ resource "terraform_data" "my-data" { t.Errorf("wrong result, diff:\n%s", diff) } } + +// Tests using the `terraform provider` subcommands in combination with pluggable state storage: +// > `terraform providers` +// > `terraform providers schema` +// +// Commands `terraform providers locks` and `terraform providers mirror` aren't tested as they +// don't interact with the backend. +func TestPrimary_stateStore_providerCmds(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") + } + + //// Providers: `terraform providers` + stdout, stderr, err := tf.Run("providers", "-no-color") + if err != nil { + t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) + } + + expectedMsgs := []string{ + "├── provider[registry.terraform.io/hashicorp/simple6]", + "└── provider[terraform.io/builtin/terraform]", + } + for _, msg := range expectedMsgs { + if !strings.Contains(stdout, msg) { + t.Errorf("unexpected output, expected %q, but got:\n%s", msg, stdout) + } + } + + //// Provider schemas: `terraform providers schema` + stdout, stderr, err = tf.Run("providers", "schema", "-json", "-no-color") + if err != nil { + t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) + } + + expectedMsgs = []string{ + `{"format_version":"1.0","provider_schemas":{`, // opening of JSON + `"registry.terraform.io/hashicorp/simple6":{`, // provider 1 + `"terraform.io/builtin/terraform":{`, // provider 2 + } + for _, msg := range expectedMsgs { + if !strings.Contains(stdout, msg) { + t.Errorf("unexpected output, expected %q, but got:\n%s", msg, stdout) + } + } +} From ed3467a6bf1aaa5abe0758169ae4e84df6ae2c15 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Tue, 2 Dec 2025 15:55:10 +0000 Subject: [PATCH 2/5] test: Add integration test for using pluggable state storage with the `providers` command --- internal/command/providers_test.go | 76 ++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/internal/command/providers_test.go b/internal/command/providers_test.go index 596a66b77b1f..34b097b71793 100644 --- a/internal/command/providers_test.go +++ b/internal/command/providers_test.go @@ -4,11 +4,16 @@ package command import ( + "bytes" "os" "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 TestProviders(t *testing.T) { @@ -203,3 +208,74 @@ func TestProviders_tests(t *testing.T) { } } } + +func TestProviders_state_withStateStore(t *testing.T) { + // State with a 'baz' provider not in the config + originalState := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "baz_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("baz"), + Module: addrs.RootModule, + }, + ) + }) + + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("state-store-unchanged"), td) + t.Chdir(td) + + // Get bytes describing the state + var stateBuf bytes.Buffer + if err := statefile.Write(statefile.New(originalState, "", 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") + + ui := new(cli.MockUi) + c := &ProvidersCommand{ + Meta: Meta{ + Ui: ui, + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + }, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + wantOutput := []string{ + "Providers required by configuration:", + "└── provider[registry.terraform.io/hashicorp/test] 1.2.3", + "Providers required by state:", + "provider[registry.terraform.io/hashicorp/baz]", + } + + output := ui.OutputWriter.String() + for _, want := range wantOutput { + if !strings.Contains(output, want) { + t.Errorf("output missing %s:\n%s", want, output) + } + } +} From 44f7ab4b6f1793d30b53fb0f3ab1db543a8d58ec Mon Sep 17 00:00:00 2001 From: Sarah French Date: Tue, 2 Dec 2025 15:57:12 +0000 Subject: [PATCH 3/5] refactor: Change ioutil.ReadDir to os.ReadDir --- internal/command/providers_schema_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/command/providers_schema_test.go b/internal/command/providers_schema_test.go index fdfc3e1747ce..c9703891b916 100644 --- a/internal/command/providers_schema_test.go +++ b/internal/command/providers_schema_test.go @@ -37,7 +37,7 @@ func TestProvidersSchema_error(t *testing.T) { func TestProvidersSchema_output(t *testing.T) { fixtureDir := "testdata/providers-schema" - testDirs, err := ioutil.ReadDir(fixtureDir) + testDirs, err := os.ReadDir(fixtureDir) if err != nil { t.Fatal(err) } From f9d9db3de32bb3c5aec91532b85115720b88d633 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Tue, 2 Dec 2025 16:04:18 +0000 Subject: [PATCH 4/5] test: Add integration test for using pluggable state storage with the `providers schema` command --- internal/command/providers_schema_test.go | 80 +++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/internal/command/providers_schema_test.go b/internal/command/providers_schema_test.go index c9703891b916..33dd19bc5361 100644 --- a/internal/command/providers_schema_test.go +++ b/internal/command/providers_schema_test.go @@ -4,20 +4,25 @@ package command import ( + "bytes" "encoding/json" "fmt" "io/ioutil" "os" "path/filepath" + "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/cli" "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statefile" ) func TestProvidersSchema_error(t *testing.T) { @@ -103,6 +108,81 @@ func TestProvidersSchema_output(t *testing.T) { } } +func TestProvidersSchema_output_withStateStore(t *testing.T) { + // State with a 'baz' provider not in the config + originalState := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "baz_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar"}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("baz"), + Module: addrs.RootModule, + }, + ) + }) + + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("state-store-unchanged"), td) + t.Chdir(td) + + // Get bytes describing the state + var stateBuf bytes.Buffer + if err := statefile.Write(statefile.New(originalState, "", 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(), + } + mockProviderAddressTest := addrs.NewDefaultProvider("test") + + // Mock for the provider in the state + mockProviderAddressBaz := addrs.NewDefaultProvider("baz") + + ui := new(cli.MockUi) + c := &ProvidersSchemaCommand{ + Meta: Meta{ + Ui: ui, + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddressTest: providers.FactoryFixed(mockProvider), + mockProviderAddressBaz: providers.FactoryFixed(mockProvider), + }, + }, + }, + } + + args := []string{"-json"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + wantOutput := []string{ + `{"format_version":"1.0","provider_schemas":{`, // Opening of JSON + `"registry.terraform.io/hashicorp/baz":{`, // provider from state + `"registry.terraform.io/hashicorp/test":{`, // provider from config + } + + output := ui.OutputWriter.String() + for _, want := range wantOutput { + if !strings.Contains(output, want) { + t.Errorf("output missing %s:\n%s", want, output) + } + } + +} + type providerSchemas struct { FormatVersion string `json:"format_version"` Schemas map[string]providerSchema `json:"provider_schemas"` From d9085077ce7332ac12835dd61ae09cce46e0baed Mon Sep 17 00:00:00 2001 From: Sarah French Date: Thu, 11 Dec 2025 16:44:03 +0000 Subject: [PATCH 5/5] fix: Cleanup after rebasing work --- internal/command/providers_schema_test.go | 2 +- internal/command/providers_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/command/providers_schema_test.go b/internal/command/providers_schema_test.go index 33dd19bc5361..97d94f6901ec 100644 --- a/internal/command/providers_schema_test.go +++ b/internal/command/providers_schema_test.go @@ -140,7 +140,7 @@ func TestProvidersSchema_output_withStateStore(t *testing.T) { } // Create a mock that contains a persisted "default" state that uses the bytes from above. - mockProvider := mockPluggableStateStorageProvider(t) + mockProvider := mockPluggableStateStorageProvider() mockProvider.MockStates = map[string]interface{}{ "default": stateBuf.Bytes(), } diff --git a/internal/command/providers_test.go b/internal/command/providers_test.go index 34b097b71793..b2755894535c 100644 --- a/internal/command/providers_test.go +++ b/internal/command/providers_test.go @@ -241,7 +241,7 @@ func TestProviders_state_withStateStore(t *testing.T) { } // Create a mock that contains a persisted "default" state that uses the bytes from above. - mockProvider := mockPluggableStateStorageProvider(t) + mockProvider := mockPluggableStateStorageProvider() mockProvider.MockStates = map[string]interface{}{ "default": stateBuf.Bytes(), }