diff --git a/command/command_test.go b/command/command_test.go index d4f263c55..8753028bc 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -152,7 +152,7 @@ func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *config // 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)) - instDiags := inst.InstallModules(dir, true, initwd.ModuleInstallHooksImpl{}) + _, instDiags := inst.InstallModules(dir, true, initwd.ModuleInstallHooksImpl{}) if instDiags.HasErrors() { t.Fatal(instDiags.Err()) } diff --git a/command/import_test.go b/command/import_test.go index 0c4757122..6b4584648 100644 --- a/command/import_test.go +++ b/command/import_test.go @@ -801,14 +801,14 @@ func TestImport_pluginDir(t *testing.T) { initCmd := &InitCommand{ Meta: Meta{ pluginPath: []string{"./plugins"}, - Ui: new(cli.MockUi), + Ui: cli.NewMockUi(), }, providerInstaller: &discovery.ProviderInstaller{ PluginProtocolVersion: plugin.Handshake.ProtocolVersion, }, } - if err := initCmd.getProviders(".", nil, false); err != nil { - t.Fatal(err) + if code := initCmd.Run(nil); code != 0 { + t.Fatal(initCmd.Meta.Ui.(*cli.MockUi).ErrorWriter.String()) } args := []string{ diff --git a/command/init.go b/command/init.go index bf6f1234c..99f0411b1 100644 --- a/command/init.go +++ b/command/init.go @@ -8,20 +8,23 @@ import ( "strings" "github.com/hashicorp/hcl2/hcl" + "github.com/hashicorp/terraform-config-inspect/tfconfig" "github.com/posener/complete" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/backend" + backendInit "github.com/hashicorp/terraform/backend/init" "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs/configschema" + "github.com/hashicorp/terraform/configs/configupgrade" + "github.com/hashicorp/terraform/internal/earlyconfig" + "github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/plugin" "github.com/hashicorp/terraform/plugin/discovery" "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/tfdiags" - - backendInit "github.com/hashicorp/terraform/backend/init" ) // InitCommand is a Command implementation that takes a Terraform @@ -164,116 +167,135 @@ func (c *InitCommand) Run(args []string) int { return 0 } - var back backend.Backend - - // If we're performing a get or loading the backend, then we perform - // some extra tasks. - if flagGet || flagBackend { - config, confDiags := c.loadSingleModule(path) - diags = diags.Append(confDiags) - if confDiags.HasErrors() { + // Before we do anything else, we'll try loading configuration with both + // our "normal" and "early" configuration codepaths. If early succeeds + // while normal fails, that strongly suggests that the configuration is + // using syntax that worked in 0.11 but no longer in 0.12, which requires + // some special behavior here to get the directory initialized just enough + // to run "terraform 0.12upgrade". + // + // FIXME: Once we reach 0.13 and remove 0.12upgrade, we should rework this + // so that we first use the early config to do a general compatibility + // check with dependencies, producing version-oriented error messages if + // dependencies aren't right, and only then use the real loader to deal + // with the backend configuration. + rootMod, confDiags := c.loadSingleModule(path) + rootModEarly, earlyConfDiags := c.loadSingleModuleEarly(path) + configUpgradeProbablyNeeded := false + if confDiags.HasErrors() { + if earlyConfDiags.HasErrors() { + // If both parsers produced errors then we'll assume the config + // is _truly_ invalid and produce error messages as normal. // Since this may be the user's first ever interaction with Terraform, // we'll provide some additional context in this case. c.Ui.Error(strings.TrimSpace(errInitConfigError)) + diags = diags.Append(confDiags) c.showDiagnostics(diags) return 1 } - // If we requested downloading modules and have modules in the config - if flagGet && len(config.ModuleCalls) > 0 { - header = true - - if flagUpgrade { - c.Ui.Output(c.Colorize().Color(fmt.Sprintf("[reset][bold]Upgrading modules..."))) - } else { - c.Ui.Output(c.Colorize().Color(fmt.Sprintf("[reset][bold]Initializing modules..."))) - } - - hooks := uiModuleInstallHooks{ - Ui: c.Ui, - ShowLocalPaths: true, - } - instDiags := c.installModules(path, flagUpgrade, hooks) - diags = diags.Append(instDiags) - if instDiags.HasErrors() { - c.showDiagnostics(diags) - return 1 - } - } - - // If we're requesting backend configuration or looking for required - // plugins, load the backend - if flagBackend { - header = true - - var backendSchema *configschema.Block - - // Only output that we're initializing a backend if we have - // something in the config. We can be UNSETTING a backend as well - // in which case we choose not to show this. - if config.Backend != nil { - c.Ui.Output(c.Colorize().Color(fmt.Sprintf("\n[reset][bold]Initializing the backend..."))) - - backendType := config.Backend.Type - bf := backendInit.Backend(backendType) - if bf == nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Unsupported backend type", - Detail: fmt.Sprintf("There is no backend type named %q.", backendType), - Subject: &config.Backend.TypeRange, - }) - c.showDiagnostics() - return 1 - } - - b := bf() - backendSchema = b.ConfigSchema() - } - - var backendConfigOverride hcl.Body - if backendSchema != nil { - var overrideDiags tfdiags.Diagnostics - backendConfigOverride, overrideDiags = c.backendConfigOverrideBody(flagConfigExtra, backendSchema) - diags = diags.Append(overrideDiags) - if overrideDiags.HasErrors() { - c.showDiagnostics() - return 1 - } - } - - opts := &BackendOpts{ - Config: config.Backend, - ConfigOverride: backendConfigOverride, - Init: true, - } - var backDiags tfdiags.Diagnostics - back, backDiags = c.Backend(opts) - diags = diags.Append(backDiags) - if backDiags.HasErrors() { + // If _only_ the main loader produced errors then that suggests an + // upgrade may help. To give us more certainty here, we'll use the + // same heuristic that "terraform 0.12upgrade" uses to guess if a + // configuration has already been upgraded, to reduce the risk that + // we'll produce a misleading message if the problem is just a regular + // syntax error that the early loader just didn't catch. + sources, err := configupgrade.LoadModule(path) + if err == nil { + if already, _ := sources.MaybeAlreadyUpgraded(); already { + // Just report the errors as normal, then. + c.Ui.Error(strings.TrimSpace(errInitConfigError)) + diags = diags.Append(confDiags) c.showDiagnostics(diags) return 1 } } + configUpgradeProbablyNeeded = true } - - // With modules now installed, we should be able to load the whole - // configuration and check the core version constraints. - config, confDiags := c.loadConfig(path) - diags = diags.Append(confDiags) - if confDiags.HasErrors() { - // Since this may be the user's first ever interaction with Terraform, - // we'll provide some additional context in this case. + if earlyConfDiags.HasErrors() { + // 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 + // some weird behavior downstream. Errors from the early loader are + // generally not as high-quality since it has less context to work with. c.Ui.Error(strings.TrimSpace(errInitConfigError)) + diags = diags.Append(earlyConfDiags) c.showDiagnostics(diags) return 1 } - confDiags = terraform.CheckCoreVersionRequirements(config) - diags = diags.Append(confDiags) - if confDiags.HasErrors() { + + if flagGet { + modsOutput, modsDiags := c.getModules(path, rootModEarly, flagUpgrade) + diags = diags.Append(modsDiags) + if modsDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + if modsOutput { + header = true + } + } + + // With all of the modules (hopefully) installed, we can now try to load + // the whole configuration tree. + // + // Just as above, we'll try loading both with the early and normal config + // loaders here. Subsequent work will only use the early config, but + // loading both gives us an opportunity to prefer the better error messages + // from the normal loader if both fail. + _, confDiags = c.loadConfig(path) + earlyConfig, earlyConfDiags := c.loadConfigEarly(path) + if confDiags.HasErrors() && !configUpgradeProbablyNeeded { + c.Ui.Error(strings.TrimSpace(errInitConfigError)) + diags = diags.Append(confDiags) c.showDiagnostics(diags) return 1 } + if earlyConfDiags.HasErrors() { + c.Ui.Error(strings.TrimSpace(errInitConfigError)) + diags = diags.Append(earlyConfDiags) + c.showDiagnostics(diags) + 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 := initwd.CheckCoreVersionRequirements(earlyConfig) + diags = diags.Append(versionDiags) + if versionDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + } + + var back backend.Backend + if flagBackend { + switch { + case configUpgradeProbablyNeeded: + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Skipping backend initialization pending configuration upgrade", + // The "below" in this message is referring to the special + // note about running "terraform 0.12upgrade" that we'll + // print out at the end when configUpgradeProbablyNeeded is set. + "The root module configuration contains errors that may be fixed by running the configuration upgrade tool, so Terraform is skipping backend initialization. See below for more information.", + )) + default: + be, backendOutput, backendDiags := c.initBackend(rootMod, flagConfigExtra) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + if backendOutput { + header = true + } + back = be + } + } if back == nil { // If we didn't initialize a backend then we'll try to at least @@ -313,12 +335,15 @@ func (c *InitCommand) Run(args []string) int { } // Now that we have loaded all modules, check the module tree for missing providers. - providerDiags := c.getProviders(path, state, flagUpgrade) + providersOutput, providerDiags := c.getProviders(earlyConfig, state, flagUpgrade) diags = diags.Append(providerDiags) if providerDiags.HasErrors() { c.showDiagnostics(diags) return 1 } + if providersOutput { + header = true + } // If we outputted information, then we need to output a newline // so that our success message is nicely spaced out from prior text. @@ -331,6 +356,15 @@ func (c *InitCommand) Run(args []string) int { // still the final thing shown. c.showDiagnostics(diags) + if configUpgradeProbablyNeeded { + switch { + case c.RunningInAutomation: + c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitSuccessConfigUpgrade))) + default: + c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitSuccessConfigUpgradeCLI))) + } + return 0 + } c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitSuccess))) if !c.RunningInAutomation { // If we're not running in an automation wrapper, give the user @@ -342,6 +376,250 @@ func (c *InitCommand) Run(args []string) int { return 0 } +func (c *InitCommand) getModules(path string, earlyRoot *tfconfig.Module, upgrade bool) (output bool, diags tfdiags.Diagnostics) { + if len(earlyRoot.ModuleCalls) == 0 { + // Nothing to do + return false, nil + } + + if upgrade { + c.Ui.Output(c.Colorize().Color(fmt.Sprintf("[reset][bold]Upgrading modules..."))) + } else { + c.Ui.Output(c.Colorize().Color(fmt.Sprintf("[reset][bold]Initializing modules..."))) + } + + hooks := uiModuleInstallHooks{ + Ui: c.Ui, + ShowLocalPaths: true, + } + instDiags := c.installModules(path, upgrade, hooks) + diags = diags.Append(instDiags) + + // Since module installer has modified the module manifest on disk, we need + // to refresh the cache of it in the loader. + if c.configLoader != nil { + if err := c.configLoader.RefreshModules(); err != nil { + // Should never happen + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to read module manifest", + fmt.Sprintf("After installing modules, Terraform could not re-read the manifest of installed modules. This is a bug in Terraform. %s.", err), + )) + } + } + + return true, diags +} + +func (c *InitCommand) initBackend(root *configs.Module, extraConfig rawFlags) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { + c.Ui.Output(c.Colorize().Color(fmt.Sprintf("\n[reset][bold]Initializing the backend..."))) + + var backendConfig *configs.Backend + var backendConfigOverride hcl.Body + if root.Backend != nil { + backendType := root.Backend.Type + bf := backendInit.Backend(backendType) + if bf == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported backend type", + Detail: fmt.Sprintf("There is no backend type named %q.", backendType), + Subject: &root.Backend.TypeRange, + }) + return nil, true, diags + } + + b := bf() + backendSchema := b.ConfigSchema() + backendConfig = root.Backend + + var overrideDiags tfdiags.Diagnostics + backendConfigOverride, overrideDiags = c.backendConfigOverrideBody(extraConfig, backendSchema) + diags = diags.Append(overrideDiags) + if overrideDiags.HasErrors() { + return nil, true, diags + } + } + + opts := &BackendOpts{ + Config: backendConfig, + ConfigOverride: backendConfigOverride, + Init: true, + } + back, backDiags := c.Backend(opts) + diags = diags.Append(backDiags) + return back, true, diags +} + +// Load the complete module tree, and fetch any missing providers. +// This method outputs its own Ui. +func (c *InitCommand) getProviders(earlyConfig *earlyconfig.Config, state *states.State, upgrade bool) (output bool, diags tfdiags.Diagnostics) { + var available discovery.PluginMetaSet + if upgrade { + // If we're in upgrade mode, we ignore any auto-installed plugins + // in "available", causing us to reinstall and possibly upgrade them. + available = c.providerPluginManuallyInstalledSet() + } else { + available = c.providerPluginSet() + } + + configDeps, depsDiags := earlyConfig.ProviderDependencies() + diags = diags.Append(depsDiags) + if depsDiags.HasErrors() { + return false, diags + } + + configReqs := configDeps.AllPluginRequirements() + // FIXME: This is weird because ConfigTreeDependencies was written before + // we switched over to using earlyConfig as the main source of dependencies. + // In future we should clean this up to be a more reasoable API. + stateReqs := terraform.ConfigTreeDependencies(nil, state).AllPluginRequirements() + + requirements := configReqs.Merge(stateReqs) + if len(requirements) == 0 { + // nothing to initialize + return false, nil + } + + c.Ui.Output(c.Colorize().Color( + "\n[reset][bold]Initializing provider plugins...", + )) + + missing := c.missingPlugins(available, requirements) + + if c.getPlugins { + if len(missing) > 0 { + c.Ui.Output("- Checking for available provider plugins...") + } + + for provider, reqd := range missing { + _, err := c.providerInstaller.Get(provider, reqd.Versions) + + if err != nil { + switch err { + case discovery.ErrorNoSuchProvider: + c.Ui.Error(fmt.Sprintf(errProviderNotFound, provider, DefaultPluginVendorDir)) + case discovery.ErrorNoSuitableVersion: + if reqd.Versions.Unconstrained() { + // This should never happen, but might crop up if we catch + // the releases server in a weird state where the provider's + // directory is present but does not yet contain any + // versions. We'll treat it like ErrorNoSuchProvider, then. + c.Ui.Error(fmt.Sprintf(errProviderNotFound, provider, DefaultPluginVendorDir)) + } else { + c.Ui.Error(fmt.Sprintf(errProviderVersionsUnsuitable, provider, reqd.Versions)) + } + case discovery.ErrorNoVersionCompatible: + // FIXME: This error message is sub-awesome because we don't + // have enough information here to tell the user which versions + // we considered and which versions might be compatible. + constraint := reqd.Versions.String() + if constraint == "" { + constraint = "(any version)" + } + c.Ui.Error(fmt.Sprintf(errProviderIncompatible, provider, constraint)) + default: + c.Ui.Error(fmt.Sprintf(errProviderInstallError, provider, err.Error(), DefaultPluginVendorDir)) + } + + diags = diags.Append(err) + } + } + + if diags.HasErrors() { + return true, diags + } + } else if len(missing) > 0 { + // we have missing providers, but aren't going to try and download them + var lines []string + for provider, reqd := range missing { + if reqd.Versions.Unconstrained() { + lines = append(lines, fmt.Sprintf("* %s (any version)\n", provider)) + } else { + lines = append(lines, fmt.Sprintf("* %s (%s)\n", provider, reqd.Versions)) + } + diags = diags.Append(fmt.Errorf("missing provider %q", provider)) + } + sort.Strings(lines) + c.Ui.Error(fmt.Sprintf(errMissingProvidersNoInstall, strings.Join(lines, ""), DefaultPluginVendorDir)) + return true, diags + } + + // With all the providers downloaded, we'll generate our lock file + // that ensures the provider binaries remain unchanged until we init + // again. If anything changes, other commands that use providers will + // fail with an error instructing the user to re-run this command. + available = c.providerPluginSet() // re-discover to see newly-installed plugins + + // internal providers were already filtered out, since we don't need to get them. + chosen := choosePlugins(available, nil, requirements) + + digests := map[string][]byte{} + for name, meta := range chosen { + digest, err := meta.SHA256() + if err != nil { + diags = diags.Append(fmt.Errorf("Failed to read provider plugin %s: %s", meta.Path, err)) + return true, diags + } + digests[name] = digest + if c.ignorePluginChecksum { + digests[name] = nil + } + } + err := c.providerPluginsLock().Write(digests) + if err != nil { + diags = diags.Append(fmt.Errorf("failed to save provider manifest: %s", err)) + return true, diags + } + + { + // Purge any auto-installed plugins that aren't being used. + purged, err := c.providerInstaller.PurgeUnused(chosen) + if err != nil { + // Failure to purge old plugins is not a fatal error + c.Ui.Warn(fmt.Sprintf("failed to purge unused plugins: %s", err)) + } + if purged != nil { + for meta := range purged { + log.Printf("[DEBUG] Purged unused %s plugin %s", meta.Name, meta.Path) + } + } + } + + // If any providers have "floating" versions (completely unconstrained) + // we'll suggest the user constrain with a pessimistic constraint to + // avoid implicitly adopting a later major release. + constraintSuggestions := make(map[string]discovery.ConstraintStr) + for name, meta := range chosen { + req := requirements[name] + if req == nil { + // should never happen, but we don't want to crash here, so we'll + // be cautious. + continue + } + + if req.Versions.Unconstrained() && meta.Version != discovery.VersionZero { + // meta.Version.MustParse is safe here because our "chosen" metas + // were already filtered for validity of versions. + constraintSuggestions[name] = meta.Version.MustParse().MinorUpgradeConstraintStr() + } + } + if len(constraintSuggestions) != 0 { + names := make([]string, 0, len(constraintSuggestions)) + for name := range constraintSuggestions { + names = append(names, name) + } + sort.Strings(names) + + c.Ui.Output(outputInitProvidersUnconstrained) + for _, name := range names { + c.Ui.Output(fmt.Sprintf("* provider.%s: version = %q", name, constraintSuggestions[name])) + } + } + + return true, diags +} + // backendConfigOverrideBody interprets the raw values of -backend-config // arguments into a hcl Body that should override the backend settings given // in the configuration. @@ -411,168 +689,6 @@ func (c *InitCommand) backendConfigOverrideBody(flags rawFlags, schema *configsc return ret, diags } -// Load the complete module tree, and fetch any missing providers. -// This method outputs its own Ui. -func (c *InitCommand) getProviders(path string, state *states.State, upgrade bool) tfdiags.Diagnostics { - config, diags := c.loadConfig(path) - if diags.HasErrors() { - return diags - } - - var available discovery.PluginMetaSet - if upgrade { - // If we're in upgrade mode, we ignore any auto-installed plugins - // in "available", causing us to reinstall and possibly upgrade them. - available = c.providerPluginManuallyInstalledSet() - } else { - available = c.providerPluginSet() - } - - requirements := terraform.ConfigTreeDependencies(config, state).AllPluginRequirements() - if len(requirements) == 0 { - // nothing to initialize - return nil - } - - c.Ui.Output(c.Colorize().Color( - "\n[reset][bold]Initializing provider plugins...", - )) - - missing := c.missingPlugins(available, requirements) - - if c.getPlugins { - if len(missing) > 0 { - c.Ui.Output("- Checking for available provider plugins...") - } - - for provider, reqd := range missing { - _, err := c.providerInstaller.Get(provider, reqd.Versions) - - if err != nil { - switch err { - case discovery.ErrorNoSuchProvider: - c.Ui.Error(fmt.Sprintf(errProviderNotFound, provider, DefaultPluginVendorDir)) - case discovery.ErrorNoSuitableVersion: - if reqd.Versions.Unconstrained() { - // This should never happen, but might crop up if we catch - // the releases server in a weird state where the provider's - // directory is present but does not yet contain any - // versions. We'll treat it like ErrorNoSuchProvider, then. - c.Ui.Error(fmt.Sprintf(errProviderNotFound, provider, DefaultPluginVendorDir)) - } else { - c.Ui.Error(fmt.Sprintf(errProviderVersionsUnsuitable, provider, reqd.Versions)) - } - case discovery.ErrorNoVersionCompatible: - // FIXME: This error message is sub-awesome because we don't - // have enough information here to tell the user which versions - // we considered and which versions might be compatible. - constraint := reqd.Versions.String() - if constraint == "" { - constraint = "(any version)" - } - c.Ui.Error(fmt.Sprintf(errProviderIncompatible, provider, constraint)) - default: - c.Ui.Error(fmt.Sprintf(errProviderInstallError, provider, err.Error(), DefaultPluginVendorDir)) - } - - diags = diags.Append(err) - } - } - - if diags.HasErrors() { - return diags - } - } else if len(missing) > 0 { - // we have missing providers, but aren't going to try and download them - var lines []string - for provider, reqd := range missing { - if reqd.Versions.Unconstrained() { - lines = append(lines, fmt.Sprintf("* %s (any version)\n", provider)) - } else { - lines = append(lines, fmt.Sprintf("* %s (%s)\n", provider, reqd.Versions)) - } - diags = diags.Append(fmt.Errorf("missing provider %q", provider)) - } - sort.Strings(lines) - c.Ui.Error(fmt.Sprintf(errMissingProvidersNoInstall, strings.Join(lines, ""), DefaultPluginVendorDir)) - return diags - } - - // With all the providers downloaded, we'll generate our lock file - // that ensures the provider binaries remain unchanged until we init - // again. If anything changes, other commands that use providers will - // fail with an error instructing the user to re-run this command. - available = c.providerPluginSet() // re-discover to see newly-installed plugins - - // internal providers were already filtered out, since we don't need to get them. - chosen := choosePlugins(available, nil, requirements) - - digests := map[string][]byte{} - for name, meta := range chosen { - digest, err := meta.SHA256() - if err != nil { - diags = diags.Append(fmt.Errorf("Failed to read provider plugin %s: %s", meta.Path, err)) - return diags - } - digests[name] = digest - if c.ignorePluginChecksum { - digests[name] = nil - } - } - err := c.providerPluginsLock().Write(digests) - if err != nil { - diags = diags.Append(fmt.Errorf("failed to save provider manifest: %s", err)) - return diags - } - - { - // Purge any auto-installed plugins that aren't being used. - purged, err := c.providerInstaller.PurgeUnused(chosen) - if err != nil { - // Failure to purge old plugins is not a fatal error - c.Ui.Warn(fmt.Sprintf("failed to purge unused plugins: %s", err)) - } - if purged != nil { - for meta := range purged { - log.Printf("[DEBUG] Purged unused %s plugin %s", meta.Name, meta.Path) - } - } - } - - // If any providers have "floating" versions (completely unconstrained) - // we'll suggest the user constrain with a pessimistic constraint to - // avoid implicitly adopting a later major release. - constraintSuggestions := make(map[string]discovery.ConstraintStr) - for name, meta := range chosen { - req := requirements[name] - if req == nil { - // should never happen, but we don't want to crash here, so we'll - // be cautious. - continue - } - - if req.Versions.Unconstrained() && meta.Version != discovery.VersionZero { - // meta.Version.MustParse is safe here because our "chosen" metas - // were already filtered for validity of versions. - constraintSuggestions[name] = meta.Version.MustParse().MinorUpgradeConstraintStr() - } - } - if len(constraintSuggestions) != 0 { - names := make([]string, 0, len(constraintSuggestions)) - for name := range constraintSuggestions { - names = append(names, name) - } - sort.Strings(names) - - c.Ui.Output(outputInitProvidersUnconstrained) - for _, name := range names { - c.Ui.Output(fmt.Sprintf("* provider.%s: version = %q", name, constraintSuggestions[name])) - } - } - - return diags -} - func (c *InitCommand) AutocompleteArgs() complete.Predictor { return complete.PredictDirs("") } @@ -705,6 +821,34 @@ rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary. ` +const outputInitSuccessConfigUpgrade = ` +[reset][bold]Terraform has initialized, but configuration upgrades may be needed.[reset] + +Terraform found syntax errors in the configuration that prevented full +initialization. If you've recently upgraded to Terraform v0.12, this may be +because your configuration uses syntax constructs that are no longer valid, +and so must be updated before full initialization is possible. + +Run terraform init for this configuration at a shell prompt for more information +on how to update it for Terraform v0.12 compatibility. +` + +const outputInitSuccessConfigUpgradeCLI = `[reset][green] +[reset][bold]Terraform has initialized, but configuration upgrades may be needed.[reset] + +Terraform found syntax errors in the configuration that prevented full +initialization. If you've recently upgraded to Terraform v0.12, this may be +because your configuration uses syntax constructs that are no longer valid, +and so must be updated before full initialization is possible. + +Terraform has installed the required providers to support the configuration +upgrade process. To begin upgrading your configuration, run the following: + terraform 0.12upgrade + +To see the full set of errors that led to this message, run: + terraform validate +` + const outputInitProvidersUnconstrained = ` The following providers do not have any version constraints in configuration, so the latest version was installed. diff --git a/command/init_test.go b/command/init_test.go index 7b4a076ca..67922c383 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -3,6 +3,7 @@ package command import ( "fmt" "io/ioutil" + "log" "os" "path/filepath" "reflect" @@ -266,7 +267,9 @@ func TestInit_backendUnset(t *testing.T) { defer testChdir(t, td)() { - ui := new(cli.MockUi) + log.Printf("[TRACE] TestInit_backendUnset: beginning first init") + + ui := cli.NewMockUi() c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -279,6 +282,9 @@ func TestInit_backendUnset(t *testing.T) { if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) } + log.Printf("[TRACE] TestInit_backendUnset: first init complete") + t.Logf("First run output:\n%s", ui.OutputWriter.String()) + t.Logf("First run errors:\n%s", ui.ErrorWriter.String()) if _, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename)); err != nil { t.Fatalf("err: %s", err) @@ -286,12 +292,14 @@ func TestInit_backendUnset(t *testing.T) { } { + log.Printf("[TRACE] TestInit_backendUnset: beginning second init") + // Unset if err := ioutil.WriteFile("main.tf", []byte(""), 0644); err != nil { t.Fatalf("err: %s", err) } - ui := new(cli.MockUi) + ui := cli.NewMockUi() c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -303,6 +311,9 @@ func TestInit_backendUnset(t *testing.T) { if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) } + log.Printf("[TRACE] TestInit_backendUnset: second init complete") + t.Logf("Second run output:\n%s", ui.OutputWriter.String()) + t.Logf("Second run errors:\n%s", ui.ErrorWriter.String()) s := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) if !s.Backend.Empty() { @@ -1220,3 +1231,80 @@ func TestInit_pluginWithInternal(t *testing.T) { t.Fatalf("error: %s", ui.ErrorWriter) } } + +func TestInit_012UpgradeNeeded(t *testing.T) { + td := tempDir(t) + copy.CopyDir(testFixturePath("init-012upgrade"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + ui := cli.NewMockUi() + m := Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + } + + installer := &mockProviderInstaller{ + Providers: map[string][]string{ + "null": []string{"1.0.0"}, + }, + Dir: m.pluginDir(), + } + + c := &InitCommand{ + Meta: m, + providerInstaller: installer, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Errorf("wrong exit status %d; want 0\nerror output:\n%s", code, ui.ErrorWriter.String()) + } + + output := ui.OutputWriter.String() + if !strings.Contains(output, "terraform 0.12upgrade") { + t.Errorf("doesn't look like we detected the need for config upgrade:\n%s", output) + } +} + +func TestInit_012UpgradeNeededInAutomation(t *testing.T) { + td := tempDir(t) + copy.CopyDir(testFixturePath("init-012upgrade"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + ui := cli.NewMockUi() + m := Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + RunningInAutomation: true, + } + + installer := &mockProviderInstaller{ + Providers: map[string][]string{ + "null": []string{"1.0.0"}, + }, + Dir: m.pluginDir(), + } + + c := &InitCommand{ + Meta: m, + providerInstaller: installer, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Errorf("wrong exit status %d; want 0\nerror output:\n%s", code, ui.ErrorWriter.String()) + } + + output := ui.OutputWriter.String() + if !strings.Contains(output, "Run terraform init for this configuration at a shell prompt") { + t.Errorf("doesn't look like we instructed to run Terraform locally:\n%s", output) + } + if strings.Contains(output, "terraform 0.12upgrade") { + // We don't prompt with an exact command in automation mode, since + // the upgrade process is interactive and so it cannot be run in + // automation. + t.Errorf("looks like we incorrectly gave an upgrade command to run:\n%s", output) + } +} diff --git a/command/meta_config.go b/command/meta_config.go index e9bee011e..e791c3c36 100644 --- a/command/meta_config.go +++ b/command/meta_config.go @@ -2,12 +2,14 @@ package command import ( "fmt" + "github.com/hashicorp/terraform/internal/earlyconfig" "os" "path/filepath" "sort" "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl/hclsyntax" + "github.com/hashicorp/terraform-config-inspect/tfconfig" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs/configload" "github.com/hashicorp/terraform/configs/configschema" @@ -65,6 +67,30 @@ func (m *Meta) loadConfig(rootDir string) (*configs.Config, tfdiags.Diagnostics) return config, diags } +// loadConfigEarly is a variant of loadConfig that uses the special +// "early config" loader that is more forgiving of unexpected constructs and +// legacy syntax. +// +// Early-loaded config is not registered in the source code cache, so +// diagnostics produced from it may render without source code snippets. In +// practice this is not a big concern because the early config loader also +// cannot generate detailed source locations, so it prefers to produce +// diagnostics without explicit source location information and instead includes +// approximate locations in the message text. +// +// Most callers should use loadConfig. This method exists to support early +// initialization use-cases where the root module must be inspected in order +// to determine what else needs to be installed before the full configuration +// can be used +func (m *Meta) loadConfigEarly(rootDir string) (*earlyconfig.Config, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + rootDir = m.normalizePath(rootDir) + + config, hclDiags := initwd.LoadConfig(rootDir, m.modulesDir()) + diags = diags.Append(hclDiags) + return config, diags +} + // loadSingleModule reads configuration from the given directory and returns // a description of that module only, without attempting to assemble a module // tree for referenced child modules. @@ -88,6 +114,31 @@ func (m *Meta) loadSingleModule(dir string) (*configs.Module, tfdiags.Diagnostic return module, diags } +// loadSingleModuleEarly is a variant of loadSingleModule that uses the special +// "early config" loader that is more forgiving of unexpected constructs and +// legacy syntax. +// +// Early-loaded config is not registered in the source code cache, so +// diagnostics produced from it may render without source code snippets. In +// practice this is not a big concern because the early config loader also +// cannot generate detailed source locations, so it prefers to produce +// diagnostics without explicit source location information and instead includes +// approximate locations in the message text. +// +// Most callers should use loadConfig. This method exists to support early +// initialization use-cases where the root module must be inspected in order +// to determine what else needs to be installed before the full configuration +// can be used. +func (m *Meta) loadSingleModuleEarly(dir string) (*tfconfig.Module, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + dir = m.normalizePath(dir) + + module, moreDiags := earlyconfig.LoadModule(dir) + diags = diags.Append(moreDiags) + + return module, diags +} + // dirIsConfigPath checks if the given path is a directory that contains at // least one Terraform configuration file (.tf or .tf.json), returning true // if so. @@ -177,7 +228,7 @@ func (m *Meta) installModules(rootDir string, upgrade bool, hooks initwd.ModuleI } inst := m.moduleInstaller() - moreDiags := inst.InstallModules(rootDir, upgrade, hooks) + _, moreDiags := inst.InstallModules(rootDir, upgrade, hooks) diags = diags.Append(moreDiags) return diags } diff --git a/command/test-fixtures/init-012upgrade/main.tf b/command/test-fixtures/init-012upgrade/main.tf new file mode 100644 index 000000000..5b77095a7 --- /dev/null +++ b/command/test-fixtures/init-012upgrade/main.tf @@ -0,0 +1,10 @@ +resource "null_resource" "foo" { + # This construct trips up the HCL2 parser because it looks like a nested block + # but has quoted keys like a map. The upgrade tool would add an equals sign + # here to turn this into a map attribute, but "terraform init" must first + # be able to install the null provider so the upgrade tool can know that + # "triggers" is a map attribute. + triggers { + "foo" = "bar" + } +} diff --git a/configs/configload/loader.go b/configs/configload/loader.go index cabf19f65..416b48fc8 100644 --- a/configs/configload/loader.go +++ b/configs/configload/loader.go @@ -77,6 +77,24 @@ func (l *Loader) ModulesDir() string { return l.modules.Dir } +// RefreshModules updates the in-memory cache of the module manifest from the +// module manifest file on disk. This is not necessary in normal use because +// module installation and configuration loading are separate steps, but it +// can be useful in tests where module installation is done as a part of +// configuration loading by a helper function. +// +// Call this function after any module installation where an existing loader +// is already alive and may be used again later. +// +// An error is returned if the manifest file cannot be read. +func (l *Loader) RefreshModules() error { + if l == nil { + // Nothing to do, then. + return nil + } + return l.modules.readModuleManifestSnapshot() +} + // Parser returns the underlying parser for this loader. // // This is useful for loading other sorts of files than the module directories diff --git a/configs/configload/loader_load.go b/configs/configload/loader_load.go index 92d09b12c..93a94204f 100644 --- a/configs/configload/loader_load.go +++ b/configs/configload/loader_load.go @@ -24,18 +24,6 @@ func (l *Loader) LoadConfig(rootDir string) (*configs.Config, hcl.Diagnostics) { return nil, diags } - // Refresh the manifest snapshot in case anything new has been installed - // since we last refreshed it. - err := l.modules.readModuleManifestSnapshot() - if err != nil { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Failed to read module manifest", - Detail: fmt.Sprintf("Terraform failed to read its manifest of locally-cached modules: %s.", err), - }) - return nil, diags - } - cfg, cDiags := configs.BuildConfig(rootMod, configs.ModuleWalkerFunc(l.moduleWalkerLoad)) diags = append(diags, cDiags...) diff --git a/helper/resource/testing.go b/helper/resource/testing.go index be0ea13bd..9188d8ec8 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -834,7 +834,7 @@ func testConfig(opts terraform.ContextOpts, step TestStep) (*configs.Config, err } inst := initwd.NewModuleInstaller(modulesDir, nil) - installDiags := inst.InstallModules(cfgPath, true, initwd.ModuleInstallHooksImpl{}) + _, installDiags := inst.InstallModules(cfgPath, true, initwd.ModuleInstallHooksImpl{}) if installDiags.HasErrors() { return nil, installDiags.Err() } diff --git a/internal/earlyconfig/config.go b/internal/earlyconfig/config.go index 7b49c1ecb..32c1327a4 100644 --- a/internal/earlyconfig/config.go +++ b/internal/earlyconfig/config.go @@ -87,7 +87,7 @@ func (c *Config) ProviderDependencies() (*moduledeps.Module, tfdiags.Diagnostics inst := moduledeps.ProviderInstance(name) var constraints version.Constraints for _, reqStr := range reqs { - if reqStr == "" { + if reqStr != "" { constraint, err := version.NewConstraint(reqStr) if err != nil { diags = diags.Append(wrapDiagnostic(tfconfig.Diagnostic{ diff --git a/internal/initwd/doc.go b/internal/initwd/doc.go index 02e372377..b9d938dbb 100644 --- a/internal/initwd/doc.go +++ b/internal/initwd/doc.go @@ -1,5 +1,5 @@ // Package initwd contains various helper functions used by the "terraform init" -// command. +// command to initialize a working directory. // // These functions may also be used from testing code to simulate the behaviors // of "terraform init" against test fixtures, but should not be used elsewhere diff --git a/internal/initwd/from_module.go b/internal/initwd/from_module.go index 63a6dd0af..6b40d08d6 100644 --- a/internal/initwd/from_module.go +++ b/internal/initwd/from_module.go @@ -141,7 +141,7 @@ func DirFromModule(rootDir, modulesDir, sourceAddr string, reg *registry.Client, Wrapped: hooks, } getter := reusingGetter{} - instDiags := inst.installDescendentModules(fakeRootModule, rootDir, instManifest, true, wrapHooks, getter) + _, instDiags := inst.installDescendentModules(fakeRootModule, rootDir, instManifest, true, wrapHooks, getter) diags = append(diags, instDiags...) if instDiags.HasErrors() { return diags diff --git a/internal/initwd/load_config.go b/internal/initwd/load_config.go new file mode 100644 index 000000000..6f77dcd84 --- /dev/null +++ b/internal/initwd/load_config.go @@ -0,0 +1,56 @@ +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/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 b9031aa57..3bb4d0233 100644 --- a/internal/initwd/module_install.go +++ b/internal/initwd/module_install.go @@ -54,16 +54,15 @@ func NewModuleInstaller(modsDir string, reg *registry.Client) *ModuleInstaller { // to find their dependencies, so this function does many of the same checks // as LoadConfig as a side-effect. // -// This function will panic if called on a loader that cannot install modules. -// Use CanInstallModules to determine if a loader can install modules, or -// refer to the documentation for that method for situations where module -// installation capability is guaranteed. -func (i *ModuleInstaller) InstallModules(rootDir string, upgrade bool, hooks ModuleInstallHooks) tfdiags.Diagnostics { +// 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(rootDir string, upgrade bool, hooks ModuleInstallHooks) (*earlyconfig.Config, tfdiags.Diagnostics) { log.Printf("[TRACE] ModuleInstaller: installing child modules for %s into %s", rootDir, i.modsDir) rootMod, diags := earlyconfig.LoadModule(rootDir) if rootMod == nil { - return diags + return nil, diags } manifest, err := modsdir.ReadManifestSnapshotForDir(i.modsDir) @@ -73,17 +72,17 @@ func (i *ModuleInstaller) InstallModules(rootDir string, upgrade bool, hooks Mod "Failed to read modules manifest file", fmt.Sprintf("Error reading manifest for %s: %s.", i.modsDir, err), )) - return diags + return nil, diags } getter := reusingGetter{} - instDiags := i.installDescendentModules(rootMod, rootDir, manifest, upgrade, hooks, getter) + cfg, instDiags := i.installDescendentModules(rootMod, rootDir, manifest, upgrade, hooks, getter) diags = append(diags, instDiags...) - return diags + return cfg, diags } -func (i *ModuleInstaller) installDescendentModules(rootMod *tfconfig.Module, rootDir string, manifest modsdir.Manifest, upgrade bool, hooks ModuleInstallHooks, getter reusingGetter) tfdiags.Diagnostics { +func (i *ModuleInstaller) installDescendentModules(rootMod *tfconfig.Module, rootDir string, manifest modsdir.Manifest, upgrade bool, hooks ModuleInstallHooks, getter reusingGetter) (*earlyconfig.Config, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics if hooks == nil { @@ -98,7 +97,7 @@ func (i *ModuleInstaller) installDescendentModules(rootMod *tfconfig.Module, roo Dir: rootDir, } - _, cDiags := earlyconfig.BuildConfig(rootMod, earlyconfig.ModuleWalkerFunc( + cfg, cDiags := earlyconfig.BuildConfig(rootMod, earlyconfig.ModuleWalkerFunc( func(req *earlyconfig.ModuleRequest) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) { key := manifest.ModuleKey(req.Path) @@ -221,7 +220,7 @@ func (i *ModuleInstaller) installDescendentModules(rootMod *tfconfig.Module, roo )) } - return diags + return cfg, diags } func (i *ModuleInstaller) installLocalModule(req *earlyconfig.ModuleRequest, key string, manifest modsdir.Manifest, hooks ModuleInstallHooks) (*tfconfig.Module, tfdiags.Diagnostics) { diff --git a/internal/initwd/module_install_test.go b/internal/initwd/module_install_test.go index 4cce4071a..13c10c181 100644 --- a/internal/initwd/module_install_test.go +++ b/internal/initwd/module_install_test.go @@ -41,7 +41,7 @@ func TestModuleInstaller(t *testing.T) { modulesDir := filepath.Join(dir, ".terraform/modules") inst := NewModuleInstaller(modulesDir, nil) - diags := inst.InstallModules(".", false, hooks) + _, diags := inst.InstallModules(".", false, hooks) assertNoDiagnostics(t, diags) wantCalls := []testInstallHookCall{ @@ -105,7 +105,7 @@ func TestLoaderInstallModules_registry(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") inst := NewModuleInstaller(modulesDir, registry.NewClient(nil, nil)) - diags := inst.InstallModules(dir, false, hooks) + _, diags := inst.InstallModules(dir, false, hooks) assertNoDiagnostics(t, diags) v := version.Must(version.NewVersion("0.0.1")) @@ -232,7 +232,7 @@ func TestLoaderInstallModules_goGetter(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") inst := NewModuleInstaller(modulesDir, registry.NewClient(nil, nil)) - diags := inst.InstallModules(dir, false, hooks) + _, diags := inst.InstallModules(dir, false, hooks) assertNoDiagnostics(t, diags) wantCalls := []testInstallHookCall{ diff --git a/internal/initwd/testing.go b/internal/initwd/testing.go index 072d42a10..8cef80a35 100644 --- a/internal/initwd/testing.go +++ b/internal/initwd/testing.go @@ -35,7 +35,7 @@ func LoadConfigForTests(t *testing.T, rootDir string) (*configs.Config, *configl loader, cleanup := configload.NewLoaderForTests(t) inst := NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) - moreDiags := inst.InstallModules(rootDir, true, ModuleInstallHooksImpl{}) + _, moreDiags := inst.InstallModules(rootDir, true, ModuleInstallHooksImpl{}) diags = diags.Append(moreDiags) if diags.HasErrors() { cleanup() @@ -43,6 +43,12 @@ func LoadConfigForTests(t *testing.T, rootDir string) (*configs.Config, *configl return nil, nil, func() {}, diags } + // Since module installer has modified the module manifest on disk, we need + // to refresh the cache of it in the loader. + if err := loader.RefreshModules(); err != nil { + t.Fatalf("failed to refresh modules after installation: %s", err) + } + config, hclDiags := loader.LoadConfig(rootDir) diags = diags.Append(hclDiags) return config, loader, cleanup, diags diff --git a/internal/initwd/version_required.go b/internal/initwd/version_required.go new file mode 100644 index 000000000..104840b93 --- /dev/null +++ b/internal/initwd/version_required.go @@ -0,0 +1,83 @@ +package initwd + +import ( + "fmt" + + version "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform/internal/earlyconfig" + "github.com/hashicorp/terraform/tfdiags" + tfversion "github.com/hashicorp/terraform/version" +) + +// CheckCoreVersionRequirements visits each of the modules in the given +// early configuration tree and verifies that any given Core version constraints +// match with the version of Terraform Core that is being used. +// +// The returned diagnostics will contain errors if any constraints do not match. +// The returned diagnostics might also return warnings, which should be +// displayed to the user. +func CheckCoreVersionRequirements(earlyConfig *earlyconfig.Config) tfdiags.Diagnostics { + if earlyConfig == nil { + return nil + } + + var diags tfdiags.Diagnostics + module := earlyConfig.Module + + var constraints version.Constraints + for _, constraintStr := range module.RequiredCore { + constraint, err := version.NewConstraint(constraintStr) + if err != nil { + // Unfortunately the early config parser doesn't preserve a source + // location for this, so we're unable to indicate a specific + // location where this constraint came from, but we can at least + // say which module set it. + switch { + case len(earlyConfig.Path) == 0: + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid provider version constraint", + fmt.Sprintf("Invalid version core constraint %q in the root module.", constraintStr), + )) + default: + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid provider version constraint", + fmt.Sprintf("Invalid version core constraint %q in %s.", constraintStr, earlyConfig.Path), + )) + } + continue + } + constraints = append(constraints, constraint...) + } + + if !constraints.Check(tfversion.SemVer) { + switch { + case len(earlyConfig.Path) == 0: + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Unsupported Terraform Core version", + fmt.Sprintf( + "This configuration does not support Terraform version %s. To proceed, either choose another supported Terraform version or update the root module's version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected behavior.", + tfversion.String(), + ), + )) + default: + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Unsupported Terraform Core version", + fmt.Sprintf( + "Module %s (from %q) does not support Terraform version %s. To proceed, either choose another supported Terraform version or update the module's version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected behavior.", + earlyConfig.Path, earlyConfig.SourceAddr, tfversion.String(), + ), + )) + } + } + + for _, c := range earlyConfig.Children { + childDiags := CheckCoreVersionRequirements(c) + diags = diags.Append(childDiags) + } + + return diags +} diff --git a/repl/session_test.go b/repl/session_test.go index 425fcdc0f..2f35c4859 100644 --- a/repl/session_test.go +++ b/repl/session_test.go @@ -2,7 +2,6 @@ package repl import ( "flag" - "github.com/hashicorp/terraform/internal/initwd" "io/ioutil" "log" "os" @@ -14,6 +13,7 @@ import ( "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/helper/logging" + "github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/terraform" diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 2407bf18f..e63737ea3 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -2,8 +2,6 @@ package terraform import ( "flag" - "github.com/hashicorp/terraform/internal/initwd" - "github.com/hashicorp/terraform/registry" "io" "io/ioutil" "log" @@ -22,9 +20,11 @@ import ( "github.com/hashicorp/terraform/configs/configload" "github.com/hashicorp/terraform/helper/experiment" "github.com/hashicorp/terraform/helper/logging" + "github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/provisioners" + "github.com/hashicorp/terraform/registry" "github.com/hashicorp/terraform/states" ) @@ -113,11 +113,17 @@ func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *config // 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)) - instDiags := inst.InstallModules(dir, true, initwd.ModuleInstallHooksImpl{}) + _, instDiags := inst.InstallModules(dir, true, initwd.ModuleInstallHooksImpl{}) if instDiags.HasErrors() { t.Fatal(instDiags.Err()) } + // Since module installer has modified the module manifest on disk, we need + // to refresh the cache of it in the loader. + if err := loader.RefreshModules(); err != nil { + t.Fatalf("failed to refresh modules after installation: %s", err) + } + config, snap, diags := loader.LoadConfigWithSnapshot(dir) if diags.HasErrors() { t.Fatal(diags.Error()) @@ -165,11 +171,17 @@ func testModuleInline(t *testing.T, sources map[string]string) *configs.Config { // 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)) - instDiags := inst.InstallModules(cfgPath, true, initwd.ModuleInstallHooksImpl{}) + _, instDiags := inst.InstallModules(cfgPath, true, initwd.ModuleInstallHooksImpl{}) if instDiags.HasErrors() { t.Fatal(instDiags.Err()) } + // Since module installer has modified the module manifest on disk, we need + // to refresh the cache of it in the loader. + if err := loader.RefreshModules(); err != nil { + t.Fatalf("failed to refresh modules after installation: %s", err) + } + config, diags := loader.LoadConfig(cfgPath) if diags.HasErrors() { t.Fatal(diags.Error())