command: "terraform init" can partially initialize for 0.12upgrade

There are a few constructs from 0.11 and prior that cause 0.12 parsing to
fail altogether, which previously created a chicken/egg problem because
we need to install the providers in order to run "terraform 0.12upgrade"
and thus fix the problem.

This changes "terraform init" to use the new "early configuration" loader
for module and provider installation. This is built on the more permissive
parser in the terraform-config-inspect package, and so it allows us to
read out the top-level blocks from the configuration while accepting
legacy HCL syntax.

In the long run this will let us do version compatibility detection before
attempting a "real" config load, giving us better error messages for any
future syntax additions, but in the short term the key thing is that it
allows us to install the dependencies even if the configuration isn't
fully valid.

Because backend init still requires full configuration, this introduces a
new mode of terraform init where it detects heuristically if it seems like
we need to do a configuration upgrade and does a partial init if so,
before finally directing the user to run "terraform 0.12upgrade" before
running any other commands.

The heuristic here is based on two assumptions:
- If the "early" loader finds no errors but the normal loader does, the
  configuration is likely to be valid for Terraform 0.11 but not 0.12.
- If there's already a version constraint in the configuration that
  excludes Terraform versions prior to v0.12 then the configuration is
  probably _already_ upgraded and so it's just a normal syntax error,
  even if the early loader didn't detect it.

Once the upgrade process is removed in 0.13.0 (users will be required to
go stepwise 0.11 -> 0.12 -> 0.13 to upgrade after that), some of this can
be simplified to remove that special mode, but the idea of doing the
dependency version checks against the liberal parser will remain valuable
to increase our chances of reporting version-based incompatibilities
rather than syntax errors as we add new features in future.
This commit is contained in:
Martin Atkins 2019-01-14 11:11:00 -08:00
parent 0c0a437bcb
commit 86c02d5c35
19 changed files with 755 additions and 300 deletions

View File

@ -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 // sources only this ultimately just records all of the module paths
// in a JSON file so that we can load them below. // 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(), registry.NewClient(nil, nil))
instDiags := inst.InstallModules(dir, true, initwd.ModuleInstallHooksImpl{}) _, instDiags := inst.InstallModules(dir, true, initwd.ModuleInstallHooksImpl{})
if instDiags.HasErrors() { if instDiags.HasErrors() {
t.Fatal(instDiags.Err()) t.Fatal(instDiags.Err())
} }

View File

