From b8c2cabee8dc3abbfc342ec4422794d4b2f6952a Mon Sep 17 00:00:00 2001 From: Sarah French <15078782+SarahFrench@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:08:08 +0000 Subject: [PATCH] init: Fix when error diagnostics are acted on in PSS's experimental version of `init`. Avoid trying to initialise a state store with insufficient config. (#38125) --- internal/command/init_run_experiment.go | 60 ++++++++++++------------ internal/command/init_test.go | 61 ++++++++++++++++++++++++- internal/configs/state_store.go | 18 ++++---- 3 files changed, 99 insertions(+), 40 deletions(-) diff --git a/internal/command/init_run_experiment.go b/internal/command/init_run_experiment.go index c9ef0abda9..00ed543850 100644 --- a/internal/command/init_run_experiment.go +++ b/internal/command/init_run_experiment.go @@ -167,9 +167,9 @@ func (c *InitCommand) runPssInit(initArgs *arguments.Init, view views.Init) int // With all of the modules (hopefully) installed, we can now try to load the // whole configuration tree. config, confDiags := c.loadConfigWithTests(path, initArgs.TestsDirectory) - // configDiags will be handled after the version constraint check, since an - // incorrect version of terraform may be producing errors for configuration - // constructs added in later versions. + // configDiags will be handled after: + // - the version constraint check has happened + // - and, the backend/state_store is initialised // Before we go further, we'll check to make sure none of the modules in // the configuration declare that they don't support this Terraform @@ -181,6 +181,14 @@ func (c *InitCommand) runPssInit(initArgs *arguments.Init, view views.Init) int return 1 } + // We've passed the core version check, now we can show errors from the early configuration. + // This prevents trying to initialise the backend with faulty configuration. + if earlyConfDiags.HasErrors() { + diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError)), earlyConfDiags) + view.Diagnostics(diags) + return 1 + } + // Now the full configuration is loaded, we can download the providers specified in the configuration. // This is step one of a two-step provider download process // Providers may be downloaded by this code, but the dependency lock file is only updated later in `init` @@ -226,6 +234,24 @@ func (c *InitCommand) runPssInit(initArgs *arguments.Init, view views.Init) int view.Output(views.EmptyMessage) } + // Show any errors from initializing the backend. + // No preamble using `InitConfigError` is present, as we expect + // any errors to from configuring the backend itself. + diags = diags.Append(backDiags) + if backDiags.HasErrors() { + view.Diagnostics(diags) + return 1 + } + + // If everything is ok with the core version check and backend/state_store initialization, + // show other errors from loading the full configuration tree. + diags = diags.Append(confDiags) + if confDiags.HasErrors() { + diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError))) + view.Diagnostics(diags) + return 1 + } + var state *states.State // If we have a functional backend (either just initialized or initialized @@ -288,33 +314,6 @@ func (c *InitCommand) runPssInit(initArgs *arguments.Init, view views.Init) int view.Output(views.EmptyMessage) } - // As Terraform version-related diagnostics are handled above, we can now - // check the diagnostics from the early configuration and the backend. - diags = diags.Append(earlyConfDiags) - diags = diags.Append(backDiags) - if earlyConfDiags.HasErrors() { - diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError))) - view.Diagnostics(diags) - return 1 - } - - // Now, we can show any errors from initializing the backend, but we won't - // show the InitConfigError preamble as we didn't detect problems with - // the early configuration. - if backDiags.HasErrors() { - view.Diagnostics(diags) - return 1 - } - - // If everything is ok with the core version check and backend initialization, - // show other errors from loading the full configuration tree. - diags = diags.Append(confDiags) - if confDiags.HasErrors() { - diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError))) - view.Diagnostics(diags) - return 1 - } - if cb, ok := back.(*cloud.Cloud); ok { if c.RunningInAutomation { if err := cb.AssertImportCompatible(config); err != nil { @@ -522,7 +521,6 @@ However, if you intended to override a defined backend, please verify that the backend configuration is present and valid. `, )) - } opts = &BackendOpts{ diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 03ead1ce0d..6f51613e66 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -4707,7 +4707,7 @@ func TestInit_stateStore_to_backend(t *testing.T) { } } -func TestInit_unitialized_stateStore(t *testing.T) { +func TestInit_uninitialized_stateStore(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() cfg := `terraform { @@ -5641,6 +5641,65 @@ func TestInit_cloud_to_stateStore(t *testing.T) { } } +// Test that config-parsing errors that prevent initialising the pluggable state store are identified and returned +// before Terraform attempts to initialise the store. +// +// These errors include omitting the necessary entry in required_providers, or causing an issue with how require_providers +// is parsed. This test uses the first scenario for simplicity. +func TestInit_configErrorsImpactingStateStore(t *testing.T) { + td := t.TempDir() + t.Chdir(td) + cfg1 := `terraform { + required_providers { + foobar = { + source = "hashicorp/foobar" + } + } + state_store "test_store" { + provider "test" {} # missing from required_providers + value = "foobar" + } +} + ` + if err := os.WriteFile(filepath.Join(td, "main.tf"), []byte(cfg1), 0644); err != nil { + t.Fatalf("err: %s", err) + } + + ui := cli.NewMockUi() + view, done := testView(t) + initCmd := &InitCommand{ + Meta: Meta{ + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + }, + } + + log.Printf("[TRACE] TestInit_configErrorsImpactingStateStore: init start") + args := []string{"-enable-pluggable-state-storage-experiment"} + code := initCmd.Run(args) + testOutput := done(t) + if code != 1 { + t.Fatalf("expected apply to fail with code 1, got code %d: \n%s", code, testOutput.All()) + } + log.Printf("[TRACE] TestInit_configErrorsImpactingStateStore: init complete") + t.Logf("init output:\n%s", testOutput.Stdout()) + t.Logf("init errors:\n%s", testOutput.Stderr()) + + expectedErrs := []string{ + // Pre-amble text that's shown when a config-parsing error occurs during init. + "Error: Terraform encountered problems during initialisation, including problems with the configuration, described below.", + // This parsing error previously wouldn't be reported before initialising the backend, so + // Terraform attempted to use a state store in the missing provider. + "Error: Missing entry in required_providers", + } + for _, e := range expectedErrs { + if !strings.Contains(cleanString(testOutput.Stderr()), e) { + t.Fatalf("unexpected error, expected %q, given: %s", e, testOutput.Stderr()) + } + } +} + // newMockProviderSource is a helper to succinctly construct a mock provider // source that contains a set of packages matching the given provider versions // that are available for installation (from temporary local files). diff --git a/internal/configs/state_store.go b/internal/configs/state_store.go index d8bb237a19..1f548d204a 100644 --- a/internal/configs/state_store.go +++ b/internal/configs/state_store.go @@ -120,14 +120,16 @@ func resolveStateStoreProviderType(requiredProviders map[string]*RequiredProvide // that the builtin provider is intended. return addrs.NewBuiltInProvider("terraform"), nil case !foundReqProviderEntry: - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Missing entry in required_providers", - Detail: fmt.Sprintf("The provider used for state storage must have a matching entry in required_providers. Please add an entry for provider %s", - stateStore.Provider.Name, - ), - Subject: &stateStore.DeclRange, - }) + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing entry in required_providers", + Detail: fmt.Sprintf("The provider used for state storage must have a matching entry in required_providers. Please add an entry for provider %s", + stateStore.Provider.Name, + ), + Subject: &stateStore.DeclRange, + }, + ) return tfaddr.Provider{}, diags default: // We've got a required_providers entry to use