mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-21 10:47:34 -05:00
Recently we added a call to Locks.UpgradeFromPredecessorProject to try to preserve dependency selections made for providers under registry.terraform.io/hashicorp/* when switching to OpenTofu for the first time. However, this behavior did not properly cater for the situation where the configuration intentionally specifies registry.terraform.io explicitly in a source address: that would then cause OpenTofu to incorrectly try to make a factory function for the shimmed provider version when working in command.Meta.providerFactories, which would then fail because no such provider appears in the cache directory. Instead then, we'll limit the shimming only to installation-related actions while only using the dependency locks exactly as written when preparing to actually _run_ the provider plugins. This is bothersome to test because our tests are not allowed to directly access registry.terraform.io; the test case here mimicks one case in which it could be valid for an OpenTofu user to explicitly use registry.terraform.io: if they've used the CLI configuration to arrange for that hostname to be handled only via a mirror rather than by direct access to the origin registry. The terms of service for registry.terraform.io currently prohibit using it for anything other than Terraform, so we ensure that this test cannot make requests to any real services at that hostname. Note that telling OpenTofu to use registry.terraform.io is not officially supported and may cause other problems beyond what was addressed by this PR, since OpenTofu tends to assume that this hostname would appear only during the process of migrating from Terraform and might make unexpected decisions based on that assumption. Despite us making this fix, those who are explicitly specifying registry.terraform.io in their configuration should make plans to stop doing that and to set things up some other way instead. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
526 lines
22 KiB
Go
526 lines
22 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package e2etest
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/depsfile"
|
|
"github.com/opentofu/opentofu/internal/e2e"
|
|
"github.com/opentofu/opentofu/internal/getproviders"
|
|
)
|
|
|
|
// TestProviderTampering tests various ways that the provider plugins in the
|
|
// local cache directory might be modified after an initial "tofu init",
|
|
// which other OpenTofu commands which use those plugins should catch and
|
|
// report early.
|
|
func TestProviderTampering(t *testing.T) {
|
|
// General setup: we'll do a one-off init of a test directory as our
|
|
// starting point, and then we'll clone that result for each test so
|
|
// that we can save the cost of a repeated re-init with the same
|
|
// provider.
|
|
t.Parallel()
|
|
|
|
// This test reaches out to registry.opentofu.org to download the
|
|
// null provider, so it can only run if network access is allowed.
|
|
skipIfCannotAccessNetwork(t)
|
|
|
|
fixturePath := filepath.Join("testdata", "provider-tampering-base")
|
|
tf := e2e.NewBinary(t, tofuBin, fixturePath)
|
|
|
|
stdout, stderr, err := tf.Run("init")
|
|
if err != nil {
|
|
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
if !strings.Contains(stdout, "Installing hashicorp/null v") {
|
|
t.Errorf("null provider download message is missing from init output:\n%s", stdout)
|
|
t.Logf("(this can happen if you have a copy of the plugin in one of the global plugin search dirs)")
|
|
}
|
|
|
|
seedDir := tf.WorkDir()
|
|
const providerVersion = "3.1.0" // must match the version in the fixture config
|
|
pluginDir := filepath.Join(".terraform", "providers", "registry.opentofu.org", "hashicorp", "null", providerVersion, getproviders.CurrentPlatform.String())
|
|
pluginExe := filepath.Join(pluginDir, "terraform-provider-null_v"+providerVersion+"_x5")
|
|
if getproviders.CurrentPlatform.OS == "windows" {
|
|
pluginExe += ".exe" // ugh
|
|
}
|
|
|
|
// filepath.Join here to make sure we get the right path separator
|
|
// for whatever OS we're running these tests on.
|
|
providerCacheDir := filepath.Join(".terraform", "providers")
|
|
|
|
t.Run("cache dir totally gone", func(t *testing.T) {
|
|
tf := e2e.NewBinary(t, tofuBin, seedDir)
|
|
workDir := tf.WorkDir()
|
|
|
|
err := os.RemoveAll(filepath.Join(workDir, ".terraform"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stdout, stderr, err := tf.Run("plan")
|
|
if err == nil {
|
|
t.Fatalf("unexpected plan success\nstdout:\n%s", stdout)
|
|
}
|
|
if want := `registry.opentofu.org/hashicorp/null: there is no package for registry.opentofu.org/hashicorp/null 3.1.0 cached in ` + providerCacheDir; !strings.Contains(stderr, want) {
|
|
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
|
|
}
|
|
if want := `tofu init`; !strings.Contains(stderr, want) {
|
|
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
|
|
}
|
|
|
|
// Running init as suggested resolves the problem
|
|
_, stderr, err = tf.Run("init")
|
|
if err != nil {
|
|
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
_, stderr, err = tf.Run("plan")
|
|
if err != nil {
|
|
t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
})
|
|
t.Run("cache dir totally gone, explicit backend", func(t *testing.T) {
|
|
tf := e2e.NewBinary(t, tofuBin, seedDir)
|
|
workDir := tf.WorkDir()
|
|
|
|
err := os.WriteFile(filepath.Join(workDir, "backend.tf"), []byte(localBackendConfig), 0600)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = os.RemoveAll(filepath.Join(workDir, ".terraform"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stdout, stderr, err := tf.Run("plan")
|
|
if err == nil {
|
|
t.Fatalf("unexpected plan success\nstdout:\n%s", stdout)
|
|
}
|
|
if want := `Initial configuration of the requested backend "local"`; !strings.Contains(stderr, want) {
|
|
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
|
|
}
|
|
if want := `tofu init`; !strings.Contains(stderr, want) {
|
|
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
|
|
}
|
|
|
|
// Running init as suggested resolves the problem
|
|
_, stderr, err = tf.Run("init")
|
|
if err != nil {
|
|
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
_, stderr, err = tf.Run("plan")
|
|
if err != nil {
|
|
t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
})
|
|
t.Run("null plugin package modified before plan", func(t *testing.T) {
|
|
tf := e2e.NewBinary(t, tofuBin, seedDir)
|
|
workDir := tf.WorkDir()
|
|
|
|
err := os.WriteFile(filepath.Join(workDir, pluginExe), []byte("tamper"), 0600)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stdout, stderr, err := tf.Run("plan")
|
|
if err == nil {
|
|
t.Fatalf("unexpected plan success\nstdout:\n%s", stdout)
|
|
}
|
|
if want := `registry.opentofu.org/hashicorp/null: the cached package for registry.opentofu.org/hashicorp/null 3.1.0 (in ` + providerCacheDir + `) does not match any of the checksums recorded in the dependency lock file`; !strings.Contains(stderr, want) {
|
|
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
|
|
}
|
|
if want := `tofu init`; !strings.Contains(stderr, want) {
|
|
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
|
|
}
|
|
})
|
|
t.Run("version constraint changed in config before plan", func(t *testing.T) {
|
|
tf := e2e.NewBinary(t, tofuBin, seedDir)
|
|
workDir := tf.WorkDir()
|
|
|
|
err := os.WriteFile(filepath.Join(workDir, "provider-tampering-base.tf"), []byte(`
|
|
terraform {
|
|
required_providers {
|
|
null = {
|
|
source = "hashicorp/null"
|
|
version = "1.0.0"
|
|
}
|
|
}
|
|
}
|
|
`), 0600)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stdout, stderr, err := tf.Run("plan")
|
|
if err == nil {
|
|
t.Fatalf("unexpected plan success\nstdout:\n%s", stdout)
|
|
}
|
|
if want := `provider registry.opentofu.org/hashicorp/null: locked version selection 3.1.0 doesn't match the updated version constraints "1.0.0"`; !strings.Contains(stderr, want) {
|
|
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
|
|
}
|
|
if want := `tofu init -upgrade`; !strings.Contains(stderr, want) {
|
|
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
|
|
}
|
|
})
|
|
t.Run("lock file modified before plan", func(t *testing.T) {
|
|
tf := e2e.NewBinary(t, tofuBin, seedDir)
|
|
workDir := tf.WorkDir()
|
|
|
|
// NOTE: We're just emptying out the lock file here because that's
|
|
// good enough for what we're trying to assert. The leaf codepath
|
|
// that generates this family of errors has some different variations
|
|
// of this error message for other sorts of inconsistency, but those
|
|
// are tested more thoroughly over in the "configs" package, which is
|
|
// ultimately responsible for that logic.
|
|
err := os.WriteFile(filepath.Join(workDir, ".terraform.lock.hcl"), []byte(``), 0600)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stdout, stderr, err := tf.Run("plan")
|
|
if err == nil {
|
|
t.Fatalf("unexpected plan success\nstdout:\n%s", stdout)
|
|
}
|
|
if want := `provider registry.opentofu.org/hashicorp/null: required by this configuration but no version is selected`; !strings.Contains(stderr, want) {
|
|
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
|
|
}
|
|
if want := `tofu init`; !strings.Contains(stderr, want) {
|
|
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
|
|
}
|
|
})
|
|
t.Run("lock file modified after plan", func(t *testing.T) {
|
|
tf := e2e.NewBinary(t, tofuBin, seedDir)
|
|
workDir := tf.WorkDir()
|
|
|
|
_, stderr, err := tf.Run("plan", "-out", "tfplan")
|
|
if err != nil {
|
|
t.Fatalf("unexpected plan failure\nstderr:\n%s", stderr)
|
|
}
|
|
|
|
err = os.Remove(filepath.Join(workDir, ".terraform.lock.hcl"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stdout, stderr, err := tf.Run("apply", "tfplan")
|
|
if err == nil {
|
|
t.Fatalf("unexpected apply success\nstdout:\n%s", stdout)
|
|
}
|
|
if want := `provider registry.opentofu.org/hashicorp/null: required by this configuration but no version is selected`; !strings.Contains(stderr, want) {
|
|
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
|
|
}
|
|
if want := `Create a new plan from the updated configuration.`; !strings.Contains(stderr, want) {
|
|
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
|
|
}
|
|
})
|
|
t.Run("plugin cache dir entirely removed after plan", func(t *testing.T) {
|
|
tf := e2e.NewBinary(t, tofuBin, seedDir)
|
|
workDir := tf.WorkDir()
|
|
|
|
_, stderr, err := tf.Run("plan", "-out", "tfplan")
|
|
if err != nil {
|
|
t.Fatalf("unexpected plan failure\nstderr:\n%s", stderr)
|
|
}
|
|
|
|
err = os.RemoveAll(filepath.Join(workDir, ".terraform"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stdout, stderr, err := tf.Run("apply", "tfplan")
|
|
if err == nil {
|
|
t.Fatalf("unexpected apply success\nstdout:\n%s", stdout)
|
|
}
|
|
if want := `registry.opentofu.org/hashicorp/null: there is no package for registry.opentofu.org/hashicorp/null 3.1.0 cached in ` + providerCacheDir; !strings.Contains(stderr, want) {
|
|
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
|
|
}
|
|
})
|
|
t.Run("null plugin package modified after plan", func(t *testing.T) {
|
|
tf := e2e.NewBinary(t, tofuBin, seedDir)
|
|
workDir := tf.WorkDir()
|
|
|
|
_, stderr, err := tf.Run("plan", "-out", "tfplan")
|
|
if err != nil {
|
|
t.Fatalf("unexpected plan failure\nstderr:\n%s", stderr)
|
|
}
|
|
|
|
err = os.WriteFile(filepath.Join(workDir, pluginExe), []byte("tamper"), 0600)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stdout, stderr, err := tf.Run("apply", "tfplan")
|
|
if err == nil {
|
|
t.Fatalf("unexpected apply success\nstdout:\n%s", stdout)
|
|
}
|
|
if want := `registry.opentofu.org/hashicorp/null: the cached package for registry.opentofu.org/hashicorp/null 3.1.0 (in ` + providerCacheDir + `) does not match any of the checksums recorded in the dependency lock file`; !strings.Contains(stderr, want) {
|
|
t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestProviderLocksFromPredecessorProject is an end-to-end test of our
|
|
// special treatment of lock files that were originally created by the
|
|
// project that OpenTofu was forked from, and so refer to providers from
|
|
// that project's registry instead of OpenTofu's registry.
|
|
//
|
|
// In that case we attempt to adjust the lock file so that we'll select
|
|
// the same version of the equivalent provider in the OpenTofu registry,
|
|
// even though normally OpenTofu would see the providers in two different
|
|
// registries as completely distinct.
|
|
//
|
|
// This special behavior applies only to providers that match
|
|
// registry.terraform.io/hashicorp/*, since those are the ones that the
|
|
// OpenTofu project rebuilds and republishes with equivalent releases under
|
|
// registry.opentofu.org/hashicorp/*.
|
|
func TestProviderLocksFromPredecessorProject(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// This test reaches out to registry.opentofu.org to download the
|
|
// null provider, so it can only run if network access is allowed.
|
|
skipIfCannotAccessNetwork(t)
|
|
|
|
fixturePath := filepath.Join("testdata", "predecessor-dependency-lock-file")
|
|
tf := e2e.NewBinary(t, tofuBin, fixturePath)
|
|
|
|
stdout, stderr, err := tf.Run("init")
|
|
if err != nil {
|
|
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
if !strings.Contains(stdout, "Installing hashicorp/null v3.2.0") {
|
|
t.Errorf("null provider download message is missing from init output:\n%s", stdout)
|
|
t.Logf("(if the output specifies a version other than v3.2.0 then the fixup behavior did not work correctly)")
|
|
}
|
|
if !strings.Contains(stdout, "- registry.terraform.io/hashicorp/null => registry.opentofu.org/hashicorp/null") {
|
|
t.Errorf("null provider dependency lock fixup message is missing from init output:\n%s", stdout)
|
|
}
|
|
|
|
// The lock file should have been updated to include the selection for
|
|
// OpenTofu-flavored version of the provider along with the checksums
|
|
// of OpenTofu's release, and the original entry should've been pruned
|
|
// because as far as OpenTofu is concerned there's no dependency on
|
|
// that provider in the current configuration.
|
|
newLocks, err := tf.ReadFile(".terraform.lock.hcl")
|
|
if err != nil {
|
|
t.Fatalf("failed to load dependency lock file after init: %s", err)
|
|
}
|
|
locks, diags := depsfile.LoadLocksFromBytes(newLocks, ".terraform.lock.hcl")
|
|
if diags.HasErrors() {
|
|
t.Fatalf("failed to load dependency lock file after init: %s", diags.Err())
|
|
}
|
|
|
|
if lock := locks.Provider(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/null")); lock != nil {
|
|
t.Errorf("still have entry for %s v%s after init", lock.Provider(), lock.Version())
|
|
}
|
|
|
|
if lock := locks.Provider(addrs.MustParseProviderSourceString("registry.opentofu.org/hashicorp/null")); lock != nil {
|
|
if got, want := lock.Version(), getproviders.MustParseVersion("3.2.0"); got != want {
|
|
t.Errorf("wrong version of %s was selected\ngot: %s\nwant: %s", lock.Provider(), got, want)
|
|
}
|
|
// The full set of hashes we captured will vary depending on the
|
|
// platform where this test is running, but the "zh:" ones in
|
|
// particular come from the remote registry rather than from local
|
|
// calculation and so we'll assume they should be consistent.
|
|
allHashes := lock.AllHashes()
|
|
wantHashes := []getproviders.Hash{
|
|
// These are the official hashes for OpenTofu's build of
|
|
// hashicorp/null v3.2.0, as recorded in the registry.
|
|
getproviders.HashSchemeZip.New("11d576a7c9b9b5c3263fae11962216e8bce9e80ab9c5c7e2635a94f410d723f0"),
|
|
getproviders.HashSchemeZip.New("11e53de20574d5e449c2d4e4f4249644244bad2a365e9793267796b9b96befab"),
|
|
getproviders.HashSchemeZip.New("1eea180daf676f35e38aa0ca237334d86bdc7a4fd78da54c139d8c6e15ad0b7e"),
|
|
getproviders.HashSchemeZip.New("47645b42501cb29acc270b99f93bf96bdae649159f2b3fdfafbc9543c36930e1"),
|
|
getproviders.HashSchemeZip.New("639854d0182d91224e67b512bcc7d12705d7aca0095b2969c65680527402eef9"),
|
|
getproviders.HashSchemeZip.New("894a3a5980bbe7e3d2948e0bcf56ae28b4ac16aa28c69f9a104c70af0f2f7ee1"),
|
|
getproviders.HashSchemeZip.New("a4b4709333738c9e14cd285879f24792d8a2e277f071c9c641b11e5289c854f3"),
|
|
getproviders.HashSchemeZip.New("c0fa29f9e93525f4672ea91b61ed866624ba3f3afd64d1c9eff8cc4c319ba69b"),
|
|
getproviders.HashSchemeZip.New("f77678a6b62eb332d867cb7671982100f463d20a0f115c88a5d23f516ee872fa"),
|
|
getproviders.HashSchemeZip.New("f7a8ab5f6b6c54667c240c8d8ed9c45a46bdbfa6bead009198a30def88e35376"),
|
|
}
|
|
var gotHashes []getproviders.Hash
|
|
for _, hash := range allHashes {
|
|
if hash.HasScheme(getproviders.HashSchemeZip) {
|
|
gotHashes = append(gotHashes, hash)
|
|
}
|
|
}
|
|
slices.Sort(gotHashes) // order is unimportant
|
|
if diff := cmp.Diff(wantHashes, gotHashes); diff != "" {
|
|
t.Error("wrong hashes in lock file after init\n" + diff)
|
|
}
|
|
} else {
|
|
t.Errorf("missing entry for registry.opentofu.org/hashicorp/null after init")
|
|
}
|
|
|
|
}
|
|
|
|
// TestProviderLocksFromPredecessorProjectWithAbsoluteSourceAddr is a variant
|
|
// of TestProviderLocksFromPredecessorProject for the not-typically-recommended
|
|
// situation where the configuration contains a source address that explicitly
|
|
// specifies our predecessor project's registry hostname.
|
|
//
|
|
// Using OpenTofu with such a configuration is problematic by default because
|
|
// that registry's terms of service prohibit using it with OpenTofu, but it
|
|
// can potentially be okay (with some caveats, and THIS IS NOT LEGAL ADVICE)
|
|
// if using a non-default installation method configuration that arranges for
|
|
// that hostname to be handled by a mirror source rather than by direct
|
|
// communication with the origin registry.
|
|
//
|
|
// This test ensures that our lock file migration behavior handles this unusual
|
|
// situation in a reasonable way, using a local filesystem mirror to avoid
|
|
// directly accessing the predecessor's registry.
|
|
func TestProviderLocksFromPredecessorProjectWithAbsoluteSourceAddr(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// We'll use an overridden CLI configuration file to force installing
|
|
// from a filesystem mirror, since we're not allowed to access
|
|
// registry.terraform.io directly from this test.
|
|
tempDir := t.TempDir()
|
|
cliConfigFile := filepath.Join(tempDir, "cliconfig.tfrc")
|
|
err := os.WriteFile(
|
|
cliConfigFile,
|
|
fmt.Appendf(nil, `
|
|
provider_installation {
|
|
filesystem_mirror {
|
|
path = %q
|
|
}
|
|
}
|
|
|
|
# The following is just some additional insurance against
|
|
# making real requests to registry.terraform.io.
|
|
host "registry.terraform.io" {
|
|
# Prevents service discovery, and instead behaves as if the
|
|
# discovery document declares nothing at all.
|
|
services = {}
|
|
}
|
|
`, tempDir),
|
|
os.ModePerm,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
platform := getproviders.CurrentPlatform
|
|
providerPkgDir := filepath.Join(tempDir, "registry.terraform.io", "hashicorp", "null", "3.2.0", platform.OS+"_"+platform.Arch)
|
|
err = os.MkdirAll(providerPkgDir, os.ModePerm)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = os.WriteFile(
|
|
filepath.Join(providerPkgDir, "terraform-provider-null"),
|
|
[]byte(`this is not a real provider plugin; it's just a placeholder`),
|
|
os.ModePerm,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// We should now be able to use the temporary directory created above to
|
|
// install our fake mirror of the provider.
|
|
fixturePath := filepath.Join("testdata", "predecessor-dependency-lock-file-abs")
|
|
tf := e2e.NewBinary(t, tofuBin, fixturePath)
|
|
tf.AddEnv("TF_CLI_CONFIG_FILE=" + cliConfigFile)
|
|
|
|
stdout, stderr, err := tf.Run("init")
|
|
if err != nil {
|
|
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
// Note that this explicitly mentions registry.terraform.io. because that
|
|
// hostname was chosen explicitly in the configuration.
|
|
if !strings.Contains(stdout, "Installing registry.terraform.io/hashicorp/null v3.2.0") {
|
|
t.Errorf("null provider download message is missing from init output:\n%s", stdout)
|
|
t.Logf("(if the output specifies a version other than v3.2.0 then the fixup behavior did not work correctly)")
|
|
}
|
|
// We always produce the warning about amending the lock file, even in this
|
|
// case where it doesn't technically apply, because we don't recommend
|
|
// using registry.terraform.io directly and only make a best effort to keep
|
|
// it working, so we don't want to over-complicate the migration logic for
|
|
// a situation that should be very rare.
|
|
if !strings.Contains(stdout, "- registry.terraform.io/hashicorp/null => registry.opentofu.org/hashicorp/null") {
|
|
t.Errorf("null provider dependency lock fixup message is missing from init output:\n%s", stdout)
|
|
}
|
|
|
|
// The lock file should still contain the entry for
|
|
// registry.terraform.io/hashicorp/null, and the synthetic extra entry
|
|
// for registry.opentofu.org/hashicorp/null should have been pruned before
|
|
// writing the file in this case because there's no mention of that provider
|
|
// in the configuration.
|
|
newLocks, err := tf.ReadFile(".terraform.lock.hcl")
|
|
if err != nil {
|
|
t.Fatalf("failed to load dependency lock file after init: %s", err)
|
|
}
|
|
locks, diags := depsfile.LoadLocksFromBytes(newLocks, ".terraform.lock.hcl")
|
|
if diags.HasErrors() {
|
|
t.Fatalf("failed to load dependency lock file after init: %s", diags.Err())
|
|
}
|
|
|
|
if lock := locks.Provider(addrs.MustParseProviderSourceString("registry.opentofu.org/hashicorp/null")); lock != nil {
|
|
t.Errorf("unexpected entry for %s v%s after init", lock.Provider(), lock.Version())
|
|
}
|
|
if lock := locks.Provider(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/null")); lock != nil {
|
|
if got, want := lock.Version(), getproviders.MustParseVersion("3.2.0"); got != want {
|
|
t.Errorf("wrong version of %s was selected\ngot: %s\nwant: %s", lock.Provider(), got, want)
|
|
}
|
|
gotHashes := lock.AllHashes()
|
|
wantHashes := []getproviders.Hash{
|
|
// This is the hash of our placeholder provider package containing
|
|
// a not-actually-executable plugin stub. We don't vary this by
|
|
// platform so this hash should match regardless of where this
|
|
// test is running.
|
|
getproviders.HashScheme1.New("DvLRiv4Pbjq3Rh0yNWtq+9dwVXqHF+bQspfhckLyFWU="),
|
|
}
|
|
if diff := cmp.Diff(wantHashes, gotHashes); diff != "" {
|
|
t.Error("wrong hashes in lock file after init\n" + diff)
|
|
}
|
|
for _, hash := range gotHashes {
|
|
if hash.HasScheme(getproviders.HashSchemeZip) {
|
|
// We should not get in here. If we do then we've likely just
|
|
// installed the _real_ hashicorp/null, and so we need to fix
|
|
// this test soon so that we're not depending on an external
|
|
// network service for this supposedly-local-only test.
|
|
t.Errorf("NOTE: unexpected hash %q suggests that this was installed from the origin registry, rather than the mirror!", hash)
|
|
}
|
|
}
|
|
} else {
|
|
t.Errorf("missing entry for registry.terraform.io/hashicorp/null after init")
|
|
return
|
|
}
|
|
|
|
// The above work should've left us in a valid situation where we can now
|
|
// run other workflow commands using the selected plugins. Since we've
|
|
// "installed" a fake thing that can't actually be executed the following
|
|
// will fail, but it should fail trying to install the fake executable
|
|
// rather than failing in command.Meta.providerFactories due to there
|
|
// being a plugin that isn't present at all.
|
|
// (Historically we had a bug where other commands would re-shim the
|
|
// dependency locks to refer to registry.opentofu.org/hashicorp/null and
|
|
// would thus make this fail because no such plugin is available in the
|
|
// cache directory: https://github.com/opentofu/opentofu/issues/2977 )
|
|
_, stderr, err = tf.Run("validate")
|
|
if err == nil {
|
|
t.Fatalf("unexpected success from tofu validate; want plugin execution error")
|
|
}
|
|
gotErr := stderr
|
|
wantErr := `failed to instantiate provider "registry.terraform.io/hashicorp/null" to obtain schema`
|
|
if !strings.Contains(gotErr, wantErr) {
|
|
t.Fatalf("unexpected validate error\ngot:\n%s\nwant substring: %s", gotErr, wantErr)
|
|
}
|
|
}
|
|
|
|
const localBackendConfig = `
|
|
terraform {
|
|
backend "local" {
|
|
path = "terraform.tfstate"
|
|
}
|
|
}
|
|
`
|