diff --git a/internal/command/e2etest/meta_backend_test.go b/internal/command/e2etest/meta_backend_test.go index c14898ff383e..f7a4abbbcd90 100644 --- a/internal/command/e2etest/meta_backend_test.go +++ b/internal/command/e2etest/meta_backend_test.go @@ -67,7 +67,7 @@ func TestMetaBackend_GetStateStoreProviderFactory(t *testing.T) { // Setup the meta and test GetStateStoreProviderFactory m := command.Meta{} - factory, diags := m.GetStateStoreProviderFactory(config, locks) + factory, diags := m.StateStoreProviderFactoryFromConfig(config, locks) if diags.HasErrors() { t.Fatalf("unexpected error : %s", err) } diff --git a/internal/command/init_run_experiment.go b/internal/command/init_run_experiment.go index 67525e80d1ac..34983553f6fb 100644 --- a/internal/command/init_run_experiment.go +++ b/internal/command/init_run_experiment.go @@ -378,7 +378,7 @@ func (c *InitCommand) initPssBackend(ctx context.Context, root *configs.Module, return nil, true, diags case root.StateStore != nil: // state_store config present - factory, fDiags := c.Meta.GetStateStoreProviderFactory(root.StateStore, configLocks) + factory, fDiags := c.Meta.StateStoreProviderFactoryFromConfig(root.StateStore, configLocks) diags = diags.Append(fDiags) if fDiags.HasErrors() { return nil, true, diags @@ -439,7 +439,6 @@ func (c *InitCommand) initPssBackend(ctx context.Context, root *configs.Module, opts = &BackendOpts{ StateStoreConfig: root.StateStore, Locks: configLocks, - ProviderFactory: factory, CreateDefaultWorkspace: initArgs.CreateDefaultWorkspace, ConfigOverride: configOverride, Init: true, @@ -492,6 +491,7 @@ func (c *InitCommand) initPssBackend(ctx context.Context, root *configs.Module, opts = &BackendOpts{ BackendConfig: backendConfig, + Locks: configLocks, ConfigOverride: configOverride, Init: true, ViewType: initArgs.ViewType, @@ -526,6 +526,7 @@ the backend configuration is present and valid. opts = &BackendOpts{ Init: true, + Locks: configLocks, ViewType: initArgs.ViewType, } } diff --git a/internal/command/init_test.go b/internal/command/init_test.go index f14c35b08b75..829b73734b27 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" "log" "os" "path/filepath" @@ -524,7 +523,7 @@ func TestInit_backendUnset(t *testing.T) { log.Printf("[TRACE] TestInit_backendUnset: beginning second init") // Unset - if err := ioutil.WriteFile("main.tf", []byte(""), 0644); err != nil { + if err := os.WriteFile("main.tf", []byte(""), 0644); err != nil { t.Fatalf("err: %s", err) } @@ -1144,7 +1143,7 @@ func TestInit_backendReinitConfigToExtra(t *testing.T) { // init again but remove the path option from the config cfg := "terraform {\n backend \"local\" {}\n}\n" - if err := ioutil.WriteFile("main.tf", []byte(cfg), 0644); err != nil { + if err := os.WriteFile("main.tf", []byte(cfg), 0644); err != nil { t.Fatal(err) } @@ -2359,7 +2358,7 @@ func TestInit_providerLockFile(t *testing.T) { } lockFile := ".terraform.lock.hcl" - buf, err := ioutil.ReadFile(lockFile) + buf, err := os.ReadFile(lockFile) if err != nil { t.Fatalf("failed to read dependency lock file %s: %s", lockFile, err) } @@ -2540,7 +2539,7 @@ provider "registry.terraform.io/hashicorp/test" { // write input lockfile lockFile := ".terraform.lock.hcl" - if err := ioutil.WriteFile(lockFile, []byte(tc.input), 0644); err != nil { + if err := os.WriteFile(lockFile, []byte(tc.input), 0644); err != nil { t.Fatalf("failed to write input lockfile: %s", err) } @@ -2552,7 +2551,7 @@ provider "registry.terraform.io/hashicorp/test" { t.Fatalf("expected error, got output: \n%s", done(t).Stdout()) } - buf, err := ioutil.ReadFile(lockFile) + buf, err := os.ReadFile(lockFile) if err != nil { t.Fatalf("failed to read dependency lock file %s: %s", lockFile, err) } @@ -4018,6 +4017,203 @@ func TestInit_stateStore_providerUpgrade(t *testing.T) { }) } +func TestInit_stateStore_unset(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("init-state-store"), td) + t.Chdir(td) + + mockProvider := mockPluggableStateStorageProvider() + storeName := "test_store" + otherStoreName := "test_otherstore" + // Make the provider report that it contains a 2nd storage implementation with the above name + mockProvider.GetProviderSchemaResponse.StateStores[otherStoreName] = mockProvider.GetProviderSchemaResponse.StateStores[storeName] + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.2.3"}, // Matches provider version in backend state file fixture + }) + defer close() + + { + log.Printf("[TRACE] TestInit_stateStore_unset: beginning first init") + + ui := cli.NewMockUi() + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: providerSource, + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + }, + } + + // Init + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + } + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) + } + log.Printf("[TRACE] TestInit_stateStore_unset: first init complete") + t.Logf("First run output:\n%s", testOutput.Stdout()) + t.Logf("First run errors:\n%s", testOutput.Stderr()) + + if _, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename)); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + log.Printf("[TRACE] TestInit_stateStore_unset: beginning second init") + + // Unset + if err := os.WriteFile("main.tf", []byte(""), 0644); err != nil { + t.Fatalf("err: %s", err) + } + + ui := cli.NewMockUi() + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: providerSource, + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + }, + } + + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + "-force-copy", + } + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) + } + log.Printf("[TRACE] TestInit_stateStore_unset: second init complete") + t.Logf("Second run output:\n%s", testOutput.Stdout()) + t.Logf("Second run errors:\n%s", testOutput.Stderr()) + + s := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) + if !s.StateStore.Empty() { + t.Fatal("should not have StateStore config") + } + } +} + +func TestInit_stateStore_unset_withoutProviderRequirements(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("init-state-store"), td) + t.Chdir(td) + + mockProvider := mockPluggableStateStorageProvider() + storeName := "test_store" + otherStoreName := "test_otherstore" + // Make the provider report that it contains a 2nd storage implementation with the above name + mockProvider.GetProviderSchemaResponse.StateStores[otherStoreName] = mockProvider.GetProviderSchemaResponse.StateStores[storeName] + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.2.3"}, // Matches provider version in backend state file fixture + }) + defer close() + + { + log.Printf("[TRACE] TestInit_stateStore_unset_withoutProviderRequirements: beginning first init") + + ui := cli.NewMockUi() + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: providerSource, + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + }, + } + + // Init + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + } + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) + } + log.Printf("[TRACE] TestInit_stateStore_unset_withoutProviderRequirements: first init complete") + t.Logf("First run output:\n%s", testOutput.Stdout()) + t.Logf("First run errors:\n%s", testOutput.Stderr()) + + if _, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename)); err != nil { + t.Fatalf("err: %s", err) + } + } + { + log.Printf("[TRACE] TestInit_stateStore_unset_withoutProviderRequirements: beginning second init") + // Unset state store and provider requirements + if err := os.WriteFile("main.tf", []byte(""), 0644); err != nil { + t.Fatalf("err: %s", err) + } + if err := os.WriteFile("providers.tf", []byte(""), 0644); err != nil { + t.Fatalf("err: %s", err) + } + + ui := cli.NewMockUi() + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: providerSource, + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + }, + } + + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + "-force-copy", + } + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) + } + log.Printf("[TRACE] TestInit_stateStore_unset_withoutProviderRequirements: second init complete") + t.Logf("Second run output:\n%s", testOutput.Stdout()) + t.Logf("Second run errors:\n%s", testOutput.Stderr()) + + s := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) + if !s.StateStore.Empty() { + t.Fatal("should not have StateStore config") + } + } +} + // newMockProviderSource is a helper to succinctly construct a mock provider // source that contains a set of packages matching the given provider versions // that are available for installation (from temporary local files). diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index e3d3bd9de924..d8282bac9823 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -60,18 +60,9 @@ type BackendOpts struct { // the root module, or nil if no such block is present. StateStoreConfig *configs.StateStore - // ProvidersFactory contains a factory for creating instances of the - // provider used for pluggable state storage. Each call created a new instance, - // so be conscious of when the provider needs to be configured, etc. - // - // This will only be set if the configuration contains a state_store block. - ProviderFactory providers.Factory - // Locks allows state-migration logic to detect when the provider used for pluggable state storage // during the last init (i.e. what's in the backend state file) is mismatched with the provider // version in use currently. - // - // This will only be set if the configuration contains a state_store block. Locks *depsfile.Locks // ConfigOverride is an hcl.Body that, if non-nil, will be used with @@ -577,16 +568,13 @@ func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, tf return nil, 0, diags } - // Check - is the state store type in the config supported by the provider? - if opts.ProviderFactory == nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Missing provider details when configuring state store", - Detail: "Terraform attempted to configure a state store and no provider factory was available to launch it. This is a bug in Terraform and should be reported.", - }) + pFactory, pDiags := m.StateStoreProviderFactoryFromConfig(opts.StateStoreConfig, opts.Locks) + diags = diags.Append(pDiags) + if pDiags.HasErrors() { return nil, 0, diags } - provider, err := opts.ProviderFactory() + + provider, err := pFactory() if err != nil { diags = diags.Append(fmt.Errorf("error when obtaining provider instance during state store initialization: %w", err)) return nil, 0, diags @@ -781,11 +769,19 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di s.StateStore.Provider.Source.Type, s.StateStore.Provider.Source, ) - return nil, diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Not implemented yet", - Detail: "Unsetting a state store is not implemented yet", - }) + + initReason := fmt.Sprintf("Unsetting the previously set state store %q", s.StateStore.Type) + if !opts.Init { + diags = diags.Append(errStateStoreInitDiag(initReason)) + return nil, diags + } + + if !m.migrateState { + diags = diags.Append(migrateOrReconfigStateStoreDiag) + return nil, diags + } + + return m.stateStore_c_S(sMgr, opts.ViewType) // Configuring a backend for the first time or -reconfigure flag was used case backendConfig != nil && s.Backend.Empty() && @@ -941,7 +937,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di // AND we're not providing any overrides. An override can mean a change overriding an unchanged backend block (indicated by the hash value). if (uint64(cHash) == s.StateStore.Hash) && (!opts.Init || opts.ConfigOverride == nil) { log.Printf("[TRACE] Meta.Backend: using already-initialized, unchanged %q state_store configuration", stateStoreConfig.Type) - savedStateStore, sssDiags := m.savedStateStore(sMgr, opts.ProviderFactory) + savedStateStore, sssDiags := m.savedStateStore(sMgr) diags = diags.Append(sssDiags) // Verify that selected workspace exist. Otherwise prompt user to create one if opts.Init && savedStateStore != nil { @@ -1587,43 +1583,37 @@ func (m *Meta) backend(configPath string, viewType arguments.ViewType) (backendr return nil, diags } + locks, lDiags := m.lockedDependencies() + diags = diags.Append(lDiags) + if lDiags.HasErrors() { + return nil, diags + } + var opts *BackendOpts switch { case root.Backend != nil: opts = &BackendOpts{ BackendConfig: root.Backend, + Locks: locks, ViewType: viewType, } case root.CloudConfig != nil: backendConfig := root.CloudConfig.ToBackendConfig() opts = &BackendOpts{ BackendConfig: &backendConfig, + Locks: locks, ViewType: viewType, } case root.StateStore != nil: - // In addition to config, use of a state_store requires - // provider factory and provider locks data - locks, lDiags := m.lockedDependencies() - diags = diags.Append(lDiags) - if lDiags.HasErrors() { - return nil, diags - } - - factory, fDiags := m.GetStateStoreProviderFactory(root.StateStore, locks) - diags = diags.Append(fDiags) - if fDiags.HasErrors() { - return nil, diags - } - opts = &BackendOpts{ StateStoreConfig: root.StateStore, - ProviderFactory: factory, Locks: locks, ViewType: viewType, } default: // there is no config; defaults to local state storage opts = &BackendOpts{ + Locks: locks, ViewType: viewType, } } @@ -1702,7 +1692,7 @@ func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, backend } // Get the state store as an instance of backend.Backend - b, storeConfigVal, providerConfigVal, moreDiags := m.stateStoreInitFromConfig(c, opts.ProviderFactory) + b, storeConfigVal, providerConfigVal, moreDiags := m.stateStoreInitFromConfig(c, opts.Locks) diags = diags.Append(moreDiags) if diags.HasErrors() { return nil, diags @@ -1869,6 +1859,60 @@ func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, backend return b, diags } +// Unconfiguring a state store (moving from state store => local). +func (m *Meta) stateStore_c_S(ssSMgr *clistate.LocalState, viewType arguments.ViewType) (backend.Backend, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + s := ssSMgr.State() + stateStoreType := s.StateStore.Type + + m.Ui.Output(fmt.Sprintf(strings.TrimSpace(outputStateStoreMigrateLocal), stateStoreType)) + + // Grab a purely local backend to get the local state if it exists + localB, moreDiags := m.Backend(&BackendOpts{ForceLocal: true, Init: true}) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return nil, diags + } + + // Initialize the configured state store + ss, moreDiags := m.savedStateStore(ssSMgr) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return nil, diags + } + + // Perform the migration + err := m.backendMigrateState(&backendMigrateOpts{ + SourceType: stateStoreType, + DestinationType: "local", + Source: ss, + Destination: localB, + ViewType: viewType, + }) + if err != nil { + diags = diags.Append(err) + return nil, diags + } + + // Remove the stored metadata + s.StateStore = nil + if err := ssSMgr.WriteState(s); err != nil { + diags = diags.Append(errStateStoreClearSaved{err}) + return nil, diags + } + if err := ssSMgr.PersistState(); err != nil { + diags = diags.Append(errStateStoreClearSaved{err}) + return nil, diags + } + + v := views.NewInit(viewType, m.View) + v.Output(views.InitMessageCode("state_store_unset"), stateStoreType) + + // Return no state store + return nil, diags +} + // getStateStorageProviderVersion gets the current version of the state store provider that's in use. This is achieved // by inspecting the current locks. // @@ -1939,7 +1983,7 @@ func (m *Meta) createDefaultWorkspace(c *configs.StateStore, b backend.Backend) } // Initializing a saved state store from the backend state file (aka 'cache file', aka 'legacy state file') -func (m *Meta) savedStateStore(sMgr *clistate.LocalState, factory providers.Factory) (backend.Backend, tfdiags.Diagnostics) { +func (m *Meta) savedStateStore(sMgr *clistate.LocalState) (backend.Backend, tfdiags.Diagnostics) { // We're preparing a state_store version of backend.Backend. // // The provider and state store will be configured using the backend state file. @@ -1947,14 +1991,14 @@ func (m *Meta) savedStateStore(sMgr *clistate.LocalState, factory providers.Fact var diags tfdiags.Diagnostics var b backend.Backend - if factory == nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Missing provider details when configuring state store", - Detail: "Terraform attempted to configure a state store and no provider factory was available to launch it. This is a bug in Terraform and should be reported.", - }) + s := sMgr.State() + + factory, pDiags := m.StateStoreProviderFactoryFromConfigState(s.StateStore) + diags = diags.Append(pDiags) + if pDiags.HasErrors() { return nil, diags } + provider, err := factory() if err != nil { diags = diags.Append(fmt.Errorf("error when obtaining provider instance during state store initialization: %w", err)) @@ -1964,7 +2008,6 @@ func (m *Meta) savedStateStore(sMgr *clistate.LocalState, factory providers.Fact // running provider instance inside the returned backend.Backend instance. // Stopping the provider process is the responsibility of the calling code. - s := sMgr.State() resp := provider.GetProviderSchema() if len(resp.StateStores) == 0 { @@ -2242,17 +2285,15 @@ func (m *Meta) backendInitFromConfig(c *configs.Backend) (backend.Backend, cty.V // // NOTE: the backend version of this method, `backendInitFromConfig`, prompts users for input if any required fields // are missing from the backend config. In `stateStoreInitFromConfig` we don't do this, and instead users will see an error. -func (m *Meta) stateStoreInitFromConfig(c *configs.StateStore, factory providers.Factory) (backend.Backend, cty.Value, cty.Value, tfdiags.Diagnostics) { +func (m *Meta) stateStoreInitFromConfig(c *configs.StateStore, locks *depsfile.Locks) (backend.Backend, cty.Value, cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - if factory == nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Missing provider details when configuring state store", - Detail: "Terraform attempted to configure a state store and no provider factory was available to launch it. This is a bug in Terraform and should be reported.", - }) + factory, pDiags := m.StateStoreProviderFactoryFromConfig(c, locks) + diags = diags.Append(pDiags) + if pDiags.HasErrors() { return nil, cty.NilVal, cty.NilVal, diags } + provider, err := factory() if err != nil { diags = diags.Append(fmt.Errorf("error when obtaining provider instance during state store initialization: %w", err)) @@ -2482,7 +2523,7 @@ func (m *Meta) assertSupportedCloudInitOptions(mode cloud.ConfigChangeMode) tfdi return diags } -func (m *Meta) GetStateStoreProviderFactory(config *configs.StateStore, locks *depsfile.Locks) (providers.Factory, tfdiags.Diagnostics) { +func (m *Meta) StateStoreProviderFactoryFromConfig(config *configs.StateStore, locks *depsfile.Locks) (providers.Factory, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics if config == nil || locks == nil { @@ -2533,6 +2574,53 @@ func (m *Meta) GetStateStoreProviderFactory(config *configs.StateStore, locks *d return factory, diags } +func (m *Meta) StateStoreProviderFactoryFromConfigState(cfgState *workdir.StateStoreConfigState) (providers.Factory, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + if cfgState == nil { + panic("nil config passed to StateStoreProviderFactoryFromConfigState") + } + + if cfgState.Provider == nil || cfgState.Provider.Source.IsZero() { + // This should not happen; this data is populated when storing config state + return nil, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unknown provider used for state storage", + Detail: "Terraform could not find the provider used with the state_store. This is a bug in Terraform and should be reported.", + }) + } + + factories, err := m.ProviderFactories() + if err != nil { + // This may happen if the provider isn't present in the provider cache. + // This should be caught earlier by logic that diffs the config against the backend state file. + return nil, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Provider unavailable", + Detail: fmt.Sprintf("Terraform experienced an error when trying to use provider %s (%q) to initialize the %q state store: %s", + cfgState.Type, + cfgState.Provider.Source, + cfgState.Type, + err), + }) + } + + factory, exists := factories[*cfgState.Provider.Source] + if !exists { + return nil, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Provider unavailable", + Detail: fmt.Sprintf("The provider %s (%q) is required to initialize the %q state store, but the matching provider factory is missing. This is a bug in Terraform and should be reported.", + cfgState.Type, + cfgState.Provider.Source, + cfgState.Type, + ), + }) + } + + return factory, diags +} + //------------------------------------------------------------------- // Output constants and initialization code //------------------------------------------------------------------- @@ -2545,6 +2633,10 @@ const outputBackendMigrateLocal = ` Terraform has detected you're unconfiguring your previously set %q backend. ` +const outputStateStoreMigrateLocal = ` +Terraform has detected you're unconfiguring your previously set %q state store. +` + const outputBackendReconfigure = ` [reset][bold]Backend configuration changed![reset] diff --git a/internal/command/meta_backend_errors.go b/internal/command/meta_backend_errors.go index 65221f1a3080..74314bf752a3 100644 --- a/internal/command/meta_backend_errors.go +++ b/internal/command/meta_backend_errors.go @@ -232,3 +232,29 @@ var migrateOrReconfigDiag = tfdiags.Sourceless( "A change in the backend configuration has been detected, which may require migrating existing state.\n\n"+ "If you wish to attempt automatic migration of the state, use \"terraform init -migrate-state\".\n"+ `If you wish to store the current configuration with no changes to the state, use "terraform init -reconfigure".`) + +// migrateOrReconfigStateStoreDiag creates a diagnostic to present to users when +// an init command encounters a mismatch in state store config state and the current config +// and Terraform needs users to provide additional instructions about how it +// should proceed. +var migrateOrReconfigStateStoreDiag = tfdiags.Sourceless( + tfdiags.Error, + "State store configuration changed", + "A change in the state store configuration has been detected, which may require migrating existing state.\n\n"+ + "If you wish to attempt automatic migration of the state, use \"terraform init -migrate-state\".\n"+ + `If you wish to store the current configuration with no changes to the state, use "terraform init -reconfigure".`) + +// errStateStoreClearSaved is a custom error used to alert users that +// Terraform failed to empty the state store state file's contents. +type errStateStoreClearSaved struct { + innerError error +} + +func (e *errStateStoreClearSaved) Error() string { + return fmt.Sprintf(`Error clearing the state store configuration: %s + +Terraform removes the saved state store configuration when you're removing a +configured state store. This must be done so future Terraform runs know to not +use the state store configuration. Please look at the error above, resolve it, +and try again.`, e.innerError) +} diff --git a/internal/command/meta_backend_test.go b/internal/command/meta_backend_test.go index 77f1b37f4362..05803622cbb1 100644 --- a/internal/command/meta_backend_test.go +++ b/internal/command/meta_backend_test.go @@ -6,7 +6,6 @@ package command import ( "context" "fmt" - "io/ioutil" "os" "path/filepath" "reflect" @@ -389,7 +388,7 @@ func TestMetaBackend_configureNewBackendWithState(t *testing.T) { // Verify the default paths don't exist if !isEmptyState(DefaultStateFilename) { - data, _ := ioutil.ReadFile(DefaultStateFilename) + data, _ := os.ReadFile(DefaultStateFilename) t.Fatal("state should not exist, but contains:\n", string(data)) } @@ -439,7 +438,7 @@ func TestMetaBackend_configureNewBackendWithoutCopy(t *testing.T) { // Verify the default paths don't exist if !isEmptyState(DefaultStateFilename) { - data, _ := ioutil.ReadFile(DefaultStateFilename) + data, _ := os.ReadFile(DefaultStateFilename) t.Fatal("state should not exist, but contains:\n", string(data)) } @@ -484,7 +483,7 @@ func TestMetaBackend_configureNewBackendWithStateNoMigrate(t *testing.T) { // Verify the default paths don't exist if !isEmptyState(DefaultStateFilename) { - data, _ := ioutil.ReadFile(DefaultStateFilename) + data, _ := os.ReadFile(DefaultStateFilename) t.Fatal("state should not exist, but contains:\n", string(data)) } @@ -555,7 +554,7 @@ func TestMetaBackend_configureNewBackendWithStateExisting(t *testing.T) { // Verify the default paths don't exist if !isEmptyState(DefaultStateFilename) { - data, _ := ioutil.ReadFile(DefaultStateFilename) + data, _ := os.ReadFile(DefaultStateFilename) t.Fatal("state should not exist, but contains:\n", string(data)) } @@ -626,7 +625,7 @@ func TestMetaBackend_configureNewBackendWithStateExistingNoMigrate(t *testing.T) // Verify the default paths don't exist if !isEmptyState(DefaultStateFilename) { - data, _ := ioutil.ReadFile(DefaultStateFilename) + data, _ := os.ReadFile(DefaultStateFilename) t.Fatal("state should not exist, but contains:\n", string(data)) } @@ -1476,13 +1475,13 @@ func TestMetaBackend_configuredBackendUnset(t *testing.T) { // Verify the default paths don't exist if !isEmptyState(DefaultStateFilename) { - data, _ := ioutil.ReadFile(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, _ := ioutil.ReadFile(DefaultStateFilename + DefaultBackupExtension) + data, _ := os.ReadFile(DefaultStateFilename + DefaultBackupExtension) t.Fatal("backup should not exist, but contains:\n", string(data)) } @@ -1499,7 +1498,7 @@ func TestMetaBackend_configuredBackendUnset(t *testing.T) { // Verify no backup since it was empty to start if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) { - data, _ := ioutil.ReadFile(DefaultStateFilename + DefaultBackupExtension) + data, _ := os.ReadFile(DefaultStateFilename + DefaultBackupExtension) t.Fatal("backup state should be empty, but contains:\n", string(data)) } } @@ -1941,7 +1940,7 @@ func TestMetaBackend_backendConfigToExtra(t *testing.T) { // init again but remove the path option from the config cfg := "terraform {\n backend \"local\" {}\n}\n" - if err := ioutil.WriteFile("main.tf", []byte(cfg), 0644); err != nil { + if err := os.WriteFile("main.tf", []byte(cfg), 0644); err != nil { t.Fatal(err) } @@ -2088,44 +2087,6 @@ func Test_determineInitReason(t *testing.T) { } } -// Unsetting a saved state store -// -// TODO(SarahFrench/radeksimko): currently this test only confirms that we're hitting the switch -// case for this scenario, and will need to be updated when that init feature is implemented. -func TestMetaBackend_configuredStateStoreUnset(t *testing.T) { - td := t.TempDir() - testCopyDir(t, testFixturePath("state-store-unset"), td) - t.Chdir(td) - - // Setup the meta - m := testMetaBackend(t, nil) - 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()) - } - - // No mock provider is used here - yet - // Logic will need to be implemented that lets the init have access to - // a factory for the 'old' provider used for PSS previously. This will be - // used when migrating away from PSS entirely, or to a new PSS configuration. - - // Get the operations backend - _, beDiags := m.Backend(&BackendOpts{ - Init: true, - StateStoreConfig: mod.StateStore, - }) - if !beDiags.HasErrors() { - t.Fatal("expected an error to be returned during partial implementation of PSS") - } - wantErr := "Unsetting a state store is not implemented yet" - if !strings.Contains(beDiags.Err().Error(), wantErr) { - t.Fatalf("expected the returned error to contain %q, but got: %s", wantErr, beDiags.Err()) - } -} - // Changing from using backend to state_store // // TODO(SarahFrench/radeksimko): currently this test only confirms that we're hitting the switch @@ -2135,8 +2096,11 @@ func TestMetaBackend_configuredBackendToStateStore(t *testing.T) { testCopyDir(t, testFixturePath("backend-to-state-store"), td) t.Chdir(td) + mock := testStateStoreMock(t) + // Setup the meta m := testMetaBackend(t, nil) + m.testingOverrides = metaOverridesForProvider(mock) m.AllowExperimentalFeatures = true // Get the state store's config @@ -2145,12 +2109,6 @@ func TestMetaBackend_configuredBackendToStateStore(t *testing.T) { t.Fatalf("unexpected error when loading test config: %s", loadDiags.Err()) } - // Get mock provider to be used during init - // - // This imagines a provider called "test" that contains - // a pluggable state store implementation called "store". - mock := testStateStoreMock(t) - // Get the operations backend locks := depsfile.NewLocks() providerAddr := addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test") @@ -2167,7 +2125,6 @@ func TestMetaBackend_configuredBackendToStateStore(t *testing.T) { _, beDiags := m.Backend(&BackendOpts{ Init: true, StateStoreConfig: mod.StateStore, - ProviderFactory: providers.FactoryFixed(mock), Locks: locks, }) if !beDiags.HasErrors() { @@ -2198,6 +2155,19 @@ func TestMetaBackend_configuredStateStoreToBackend(t *testing.T) { t.Fatalf("unexpected error when loading test config: %s", loadDiags.Err()) } + 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{""}, + ) + // No mock provider is used here - yet // Logic will need to be implemented that lets the init have access to // a factory for the 'old' provider used for PSS previously. This will be @@ -2207,6 +2177,7 @@ func TestMetaBackend_configuredStateStoreToBackend(t *testing.T) { _, beDiags := m.Backend(&BackendOpts{ Init: true, BackendConfig: mod.Backend, + Locks: locks, }) if !beDiags.HasErrors() { t.Fatal("expected an error to be returned during partial implementation of PSS") @@ -2255,8 +2226,11 @@ func TestMetaBackend_configureStateStoreVariableUse(t *testing.T) { 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 @@ -2265,17 +2239,10 @@ func TestMetaBackend_configureStateStoreVariableUse(t *testing.T) { t.Fatalf("unexpected error when loading test config: %s", loadDiags.Err()) } - // Get mock provider to be used during init - // - // This imagines a provider called "test" that contains - // a pluggable state store implementation called "store". - mock := testStateStoreMock(t) - // Get the operations backend _, err := m.Backend(&BackendOpts{ Init: true, StateStoreConfig: mod.StateStore, - ProviderFactory: providers.FactoryFixed(mock), Locks: locks, }) if err == nil { @@ -2330,21 +2297,7 @@ func TestSavedStateStore(t *testing.T) { testCopyDir(t, testFixturePath("state-store-changed/store-config"), td) // Fixtures with config that differs from backend state file t.Chdir(td) - // Make a state manager for accessing the backend state file, - // and read the backend 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) - } - - // Prepare provider factories for use mock := testStateStoreMock(t) - factory := func() (providers.Interface, error) { - return mock, nil - } 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() @@ -2377,8 +2330,19 @@ func TestSavedStateStore(t *testing.T) { } } + // 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, factory) + b, diags := m.savedStateStore(sMgr) if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } @@ -2396,34 +2360,20 @@ func TestSavedStateStore(t *testing.T) { } }) - t.Run("error - no provider factory", func(t *testing.T) { - // sMgr pointing to a file that doesn't exist is sufficient setup for this test - sMgr := &clistate.LocalState{Path: "foobar.tfstate"} - - m := testMetaBackend(t, nil) - _, diags := m.savedStateStore(sMgr, nil) - if !diags.HasErrors() { - t.Fatal("expected errors but got none") - } - - expectedErr := "Missing provider details when configuring state store" - 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) { // 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() @@ -2431,10 +2381,7 @@ func TestSavedStateStore(t *testing.T) { t.Fatalf("unexpected error: %s", err) } - mock := testStateStoreMock(t) - delete(mock.GetProviderSchemaResponse.StateStores, "test_store") // Remove the only state store impl. - - _, diags := m.savedStateStore(sMgr, providers.FactoryFixed(mock)) + _, diags := m.savedStateStore(sMgr) if !diags.HasErrors() { t.Fatal("expected errors but got none") } @@ -2453,9 +2400,17 @@ func TestSavedStateStore(t *testing.T) { 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() @@ -2463,13 +2418,7 @@ func TestSavedStateStore(t *testing.T) { t.Fatalf("unexpected error: %s", err) } - 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 - - _, diags := m.savedStateStore(sMgr, providers.FactoryFixed(mock)) + _, diags := m.savedStateStore(sMgr) if !diags.HasErrors() { t.Fatal("expected errors but got none") } @@ -2519,7 +2468,7 @@ func TestMetaBackend_GetStateStoreProviderFactory(t *testing.T) { // Setup the meta and test providerFactoriesDuringInit m := testMetaBackend(t, nil) - _, diags := m.GetStateStoreProviderFactory(config, locks) + _, diags := m.StateStoreProviderFactoryFromConfig(config, locks) if !diags.HasErrors() { t.Fatalf("expected error but got none") } @@ -2549,7 +2498,7 @@ func TestMetaBackend_GetStateStoreProviderFactory(t *testing.T) { // Setup the meta and test providerFactoriesDuringInit m := testMetaBackend(t, nil) - _, diags := m.GetStateStoreProviderFactory(config, locks) + _, diags := m.StateStoreProviderFactoryFromConfig(config, locks) if !diags.HasErrors() { t.Fatal("expected and error but got none") } @@ -2612,11 +2561,25 @@ func TestMetaBackend_stateStoreInitFromConfig(t *testing.T) { } } + 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 - b, _, _, diags := m.stateStoreInitFromConfig(config, providers.FactoryFixed(mock)) + b, _, _, diags := m.stateStoreInitFromConfig(config, locks) if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } @@ -2633,31 +2596,27 @@ func TestMetaBackend_stateStoreInitFromConfig(t *testing.T) { } }) - t.Run("error - no provider factory set", func(t *testing.T) { - // Prepare the meta - m := testMetaBackend(t, nil) - - _, _, _, diags := m.stateStoreInitFromConfig(config, nil) // Factory value isn't set - if !diags.HasErrors() { - t.Fatal("expected errors but got none") - } - expectedErr := "Missing provider details when configuring state store" - 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) { // 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, providers.FactoryFixed(mock)) + _, _, _, diags := m.stateStoreInitFromConfig(config, locks) if !diags.HasErrors() { t.Fatal("expected errors but got none") } @@ -2673,14 +2632,27 @@ func TestMetaBackend_stateStoreInitFromConfig(t *testing.T) { 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, providers.FactoryFixed(mock)) + _, _, _, diags := m.stateStoreInitFromConfig(config, locks) if !diags.HasErrors() { t.Fatal("expected errors but got none") } @@ -2728,16 +2700,17 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) { 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)}) - mock := testStateStoreMock(t) opts := &BackendOpts{ StateStoreConfig: config, ConfigOverride: configOverride, - ProviderFactory: providers.FactoryFixed(mock), 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()) @@ -2765,34 +2738,15 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) { Locks: locks, } - m := testMetaBackend(t, nil) - _, _, 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 - no provider factory present", func(t *testing.T) { - opts := &BackendOpts{ - StateStoreConfig: config, - ProviderFactory: 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 provider details when configuring state store" + expectedErr := "Missing state store configuration" if !strings.Contains(diags.Err().Error(), expectedErr) { t.Fatalf("expected the returned error to include %q, got: %s", expectedErr, @@ -2807,12 +2761,12 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) { opts := &BackendOpts{ StateStoreConfig: config, - ProviderFactory: providers.FactoryFixed(mock), 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") @@ -2835,12 +2789,13 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) { opts := &BackendOpts{ StateStoreConfig: config, - ProviderFactory: providers.FactoryFixed(mock), 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") @@ -3129,6 +3084,13 @@ func testStateStoreMock(t *testing.T) *testing_provider.MockProvider { }, }, }, + ConfigureStateStoreFn: func(cssr providers.ConfigureStateStoreRequest) providers.ConfigureStateStoreResponse { + return providers.ConfigureStateStoreResponse{ + Capabilities: providers.StateStoreServerCapabilities{ + ChunkSize: cssr.Capabilities.ChunkSize, + }, + } + }, } } diff --git a/internal/command/testdata/init-state-store/main.tf b/internal/command/testdata/init-state-store/main.tf new file mode 100644 index 000000000000..9f39a46e775f --- /dev/null +++ b/internal/command/testdata/init-state-store/main.tf @@ -0,0 +1,6 @@ +terraform { + state_store "test_store" { + provider "test" {} + value = "foobar" + } +} diff --git a/internal/command/testdata/state-store-unset/main.tf b/internal/command/testdata/init-state-store/providers.tf similarity index 60% rename from internal/command/testdata/state-store-unset/main.tf rename to internal/command/testdata/init-state-store/providers.tf index ea30f1e8962c..a6475e1bcf86 100644 --- a/internal/command/testdata/state-store-unset/main.tf +++ b/internal/command/testdata/init-state-store/providers.tf @@ -4,6 +4,4 @@ terraform { source = "hashicorp/test" } } - - # Config has been updated to remove a state_store block } diff --git a/internal/command/testdata/state-store-unset/.terraform.lock.hcl b/internal/command/testdata/state-store-unset/.terraform.lock.hcl deleted file mode 100644 index e5c03757a7fa..000000000000 --- a/internal/command/testdata/state-store-unset/.terraform.lock.hcl +++ /dev/null @@ -1,6 +0,0 @@ -# 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" -} diff --git a/internal/command/testdata/state-store-unset/.terraform/terraform.tfstate b/internal/command/testdata/state-store-unset/.terraform/terraform.tfstate deleted file mode 100644 index b7e79f249766..000000000000 --- a/internal/command/testdata/state-store-unset/.terraform/terraform.tfstate +++ /dev/null @@ -1,19 +0,0 @@ -{ - "version": 3, - "serial": 0, - "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", - "state_store": { - "type": "test_store", - "config": { - "value": "foobar" - }, - "provider": { - "version": "1.2.3", - "source": "registry.terraform.io/hashicorp/test", - "config": { - "region": null - } - }, - "hash": 0 - } -} \ No newline at end of file diff --git a/internal/command/views/init.go b/internal/command/views/init.go index ab16c436b14c..4a633ce69b8b 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -258,6 +258,10 @@ var MessageRegistry map[InitMessageCode]InitMessage = map[InitMessageCode]InitMe HumanValue: errInitConfigError, JSONValue: errInitConfigErrorJSON, }, + "state_store_unset": { + HumanValue: "[reset][green]\n\nSuccessfully unset the state store %q. Terraform will now operate locally.", + JSONValue: "Successfully unset the state store %q. Terraform will now operate locally.", + }, "empty_message": { HumanValue: "", JSONValue: "",