@ -801,14 +801,14 @@ func TestImport_pluginDir(t *testing.T) {
initCmd := &InitCommand{ initCmd := &InitCommand{
Meta: Meta{ Meta: Meta{
pluginPath: []string{"./plugins"}, pluginPath: []string{"./plugins"},
Ui: new(cli.MockUi), Ui: cli.NewMockUi(),
}, },
providerInstaller: &discovery.ProviderInstaller{ providerInstaller: &discovery.ProviderInstaller{
PluginProtocolVersion: plugin.Handshake.ProtocolVersion, PluginProtocolVersion: plugin.Handshake.ProtocolVersion,
}, },
} }
if err := initCmd.getProviders(".", nil, false); err != nil { if code := initCmd.Run(nil); code != 0 {
t.Fatal(err) t.Fatal(initCmd.Meta.Ui.(*cli.MockUi).ErrorWriter.String())
} }
args := []string{ args := []string{

View File

@ -8,20 +8,23 @@ import (
"strings" "strings"
"github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/terraform-config-inspect/tfconfig"
"github.com/posener/complete" "github.com/posener/complete"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/backend"
backendInit "github.com/hashicorp/terraform/backend/init"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/configs/configschema" "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"
"github.com/hashicorp/terraform/plugin/discovery" "github.com/hashicorp/terraform/plugin/discovery"
"github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/tfdiags"
backendInit "github.com/hashicorp/terraform/backend/init"
) )
// InitCommand is a Command implementation that takes a Terraform // InitCommand is a Command implementation that takes a Terraform
@ -164,116 +167,135 @@ func (c *InitCommand) Run(args []string) int {
return 0 return 0
} }
var back backend.Backend // Before we do anything else, we'll try loading configuration with both
// our "normal" and "early" configuration codepaths. If early succeeds
// If we're performing a get or loading the backend, then we perform // while normal fails, that strongly suggests that the configuration is
// some extra tasks. // using syntax that worked in 0.11 but no longer in 0.12, which requires
if flagGet || flagBackend { // some special behavior here to get the directory initialized just enough
config, confDiags := c.loadSingleModule(path) // to run "terraform 0.12upgrade".
diags = diags.Append(confDiags) //
if confDiags.HasErrors() { // 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, // Since this may be the user's first ever interaction with Terraform,
// we'll provide some additional context in this case. // we'll provide some additional context in this case.
c.Ui.Error(strings.TrimSpace(errInitConfigError)) c.Ui.Error(strings.TrimSpace(errInitConfigError))
diags = diags.Append(confDiags)
c.showDiagnostics(diags) c.showDiagnostics(diags)
return 1 return 1
} }
// If we requested downloading modules and have modules in the config // If _only_ the main loader produced errors then that suggests an
if flagGet && len(config.ModuleCalls) > 0 { // upgrade may help. To give us more certainty here, we'll use the
header = true // same heuristic that "terraform 0.12upgrade" uses to guess if a
// configuration has already been upgraded, to reduce the risk that
if flagUpgrade { // we'll produce a misleading message if the problem is just a regular
c.Ui.Output(c.Colorize().Color(fmt.Sprintf("[reset][bold]Upgrading modules..."))) // syntax error that the early loader just didn't catch.
} else { sources, err := configupgrade.LoadModule(path)
c.Ui.Output(c.Colorize().Color(fmt.Sprintf("[reset][bold]Initializing modules..."))) if err == nil {
} if already, _ := sources.MaybeAlreadyUpgraded(); already {
// Just report the errors as normal, then.
hooks := uiModuleInstallHooks{ c.Ui.Error(strings.TrimSpace(errInitConfigError))
Ui: c.Ui, diags = diags.Append(confDiags)
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() {
c.showDiagnostics(diags) c.showDiagnostics(diags)
return 1 return 1
} }
} }
configUpgradeProbablyNeeded = true
} }
if earlyConfDiags.HasErrors() {
// With modules now installed, we should be able to load the whole // If _only_ the early loader encountered errors then that's unusual
// configuration and check the core version constraints. // (it should generally be a superset of the normal loader) but we'll
config, confDiags := c.loadConfig(path) // return those errors anyway since otherwise we'll probably get
diags = diags.Append(confDiags) // some weird behavior downstream. Errors from the early loader are
if confDiags.HasErrors() { // generally not as high-quality since it has less context to work with.
// 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)) c.Ui.Error(strings.TrimSpace(errInitConfigError))
diags = diags.Append(earlyConfDiags)
c.showDiagnostics(diags) c.showDiagnostics(diags)
return 1 return 1
} }
confDiags = terraform.CheckCoreVersionRequirements(config)
diags = diags.Append(confDiags) if flagGet {
if confDiags.HasErrors() { 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) c.showDiagnostics(diags)
return 1 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 back == nil {
// If we didn't initialize a backend then we'll try to at least // 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. // 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) diags = diags.Append(providerDiags)
if providerDiags.HasErrors() { if providerDiags.HasErrors() {
c.showDiagnostics(diags) c.showDiagnostics(diags)
return 1 return 1
} }
if providersOutput {
header = true
}
// If we outputted information, then we need to output a newline // If we outputted information, then we need to output a newline
// so that our success message is nicely spaced out from prior text. // 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. // still the final thing shown.
c.showDiagnostics(diags) 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))) c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitSuccess)))
if !c.RunningInAutomation { if !c.RunningInAutomation {
// If we're not running in an automation wrapper, give the user // 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 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 // backendConfigOverrideBody interprets the raw values of -backend-config
// arguments into a hcl Body that should override the backend settings given // arguments into a hcl Body that should override the backend settings given
// in the configuration. // in the configuration.
@ -411,168 +689,6 @@ func (c *InitCommand) backendConfigOverrideBody(flags rawFlags, schema *configsc
return ret, diags 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 { func (c *InitCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictDirs("") 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. 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 = ` const outputInitProvidersUnconstrained = `
The following providers do not have any version constraints in configuration, The following providers do not have any version constraints in configuration,
so the latest version was installed. so the latest version was installed.

View File

@ -3,6 +3,7 @@ package command
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -266,7 +267,9 @@ func TestInit_backendUnset(t *testing.T) {
defer testChdir(t, td)() defer testChdir(t, td)()
{ {
ui := new(cli.MockUi) log.Printf("[TRACE] TestInit_backendUnset: beginning first init")
ui := cli.NewMockUi()
c := &InitCommand{ c := &InitCommand{
Meta: Meta{ Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()), testingOverrides: metaOverridesForProvider(testProvider()),
@ -279,6 +282,9 @@ func TestInit_backendUnset(t *testing.T) {
if code := c.Run(args); code != 0 { if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 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 { if _, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename)); err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -286,12 +292,14 @@ func TestInit_backendUnset(t *testing.T) {
} }
{ {
log.Printf("[TRACE] TestInit_backendUnset: beginning second init")
// Unset // Unset
if err := ioutil.WriteFile("main.tf", []byte(""), 0644); err != nil { if err := ioutil.WriteFile("main.tf", []byte(""), 0644); err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
ui := new(cli.MockUi) ui := cli.NewMockUi()
c := &InitCommand{ c := &InitCommand{
Meta: Meta{ Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()), testingOverrides: metaOverridesForProvider(testProvider()),
@ -303,6 +311,9 @@ func TestInit_backendUnset(t *testing.T) {
if code := c.Run(args); code != 0 { if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 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)) s := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename))
if !s.Backend.Empty() { if !s.Backend.Empty() {
@ -1220,3 +1231,80 @@ func TestInit_pluginWithInternal(t *testing.T) {
t.Fatalf("error: %s", ui.ErrorWriter) 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)
}
}

View File

@ -2,12 +2,14 @@ package command
import ( import (
"fmt" "fmt"
"github.com/hashicorp/terraform/internal/earlyconfig"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax" "github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/terraform-config-inspect/tfconfig"
"github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/configs/configload" "github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/configs/configschema"
@ -65,6 +67,30 @@ func (m *Meta) loadConfig(rootDir string) (*configs.Config, tfdiags.Diagnostics)
return config, diags 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 // loadSingleModule reads configuration from the given directory and returns
// a description of that module only, without attempting to assemble a module // a description of that module only, without attempting to assemble a module
// tree for referenced child modules. // tree for referenced child modules.
@ -88,6 +114,31 @@ func (m *Meta) loadSingleModule(dir string) (*configs.Module, tfdiags.Diagnostic
return module, diags 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 // dirIsConfigPath checks if the given path is a directory that contains at
// least one Terraform configuration file (.tf or .tf.json), returning true // least one Terraform configuration file (.tf or .tf.json), returning true
// if so. // if so.
@ -177,7 +228,7 @@ func (m *Meta) installModules(rootDir string, upgrade bool, hooks initwd.ModuleI
} }
inst := m.moduleInstaller() inst := m.moduleInstaller()
moreDiags := inst.InstallModules(rootDir, upgrade, hooks) _, moreDiags := inst.InstallModules(rootDir, upgrade, hooks)
diags = diags.Append(moreDiags) diags = diags.Append(moreDiags)
return diags return diags
} }

View File

@ -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"
}
}

View File

@ -77,6 +77,24 @@ func (l *Loader) ModulesDir() string {
return l.modules.Dir 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. // Parser returns the underlying parser for this loader.
// //
// This is useful for loading other sorts of files than the module directories // This is useful for loading other sorts of files than the module directories

View File

@ -24,18 +24,6 @@ func (l *Loader) LoadConfig(rootDir string) (*configs.Config, hcl.Diagnostics) {
return nil, diags 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)) cfg, cDiags := configs.BuildConfig(rootMod, configs.ModuleWalkerFunc(l.moduleWalkerLoad))
diags = append(diags, cDiags...) diags = append(diags, cDiags...)

View File

@ -834,7 +834,7 @@ func testConfig(opts terraform.ContextOpts, step TestStep) (*configs.Config, err
} }
inst := initwd.NewModuleInstaller(modulesDir, nil) inst := initwd.NewModuleInstaller(modulesDir, nil)
installDiags := inst.InstallModules(cfgPath, true, initwd.ModuleInstallHooksImpl{}) _, installDiags := inst.InstallModules(cfgPath, true, initwd.ModuleInstallHooksImpl{})
if installDiags.HasErrors() { if installDiags.HasErrors() {
return nil, installDiags.Err() return nil, installDiags.Err()
} }

View File

@ -87,7 +87,7 @@ func (c *Config) ProviderDependencies() (*moduledeps.Module, tfdiags.Diagnostics
inst := moduledeps.ProviderInstance(name) inst := moduledeps.ProviderInstance(name)
var constraints version.Constraints var constraints version.Constraints
for _, reqStr := range reqs { for _, reqStr := range reqs {
if reqStr == "" { if reqStr != "" {
constraint, err := version.NewConstraint(reqStr) constraint, err := version.NewConstraint(reqStr)
if err != nil { if err != nil {
diags = diags.Append(wrapDiagnostic(tfconfig.Diagnostic{ diags = diags.Append(wrapDiagnostic(tfconfig.Diagnostic{

View File

@ -1,5 +1,5 @@
// Package initwd contains various helper functions used by the "terraform init" // 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 // 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 // of "terraform init" against test fixtures, but should not be used elsewhere

View File

@ -141,7 +141,7 @@ func DirFromModule(rootDir, modulesDir, sourceAddr string, reg *registry.Client,
Wrapped: hooks, Wrapped: hooks,
} }
getter := reusingGetter{} getter := reusingGetter{}
instDiags := inst.installDescendentModules(fakeRootModule, rootDir, instManifest, true, wrapHooks, getter) _, instDiags := inst.installDescendentModules(fakeRootModule, rootDir, instManifest, true, wrapHooks, getter)
diags = append(diags, instDiags...) diags = append(diags, instDiags...)
if instDiags.HasErrors() { if instDiags.HasErrors() {
return diags return diags

View File

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

View File

@ -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 // to find their dependencies, so this function does many of the same checks
// as LoadConfig as a side-effect. // as LoadConfig as a side-effect.
// //
// This function will panic if called on a loader that cannot install modules. // If successful (the returned diagnostics contains no errors) then the
// Use CanInstallModules to determine if a loader can install modules, or // first return value is the early configuration tree that was constructed by
// refer to the documentation for that method for situations where module // the installation process.
// installation capability is guaranteed. func (i *ModuleInstaller) InstallModules(rootDir string, upgrade bool, hooks ModuleInstallHooks) (*earlyconfig.Config, tfdiags.Diagnostics) {
func (i *ModuleInstaller) InstallModules(rootDir string, upgrade bool, hooks ModuleInstallHooks) tfdiags.Diagnostics {
log.Printf("[TRACE] ModuleInstaller: installing child modules for %s into %s", rootDir, i.modsDir) log.Printf("[TRACE] ModuleInstaller: installing child modules for %s into %s", rootDir, i.modsDir)
rootMod, diags := earlyconfig.LoadModule(rootDir) rootMod, diags := earlyconfig.LoadModule(rootDir)
if rootMod == nil { if rootMod == nil {
return diags return nil, diags
} }
manifest, err := modsdir.ReadManifestSnapshotForDir(i.modsDir) 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", "Failed to read modules manifest file",
fmt.Sprintf("Error reading manifest for %s: %s.", i.modsDir, err), fmt.Sprintf("Error reading manifest for %s: %s.", i.modsDir, err),
)) ))
return diags return nil, diags
} }
getter := reusingGetter{} 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...) 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 var diags tfdiags.Diagnostics
if hooks == nil { if hooks == nil {
@ -98,7 +97,7 @@ func (i *ModuleInstaller) installDescendentModules(rootMod *tfconfig.Module, roo
Dir: rootDir, 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) { func(req *earlyconfig.ModuleRequest) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) {
key := manifest.ModuleKey(req.Path) 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) { func (i *ModuleInstaller) installLocalModule(req *earlyconfig.ModuleRequest, key string, manifest modsdir.Manifest, hooks ModuleInstallHooks) (*tfconfig.Module, tfdiags.Diagnostics) {

View File

@ -41,7 +41,7 @@ func TestModuleInstaller(t *testing.T) {
modulesDir := filepath.Join(dir, ".terraform/modules") modulesDir := filepath.Join(dir, ".terraform/modules")
inst := NewModuleInstaller(modulesDir, nil) inst := NewModuleInstaller(modulesDir, nil)
diags := inst.InstallModules(".", false, hooks) _, diags := inst.InstallModules(".", false, hooks)
assertNoDiagnostics(t, diags) assertNoDiagnostics(t, diags)
wantCalls := []testInstallHookCall{ wantCalls := []testInstallHookCall{
@ -105,7 +105,7 @@ func TestLoaderInstallModules_registry(t *testing.T) {
hooks := &testInstallHooks{} hooks := &testInstallHooks{}
modulesDir := filepath.Join(dir, ".terraform/modules") modulesDir := filepath.Join(dir, ".terraform/modules")
inst := NewModuleInstaller(modulesDir, registry.NewClient(nil, nil)) inst := NewModuleInstaller(modulesDir, registry.NewClient(nil, nil))
diags := inst.InstallModules(dir, false, hooks) _, diags := inst.InstallModules(dir, false, hooks)
assertNoDiagnostics(t, diags) assertNoDiagnostics(t, diags)
v := version.Must(version.NewVersion("0.0.1")) v := version.Must(version.NewVersion("0.0.1"))
@ -232,7 +232,7 @@ func TestLoaderInstallModules_goGetter(t *testing.T) {
hooks := &testInstallHooks{} hooks := &testInstallHooks{}
modulesDir := filepath.Join(dir, ".terraform/modules") modulesDir := filepath.Join(dir, ".terraform/modules")
inst := NewModuleInstaller(modulesDir, registry.NewClient(nil, nil)) inst := NewModuleInstaller(modulesDir, registry.NewClient(nil, nil))
diags := inst.InstallModules(dir, false, hooks) _, diags := inst.InstallModules(dir, false, hooks)
assertNoDiagnostics(t, diags) assertNoDiagnostics(t, diags)
wantCalls := []testInstallHookCall{ wantCalls := []testInstallHookCall{

View File

@ -35,7 +35,7 @@ func LoadConfigForTests(t *testing.T, rootDir string) (*configs.Config, *configl
loader, cleanup := configload.NewLoaderForTests(t) loader, cleanup := configload.NewLoaderForTests(t)
inst := NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) inst := NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil))
moreDiags := inst.InstallModules(rootDir, true, ModuleInstallHooksImpl{}) _, moreDiags := inst.InstallModules(rootDir, true, ModuleInstallHooksImpl{})
diags = diags.Append(moreDiags) diags = diags.Append(moreDiags)
if diags.HasErrors() { if diags.HasErrors() {
cleanup() cleanup()
@ -43,6 +43,12 @@ func LoadConfigForTests(t *testing.T, rootDir string) (*configs.Config, *configl
return nil, nil, func() {}, diags 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) config, hclDiags := loader.LoadConfig(rootDir)
diags = diags.Append(hclDiags) diags = diags.Append(hclDiags)
return config, loader, cleanup, diags return config, loader, cleanup, diags

View File

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

View File

@ -2,7 +2,6 @@ package repl
import ( import (
"flag" "flag"
"github.com/hashicorp/terraform/internal/initwd"
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
@ -14,6 +13,7 @@ import (
"github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/helper/logging" "github.com/hashicorp/terraform/helper/logging"
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"

View File

@ -2,8 +2,6 @@ package terraform
import ( import (
"flag" "flag"
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/registry"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
@ -22,9 +20,11 @@ import (
"github.com/hashicorp/terraform/configs/configload" "github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/helper/experiment" "github.com/hashicorp/terraform/helper/experiment"
"github.com/hashicorp/terraform/helper/logging" "github.com/hashicorp/terraform/helper/logging"
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/provisioners" "github.com/hashicorp/terraform/provisioners"
"github.com/hashicorp/terraform/registry"
"github.com/hashicorp/terraform/states" "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 // sources only this ultimately just records all of the module paths
// in a JSON file so that we can load them below. // 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(), registry.NewClient(nil, nil))
instDiags := inst.InstallModules(dir, true, initwd.ModuleInstallHooksImpl{}) _, instDiags := inst.InstallModules(dir, true, initwd.ModuleInstallHooksImpl{})
if instDiags.HasErrors() { if instDiags.HasErrors() {
t.Fatal(instDiags.Err()) 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) config, snap, diags := loader.LoadConfigWithSnapshot(dir)
if diags.HasErrors() { if diags.HasErrors() {
t.Fatal(diags.Error()) 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 // sources only this ultimately just records all of the module paths
// in a JSON file so that we can load them below. // 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(), registry.NewClient(nil, nil))
instDiags := inst.InstallModules(cfgPath, true, initwd.ModuleInstallHooksImpl{}) _, instDiags := inst.InstallModules(cfgPath, true, initwd.ModuleInstallHooksImpl{})
if instDiags.HasErrors() { if instDiags.HasErrors() {
t.Fatal(instDiags.Err()) 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) config, diags := loader.LoadConfig(cfgPath)
if diags.HasErrors() { if diags.HasErrors() {
t.Fatal(diags.Error()) t.Fatal(diags.Error())