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)

pull/38146/head
Sarah French 3 months ago committed by GitHub
parent 27770ee805
commit b8c2cabee8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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{

@ -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).

@ -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

Loading…
Cancel
Save