PSS: Migration from a backend to a state store (#38048)
Some checks are pending
build / Determine intended Terraform version (push) Waiting to run
build / Determine Go toolchain version (push) Waiting to run
build / Generate release metadata (push) Blocked by required conditions
build / Build for freebsd_386 (push) Blocked by required conditions
build / Build for linux_386 (push) Blocked by required conditions
build / Build for openbsd_386 (push) Blocked by required conditions
build / Build for windows_386 (push) Blocked by required conditions
build / Build for darwin_amd64 (push) Blocked by required conditions
build / Build for freebsd_amd64 (push) Blocked by required conditions
build / Build for linux_amd64 (push) Blocked by required conditions
build / Build for openbsd_amd64 (push) Blocked by required conditions
build / Build for solaris_amd64 (push) Blocked by required conditions
build / Build for windows_amd64 (push) Blocked by required conditions
build / Build for freebsd_arm (push) Blocked by required conditions
build / Build for linux_arm (push) Blocked by required conditions
build / Build for darwin_arm64 (push) Blocked by required conditions
build / Build for linux_arm64 (push) Blocked by required conditions
build / Build for windows_arm64 (push) Blocked by required conditions
build / Build Docker image for linux_386 (push) Blocked by required conditions
build / Build Docker image for linux_amd64 (push) Blocked by required conditions
build / Build Docker image for linux_arm (push) Blocked by required conditions
build / Build Docker image for linux_arm64 (push) Blocked by required conditions
build / Build e2etest for linux_386 (push) Blocked by required conditions
build / Build e2etest for windows_386 (push) Blocked by required conditions
build / Build e2etest for darwin_amd64 (push) Blocked by required conditions
build / Build e2etest for linux_amd64 (push) Blocked by required conditions
build / Build e2etest for windows_amd64 (push) Blocked by required conditions
build / Build e2etest for linux_arm (push) Blocked by required conditions
build / Build e2etest for darwin_arm64 (push) Blocked by required conditions
build / Build e2etest for linux_arm64 (push) Blocked by required conditions
build / Run e2e test for linux_386 (push) Blocked by required conditions
build / Run e2e test for windows_386 (push) Blocked by required conditions
build / Run e2e test for darwin_amd64 (push) Blocked by required conditions
build / Run e2e test for linux_amd64 (push) Blocked by required conditions
build / Run e2e test for windows_amd64 (push) Blocked by required conditions
build / Run e2e test for linux_arm (push) Blocked by required conditions
build / Run e2e test for linux_arm64 (push) Blocked by required conditions
build / Run terraform-exec test for linux amd64 (push) Blocked by required conditions
Quick Checks / Unit Tests (push) Waiting to run
Quick Checks / Race Tests (push) Waiting to run
Quick Checks / End-to-end Tests (push) Waiting to run
Quick Checks / Code Consistency Checks (push) Waiting to run

* backend/inmem: Make it easier to use the backend in tests

* cloud: Make the cloud backend testable

* command/views: Introduce migration UI messages

* command/init: Add 3 tests for migrations from a backend to PSS

 - TestInit_backend_to_stateStore_singleWorkspace
 - TestInit_backend_to_stateStore_multipleWorkspaces
 - TestInit_cloud_to_stateStore

* command/init: Implement migration from a backend to PSS

* address PR feedback

* remove local state after migration
This commit is contained in:
Radek Simko 2026-02-10 11:39:33 +00:00 committed by GitHub
parent a48e873790
commit 27770ee805
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1182 additions and 80 deletions

View file

@ -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

View file

@ -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
}

View file

@ -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{

View file

@ -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)
}

View file

@ -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).

View file

@ -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

View file

@ -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"

View file

@ -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