// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package e2etest import ( "bytes" "context" "encoding/json" "fmt" "io" "os" "path" "path/filepath" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/e2e" "github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/grpcwrap" tfplugin "github.com/hashicorp/terraform/internal/plugin6" simple "github.com/hashicorp/terraform/internal/provider-simple-v6" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" proto "github.com/hashicorp/terraform/internal/tfplugin6" ) // Test that users can do the full init-plan-apply workflow with pluggable state storage // when the state storage provider is reattached/unmanaged by Terraform. // As well as ensuring that the state store can be initialised ok, this tests that // the state store's details can be stored in the plan file despite the fact it's reattached. func TestPrimary_stateStore_unmanaged_separatePlan(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") } fixturePath := filepath.Join("testdata", "full-workflow-with-state-store-fs") t.Setenv(e2e.TestExperimentFlag, "true") terraformBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform") tf := e2e.NewBinary(t, terraformBin, fixturePath) reattachCh := make(chan *plugin.ReattachConfig) closeCh := make(chan struct{}) provider := &providerServer{ ProviderServer: grpcwrap.Provider6(simple.Provider()), } ctx, cancel := context.WithCancel(context.Background()) defer cancel() go plugin.Serve(&plugin.ServeConfig{ Logger: hclog.New(&hclog.LoggerOptions{ Name: "plugintest", Level: hclog.Trace, Output: io.Discard, }), Test: &plugin.ServeTestConfig{ Context: ctx, ReattachConfigCh: reattachCh, CloseCh: closeCh, }, GRPCServer: plugin.DefaultGRPCServer, VersionedPlugins: map[int]plugin.PluginSet{ 6: { "provider": &tfplugin.GRPCProviderPlugin{ GRPCProvider: func() proto.ProviderServer { return provider }, }, }, }, }) config := <-reattachCh if config == nil { t.Fatalf("no reattach config received") } reattachStr, err := json.Marshal(map[string]reattachConfig{ "hashicorp/simple6": { Protocol: string(config.Protocol), ProtocolVersion: 6, Pid: config.Pid, Test: true, Addr: reattachConfigAddr{ Network: config.Addr.Network(), String: config.Addr.String(), }, }, }) if err != nil { t.Fatal(err) } tf.AddEnv("TF_REATTACH_PROVIDERS=" + string(reattachStr)) // Required for the local state files to be written to the temp directory, // instead of the e2e directory in the repo. t.Chdir(tf.WorkDir()) //// INIT t.Setenv("TF_ENABLE_PLUGGABLE_STATE_STORAGE", "1") stdout, stderr, err := tf.Run("init") if err != nil { t.Fatalf("unexpected init error: %s\nstderr:\n%s\nstdout:\n%s", err, stderr, stdout) } if !provider.ReadStateBytesCalled() { t.Error("ReadStateBytes not called on un-managed provider") } if !provider.WriteStateBytesCalled() { t.Error("WriteStateBytes not called on un-managed provider") } provider.ResetReadStateBytesCalled() provider.ResetWriteStateBytesCalled() // Make sure we didn't download the binary if strings.Contains(stdout, "Installing hashicorp/simple6 v") { t.Errorf("test provider download message is present in init output:\n%s", stdout) } if tf.FileExists(filepath.Join(".terraform", "plugins", "registry.terraform.io", "hashicorp", "simple6")) { t.Errorf("test provider binary found in .terraform dir") } //// PLAN stdout, stderr, err = tf.Run("plan", "-out=tfplan") if err != nil { t.Fatalf("unexpected plan error: %s\nstderr:\n%s\nstdout:\n%s", err, stderr, stdout) } if !provider.ReadStateBytesCalled() { t.Error("ReadStateBytes not called on un-managed provider") } if provider.WriteStateBytesCalled() { t.Error("WriteStateBytes should not be called on un-managed provider during plan") } provider.ResetReadStateBytesCalled() provider.ResetWriteStateBytesCalled() //// APPLY stdout, stderr, err = tf.Run("apply", "tfplan") if err != nil { t.Fatalf("unexpected apply error: %s\nstderr:\n%s\nstdout:\n%s", err, stderr, stdout) } if !provider.ReadStateBytesCalled() { t.Error("ReadStateBytes not called on un-managed provider") } if !provider.WriteStateBytesCalled() { t.Error("WriteStateBytes not called on un-managed provider") } provider.ResetReadStateBytesCalled() provider.ResetWriteStateBytesCalled() // Check the apply process has made a state file as expected. stateFilePath := filepath.Join("states", "default", "terraform.tfstate") if !tf.FileExists(stateFilePath) { t.Fatalf("state file not found at expected path: %s", filepath.Join(tf.WorkDir(), stateFilePath)) } //// DESTROY stdout, stderr, err = tf.Run("destroy", "-auto-approve") if err != nil { t.Fatalf("unexpected destroy error: %s\nstderr:\n%s\nstdout:\n%s", err, stderr, stdout) } cancel() <-closeCh } // Tests using `terraform workspace` commands in combination with pluggable state storage. func TestPrimary_stateStore_workspaceCmd(t *testing.T) { if !canRunGoBuild { // We're running in a separate-build-then-run context, so we can't // currently execute this test which depends on being able to build // new executable at runtime. // // (See the comment on canRunGoBuild's declaration for more information.) t.Skip("can't run without building a new provider executable") } t.Setenv(e2e.TestExperimentFlag, "true") terraformBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform") fixturePath := filepath.Join("testdata", "full-workflow-with-state-store-fs") tf := e2e.NewBinary(t, terraformBin, fixturePath) workspaceDirName := "states" // See workspace_dir value in the configuration // In order to test integration with PSS we need a provider plugin implementing a state store. // Here will build the simple6 (built with protocol v6) provider, which implements PSS. simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6") simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider) // Move the provider binaries into a directory that we will point terraform // to using the -plugin-dir cli flag. platform := getproviders.CurrentPlatform.String() fsMirrorPath := "cache/registry.terraform.io/hashicorp/simple6/0.0.1/" if err := os.MkdirAll(tf.Path(fsMirrorPath, platform), os.ModePerm); err != nil { t.Fatal(err) } if err := os.Rename(simple6ProviderExe, tf.Path(fsMirrorPath, platform, "terraform-provider-simple6")); err != nil { t.Fatal(err) } //// Init _, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color") if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } fi, err := os.Stat(path.Join(tf.WorkDir(), workspaceDirName, "default", "terraform.tfstate")) if err != nil { t.Fatalf("failed to open default workspace's state file: %s", err) } if fi.Size() == 0 { t.Fatal("default workspace's state file should not have size 0 bytes") } //// Create Workspace: terraform workspace new newWorkspace := "foobar" stdout, stderr, err := tf.Run("workspace", "new", newWorkspace, "-no-color") if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } expectedMsg := fmt.Sprintf("Created and switched to workspace %q!", newWorkspace) if !strings.Contains(stdout, expectedMsg) { t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) } fi, err = os.Stat(path.Join(tf.WorkDir(), workspaceDirName, newWorkspace, "terraform.tfstate")) if err != nil { t.Fatalf("failed to open %s workspace's state file: %s", newWorkspace, err) } if fi.Size() == 0 { t.Fatalf("%s workspace's state file should not have size 0 bytes", newWorkspace) } //// List Workspaces: : terraform workspace list stdout, stderr, err = tf.Run("workspace", "list", "-no-color") if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } if !strings.Contains(stdout, newWorkspace) { t.Errorf("unexpected output, expected the new %q workspace to be listed present, but it's missing. Got:\n%s", newWorkspace, stdout) } //// Select Workspace: terraform workspace select selectedWorkspace := "default" stdout, stderr, err = tf.Run("workspace", "select", selectedWorkspace, "-no-color") if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } expectedMsg = fmt.Sprintf("Switched to workspace %q.", selectedWorkspace) if !strings.Contains(stdout, expectedMsg) { t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) } //// Show Workspace: terraform workspace show stdout, stderr, err = tf.Run("workspace", "show", "-no-color") if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } expectedMsg = fmt.Sprintf("%s\n", selectedWorkspace) if stdout != expectedMsg { t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) } //// Delete Workspace: terraform workspace delete stdout, stderr, err = tf.Run("workspace", "delete", newWorkspace, "-no-color") if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } expectedMsg = fmt.Sprintf("Deleted workspace %q!\n", newWorkspace) if stdout != expectedMsg { t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) } } // Tests using `terraform state` subcommands in combination with pluggable state storage: // > `terraform state show` // > `terraform state list` func TestPrimary_stateStore_stateCmds(t *testing.T) { if !canRunGoBuild { // We're running in a separate-build-then-run context, so we can't // currently execute this test which depends on being able to build // new executable at runtime. // // (See the comment on canRunGoBuild's declaration for more information.) t.Skip("can't run without building a new provider executable") } t.Setenv(e2e.TestExperimentFlag, "true") tfBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform") fixturePath := filepath.Join("testdata", "initialized-directory-with-state-store-fs") tf := e2e.NewBinary(t, tfBin, fixturePath) workspaceDirName := "states" // see test fixture value for workspace_dir // In order to test integration with PSS we need a provider plugin implementing a state store. // Here will build the simple6 (built with protocol v6) provider, which implements PSS. simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6") simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider) // Move the provider binaries into the correct .terraform/providers/ directory // that will contain provider binaries in an initialized working directory. platform := getproviders.CurrentPlatform.String() providerCachePath := ".terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/" if err := os.MkdirAll(tf.Path(providerCachePath, platform), os.ModePerm); err != nil { t.Fatal(err) } if err := os.Rename(simple6ProviderExe, tf.Path(providerCachePath, 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) } } // Tests using the `terraform output` command in combination with pluggable state storage: // > `terraform output` // > `terraform output ` func TestPrimary_stateStore_outputCmd(t *testing.T) { if !canRunGoBuild { // We're running in a separate-build-then-run context, so we can't // currently execute this test which depends on being able to build // new executable at runtime. // // (See the comment on canRunGoBuild's declaration for more information.) t.Skip("can't run without building a new provider executable") } t.Setenv(e2e.TestExperimentFlag, "true") tfBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform") fixturePath := filepath.Join("testdata", "initialized-directory-with-state-store-fs") tf := e2e.NewBinary(t, tfBin, fixturePath) workspaceDirName := "states" // see test fixture value for workspace_dir // In order to test integration with PSS we need a provider plugin implementing a state store. // Here will build the simple6 (built with protocol v6) provider, which implements PSS. simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6") simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider) // Move the provider binaries into the correct .terraform/providers/ directory // that will contain provider binaries in an initialized working directory. platform := getproviders.CurrentPlatform.String() if err := os.MkdirAll(tf.Path(".terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/", platform), os.ModePerm); err != nil { t.Fatal(err) } if err := os.Rename(simple6ProviderExe, tf.Path(".terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/", platform, "terraform-provider-simple6")); err != nil { t.Fatal(err) } // Assert that the test starts with the default state present from test fixtures defaultStateId := "default" fi, err := os.Stat(path.Join(tf.WorkDir(), workspaceDirName, defaultStateId, "terraform.tfstate")) if err != nil { t.Fatalf("failed to open default workspace's state file: %s", err) } if fi.Size() == 0 { t.Fatal("default workspace's state file should not have size 0 bytes") } //// List all outputs: terraform output stdout, stderr, err := tf.Run("output", "-no-color") if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } expectedMsg := "greeting = \"hello world\"\n" // See the test fixture files if stdout != expectedMsg { t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) } //// View a specific output: terraform output outputName := "greeting" stdout, stderr, err = tf.Run("output", outputName, "-no-color") if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } expectedMsg = "\"hello world\"\n" // Only the value is outputted, no name present if stdout != expectedMsg { t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) } } // Tests using the `terraform show` command in combination with pluggable state storage // > `terraform show` // > `terraform show ` // > `terraform show ` func TestPrimary_stateStore_showCmd(t *testing.T) { if !canRunGoBuild { // We're running in a separate-build-then-run context, so we can't // currently execute this test which depends on being able to build // new executable at runtime. // // (See the comment on canRunGoBuild's declaration for more information.) t.Skip("can't run without building a new provider executable") } t.Setenv(e2e.TestExperimentFlag, "true") tfBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform") fixturePath := filepath.Join("testdata", "initialized-directory-with-state-store-fs") tf := e2e.NewBinary(t, tfBin, fixturePath) workspaceDirName := "states" // see test fixture value for workspace_dir // In order to test integration with PSS we need a provider plugin implementing a state store. // Here will build the simple6 (built with protocol v6) provider, which implements PSS. simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6") simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider) // Move the provider binaries into the correct .terraform/providers/ directory // that will contain provider binaries in an initialized working directory. platform := getproviders.CurrentPlatform.String() if err := os.MkdirAll(tf.Path(".terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/", platform), os.ModePerm); err != nil { t.Fatal(err) } if err := os.Rename(simple6ProviderExe, tf.Path(".terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/", platform, "terraform-provider-simple6")); err != nil { t.Fatal(err) } // Assert that the test starts with the default state present from test fixtures defaultStateId := "default" fi, err := os.Stat(path.Join(tf.WorkDir(), workspaceDirName, defaultStateId, "terraform.tfstate")) if err != nil { t.Fatalf("failed to open default workspace's state file: %s", err) } if fi.Size() == 0 { t.Fatal("default workspace's state file should not have size 0 bytes") } //// Show state: terraform state stdout, stderr, err := tf.Run("show", "-no-color") if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } expectedMsg := `# terraform_data.my-data: resource "terraform_data" "my-data" { id = "d71fb368-2ba1-fb4c-5bd9-6a2b7f05d60c" input = "hello world" output = "hello world" } Outputs: greeting = "hello world" ` // See the test fixture folder's state file if diff := cmp.Diff(stdout, expectedMsg); diff != "" { t.Errorf("wrong result, diff:\n%s", diff) } //// Show state: terraform show path := fmt.Sprintf("./%s/%s/terraform.tfstate", workspaceDirName, defaultStateId) stdout, stderr, err = tf.Run("show", path, "-no-color") if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } if diff := cmp.Diff(stdout, expectedMsg); diff != "" { t.Errorf("wrong result, diff:\n%s", diff) } //// Show state: terraform show // 1. Create a plan file via plan command newOutput := `output "replacement" { value = resource.terraform_data.my-data.output }` if err := os.WriteFile(filepath.Join(tf.WorkDir(), "outputs.tf"), []byte(newOutput), 0644); err != nil { t.Fatalf("err: %s", err) } planFile := "tfplan" stdout, stderr, err = tf.Run("plan", fmt.Sprintf("-out=%s", planFile), "-no-color") if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } expectedMsg = "Changes to Outputs" if !strings.Contains(stdout, expectedMsg) { t.Errorf("wrong result, expected the plan command to create a plan file but that hasn't happened, got:\n%s", stdout, ) } // 2. Inspect plan file stdout, stderr, err = tf.Run("show", planFile, "-no-color") if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } expectedMsg = ` Changes to Outputs: - greeting = "hello world" -> null + replacement = "hello world" You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure. ` if diff := cmp.Diff(stdout, expectedMsg); diff != "" { 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. // // The test `TestProvidersSchema` has test coverage showing that state store schemas are present // in the command's outputs. _This_ test is intended to assert that the command is able to read and use // state via a state store ok, and is able to detect providers required only by the state. 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 // Add a state file describing a resource from the simple (v5) provider, so // we can test that the state is read and used to get all the provider schemas fakeState := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "simple_resource", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"id":"bar"}`), Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("simple"), Module: addrs.RootModule, }, ) }) fakeStateFile := &statefile.File{ Lineage: "boop", Serial: 4, TerraformVersion: version.Must(version.NewVersion("1.0.0")), State: fakeState, } var fakeStateBuf bytes.Buffer err := statefile.WriteForTest(fakeStateFile, &fakeStateBuf) if err != nil { t.Error(err) } fakeStateBytes := fakeStateBuf.Bytes() if err := os.MkdirAll(tf.Path(workspaceDirName, "default"), os.ModePerm); err != nil { t.Fatal(err) } if err := os.WriteFile(tf.Path(workspaceDirName, "default", "terraform.tfstate"), fakeStateBytes, 0644); err != nil { t.Fatal(err) } // 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 will be used for PSS. // The simple (v5) provider is also built, as that provider will be present in the state and therefore // needed for creating the schema output. simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6") simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider) simpleProvider := filepath.Join(tf.WorkDir(), "terraform-provider-simple") simpleProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple/main", simpleProvider) // Move the provider binaries into a directory that we will point terraform // to using the -plugin-dir cli flag. platform := getproviders.CurrentPlatform.String() fsMirrorPathV6 := "cache/registry.terraform.io/hashicorp/simple6/0.0.1/" if err := os.MkdirAll(tf.Path(fsMirrorPathV6, platform), os.ModePerm); err != nil { t.Fatal(err) } if err := os.Rename(simple6ProviderExe, tf.Path(fsMirrorPathV6, platform, "terraform-provider-simple6")); err != nil { t.Fatal(err) } fsMirrorPathV5 := "cache/registry.terraform.io/hashicorp/simple/0.0.1/" if err := os.MkdirAll(tf.Path(fsMirrorPathV5, platform), os.ModePerm); err != nil { t.Fatal(err) } if err := os.Rename(simpleProviderExe, tf.Path(fsMirrorPathV5, platform, "terraform-provider-simple")); 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) } // We expect the command to be able to use the state store to // detect providers that come from only the state. expectedMsgs := []string{ "Providers required by configuration:", "provider[registry.terraform.io/hashicorp/simple6]", "provider[terraform.io/builtin/terraform]", "Providers required by state:", "provider[registry.terraform.io/hashicorp/simple]", } 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{ `"registry.terraform.io/hashicorp/simple6"`, // provider used for PSS `"terraform.io/builtin/terraform"`, // provider used for resources `"registry.terraform.io/hashicorp/simple"`, // provider present only in the state } for _, msg := range expectedMsgs { if !strings.Contains(stdout, msg) { t.Errorf("unexpected output, expected %q, but got:\n%s", msg, stdout) } } // More thorough checking of the JSON output is in `TestProvidersSchema`. // This test just asserts that `terraform providers schema` can read state // via the state store, and therefore detects all 3 providers needed for the output. }