From 44e5f8637558ac00a81c593994aeb8b664bb960c Mon Sep 17 00:00:00 2001 From: Sarah French <15078782+SarahFrench@users.noreply.github.com> Date: Tue, 20 Jan 2026 09:12:52 +0000 Subject: [PATCH] PSS: Allow use of pluggable state stores with `-backend=false` during `init` commands (#38066) --- internal/command/init_test.go | 66 ++++++++++++++++-- internal/command/meta_backend.go | 115 +++++++++++++++++-------------- 2 files changed, 127 insertions(+), 54 deletions(-) diff --git a/internal/command/init_test.go b/internal/command/init_test.go index a522e49912..9648701bb3 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -199,7 +199,6 @@ func TestInit_two_step_provider_download(t *testing.T) { for tn, tc := range cases { t.Run(tn, func(t *testing.T) { - // Create a temporary working directory no tf configuration but has state td := t.TempDir() testCopyDir(t, testFixturePath(tc.workDirPath), td) @@ -1578,7 +1577,6 @@ prompts. t.Errorf("wrong error output\n%s", diff) } }) - } // make sure inputFalse stops execution on migrate @@ -3858,6 +3856,67 @@ func TestInit_stateStore_configChanges(t *testing.T) { } }) + t.Run("the -backend=false flag makes Terraform ignore config and use only the the backend state file during initialization", func(t *testing.T) { + // Create a temporary working directory with state store configuration + // that doesn't match the backend state file + td := t.TempDir() + testCopyDir(t, testFixturePath("state-store-changed/store-config"), td) + t.Chdir(td) + + mockProvider := mockPluggableStateStorageProvider() + + // The previous init implied by this test scenario would have created this. + mockProvider.GetStatesResponse = &providers.GetStatesResponse{States: []string{"default"}} + mockProvider.MockStates = map[string]interface{}{"default": []byte(`{"version": 4,"terraform_version":"1.15.0","serial": 1,"lineage": "","outputs": {},"resources": [],"checks":[]}`)} + + 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() + + ui := new(cli.MockUi) + view, done := testView(t) + meta := Meta{ + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: providerSource, + } + c := &InitCommand{ + Meta: meta, + } + + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + "-backend=false", + } + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("expected code 0 exit code, got %d, output: \n%s", code, testOutput.All()) + } + + // Check output + output := testOutput.All() + expectedOutput := "Terraform has been successfully initialized!" + if !strings.Contains(output, expectedOutput) { + t.Fatalf("expected output to include %q, but got':\n %s", expectedOutput, output) + } + + // When -backend=false the backend/state store isn't initialized, so we don't expect this + // output if the flag has the expected effect on Terraform. + unexpectedOutput := "Initializing the state store..." + if strings.Contains(output, unexpectedOutput) { + t.Fatalf("output included %q, which is unexpected if -backend=false is behaving correctly':\n %s", unexpectedOutput, output) + } + }) + t.Run("handling changed state store config is currently unimplemented", func(t *testing.T) { // Create a temporary working directory with state store configuration // that doesn't match the backend state file @@ -3905,7 +3964,6 @@ func TestInit_stateStore_configChanges(t *testing.T) { if !strings.Contains(output, expectedMsg) { t.Fatalf("expected output to include %q, but got':\n %s", expectedMsg, output) } - }) t.Run("handling changed state store provider config is currently unimplemented", func(t *testing.T) { @@ -4504,7 +4562,7 @@ func TestInit_stateStore_to_backend(t *testing.T) { t.Fatal(err) } expectedOutputs := map[string]*states.OutputValue{ - "test": &states.OutputValue{ + "test": { Addr: addrs.AbsOutputValue{ OutputValue: addrs.OutputValue{ Name: "test", diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index e8e6aceb9c..6911fd4c5c 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -1166,7 +1166,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di cloudMode := cloud.DetectConfigChangeType(s.Backend, backendConfig, false) if !opts.Init { - //user ran another cmd that is not init but they are required to initialize because of a potential relevant change to their backend configuration + // user ran another cmd that is not init but they are required to initialize because of a potential relevant change to their backend configuration initDiag := m.determineInitReason(s.Backend.Type, backendConfig.Type, cloudMode) diags = diags.Append(initDiag) return nil, diags @@ -1293,60 +1293,75 @@ func (m *Meta) backendFromState(_ context.Context) (backend.Backend, tfdiags.Dia log.Printf("[TRACE] Meta.Backend: backend has not previously been initialized in this working directory") return backendLocal.New(), diags } - if s.Backend == nil { - // s.Backend is nil, so return a local backend - log.Printf("[TRACE] Meta.Backend: working directory was previously initialized but has no backend (is using legacy remote state?)") - return backendLocal.New(), diags - } - log.Printf("[TRACE] Meta.Backend: working directory was previously initialized for %q backend", s.Backend.Type) - //backend init function - if s.Backend.Type == "" { - return backendLocal.New(), diags - } - f := backendInit.Backend(s.Backend.Type) - if f == nil { - diags = diags.Append(errBackendSavedUnknown{s.Backend.Type}) - return nil, diags - } - b := f() + // Depending on the contents of the backend state file, + // prepare a backend.Backend in the appropriate way. + var b backend.Backend + switch { + case !s.StateStore.Empty(): + // state_store + log.Printf("[TRACE] Meta.Backend: working directory was previously initialized for %q state store", s.StateStore.Type) + var ssDiags tfdiags.Diagnostics + b, ssDiags = m.savedStateStore(sMgr) // Relies on the state manager's internal state being refreshed above. + diags = diags.Append(ssDiags) + if ssDiags.HasErrors() { + return nil, diags + } + case !s.Backend.Empty(): + // backend or cloud + if s.Backend.Type == "" { + return backendLocal.New(), diags + } + f := backendInit.Backend(s.Backend.Type) + if f == nil { + diags = diags.Append(errBackendSavedUnknown{s.Backend.Type}) + return nil, diags + } + b = f() - // The configuration saved in the working directory state file is used - // in this case, since it will contain any additional values that - // were provided via -backend-config arguments on terraform init. - schema := b.ConfigSchema() - configVal, err := s.Backend.Config(schema) - if err != nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Failed to decode current backend config", - fmt.Sprintf("The backend configuration created by the most recent run of \"terraform init\" could not be decoded: %s. The configuration may have been initialized by an earlier version that used an incompatible configuration structure. Run \"terraform init -reconfigure\" to force re-initialization of the backend.", err), - )) - return nil, diags - } + // The configuration saved in the working directory state file is used + // in this case, since it will contain any additional values that + // were provided via -backend-config arguments on terraform init. + schema := b.ConfigSchema() + configVal, err := s.Backend.Config(schema) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to decode current backend config", + fmt.Sprintf("The backend configuration created by the most recent run of \"terraform init\" could not be decoded: %s. The configuration may have been initialized by an earlier version that used an incompatible configuration structure. Run \"terraform init -reconfigure\" to force re-initialization of the backend.", err), + )) + return nil, diags + } - // Validate the config and then configure the backend - newVal, validDiags := b.PrepareConfig(configVal) - diags = diags.Append(validDiags) - if validDiags.HasErrors() { - return nil, diags - } + // Validate the config and then configure the backend + newVal, validDiags := b.PrepareConfig(configVal) + diags = diags.Append(validDiags) + if validDiags.HasErrors() { + return nil, diags + } - configDiags := b.Configure(newVal) - diags = diags.Append(configDiags) - if configDiags.HasErrors() { - return nil, diags - } + configDiags := b.Configure(newVal) + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + return nil, diags + } - // If the result of loading the backend is an enhanced backend, - // then set up enhanced backend service aliases. - if enhanced, ok := b.(backendrun.OperationsBackend); ok { - log.Printf("[TRACE] Meta.BackendForPlan: backend %T supports operations", b) + // If the result of loading the backend is an enhanced backend, + // then set up enhanced backend service aliases. + if enhanced, ok := b.(backendrun.OperationsBackend); ok { + log.Printf("[TRACE] Meta.BackendForPlan: backend %T supports operations", b) - if err := m.setupEnhancedBackendAliases(enhanced); err != nil { - diags = diags.Append(err) - return nil, diags + if err := m.setupEnhancedBackendAliases(enhanced); err != nil { + diags = diags.Append(err) + return nil, diags + } } + + log.Printf("[TRACE] Meta.Backend: working directory was previously initialized for %q backend", s.Backend.Type) + default: + // s.StateStore and s.Backend are empty, so return a local backend + log.Printf("[TRACE] Meta.Backend: working directory was previously initialized but has no backend (is using legacy remote state?)") + b = backendLocal.New() } return b, diags @@ -1371,8 +1386,8 @@ func (m *Meta) backendFromState(_ context.Context) (backend.Backend, tfdiags.Dia // Unconfiguring a backend (moving from backend => local). func (m *Meta) backend_c_r_S( - c *configs.Backend, cHash int, sMgr *clistate.LocalState, output bool, opts *BackendOpts) (backend.Backend, tfdiags.Diagnostics) { - + c *configs.Backend, cHash int, sMgr *clistate.LocalState, output bool, opts *BackendOpts, +) (backend.Backend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics vt := arguments.ViewJSON