Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check required_version as early as possible #29665

Merged
merged 4 commits into from
Sep 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 20 additions & 24 deletions internal/command/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -192,28 +185,31 @@ 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
jbardin marked this conversation as resolved.
Show resolved Hide resolved
// 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
}

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)
Expand Down
53 changes: 53 additions & 0 deletions internal/command/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 0 additions & 2 deletions internal/command/show_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module "mod" {
source = "./mod"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
terraform {
required_version = ">200.0.0"

bad {
block = "false"
}

required_providers {
bang = {
oops = "boom"
}
}
}

nope {
boom {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
terraform {
required_version = ">200.0.0"

bad {
block = "false"
}

required_providers {
bang = {
oops = "boom"
}
}
}

nope {
boom {}
}
8 changes: 7 additions & 1 deletion internal/configs/configload/loader_load.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
12 changes: 10 additions & 2 deletions internal/configs/configload/loader_load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}