command/init: Use full config for provider reqs

Relying on the early config for provider requirements was necessary in
Terraform 0.12, to allow the 0.12upgrade command to run after init
installs providers.

However in 0.13, the same restrictions do not apply, and the detection
of provider requirements has changed. As a result, the early config
loader gives incorrect provider requirements in some circumstances,
such as those in the new test in this commit.

Therefore we are changing the init command to use the requirements found
by the full configuration loader. This also means that we can remove the
internal initwd CheckCoreVersionRequirements function.
This commit is contained in:
Alisdair McDiarmid 2020-05-25 16:38:01 -04:00
parent df39e0a806
commit 62d826e066
4 changed files with 84 additions and 112 deletions

View File

@ -18,11 +18,10 @@ import (
backendInit "github.com/hashicorp/terraform/backend/init"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/internal/earlyconfig"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/internal/providercache"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
tfversion "github.com/hashicorp/terraform/version"
)
@ -199,21 +198,7 @@ func (c *InitCommand) Run(args []string) int {
// 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.
earlyConfig, earlyConfDiags := c.loadConfigEarly(path)
if earlyConfDiags.HasErrors() {
c.Ui.Error(strings.TrimSpace(errInitConfigError))
diags = diags.Append(earlyConfDiags)
c.showDiagnostics(diags)
return 1
}
_, confDiags = c.loadConfig(path)
config, confDiags := c.loadConfig(path)
diags = diags.Append(confDiags)
if confDiags.HasErrors() {
c.Ui.Error(strings.TrimSpace(errInitConfigError))
@ -225,7 +210,7 @@ func (c *InitCommand) Run(args []string) int {
// 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)
versionDiags := terraform.CheckCoreVersionRequirements(config)
diags = diags.Append(versionDiags)
if versionDiags.HasErrors() {
c.showDiagnostics(diags)
@ -294,7 +279,7 @@ func (c *InitCommand) Run(args []string) int {
}
// Now that we have loaded all modules, check the module tree for missing providers.
providersOutput, providerDiags := c.getProviders(earlyConfig, state, flagUpgrade, flagPluginPath)
providersOutput, providerDiags := c.getProviders(config, state, flagUpgrade, flagPluginPath)
diags = diags.Append(providerDiags)
if providerDiags.HasErrors() {
c.showDiagnostics(diags)
@ -425,10 +410,10 @@ the backend configuration is present and valid.
// 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, pluginDirs []string) (output bool, diags tfdiags.Diagnostics) {
func (c *InitCommand) getProviders(config *configs.Config, state *states.State, upgrade bool, pluginDirs []string) (output bool, diags tfdiags.Diagnostics) {
// First we'll collect all the provider dependencies we can see in the
// configuration and the state.
reqs, moreDiags := earlyConfig.ProviderRequirements()
reqs, moreDiags := config.ProviderRequirements()
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return false, diags

View File

@ -849,6 +849,55 @@ func TestInit_getProvider(t *testing.T) {
})
}
func TestInit_getProviderSource(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("init-get-provider-source"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
overrides := metaOverridesForProvider(testProvider())
ui := new(cli.MockUi)
providerSource, close := newMockProviderSource(t, map[string][]string{
// looking for an exact version
"acme/alpha": []string{"1.2.3"},
// config doesn't specify versions for other providers
"registry.example.com/acme/beta": []string{"1.0.0"},
"gamma": []string{"2.0.0"},
})
defer close()
m := Meta{
testingOverrides: overrides,
Ui: ui,
ProviderSource: providerSource,
}
c := &InitCommand{
Meta: m,
}
args := []string{
"-backend=false", // should be possible to install plugins without backend init
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
// check that we got the providers for our config
exactPath := fmt.Sprintf(".terraform/plugins/registry.terraform.io/acme/alpha/1.2.3/%s", getproviders.CurrentPlatform)
if _, err := os.Stat(exactPath); os.IsNotExist(err) {
t.Fatal("provider 'alpha' not downloaded")
}
greaterThanPath := fmt.Sprintf(".terraform/plugins/registry.example.com/acme/beta/1.0.0/%s", getproviders.CurrentPlatform)
if _, err := os.Stat(greaterThanPath); os.IsNotExist(err) {
t.Fatal("provider 'beta' not downloaded")
}
betweenPath := fmt.Sprintf(".terraform/plugins/registry.terraform.io/hashicorp/gamma/2.0.0/%s", getproviders.CurrentPlatform)
if _, err := os.Stat(betweenPath); os.IsNotExist(err) {
t.Fatal("provider 'gamma' not downloaded")
}
}
func TestInit_providerSource(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
@ -1526,10 +1575,10 @@ func TestInit_syntaxErrorUpgradeHint(t *testing.T) {
// longer needed, at which point it will clean up all of the temporary files
// and the packages in the source will no longer be available for installation.
//
// For ease of use in the common case, this function just treats all of the
// provider given names as "default" providers under
// registry.terraform.io/hashicorp . If you need more control over the
// provider addresses, construct a getproviders.MockSource directly instead.
// Provider addresses must be valid source strings, and passing only the
// provider name will be interpreted as a "default" provider under
// registry.terraform.io/hashicorp. If you need more control over the
// provider addresses, pass a full provider source string.
//
// This function also registers providers as belonging to the current platform,
// to ensure that they will be available to a provider installer operating in
@ -1548,18 +1597,18 @@ func newMockProviderSource(t *testing.T, availableProviderVersions map[string][]
f()
}
}
for name, versions := range availableProviderVersions {
addr := addrs.NewDefaultProvider(name)
for source, versions := range availableProviderVersions {
addr := addrs.MustParseProviderSourceString(source)
for _, versionStr := range versions {
version, err := getproviders.ParseVersion(versionStr)
if err != nil {
close()
t.Fatalf("failed to parse %q as a version number for %q: %s", versionStr, name, err)
t.Fatalf("failed to parse %q as a version number for %q: %s", versionStr, addr.ForDisplay(), err)
}
meta, close, err := getproviders.FakeInstallablePackageMeta(addr, version, getproviders.VersionList{getproviders.MustParseVersion("5.0")}, getproviders.CurrentPlatform)
if err != nil {
close()
t.Fatalf("failed to prepare fake package for %s %s: %s", name, versionStr, err)
t.Fatalf("failed to prepare fake package for %s %s: %s", addr.ForDisplay(), versionStr, err)
}
closes = append(closes, close)
packages = append(packages, meta)

View File

@ -0,0 +1,21 @@
provider alpha {
version = "1.2.3"
}
resource beta_resource b {}
resource gamma_resource g {}
terraform {
required_providers {
alpha = {
source = "acme/alpha"
}
beta = {
source = "registry.example.com/acme/beta"
}
}
}
provider beta {
region = "foo"
}

View File

@ -1,83 +0,0 @@
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
}