From 625e76867883e41b7bdac50a56952dc3d09f94f7 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 28 Sep 2021 12:38:40 -0400 Subject: [PATCH] make sure required_version is checked before diags We must ensure that the terraform required_version is checked as early as possible, so that new configuration constructs don't cause init to fail without indicating the version is incompatible. The loadConfig call before the earlyconfig parsing seems to be unneeded, and we can delay that to de-tangle it from installing the modules which may have their own constraints. TODO: it seems that loadConfig should be able to handle returning the version constraints in the same manner as loadSingleModule. --- internal/command/init.go | 62 ++++++++++++------- internal/command/init_test.go | 27 ++++++++ .../init-check-required-version-first/main.tf | 17 +++++ 3 files changed, 82 insertions(+), 24 deletions(-) create mode 100644 internal/command/testdata/init-check-required-version-first/main.tf diff --git a/internal/command/init.go b/internal/command/init.go index 96a383159f04..02f555ee0a8e 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 } @@ -189,23 +182,44 @@ func (c *InitCommand) Run(args []string) int { } } + // Using loadSingleModule will allow us to get the sniffed required_version + // before trying to build the complete config. + rootMod, _ := c.loadSingleModule(path) + // We can ignore the error, since we are going to reload the full config + // again below once we know the root module constraints are valid. + if rootMod != nil { + rootCfg := &configs.Config{ + Module: rootMod, + } + // If our module version constraints are not valid, then there is no + // need to continue processing. + versionDiags := terraform.CheckCoreVersionRequirements(rootCfg) + if versionDiags.HasErrors() { + c.showDiagnostics(versionDiags) + return 1 + } + } + // 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 + // Check again 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 +227,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.Root.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..2bf1e0e26481 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -1608,6 +1608,33 @@ 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) { + 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) + } +} + func TestInit_providerLockFile(t *testing.T) { // Create a temporary working directory that is empty td := tempDir(t) 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 {} +}