diff --git a/internal/backend/remote-state/inmem/backend.go b/internal/backend/remote-state/inmem/backend.go index 96cddd0911..2197a280ee 100644 --- a/internal/backend/remote-state/inmem/backend.go +++ b/internal/backend/remote-state/inmem/backend.go @@ -71,22 +71,27 @@ func (b *Backend) Configure(configVal cty.Value) tfdiags.Diagnostics { states.Lock() defer states.Unlock() - defaultClient := &RemoteClient{ - Name: backend.DefaultStateName, - } + // Some tests may configure this backend multiple times + // and expect the same state from memory afterwards. + _, ok := states.m[backend.DefaultStateName] + if !ok { + defaultClient := &RemoteClient{ + Name: backend.DefaultStateName, + } - states.m[backend.DefaultStateName] = &remote.State{ - Client: defaultClient, - } + states.m[backend.DefaultStateName] = &remote.State{ + Client: defaultClient, + } - // set the default client lock info per the test config - if v := configVal.GetAttr("lock_id"); !v.IsNull() { - info := statemgr.NewLockInfo() - info.ID = v.AsString() - info.Operation = "test" - info.Info = "test config" + // set the default client lock info per the test config + if v := configVal.GetAttr("lock_id"); !v.IsNull() { + info := statemgr.NewLockInfo() + info.ID = v.AsString() + info.Operation = "test" + info.Info = "test config" - locks.lock(backend.DefaultStateName, info) + locks.lock(backend.DefaultStateName, info) + } } return nil diff --git a/internal/backend/remote-state/inmem/testing.go b/internal/backend/remote-state/inmem/testing.go new file mode 100644 index 0000000000..1a7f9d0699 --- /dev/null +++ b/internal/backend/remote-state/inmem/testing.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package inmem + +import ( + "testing" + + statespkg "github.com/hashicorp/terraform/internal/states" +) + +func ReadState(t *testing.T, wsName string) *statespkg.State { + states.Lock() + defer states.Unlock() + + stateMgr, ok := states.m[wsName] + if !ok { + t.Fatalf("state not found for workspace %s", wsName) + } + + return stateMgr.State() +} + +func ReadWorkspaces(t *testing.T) []string { + states.Lock() + defer states.Unlock() + + workspaces := make([]string, 0, len(states.m)) + for wsName := range states.m { + workspaces = append(workspaces, wsName) + } + + return workspaces +} diff --git a/internal/cloud/backend_test.go b/internal/cloud/backend_test.go index a838ad852a..5aba0fdeb4 100644 --- a/internal/cloud/backend_test.go +++ b/internal/cloud/backend_test.go @@ -261,7 +261,7 @@ func TestCloud_PrepareConfig(t *testing.T) { } for name, tc := range cases { - s := testServer(t) + s := TestServer(t) b := New(testDisco(s)) // Validate @@ -794,7 +794,7 @@ func TestCloud_configVerifyMinimumTFEVersion(t *testing.T) { w.Header().Set("TFP-API-Version", "2.4") }, } - s := testServerWithHandlers(handlers) + s := TestServerWithHandlers(t, handlers) b := New(testDisco(s)) @@ -831,7 +831,7 @@ func TestCloud_configVerifyMinimumTFEVersionInAutomation(t *testing.T) { w.Header().Set("TFP-API-Version", "2.4") }, } - s := testServerWithHandlers(handlers) + s := TestServerWithHandlers(t, handlers) b := New(testDisco(s)) b.runningInAutomation = true @@ -1680,7 +1680,7 @@ func TestCloudBackend_DeleteWorkspace_DoesNotExist(t *testing.T) { } func TestCloud_ServiceDiscoveryAliases(t *testing.T) { - s := testServer(t) + s := TestServer(t) b := New(testDisco(s)) diag := b.Configure(cty.ObjectVal(map[string]cty.Value{ diff --git a/internal/cloud/testing.go b/internal/cloud/testing.go index 4d197c430f..62ced1c389 100644 --- a/internal/cloud/testing.go +++ b/internal/cloud/testing.go @@ -245,9 +245,9 @@ func testBackendWithOutputs(t *testing.T) (*Cloud, func()) { func testBackend(t *testing.T, obj cty.Value, handlers map[string]func(http.ResponseWriter, *http.Request)) (*Cloud, *MockClient, func()) { var s *httptest.Server if handlers != nil { - s = testServerWithHandlers(handlers) + s = TestServerWithHandlers(t, handlers) } else { - s = testServer(t) + s = TestServer(t) } b := New(testDisco(s)) @@ -324,7 +324,7 @@ func testBackend(t *testing.T, obj cty.Value, handlers map[string]func(http.Resp // testUnconfiguredBackend is used for testing the configuration of the backend // with the mock client func testUnconfiguredBackend(t *testing.T) (*Cloud, func()) { - s := testServer(t) + s := TestServer(t) b := New(testDisco(s)) // Normally, the client is created during configuration, but the configuration uses the @@ -395,15 +395,15 @@ func testLocalBackend(t *testing.T, cloud *Cloud) backendrun.OperationsBackend { return b } -// testServer returns a started *httptest.Server used for local testing with the default set of +// TestServer returns a started *httptest.Server used for local testing with the default set of // request handlers. -func testServer(t *testing.T) *httptest.Server { - return testServerWithHandlers(testDefaultRequestHandlers) +func TestServer(t *testing.T) *httptest.Server { + return TestServerWithHandlers(t, testDefaultRequestHandlers) } -// testServerWithHandlers returns a started *httptest.Server with the given set of request handlers +// TestServerWithHandlers returns a started *httptest.Server with the given set of request handlers // overriding any default request handlers (testDefaultRequestHandlers). -func testServerWithHandlers(handlers map[string]func(http.ResponseWriter, *http.Request)) *httptest.Server { +func TestServerWithHandlers(t *testing.T, handlers map[string]func(http.ResponseWriter, *http.Request)) *httptest.Server { mux := http.NewServeMux() for route, handler := range handlers { mux.HandleFunc(route, handler) @@ -414,6 +414,11 @@ func testServerWithHandlers(handlers map[string]func(http.ResponseWriter, *http. } } + mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + t.Logf("unexpected %s request received for %q", req.Method, req.URL.String()) + w.WriteHeader(http.StatusBadRequest) + }) + return httptest.NewServer(mux) } diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 68f7000d54..03ead1ce0d 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -6,11 +6,14 @@ import ( "encoding/json" "fmt" "log" + "maps" "net/http" "net/http/httptest" + "net/url" "os" "path/filepath" "regexp" + "slices" "strings" "testing" @@ -23,7 +26,10 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/backend" + backendInit "github.com/hashicorp/terraform/internal/backend/init" httpBackend "github.com/hashicorp/terraform/internal/backend/remote-state/http" + "github.com/hashicorp/terraform/internal/backend/remote-state/inmem" + "github.com/hashicorp/terraform/internal/cloud" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" @@ -4742,6 +4748,899 @@ func TestInit_unitialized_stateStore(t *testing.T) { } } +func TestInit_backend_to_stateStore_singleWorkspace(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + + testBackend := new(httpBackend.TestHTTPBackend) + ts := httptest.NewServer(http.HandlerFunc(testBackend.Handle)) + t.Cleanup(ts.Close) + + cfg := fmt.Sprintf(`terraform { + backend "http" { + address = %q + } +} +`, ts.URL) + if err := os.WriteFile(filepath.Join(td, "main.tf"), []byte(cfg), 0644); err != nil { + t.Fatalf("err: %s", err) + } + t.Chdir(td) + + mockProvider := mockPluggableStateStorageProvider() + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.2.3"}, + }) + defer close() + + tOverrides := &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + } + + { + log.Printf("[TRACE] %s: beginning first init with backend", t.Name()) + // Init + ui := cli.NewMockUi() + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + }, + } + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + } + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) + } + log.Printf("[TRACE] %s: first init complete", t.Name()) + t.Logf("First run output:\n%s", testOutput.Stdout()) + t.Logf("First run errors:\n%s", testOutput.Stderr()) + + if testBackend.CallCount("POST") != 0 { + t.Fatalf("expected 0 POST calls after init, got %d", testBackend.CallCount("POST")) + } + if testBackend.CallCount("GET") != 2 { + t.Fatalf("expected 2 GET calls after init, got %d", testBackend.CallCount("GET")) + } + } + { + // run apply to ensure state isn't empty + // to bypass edge case handling which causes empty state to stop migration + log.Printf("[TRACE] %s: beginning apply with backend", t.Name()) + + outputCfg := `output "test" { + value = "test" +} +` + if err := os.WriteFile(filepath.Join(td, "output.tf"), []byte(outputCfg), 0644); err != nil { + t.Fatalf("err: %s", err) + } + + ui := cli.NewMockUi() + aView, aDone := testView(t) + cApply := &ApplyCommand{ + Meta: Meta{ + Ui: ui, + View: aView, + AllowExperimentalFeatures: true, + }, + } + aCode := cApply.Run([]string{"-auto-approve"}) + aTestOutput := aDone(t) + if aCode != 0 { + t.Fatalf("bad: \n%s", aTestOutput.All()) + } + + t.Logf("Apply output:\n%s", aTestOutput.Stdout()) + t.Logf("Apply errors:\n%s", aTestOutput.Stderr()) + + if testBackend.CallCount("POST") != 1 { + t.Fatalf("expected 1 POST call after apply, got %d", testBackend.CallCount("POST")) + } + if testBackend.CallCount("GET") != 5 { + t.Fatalf("expected 5 GET calls after apply, got %d", testBackend.CallCount("GET")) + } + data, err := statefile.Read(bytes.NewBuffer(testBackend.Data)) + if err != nil { + t.Fatal(err) + } + expectedOutputs := map[string]*states.OutputValue{ + "test": { + Addr: addrs.AbsOutputValue{ + OutputValue: addrs.OutputValue{ + Name: "test", + }, + }, + Value: cty.StringVal("test"), + }, + } + if diff := cmp.Diff(expectedOutputs, data.State.RootOutputValues); diff != "" { + t.Fatalf("unexpected data after apply: %s", diff) + } + } + { + log.Printf("[TRACE] %s: beginning second init with state store", t.Name()) + + ssCfg := `terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } + state_store "test_store" { + provider "test" {} + value = "foobar" + } +} +` + if err := os.WriteFile(filepath.Join(td, "main.tf"), []byte(ssCfg), 0644); err != nil { + t.Fatalf("err: %s", err) + } + + ui := cli.NewMockUi() + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: tOverrides, + ProviderSource: providerSource, + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + }, + } + + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + "-force-copy", + } + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) + } + log.Printf("[TRACE] %s: second init with state store complete", t.Name()) + t.Logf("Second run output:\n%s", testOutput.Stdout()) + t.Logf("Second run errors:\n%s", testOutput.Stderr()) + + s := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) + if s.StateStore.Empty() { + t.Fatal("should have StateStore config") + } + if !s.Backend.Empty() { + t.Fatalf("expected backend to be empty") + } + + rawData, ok := mockProvider.MockStates[backend.DefaultStateName].([]byte) + if !ok { + t.Fatalf("expected %q state to exist in %s: %#v", + backend.DefaultStateName, + mockProviderAddress, + mockProvider.MockStates) + } + + data, err := statefile.Read(bytes.NewBuffer(rawData)) + if err != nil { + t.Fatal(err) + } + expectedOutputs := map[string]*states.OutputValue{ + "test": { + Addr: addrs.AbsOutputValue{ + OutputValue: addrs.OutputValue{ + Name: "test", + }, + }, + Value: cty.StringVal("test"), + }, + } + if diff := cmp.Diff(expectedOutputs, data.State.RootOutputValues); diff != "" { + t.Fatalf("unexpected data: %s", diff) + } + } +} + +// TestInit_backend_to_stateStore_noState tests that given no state +// in the source backend, no state is created in the destination state store +// as a result of the migration. +func TestInit_backend_to_stateStore_noState(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + + testBackend := new(httpBackend.TestHTTPBackend) + ts := httptest.NewServer(http.HandlerFunc(testBackend.Handle)) + t.Cleanup(ts.Close) + + cfg := fmt.Sprintf(`terraform { + backend "http" { + address = %q + } +} +`, ts.URL) + if err := os.WriteFile(filepath.Join(td, "main.tf"), []byte(cfg), 0644); err != nil { + t.Fatalf("err: %s", err) + } + t.Chdir(td) + + mockProvider := mockPluggableStateStorageProvider() + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.2.3"}, + }) + defer close() + + tOverrides := &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + } + { + log.Printf("[TRACE] %s: beginning first init with backend", t.Name()) + // Init + ui := cli.NewMockUi() + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + }, + } + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + } + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("first init exited with non-zero code %d:\n%s", code, testOutput.Stderr()) + } + log.Printf("[TRACE] %s: first init complete", t.Name()) + t.Logf("First run output:\n%s", testOutput.Stdout()) + t.Logf("First run errors:\n%s", testOutput.Stderr()) + + if testBackend.CallCount("POST") != 0 { + t.Fatalf("expected 0 POST calls after init, got %d", testBackend.CallCount("POST")) + } + if testBackend.CallCount("GET") != 2 { + t.Fatalf("expected 2 GET calls after init, got %d", testBackend.CallCount("GET")) + } + } + { + log.Printf("[TRACE] %s: beginning second init with state store", t.Name()) + + ssCfg := `terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } + state_store "test_store" { + provider "test" {} + value = "foobar" + } +} +` + if err := os.WriteFile(filepath.Join(td, "main.tf"), []byte(ssCfg), 0644); err != nil { + t.Fatalf("err: %s", err) + } + + ui := cli.NewMockUi() + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: tOverrides, + ProviderSource: providerSource, + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + }, + } + + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + "-force-copy", + } + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("second init exited with non-zero code %d:\n%s", code, testOutput.Stderr()) + } + log.Printf("[TRACE] %s: second init with state store complete", t.Name()) + t.Logf("Second run output:\n%s", testOutput.Stdout()) + t.Logf("Second run errors:\n%s", testOutput.Stderr()) + + s := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) + if s.StateStore.Empty() { + t.Fatal("should have StateStore config") + } + if !s.Backend.Empty() { + t.Fatalf("expected backend to be empty") + } + + if len(mockProvider.MockStates) != 0 { + t.Fatalf("expected no state to exist in %s: %#v", + mockProviderAddress, + mockProvider.MockStates) + } + } +} + +func TestInit_localBackend_to_stateStore(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + + cfg := `terraform { + backend "local" {} +} +` + if err := os.WriteFile(filepath.Join(td, "main.tf"), []byte(cfg), 0644); err != nil { + t.Fatalf("err: %s", err) + } + t.Chdir(td) + + mockProvider := mockPluggableStateStorageProvider() + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.2.3"}, + }) + defer close() + + tOverrides := &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + } + { + log.Printf("[TRACE] %s: beginning first init with local backend", t.Name()) + // Init + ui := cli.NewMockUi() + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + }, + } + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + } + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("first init exited with non-zero code %d:\n%s", code, testOutput.Stderr()) + } + log.Printf("[TRACE] %s: first init complete", t.Name()) + t.Logf("First run output:\n%s", testOutput.Stdout()) + t.Logf("First run errors:\n%s", testOutput.Stderr()) + } + { + // run apply to ensure state isn't empty + // to bypass edge case handling which causes empty state to stop migration + log.Printf("[TRACE] %s: beginning apply with backend", t.Name()) + + outputCfg := `output "test" { + value = "test" +} +` + if err := os.WriteFile(filepath.Join(td, "output.tf"), []byte(outputCfg), 0644); err != nil { + t.Fatalf("err: %s", err) + } + + ui := cli.NewMockUi() + aView, aDone := testView(t) + cApply := &ApplyCommand{ + Meta: Meta{ + Ui: ui, + View: aView, + AllowExperimentalFeatures: true, + }, + } + aCode := cApply.Run([]string{"-auto-approve"}) + aTestOutput := aDone(t) + if aCode != 0 { + t.Fatalf("bad: \n%s", aTestOutput.All()) + } + + t.Logf("Apply output:\n%s", aTestOutput.Stdout()) + t.Logf("Apply errors:\n%s", aTestOutput.Stderr()) + + b, err := os.ReadFile(DefaultStateFilename) + if err != nil { + t.Fatalf("unable to read state file: %s", err) + } + + data, err := statefile.Read(bytes.NewBuffer(b)) + if err != nil { + t.Fatal(err) + } + expectedOutputs := map[string]*states.OutputValue{ + "test": { + Addr: addrs.AbsOutputValue{ + OutputValue: addrs.OutputValue{ + Name: "test", + }, + }, + Value: cty.StringVal("test"), + }, + } + if diff := cmp.Diff(expectedOutputs, data.State.RootOutputValues); diff != "" { + t.Fatalf("unexpected data after apply: %s", diff) + } + } + { + log.Printf("[TRACE] %s: beginning second init with state store", t.Name()) + + ssCfg := `terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } + state_store "test_store" { + provider "test" {} + value = "foobar" + } +} +` + if err := os.WriteFile(filepath.Join(td, "main.tf"), []byte(ssCfg), 0644); err != nil { + t.Fatalf("err: %s", err) + } + + ui := cli.NewMockUi() + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: tOverrides, + ProviderSource: providerSource, + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + }, + } + + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + "-force-copy", + } + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("second init exited with non-zero code %d:\n%s", code, testOutput.Stderr()) + } + log.Printf("[TRACE] %s: second init with state store complete", t.Name()) + t.Logf("Second run output:\n%s", testOutput.Stdout()) + t.Logf("Second run errors:\n%s", testOutput.Stderr()) + + s := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) + if s.StateStore.Empty() { + t.Fatal("should have StateStore config") + } + if !s.Backend.Empty() { + t.Fatalf("expected backend to be empty") + } + + rawData, ok := mockProvider.MockStates[backend.DefaultStateName].([]byte) + if !ok { + t.Fatalf("expected %q state to exist in %s: %#v", + backend.DefaultStateName, + mockProviderAddress, + mockProvider.MockStates) + } + + data, err := statefile.Read(bytes.NewBuffer(rawData)) + if err != nil { + t.Fatal(err) + } + expectedOutputs := map[string]*states.OutputValue{ + "test": { + Addr: addrs.AbsOutputValue{ + OutputValue: addrs.OutputValue{ + Name: "test", + }, + }, + Value: cty.StringVal("test"), + }, + } + if diff := cmp.Diff(expectedOutputs, data.State.RootOutputValues); diff != "" { + t.Fatalf("unexpected data: %s", diff) + } + + if f, err := os.Stat(DefaultStateFilename); err == nil && f.Size() > 0 { + t.Fatalf("expected state file to have been removed at %q. Has size %d bytes.", DefaultStateFilename, f.Size()) + } + } +} + +func TestInit_backend_to_stateStore_multipleWorkspaces(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + + cfg := `terraform { + backend "inmem" {} +} +` + if err := os.WriteFile(filepath.Join(td, "main.tf"), []byte(cfg), 0644); err != nil { + t.Fatalf("err: %s", err) + } + t.Chdir(td) + + mockProvider := mockPluggableStateStorageProvider() + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.2.3"}, + }) + defer close() + + tOverrides := &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + } + + { + log.Printf("[TRACE] %s: beginning first init with backend", t.Name()) + // Init + ui := cli.NewMockUi() + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + }, + } + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + } + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) + } + log.Printf("[TRACE] %s: first init complete", t.Name()) + t.Logf("First run output:\n%s", testOutput.Stdout()) + t.Logf("First run errors:\n%s", testOutput.Stderr()) + } + { + // run apply to ensure state isn't empty + // to bypass edge case handling which causes empty state to stop migration + log.Printf("[TRACE] %s: beginning first apply to default workspace with backend", t.Name()) + + outputCfg := `output "test" { + value = "test" +} +` + if err := os.WriteFile(filepath.Join(td, "output.tf"), []byte(outputCfg), 0644); err != nil { + t.Fatalf("err: %s", err) + } + + ui := cli.NewMockUi() + aView, aDone := testView(t) + cApply := &ApplyCommand{ + Meta: Meta{ + Ui: ui, + View: aView, + AllowExperimentalFeatures: true, + }, + } + aCode := cApply.Run([]string{"-auto-approve"}) + aTestOutput := aDone(t) + if aCode != 0 { + t.Fatalf("bad: \n%s", aTestOutput.All()) + } + + t.Logf("Apply output:\n%s", aTestOutput.Stdout()) + t.Logf("Apply errors:\n%s", aTestOutput.Stderr()) + + data := inmem.ReadState(t, backend.DefaultStateName) + expectedOutputs := map[string]*states.OutputValue{ + "test": { + Addr: addrs.AbsOutputValue{ + OutputValue: addrs.OutputValue{ + Name: "test", + }, + }, + Value: cty.StringVal("test"), + }, + } + if diff := cmp.Diff(expectedOutputs, data.RootOutputValues); diff != "" { + t.Fatalf("unexpected data after apply: %s", diff) + } + } + { + ui := cli.NewMockUi() + aView, aDone := testView(t) + cSelect := &WorkspaceSelectCommand{ + Meta: Meta{ + Ui: ui, + View: aView, + AllowExperimentalFeatures: true, + }, + } + sCode := cSelect.Run([]string{"-or-create", "second"}) + aTestOutput := aDone(t) + if sCode != 0 { + t.Fatalf("unable to select workspace: \n%s", aTestOutput.All()) + } + t.Logf("Select workspace output:\n%s", aTestOutput.All()) + } + { + ui := cli.NewMockUi() + aView, aDone := testView(t) + cApply := &ApplyCommand{ + Meta: Meta{ + Ui: ui, + View: aView, + AllowExperimentalFeatures: true, + }, + } + aCode := cApply.Run([]string{"-auto-approve"}) + aTestOutput := aDone(t) + if aCode != 0 { + t.Fatalf("bad: \n%s", aTestOutput.All()) + } + + t.Logf("Apply output:\n%s", aTestOutput.Stdout()) + t.Logf("Apply errors:\n%s", aTestOutput.Stderr()) + + data := inmem.ReadState(t, "second") + expectedOutputs := map[string]*states.OutputValue{ + "test": { + Addr: addrs.AbsOutputValue{ + OutputValue: addrs.OutputValue{ + Name: "test", + }, + }, + Value: cty.StringVal("test"), + }, + } + if diff := cmp.Diff(expectedOutputs, data.RootOutputValues); diff != "" { + t.Fatalf("unexpected data after apply: %s", diff) + } + } + { + log.Printf("[TRACE] %s: beginning second init with state store", t.Name()) + + ssCfg := `terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } + state_store "test_store" { + provider "test" {} + value = "foobar" + } +} +` + if err := os.WriteFile(filepath.Join(td, "main.tf"), []byte(ssCfg), 0644); err != nil { + t.Fatalf("err: %s", err) + } + + ui := cli.NewMockUi() + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: tOverrides, + ProviderSource: providerSource, + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + }, + } + + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + "-force-copy", + "-migrate-state", + } + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("second init failed: \n%s", testOutput.All()) + } + log.Printf("[TRACE] %s: second init with state store complete", t.Name()) + t.Logf("Second init output:\n%s", testOutput.All()) + + s := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) + if s.StateStore.Empty() { + t.Fatal("should have StateStore config") + } + if !s.Backend.Empty() { + t.Fatalf("expected backend to be empty") + } + + expectedOutputs := map[string]*states.OutputValue{ + "test": { + Addr: addrs.AbsOutputValue{ + OutputValue: addrs.OutputValue{ + Name: "test", + }, + }, + Value: cty.StringVal("test"), + }, + } + + expectedWorkspaces := []string{"default", "second"} + ws := slices.Sorted(maps.Keys(mockProvider.MockStates)) + if diff := cmp.Diff(expectedWorkspaces, ws); diff != "" { + t.Fatalf("unexpected workspaces: %s", diff) + } + + // check default workspace first + rawData, ok := mockProvider.MockStates[backend.DefaultStateName].([]byte) + if !ok { + t.Fatalf("expected %q state to exist in %s: %#v", + backend.DefaultStateName, + mockProviderAddress, + mockProvider.MockStates) + } + stateData, err := statefile.Read(bytes.NewBuffer(rawData)) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(expectedOutputs, stateData.State.RootOutputValues); diff != "" { + t.Fatalf("unexpected data: %s", diff) + } + + // check second workspace + rawData2, ok := mockProvider.MockStates["second"].([]byte) + if !ok { + t.Fatalf("expected %q state to exist in %s: %#v", + backend.DefaultStateName, + mockProviderAddress, + mockProvider.MockStates) + } + stateData2, err := statefile.Read(bytes.NewBuffer(rawData2)) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(expectedOutputs, stateData2.State.RootOutputValues); diff != "" { + t.Fatalf("unexpected data: %s", diff) + } + } +} + +func TestInit_cloud_to_stateStore(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + + ts := cloud.TestServerWithHandlers(t, map[string]func(http.ResponseWriter, *http.Request){ + "/api/v2/organizations/hashicorp/workspaces/test": func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + w.Write([]byte(`{"data":{"id":"ws-TEST","type":"workspaces","attributes":{"allow-destroy-plan":true,"auto-apply":false,"auto-apply-run-trigger":false,"auto-destroy-activity-duration":null,"auto-destroy-at":null,"auto-destroy-status":null,"inherits-project-auto-destroy":true,"created-at":"2022-06-22T14:24:13.836Z","environment":"default","locked":false,"name":"test","queue-all-runs":false,"speculative-enabled":true,"structured-run-output-enabled":true,"terraform-version":"1.10.0","working-directory":null,"global-remote-state":false,"updated-at":"2026-01-29T15:09:18.075Z","resource-count":0,"apply-duration-average":2000,"plan-duration-average":4000,"policy-check-failures":0,"run-failures":0,"workspace-kpis-runs-count":1,"unarchived-workspace-change-requests-count":0,"latest-change-at":"2026-01-29T15:09:17.200Z","operations":true,"execution-mode":"remote","vcs-repo":null,"vcs-repo-identifier":null,"permissions":{"can-update":true,"can-destroy":true,"can-queue-run":true,"can-read-run":true,"can-read-variable":true,"can-update-variable":true,"can-read-state-versions":true,"can-read-state-outputs":true,"can-create-state-versions":true,"can-queue-apply":true,"can-lock":true,"can-unlock":true,"can-force-unlock":true,"can-read-settings":true,"can-manage-tags":true,"can-manage-run-tasks":true,"can-force-delete":true,"can-manage-assessments":true,"can-manage-ephemeral-workspaces":false,"can-read-assessment-results":true,"can-read-change-requests":false,"can-update-change-requests":false,"can-queue-destroy":true},"actions":{"is-destroyable":true},"description":null,"file-triggers-enabled":true,"trigger-prefixes":[],"trigger-patterns":[],"assessments-enabled":false,"last-assessment-result-at":null,"locked-reason":"","source":"terraform","source-name":null,"source-url":null,"tag-names":[],"setting-overwrites":{"execution-mode":true,"agent-pool":true}},"relationships":{"organization":{"data":{"id":"hashicorp","type":"organizations"}},"current-run":{"data":{"id":"run-TEST","type":"runs"},"links":{"related":"/api/v2/runs/run-TEST"}},"latest-run":{"data":{"id":"run-TEST","type":"runs"},"links":{"related":"/api/v2/runs/run-TEST"}},"outputs":{"data":[{"id":"wsout-TEST","type":"workspace-outputs"}],"links":{"related":"/api/v2/workspaces/ws-TEST/current-state-version-outputs"}},"remote-state-consumers":{"links":{"related":"/api/v2/workspaces/ws-TEST/relationships/remote-state-consumers"}},"current-state-version":{"data":{"id":"sv-TEST","type":"state-versions"},"links":{"related":"/api/v2/workspaces/ws-TEST/current-state-version"}},"current-configuration-version":{"data":{"id":"cv-TEST","type":"configuration-versions"},"links":{"related":"/api/v2/configuration-versions/cv-TEST"}},"agent-pool":{"data":null},"readme":{"data":null},"project":{"data":{"id":"prj-TEST","type":"projects"}},"current-assessment-result":{"data":null},"vars":{"data":[]}},"links":{"self":"/api/v2/organizations/hashicorp/workspaces/test","self-html":"/app/hashicorp/workspaces/test"}}}`)) + w.WriteHeader(http.StatusOK) + return + } + }, + "/api/v2/workspaces/ws-TEST/current-state-version": func(w http.ResponseWriter, r *http.Request) { + hostname := r.URL.Hostname() + w.Write(fmt.Appendf([]byte{}, `{"data":{"id":"sv-TEST","type":"state-versions","attributes":{"created-at":"2026-01-29T15:09:17.200Z","size":651,"hosted-state-download-url":"%s/api/state-versions/sv-TEST/hosted_state","hosted-json-state-download-url":"%s/api/state-versions/sv-TEST/hosted_json_state","modules":{},"providers":{},"resources-processed":true,"serial":1,"state-version":4,"status":"finalized","terraform-version":"1.10.0","vcs-commit-url":null,"vcs-commit-sha":null,"resources":[],"billable-rum-count":0},"relationships":{"run":{"data":{"id":"run-TEST","type":"runs"}},"rollback-state-version":{"data":null},"created-by":{"data":{"id":"user-TEST","type":"users"},"links":{"self":"/api/v2/users/user-TEST","related":"/api/v2/runs/run-TEST/created-by"}},"workspace":{"data":{"id":"ws-TEST","type":"workspaces"}},"outputs":{"data":[{"id":"wsout-TEST","type":"state-version-outputs"}],"links":{"related":"/api/v2/state-versions/sv-TEST/outputs"}}},"links":{"self":"/api/v2/state-versions/sv-TEST"}}}`, hostname, hostname)) + w.WriteHeader(http.StatusOK) + }, + "/api/state-versions/sv-TEST/hosted_state": func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"version":4,"terraform_version":"1.15.0","serial":1,"lineage":"91adaece-23b3-7bce-0695-5aea537d2fef","outputs":{"test":{"value":"test","type":"string"}},"resources":[],"check_results":null}`)) + w.WriteHeader(http.StatusOK) + }, + }) + t.Cleanup(ts.Close) + mockURL, err := url.Parse(ts.URL) + if err != nil { + t.Fatal(err) + } + + backendInit.Init(testDisco(ts)) + t.Cleanup(func() { backendInit.Init(nil) }) + + cfg := fmt.Sprintf(`terraform { + cloud { + hostname = %q + organization = "hashicorp" + token = "test-token" + workspaces { + name = "test" + } + } +} +`, mockURL.Host) + if err := os.WriteFile(filepath.Join(td, "main.tf"), []byte(cfg), 0644); err != nil { + t.Fatalf("err: %s", err) + } + t.Chdir(td) + + mockProvider := mockPluggableStateStorageProvider() + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.2.3"}, + }) + defer close() + + tOverrides := &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + } + + { + log.Printf("[TRACE] %s: beginning first init with backend", t.Name()) + // Init + ui := cli.NewMockUi() + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + Services: testDisco(ts), + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + }, + } + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + } + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) + } + log.Printf("[TRACE] %s: first init complete", t.Name()) + t.Logf("First run output:\n%s", testOutput.Stdout()) + t.Logf("First run errors:\n%s", testOutput.Stderr()) + } + { + log.Printf("[TRACE] %s: beginning second init with state store", t.Name()) + + ssCfg := `terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } + state_store "test_store" { + provider "test" {} + value = "foobar" + } +} +` + if err := os.WriteFile(filepath.Join(td, "main.tf"), []byte(ssCfg), 0644); err != nil { + t.Fatalf("err: %s", err) + } + + ui := cli.NewMockUi() + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + Services: testDisco(ts), + testingOverrides: tOverrides, + ProviderSource: providerSource, + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + }, + } + + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + } + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("expected migration from cloud to fail: \n%s", testOutput.Stdout()) + } + log.Printf("[TRACE] %s: second init with state store complete", t.Name()) + expectedMsg := "Migrating state from HCP Terraform or Terraform Enterprise to another backend is not \nyet implemented." + if !strings.Contains(testOutput.Stderr(), expectedMsg) { + t.Fatalf("expected error message %q not found: \n%s", expectedMsg, testOutput.Stderr()) + } + } +} + // newMockProviderSource is a helper to succinctly construct a mock provider // source that contains a set of packages matching the given provider versions // that are available for installation (from temporary local files). diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index cd68ad1f64..238a6eb6f0 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -1159,11 +1159,16 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di stateStoreConfig.Provider.Name, stateStoreConfig.ProviderAddr, ) - return nil, diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Not implemented yet", - Detail: "Migration from backend to state store is not implemented yet", - }) + + if !opts.Init { + initReason := fmt.Sprintf("Migrating from backend %q to state store %q in provider %s (%q)", + s.Backend.Type, stateStoreConfig.Type, + stateStoreConfig.Provider.Name, stateStoreConfig.ProviderAddr) + diags = diags.Append(errBackendInitDiag(initReason)) + return nil, diags + } + + return m.backend_to_stateStore(s.Backend, sMgr, stateStoreConfig, cHash, opts) // Potentially changing a backend configuration case backendConfig != nil && !s.Backend.Empty() && @@ -1958,6 +1963,198 @@ func (m *Meta) backend(configPath string, viewType arguments.ViewType) (backendr return be, diags } +func (m *Meta) backend_to_stateStore(bcs *workdir.BackendConfigState, sMgr *clistate.LocalState, c *configs.StateStore, cHash int, opts *BackendOpts) (backend.Backend, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + vt := arguments.ViewJSON + // Set default viewtype if none was set as the StateLocker needs to know exactly + // what viewType we want to have. + if opts == nil || opts.ViewType != vt { + vt = arguments.ViewHuman + } + + s := sMgr.State() + + cloudMode := cloud.DetectConfigChangeType(bcs, nil, false) + diags = diags.Append(m.assertSupportedCloudInitOptions(cloudMode)) + if diags.HasErrors() { + return nil, diags + } + + view := views.NewInit(vt, m.View) + if cloudMode == cloud.ConfigMigrationOut { + view.Output(views.BackendCloudMigrateStateStoreMessage, c.Type) + } else { + view.Output(views.BackendMigrateStateStoreMessage, bcs.Type, c.Type) + } + + // Initialize the configured backend + b, moreDiags := m.savedBackend(sMgr) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return nil, diags + } + + // Get the state store as an instance of backend.Backend + ssBackend, storeConfigVal, providerConfigVal, moreDiags := m.stateStoreInitFromConfig(c, opts.Locks) + diags = diags.Append(moreDiags) + if diags.HasErrors() { + return nil, diags + } + + // Perform the migration + err := m.backendMigrateState(&backendMigrateOpts{ + SourceType: bcs.Type, + DestinationType: c.Type, + Source: b, + Destination: ssBackend, + ViewType: vt, + }) + if err != nil { + diags = diags.Append(err) + return nil, diags + } + + rDiags := m.removeLocalState(bcs.Type, b) + if rDiags.HasErrors() { + diags = diags.Append(rDiags) + return nil, diags + } + + if m.stateLock { + view := views.NewStateLocker(vt, m.View) + stateLocker := clistate.NewLocker(m.stateLockTimeout, view) + if err := stateLocker.Lock(sMgr, "init is initializing state_store first time"); err != nil { + diags = diags.Append(fmt.Errorf("Error locking state: %s", err)) + return nil, diags + } + defer stateLocker.Unlock() + } + + // Store the state_store metadata in our saved state location + + var pVersion *version.Version // This will remain nil for builtin providers or unmanaged providers. + if c.ProviderAddr.IsBuiltIn() { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "State storage is using a builtin provider", + Detail: "Terraform is using a builtin provider for initializing state storage. Terraform will be less able to detect when state migrations are required in future init commands.", + }) + } else { + isReattached, err := reattach.IsProviderReattached(c.ProviderAddr, os.Getenv("TF_REATTACH_PROVIDERS")) + if err != nil { + diags = diags.Append(fmt.Errorf("Unable to determine if state storage provider is reattached while initializing state store for the first time. This is a bug in Terraform and should be reported: %w", err)) + return nil, diags + } + if isReattached { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "State storage provider is not managed by Terraform", + Detail: "Terraform is using a provider supplied via TF_REATTACH_PROVIDERS for initializing state storage. Terraform will be less able to detect when state migrations are required in future init commands.", + }) + } else { + // The provider is not built in and is being managed by Terraform + // This is the most common scenario, by far. + var vDiags tfdiags.Diagnostics + pVersion, vDiags = getStateStorageProviderVersion(c, opts.Locks) + diags = diags.Append(vDiags) + if vDiags.HasErrors() { + return nil, diags + } + } + } + + // Update the stored metadata + s.Backend = nil + s.StateStore = &workdir.StateStoreConfigState{ + Type: c.Type, + Hash: uint64(cHash), + Provider: &workdir.ProviderConfigState{ + Source: &c.ProviderAddr, + Version: pVersion, + }, + } + err = s.StateStore.SetConfig(storeConfigVal, ssBackend.ConfigSchema()) + if err != nil { + diags = diags.Append(fmt.Errorf("Failed to set state store configuration: %w", err)) + return nil, diags + } + + // We need to briefly convert away from backend.Backend interface to use the method + // for accessing the provider schema. In this method we _always_ expect the concrete value + // to be backendPluggable.Pluggable. + plug := ssBackend.(*backendPluggable.Pluggable) + err = s.StateStore.Provider.SetConfig(providerConfigVal, plug.ProviderSchema()) + if err != nil { + diags = diags.Append(fmt.Errorf("Failed to set state store provider configuration: %w", err)) + return nil, diags + } + + // Update backend state file + if err := sMgr.WriteState(s); err != nil { + diags = diags.Append(errBackendWriteSavedDiag(err)) + return nil, diags + } + if err := sMgr.PersistState(); err != nil { + diags = diags.Append(errBackendWriteSavedDiag(err)) + return nil, diags + } + + return b, diags +} + +func (m *Meta) removeLocalState(backendType string, b backend.Backend) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + if backendType != "local" { + return diags + } + + workspaces, wDiags := b.Workspaces() + if wDiags.HasErrors() { + diags = diags.Append(&errBackendLocalRead{wDiags.Err()}) + return diags + } + + var localStates []statemgr.Full + for _, workspace := range workspaces { + localState, sDiags := b.StateMgr(workspace) + if sDiags.HasErrors() { + diags = diags.Append(&errBackendLocalRead{sDiags.Err()}) + return diags + } + if err := localState.RefreshState(); err != nil { + diags = diags.Append(&errBackendLocalRead{err}) + return diags + } + + // We only care about non-empty states. + if localS := localState.State(); !localS.Empty() { + log.Printf("[TRACE] Meta.Backend: will need to migrate workspace states because of existing %q workspace", workspace) + localStates = append(localStates, localState) + } else { + log.Printf("[TRACE] Meta.Backend: ignoring local %q workspace because its state is empty", workspace) + } + } + + if len(localStates) > 0 { + log.Printf("[TRACE] Meta.removeLocalState: removing old state snapshots (%d) from old backend", len(localStates)) + for idx, localState := range localStates { + // We always delete the local state, unless that was our new state too. + if err := localState.WriteState(nil); err != nil { + diags = diags.Append(&errBackendMigrateLocalDelete{err}) + return diags + } + if err := localState.PersistState(nil); err != nil { + diags = diags.Append(&errBackendMigrateLocalDelete{err}) + return diags + } + log.Printf("[DEBUG] Meta.removeLocalState: deleted local state for workspace %q", workspaces[idx]) + } + } + return diags +} + //------------------------------------------------------------------- // State Store Config Scenarios // The functions below cover handling all the various scenarios that diff --git a/internal/command/meta_backend_test.go b/internal/command/meta_backend_test.go index 22320c1371..7901e9ab41 100644 --- a/internal/command/meta_backend_test.go +++ b/internal/command/meta_backend_test.go @@ -2262,56 +2262,6 @@ func Test_determineInitReason(t *testing.T) { } } -// Changing from using backend to state_store -// -// TODO(SarahFrench/radeksimko): currently this test only confirms that we're hitting the switch -// case for this scenario, and will need to be updated when that init feature is implemented. -func TestMetaBackend_configuredBackendToStateStore(t *testing.T) { - td := t.TempDir() - testCopyDir(t, testFixturePath("backend-to-state-store"), td) - t.Chdir(td) - - mock := testStateStoreMock(t) - - // Setup the meta - m := testMetaBackend(t, nil) - m.testingOverrides = metaOverridesForProvider(mock) - m.AllowExperimentalFeatures = true - - // Get the state store's config - mod, loadDiags := m.loadSingleModule(td) - if loadDiags.HasErrors() { - t.Fatalf("unexpected error when loading test config: %s", loadDiags.Err()) - } - - // Get the operations backend - locks := depsfile.NewLocks() - providerAddr := addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test") - constraint, err := providerreqs.ParseVersionConstraints(">1.0.0") - if err != nil { - t.Fatalf("test setup failed when making constraint: %s", err) - } - locks.SetProvider( - providerAddr, - versions.MustParseVersion("9.9.9"), - constraint, - []providerreqs.Hash{""}, - ) - _, beDiags := m.Backend(&BackendOpts{ - Init: true, - StateStoreConfig: mod.StateStore, - ProviderRequirements: mod.ProviderRequirements, - Locks: locks, - }) - if !beDiags.HasErrors() { - t.Fatal("expected an error to be returned during partial implementation of PSS") - } - wantErr := "Migration from backend to state store is not implemented yet" - if !strings.Contains(beDiags.Err().Error(), wantErr) { - t.Fatalf("expected the returned error to contain %q, but got: %s", wantErr, beDiags.Err()) - } -} - // Verify that using variables results in an error func TestMetaBackend_configureStateStoreVariableUse(t *testing.T) { wantErr := "Variables not allowed" diff --git a/internal/command/views/init.go b/internal/command/views/init.go index d4997af929..bb9775aab0 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -303,6 +303,14 @@ var MessageRegistry map[InitMessageCode]InitMessage = map[InitMessageCode]InitMe HumanValue: "Migrating from HCP Terraform or Terraform Enterprise to local state.", JSONValue: "Migrating from HCP Terraform or Terraform Enterprise to local state.", }, + "backend_cloud_migrate_state_store": { + HumanValue: "Migrating from HCP Terraform Terraform Enterprise to state store %q.", + JSONValue: "Migrating from HCP Terraform Terraform Enterprise to state store %q.", + }, + "backend_migrate_state_store": { + HumanValue: "Migrating from backend %q to state store %q.", + JSONValue: "Migrating from backend %q to state store %q.", + }, "state_store_migrate_local": { HumanValue: stateMigrateLocalHuman, JSONValue: stateMigrateLocalJSON, @@ -365,6 +373,10 @@ const ( BackendMigrateLocalMessage InitMessageCode = "backend_migrate_local" // BackendCloudMigrateLocalMessage indicates migration from cloud to local BackendCloudMigrateLocalMessage InitMessageCode = "backend_cloud_migrate_local" + // BackendCloudMigrateStateStoreMessage indicates migration from cloud to a state store + BackendCloudMigrateStateStoreMessage InitMessageCode = "backend_cloud_migrate_state_store" + // BackendMigrateStateStoreMessage indicates migration from a backend to a state store + BackendMigrateStateStoreMessage InitMessageCode = "backend_migrate_state_store" // StateMigrateLocalMessage indicates migration from state store to local StateMigrateLocalMessage InitMessageCode = "state_store_migrate_local" // FindingMatchingVersionMessage indicates that Terraform is looking for a provider version that matches the constraint during installation