diff --git a/internal/command/init.go b/internal/command/init.go index 96a383159f04..551a4d0b8a5a 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -150,19 +150,7 @@ func (c *InitCommand) Run(args []string) int { // initialization functionality remains built around "earlyconfig" and // so we need to still load the module via that mechanism anyway until we // can do some more invasive refactoring here. - rootMod, confDiags := c.loadSingleModule(path) rootModEarly, earlyConfDiags := c.loadSingleModuleEarly(path) - if confDiags.HasErrors() { - c.Ui.Error(c.Colorize().Color(strings.TrimSpace(errInitConfigError))) - // TODO: It would be nice to check the version constraints in - // rootModEarly.RequiredCore and print out a hint if the module is - // declaring that it's not compatible with this version of Terraform, - // though we're deferring that for now because we're intending to - // refactor our use of "earlyconfig" here anyway and so whatever we - // might do here right now would likely be invalidated by that. - c.showDiagnostics(confDiags) - return 1 - } // If _only_ the early loader encountered errors then that's unusual // (it should generally be a superset of the normal loader) but we'll // return those errors anyway since otherwise we'll probably get @@ -172,7 +160,12 @@ func (c *InitCommand) Run(args []string) int { c.Ui.Error(c.Colorize().Color(strings.TrimSpace(errInitConfigError))) // Errors from the early loader are generally not as high-quality since // it has less context to work with. - diags = diags.Append(confDiags) + + // TODO: It would be nice to check the version constraints in + // rootModEarly.RequiredCore and print out a hint if the module is + // declaring that it's not compatible with this version of Terraform, + // and that may be what caused earlyconfig to fail. + diags = diags.Append(earlyConfDiags) c.showDiagnostics(diags) return 1 } @@ -192,20 +185,23 @@ func (c *InitCommand) Run(args []string) int { // With all of the modules (hopefully) installed, we can now try to load the // whole configuration tree. config, confDiags := c.loadConfig(path) - diags = diags.Append(confDiags) - if confDiags.HasErrors() { - c.Ui.Error(strings.TrimSpace(errInitConfigError)) - c.showDiagnostics(diags) - return 1 - } + // 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. - // 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 version, so - // we can produce a version-related error message rather than + // 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 + // version, so we can produce a version-related error message rather than // potentially-confusing downstream errors. versionDiags := terraform.CheckCoreVersionRequirements(config) - diags = diags.Append(versionDiags) if versionDiags.HasErrors() { + c.showDiagnostics(versionDiags) + return 1 + } + + diags = diags.Append(confDiags) + if confDiags.HasErrors() { + c.Ui.Error(strings.TrimSpace(errInitConfigError)) c.showDiagnostics(diags) return 1 } @@ -213,7 +209,7 @@ func (c *InitCommand) Run(args []string) int { var back backend.Backend if flagBackend { - be, backendOutput, backendDiags := c.initBackend(rootMod, flagConfigExtra) + be, backendOutput, backendDiags := c.initBackend(config.Module, flagConfigExtra) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) diff --git a/internal/command/init_test.go b/internal/command/init_test.go index b75da1f4f899..2d96b27b4403 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -1608,6 +1608,59 @@ func TestInit_checkRequiredVersion(t *testing.T) { } } +// Verify that init will error out with an invalid version constraint, even if +// there are other invalid configuration constructs. +func TestInit_checkRequiredVersionFirst(t *testing.T) { + t.Run("root_module", func(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("init-check-required-version-first"), td) + defer testChdir(t, td)() + + ui := cli.NewMockUi() + view, _ := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{} + if code := c.Run(args); code != 1 { + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + } + errStr := ui.ErrorWriter.String() + if !strings.Contains(errStr, `Unsupported Terraform Core version`) { + t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) + } + }) + t.Run("sub_module", func(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("init-check-required-version-first-module"), td) + defer testChdir(t, td)() + + ui := cli.NewMockUi() + view, _ := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{} + if code := c.Run(args); code != 1 { + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + } + errStr := ui.ErrorWriter.String() + if !strings.Contains(errStr, `Unsupported Terraform Core version`) { + t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) + } + }) +} + func TestInit_providerLockFile(t *testing.T) { // Create a temporary working directory that is empty td := tempDir(t) diff --git a/internal/command/show_test.go b/internal/command/show_test.go index b70ce14c1cb4..ea266d2cb32c 100644 --- a/internal/command/show_test.go +++ b/internal/command/show_test.go @@ -103,8 +103,6 @@ func TestShow_aliasedProvider(t *testing.T) { }, } - fmt.Println(os.Getwd()) - // the statefile created by testStateFile is named state.tfstate args := []string{"state.tfstate"} if code := c.Run(args); code != 0 { diff --git a/internal/command/testdata/init-check-required-version-first-module/main.tf b/internal/command/testdata/init-check-required-version-first-module/main.tf new file mode 100644 index 000000000000..ba846846994e --- /dev/null +++ b/internal/command/testdata/init-check-required-version-first-module/main.tf @@ -0,0 +1,3 @@ +module "mod" { + source = "./mod" +} diff --git a/internal/command/testdata/init-check-required-version-first-module/mod/main.tf b/internal/command/testdata/init-check-required-version-first-module/mod/main.tf new file mode 100644 index 000000000000..ab311d066953 --- /dev/null +++ b/internal/command/testdata/init-check-required-version-first-module/mod/main.tf @@ -0,0 +1,17 @@ +terraform { + required_version = ">200.0.0" + + bad { + block = "false" + } + + required_providers { + bang = { + oops = "boom" + } + } +} + +nope { + boom {} +} diff --git a/internal/command/testdata/init-check-required-version-first/main.tf b/internal/command/testdata/init-check-required-version-first/main.tf new file mode 100644 index 000000000000..ab311d066953 --- /dev/null +++ b/internal/command/testdata/init-check-required-version-first/main.tf @@ -0,0 +1,17 @@ +terraform { + required_version = ">200.0.0" + + bad { + block = "false" + } + + required_providers { + bang = { + oops = "boom" + } + } +} + +nope { + boom {} +} diff --git a/internal/configs/configload/loader_load.go b/internal/configs/configload/loader_load.go index 323001de1028..9ae440274035 100644 --- a/internal/configs/configload/loader_load.go +++ b/internal/configs/configload/loader_load.go @@ -21,7 +21,13 @@ import ( func (l *Loader) LoadConfig(rootDir string) (*configs.Config, hcl.Diagnostics) { rootMod, diags := l.parser.LoadConfigDir(rootDir) if rootMod == nil || diags.HasErrors() { - return nil, diags + // Ensure we return any parsed modules here so that required_version + // constraints can be verified even when encountering errors. + cfg := &configs.Config{ + Module: rootMod, + } + + return cfg, diags } cfg, cDiags := configs.BuildConfig(rootMod, configs.ModuleWalkerFunc(l.moduleWalkerLoad)) diff --git a/internal/configs/configload/loader_load_test.go b/internal/configs/configload/loader_load_test.go index 82d8db0cd4fe..ab8dd5dee630 100644 --- a/internal/configs/configload/loader_load_test.go +++ b/internal/configs/configload/loader_load_test.go @@ -91,8 +91,16 @@ func TestLoaderLoadConfig_loadDiags(t *testing.T) { t.Fatalf("unexpected error from NewLoader: %s", err) } - _, diags := loader.LoadConfig(fixtureDir) + cfg, diags := loader.LoadConfig(fixtureDir) if !diags.HasErrors() { - t.Fatalf("success; want error") + t.Fatal("success; want error") + } + + if cfg == nil { + t.Fatal("partial config not returned with diagnostics") + } + + if cfg.Module == nil { + t.Fatal("expected config module") } }