blob: 95d8185e5287d82f085de3e71d1be398a60b8d48 [file] [edit]
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/command/clistate"
"github.com/hashicorp/terraform/internal/command/workdir"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/providers"
testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
"github.com/hashicorp/terraform/internal/states/statefile"
)
func TestStateMigrate_fromBackendToBackend(t *testing.T) {
fixture := "state-migrate-backend-to-backend"
wd := tempWorkingDirFixture(t, fixture)
t.Chdir(wd.RootModuleDir())
ui := testUiWrapped(t)
view, done := testView(t)
c := &StateMigrateCommand{
Meta: Meta{
Ui: ui,
View: view,
WorkingDir: wd,
AllowExperimentalFeatures: true,
},
}
_ = testInputMap(t, map[string]string{
"backend-migrate-copy-to-empty": "yes",
})
args := []string{"-no-color"}
code := c.Run(args)
out := done(t)
if code != 0 {
t.Fatalf("expected exit code 1, got %d\nstderr: %q", code, out.Stderr())
}
// Assert expected human output is made
checkGoldenReferenceHumanOutput(t, out, fixture)
// Assert the migrated state contains expected content
f, err := os.Open("destination-backend.tfstate")
if err != nil {
t.Fatalf("failed to read migrated state: %s", err)
}
t.Cleanup(func() {
err := f.Close()
if err != nil {
t.Fatal(err)
}
})
s, err := statefile.Read(f)
if err != nil {
t.Fatal(err)
}
_, ok := s.State.RootOutputValues["test"]
if !ok {
t.Fatalf("unable to find test output in migrated state")
}
// Assert the backend state file describes the new backend location,
statePath := filepath.Join(c.DataDir(), DefaultStateFilename)
sMgr := &clistate.LocalState{Path: statePath}
if err := sMgr.RefreshState(); err != nil {
t.Fatal(err)
}
backendState := sMgr.State()
if backendState.StateStore != nil {
t.Fatalf("expected backend state file to not describe a state_store during backend=>backend, but it's set")
}
if backendState.Backend == nil {
t.Fatalf("expected backend state file to describe a backend during backend=>backend, but it's nil")
}
if backendState.Backend.Type != "local" {
t.Fatalf("expected backend state file to describe the destination backend \"local\", but got %q", backendState.Backend.Type)
}
if got, want := normalizeJSON(t, backendState.Backend.ConfigRaw), `{"path":"destination-backend.tfstate","workspace_dir":null}`; got != want {
t.Errorf("wrong config\ngot: %s\nwant: %s", got, want)
}
}
func TestStateMigrate_fromBackendToStateStore(t *testing.T) {
fixture := "state-migrate-backend-to-state-store"
wd := tempWorkingDirFixture(t, fixture)
t.Chdir(wd.RootModuleDir())
p := mockPluggableStateStorageProvider(mockSingleStateStoreSchema("test_store"))
p.MockStates = testing_provider.NewMockStateBytesWithStateIds("test_store", []string{"default"})
providerSource := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.2.3"},
})
ui := testUiWrapped(t)
view, done := testView(t)
c := &StateMigrateCommand{
Meta: Meta{
Ui: ui,
View: view,
WorkingDir: wd,
AllowExperimentalFeatures: true,
testingOverrides: metaOverridesForProvider(p),
ProviderSource: providerSource,
},
}
_ = testInputMap(t, map[string]string{
"backend-migrate-copy-to-empty": "yes",
})
args := []string{"-no-color"}
code := c.Run(args)
out := done(t)
if code != 0 {
t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr())
}
// Assert expected human output is made
checkGoldenReferenceHumanOutput(t, out, fixture)
// Assert the migrated state contains expected content
b, err := p.MockStates.Read("test_store", "default")
if err != nil {
t.Fatalf("unable to find migrated state in mock provider: %s", err)
}
s, err := statefile.Read(bytes.NewBuffer(b))
if err != nil {
t.Fatal(err)
}
_, ok := s.State.RootOutputValues["test"]
if !ok {
t.Fatalf("unable to find test output in migrated state")
}
// Assert the backend state file describes the new state store location.
statePath := filepath.Join(c.DataDir(), DefaultStateFilename)
sMgr := &clistate.LocalState{Path: statePath}
if err := sMgr.RefreshState(); err != nil {
t.Fatal(err)
}
backendState := sMgr.State()
if backendState.Backend != nil {
t.Fatalf("expected backend state file to not describe a backend during backend=>state_store migration, but it's set")
}
if backendState.StateStore == nil {
t.Fatalf("expected backend state file to describe a state store during backend=>state_store migration, but it's nil")
}
if backendState.StateStore.Provider.Source.Type != "test" {
t.Fatalf("expected backend state file to describe the destination state store provider \"test\", but got %q", backendState.StateStore.Provider)
}
if backendState.StateStore.Provider.Version.String() != "1.2.3" {
t.Fatalf("expected backend state file to describe the destination state store provider version \"1.2.3\", but got %q", backendState.StateStore.Provider.Version)
}
if backendState.StateStore.Type != "test_store" {
t.Fatalf("expected backend state file to describe the destination state store type \"test_store\", but got %q", backendState.StateStore.Type)
}
}
// Testing migration between two state stores in a single provider.
// Different cases describe whether the source provider is already in the dependency lock file or not.
func TestStateMigrate_fromStateStoreToStateStore_inSingleProvider(t *testing.T) {
// All test cases perform a migration from test_src to test_dst store implementations in v1.2.3 of hashicorp/test provider.
// So a common assertion func can be re-used.
assertBackendStateFile := func(t *testing.T, c *StateMigrateCommand) {
statePath := filepath.Join(c.DataDir(), DefaultStateFilename)
sMgr := &clistate.LocalState{Path: statePath}
if err := sMgr.RefreshState(); err != nil {
t.Fatal(err)
}
backendState := sMgr.State()
if backendState.Backend != nil {
t.Fatalf("expected backend state file to not describe a backend during state_store=>state_store migration, but it's set")
}
if backendState.StateStore == nil {
t.Fatalf("expected backend state file to describe a state store during state_store=>state_store migration, but it's nil")
}
if backendState.StateStore.Provider.Source.Type != "test" {
t.Fatalf("expected backend state file to describe the destination state store provider \"test\", but got %q", backendState.StateStore.Provider)
}
if backendState.StateStore.Provider.Version.String() != "1.2.3" {
t.Fatalf("expected backend state file to describe the destination state store provider version \"1.2.3\", but got %q", backendState.StateStore.Provider.Version)
}
if backendState.StateStore.Type != "test_dst" {
t.Fatalf("expected backend state file to describe the destination state store type \"test_dst\", but got %q", backendState.StateStore.Type)
}
// The state store schema is empty, so not even any null fields in expected JSON
if got, want := normalizeJSON(t, backendState.StateStore.ConfigRaw), `{}`; got != want {
t.Errorf("wrong config\ngot: %s\nwant: %s", got, want)
}
// The provider schema includes a 'region' attr, but it's unset in config; null.
if got, want := normalizeJSON(t, backendState.StateStore.Provider.ConfigRaw), `{"region":null}`; got != want {
t.Errorf("wrong config\ngot: %s\nwant: %s", got, want)
}
}
t.Run("provider is already in the dependency lock file", func(t *testing.T) {
wd := tempWorkingDirFixture(t, "state-migrate-state-store-to-state-store")
t.Chdir(wd.RootModuleDir())
b, err := os.ReadFile("source-pss.tfstate")
if err != nil {
t.Fatal(err)
}
pssSchemas := map[string]providers.Schema{
"test_src": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{},
},
},
"test_dst": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{},
},
},
}
p := mockPluggableStateStorageProvider(pssSchemas)
p.MockStates = testing_provider.MockStateBytes{
"test_src": map[string][]byte{"default": []byte(b)},
"test_dst": map[string][]byte{},
}
providerSource := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.2.3"},
})
ui := testUiWrapped(t)
view, done := testView(t)
c := &StateMigrateCommand{
Meta: Meta{
Ui: ui,
View: view,
WorkingDir: wd,
AllowExperimentalFeatures: true,
testingOverrides: metaOverridesForProvider(p),
ProviderSource: providerSource,
},
}
_ = testInputMap(t, map[string]string{
"backend-migrate-copy-to-empty": "yes",
})
args := []string{"-no-color"}
code := c.Run(args)
out := done(t)
if code != 0 {
t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr())
}
expectedMsg := []string{
`Migrating state from state store "test_src" (hashicorp/test) to state store "test_dst" (hashicorp/test)...`,
"Initializing provider plugin for state store \"test_src\"...\n- Reusing previous version of hashicorp/test from the dependency lock file",
"Initializing provider plugin for state store \"test_dst\"...\n- Reusing previous version of hashicorp/test from the dependency lock file",
}
for _, expectedMsg := range expectedMsg {
if !strings.Contains(out.Stdout(), expectedMsg) {
t.Fatalf("expected output %q, got %q", expectedMsg, out.Stdout())
}
}
b, err = p.MockStates.Read("test_dst", "default")
if err != nil {
t.Fatalf("unable to find migrated state in mock provider: %s", err)
}
s, err := statefile.Read(bytes.NewBuffer(b))
if err != nil {
t.Fatal(err)
}
_, ok := s.State.RootOutputValues["test"]
if !ok {
t.Fatalf("unable to find test output in migrated state")
}
// Assert the backend state file describes the new state store location.
assertBackendStateFile(t, c)
})
t.Run("no existing dependency lock file: provider is downloaded and added to the dependency lock file", func(t *testing.T) {
wd := tempWorkingDirFixture(t, "state-migrate-state-store-to-state-store")
t.Chdir(wd.RootModuleDir())
// In this scenario, there is no provider in the dep lock file
// Achieve this by truncating the dep lock file from the copied fixtures
if err := os.Truncate(filepath.Join(wd.RootModuleDir(), dependencyLockFilename), 0); err != nil {
t.Fatalf("error while truncating lock file during test setup: %s", err)
}
b, err := os.ReadFile("source-pss.tfstate")
if err != nil {
t.Fatal(err)
}
pssSchemas := map[string]providers.Schema{
"test_src": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{},
},
},
"test_dst": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{},
},
},
}
p := mockPluggableStateStorageProvider(pssSchemas)
p.MockStates = testing_provider.MockStateBytes{
"test_src": map[string][]byte{"default": []byte(b)},
"test_dst": map[string][]byte{},
}
providerSource := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.2.3"},
})
ui := testUiWrapped(t)
view, done := testView(t)
c := &StateMigrateCommand{
Meta: Meta{
Ui: ui,
View: view,
WorkingDir: wd,
AllowExperimentalFeatures: true,
testingOverrides: metaOverridesForProvider(p),
ProviderSource: providerSource,
},
}
_ = testInputMap(t, map[string]string{
"backend-migrate-copy-to-empty": "yes",
})
args := []string{"-no-color"}
code := c.Run(args)
out := done(t)
if code != 0 {
t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr())
}
expectedMsg := []string{
`Migrating state from state store "test_src" (hashicorp/test) to state store "test_dst" (hashicorp/test)...`,
"Initializing provider plugin for state store \"test_src\"...\n- Finding hashicorp/test versions matching \"1.2.3\"...\n- Installing hashicorp/test v1.2.3...\n- Installed hashicorp/test v1.2.3 (verified checksum)",
"Initializing provider plugin for state store \"test_dst\"...\n- Reusing previous version of hashicorp/test from the dependency lock file\n- Using previously-installed hashicorp/test v1.2.3",
}
for _, expectedMsg := range expectedMsg {
if !strings.Contains(out.Stdout(), expectedMsg) {
t.Fatalf("expected output %q, got %q", expectedMsg, out.Stdout())
}
}
// Assert the state is migrated successfully to the
// destination state store by inspecting the mock.
b, err = p.MockStates.Read("test_dst", "default")
if err != nil {
t.Fatalf("unable to find migrated state in mock provider: %s", err)
}
s, err := statefile.Read(bytes.NewBuffer(b))
if err != nil {
t.Fatal(err)
}
_, ok := s.State.RootOutputValues["test"]
if !ok {
t.Fatalf("unable to find test output in migrated state")
}
// Assert the provider is added to the dependency lock file
lockFilePath := filepath.Join(wd.RootModuleDir(), dependencyLockFilename)
lockFileBytes, err := os.ReadFile(lockFilePath)
if err != nil {
t.Fatalf("unable to read dependency lock file: %s", err)
}
if !strings.Contains(string(lockFileBytes), "hashicorp/test") {
t.Fatalf("expected provider hashicorp/test to be added to the dependency lock file, got: %s", string(lockFileBytes))
}
// Assert the backend state file describes the new state store location.
assertBackendStateFile(t, c)
})
}
// Test migration between two state stores in different providers.
// Different cases describe whether the source provider is already in the dependency lock file or not.
func TestStateMigrate_fromStateStoreToStateStore_inDifferentProviders(t *testing.T) {
// All test cases perform a migration to hashicorp/test2, version v3.2.1, test2_store store.
// So a common assertion func can be re-used.
assertBackendStateFile := func(t *testing.T, c *StateMigrateCommand) {
statePath := filepath.Join(c.DataDir(), DefaultStateFilename)
sMgr := &clistate.LocalState{Path: statePath}
if err := sMgr.RefreshState(); err != nil {
t.Fatal(err)
}
backendState := sMgr.State()
if backendState.Backend != nil {
t.Fatalf("expected backend state file to not describe a backend during state_store=>state_store migration, but it's set")
}
if backendState.StateStore == nil {
t.Fatalf("expected backend state file to describe a state store during state_store=>state_store migration, but it's nil")
}
if backendState.StateStore.Provider.Source.Type != "test2" {
t.Fatalf("expected backend state file to describe the destination state store provider \"test2\", but got %q", backendState.StateStore.Provider)
}
if backendState.StateStore.Provider.Version.String() != "3.2.1" {
t.Fatalf("expected backend state file to describe the destination state store provider version \"3.2.1\", but got %q", backendState.StateStore.Provider.Version)
}
if backendState.StateStore.Type != "test2_store" {
t.Fatalf("expected backend state file to describe the destination state store type \"test2_store\", but got %q", backendState.StateStore.Type)
}
// The state store schema is empty, so not even any null fields in expected JSON
if got, want := normalizeJSON(t, backendState.StateStore.ConfigRaw), `{}`; got != want {
t.Errorf("wrong config\ngot: %s\nwant: %s", got, want)
}
// The provider schema includes a 'region' attr, but it's unset in config; null.
if got, want := normalizeJSON(t, backendState.StateStore.Provider.ConfigRaw), `{"region":"foobar"}`; got != want {
t.Errorf("wrong config\ngot: %s\nwant: %s", got, want)
}
}
t.Run("source provider already in the dependency lock file, destination is not", func(t *testing.T) {
fixture := "state-store-changed/provider-used"
wd := tempWorkingDirFixture(t, fixture)
t.Chdir(wd.RootModuleDir())
b, err := os.ReadFile("source-pss.tfstate")
if err != nil {
t.Fatal(err)
}
// hashicorp/test
sourcePssSchema := map[string]providers.Schema{
"test_src": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{},
},
},
}
sourceProvider := mockPluggableStateStorageProvider(sourcePssSchema)
sourceProvider.MockStates = testing_provider.MockStateBytes{
"test_src": map[string][]byte{"default": []byte(b)},
}
// hashicorp/test2
destinationPssSchema := map[string]providers.Schema{
"test2_store": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{},
},
},
}
destinationProvider := mockPluggableStateStorageProvider(destinationPssSchema)
destinationProvider.MockStates = testing_provider.MockStateBytes{
"test2_store": map[string][]byte{}, // No existing state in the destination
}
providerSource := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.2.3"},
"hashicorp/test2": {"3.2.1"},
})
ui := testUiWrapped(t)
view, done := testView(t)
c := &StateMigrateCommand{
Meta: Meta{
Ui: ui,
View: view,
WorkingDir: wd,
AllowExperimentalFeatures: true,
testingOverrides: &testingOverrides{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): providers.FactoryFixed(sourceProvider),
addrs.NewDefaultProvider("test2"): providers.FactoryFixed(destinationProvider),
},
},
ProviderSource: providerSource,
},
}
_ = testInputMap(t, map[string]string{
"backend-migrate-copy-to-empty": "yes",
})
args := []string{"-no-color"}
code := c.Run(args)
out := done(t)
if code != 0 {
t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr())
}
// Assert expected human output is made
// Parameterized due to output referencing the current platform.
checkParameterizedGoldenReferenceHumanOutput(t, out, fixture, getproviders.CurrentPlatform.String())
// Assert the state is migrated successfully to the destination state store by inspecting the mock.
b, err = destinationProvider.MockStates.Read("test2_store", "default")
if err != nil {
t.Fatalf("unable to find migrated state in mock provider: %s", err)
}
s, err := statefile.Read(bytes.NewBuffer(b))
if err != nil {
t.Fatal(err)
}
_, ok := s.State.RootOutputValues["test"]
if !ok {
t.Fatalf("unable to find test output in migrated state")
}
// Assert the destination provider is added to the dependency lock file
// Also, no providers have been removed from the file.
lockFilePath := filepath.Join(wd.RootModuleDir(), dependencyLockFilename)
lockFileBytes, err := os.ReadFile(lockFilePath)
if err != nil {
t.Fatalf("unable to read dependency lock file: %s", err)
}
expectedContents := `# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/test" {
version = "1.2.3"
}
provider "registry.terraform.io/hashicorp/test2" {
version = "3.2.1"
hashes = [
"h1:gv1gFnIZulslzchnaoyMJ5KoPvoRgVvSGb3tVS803iw=",
]
}
`
if diff := cmp.Diff(expectedContents, string(lockFileBytes)); diff != "" {
t.Fatalf("unexpected dependency lock file contents, diff:\n%s", diff)
}
// Assert the backend state file describes the new state store location.
assertBackendStateFile(t, c)
})
t.Run("destination provider already in the dependency lock file, source is not", func(t *testing.T) {
wd := tempWorkingDirFixture(t, "state-store-changed/provider-used")
t.Chdir(wd.RootModuleDir())
// Replace dep lock file in fixtures so that the destination provider is already in the dep lock file, but the source provider is not.
lockFileContents := `# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/test2" {
version = "3.2.1"
hashes = [
"h1:gv1gFnIZulslzchnaoyMJ5KoPvoRgVvSGb3tVS803iw=",
]
}
`
if err := os.WriteFile(filepath.Join(wd.RootModuleDir(), dependencyLockFilename), []byte(lockFileContents), 0644); err != nil {
t.Fatalf("unable to overwrite dependency lock file as part of test setup: %s", err)
}
b, err := os.ReadFile("source-pss.tfstate")
if err != nil {
t.Fatal(err)
}
// hashicorp/test
sourcePssSchema := map[string]providers.Schema{
"test_src": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{},
},
},
}
sourceProvider := mockPluggableStateStorageProvider(sourcePssSchema)
sourceProvider.MockStates = testing_provider.MockStateBytes{
"test_src": map[string][]byte{"default": []byte(b)},
}
// hashicorp/test2
destinationPssSchema := map[string]providers.Schema{
"test2_store": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{},
},
},
}
destinationProvider := mockPluggableStateStorageProvider(destinationPssSchema)
destinationProvider.MockStates = testing_provider.MockStateBytes{
"test2_store": map[string][]byte{}, // No existing state in the destination
}
providerSource := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.2.3"},
"hashicorp/test2": {"3.2.1"},
})
ui := testUiWrapped(t)
view, done := testView(t)
c := &StateMigrateCommand{
Meta: Meta{
Ui: ui,
View: view,
WorkingDir: wd,
AllowExperimentalFeatures: true,
testingOverrides: &testingOverrides{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): providers.FactoryFixed(sourceProvider),
addrs.NewDefaultProvider("test2"): providers.FactoryFixed(destinationProvider),
},
},
ProviderSource: providerSource,
},
}
_ = testInputMap(t, map[string]string{
"backend-migrate-copy-to-empty": "yes",
})
args := []string{"-no-color"}
code := c.Run(args)
out := done(t)
if code != 0 {
t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr())
}
expectedMsg := []string{
"Initializing provider plugin for state store \"test_src\"...\n- Finding hashicorp/test versions matching \"1.2.3\"...\n- Installing hashicorp/test v1.2.3...\n- Installed hashicorp/test v1.2.3 (verified checksum)",
"Initializing provider plugin for state store \"test2_store\"...\n- Reusing previous version of hashicorp/test2 from the dependency lock file",
`Migrating state from state store "test_src" (hashicorp/test) to state store "test2_store" (hashicorp/test2)...`,
}
for _, expectedMsg := range expectedMsg {
if !strings.Contains(out.Stdout(), expectedMsg) {
t.Fatalf("expected output %q, got %q", expectedMsg, out.Stdout())
}
}
// Assert the state is migrated successfully to the destination state store by inspecting the mock.
b, err = destinationProvider.MockStates.Read("test2_store", "default")
if err != nil {
t.Fatalf("unable to find migrated state in mock provider: %s", err)
}
s, err := statefile.Read(bytes.NewBuffer(b))
if err != nil {
t.Fatal(err)
}
_, ok := s.State.RootOutputValues["test"]
if !ok {
t.Fatalf("unable to find test output in migrated state")
}
// Assert the dependency lock file is unchanged, as it was already in the lock file.
lockFilePath := filepath.Join(wd.RootModuleDir(), dependencyLockFilename)
lockFileBytes, err := os.ReadFile(lockFilePath)
if err != nil {
t.Fatalf("unable to read dependency lock file: %s", err)
}
if diff := cmp.Diff(lockFileContents, string(lockFileBytes)); diff != "" {
t.Fatalf("unexpected dependency lock file contents, diff:\n%s", diff)
}
// Assert the backend state file describes the new state store location.
assertBackendStateFile(t, c)
})
t.Run("no existing dependency lock file: only destination provider saved to the dependency lock file", func(t *testing.T) {
wd := tempWorkingDirFixture(t, "state-store-changed/provider-used")
t.Chdir(wd.RootModuleDir())
// Remove lock file, so source provider isn't in the dep lock file for this test scenario
if err := os.Remove(filepath.Join(wd.RootModuleDir(), dependencyLockFilename)); err != nil {
t.Fatalf("error while deleting lock file during test setup: %s", err)
}
b, err := os.ReadFile("source-pss.tfstate")
if err != nil {
t.Fatal(err)
}
// hashicorp/test
sourcePssSchema := map[string]providers.Schema{
"test_src": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{},
},
},
}
sourceProvider := mockPluggableStateStorageProvider(sourcePssSchema)
sourceProvider.MockStates = testing_provider.MockStateBytes{
"test_src": map[string][]byte{"default": []byte(b)},
}
// hashicorp/test2
destinationPssSchema := map[string]providers.Schema{
"test2_store": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{},
},
},
}
destinationProvider := mockPluggableStateStorageProvider(destinationPssSchema)
destinationProvider.MockStates = testing_provider.MockStateBytes{
"test2_store": map[string][]byte{}, // No existing state in the destination
}
providerSource := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.2.3"},
"hashicorp/test2": {"3.2.1"},
})
ui := testUiWrapped(t)
view, done := testView(t)
c := &StateMigrateCommand{
Meta: Meta{
Ui: ui,
View: view,
WorkingDir: wd,
AllowExperimentalFeatures: true,
testingOverrides: &testingOverrides{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): providers.FactoryFixed(sourceProvider),
addrs.NewDefaultProvider("test2"): providers.FactoryFixed(destinationProvider),
},
},
ProviderSource: providerSource,
},
}
_ = testInputMap(t, map[string]string{
"backend-migrate-copy-to-empty": "yes",
})
args := []string{"-no-color"}
code := c.Run(args)
out := done(t)
if code != 0 {
t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr())
}
expectedMsg := []string{
"Initializing provider plugin for state store \"test_src\"...\n- Finding hashicorp/test versions matching \"1.2.3\"...\n- Installing hashicorp/test v1.2.3...\n- Installed hashicorp/test v1.2.3 (verified checksum)",
"Initializing provider plugin for state store \"test2_store\"...\n- Finding latest version of hashicorp/test2...\n- Installing hashicorp/test2 v3.2.1...\n- Installed hashicorp/test2 v3.2.1 (verified checksum)",
`Migrating state from state store "test_src" (hashicorp/test) to state store "test2_store" (hashicorp/test2)...`,
}
for _, expectedMsg := range expectedMsg {
if !strings.Contains(out.Stdout(), expectedMsg) {
t.Fatalf("expected output %q, got %q", expectedMsg, out.Stdout())
}
}
// Assert the state is migrated successfully to the destination state store by inspecting the mock.
b, err = destinationProvider.MockStates.Read("test2_store", "default")
if err != nil {
t.Fatalf("unable to find migrated state in mock provider: %s", err)
}
s, err := statefile.Read(bytes.NewBuffer(b))
if err != nil {
t.Fatal(err)
}
_, ok := s.State.RootOutputValues["test"]
if !ok {
t.Fatalf("unable to find test output in migrated state")
}
// Assert the provider is added to the dependency lock file
lockFilePath := filepath.Join(wd.RootModuleDir(), dependencyLockFilename)
lockFileBytes, err := os.ReadFile(lockFilePath)
if err != nil {
t.Fatalf("unable to read dependency lock file: %s", err)
}
// The source provider is not added here, as it's not used post-migration.
expectedContents := `# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/test2" {
version = "3.2.1"
hashes = [
"h1:gv1gFnIZulslzchnaoyMJ5KoPvoRgVvSGb3tVS803iw=",
]
}
`
if diff := cmp.Diff(expectedContents, string(lockFileBytes)); diff != "" {
t.Fatalf("unexpected dependency lock file contents, diff:\n%s", diff)
}
// Assert the backend state file describes the new state store location.
assertBackendStateFile(t, c)
})
}
func TestStateMigrate_stateStore_newWorkingDir_inAutomationProviderApproval(t *testing.T) {
t.Run("both providers already in lock file, user not prompted for provider approval", func(t *testing.T) {
wd := tempWorkingDirFixture(t, "state-store-changed/provider-used")
t.Chdir(wd.RootModuleDir())
// Replace dep lock file in fixtures so that both providers are already in the dep lock file.
lockFileContents := `# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/test" {
version = "1.2.3"
}
provider "registry.terraform.io/hashicorp/test2" {
version = "3.2.1"
hashes = [
"h1:gv1gFnIZulslzchnaoyMJ5KoPvoRgVvSGb3tVS803iw=",
]
}`
if err := os.WriteFile(filepath.Join(wd.RootModuleDir(), dependencyLockFilename), []byte(lockFileContents), 0644); err != nil {
t.Fatalf("unable to overwrite dependency lock file as part of test setup: %s", err)
}
b, err := os.ReadFile("source-pss.tfstate")
if err != nil {
t.Fatal(err)
}
// hashicorp/test - source
sourcePssSchema := map[string]providers.Schema{
"test_src": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{},
},
},
}
sourceProvider := mockPluggableStateStorageProvider(sourcePssSchema)
sourceProvider.MockStates = testing_provider.MockStateBytes{
"test_src": map[string][]byte{"default": []byte(b)},
}
// hashicorp/test2 - destination
destinationPssSchema := map[string]providers.Schema{
"test2_store": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{},
},
},
}
destinationProvider := mockPluggableStateStorageProvider(destinationPssSchema)
destinationProvider.MockStates = testing_provider.MockStateBytes{
"test2_store": map[string][]byte{}, // No existing state in the destination
}
// Supplying providers via HTTP triggers security features.
providerSource := newMockProviderSourceUsingTestHttpServer(t, map[string][]string{
"hashicorp/test": {"1.2.3"},
"hashicorp/test2": {"3.2.1"},
})
ui := testUiWrapped(t)
view, done := testView(t)
c := &StateMigrateCommand{
Meta: Meta{
Ui: ui,
View: view,
WorkingDir: wd,
AllowExperimentalFeatures: true,
testingOverrides: &testingOverrides{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): providers.FactoryFixed(sourceProvider),
addrs.NewDefaultProvider("test2"): providers.FactoryFixed(destinationProvider),
},
},
ProviderSource: providerSource,
},
}
args := []string{
"-input=false", // Simulate running in automation where input is disabled
"-force-copy", // Suppress prompts to perform the migration
"-no-color",
}
code := c.Run(args)
out := done(t)
if code != 0 {
t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr())
}
expectedMsg := []string{
"Initializing provider plugin for state store \"test_src\"...\n- Reusing previous version of hashicorp/test from the dependency lock file",
"Initializing provider plugin for state store \"test2_store\"...\n- Reusing previous version of hashicorp/test2 from the dependency lock file",
`Migrating state from state store "test_src" (hashicorp/test) to state store "test2_store" (hashicorp/test2)...`,
}
for _, expectedMsg := range expectedMsg {
if !strings.Contains(out.Stdout(), expectedMsg) {
t.Fatalf("expected output %q, got %q", expectedMsg, out.Stdout())
}
}
notExpectedMsg := []string{
"The state store provider was approved automatically", // shouldn't be there as no explicit CLI flag used.
}
for _, notExpected := range notExpectedMsg {
if strings.Contains(out.Stdout(), notExpected) {
t.Fatalf("did not expect output %q, but got %q", notExpected, out.Stdout())
}
}
// Assert the state is migrated successfully to the destination state store by inspecting the mock.
b, err = destinationProvider.MockStates.Read("test2_store", "default")
if err != nil {
t.Fatalf("unable to find migrated state in mock provider: %s", err)
}
s, err := statefile.Read(bytes.NewBuffer(b))
if err != nil {
t.Fatal(err)
}
_, ok := s.State.RootOutputValues["test"]
if !ok {
t.Fatalf("unable to find test output in migrated state")
}
// Assert the dependency lock file is unchanged, as both providers were already in the lock file.
lockFilePath := filepath.Join(wd.RootModuleDir(), dependencyLockFilename)
lockFileBytes, err := os.ReadFile(lockFilePath)
if err != nil {
t.Fatalf("unable to read dependency lock file: %s", err)
}
if diff := cmp.Diff(lockFileContents, string(lockFileBytes)); diff != "" {
t.Fatalf("unexpected dependency lock file contents, diff:\n%s", diff)
}
})
}
func TestStateMigrate_fromStateStoreToBackend(t *testing.T) {
wd := tempWorkingDirFixture(t, "state-migrate-state-store-to-backend")
t.Chdir(wd.RootModuleDir())
p := mockPluggableStateStorageProvider(mockSingleStateStoreSchema("test_store"))
b, err := os.ReadFile("source-pss.tfstate")
if err != nil {
t.Fatal(err)
}
p.MockStates = testing_provider.NewMockStateBytesWithSingleState(
"test_store",
"default",
b,
)
providerSource := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.2.3"},
})
ui := testUiWrapped(t)
view, done := testView(t)
c := &StateMigrateCommand{
Meta: Meta{
Ui: ui,
View: view,
WorkingDir: wd,
AllowExperimentalFeatures: true,
testingOverrides: metaOverridesForProvider(p),
ProviderSource: providerSource,
},
}
_ = testInputMap(t, map[string]string{
"backend-migrate-copy-to-empty": "yes",
})
args := []string{"-no-color"}
code := c.Run(args)
out := done(t)
if code != 0 {
t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr())
}
expectedMsg := []string{
"Initializing provider plugin for state store \"test_store\"...\n- Reusing previous version of hashicorp/test from the dependency lock file",
`Migrating state from state store "test_store" (hashicorp/test) to backend "local"...`,
}
for _, expectedMsg := range expectedMsg {
if !strings.Contains(out.Stdout(), expectedMsg) {
t.Fatalf("expected output %q, got %q", expectedMsg, out.Stdout())
}
}
f, err := os.Open("destination-backend.tfstate")
if err != nil {
t.Fatalf("failed to read migrated state: %s", err)
}
t.Cleanup(func() {
err := f.Close()
if err != nil {
t.Fatal(err)
}
})
s, err := statefile.Read(f)
if err != nil {
t.Fatal(err)
}
_, ok := s.State.RootOutputValues["test"]
if !ok {
t.Fatalf("unable to find test output in migrated state")
}
// Assert the backend state file describes the new backend location,
statePath := filepath.Join(c.DataDir(), DefaultStateFilename)
sMgr := &clistate.LocalState{Path: statePath}
if err := sMgr.RefreshState(); err != nil {
t.Fatal(err)
}
backendState := sMgr.State()
if backendState.StateStore != nil {
t.Fatalf("expected backend state file to not describe a state_store during state_store=>backend migration, but it's set")
}
if backendState.Backend == nil {
t.Fatalf("expected backend state file to describe a backend during state_store=>backend migration, but it's nil")
}
if backendState.Backend.Type != "local" {
t.Fatalf("expected backend state file to describe the destination backend \"local\", but got %q", backendState.Backend.Type)
}
if got, want := normalizeJSON(t, backendState.Backend.ConfigRaw), `{"path":"destination-backend.tfstate","workspace_dir":null}`; got != want {
t.Errorf("wrong config\ngot: %s\nwant: %s", got, want)
}
}
func TestStateMigrate_missingModuleFiles(t *testing.T) {
wd := tempWorkingDirFixture(t, "state-migrate-missing-mod-files")
t.Chdir(wd.RootModuleDir())
providerSource := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.2.3"},
})
ui := testUiWrapped(t)
view, done := testView(t)
c := &StateMigrateCommand{
Meta: Meta{
Ui: ui,
View: view,
WorkingDir: wd,
AllowExperimentalFeatures: true,
ProviderSource: providerSource,
},
}
args := []string{
"-input=false",
"-no-color",
}
code := c.Run(args)
if code != 1 {
t.Fatalf("expected exit code 1, got %d", code)
}
out := done(t)
expectedMsg := "Error: Unknown migration destination"
if !strings.Contains(out.Stderr(), expectedMsg) {
t.Fatalf("expected output %q, got %q", expectedMsg, out.All())
}
}
func TestStateMigrate_emptyModuleFiles(t *testing.T) {
wd := tempWorkingDirFixture(t, "state-migrate-empty-mod-files")
t.Chdir(wd.RootModuleDir())
providerSource := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.2.3"},
})
ui := testUiWrapped(t)
view, done := testView(t)
c := &StateMigrateCommand{
Meta: Meta{
Ui: ui,
View: view,
WorkingDir: wd,
AllowExperimentalFeatures: true,
ProviderSource: providerSource,
},
}
args := []string{
"-input=false",
"-no-color",
}
code := c.Run(args)
if code != 1 {
t.Fatalf("expected exit code 1, got %d", code)
}
out := done(t)
expectedMsg := "Error: Unknown migration destination"
if !strings.Contains(out.Stderr(), expectedMsg) {
t.Fatalf("expected output %q, got %q", expectedMsg, out.All())
}
}
func TestStateMigrate_missingMigrationFiles(t *testing.T) {
wd := tempWorkingDirFixture(t, "state-migrate-missing-migrate-files")
t.Chdir(wd.RootModuleDir())
ui := testUiWrapped(t)
view, done := testView(t)
c := &StateMigrateCommand{
Meta: Meta{
Ui: ui,
View: view,
WorkingDir: wd,
AllowExperimentalFeatures: true,
},
}
args := []string{
"-input=false",
"-no-color",
}
code := c.Run(args)
if code != 1 {
t.Fatalf("expected exit code 1, got %d", code)
}
out := done(t)
expectedMsg := "Error: No state migration instructions found"
if !strings.Contains(out.Stderr(), expectedMsg) {
t.Fatalf("expected output %q, got %q", expectedMsg, out.All())
}
}
func TestStateMigrate_nonExistentLockFiles(t *testing.T) {
// use temporary directory to ensure the lock files certainly do not exist
tmpDir := t.TempDir()
t.Chdir(tmpDir)
ui := testUiWrapped(t)
view, done := testView(t)
c := &StateMigrateCommand{
Meta: Meta{
Ui: ui,
View: view,
WorkingDir: workdir.NewDir(tmpDir),
AllowExperimentalFeatures: true,
},
}
args := []string{
"-input=false",
"-source-provider-lock-file", filepath.Join(tmpDir, ".terraform.lock.hcl"),
"-destination-provider-lock-file", filepath.Join(tmpDir, ".terraform.lock.hcl"),
}
code := c.Run(args)
if code != 1 {
t.Fatalf("expected exit code 1, got %d", code)
}
out := done(t)
expectedMsg := "Unreadable source provider lock file"
if !strings.Contains(out.Stderr(), expectedMsg) {
t.Fatalf("expected output %q, got %q", expectedMsg, out.All())
}
expectedMsg = "Unreadable destination provider lock file"
if !strings.Contains(out.Stderr(), expectedMsg) {
t.Fatalf("expected output %q, got %q", expectedMsg, out.All())
}
}