From 62d826e066817f1c754236a3fd7e88fa88014bb7 Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Mon, 25 May 2020 16:38:01 -0400 Subject: [PATCH] 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. --- command/init.go | 27 ++---- command/init_test.go | 65 +++++++++++++-- .../testdata/init-get-provider-source/main.tf | 21 +++++ internal/initwd/version_required.go | 83 ------------------- 4 files changed, 84 insertions(+), 112 deletions(-) create mode 100644 command/testdata/init-get-provider-source/main.tf delete mode 100644 internal/initwd/version_required.go diff --git a/command/init.go b/command/init.go index 21e9959ca..9a4be9a9e 100644 --- a/command/init.go +++ b/command/init.go @@ -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 diff --git a/command/init_test.go b/command/init_test.go index c70d97793..1e37a5ebe 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -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) diff --git a/command/testdata/init-get-provider-source/main.tf b/command/testdata/init-get-provider-source/main.tf new file mode 100644 index 000000000..a434a9788 --- /dev/null +++ b/command/testdata/init-get-provider-source/main.tf @@ -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" +} diff --git a/internal/initwd/version_required.go b/internal/initwd/version_required.go deleted file mode 100644 index 104840b93..000000000 --- a/internal/initwd/version_required.go +++ /dev/null @@ -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 -}