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:
parent
0c0a437bcb
commit
86c02d5c35
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
682
command/init.go
682
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,115 +167,134 @@ func (c *InitCommand) Run(args []string) int {
|
|||
return 0
|
||||
}
|
||||
|
||||
// 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 _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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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 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() {
|
||||
// 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.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 {
|
||||
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
|
||||
|
||||
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()
|
||||
back = be
|
||||
}
|
||||
|
||||
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)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
c.Ui.Error(strings.TrimSpace(errInitConfigError))
|
||||
c.showDiagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
confDiags = terraform.CheckCoreVersionRequirements(config)
|
||||
diags = diags.Append(confDiags)
|
||||
if confDiags.HasErrors() {
|
||||
c.showDiagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
if back == nil {
|
||||
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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...)
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
},
|
||||
))
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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())
|
||||
|
|
Loading…
Reference in New Issue