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.
This commit is contained in:
James Bardin 2021-09-28 12:38:40 -04:00
parent 372814e49a
commit 625e768678
3 changed files with 83 additions and 25 deletions

View File

@ -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,9 +182,41 @@ 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)
// 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.
// 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)
if versionDiags.HasErrors() {
c.showDiagnostics(versionDiags)
return 1
}
diags = diags.Append(confDiags)
if confDiags.HasErrors() {
c.Ui.Error(strings.TrimSpace(errInitConfigError))
@ -199,21 +224,10 @@ func (c *InitCommand) Run(args []string) int {
return 1
}
// 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(diags)
return 1
}
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)

View File

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

View File

@ -0,0 +1,17 @@
terraform {
required_version = ">200.0.0"
bad {
block = "false"
}
required_providers {
bang = {
oops = "boom"
}
}
}
nope {
boom {}
}