diff --git a/internal/checks/state_test.go b/internal/checks/state_test.go index 8c0f0d447f..03168daebf 100644 --- a/internal/checks/state_test.go +++ b/internal/checks/state_test.go @@ -14,7 +14,7 @@ func TestChecksHappyPath(t *testing.T) { const fixtureDir = "testdata/happypath" loader, close := configload.NewLoaderForTests(t) defer close() - inst := initwd.NewModuleInstaller(loader.ModulesDir(), nil) + inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, nil) _, instDiags := inst.InstallModules(context.Background(), fixtureDir, true, initwd.ModuleInstallHooksImpl{}) if instDiags.HasErrors() { t.Fatal(instDiags.Err()) diff --git a/internal/command/command_test.go b/internal/command/command_test.go index 7998a55422..4da1b9b342 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -154,7 +154,7 @@ func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *config // Test modules usually do not refer to remote sources, and for local // sources only this ultimately just records all of the module paths // in a JSON file so that we can load them below. - inst := initwd.NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) + inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil)) _, instDiags := inst.InstallModules(context.Background(), dir, true, initwd.ModuleInstallHooksImpl{}) if instDiags.HasErrors() { t.Fatal(instDiags.Err()) diff --git a/internal/command/meta_config.go b/internal/command/meta_config.go index 349a791365..fba00cddc5 100644 --- a/internal/command/meta_config.go +++ b/internal/command/meta_config.go @@ -152,7 +152,13 @@ func (m *Meta) installModules(rootDir string, upgrade bool, hooks initwd.ModuleI return true, diags } - inst := m.moduleInstaller() + loader, err := m.initConfigLoader() + if err != nil { + diags = diags.Append(err) + return true, diags + } + + inst := initwd.NewModuleInstaller(m.modulesDir(), loader, m.registryClient()) // Installation can be aborted by interruption signals ctx, done := m.InterruptibleContext() @@ -184,8 +190,14 @@ func (m *Meta) initDirFromModule(targetDir string, addr string, hooks initwd.Mod ctx, done := m.InterruptibleContext() defer done() + loader, err := m.initConfigLoader() + if err != nil { + diags = diags.Append(err) + return true, diags + } + targetDir = m.normalizePath(targetDir) - moreDiags := initwd.DirFromModule(ctx, targetDir, m.modulesDir(), addr, m.registryClient(), hooks) + moreDiags := initwd.DirFromModule(ctx, loader, targetDir, m.modulesDir(), addr, m.registryClient(), hooks) diags = diags.Append(moreDiags) if ctx.Err() == context.Canceled { m.showDiagnostics(diags) @@ -316,13 +328,6 @@ func (m *Meta) initConfigLoader() (*configload.Loader, error) { return m.configLoader, nil } -// moduleInstaller instantiates and returns a module installer for use by -// "terraform init" (directly or indirectly). -func (m *Meta) moduleInstaller() *initwd.ModuleInstaller { - reg := m.registryClient() - return initwd.NewModuleInstaller(m.modulesDir(), reg) -} - // registryClient instantiates and returns a new Terraform Registry client. func (m *Meta) registryClient() *registry.Client { return registry.NewClient(m.Services, nil) diff --git a/internal/command/test.go b/internal/command/test.go index 1f18689f1b..ec4a4c4f3e 100644 --- a/internal/command/test.go +++ b/internal/command/test.go @@ -248,7 +248,12 @@ func (c *TestCommand) prepareSuiteDir(ctx context.Context, suiteName string) (te suiteDirs.ModulesDir = filepath.Join(configDir, ".terraform", "modules") os.MkdirAll(suiteDirs.ModulesDir, 0755) // if this fails then we'll ignore it and let InstallModules below fail instead reg := c.registryClient() - moduleInst := initwd.NewModuleInstaller(suiteDirs.ModulesDir, reg) + loader, err := c.initConfigLoader() + if err != nil { + diags = diags.Append(err) + return suiteDirs, diags + } + moduleInst := initwd.NewModuleInstaller(suiteDirs.ModulesDir, loader, reg) _, moreDiags := moduleInst.InstallModules(ctx, configDir, true, nil) diags = diags.Append(moreDiags) if diags.HasErrors() { @@ -260,7 +265,7 @@ func (c *TestCommand) prepareSuiteDir(ctx context.Context, suiteName string) (te // with a separate config loader because the Meta.configLoader instance // is intended for interacting with the current working directory, not // with the test suite subdirectories. - loader, err := configload.NewLoader(&configload.Config{ + loader, err = configload.NewLoader(&configload.Config{ ModulesDir: suiteDirs.ModulesDir, Services: c.Services, }) diff --git a/internal/configs/config.go b/internal/configs/config.go index f38d3cd85d..6b7a03a61a 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -555,3 +555,16 @@ func (c *Config) ProviderForConfigAddr(addr addrs.LocalProviderConfig) addrs.Pro } return c.ResolveAbsProviderAddr(addr, addrs.RootModule).Provider } + +func (c *Config) CheckCoreVersionRequirements() hcl.Diagnostics { + var diags hcl.Diagnostics + + diags = diags.Extend(c.Module.CheckCoreVersionRequirements(c.Path, c.SourceAddr)) + + for _, c := range c.Children { + childDiags := c.CheckCoreVersionRequirements() + diags = diags.Extend(childDiags) + } + + return diags +} diff --git a/internal/configs/module.go b/internal/configs/module.go index c2088b9fde..6ecae9c290 100644 --- a/internal/configs/module.go +++ b/internal/configs/module.go @@ -7,6 +7,8 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/experiments" + + tfversion "github.com/hashicorp/terraform/version" ) // Module is a container for a set of configuration constructs that are @@ -589,3 +591,59 @@ func (m *Module) ImpliedProviderForUnqualifiedType(pType string) addrs.Provider } return addrs.ImpliedProviderForUnqualifiedType(pType) } + +func (m *Module) CheckCoreVersionRequirements(path addrs.Module, sourceAddr addrs.ModuleSource) hcl.Diagnostics { + var diags hcl.Diagnostics + + for _, constraint := range m.CoreVersionConstraints { + // Before checking if the constraints are met, check that we are not using any prerelease fields as these + // are not currently supported. + var prereleaseDiags hcl.Diagnostics + for _, required := range constraint.Required { + if required.Prerelease() { + prereleaseDiags = prereleaseDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid required_version constraint", + Detail: fmt.Sprintf( + "Prerelease version constraints are not supported: %s. Remove the prerelease information from the constraint. Prerelease versions of terraform will match constraints using their version core only.", + required.String()), + Subject: constraint.DeclRange.Ptr(), + }) + } + } + + if len(prereleaseDiags) > 0 { + // There were some prerelease fields in the constraints. Don't check the constraints as they will + // fail, and populate the diagnostics for these constraints with the prerelease diagnostics. + diags = diags.Extend(prereleaseDiags) + continue + } + + if !constraint.Required.Check(tfversion.SemVer) { + switch { + case len(path) == 0: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported Terraform Core version", + Detail: fmt.Sprintf( + "This configuration does not support Terraform version %s. To proceed, either choose another supported Terraform version or update this version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected behavior.", + tfversion.String(), + ), + Subject: constraint.DeclRange.Ptr(), + }) + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported Terraform Core version", + Detail: fmt.Sprintf( + "Module %s (from %s) does not support Terraform version %s. To proceed, either choose another supported Terraform version or update this version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected behavior.", + path, sourceAddr, tfversion.String(), + ), + Subject: constraint.DeclRange.Ptr(), + }) + } + } + } + + return diags +} diff --git a/internal/initwd/from_module.go b/internal/initwd/from_module.go index 38a4e49b5c..806e505e61 100644 --- a/internal/initwd/from_module.go +++ b/internal/initwd/from_module.go @@ -10,12 +10,13 @@ import ( "sort" "strings" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/configload" "github.com/hashicorp/terraform/internal/copy" - "github.com/hashicorp/terraform/internal/earlyconfig" "github.com/hashicorp/terraform/internal/getmodules" version "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform-config-inspect/tfconfig" "github.com/hashicorp/terraform/internal/modsdir" "github.com/hashicorp/terraform/internal/registry" "github.com/hashicorp/terraform/internal/tfdiags" @@ -42,7 +43,7 @@ const initFromModuleRootKeyPrefix = initFromModuleRootCallName + "." // references using ../ from that module to be unresolvable. Error diagnostics // are produced in that case, to prompt the user to rewrite the source strings // to be absolute references to the original remote module. -func DirFromModule(ctx context.Context, rootDir, modulesDir, sourceAddr string, reg *registry.Client, hooks ModuleInstallHooks) tfdiags.Diagnostics { +func DirFromModule(ctx context.Context, loader *configload.Loader, rootDir, modulesDir, sourceAddrStr string, reg *registry.Client, hooks ModuleInstallHooks) tfdiags.Diagnostics { var diags tfdiags.Diagnostics // The way this function works is pretty ugly, but we accept it because @@ -87,8 +88,8 @@ func DirFromModule(ctx context.Context, rootDir, modulesDir, sourceAddr string, } instDir := filepath.Join(rootDir, ".terraform/init-from-module") - inst := NewModuleInstaller(instDir, reg) - log.Printf("[DEBUG] installing modules in %s to initialize working directory from %q", instDir, sourceAddr) + inst := NewModuleInstaller(instDir, loader, reg) + log.Printf("[DEBUG] installing modules in %s to initialize working directory from %q", instDir, sourceAddrStr) os.RemoveAll(instDir) // if this fails then we'll fail on MkdirAll below too err := os.MkdirAll(instDir, os.ModePerm) if err != nil { @@ -103,12 +104,6 @@ func DirFromModule(ctx context.Context, rootDir, modulesDir, sourceAddr string, instManifest := make(modsdir.Manifest) retManifest := make(modsdir.Manifest) - fakeFilename := fmt.Sprintf("-from-module=%q", sourceAddr) - fakePos := tfconfig.SourcePos{ - Filename: fakeFilename, - Line: 1, - } - // -from-module allows relative paths but it's different than a normal // module address where it'd be resolved relative to the module call // (which is synthetic, here.) To address this, we'll just patch up any @@ -117,25 +112,33 @@ func DirFromModule(ctx context.Context, rootDir, modulesDir, sourceAddr string, // that the result will be "downloaded" with go-getter (copied from the // source location), rather than just recorded as a relative path. { - maybePath := filepath.ToSlash(sourceAddr) + maybePath := filepath.ToSlash(sourceAddrStr) if maybePath == "." || strings.HasPrefix(maybePath, "./") || strings.HasPrefix(maybePath, "../") { if wd, err := os.Getwd(); err == nil { - sourceAddr = filepath.Join(wd, sourceAddr) - log.Printf("[TRACE] -from-module relative path rewritten to absolute path %s", sourceAddr) + sourceAddrStr = filepath.Join(wd, sourceAddrStr) + log.Printf("[TRACE] -from-module relative path rewritten to absolute path %s", sourceAddrStr) } } } // Now we need to create an artificial root module that will seed our // installation process. - fakeRootModule := &tfconfig.Module{ - ModuleCalls: map[string]*tfconfig.ModuleCall{ + sourceAddr, err := addrs.ParseModuleSource(sourceAddrStr) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid module source address", + fmt.Sprintf("Failed to parse module source address: %s", err), + )) + } + fakeRootModule := &configs.Module{ + ModuleCalls: map[string]*configs.ModuleCall{ initFromModuleRootCallName: { - Name: initFromModuleRootCallName, - Source: sourceAddr, - Pos: fakePos, + Name: initFromModuleRootCallName, + SourceAddr: sourceAddr, }, }, + ProviderRequirements: &configs.RequiredProviders{}, } // wrapHooks filters hook notifications to only include Download calls @@ -181,7 +184,7 @@ func DirFromModule(ctx context.Context, rootDir, modulesDir, sourceAddr string, diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Failed to copy root module", - fmt.Sprintf("Error copying root module %q from %s to %s: %s.", sourceAddr, record.Dir, rootDir, err), + fmt.Sprintf("Error copying root module %q from %s to %s: %s.", sourceAddrStr, record.Dir, rootDir, err), )) continue } @@ -191,12 +194,12 @@ func DirFromModule(ctx context.Context, rootDir, modulesDir, sourceAddr string, // and must thus be rewritten to be absolute addresses again. // For now we can't do this rewriting automatically, but we'll // generate an error to help the user do it manually. - mod, _ := earlyconfig.LoadModule(rootDir) // ignore diagnostics since we're just doing value-add here anyway + mod, _ := loader.Parser().LoadConfigDir(rootDir) // ignore diagnostics since we're just doing value-add here anyway if mod != nil { for _, mc := range mod.ModuleCalls { - if pathTraversesUp(mc.Source) { - packageAddr, givenSubdir := getmodules.SplitPackageSubdir(sourceAddr) - newSubdir := filepath.Join(givenSubdir, mc.Source) + if pathTraversesUp(mc.SourceAddrRaw) { + packageAddr, givenSubdir := getmodules.SplitPackageSubdir(sourceAddrStr) + newSubdir := filepath.Join(givenSubdir, mc.SourceAddrRaw) if pathTraversesUp(newSubdir) { // This should never happen in any reasonable // configuration since this suggests a path that @@ -214,7 +217,7 @@ func DirFromModule(ctx context.Context, rootDir, modulesDir, sourceAddr string, diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Root module references parent directory", - fmt.Sprintf("The requested module %q refers to a module via its parent directory. To use this as a new root module this source string must be rewritten as a remote source address, such as %q.", sourceAddr, newAddr), + fmt.Sprintf("The requested module %q refers to a module via its parent directory. To use this as a new root module this source string must be rewritten as a remote source address, such as %q.", sourceAddrStr, newAddr), )) continue } diff --git a/internal/initwd/from_module_test.go b/internal/initwd/from_module_test.go index 9714ed3465..dd8ca95d17 100644 --- a/internal/initwd/from_module_test.go +++ b/internal/initwd/from_module_test.go @@ -38,7 +38,9 @@ func TestDirFromModule_registry(t *testing.T) { hooks := &testInstallHooks{} reg := registry.NewClient(nil, nil) - diags := DirFromModule(context.Background(), dir, modsDir, "hashicorp/module-installer-acctest/aws//examples/main", reg, hooks) + loader, cleanup := configload.NewLoaderForTests(t) + defer cleanup() + diags := DirFromModule(context.Background(), loader, dir, modsDir, "hashicorp/module-installer-acctest/aws//examples/main", reg, hooks) assertNoDiagnostics(t, diags) v := version.Must(version.NewVersion("0.0.2")) @@ -93,7 +95,7 @@ func TestDirFromModule_registry(t *testing.T) { t.Fatalf("wrong installer calls\n%s", diff) } - loader, err := configload.NewLoader(&configload.Config{ + loader, err = configload.NewLoader(&configload.Config{ ModulesDir: modsDir, }) if err != nil { @@ -154,7 +156,9 @@ func TestDirFromModule_submodules(t *testing.T) { } modInstallDir := filepath.Join(dir, ".terraform/modules") - diags := DirFromModule(context.Background(), dir, modInstallDir, fromModuleDir, nil, hooks) + loader, cleanup := configload.NewLoaderForTests(t) + defer cleanup() + diags := DirFromModule(context.Background(), loader, dir, modInstallDir, fromModuleDir, nil, hooks) assertNoDiagnostics(t, diags) wantCalls := []testInstallHookCall{ { @@ -173,7 +177,7 @@ func TestDirFromModule_submodules(t *testing.T) { return } - loader, err := configload.NewLoader(&configload.Config{ + loader, err = configload.NewLoader(&configload.Config{ ModulesDir: modInstallDir, }) if err != nil { @@ -246,7 +250,9 @@ func TestDirFromModule_rel_submodules(t *testing.T) { modInstallDir := ".terraform/modules" sourceDir := "../local-modules" - diags := DirFromModule(context.Background(), ".", modInstallDir, sourceDir, nil, hooks) + loader, cleanup := configload.NewLoaderForTests(t) + defer cleanup() + diags := DirFromModule(context.Background(), loader, ".", modInstallDir, sourceDir, nil, hooks) assertNoDiagnostics(t, diags) wantCalls := []testInstallHookCall{ { @@ -265,7 +271,7 @@ func TestDirFromModule_rel_submodules(t *testing.T) { return } - loader, err := configload.NewLoader(&configload.Config{ + loader, err = configload.NewLoader(&configload.Config{ ModulesDir: modInstallDir, }) if err != nil { diff --git a/internal/initwd/load_config.go b/internal/initwd/load_config.go deleted file mode 100644 index 6dc032ba17..0000000000 --- a/internal/initwd/load_config.go +++ /dev/null @@ -1,56 +0,0 @@ -package initwd - -import ( - "fmt" - - version "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform-config-inspect/tfconfig" - "github.com/hashicorp/terraform/internal/earlyconfig" - "github.com/hashicorp/terraform/internal/modsdir" - "github.com/hashicorp/terraform/internal/tfdiags" -) - -// LoadConfig loads a full configuration tree that has previously had all of -// its dependent modules installed to the given modulesDir using a -// ModuleInstaller. -// -// This uses the early configuration loader and thus only reads top-level -// metadata from the modules in the configuration. Most callers should use -// the configs/configload package to fully load a configuration. -func LoadConfig(rootDir, modulesDir string) (*earlyconfig.Config, tfdiags.Diagnostics) { - rootMod, diags := earlyconfig.LoadModule(rootDir) - if rootMod == nil { - return nil, diags - } - - manifest, err := modsdir.ReadManifestSnapshotForDir(modulesDir) - if err != nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Failed to read module manifest", - fmt.Sprintf("Terraform failed to read its manifest of locally-cached modules: %s.", err), - )) - return nil, diags - } - - return earlyconfig.BuildConfig(rootMod, earlyconfig.ModuleWalkerFunc( - func(req *earlyconfig.ModuleRequest) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - - key := manifest.ModuleKey(req.Path) - record, exists := manifest[key] - if !exists { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Module not installed", - fmt.Sprintf("Module %s is not yet installed. Run \"terraform init\" to install all modules required by this configuration.", req.Path.String()), - )) - return nil, nil, diags - } - - mod, mDiags := earlyconfig.LoadModule(record.Dir) - diags = diags.Append(mDiags) - return mod, record.Version, diags - }, - )) -} diff --git a/internal/initwd/module_install.go b/internal/initwd/module_install.go index 2a1f7b685f..ba2b465c8f 100644 --- a/internal/initwd/module_install.go +++ b/internal/initwd/module_install.go @@ -12,10 +12,11 @@ import ( "github.com/apparentlymart/go-versions/versions" version "github.com/hashicorp/go-version" + "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/terraform-config-inspect/tfconfig" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/earlyconfig" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/configload" "github.com/hashicorp/terraform/internal/getmodules" "github.com/hashicorp/terraform/internal/modsdir" "github.com/hashicorp/terraform/internal/registry" @@ -26,6 +27,7 @@ import ( type ModuleInstaller struct { modsDir string + loader *configload.Loader reg *registry.Client // The keys in moduleVersions are resolved and trimmed registry source @@ -42,9 +44,10 @@ type moduleVersion struct { version string } -func NewModuleInstaller(modsDir string, reg *registry.Client) *ModuleInstaller { +func NewModuleInstaller(modsDir string, loader *configload.Loader, reg *registry.Client) *ModuleInstaller { return &ModuleInstaller{ modsDir: modsDir, + loader: loader, reg: reg, registryPackageVersions: make(map[addrs.ModuleRegistryPackage]*response.ModuleVersions), registryPackageSources: make(map[moduleVersion]addrs.ModuleSourceRemote), @@ -79,12 +82,23 @@ func NewModuleInstaller(modsDir string, reg *registry.Client) *ModuleInstaller { // If successful (the returned diagnostics contains no errors) then the // first return value is the early configuration tree that was constructed by // the installation process. -func (i *ModuleInstaller) InstallModules(ctx context.Context, rootDir string, upgrade bool, hooks ModuleInstallHooks) (*earlyconfig.Config, tfdiags.Diagnostics) { +func (i *ModuleInstaller) InstallModules(ctx context.Context, rootDir string, upgrade bool, hooks ModuleInstallHooks) (*configs.Config, tfdiags.Diagnostics) { log.Printf("[TRACE] ModuleInstaller: installing child modules for %s into %s", rootDir, i.modsDir) + var diags tfdiags.Diagnostics - rootMod, diags := earlyconfig.LoadModule(rootDir) + rootMod, mDiags := i.loader.Parser().LoadConfigDir(rootDir) if rootMod == nil { + // We drop the diagnostics here because we only want to report module + // loading errors after checking the core version constraints, which we + // can only do if the module can be at least partially loaded. return nil, diags + } else if vDiags := rootMod.CheckCoreVersionRequirements(nil, nil); vDiags.HasErrors() { + // If the core version requirements are not met, we drop any other + // diagnostics, as they may reflect language changes from future + // Terraform versions. + diags = diags.Append(vDiags) + } else { + diags = diags.Append(mDiags) } manifest, err := modsdir.ReadManifestSnapshotForDir(i.modsDir) @@ -104,7 +118,7 @@ func (i *ModuleInstaller) InstallModules(ctx context.Context, rootDir string, up return cfg, diags } -func (i *ModuleInstaller) installDescendentModules(ctx context.Context, rootMod *tfconfig.Module, rootDir string, manifest modsdir.Manifest, upgrade bool, hooks ModuleInstallHooks, fetcher *getmodules.PackageFetcher) (*earlyconfig.Config, tfdiags.Diagnostics) { +func (i *ModuleInstaller) installDescendentModules(ctx context.Context, rootMod *configs.Module, rootDir string, manifest modsdir.Manifest, upgrade bool, hooks ModuleInstallHooks, fetcher *getmodules.PackageFetcher) (*configs.Config, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics if hooks == nil { @@ -119,8 +133,25 @@ func (i *ModuleInstaller) installDescendentModules(ctx context.Context, rootMod Dir: rootDir, } - cfg, cDiags := earlyconfig.BuildConfig(rootMod, earlyconfig.ModuleWalkerFunc( - func(req *earlyconfig.ModuleRequest) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) { + cfg, cDiags := configs.BuildConfig(rootMod, configs.ModuleWalkerFunc( + func(req *configs.ModuleRequest) (*configs.Module, *version.Version, hcl.Diagnostics) { + var diags hcl.Diagnostics + + if req.SourceAddr == nil { + // If the parent module failed to parse the module source + // address, we can't load it here. Return nothing as the parent + // module's diagnostics should explain this. + return nil, nil, diags + } + + if req.Name == "" { + // An empty string for a module instance name breaks our + // manifest map, which uses that to indicate the root module. + // Because we descend into modules which have errors, we need + // to look out for this case, but the config loader's + // diagnostics will report the error later. + return nil, nil, diags + } key := manifest.ModuleKey(req.Path) instPath := i.packageInstallPath(req.Path) @@ -139,8 +170,8 @@ func (i *ModuleInstaller) installDescendentModules(ctx context.Context, rootMod case record.SourceAddr != req.SourceAddr.String(): log.Printf("[TRACE] ModuleInstaller: %s source address has changed from %q to %q", key, record.SourceAddr, req.SourceAddr) replace = true - case record.Version != nil && !req.VersionConstraints.Check(record.Version): - log.Printf("[TRACE] ModuleInstaller: %s version %s no longer compatible with constraints %s", key, record.Version, req.VersionConstraints) + case record.Version != nil && !req.VersionConstraint.Required.Check(record.Version): + log.Printf("[TRACE] ModuleInstaller: %s version %s no longer compatible with constraints %s", key, record.Version, req.VersionConstraint.Required) replace = true } } @@ -174,14 +205,14 @@ func (i *ModuleInstaller) installDescendentModules(ctx context.Context, rootMod err := os.RemoveAll(instPath) if err != nil && !os.IsNotExist(err) { log.Printf("[TRACE] ModuleInstaller: failed to remove %s: %s", key, err) - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Failed to remove local module cache", - fmt.Sprintf( + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to remove local module cache", + Detail: fmt.Sprintf( "Terraform tried to remove %s in order to reinstall this module, but encountered an error: %s", instPath, err, ), - )) + }) return nil, nil, diags } } else { @@ -190,8 +221,19 @@ func (i *ModuleInstaller) installDescendentModules(ctx context.Context, rootMod // keep our existing record. info, err := os.Stat(record.Dir) if err == nil && info.IsDir() { - mod, mDiags := earlyconfig.LoadModule(record.Dir) - diags = diags.Append(mDiags) + mod, mDiags := i.loader.Parser().LoadConfigDir(record.Dir) + if mod == nil { + // nil indicates an unreadable module, which should never happen, + // so we return the full loader diagnostics here. + diags = diags.Extend(mDiags) + } else if vDiags := mod.CheckCoreVersionRequirements(req.Path, req.SourceAddr); vDiags.HasErrors() { + // If the core version requirements are not met, we drop any other + // diagnostics, as they may reflect language changes from future + // Terraform versions. + diags = diags.Extend(vDiags) + } else { + diags = diags.Extend(mDiags) + } log.Printf("[TRACE] ModuleInstaller: Module installer: %s %s already installed in %s", key, record.Version, record.Dir) return mod, record.Version, diags @@ -231,7 +273,7 @@ func (i *ModuleInstaller) installDescendentModules(ctx context.Context, rootMod }, )) - diags = append(diags, cDiags...) + diags = diags.Append(cDiags) err := manifest.WriteSnapshotToDir(i.modsDir) if err != nil { @@ -245,8 +287,8 @@ func (i *ModuleInstaller) installDescendentModules(ctx context.Context, rootMod return cfg, diags } -func (i *ModuleInstaller) installLocalModule(req *earlyconfig.ModuleRequest, key string, manifest modsdir.Manifest, hooks ModuleInstallHooks) (*tfconfig.Module, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics +func (i *ModuleInstaller) installLocalModule(req *configs.ModuleRequest, key string, manifest modsdir.Manifest, hooks ModuleInstallHooks) (*configs.Module, hcl.Diagnostics) { + var diags hcl.Diagnostics parentKey := manifest.ModuleKey(req.Parent.Path) parentRecord, recorded := manifest[parentKey] @@ -255,12 +297,13 @@ func (i *ModuleInstaller) installLocalModule(req *earlyconfig.ModuleRequest, key panic(fmt.Errorf("missing manifest record for parent module %s", parentKey)) } - if len(req.VersionConstraints) != 0 { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Invalid version constraint", - fmt.Sprintf("Cannot apply a version constraint to module %q (at %s:%d) because it has a relative local path.", req.Name, req.CallPos.Filename, req.CallPos.Line), - )) + if len(req.VersionConstraint.Required) != 0 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid version constraint", + Detail: fmt.Sprintf("Cannot apply a version constraint to module %q (at %s:%d) because it has a relative local path.", req.Name, req.CallRange.Filename, req.CallRange.Start.Line), + Subject: req.CallRange.Ptr(), + }) } // For local sources we don't actually need to modify the @@ -272,25 +315,31 @@ func (i *ModuleInstaller) installLocalModule(req *earlyconfig.ModuleRequest, key // it is possible that the local directory is a symlink newDir, err := filepath.EvalSymlinks(newDir) if err != nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Unreadable module directory", - fmt.Sprintf("Unable to evaluate directory symlink: %s", err.Error()), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unreadable module directory", + Detail: fmt.Sprintf("Unable to evaluate directory symlink: %s", err.Error()), + }) } - mod, mDiags := earlyconfig.LoadModule(newDir) + // Finally we are ready to try actually loading the module. + mod, mDiags := i.loader.Parser().LoadConfigDir(newDir) if mod == nil { // nil indicates missing or unreadable directory, so we'll // discard the returned diags and return a more specific // error message here. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Unreadable module directory", - fmt.Sprintf("The directory %s could not be read for module %q at %s:%d.", newDir, req.Name, req.CallPos.Filename, req.CallPos.Line), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unreadable module directory", + Detail: fmt.Sprintf("The directory %s could not be read for module %q at %s:%d.", newDir, req.Name, req.CallRange.Filename, req.CallRange.Start.Line), + }) + } else if vDiags := mod.CheckCoreVersionRequirements(req.Path, req.SourceAddr); vDiags.HasErrors() { + // If the core version requirements are not met, we drop any other + // diagnostics, as they may reflect language changes from future + // Terraform versions. + diags = diags.Extend(vDiags) } else { - diags = diags.Append(mDiags) + diags = diags.Extend(mDiags) } // Note the local location in our manifest. @@ -305,8 +354,8 @@ func (i *ModuleInstaller) installLocalModule(req *earlyconfig.ModuleRequest, key return mod, diags } -func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyconfig.ModuleRequest, key string, instPath string, addr addrs.ModuleSourceRegistry, manifest modsdir.Manifest, hooks ModuleInstallHooks, fetcher *getmodules.PackageFetcher) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics +func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *configs.ModuleRequest, key string, instPath string, addr addrs.ModuleSourceRegistry, manifest modsdir.Manifest, hooks ModuleInstallHooks, fetcher *getmodules.PackageFetcher) (*configs.Module, *version.Version, hcl.Diagnostics) { + var diags hcl.Diagnostics hostname := addr.Package.Host reg := i.reg @@ -331,23 +380,25 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc resp, err = reg.ModuleVersions(ctx, regsrcAddr) if err != nil { if registry.IsModuleNotFound(err) { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Module not found", - fmt.Sprintf("Module %q (from %s:%d) cannot be found in the module registry at %s.", req.Name, req.CallPos.Filename, req.CallPos.Line, hostname), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Module not found", + Detail: fmt.Sprintf("Module %q (from %s:%d) cannot be found in the module registry at %s.", req.Name, req.CallRange.Filename, req.CallRange.Start.Line, hostname), + Subject: req.CallRange.Ptr(), + }) } else if errors.Is(err, context.Canceled) { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Module installation was interrupted", - fmt.Sprintf("Received interrupt signal while retrieving available versions for module %q.", req.Name), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Module installation was interrupted", + Detail: fmt.Sprintf("Received interrupt signal while retrieving available versions for module %q.", req.Name), + }) } else { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Error accessing remote module registry", - fmt.Sprintf("Failed to retrieve available versions for module %q (%s:%d) from %s: %s.", req.Name, req.CallPos.Filename, req.CallPos.Line, hostname, err), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Error accessing remote module registry", + Detail: fmt.Sprintf("Failed to retrieve available versions for module %q (%s:%d) from %s: %s.", req.Name, req.CallRange.Filename, req.CallRange.Start.Line, hostname, err), + Subject: req.CallRange.Ptr(), + }) } return nil, nil, diags } @@ -361,11 +412,12 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc if len(resp.Modules) < 1 { // Should never happen, but since this is a remote service that may // be implemented by third-parties we will handle it gracefully. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Invalid response from remote module registry", - fmt.Sprintf("The registry at %s returned an invalid response when Terraform requested available versions for module %q (%s:%d).", hostname, req.Name, req.CallPos.Filename, req.CallPos.Line), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid response from remote module registry", + Detail: fmt.Sprintf("The registry at %s returned an invalid response when Terraform requested available versions for module %q (%s:%d).", hostname, req.Name, req.CallRange.Filename, req.CallRange.Start.Line), + Subject: req.CallRange.Ptr(), + }) return nil, nil, diags } @@ -379,11 +431,12 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc // Should never happen if the registry server is compliant with // the protocol, but we'll warn if not to assist someone who // might be developing a module registry server. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Warning, - "Invalid response from remote module registry", - fmt.Sprintf("The registry at %s returned an invalid version string %q for module %q (%s:%d), which Terraform ignored.", hostname, mv.Version, req.Name, req.CallPos.Filename, req.CallPos.Line), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Invalid response from remote module registry", + Detail: fmt.Sprintf("The registry at %s returned an invalid version string %q for module %q (%s:%d), which Terraform ignored.", hostname, mv.Version, req.Name, req.CallRange.Filename, req.CallRange.Start.Line), + Subject: req.CallRange.Ptr(), + }) continue } @@ -401,9 +454,9 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc // prerelease metadata will be checked. Users may not have even // requested this prerelease so don't print lots of unnecessary # // warnings. - acceptableVersions, err := versions.MeetingConstraintsString(req.VersionConstraints.String()) + acceptableVersions, err := versions.MeetingConstraintsString(req.VersionConstraint.Required.String()) if err != nil { - log.Printf("[WARN] ModuleInstaller: %s ignoring %s because the version constraints (%s) could not be parsed: %s", key, v, req.VersionConstraints.String(), err.Error()) + log.Printf("[WARN] ModuleInstaller: %s ignoring %s because the version constraints (%s) could not be parsed: %s", key, v, req.VersionConstraint.Required.String(), err.Error()) continue } @@ -434,7 +487,7 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc latestVersion = v } - if req.VersionConstraints.Check(v) { + if req.VersionConstraint.Required.Check(v) { if latestMatch == nil || v.GreaterThan(latestMatch) { latestMatch = v } @@ -442,20 +495,22 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc } if latestVersion == nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Module has no versions", - fmt.Sprintf("Module %q (%s:%d) has no versions available on %s.", addr, req.CallPos.Filename, req.CallPos.Line, hostname), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Module has no versions", + Detail: fmt.Sprintf("Module %q (%s:%d) has no versions available on %s.", addr, req.CallRange.Filename, req.CallRange.Start.Line, hostname), + Subject: req.CallRange.Ptr(), + }) return nil, nil, diags } if latestMatch == nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Unresolvable module version constraint", - fmt.Sprintf("There is no available version of module %q (%s:%d) which matches the given version constraint. The newest available version is %s.", addr, req.CallPos.Filename, req.CallPos.Line, latestVersion), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unresolvable module version constraint", + Detail: fmt.Sprintf("There is no available version of module %q (%s:%d) which matches the given version constraint. The newest available version is %s.", addr, req.CallRange.Filename, req.CallRange.Start.Line, latestVersion), + Subject: req.CallRange.Ptr(), + }) return nil, nil, diags } @@ -472,20 +527,20 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc realAddrRaw, err := reg.ModuleLocation(ctx, regsrcAddr, latestMatch.String()) if err != nil { log.Printf("[ERROR] %s from %s %s: %s", key, addr, latestMatch, err) - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Error accessing remote module registry", - fmt.Sprintf("Failed to retrieve a download URL for %s %s from %s: %s", addr, latestMatch, hostname, err), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Error accessing remote module registry", + Detail: fmt.Sprintf("Failed to retrieve a download URL for %s %s from %s: %s", addr, latestMatch, hostname, err), + }) return nil, nil, diags } realAddr, err := addrs.ParseModuleSource(realAddrRaw) if err != nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Invalid package location from module registry", - fmt.Sprintf("Module registry %s returned invalid source location %q for %s %s: %s.", hostname, realAddrRaw, addr, latestMatch, err), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid package location from module registry", + Detail: fmt.Sprintf("Module registry %s returned invalid source location %q for %s %s: %s.", hostname, realAddrRaw, addr, latestMatch, err), + }) return nil, nil, diags } switch realAddr := realAddr.(type) { @@ -496,11 +551,11 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc case addrs.ModuleSourceRemote: i.registryPackageSources[moduleAddr] = realAddr default: - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Invalid package location from module registry", - fmt.Sprintf("Module registry %s returned invalid source location %q for %s %s: must be a direct remote package address.", hostname, realAddrRaw, addr, latestMatch), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid package location from module registry", + Detail: fmt.Sprintf("Module registry %s returned invalid source location %q for %s %s: must be a direct remote package address.", hostname, realAddrRaw, addr, latestMatch), + }) return nil, nil, diags } } @@ -511,11 +566,11 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc err := fetcher.FetchPackage(ctx, instPath, dlAddr.Package.String()) if errors.Is(err, context.Canceled) { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Module download was interrupted", - fmt.Sprintf("Interrupt signal received when downloading module %s.", addr), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Module download was interrupted", + Detail: fmt.Sprintf("Interrupt signal received when downloading module %s.", addr), + }) return nil, nil, diags } if err != nil { @@ -524,11 +579,12 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc // we have no way to recognize any specific errors to improve them // and masking the error entirely would hide valuable diagnostic // information from the user. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Failed to download module", - fmt.Sprintf("Could not download module %q (%s:%d) source code from %q: %s.", req.Name, req.CallPos.Filename, req.CallPos.Line, dlAddr, err), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to download module", + Detail: fmt.Sprintf("Could not download module %q (%s:%d) source code from %q: %s.", req.Name, req.CallRange.Filename, req.CallRange.Start.Line, dlAddr, err), + Subject: req.CallRange.Ptr(), + }) return nil, nil, diags } @@ -544,20 +600,25 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc log.Printf("[TRACE] ModuleInstaller: %s should now be at %s", key, modDir) // Finally we are ready to try actually loading the module. - mod, mDiags := earlyconfig.LoadModule(modDir) + mod, mDiags := i.loader.Parser().LoadConfigDir(modDir) if mod == nil { // nil indicates missing or unreadable directory, so we'll // discard the returned diags and return a more specific // error message here. For registry modules this actually // indicates a bug in the code above, since it's not the // user's responsibility to create the directory in this case. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Unreadable module directory", - fmt.Sprintf("The directory %s could not be read. This is a bug in Terraform and should be reported.", modDir), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unreadable module directory", + Detail: fmt.Sprintf("The directory %s could not be read. This is a bug in Terraform and should be reported.", modDir), + }) + } else if vDiags := mod.CheckCoreVersionRequirements(req.Path, req.SourceAddr); vDiags.HasErrors() { + // If the core version requirements are not met, we drop any other + // diagnostics, as they may reflect language changes from future + // Terraform versions. + diags = diags.Extend(vDiags) } else { - diags = append(diags, mDiags...) + diags = diags.Extend(mDiags) } // Note the local location in our manifest. @@ -573,20 +634,21 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc return mod, latestMatch, diags } -func (i *ModuleInstaller) installGoGetterModule(ctx context.Context, req *earlyconfig.ModuleRequest, key string, instPath string, manifest modsdir.Manifest, hooks ModuleInstallHooks, fetcher *getmodules.PackageFetcher) (*tfconfig.Module, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics +func (i *ModuleInstaller) installGoGetterModule(ctx context.Context, req *configs.ModuleRequest, key string, instPath string, manifest modsdir.Manifest, hooks ModuleInstallHooks, fetcher *getmodules.PackageFetcher) (*configs.Module, hcl.Diagnostics) { + var diags hcl.Diagnostics // Report up to the caller that we're about to start downloading. addr := req.SourceAddr.(addrs.ModuleSourceRemote) packageAddr := addr.Package hooks.Download(key, packageAddr.String(), nil) - if len(req.VersionConstraints) != 0 { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Invalid version constraint", - fmt.Sprintf("Cannot apply a version constraint to module %q (at %s:%d) because it doesn't come from a module registry.", req.Name, req.CallPos.Filename, req.CallPos.Line), - )) + if len(req.VersionConstraint.Required) != 0 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid version constraint", + Detail: fmt.Sprintf("Cannot apply a version constraint to module %q (at %s:%d) because it doesn't come from a module registry.", req.Name, req.CallRange.Filename, req.CallRange.Start.Line), + Subject: req.CallRange.Ptr(), + }) return nil, diags } @@ -599,54 +661,65 @@ func (i *ModuleInstaller) installGoGetterModule(ctx context.Context, req *earlyc "[TRACE] ModuleInstaller: %s looks like a local path but is missing ./ or ../", req.SourceAddr, ) - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Module not found", - fmt.Sprintf( + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Module not found", + Detail: fmt.Sprintf( "The module address %q could not be resolved.\n\n"+ "If you intended this as a path relative to the current "+ "module, use \"./%s\" instead. The \"./\" prefix "+ "indicates that the address is a relative filesystem path.", req.SourceAddr, req.SourceAddr, ), - )) + }) } else { // Errors returned by go-getter have very inconsistent quality as // end-user error messages, but for now we're accepting that because // we have no way to recognize any specific errors to improve them // and masking the error entirely would hide valuable diagnostic // information from the user. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Failed to download module", - fmt.Sprintf("Could not download module %q (%s:%d) source code from %q: %s", req.Name, req.CallPos.Filename, req.CallPos.Line, packageAddr, err), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to download module", + Detail: fmt.Sprintf("Could not download module %q (%s:%d) source code from %q: %s", req.Name, req.CallRange.Filename, req.CallRange.Start.Line, packageAddr, err), + Subject: req.CallRange.Ptr(), + }) } return nil, diags } modDir, err := getmodules.ExpandSubdirGlobs(instPath, addr.Subdir) if err != nil { - diags = diags.Append(err) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to expand subdir globs", + Detail: err.Error(), + }) return nil, diags } log.Printf("[TRACE] ModuleInstaller: %s %q was downloaded to %s", key, addr, modDir) - mod, mDiags := earlyconfig.LoadModule(modDir) + // Finally we are ready to try actually loading the module. + mod, mDiags := i.loader.Parser().LoadConfigDir(modDir) if mod == nil { // nil indicates missing or unreadable directory, so we'll // discard the returned diags and return a more specific // error message here. For go-getter modules this actually // indicates a bug in the code above, since it's not the // user's responsibility to create the directory in this case. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Unreadable module directory", - fmt.Sprintf("The directory %s could not be read. This is a bug in Terraform and should be reported.", modDir), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unreadable module directory", + Detail: fmt.Sprintf("The directory %s could not be read. This is a bug in Terraform and should be reported.", modDir), + }) + } else if vDiags := mod.CheckCoreVersionRequirements(req.Path, req.SourceAddr); vDiags.HasErrors() { + // If the core version requirements are not met, we drop any other + // diagnostics, as they may reflect language changes from future + // Terraform versions. + diags = diags.Extend(vDiags) } else { - diags = append(diags, mDiags...) + diags = diags.Extend(mDiags) } // Note the local location in our manifest. @@ -674,7 +747,7 @@ func (i *ModuleInstaller) packageInstallPath(modulePath addrs.Module) string { // // This function's behavior is only reasonable for errors returned from the // ModuleInstaller.installLocalModule function. -func maybeImproveLocalInstallError(req *earlyconfig.ModuleRequest, diags tfdiags.Diagnostics) tfdiags.Diagnostics { +func maybeImproveLocalInstallError(req *configs.ModuleRequest, diags hcl.Diagnostics) hcl.Diagnostics { if !diags.HasErrors() { return diags } @@ -709,7 +782,7 @@ func maybeImproveLocalInstallError(req *earlyconfig.ModuleRequest, diags tfdiags Path: req.Path, SourceAddr: req.SourceAddr, }) - current := req.Parent // an earlyconfig.Config where Children isn't populated yet + current := req.Parent // a configs.Config where Children isn't populated yet for { if current == nil || current.SourceAddr == nil { // We've reached the root module, in which case we aren't @@ -753,11 +826,11 @@ func maybeImproveLocalInstallError(req *earlyconfig.ModuleRequest, diags tfdiags if !strings.HasPrefix(nextPath, prefix) { // ESCAPED! escapeeAddr := step.Path.String() - var newDiags tfdiags.Diagnostics + var newDiags hcl.Diagnostics // First we'll copy over any non-error diagnostics from the source diags for _, diag := range diags { - if diag.Severity() != tfdiags.Error { + if diag.Severity != hcl.DiagError { newDiags = newDiags.Append(diag) } } @@ -772,14 +845,14 @@ func maybeImproveLocalInstallError(req *earlyconfig.ModuleRequest, diags tfdiags // about it. suggestion = "\n\nTerraform treats absolute filesystem paths as external modules which establish a new module package. To treat this directory as part of the same package as its caller, use a local path starting with either \"./\" or \"../\"." } - newDiags = newDiags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Local module path escapes module package", - fmt.Sprintf( + newDiags = newDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Local module path escapes module package", + Detail: fmt.Sprintf( "The given source directory for %s would be outside of its containing package %q. Local source addresses starting with \"../\" must stay within the same package that the calling module belongs to.%s", escapeeAddr, packageAddr, suggestion, ), - )) + }) return newDiags } diff --git a/internal/initwd/module_install_test.go b/internal/initwd/module_install_test.go index 6f77c60f60..df7fb271ed 100644 --- a/internal/initwd/module_install_test.go +++ b/internal/initwd/module_install_test.go @@ -39,7 +39,9 @@ func TestModuleInstaller(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, nil) + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) _, diags := inst.InstallModules(context.Background(), ".", false, hooks) assertNoDiagnostics(t, diags) @@ -100,7 +102,10 @@ func TestModuleInstaller_error(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, nil) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) _, diags := inst.InstallModules(context.Background(), ".", false, hooks) if !diags.HasErrors() { @@ -110,6 +115,27 @@ func TestModuleInstaller_error(t *testing.T) { } } +func TestModuleInstaller_emptyModuleName(t *testing.T) { + fixtureDir := filepath.Clean("testdata/empty-module-name") + dir, done := tempChdir(t, fixtureDir) + defer done() + + hooks := &testInstallHooks{} + + modulesDir := filepath.Join(dir, ".terraform/modules") + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) + _, diags := inst.InstallModules(context.Background(), ".", false, hooks) + + if !diags.HasErrors() { + t.Fatal("expected error") + } else { + assertDiagnosticSummary(t, diags, "Invalid module instance name") + } +} + func TestModuleInstaller_packageEscapeError(t *testing.T) { fixtureDir := filepath.Clean("testdata/load-module-package-escape") dir, done := tempChdir(t, fixtureDir) @@ -135,7 +161,10 @@ func TestModuleInstaller_packageEscapeError(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, nil) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) _, diags := inst.InstallModules(context.Background(), ".", false, hooks) if !diags.HasErrors() { @@ -170,7 +199,10 @@ func TestModuleInstaller_explicitPackageBoundary(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, nil) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) _, diags := inst.InstallModules(context.Background(), ".", false, hooks) if diags.HasErrors() { @@ -190,7 +222,10 @@ func TestModuleInstaller_ExactMatchPrerelease(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, registry.NewClient(nil, nil)) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil)) cfg, diags := inst.InstallModules(context.Background(), ".", false, hooks) if diags.HasErrors() { @@ -214,7 +249,10 @@ func TestModuleInstaller_PartialMatchPrerelease(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, registry.NewClient(nil, nil)) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil)) cfg, diags := inst.InstallModules(context.Background(), ".", false, hooks) if diags.HasErrors() { @@ -234,7 +272,10 @@ func TestModuleInstaller_invalid_version_constraint_error(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, nil) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) _, diags := inst.InstallModules(context.Background(), ".", false, hooks) if !diags.HasErrors() { @@ -257,7 +298,10 @@ func TestModuleInstaller_invalidVersionConstraintGetter(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, nil) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) _, diags := inst.InstallModules(context.Background(), ".", false, hooks) if !diags.HasErrors() { @@ -280,7 +324,10 @@ func TestModuleInstaller_invalidVersionConstraintLocal(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, nil) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) _, diags := inst.InstallModules(context.Background(), ".", false, hooks) if !diags.HasErrors() { @@ -303,7 +350,10 @@ func TestModuleInstaller_symlink(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, nil) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) _, diags := inst.InstallModules(context.Background(), ".", false, hooks) assertNoDiagnostics(t, diags) @@ -376,7 +426,10 @@ func TestLoaderInstallModules_registry(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, registry.NewClient(nil, nil)) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil)) _, diags := inst.InstallModules(context.Background(), dir, false, hooks) assertNoDiagnostics(t, diags) @@ -482,7 +535,7 @@ func TestLoaderInstallModules_registry(t *testing.T) { t.Errorf("module download url cache was not populated\ngot: %s", spew.Sdump(inst.registryPackageSources)) } - loader, err := configload.NewLoader(&configload.Config{ + loader, err = configload.NewLoader(&configload.Config{ ModulesDir: modulesDir, }) if err != nil { @@ -536,7 +589,10 @@ func TestLoaderInstallModules_goGetter(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, registry.NewClient(nil, nil)) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil)) _, diags := inst.InstallModules(context.Background(), dir, false, hooks) assertNoDiagnostics(t, diags) @@ -609,7 +665,7 @@ func TestLoaderInstallModules_goGetter(t *testing.T) { t.Fatalf("wrong installer calls\n%s", diff) } - loader, err := configload.NewLoader(&configload.Config{ + loader, err = configload.NewLoader(&configload.Config{ ModulesDir: modulesDir, }) if err != nil { diff --git a/internal/initwd/testdata/empty-module-name/child/main.tf b/internal/initwd/testdata/empty-module-name/child/main.tf new file mode 100644 index 0000000000..6187fa659d --- /dev/null +++ b/internal/initwd/testdata/empty-module-name/child/main.tf @@ -0,0 +1,3 @@ +output "boop" { + value = "beep" +} diff --git a/internal/initwd/testdata/empty-module-name/main.tf b/internal/initwd/testdata/empty-module-name/main.tf new file mode 100644 index 0000000000..45add55b6f --- /dev/null +++ b/internal/initwd/testdata/empty-module-name/main.tf @@ -0,0 +1,3 @@ +module "" { + source = "./child" +} diff --git a/internal/initwd/testing.go b/internal/initwd/testing.go index 406718159c..34ca5686fe 100644 --- a/internal/initwd/testing.go +++ b/internal/initwd/testing.go @@ -34,7 +34,7 @@ func LoadConfigForTests(t *testing.T, rootDir string) (*configs.Config, *configl var diags tfdiags.Diagnostics loader, cleanup := configload.NewLoaderForTests(t) - inst := NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) + inst := NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil)) _, moreDiags := inst.InstallModules(context.Background(), rootDir, true, ModuleInstallHooksImpl{}) diags = diags.Append(moreDiags) diff --git a/internal/lang/globalref/analyzer_test.go b/internal/lang/globalref/analyzer_test.go index 0a66217e7d..b1fc42447e 100644 --- a/internal/lang/globalref/analyzer_test.go +++ b/internal/lang/globalref/analyzer_test.go @@ -20,7 +20,7 @@ func testAnalyzer(t *testing.T, fixtureName string) *Analyzer { loader, cleanup := configload.NewLoaderForTests(t) defer cleanup() - inst := initwd.NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) + inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil)) _, instDiags := inst.InstallModules(context.Background(), configDir, true, initwd.ModuleInstallHooksImpl{}) if instDiags.HasErrors() { t.Fatalf("unexpected module installation errors: %s", instDiags.Err().Error()) diff --git a/internal/refactoring/move_validate_test.go b/internal/refactoring/move_validate_test.go index 56d767af51..837119bc73 100644 --- a/internal/refactoring/move_validate_test.go +++ b/internal/refactoring/move_validate_test.go @@ -534,7 +534,7 @@ func loadRefactoringFixture(t *testing.T, dir string) (*configs.Config, instance loader, cleanup := configload.NewLoaderForTests(t) defer cleanup() - inst := initwd.NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) + inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil)) _, instDiags := inst.InstallModules(context.Background(), dir, true, initwd.ModuleInstallHooksImpl{}) if instDiags.HasErrors() { t.Fatal(instDiags.Err()) diff --git a/internal/terraform/context_test.go b/internal/terraform/context_test.go index 12c376622b..145c3b4cac 100644 --- a/internal/terraform/context_test.go +++ b/internal/terraform/context_test.go @@ -38,14 +38,12 @@ var ( func TestNewContextRequiredVersion(t *testing.T) { cases := []struct { Name string - Module string Version string Value string Err bool }{ { "no requirement", - "", "0.1.0", "", false, @@ -53,7 +51,6 @@ func TestNewContextRequiredVersion(t *testing.T) { { "doesn't match", - "", "0.1.0", "> 0.6.0", true, @@ -61,7 +58,6 @@ func TestNewContextRequiredVersion(t *testing.T) { { "matches", - "", "0.7.0", "> 0.6.0", false, @@ -69,7 +65,6 @@ func TestNewContextRequiredVersion(t *testing.T) { { "prerelease doesn't match with inequality", - "", "0.8.0", "> 0.7.0-beta", true, @@ -77,27 +72,10 @@ func TestNewContextRequiredVersion(t *testing.T) { { "prerelease doesn't match with equality", - "", "0.7.0", "0.7.0-beta", true, }, - - { - "module matches", - "context-required-version-module", - "0.5.0", - "", - false, - }, - - { - "module doesn't match", - "context-required-version-module", - "0.4.0", - "", - true, - }, } for i, tc := range cases { @@ -107,11 +85,7 @@ func TestNewContextRequiredVersion(t *testing.T) { tfversion.SemVer = version.Must(version.NewVersion(tc.Version)) defer func() { tfversion.SemVer = old }() - name := "context-required-version" - if tc.Module != "" { - name = tc.Module - } - mod := testModule(t, name) + mod := testModule(t, "context-required-version") if tc.Value != "" { constraint, err := version.NewConstraint(tc.Value) if err != nil { @@ -134,6 +108,65 @@ func TestNewContextRequiredVersion(t *testing.T) { } } +func TestNewContextRequiredVersion_child(t *testing.T) { + mod := testModuleInline(t, map[string]string{ + "main.tf": ` +module "child" { + source = "./child" +} +`, + "child/main.tf": ` +terraform {} +`, + }) + + cases := map[string]struct { + Version string + Constraint string + Err bool + }{ + "matches": { + "0.5.0", + ">= 0.5.0", + false, + }, + "doesn't match": { + "0.4.0", + ">= 0.5.0", + true, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + // Reset the version for the tests + old := tfversion.SemVer + tfversion.SemVer = version.Must(version.NewVersion(tc.Version)) + defer func() { tfversion.SemVer = old }() + + if tc.Constraint != "" { + constraint, err := version.NewConstraint(tc.Constraint) + if err != nil { + t.Fatalf("can't parse %q as version constraint", tc.Constraint) + } + child := mod.Children["child"] + child.Module.CoreVersionConstraints = append(child.Module.CoreVersionConstraints, configs.VersionConstraint{ + Required: constraint, + }) + } + c, diags := NewContext(&ContextOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected NewContext errors: %s", diags.Err()) + } + + diags = c.Validate(mod) + if diags.HasErrors() != tc.Err { + t.Fatalf("err: %s", diags.Err()) + } + }) + } +} + func TestContext_missingPlugins(t *testing.T) { ctx, diags := NewContext(&ContextOpts{}) assertNoDiagnostics(t, diags) diff --git a/internal/terraform/terraform_test.go b/internal/terraform/terraform_test.go index 4192459583..ac8ad1fcb5 100644 --- a/internal/terraform/terraform_test.go +++ b/internal/terraform/terraform_test.go @@ -63,7 +63,7 @@ func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *config // Test modules usually do not refer to remote sources, and for local // sources only this ultimately just records all of the module paths // in a JSON file so that we can load them below. - inst := initwd.NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) + inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil)) _, instDiags := inst.InstallModules(context.Background(), dir, true, initwd.ModuleInstallHooksImpl{}) if instDiags.HasErrors() { t.Fatal(instDiags.Err()) @@ -120,7 +120,7 @@ func testModuleInline(t *testing.T, sources map[string]string) *configs.Config { // Test modules usually do not refer to remote sources, and for local // sources only this ultimately just records all of the module paths // in a JSON file so that we can load them below. - inst := initwd.NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) + inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil)) _, instDiags := inst.InstallModules(context.Background(), cfgPath, true, initwd.ModuleInstallHooksImpl{}) if instDiags.HasErrors() { t.Fatal(instDiags.Err()) diff --git a/internal/terraform/testdata/context-required-version-module/child/main.tf b/internal/terraform/testdata/context-required-version-module/child/main.tf deleted file mode 100644 index 3b52ffab91..0000000000 --- a/internal/terraform/testdata/context-required-version-module/child/main.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.5.0" -} diff --git a/internal/terraform/testdata/context-required-version-module/main.tf b/internal/terraform/testdata/context-required-version-module/main.tf deleted file mode 100644 index 0f6991c536..0000000000 --- a/internal/terraform/testdata/context-required-version-module/main.tf +++ /dev/null @@ -1,3 +0,0 @@ -module "child" { - source = "./child" -} diff --git a/internal/terraform/version_required.go b/internal/terraform/version_required.go index 1861050b95..8d891d5c95 100644 --- a/internal/terraform/version_required.go +++ b/internal/terraform/version_required.go @@ -1,14 +1,9 @@ package terraform import ( - "fmt" - - "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/hashicorp/terraform/internal/configs" - - tfversion "github.com/hashicorp/terraform/version" ) // CheckCoreVersionRequirements visits each of the modules in the given @@ -24,62 +19,7 @@ func CheckCoreVersionRequirements(config *configs.Config) tfdiags.Diagnostics { } var diags tfdiags.Diagnostics - module := config.Module - - for _, constraint := range module.CoreVersionConstraints { - // Before checking if the constraints are met, check that we are not using any prerelease fields as these - // are not currently supported. - var prereleaseDiags tfdiags.Diagnostics - for _, required := range constraint.Required { - if required.Prerelease() { - prereleaseDiags = prereleaseDiags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid required_version constraint", - Detail: fmt.Sprintf( - "Prerelease version constraints are not supported: %s. Remove the prerelease information from the constraint. Prerelease versions of terraform will match constraints using their version core only.", - required.String()), - Subject: constraint.DeclRange.Ptr(), - }) - } - } - - if len(prereleaseDiags) > 0 { - // There were some prerelease fields in the constraints. Don't check the constraints as they will - // fail, and populate the diagnostics for these constraints with the prerelease diagnostics. - diags = diags.Append(prereleaseDiags) - continue - } - - if !constraint.Required.Check(tfversion.SemVer) { - switch { - case len(config.Path) == 0: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Unsupported Terraform Core version", - Detail: fmt.Sprintf( - "This configuration does not support Terraform version %s. To proceed, either choose another supported Terraform version or update this version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected behavior.", - tfversion.String(), - ), - Subject: constraint.DeclRange.Ptr(), - }) - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Unsupported Terraform Core version", - Detail: fmt.Sprintf( - "Module %s (from %s) does not support Terraform version %s. To proceed, either choose another supported Terraform version or update this version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected behavior.", - config.Path, config.SourceAddr, tfversion.String(), - ), - Subject: constraint.DeclRange.Ptr(), - }) - } - } - } - - for _, c := range config.Children { - childDiags := CheckCoreVersionRequirements(c) - diags = diags.Append(childDiags) - } + diags = diags.Append(config.CheckCoreVersionRequirements()) return diags }