// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package command import ( "context" "fmt" "os" "path/filepath" "reflect" "sort" "strings" "testing" "github.com/apparentlymart/go-versions/versions" "github.com/hashicorp/cli" version "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" tfaddr "github.com/hashicorp/terraform-registry-address" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/backend" "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/workdir" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/copy" "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/getproviders/providerreqs" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/zclconf/go-cty/cty" backendInit "github.com/hashicorp/terraform/internal/backend/init" "github.com/hashicorp/terraform/internal/backend/local" backendLocal "github.com/hashicorp/terraform/internal/backend/local" "github.com/hashicorp/terraform/internal/backend/pluggable" backendInmem "github.com/hashicorp/terraform/internal/backend/remote-state/inmem" ) // Test empty directory with no config/state creates a local state. func TestMetaBackend_emptyDir(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() os.MkdirAll(td, 0755) t.Chdir(td) // Get the backend m := testMetaBackend(t, nil) b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Write some state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } s.WriteState(testState()) if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } // Verify it exists where we expect it to if isEmptyState(DefaultStateFilename) { t.Fatalf("no state was written") } // Verify no backup since it was empty to start if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) { t.Fatal("backup state should be empty") } // Verify no backend state was made if !isEmptyState(filepath.Join(m.DataDir(), DefaultStateFilename)) { t.Fatal("backend state should be empty") } } // check for no state. Either the file doesn't exist, or is empty func isEmptyState(path string) bool { fi, err := os.Stat(path) if os.IsNotExist(err) { return true } if fi.Size() == 0 { return true } return false } // Test a directory with a legacy state and no config continues to // use the legacy state. func TestMetaBackend_emptyWithDefaultState(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() os.MkdirAll(td, 0755) t.Chdir(td) // Write the legacy state statePath := DefaultStateFilename { f, err := os.Create(statePath) if err != nil { t.Fatalf("err: %s", err) } err = writeStateForTesting(testState(), f) f.Close() if err != nil { t.Fatalf("err: %s", err) } } // Get the backend m := testMetaBackend(t, nil) b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("err: %s", err) } if actual := s.State().String(); actual != testState().String() { t.Fatalf("bad: %s", actual) } // Verify it exists where we expect it to if _, err := os.Stat(DefaultStateFilename); err != nil { t.Fatalf("err: %s", err) } stateName := filepath.Join(m.DataDir(), DefaultStateFilename) if !isEmptyState(stateName) { t.Fatal("expected no state at", stateName) } // Write some state next := testState() next.SetOutputValue( addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), cty.StringVal("bar"), false, ) s.WriteState(next) if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } // Verify a backup was made since we're modifying a pre-existing state if isEmptyState(DefaultStateFilename + DefaultBackupExtension) { t.Fatal("backup state should not be empty") } } // Test an empty directory with an explicit state path (outside the dir) func TestMetaBackend_emptyWithExplicitState(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() os.MkdirAll(td, 0755) t.Chdir(td) // Create another directory to store our state stateDir := t.TempDir() os.MkdirAll(stateDir, 0755) // Write the legacy state statePath := filepath.Join(stateDir, "foo") { f, err := os.Create(statePath) if err != nil { t.Fatalf("err: %s", err) } err = writeStateForTesting(testState(), f) f.Close() if err != nil { t.Fatalf("err: %s", err) } } // Setup the meta m := testMetaBackend(t, nil) m.statePath = statePath // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("err: %s", err) } if actual := s.State().String(); actual != testState().String() { t.Fatalf("bad: %s", actual) } // Verify neither defaults exist if _, err := os.Stat(DefaultStateFilename); err == nil { t.Fatal("file should not exist") } stateName := filepath.Join(m.DataDir(), DefaultStateFilename) if !isEmptyState(stateName) { t.Fatal("expected no state at", stateName) } // Write some state next := testState() markStateForMatching(next, "bar") // just any change so it shows as different than before s.WriteState(next) if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } // Verify a backup was made since we're modifying a pre-existing state if isEmptyState(statePath + DefaultBackupExtension) { t.Fatal("backup state should not be empty") } } // Verify that interpolations result in an error func TestMetaBackend_configureBackendInterpolation(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-new-interp"), td) t.Chdir(td) // Setup the meta m := testMetaBackend(t, nil) // Get the backend _, err := m.Backend(&BackendOpts{Init: true}) if err == nil { t.Fatal("should error") } wantErr := "Variables not allowed" if !strings.Contains(err.Err().Error(), wantErr) { t.Fatalf("error should include %q, got: %s", wantErr, err.Err()) } } // Newly configured backend func TestMetaBackend_configureNewBackend(t *testing.T) { td := t.TempDir() testCopyDir(t, testFixturePath("backend-new"), td) t.Chdir(td) // Setup the meta m := testMetaBackend(t, nil) // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state != nil { t.Fatal("state should be nil") } // Write some state state = states.NewState() mark := markStateForMatching(state, "changing") s.WriteState(state) if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect { f, err := os.Open("local-state.tfstate") if err != nil { t.Fatalf("err: %s", err) } actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } assertStateHasMarker(t, actual.State, mark) } // Verify the default paths don't exist if _, err := os.Stat(DefaultStateFilename); err == nil { t.Fatal("file should not exist") } // Verify a backup doesn't exist if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { t.Fatal("file should not exist") } } // Newly configured backend with prior local state and no remote state func TestMetaBackend_configureNewBackendWithState(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-new-migrate"), td) t.Chdir(td) // Ask input defer testInteractiveInput(t, []string{"yes"})() // Setup the meta m := testMetaBackend(t, nil) // This combination should not require the extra -migrate-state flag, since // there is no existing backend config m.migrateState = false // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } state, err := statemgr.RefreshAndRead(s) if err != nil { t.Fatalf("unexpected error: %s", err) } if state == nil { t.Fatal("state is nil") } if got, want := testStateMgrCurrentLineage(s), "backend-new-migrate"; got != want { t.Fatalf("lineage changed during migration\nnow: %s\nwas: %s", got, want) } // Write some state state = states.NewState() mark := markStateForMatching(state, "changing") if err := statemgr.WriteAndPersist(s, state, nil); err != nil { t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect { f, err := os.Open("local-state.tfstate") if err != nil { t.Fatalf("err: %s", err) } actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } assertStateHasMarker(t, actual.State, mark) } // Verify the default paths don't exist if !isEmptyState(DefaultStateFilename) { data, _ := os.ReadFile(DefaultStateFilename) t.Fatal("state should not exist, but contains:\n", string(data)) } // Verify a backup does exist if isEmptyState(DefaultStateFilename + DefaultBackupExtension) { t.Fatal("backup state is empty or missing") } } // Newly configured backend with matching local and remote state doesn't prompt // for copy. func TestMetaBackend_configureNewBackendWithoutCopy(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-new-migrate"), td) t.Chdir(td) if err := copy.CopyFile(DefaultStateFilename, "local-state.tfstate"); err != nil { t.Fatal(err) } // Setup the meta m := testMetaBackend(t, nil) m.input = false // init the backend _, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Verify the state is where we expect f, err := os.Open("local-state.tfstate") if err != nil { t.Fatalf("err: %s", err) } actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } if actual.Lineage != "backend-new-migrate" { t.Fatalf("incorrect state lineage: %q", actual.Lineage) } // Verify the default paths don't exist if !isEmptyState(DefaultStateFilename) { data, _ := os.ReadFile(DefaultStateFilename) t.Fatal("state should not exist, but contains:\n", string(data)) } // Verify a backup does exist if isEmptyState(DefaultStateFilename + DefaultBackupExtension) { t.Fatal("backup state is empty or missing") } } // Newly configured backend with prior local state and no remote state, // but opting to not migrate. func TestMetaBackend_configureNewBackendWithStateNoMigrate(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-new-migrate"), td) t.Chdir(td) // Ask input defer testInteractiveInput(t, []string{"no"})() // Setup the meta m := testMetaBackend(t, nil) // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } if state := s.State(); state != nil { t.Fatal("state is not nil") } // Verify the default paths don't exist if !isEmptyState(DefaultStateFilename) { data, _ := os.ReadFile(DefaultStateFilename) t.Fatal("state should not exist, but contains:\n", string(data)) } // Verify a backup does exist if isEmptyState(DefaultStateFilename + DefaultBackupExtension) { t.Fatal("backup state is empty or missing") } } // Newly configured backend with prior local state and remote state func TestMetaBackend_configureNewBackendWithStateExisting(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-new-migrate-existing"), td) t.Chdir(td) // Setup the meta m := testMetaBackend(t, nil) // suppress input m.forceInitCopy = true // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state is nil") } if got, want := testStateMgrCurrentLineage(s), "local"; got != want { t.Fatalf("wrong lineage %q; want %q", got, want) } // Write some state state = states.NewState() mark := markStateForMatching(state, "changing") s.WriteState(state) if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect { f, err := os.Open("local-state.tfstate") if err != nil { t.Fatalf("err: %s", err) } actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } assertStateHasMarker(t, actual.State, mark) } // Verify the default paths don't exist if !isEmptyState(DefaultStateFilename) { data, _ := os.ReadFile(DefaultStateFilename) t.Fatal("state should not exist, but contains:\n", string(data)) } // Verify a backup does exist if isEmptyState(DefaultStateFilename + DefaultBackupExtension) { t.Fatal("backup state is empty or missing") } } // Newly configured backend with prior local state and remote state func TestMetaBackend_configureNewBackendWithStateExistingNoMigrate(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-new-migrate-existing"), td) t.Chdir(td) // Ask input defer testInteractiveInput(t, []string{"no"})() // Setup the meta m := testMetaBackend(t, nil) // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state is nil") } if testStateMgrCurrentLineage(s) != "remote" { t.Fatalf("bad: %#v", state) } // Write some state state = states.NewState() mark := markStateForMatching(state, "changing") s.WriteState(state) if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect { f, err := os.Open("local-state.tfstate") if err != nil { t.Fatalf("err: %s", err) } actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } assertStateHasMarker(t, actual.State, mark) } // Verify the default paths don't exist if !isEmptyState(DefaultStateFilename) { data, _ := os.ReadFile(DefaultStateFilename) t.Fatal("state should not exist, but contains:\n", string(data)) } // Verify a backup does exist if isEmptyState(DefaultStateFilename + DefaultBackupExtension) { t.Fatal("backup state is empty or missing") } } // Saved backend state matching config func TestMetaBackend_configuredBackendUnchanged(t *testing.T) { td := t.TempDir() testCopyDir(t, testFixturePath("backend-unchanged"), td) t.Chdir(td) // Setup the meta m := testMetaBackend(t, nil) // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("nil state") } if testStateMgrCurrentLineage(s) != "configuredUnchanged" { t.Fatalf("bad: %#v", state) } // Verify the default paths don't exist if _, err := os.Stat(DefaultStateFilename); err == nil { t.Fatal("file should not exist") } // Verify a backup doesn't exist if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { t.Fatal("file should not exist") } } // Changing a configured backend func TestMetaBackend_changeConfiguredBackend(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change"), td) t.Chdir(td) // Ask input defer testInteractiveInput(t, []string{"no"})() // Setup the meta m := testMetaBackend(t, nil) // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state != nil { t.Fatal("state should be nil") } // Verify the default paths don't exist if _, err := os.Stat(DefaultStateFilename); err == nil { t.Fatal("file should not exist") } // Verify a backup doesn't exist if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { t.Fatal("file should not exist") } // Write some state state = states.NewState() mark := markStateForMatching(state, "changing") s.WriteState(state) if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect { f, err := os.Open("local-state-2.tfstate") if err != nil { t.Fatalf("err: %s", err) } actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } assertStateHasMarker(t, actual.State, mark) } // Verify no local state if _, err := os.Stat(DefaultStateFilename); err == nil { t.Fatal("file should not exist") } // Verify no local backup if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { t.Fatal("file should not exist") } } // Reconfiguring with an already configured backend. // This should ignore the existing backend config, and configure the new // backend is if this is the first time. func TestMetaBackend_reconfigureBackendChange(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change-single-to-single"), td) t.Chdir(td) // Register the single-state backend backendInit.Set("local-single", backendLocal.TestNewLocalSingle) defer backendInit.Set("local-single", nil) // Setup the meta m := testMetaBackend(t, nil) // this should not ask for input m.input = false // cli flag -reconfigure m.reconfigure = true // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } newState := s.State() if newState != nil || !newState.Empty() { t.Fatal("state should be nil/empty after forced reconfiguration") } // verify that the old state is still there s = statemgr.NewFilesystem("local-state.tfstate") if err := s.RefreshState(); err != nil { t.Fatal(err) } oldState := s.State() if oldState == nil || oldState.Empty() { t.Fatal("original state should be untouched") } } // Initializing a backend which supports workspaces and does *not* have // the currently selected workspace should prompt the user with a list of // workspaces to choose from to select a valid one, if more than one workspace // is available. func TestMetaBackend_initBackendSelectedWorkspaceDoesNotExist(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("init-backend-selected-workspace-doesnt-exist-multi"), td) t.Chdir(td) // Setup the meta m := testMetaBackend(t, nil) terminalPrompts := testInputMap(t, map[string]string{ "select-workspace": "2", }) // Get the backend _, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } expectedMsg := "The currently selected workspace (bar) does not exist" if !strings.Contains(terminalPrompts.String(), expectedMsg) { t.Errorf("expected error message to contain %q, but got %q", expectedMsg, terminalPrompts.String()) } expected := "foo" actual, err := m.Workspace() if err != nil { t.Fatal(err) } if actual != expected { t.Fatalf("expected selected workspace to be %q, but was %q", expected, actual) } } // Initializing a backend which supports workspaces and does *not* have the // currently selected workspace - and which only has a single workspace - should // automatically select that single workspace. func TestMetaBackend_initBackendSelectedWorkspaceDoesNotExistAutoSelect(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("init-backend-selected-workspace-doesnt-exist-single"), td) t.Chdir(td) // Setup the meta m := testMetaBackend(t, nil) // this should not ask for input m.input = false // Assert test precondition: The current selected workspace is "bar" previousName, err := m.Workspace() if err != nil { t.Fatal(err) } if previousName != "bar" { t.Fatalf("expected test fixture to start with 'bar' as the current selected workspace") } // Get the backend _, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } expected := "default" actual, err := m.Workspace() if err != nil { t.Fatal(err) } if actual != expected { t.Fatalf("expected selected workspace to be %q, but was %q", expected, actual) } } // Initializing a backend which supports workspaces and does *not* have // the currently selected workspace with input=false should fail. func TestMetaBackend_initBackendSelectedWorkspaceDoesNotExistInputFalse(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("init-backend-selected-workspace-doesnt-exist-multi"), td) t.Chdir(td) // Setup the meta m := testMetaBackend(t, nil) m.input = false // Get the backend _, diags := m.Backend(&BackendOpts{Init: true}) // Should fail immediately if got, want := diags.ErrWithWarnings().Error(), `Currently selected workspace "bar" does not exist`; !strings.Contains(got, want) { t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want) } } // Changing a configured backend, copying state func TestMetaBackend_configuredBackendChangeCopy(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change"), td) t.Chdir(td) // Ask input defer testInteractiveInput(t, []string{"yes", "yes"})() // Setup the meta m := testMetaBackend(t, nil) // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state should not be nil") } if testStateMgrCurrentLineage(s) != "backend-change" { t.Fatalf("bad: %#v", state) } // Verify no local state if _, err := os.Stat(DefaultStateFilename); err == nil { t.Fatal("file should not exist") } // Verify no local backup if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { t.Fatal("file should not exist") } } // Changing a configured backend that supports only single states to another // backend that only supports single states. func TestMetaBackend_configuredBackendChangeCopy_singleState(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change-single-to-single"), td) t.Chdir(td) // Register the single-state backend backendInit.Set("local-single", backendLocal.TestNewLocalSingle) defer backendInit.Set("local-single", nil) // Ask input _ = testInputMap(t, map[string]string{ "backend-migrate-copy-to-empty": "yes", }) // Setup the meta m := testMetaBackend(t, nil) // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state should not be nil") } if testStateMgrCurrentLineage(s) != "backend-change" { t.Fatalf("bad: %#v", state) } // Verify no local state if _, err := os.Stat(DefaultStateFilename); err == nil { t.Fatal("file should not exist") } // Verify no local backup if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { t.Fatal("file should not exist") } } // Changing a configured backend that supports multi-state to a // backend that only supports single states. The multi-state only has // a default state. func TestMetaBackend_configuredBackendChangeCopy_multiToSingleDefault(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change-multi-default-to-single"), td) t.Chdir(td) // Register the single-state backend backendInit.Set("local-single", backendLocal.TestNewLocalSingle) defer backendInit.Set("local-single", nil) // Ask input _ = testInputMap(t, map[string]string{ "backend-migrate-copy-to-empty": "yes", }) // Setup the meta m := testMetaBackend(t, nil) // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state should not be nil") } if testStateMgrCurrentLineage(s) != "backend-change" { t.Fatalf("bad: %#v", state) } // Verify no local state if _, err := os.Stat(DefaultStateFilename); err == nil { t.Fatal("file should not exist") } // Verify no local backup if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { t.Fatal("file should not exist") } } // Changing a configured backend that supports multi-state to a // backend that only supports single states. func TestMetaBackend_configuredBackendChangeCopy_multiToSingle(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change-multi-to-single"), td) t.Chdir(td) // Register the single-state backend backendInit.Set("local-single", backendLocal.TestNewLocalSingle) defer backendInit.Set("local-single", nil) // Ask input _ = testInputMap(t, map[string]string{ "backend-migrate-multistate-to-single": "yes", "backend-migrate-copy-to-empty": "yes", }) // Setup the meta m := testMetaBackend(t, nil) // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state should not be nil") } if testStateMgrCurrentLineage(s) != "backend-change" { t.Fatalf("bad: %#v", state) } // Verify no local state if _, err := os.Stat(DefaultStateFilename); err == nil { t.Fatal("file should not exist") } // Verify no local backup if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { t.Fatal("file should not exist") } // Verify existing workspaces exist envPath := filepath.Join(backendLocal.DefaultWorkspaceDir, "env2", backendLocal.DefaultStateFilename) if _, err := os.Stat(envPath); err != nil { t.Fatal("env should exist") } // Verify we are now in the default env, or we may not be able to access the new backend env, err := m.Workspace() if err != nil { t.Fatal(err) } if env != backend.DefaultStateName { t.Fatal("using non-default env with single-env backend") } } // Changing a configured backend that supports multi-state to a // backend that only supports single states. func TestMetaBackend_configuredBackendChangeCopy_multiToSingleCurrentEnv(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change-multi-to-single"), td) t.Chdir(td) // Register the single-state backend backendInit.Set("local-single", backendLocal.TestNewLocalSingle) defer backendInit.Set("local-single", nil) // Ask input _ = testInputMap(t, map[string]string{ "backend-migrate-multistate-to-single": "yes", "backend-migrate-copy-to-empty": "yes", }) // Setup the meta m := testMetaBackend(t, nil) // Change env if err := m.SetWorkspace("env2"); err != nil { t.Fatalf("unexpected error: %s", err) } // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state should not be nil") } if testStateMgrCurrentLineage(s) != "backend-change-env2" { t.Fatalf("bad: %#v", state) } // Verify no local state if _, err := os.Stat(DefaultStateFilename); err == nil { t.Fatal("file should not exist") } // Verify no local backup if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { t.Fatal("file should not exist") } // Verify existing workspaces exist envPath := filepath.Join(backendLocal.DefaultWorkspaceDir, "env2", backendLocal.DefaultStateFilename) if _, err := os.Stat(envPath); err != nil { t.Fatal("env should exist") } } // Changing a configured backend that supports multi-state to a // backend that also supports multi-state. func TestMetaBackend_configuredBackendChangeCopy_multiToMulti(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change-multi-to-multi"), td) t.Chdir(td) // Ask input _ = testInputMap(t, map[string]string{ "backend-migrate-multistate-to-multistate": "yes", }) // Setup the meta m := testMetaBackend(t, nil) // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check resulting states workspaces, wDiags := b.Workspaces() if wDiags.HasErrors() { t.Fatalf("unexpected error: %s", wDiags.Err()) } if wDiags.HasWarnings() { t.Logf("warning returned : %s", wDiags.ErrWithWarnings()) } sort.Strings(workspaces) expected := []string{"default", "env2"} if !reflect.DeepEqual(workspaces, expected) { t.Fatalf("bad: %#v", workspaces) } { // Check the default state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state should not be nil") } if testStateMgrCurrentLineage(s) != "backend-change" { t.Fatalf("bad: %#v", state) } } { // Check the other state s, sDiags := b.StateMgr("env2") if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state should not be nil") } if testStateMgrCurrentLineage(s) != "backend-change-env2" { t.Fatalf("bad: %#v", state) } } // Verify no local backup if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { t.Fatal("file should not exist") } { // Verify existing workspaces exist envPath := filepath.Join(backendLocal.DefaultWorkspaceDir, "env2", backendLocal.DefaultStateFilename) if _, err := os.Stat(envPath); err != nil { t.Fatalf("%s should exist, but does not", envPath) } } { // Verify new workspaces exist envPath := filepath.Join("envdir-new", "env2", backendLocal.DefaultStateFilename) if _, err := os.Stat(envPath); err != nil { t.Fatalf("%s should exist, but does not", envPath) } } } // Changing a configured backend that supports multi-state to a // backend that also supports multi-state, but doesn't allow a // default state while the default state is non-empty. func TestMetaBackend_configuredBackendChangeCopy_multiToNoDefaultWithDefault(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change-multi-to-no-default-with-default"), td) t.Chdir(td) // Register the single-state backend backendInit.Set("local-no-default", backendLocal.TestNewLocalNoDefault) defer backendInit.Set("local-no-default", nil) // Ask input _ = testInputMap(t, map[string]string{ "backend-migrate-multistate-to-multistate": "yes", "new-state-name": "env1", }) // Setup the meta m := testMetaBackend(t, nil) // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check resulting states workspaces, wDiags := b.Workspaces() if wDiags.HasErrors() { t.Fatalf("unexpected error: %s", wDiags.Err()) } if wDiags.HasWarnings() { t.Logf("warning returned : %s", wDiags.ErrWithWarnings()) } sort.Strings(workspaces) expected := []string{"env1", "env2"} if !reflect.DeepEqual(workspaces, expected) { t.Fatalf("bad: %#v", workspaces) } { // Check the renamed default state s, sDiags := b.StateMgr("env1") if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state should not be nil") } if testStateMgrCurrentLineage(s) != "backend-change-env1" { t.Fatalf("bad: %#v", state) } } { // Verify existing workspaces exist envPath := filepath.Join(backendLocal.DefaultWorkspaceDir, "env2", backendLocal.DefaultStateFilename) if _, err := os.Stat(envPath); err != nil { t.Fatal("env should exist") } } { // Verify new workspaces exist envPath := filepath.Join("envdir-new", "env2", backendLocal.DefaultStateFilename) if _, err := os.Stat(envPath); err != nil { t.Fatal("env should exist") } } } // Changing a configured backend that supports multi-state to a // backend that also supports multi-state, but doesn't allow a // default state while the default state is empty. func TestMetaBackend_configuredBackendChangeCopy_multiToNoDefaultWithoutDefault(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change-multi-to-no-default-without-default"), td) t.Chdir(td) // Register the single-state backend backendInit.Set("local-no-default", backendLocal.TestNewLocalNoDefault) defer backendInit.Set("local-no-default", nil) // Ask input _ = testInputMap(t, map[string]string{ "backend-migrate-multistate-to-multistate": "yes", }) // Setup the meta m := testMetaBackend(t, nil) // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check resulting states workspaces, wDiags := b.Workspaces() if wDiags.HasErrors() { t.Fatalf("unexpected error: %s", wDiags.Err()) } if wDiags.HasWarnings() { t.Logf("warning returned : %s", wDiags.ErrWithWarnings()) } sort.Strings(workspaces) expected := []string{"env2"} // default is skipped because it is absent in the source backend if !reflect.DeepEqual(workspaces, expected) { t.Fatalf("wrong workspaces\ngot: %#v\nwant: %#v", workspaces, expected) } { // Check the named state s, sDiags := b.StateMgr("env2") if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state should not be nil") } if testStateMgrCurrentLineage(s) != "backend-change-env2" { t.Fatalf("bad: %#v", state) } } { // Verify existing workspaces exist envPath := filepath.Join(backendLocal.DefaultWorkspaceDir, "env2", backendLocal.DefaultStateFilename) if _, err := os.Stat(envPath); err != nil { t.Fatalf("%s should exist, but does not", envPath) } } { // Verify new workspaces exist envPath := filepath.Join("envdir-new", "env2", backendLocal.DefaultStateFilename) if _, err := os.Stat(envPath); err != nil { t.Fatalf("%s should exist, but does not", envPath) } } } // Unsetting a saved backend func TestMetaBackend_configuredBackendUnset(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-unset"), td) t.Chdir(td) // Ask input defer testInteractiveInput(t, []string{"no"})() // Setup the meta m := testMetaBackend(t, nil) // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state != nil { t.Fatal("state should be nil") } // Verify the default paths don't exist if !isEmptyState(DefaultStateFilename) { data, _ := os.ReadFile(DefaultStateFilename) t.Fatal("state should not exist, but contains:\n", string(data)) } // Verify a backup doesn't exist if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) { data, _ := os.ReadFile(DefaultStateFilename + DefaultBackupExtension) t.Fatal("backup should not exist, but contains:\n", string(data)) } // Write some state s.WriteState(testState()) if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } // Verify it exists where we expect it to if isEmptyState(DefaultStateFilename) { t.Fatal(DefaultStateFilename, "is empty") } // Verify no backup since it was empty to start if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) { data, _ := os.ReadFile(DefaultStateFilename + DefaultBackupExtension) t.Fatal("backup state should be empty, but contains:\n", string(data)) } } // Unsetting a saved backend and copying the remote state func TestMetaBackend_configuredBackendUnsetCopy(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-unset"), td) t.Chdir(td) // Ask input defer testInteractiveInput(t, []string{"yes", "yes"})() // Setup the meta m := testMetaBackend(t, nil) // Get the backend b, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("state is nil") } if got, want := testStateMgrCurrentLineage(s), "configuredUnset"; got != want { t.Fatalf("wrong state lineage %q; want %q", got, want) } // Verify a backup doesn't exist if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) { t.Fatalf("backup state should be empty") } // Write some state s.WriteState(testState()) if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } // Verify it exists where we expect it to if _, err := os.Stat(DefaultStateFilename); err != nil { t.Fatalf("err: %s", err) } // Verify a backup since it wasn't empty to start if isEmptyState(DefaultStateFilename + DefaultBackupExtension) { t.Fatal("backup is empty") } } // A plan that has uses the local backend and local state storage func TestMetaBackend_planLocal(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-plan-local"), td) t.Chdir(td) backendConfigBlock := cty.ObjectVal(map[string]cty.Value{ "path": cty.NullVal(cty.String), "workspace_dir": cty.NullVal(cty.String), }) backendConfigRaw, err := plans.NewDynamicValue(backendConfigBlock, backendConfigBlock.Type()) if err != nil { t.Fatal(err) } plan := &plans.Plan{ Backend: &plans.Backend{ Type: "local", Config: backendConfigRaw, Workspace: "default", }, } // Setup the meta m := testMetaBackend(t, nil) // Get the backend b, diags := m.BackendForLocalPlan(plan) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state != nil { t.Fatalf("state should be nil: %#v", state) } // The default state file should not exist yet if !isEmptyState(DefaultStateFilename) { t.Fatal("expected empty state") } // A backup file shouldn't exist yet either. if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) { t.Fatal("expected empty backup") } // Verify we have no configured backend path := filepath.Join(m.DataDir(), DefaultStateFilename) if _, err := os.Stat(path); err == nil { t.Fatalf("should not have backend configured") } // Write some state state = states.NewState() mark := markStateForMatching(state, "changing") s.WriteState(state) if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect { f, err := os.Open(DefaultStateFilename) if err != nil { t.Fatalf("err: %s", err) } actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } assertStateHasMarker(t, actual.State, mark) } // Verify no local backup if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) { t.Fatalf("backup state should be empty") } } // A plan that has uses the local backend and pluggable state storage func TestMetaBackend_planLocal_stateStore(t *testing.T) { // Create a temporary working directory td := t.TempDir() testCopyDir(t, testFixturePath("state-store-unchanged"), td) t.Chdir(td) stateStoreConfigBlock := cty.ObjectVal(map[string]cty.Value{ "value": cty.StringVal("foobar"), }) stateStoreConfigRaw, err := plans.NewDynamicValue(stateStoreConfigBlock, stateStoreConfigBlock.Type()) if err != nil { t.Fatal(err) } providerAddr := addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test") plan := &plans.Plan{ StateStore: &plans.StateStore{ Type: "test_store", Config: stateStoreConfigRaw, Workspace: backend.DefaultStateName, Provider: &plans.Provider{ Version: version.Must(version.NewVersion("1.2.3")), // Matches lock file in the test fixtures Source: &providerAddr, Config: nil, }, }, } // Setup the meta, including a mock provider set up to mock PSS m := testMetaBackend(t, nil) mock := testStateStoreMockWithChunkNegotiation(t, 1000) m.testingOverrides = &testingOverrides{ Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): providers.FactoryFixed(mock), }, } // Get the backend b, diags := m.BackendForLocalPlan(plan) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state != nil { t.Fatalf("state should be nil: %#v", state) } // Write some state state = states.NewState() s.WriteState(state) if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } } // A plan with a custom state save path func TestMetaBackend_planLocalStatePath(t *testing.T) { td := t.TempDir() testCopyDir(t, testFixturePath("backend-plan-local"), td) t.Chdir(td) original := testState() markStateForMatching(original, "hello") backendConfigBlock := cty.ObjectVal(map[string]cty.Value{ "path": cty.NullVal(cty.String), "workspace_dir": cty.NullVal(cty.String), }) backendConfigRaw, err := plans.NewDynamicValue(backendConfigBlock, backendConfigBlock.Type()) if err != nil { t.Fatal(err) } plan := &plans.Plan{ Backend: &plans.Backend{ Type: "local", Config: backendConfigRaw, Workspace: "default", }, } // Create an alternate output path statePath := "foo.tfstate" // put an initial state there that needs to be backed up err = (statemgr.NewFilesystem(statePath)).WriteState(original) if err != nil { t.Fatal(err) } // Setup the meta m := testMetaBackend(t, nil) m.stateOutPath = statePath // Get the backend b, diags := m.BackendForLocalPlan(plan) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state != nil { t.Fatal("default workspace state is not nil, but should be because we've not put anything there") } // Verify the default path doesn't exist if _, err := os.Stat(DefaultStateFilename); err == nil { t.Fatalf("err: %s", err) } // Verify a backup doesn't exists if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil { t.Fatal("file should not exist") } // Verify we have no configured backend/legacy path := filepath.Join(m.DataDir(), DefaultStateFilename) if _, err := os.Stat(path); err == nil { t.Fatalf("should not have backend configured") } // Write some state state = states.NewState() mark := markStateForMatching(state, "changing") s.WriteState(state) if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect { f, err := os.Open(statePath) if err != nil { t.Fatalf("err: %s", err) } actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } assertStateHasMarker(t, actual.State, mark) } // Verify we have a backup if isEmptyState(statePath + DefaultBackupExtension) { t.Fatal("backup is empty") } } // A plan that has no backend config, matching local state func TestMetaBackend_planLocalMatch(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-plan-local-match"), td) t.Chdir(td) backendConfigBlock := cty.ObjectVal(map[string]cty.Value{ "path": cty.NullVal(cty.String), "workspace_dir": cty.NullVal(cty.String), }) backendConfigRaw, err := plans.NewDynamicValue(backendConfigBlock, backendConfigBlock.Type()) if err != nil { t.Fatal(err) } plan := &plans.Plan{ Backend: &plans.Backend{ Type: "local", Config: backendConfigRaw, Workspace: "default", }, } // Setup the meta m := testMetaBackend(t, nil) // Get the backend b, diags := m.BackendForLocalPlan(plan) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s, sDiags := b.StateMgr(backend.DefaultStateName) if sDiags.HasErrors() { t.Fatalf("unexpected error: %s", sDiags.Err()) } if err := s.RefreshState(); err != nil { t.Fatalf("unexpected error: %s", err) } state := s.State() if state == nil { t.Fatal("should is nil") } if testStateMgrCurrentLineage(s) != "hello" { t.Fatalf("bad: %#v", state) } // Verify the default path if isEmptyState(DefaultStateFilename) { t.Fatal("state is empty") } // Verify we have no configured backend/legacy path := filepath.Join(m.DataDir(), DefaultStateFilename) if _, err := os.Stat(path); err == nil { t.Fatalf("should not have backend configured") } // Write some state state = states.NewState() mark := markStateForMatching(state, "changing") s.WriteState(state) if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } // Verify the state is where we expect { f, err := os.Open(DefaultStateFilename) if err != nil { t.Fatalf("err: %s", err) } actual, err := statefile.Read(f) f.Close() if err != nil { t.Fatalf("err: %s", err) } assertStateHasMarker(t, actual.State, mark) } // Verify local backup if isEmptyState(DefaultStateFilename + DefaultBackupExtension) { t.Fatal("backup is empty") } } // A plan that contains a workspace that isn't the currently selected workspace func TestMetaBackend_planLocal_mismatchedWorkspace(t *testing.T) { t.Run("local backend", func(t *testing.T) { td := t.TempDir() t.Chdir(td) backendConfigBlock := cty.ObjectVal(map[string]cty.Value{ "path": cty.NullVal(cty.String), "workspace_dir": cty.NullVal(cty.String), }) backendConfigRaw, err := plans.NewDynamicValue(backendConfigBlock, backendConfigBlock.Type()) if err != nil { t.Fatal(err) } planWorkspace := "default" plan := &plans.Plan{ Backend: &plans.Backend{ Type: "local", Config: backendConfigRaw, Workspace: planWorkspace, }, } // Setup the meta m := testMetaBackend(t, nil) otherWorkspace := "foobar" err = m.SetWorkspace(otherWorkspace) if err != nil { t.Fatalf("error in test setup: %s", err) } // Get the backend _, diags := m.BackendForLocalPlan(plan) if !diags.HasErrors() { t.Fatalf("expected an error but got none: %s", diags.ErrWithWarnings()) } expectedMsgs := []string{ fmt.Sprintf("The plan file describes changes to the %q workspace, but the %q workspace is currently in use", planWorkspace, otherWorkspace, ), fmt.Sprintf("terraform workspace select %s", planWorkspace), } for _, msg := range expectedMsgs { if !strings.Contains(diags.Err().Error(), msg) { t.Fatalf("expected error to include %q, but got:\n%s", msg, diags.Err()) } } }) t.Run("cloud backend", func(t *testing.T) { td := t.TempDir() t.Chdir(td) planWorkspace := "prod" cloudConfigBlock := cty.ObjectVal(map[string]cty.Value{ "organization": cty.StringVal("hashicorp"), "workspaces": cty.ObjectVal(map[string]cty.Value{ "name": cty.StringVal(planWorkspace), }), }) cloudConfigRaw, err := plans.NewDynamicValue(cloudConfigBlock, cloudConfigBlock.Type()) if err != nil { t.Fatal(err) } plan := &plans.Plan{ Backend: &plans.Backend{ Type: "cloud", Config: cloudConfigRaw, Workspace: planWorkspace, }, } // Setup the meta m := testMetaBackend(t, nil) otherWorkspace := "foobar" err = m.SetWorkspace(otherWorkspace) if err != nil { t.Fatalf("error in test setup: %s", err) } // Get the backend _, diags := m.BackendForLocalPlan(plan) if !diags.HasErrors() { t.Fatalf("expected an error but got none: %s", diags.ErrWithWarnings()) } expectedMsgs := []string{ fmt.Sprintf("The plan file describes changes to the %q workspace, but the %q workspace is currently in use", planWorkspace, otherWorkspace, ), fmt.Sprintf(`If you'd like to continue to use the plan file, make sure the cloud block in your configuration contains the workspace name %q`, planWorkspace), } for _, msg := range expectedMsgs { if !strings.Contains(diags.Err().Error(), msg) { t.Fatalf("expected error to include `%s`, but got:\n%s", msg, diags.Err()) } } }) } // init a backend using -backend-config options multiple times func TestMetaBackend_configureBackendWithExtra(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("init-backend-empty"), td) t.Chdir(td) extras := map[string]cty.Value{"path": cty.StringVal("hello")} m := testMetaBackend(t, nil) opts := &BackendOpts{ ConfigOverride: configs.SynthBody("synth", extras), Init: true, } _, cHash, err := m.backendConfig(opts) if err != nil { t.Fatal(err) } // init the backend _, diags := m.Backend(&BackendOpts{ ConfigOverride: configs.SynthBody("synth", extras), Init: true, }) if diags.HasErrors() { t.Fatal(diags.Err()) } // Check the state s := testDataStateRead(t, filepath.Join(DefaultDataDir, backendLocal.DefaultStateFilename)) if s.Backend.Hash != uint64(cHash) { t.Fatal("mismatched state and config backend hashes") } // init the backend again with the same options m = testMetaBackend(t, nil) _, err = m.Backend(&BackendOpts{ ConfigOverride: configs.SynthBody("synth", extras), Init: true, }) if err != nil { t.Fatalf("unexpected error: %s", err) } // Check the state s = testDataStateRead(t, filepath.Join(DefaultDataDir, backendLocal.DefaultStateFilename)) if s.Backend.Hash != uint64(cHash) { t.Fatal("mismatched state and config backend hashes") } } // when configuring a default local state, don't delete local state func TestMetaBackend_localDoesNotDeleteLocal(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("init-backend-empty"), td) t.Chdir(td) // // create our local state orig := states.NewState() orig.SetOutputValue( addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), cty.StringVal("bar"), false, ) testStateFileDefault(t, orig) m := testMetaBackend(t, nil) m.forceInitCopy = true // init the backend _, diags := m.Backend(&BackendOpts{Init: true}) if diags.HasErrors() { t.Fatal(diags.Err()) } // check that we can read the state s := testStateRead(t, DefaultStateFilename) if s.Empty() { t.Fatal("our state was deleted") } } // move options from config to -backend-config func TestMetaBackend_backendConfigToExtra(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("init-backend"), td) t.Chdir(td) // init the backend m := testMetaBackend(t, nil) _, err := m.Backend(&BackendOpts{ Init: true, }) if err != nil { t.Fatalf("unexpected error: %s", err) } // Check the state s := testDataStateRead(t, filepath.Join(DefaultDataDir, backendLocal.DefaultStateFilename)) backendHash := s.Backend.Hash // init again but remove the path option from the config cfg := "terraform {\n backend \"local\" {}\n}\n" if err := os.WriteFile("main.tf", []byte(cfg), 0644); err != nil { t.Fatal(err) } // init the backend again with the options extras := map[string]cty.Value{"path": cty.StringVal("hello")} m = testMetaBackend(t, nil) m.forceInitCopy = true _, diags := m.Backend(&BackendOpts{ ConfigOverride: configs.SynthBody("synth", extras), Init: true, }) if diags.HasErrors() { t.Fatal(diags.Err()) } s = testDataStateRead(t, filepath.Join(DefaultDataDir, backendLocal.DefaultStateFilename)) if s.Backend.Hash == backendHash { t.Fatal("state.Backend.Hash was not updated") } } // no config; return inmem backend stored in state func TestBackendFromState(t *testing.T) { wd := tempWorkingDirFixture(t, "backend-from-state") t.Chdir(wd.RootModuleDir()) // Setup the meta m := testMetaBackend(t, nil) m.WorkingDir = wd // terraform caches a small "state" file that stores the backend config. // This test must override m.dataDir so it loads the "terraform.tfstate" file in the // test directory as the backend config cache. This fixture is really a // fixture for the data dir rather than the module dir, so we'll override // them to match just for this test. wd.OverrideDataDir(".") stateBackend, diags := m.backendFromState(context.Background()) if diags.HasErrors() { t.Fatal(diags.Err()) } if _, ok := stateBackend.(*backendInmem.Backend); !ok { t.Fatal("did not get expected inmem backend") } } func Test_determineInitReason(t *testing.T) { cases := map[string]struct { cloudMode cloud.ConfigChangeMode backendState workdir.BackendStateFile backendConfig configs.Backend wantErr string }{ // All scenarios involving Cloud backend "change in cloud config": { cloudMode: cloud.ConfigChangeInPlace, backendState: workdir.BackendStateFile{ Backend: &workdir.BackendConfigState{ Type: "cloud", // Other fields unnecessary }, }, backendConfig: configs.Backend{ Type: "cloud", // Other fields unnecessary }, wantErr: `HCP Terraform configuration block has changed`, }, "migrate backend to cloud": { cloudMode: cloud.ConfigMigrationIn, backendState: workdir.BackendStateFile{ Backend: &workdir.BackendConfigState{ Type: "foobar", // Other fields unnecessary }, }, backendConfig: configs.Backend{ Type: "cloud", // Other fields unnecessary }, wantErr: `Changed from backend "foobar" to HCP Terraform`, }, "migrate cloud to backend": { cloudMode: cloud.ConfigMigrationOut, backendState: workdir.BackendStateFile{ Backend: &workdir.BackendConfigState{ Type: "cloud", // Other fields unnecessary }, }, backendConfig: configs.Backend{ Type: "foobar", // Other fields unnecessary }, wantErr: `Changed from HCP Terraform to backend "foobar"`, }, // Changes within the backend config block "backend type changed": { cloudMode: cloud.ConfigChangeIrrelevant, backendState: workdir.BackendStateFile{ Backend: &workdir.BackendConfigState{ Type: "foobar1", // Other fields unnecessary }, }, backendConfig: configs.Backend{ Type: "foobar2", // Other fields unnecessary }, wantErr: `Backend type changed from "foobar1" to "foobar2`, }, "backend config changed": { // Note that we don't need to include differing config to trigger this // scenario, as we're hitting the default case. If the types match, then // only the config is left to differ. // See the comment above determineInitReason for more info. cloudMode: cloud.ConfigChangeIrrelevant, backendState: workdir.BackendStateFile{ Backend: &workdir.BackendConfigState{ Type: "foobar", // Other fields unnecessary }, }, backendConfig: configs.Backend{ Type: "foobar", // Other fields unnecessary }, wantErr: `Backend configuration block has changed`, }, } for tn, tc := range cases { t.Run(tn, func(t *testing.T) { m := Meta{} diags := m.determineInitReason(tc.backendState.Backend.Type, tc.backendConfig.Type, tc.cloudMode) if !strings.Contains(diags.Err().Error(), tc.wantErr) { t.Fatalf("expected error diagnostic detail to include \"%s\" but it's missing: %s", tc.wantErr, diags.Err()) } }) } } // Verify that using variables results in an error func TestMetaBackend_configureStateStoreVariableUse(t *testing.T) { wantErr := "Variables not allowed" 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{""}, ) cases := map[string]struct { fixture string wantErr string }{ "no variables in nested provider block": { fixture: "state-store-new-vars-in-provider", wantErr: wantErr, }, "no variables in the state_store block": { fixture: "state-store-new-vars-in-store", wantErr: wantErr, }, } for tn, tc := range cases { t.Run(tn, func(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath(tc.fixture), 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 _, err := m.Backend(&BackendOpts{ Init: true, StateStoreConfig: mod.StateStore, Locks: locks, }) if err == nil { t.Fatal("should error") } if !strings.Contains(err.Err().Error(), tc.wantErr) { t.Fatalf("error should include %q, got: %s", tc.wantErr, err.Err()) } }) } } func TestSavedBackend(t *testing.T) { // Create a temporary working directory td := t.TempDir() testCopyDir(t, testFixturePath("backend-unset"), td) // Backend state file describes local backend, config lacks backend config t.Chdir(td) // Make a state manager for the backend state file, // read state from file m := testMetaBackend(t, nil) statePath := filepath.Join(m.DataDir(), DefaultStateFilename) sMgr := &clistate.LocalState{Path: statePath} err := sMgr.RefreshState() if err != nil { t.Fatalf("unexpected error: %s", err) } // Code under test b, diags := m.savedBackend(sMgr) if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } // The test fixtures used in this test include a backend state file describing // a local backend with the non-default path value below (local-state.tfstate) localB, ok := b.(*local.Local) if !ok { t.Fatalf("expected the returned backend to be a local backend, matching the test fixtures.") } if localB.StatePath != "local-state.tfstate" { t.Fatalf("expected the local backend to be configured using the backend state file, but got unexpected configuration values.") } } func TestSavedStateStore(t *testing.T) { t.Run("the returned state store is configured with the backend state and not the current config", func(t *testing.T) { // Create a temporary working directory chunkSize := 42 td := t.TempDir() testCopyDir(t, testFixturePath("state-store-changed/store-config"), td) // Fixtures with config that differs from backend state file t.Chdir(td) mock := testStateStoreMock(t) mock.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { // Assert that the state store is configured using backend state file values from the fixtures config := req.Config.AsValueMap() if v, ok := config["region"]; ok && (v.Equals(cty.NullVal(cty.String)) != cty.True) { // The backend state file has a null value for region, so if we're here we've somehow got a non-null value t.Fatalf("expected the provider to be configured with values from the backend state file (where region is unset/null), not the config. Got value: %#v", v) } return providers.ConfigureProviderResponse{} } mock.ConfigureStateStoreFn = func(req providers.ConfigureStateStoreRequest) providers.ConfigureStateStoreResponse { // Assert that the state store is configured using backend state file values from the fixtures config := req.Config.AsValueMap() if config["value"].AsString() != "old-value" { t.Fatalf("expected the state store to be configured with values from the backend state file (the string \"old-value\"), not the config. Got: %#v", config) } return providers.ConfigureStateStoreResponse{ Capabilities: providers.StateStoreServerCapabilities{ ChunkSize: int64(chunkSize), }, } } mock.SetStateStoreChunkSizeFn = func(storeType string, size int) { if storeType != "test_store" || size != chunkSize { t.Fatalf("expected SetStateStoreChunkSize to be passed store type %q and chunk size %v, but got %q and %v", "test_store", chunkSize, storeType, size, ) } } // Make a state manager for accessing the backend state file, // and read the backend state from file m := testMetaBackend(t, nil) m.testingOverrides = metaOverridesForProvider(mock) statePath := filepath.Join(m.DataDir(), DefaultStateFilename) sMgr := &clistate.LocalState{Path: statePath} err := sMgr.RefreshState() if err != nil { t.Fatalf("unexpected error: %s", err) } // Code under test b, diags := m.savedStateStore(sMgr) if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } if _, ok := b.(*pluggable.Pluggable); !ok { t.Fatalf( "expected savedStateStore to return a backend.Backend interface with concrete type %s, but got something else: %#v", "*pluggable.Pluggable", b, ) } if !mock.SetStateStoreChunkSizeCalled { t.Fatal("expected configuring the pluggable state store to include a call to SetStateStoreChunkSize on the provider") } }) t.Run("error - when there's no state stores in provider", func(t *testing.T) { // Create a temporary working directory td := t.TempDir() testCopyDir(t, testFixturePath("state-store-changed/store-config"), td) // Fixtures with config that differs from backend state file t.Chdir(td) mock := testStateStoreMock(t) delete(mock.GetProviderSchemaResponse.StateStores, "test_store") // Remove the only state store impl. // Make a state manager for accessing the backend state file, // and read the backend state from file m := testMetaBackend(t, nil) m.testingOverrides = metaOverridesForProvider(mock) statePath := filepath.Join(m.DataDir(), DefaultStateFilename) sMgr := &clistate.LocalState{Path: statePath} err := sMgr.RefreshState() if err != nil { t.Fatalf("unexpected error: %s", err) } _, diags := m.savedStateStore(sMgr) if !diags.HasErrors() { t.Fatal("expected errors but got none") } expectedErr := "Provider does not support pluggable state storage" if !strings.Contains(diags.Err().Error(), expectedErr) { t.Fatalf("expected the returned error to include %q, got: %s", expectedErr, diags.Err(), ) } }) t.Run("error - when there's no matching state store in provider Terraform suggests different identifier", func(t *testing.T) { // Create a temporary working directory td := t.TempDir() testCopyDir(t, testFixturePath("state-store-changed/store-config"), td) // Fixtures with config that differs from backend state file t.Chdir(td) mock := testStateStoreMock(t) testStore := mock.GetProviderSchemaResponse.StateStores["test_store"] delete(mock.GetProviderSchemaResponse.StateStores, "test_store") // Make the provider contain a "test_bore" impl., while the config specifies a "test_store" impl. mock.GetProviderSchemaResponse.StateStores["test_bore"] = testStore // Make a state manager for accessing the backend state file, // and read the backend state from file m := testMetaBackend(t, nil) m.testingOverrides = metaOverridesForProvider(mock) statePath := filepath.Join(m.DataDir(), DefaultStateFilename) sMgr := &clistate.LocalState{Path: statePath} err := sMgr.RefreshState() if err != nil { t.Fatalf("unexpected error: %s", err) } _, diags := m.savedStateStore(sMgr) if !diags.HasErrors() { t.Fatal("expected errors but got none") } expectedErr := "State store not implemented by the provider" if !strings.Contains(diags.Err().Error(), expectedErr) { t.Fatalf("expected the returned error to include %q, got: %s", expectedErr, diags.Err(), ) } expectedMsg := `Did you mean "test_bore"?` if !strings.Contains(diags.Err().Error(), expectedMsg) { t.Fatalf("expected the returned error to include %q, got: %s", expectedMsg, diags.Err(), ) } }) } func TestMetaBackend_GetStateStoreProviderFactory(t *testing.T) { // See internal/command/e2etest/meta_backend_test.go for test case // where a provider factory is found using a local provider cache t.Run("returns an error if a matching factory can't be found", func(t *testing.T) { // Set up locks locks := depsfile.NewLocks() providerAddr := addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/simple") 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{""}, ) config := &configs.StateStore{ ProviderAddr: tfaddr.MustParseProviderSource("registry.terraform.io/hashicorp/simple"), Provider: &configs.Provider{ Name: "foobar", }, Type: "store", } // Setup the meta and test providerFactoriesDuringInit m := testMetaBackend(t, nil) _, diags := m.StateStoreProviderFactoryFromConfig(config, locks) if !diags.HasErrors() { t.Fatalf("expected error but got none") } expectedErr := "Provider unavailable" expectedDetail := "Terraform experienced an error when trying to use provider foobar (\"registry.terraform.io/hashicorp/simple\") to initialize the \"store\" state store" if diags[0].Description().Summary != expectedErr { t.Fatalf("expected error summary to include %q but got: %s", expectedErr, diags[0].Description().Summary, ) } if !strings.Contains(diags[0].Description().Detail, expectedDetail) { t.Fatalf("expected error detail to include %q but got: %s", expectedErr, diags[0].Description().Detail, ) } }) t.Run("returns an error if provider addr data is missing", func(t *testing.T) { // Only minimal locks needed locks := depsfile.NewLocks() config := &configs.StateStore{ ProviderAddr: tfaddr.Provider{}, // Empty } // Setup the meta and test providerFactoriesDuringInit m := testMetaBackend(t, nil) _, diags := m.StateStoreProviderFactoryFromConfig(config, locks) if !diags.HasErrors() { t.Fatal("expected and error but got none") } expectedErr := "Unknown provider used for state storage" if !strings.Contains(diags.Err().Error(), expectedErr) { t.Fatalf("expected error to include %q but got: %s", expectedErr, diags.Err().Error(), ) } }) } // Test the stateStoreInitFromConfig method, which relies on calling code to have already parsed the state_store block // from the config and for config overrides to already be reflected in the first config argument. func TestMetaBackend_stateStoreInitFromConfig(t *testing.T) { expectedRegionAttr := "foobar" expectedValueAttr := "foobar" config := &configs.StateStore{ Type: "test_store", Config: configBodyForTest(t, fmt.Sprintf(`value = "%s"`, expectedValueAttr)), Provider: &configs.Provider{ Config: configBodyForTest(t, fmt.Sprintf(`region = "%s"`, expectedRegionAttr)), }, ProviderAddr: addrs.NewDefaultProvider("test"), } t.Run("the returned state store is configured with the provided config and expected chunk size", func(t *testing.T) { // Prepare provider factories for use chunkSize := 42 mock := testStateStoreMock(t) mock.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { // Assert that the state store is configured using backend state file values from the fixtures config := req.Config.AsValueMap() if config["region"].AsString() != expectedRegionAttr { t.Fatalf("expected the provider attr to be configured with %q, got %q", expectedRegionAttr, config["region"].AsString()) } return providers.ConfigureProviderResponse{} } mock.ConfigureStateStoreFn = func(req providers.ConfigureStateStoreRequest) providers.ConfigureStateStoreResponse { // Assert that the state store is configured using backend state file values from the fixtures config := req.Config.AsValueMap() if config["value"].AsString() != expectedValueAttr { t.Fatalf("expected the state store attr to be configured with %q, got %q", expectedValueAttr, config["value"].AsString()) } return providers.ConfigureStateStoreResponse{ Capabilities: providers.StateStoreServerCapabilities{ ChunkSize: int64(chunkSize), }, } } mock.SetStateStoreChunkSizeFn = func(storeType string, size int) { if storeType != "test_store" || size != chunkSize { t.Fatalf("expected SetStateStoreChunkSize to be passed store type %q and chunk size %v, but got %q and %v", "test_store", chunkSize, storeType, size, ) } } providerAddr := tfaddr.MustParseProviderSource("hashicorp/test") constraint, err := providerreqs.ParseVersionConstraints(">1.0.0") if err != nil { t.Fatalf("test setup failed when making constraint: %s", err) } locks := depsfile.NewLocks() locks.SetProvider( providerAddr, versions.MustParseVersion("1.2.3"), constraint, []providerreqs.Hash{""}, ) // Prepare the meta m := testMetaBackend(t, nil) m.testingOverrides = metaOverridesForProvider(mock) // Code under test _, _, _, diags := m.stateStoreInitFromConfig(config, locks) if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } if !mock.SetStateStoreChunkSizeCalled { t.Fatal("expected configuring the pluggable state store to include a call to SetStateStoreChunkSize on the provider") } }) t.Run("error - when there's no state stores in provider", func(t *testing.T) { // Prepare the meta m := testMetaBackend(t, nil) mock := testStateStoreMock(t) delete(mock.GetProviderSchemaResponse.StateStores, "test_store") // Remove the only state store impl. m.testingOverrides = metaOverridesForProvider(mock) 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{""}, ) _, _, _, diags := m.stateStoreInitFromConfig(config, locks) if !diags.HasErrors() { t.Fatal("expected errors but got none") } expectedErr := "Provider does not support pluggable state storage" if !strings.Contains(diags.Err().Error(), expectedErr) { t.Fatalf("expected the returned error to include %q, got: %s", expectedErr, diags.Err(), ) } }) t.Run("error - when there's no matching state store in provider Terraform suggests different identifier", func(t *testing.T) { // Prepare the meta m := testMetaBackend(t, nil) mock := testStateStoreMock(t) testStore := mock.GetProviderSchemaResponse.StateStores["test_store"] delete(mock.GetProviderSchemaResponse.StateStores, "test_store") // Make the provider contain a "test_bore" impl., while the config specifies a "test_store" impl. mock.GetProviderSchemaResponse.StateStores["test_bore"] = testStore m.testingOverrides = metaOverridesForProvider(mock) 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("1.2.3"), constraint, []providerreqs.Hash{""}, ) _, _, _, diags := m.stateStoreInitFromConfig(config, locks) if !diags.HasErrors() { t.Fatal("expected errors but got none") } expectedErr := "State store not implemented by the provider" if !strings.Contains(diags.Err().Error(), expectedErr) { t.Fatalf("expected the returned error to include %q, got: %s", expectedErr, diags.Err(), ) } expectedMsg := `Did you mean "test_bore"?` if !strings.Contains(diags.Err().Error(), expectedMsg) { t.Fatalf("expected the returned error to include %q, got: %s", expectedMsg, diags.Err(), ) } }) } func TestMetaBackend_stateStoreConfig(t *testing.T) { // Reused in tests config := &configs.StateStore{ Type: "test_store", Config: configBodyForTest(t, fmt.Sprintf(`value = "%s"`, "foobar")), Provider: &configs.Provider{ Config: configBodyForTest(t, fmt.Sprintf(`region = "%s"`, "foobar")), }, ProviderAddr: addrs.NewDefaultProvider("test"), } 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{""}, ) t.Run("override config can change values of custom attributes in the state_store block", func(t *testing.T) { overrideValue := "overridden" configOverride := configs.SynthBody("synth", map[string]cty.Value{"value": cty.StringVal(overrideValue)}) opts := &BackendOpts{ StateStoreConfig: config, ConfigOverride: configOverride, Init: true, Locks: locks, } mock := testStateStoreMock(t) m := testMetaBackend(t, nil) m.testingOverrides = metaOverridesForProvider(mock) finalConfig, _, diags := m.stateStoreConfig(opts) if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } attrs, attrDiags := finalConfig.Config.JustAttributes() if attrDiags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } gotAttr, attrDiags := attrs["value"].Expr.Value(nil) if attrDiags.HasErrors() { t.Fatalf("unexpected errors: %s", attrDiags.Error()) } if gotAttr.AsString() != overrideValue { t.Fatalf("expected the `value` attr in the state_store block to be overridden with value %q, but got %q", overrideValue, attrs["value"], ) } }) t.Run("error - no config present", func(t *testing.T) { opts := &BackendOpts{ StateStoreConfig: nil, // unset Init: true, Locks: locks, } mock := testStateStoreMock(t) m := testMetaBackend(t, nil) m.testingOverrides = metaOverridesForProvider(mock) _, _, diags := m.stateStoreConfig(opts) if !diags.HasErrors() { t.Fatal("expected errors but got none") } expectedErr := "Missing state store configuration" if !strings.Contains(diags.Err().Error(), expectedErr) { t.Fatalf("expected the returned error to include %q, got: %s", expectedErr, diags.Err(), ) } }) t.Run("error - when there's no state stores in provider", func(t *testing.T) { mock := testStateStoreMock(t) delete(mock.GetProviderSchemaResponse.StateStores, "test_store") // Remove the only state store impl. opts := &BackendOpts{ StateStoreConfig: config, Init: true, Locks: locks, } m := testMetaBackend(t, nil) m.testingOverrides = metaOverridesForProvider(mock) _, _, diags := m.stateStoreConfig(opts) if !diags.HasErrors() { t.Fatal("expected errors but got none") } expectedErr := "Provider does not support pluggable state storage" if !strings.Contains(diags.Err().Error(), expectedErr) { t.Fatalf("expected the returned error to include %q, got: %s", expectedErr, diags.Err(), ) } }) t.Run("error - when there's no matching state store in provider Terraform suggests different identifier", func(t *testing.T) { mock := testStateStoreMock(t) testStore := mock.GetProviderSchemaResponse.StateStores["test_store"] delete(mock.GetProviderSchemaResponse.StateStores, "test_store") // Make the provider contain a "test_bore" impl., while the config specifies a "test_store" impl. mock.GetProviderSchemaResponse.StateStores["test_bore"] = testStore opts := &BackendOpts{ StateStoreConfig: config, Init: true, Locks: locks, } m := testMetaBackend(t, nil) m.testingOverrides = metaOverridesForProvider(mock) _, _, diags := m.stateStoreConfig(opts) if !diags.HasErrors() { t.Fatal("expected errors but got none") } expectedErr := "State store not implemented by the provider" if !strings.Contains(diags.Err().Error(), expectedErr) { t.Fatalf("expected the returned error to include %q, got: %s", expectedErr, diags.Err(), ) } expectedSuggestion := `Did you mean "test_bore"?` if !strings.Contains(diags.Err().Error(), expectedSuggestion) { t.Fatalf("expected the returned error to include a suggestion for fixing a typo %q, got: %s", expectedSuggestion, diags.Err(), ) } }) t.Run("error - locks are empty and the provider required by the state_store block isn't present", func(t *testing.T) { opts := &BackendOpts{ StateStoreConfig: config, Init: false, // Not being used in an init operation; hence why we're checking dependencies. Locks: depsfile.NewLocks(), // empty! } mock := testStateStoreMock(t) m := testMetaBackend(t, nil) m.testingOverrides = metaOverridesForProvider(mock) _, _, diags := m.stateStoreConfig(opts) if !diags.HasErrors() { t.Fatal("expected errors but got none") } expectedErrMsgs := []string{ "Inconsistent dependency lock file", "- provider registry.terraform.io/hashicorp/test: required by this configuration but no version is selected", } for _, errMsg := range expectedErrMsgs { if !strings.Contains(diags.Err().Error(), errMsg) { t.Fatalf("expected the returned error to include %q, got: %s", errMsg, diags.Err(), ) } } }) t.Run("ok - locks are empty but reattach config supplies the provider required by state_store block", func(t *testing.T) { reattachConfig := `{ "hashicorp/test": { "Protocol": "grpc", "ProtocolVersion": 5, "Pid": 12345, "Test": true, "Addr": { "Network": "unix", "String":"/var/folders/xx/abcde12345/T/plugin12345" } } }` t.Setenv("TF_REATTACH_PROVIDERS", reattachConfig) opts := &BackendOpts{ StateStoreConfig: config, Init: false, // Not being used in an init operation; hence why we're checking dependencies. Locks: depsfile.NewLocks(), // empty! } mock := testStateStoreMock(t) m := testMetaBackend(t, nil) m.testingOverrides = metaOverridesForProvider(mock) _, _, diags := m.stateStoreConfig(opts) if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } }) } func Test_getStateStorageProviderVersion(t *testing.T) { // Locks only contain hashicorp/test provider 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) } setVersion := versions.MustParseVersion("9.9.9") locks.SetProvider( providerAddr, setVersion, constraint, []providerreqs.Hash{""}, ) t.Run("returns the version of the provider represented in the locks", func(t *testing.T) { c := &configs.StateStore{ Provider: &configs.Provider{}, ProviderAddr: tfaddr.NewProvider(addrs.DefaultProviderRegistryHost, "hashicorp", "test"), } v, diags := getStateStorageProviderVersion(c, locks) if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } expectedVersion, err := providerreqs.GoVersionFromVersion(setVersion) if err != nil { t.Fatalf("test setup failed when making expected version: %s", err) } if !v.Equal(expectedVersion) { t.Fatalf("expected version to be %#v, got %#v", expectedVersion, v) } }) t.Run("returns a nil version when using a builtin provider", func(t *testing.T) { c := &configs.StateStore{ Provider: &configs.Provider{}, ProviderAddr: tfaddr.NewProvider(addrs.BuiltInProviderHost, addrs.BuiltInProviderNamespace, "test"), } v, diags := getStateStorageProviderVersion(c, locks) if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } var expectedVersion *version.Version = nil if !v.Equal(expectedVersion) { t.Fatalf("expected version to be %#v, got %#v", expectedVersion, v) } }) t.Run("returns a nil version when using a re-attached provider", func(t *testing.T) { t.Setenv("TF_REATTACH_PROVIDERS", `{ "test": { "Protocol": "grpc", "ProtocolVersion": 6, "Pid": 12345, "Test": true, "Addr": { "Network": "unix", "String":"/var/folders/xx/abcde12345/T/plugin12345" } } }`) c := &configs.StateStore{ Provider: &configs.Provider{}, ProviderAddr: tfaddr.NewProvider(addrs.DefaultProviderRegistryHost, "hashicorp", "test"), } v, diags := getStateStorageProviderVersion(c, locks) if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } var expectedVersion *version.Version = nil if !v.Equal(expectedVersion) { t.Fatalf("expected version to be %#v, got %#v", expectedVersion, v) } }) t.Run("returns an error diagnostic when version info cannot be obtained from locks", func(t *testing.T) { c := &configs.StateStore{ Type: "missing-provider_foobar", Provider: &configs.Provider{ Name: "missing-provider", }, ProviderAddr: tfaddr.NewProvider(addrs.DefaultProviderRegistryHost, "hashicorp", "missing-provider"), } _, diags := getStateStorageProviderVersion(c, locks) if !diags.HasErrors() { t.Fatal("expected errors but got none") } expectMsg := "The provider dependency used for state storage is missing from the lock file despite being present in the current configuration" if !strings.Contains(diags.Err().Error(), expectMsg) { t.Fatalf("expected error to include %q but got: %s", expectMsg, diags.Err(), ) } }) } func TestMetaBackend_prepareBackend(t *testing.T) { t.Run("it returns a cloud backend from cloud backend config", func(t *testing.T) { // Create a temporary working directory with cloud configuration in td := t.TempDir() testCopyDir(t, testFixturePath("cloud-config"), td) t.Chdir(td) m := testMetaBackend(t, nil) // We cannot initialize a cloud backend so we instead check // the init error is referencing HCP Terraform _, bDiags := m.backend(td, arguments.ViewHuman) if !bDiags.HasErrors() { t.Fatal("expected error but got none") } wantErr := "HCP Terraform or Terraform Enterprise initialization required: please run \"terraform init\"" if !strings.Contains(bDiags.Err().Error(), wantErr) { t.Fatalf("expected error to contain %q, but got: %q", wantErr, bDiags.Err()) } }) t.Run("it returns a backend from backend config", func(t *testing.T) { // Create a temporary working directory with backend configuration in td := t.TempDir() testCopyDir(t, testFixturePath("backend-unchanged"), td) t.Chdir(td) m := testMetaBackend(t, nil) b, bDiags := m.backend(td, arguments.ViewHuman) if bDiags.HasErrors() { t.Fatal("unexpected error: ", bDiags.Err()) } if _, ok := b.(*local.Local); !ok { t.Fatal("expected returned operations backend to be a Local backend") } // Check the type of backend inside the Local via schema // In this case a `local` backend should have been returned by default. // // Look for the path attribute. schema := b.ConfigSchema() if _, ok := schema.Attributes["path"]; !ok { t.Fatalf("expected the operations backend to report the schema of a local backend, but got something unexpected: %#v", schema) } }) t.Run("it returns a local backend when there is empty configuration", func(t *testing.T) { td := t.TempDir() testCopyDir(t, testFixturePath("empty"), td) t.Chdir(td) m := testMetaBackend(t, nil) b, bDiags := m.backend(td, arguments.ViewHuman) if bDiags.HasErrors() { t.Fatal("unexpected error: ", bDiags.Err()) } if _, ok := b.(*local.Local); !ok { t.Fatal("expected returned operations backend to be a Local backend") } // Check the type of backend inside the Local via schema // In this case a `local` backend should have been returned by default. // // Look for the path attribute. schema := b.ConfigSchema() if _, ok := schema.Attributes["path"]; !ok { t.Fatalf("expected the operations backend to report the schema of a local backend, but got something unexpected: %#v", schema) } }) t.Run("it returns a state_store from state_store config", func(t *testing.T) { // Create a temporary working directory with backend configuration in td := t.TempDir() testCopyDir(t, testFixturePath("state-store-unchanged"), td) t.Chdir(td) m := testMetaBackend(t, nil) m.AllowExperimentalFeatures = true mock := testStateStoreMockWithChunkNegotiation(t, 12345) // chunk size needs to be set, value is arbitrary m.testingOverrides = &testingOverrides{ Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): providers.FactoryFixed(mock), }, } // Prepare appropriate locks; config uses a hashicorp/test provider @ v1.2.3 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("1.2.3"), constraint, []providerreqs.Hash{""}, ) b, bDiags := m.backend(td, arguments.ViewHuman) if bDiags.HasErrors() { t.Fatalf("unexpected error: %s", bDiags.Err()) } if _, ok := b.(*local.Local); !ok { t.Fatal("expected returned operations backend to be a Local backend") } // Check the state_store inside the Local via schema // Look for the mock state_store's attribute called `value`. schema := b.ConfigSchema() if _, ok := schema.Attributes["value"]; !ok { t.Fatalf("expected the operations backend to report the schema of the state_store, but got something unexpected: %#v", schema) } }) } func testMetaBackend(t *testing.T, args []string) *Meta { var m Meta m.Ui = new(cli.MockUi) view, _ := testView(t) m.View = view m.process(args) f := m.extendedFlagSet("test") if err := f.Parse(args); err != nil { t.Fatalf("unexpected error: %s", err) } // metaBackend tests are verifying migrate actions m.migrateState = true return &m } // testStateStoreMock returns a mock provider that has a state store implementation // The provider uses the name "test" and the store inside is "test_store". func testStateStoreMock(t *testing.T) *testing_provider.MockProvider { t.Helper() return &testing_provider.MockProvider{ GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "region": {Type: cty.String, Optional: true}, }, }, }, DataSources: map[string]providers.Schema{}, ResourceTypes: map[string]providers.Schema{}, ListResourceTypes: map[string]providers.Schema{}, StateStores: map[string]providers.Schema{ "test_store": { Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "value": { Type: cty.String, Required: true, }, }, }, }, }, }, ConfigureStateStoreFn: func(cssr providers.ConfigureStateStoreRequest) providers.ConfigureStateStoreResponse { return providers.ConfigureStateStoreResponse{ Capabilities: providers.StateStoreServerCapabilities{ ChunkSize: cssr.Capabilities.ChunkSize, }, } }, } } // testStateStoreMockWithChunkNegotiation is just like testStateStoreMock but the returned mock is set up so it'll be configured // without this error: `Failed to negotiate acceptable chunk size` // // This is meant to be a convenience method when a test is definitely not testing anything related to state store configuration. func testStateStoreMockWithChunkNegotiation(t *testing.T, chunkSize int64) *testing_provider.MockProvider { t.Helper() mock := testStateStoreMock(t) mock.ConfigureStateStoreResponse = &providers.ConfigureStateStoreResponse{ Capabilities: providers.StateStoreServerCapabilities{ ChunkSize: chunkSize, }, } return mock } func configBodyForTest(t *testing.T, config string) hcl.Body { t.Helper() f, diags := hclsyntax.ParseConfig([]byte(config), "", hcl.Pos{Line: 1, Column: 1}) if diags.HasErrors() { t.Fatalf("failure creating hcl.Body during test setup: %s", diags.Error()) } return f.Body }