Files
opentf/internal/providercache/installer_test.go
Martin Atkins d2bef1fd47 Adopt OpenTofu's own "svchost" module
Previously we were using a third-party library, but that doesn't have any
support for passing context.Context through its API and so isn't suitable
for our goals of adding OpenTelemetry tracing for all outgoing network
requests.

We now have our own fork that is updated to use context.Context. It also
has a slightly reduced scope no longer including various details that
are tightly-coupled to our cliconfig mechanism and so better placed in the
main OpenTofu codebase so we can evolve it in future without making
lockstep library releases.

The "registry-address" library also uses svchost and uses some of its types
in its public API, so this also incorporates v2 of that library that is
updated to use our own svchost module.

Unfortunately this commit is a mix of mechanical updates to the new
libraries and some new code dealing with the functionality that is removed
in our fork of svchost. The new code is primarily in the "svcauthconfig"
package, which is similar in purpose "ociauthconfig" but for OpenTofu's
own auth mechanism instead of the OCI Distribution protocol's auth
mechanism.

This includes some additional plumbing of context.Context where it was
possible to do so without broad changes to files that would not otherwise
have been included in this commit, but there are a few leftover spots that
are context.TODO() which we'll address separately in later commits.

This removes the temporary workaround from d079da6e9e, since we are now
able to plumb the OpenTelemetry span tree all the way to the service
discovery requests.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-06-12 09:37:59 -07:00

2947 lines
93 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 providercache
import (
"archive/zip"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/apparentlymart/go-versions/versions"
"github.com/apparentlymart/go-versions/versions/constraints"
"github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp"
"github.com/opentofu/svchost"
"github.com/opentofu/svchost/disco"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/depsfile"
"github.com/opentofu/opentofu/internal/getproviders"
)
func TestEnsureProviderVersions(t *testing.T) {
// This is a sort of hybrid between table-driven and imperative-style
// testing, because the overall sequence of steps is the same for all
// of the test cases but the setup and verification have enough different
// permutations that it ends up being more concise to express them as
// normal code.
type Test struct {
Source getproviders.Source
Prepare func(*testing.T, *Installer, *Dir)
LockFile string
Reqs getproviders.Requirements
Mode InstallMode
Check func(*testing.T, *Dir, *depsfile.Locks)
WantErr string
WantEvents func(*Installer, *Dir) map[addrs.Provider][]*testInstallerEventLogItem
}
// noProvider is just the zero value of addrs.Provider, which we're
// using in this test as the key for installer events that are not
// specific to a particular provider.
var noProvider addrs.Provider
// beepProvider (and its associated derived values) represent a provider package
// fixture that's realistic enough to really be installed, although the
// plugin executable in it is really just a text file placeholder so it
// cannot actually be executed after installation.
beepProvider := addrs.MustParseProviderSourceString("example.com/foo/beep")
beepProviderDir := getproviders.PackageLocalDir("testdata/beep-provider")
beepProviderHash := getproviders.HashScheme1.New("2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=")
// We also derive a zip archive of the beep provider that we can use to test
// the slightly-different treatment of installation sources that can provide
// an uncompressed archive: specifically, that we can also calculate and record
// a "ziphash"-style hash that is based on the zip file itself instead of its content.
tempDir := t.TempDir()
beepProviderZip := getproviders.PackageLocalArchive(filepath.Join(tempDir, "beep-provider.zip"))
{
f, err := os.Create(string(beepProviderZip))
if err != nil {
t.Fatal(err)
}
zw := zip.NewWriter(f)
err = zw.AddFS(os.DirFS(string(beepProviderDir)))
if err != nil {
t.Fatal(err)
}
err = zw.Close()
if err != nil {
t.Fatal(err)
}
err = f.Close()
if err != nil {
t.Fatal(err)
}
}
// We calculate this particular hash dynamically, rather than hard-coding it
// as we normally do for these situations in tests, because archive/zip
// can generate slightly different entry metadata depending on the platform
// where the test is running and other details of the execution environment.
// We only care that the final result uses a hash that matches the zip file.
beepProviderZipHash, err := getproviders.PackageHashLegacyZipSHA(beepProviderZip)
if err != nil {
t.Fatal(err)
}
fakePlatform := getproviders.Platform{OS: "bleep", Arch: "bloop"}
wrongPlatform := getproviders.Platform{OS: "wrong", Arch: "wrong"}
terraformProvider := addrs.MustParseProviderSourceString("terraform.io/builtin/terraform")
tests := map[string]Test{
"no dependencies": {
Mode: InstallNewProvidersOnly,
Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
if allCached := dir.AllAvailablePackages(); len(allCached) != 0 {
t.Errorf("unexpected cache directory entries\n%s", spew.Sdump(allCached))
}
if allLocked := locks.AllProviders(); len(allLocked) != 0 {
t.Errorf("unexpected provider lock entries\n%s", spew.Sdump(allLocked))
}
},
WantEvents: func(*Installer, *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints(nil),
},
},
}
},
},
"successful initial install of one provider": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("1.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
},
nil,
),
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
}
if allLocked := locks.AllProviders(); len(allLocked) != 1 {
t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
}
gotLock := locks.Provider(beepProvider)
wantLock := depsfile.NewProviderLock(
beepProvider,
getproviders.MustParseVersion("2.1.0"),
getproviders.MustParseVersionConstraints(">= 2.0.0"),
[]getproviders.Hash{beepProviderHash},
)
if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
t.Errorf("wrong lock entry\n%s", diff)
}
gotEntry := dir.ProviderLatestVersion(beepProvider)
wantEntry := &CachedProvider{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
}
if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
t.Errorf("wrong cache entry\n%s", diff)
}
},
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
},
},
beepProvider: {
{
Event: "QueryPackagesBegin",
Provider: beepProvider,
Args: struct {
Constraints string
Locked bool
}{">= 2.0.0", false},
},
{
Event: "QueryPackagesSuccess",
Provider: beepProvider,
Args: "2.1.0",
},
{
Event: "FetchPackageMeta",
Provider: beepProvider,
Args: "2.1.0",
},
{
Event: "FetchPackageBegin",
Provider: beepProvider,
Args: struct {
Version string
Location getproviders.PackageLocation
InCache bool
}{"2.1.0", beepProviderDir, false},
},
{
Event: "ProvidersLockUpdated",
Provider: beepProvider,
Args: struct {
Version string
Local []getproviders.Hash
Signed []getproviders.Hash
Prior []getproviders.Hash
}{
"2.1.0",
[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
nil,
nil,
},
},
{
Event: "FetchPackageSuccess",
Provider: beepProvider,
Args: struct {
Version string
LocalDir string
AuthResult string
}{
"2.1.0",
filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
"unauthenticated",
},
},
},
}
},
},
"successful initial install of one provider from a .zip archive": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
TargetPlatform: fakePlatform,
Location: beepProviderZip,
Authentication: getproviders.NewPackageHashAuthentication(
fakePlatform, []getproviders.Hash{beepProviderZipHash},
),
},
},
nil,
),
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
}
if allLocked := locks.AllProviders(); len(allLocked) != 1 {
t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
}
gotLock := locks.Provider(beepProvider)
wantLock := depsfile.NewProviderLock(
beepProvider,
getproviders.MustParseVersion("2.1.0"),
getproviders.MustParseVersionConstraints(">= 2.0.0"),
[]getproviders.Hash{
beepProviderHash,
beepProviderZipHash, // additional hash should appear when we install from zip
},
)
if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
t.Errorf("wrong lock entry\n%s", diff)
}
gotEntry := dir.ProviderLatestVersion(beepProvider)
wantEntry := &CachedProvider{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
}
if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
t.Errorf("wrong cache entry\n%s", diff)
}
},
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
},
{
Event: "ProvidersAuthenticated",
Args: map[addrs.Provider]*getproviders.PackageAuthenticationResult{
beepProvider: getproviders.NewPackageAuthenticationResult(getproviders.HashDispositions{
beepProviderZipHash: {
VerifiedLocally: true,
},
}),
},
},
},
beepProvider: {
{
Event: "QueryPackagesBegin",
Provider: beepProvider,
Args: struct {
Constraints string
Locked bool
}{">= 2.0.0", false},
},
{
Event: "QueryPackagesSuccess",
Provider: beepProvider,
Args: "2.1.0",
},
{
Event: "FetchPackageMeta",
Provider: beepProvider,
Args: "2.1.0",
},
{
Event: "FetchPackageBegin",
Provider: beepProvider,
Args: struct {
Version string
Location getproviders.PackageLocation
InCache bool
}{"2.1.0", beepProviderZip, false},
},
{
Event: "ProvidersLockUpdated",
Provider: beepProvider,
Args: struct {
Version string
Local []getproviders.Hash
Signed []getproviders.Hash
Prior []getproviders.Hash
}{
"2.1.0",
[]getproviders.Hash{
"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=",
// When installing from a .zip archive and using
// getproviders.NewPackageHashAuthentication the lock file also
// records the "ziphash" of the .zip archive itself, whereas the
// "h1" hash is calculated from only the files inside the archive.
beepProviderZipHash,
},
nil,
nil,
},
},
{
Event: "FetchPackageSuccess",
Provider: beepProvider,
Args: struct {
Version string
LocalDir string
AuthResult string
}{
"2.1.0",
filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
"verified checksum",
},
},
},
}
},
},
"successful initial install of one provider through a cold global cache": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
},
nil,
),
Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
globalCacheDirPath := tmpDir(t)
globalCacheDir := NewDirWithPlatform(globalCacheDirPath, fakePlatform)
inst.SetGlobalCacheDir(globalCacheDir)
},
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
}
if allLocked := locks.AllProviders(); len(allLocked) != 1 {
t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
}
gotLock := locks.Provider(beepProvider)
wantLock := depsfile.NewProviderLock(
beepProvider,
getproviders.MustParseVersion("2.1.0"),
getproviders.MustParseVersionConstraints(">= 2.0.0"),
[]getproviders.Hash{beepProviderHash},
)
if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
t.Errorf("wrong lock entry\n%s", diff)
}
gotEntry := dir.ProviderLatestVersion(beepProvider)
wantEntry := &CachedProvider{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
}
if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
t.Errorf("wrong cache entry\n%s", diff)
}
},
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
},
},
beepProvider: {
{
Event: "QueryPackagesBegin",
Provider: beepProvider,
Args: struct {
Constraints string
Locked bool
}{">= 2.0.0", false},
},
{
Event: "QueryPackagesSuccess",
Provider: beepProvider,
Args: "2.1.0",
},
{
Event: "FetchPackageMeta",
Provider: beepProvider,
Args: "2.1.0",
},
{
Event: "FetchPackageBegin",
Provider: beepProvider,
Args: struct {
Version string
Location getproviders.PackageLocation
InCache bool
}{"2.1.0", beepProviderDir, true},
},
{
Event: "ProvidersLockUpdated",
Provider: beepProvider,
Args: struct {
Version string
Local []getproviders.Hash
Signed []getproviders.Hash
Prior []getproviders.Hash
}{
"2.1.0",
[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
nil,
nil,
},
},
{
Event: "FetchPackageSuccess",
Provider: beepProvider,
Args: struct {
Version string
LocalDir string
AuthResult string
}{
"2.1.0",
filepath.Join(inst.globalCacheDir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
"unauthenticated",
},
},
{
Event: "LinkFromCacheBegin",
Provider: beepProvider,
Args: struct {
Version string
CacheRoot string
}{
"2.1.0",
inst.globalCacheDir.BasePath(),
},
},
{
Event: "LinkFromCacheSuccess",
Provider: beepProvider,
Args: struct {
Version string
LocalDir string
}{
"2.1.0",
filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
},
},
},
}
},
},
"successful initial install of one provider through a warm global cache but without a lock file entry": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
},
nil,
),
Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
globalCacheDirPath := tmpDir(t)
globalCacheDir := NewDirWithPlatform(globalCacheDirPath, fakePlatform)
_, err := globalCacheDir.InstallPackage(
context.Background(),
getproviders.PackageMeta{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
nil,
false,
)
if err != nil {
t.Fatalf("failed to populate global cache: %s", err)
}
inst.SetGlobalCacheDir(globalCacheDir)
},
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
}
if allLocked := locks.AllProviders(); len(allLocked) != 1 {
t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
}
gotLock := locks.Provider(beepProvider)
wantLock := depsfile.NewProviderLock(
beepProvider,
getproviders.MustParseVersion("2.1.0"),
getproviders.MustParseVersionConstraints(">= 2.0.0"),
[]getproviders.Hash{beepProviderHash},
)
if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
t.Errorf("wrong lock entry\n%s", diff)
}
gotEntry := dir.ProviderLatestVersion(beepProvider)
wantEntry := &CachedProvider{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
}
if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
t.Errorf("wrong cache entry\n%s", diff)
}
},
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
},
},
beepProvider: {
{
Event: "QueryPackagesBegin",
Provider: beepProvider,
Args: struct {
Constraints string
Locked bool
}{">= 2.0.0", false},
},
{
Event: "QueryPackagesSuccess",
Provider: beepProvider,
Args: "2.1.0",
},
// Existing cache entry is ineligible for linking because
// we have no lock file checksums to compare it to.
// Instead, we install from upstream and lock with
// whatever checksums we learn in that process.
{
Event: "FetchPackageMeta",
Provider: beepProvider,
Args: "2.1.0",
},
{
Event: "FetchPackageBegin",
Provider: beepProvider,
Args: struct {
Version string
Location getproviders.PackageLocation
InCache bool
}{
"2.1.0",
beepProviderDir,
true,
},
},
{
Event: "ProvidersLockUpdated",
Provider: beepProvider,
Args: struct {
Version string
Local []getproviders.Hash
Signed []getproviders.Hash
Prior []getproviders.Hash
}{
"2.1.0",
[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
nil,
nil,
},
},
{
Event: "FetchPackageSuccess",
Provider: beepProvider,
Args: struct {
Version string
LocalDir string
AuthResult string
}{
"2.1.0",
filepath.Join(inst.globalCacheDir.BasePath(), "/example.com/foo/beep/2.1.0/bleep_bloop"),
"unauthenticated",
},
},
{
Event: "LinkFromCacheBegin",
Provider: beepProvider,
Args: struct {
Version string
CacheRoot string
}{
"2.1.0",
inst.globalCacheDir.BasePath(),
},
},
{
Event: "LinkFromCacheSuccess",
Provider: beepProvider,
Args: struct {
Version string
LocalDir string
}{
"2.1.0",
filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
},
},
},
}
},
},
"successful initial install of one provider through a warm global cache and correct locked checksum": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
},
nil,
),
LockFile: `
# The existing cache entry is valid only if it matches a
# checksum already recorded in the lock file.
provider "example.com/foo/beep" {
version = "2.1.0"
constraints = ">= 1.0.0"
hashes = [
"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=",
]
}
`,
Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
globalCacheDirPath := tmpDir(t)
globalCacheDir := NewDirWithPlatform(globalCacheDirPath, fakePlatform)
_, err := globalCacheDir.InstallPackage(
context.Background(),
getproviders.PackageMeta{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
nil,
false,
)
if err != nil {
t.Fatalf("failed to populate global cache: %s", err)
}
inst.SetGlobalCacheDir(globalCacheDir)
},
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
}
if allLocked := locks.AllProviders(); len(allLocked) != 1 {
t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
}
gotLock := locks.Provider(beepProvider)
wantLock := depsfile.NewProviderLock(
beepProvider,
getproviders.MustParseVersion("2.1.0"),
getproviders.MustParseVersionConstraints(">= 2.0.0"),
[]getproviders.Hash{beepProviderHash},
)
if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
t.Errorf("wrong lock entry\n%s", diff)
}
gotEntry := dir.ProviderLatestVersion(beepProvider)
wantEntry := &CachedProvider{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
}
if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
t.Errorf("wrong cache entry\n%s", diff)
}
},
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
},
},
beepProvider: {
{
Event: "QueryPackagesBegin",
Provider: beepProvider,
Args: struct {
Constraints string
Locked bool
}{">= 2.0.0", true},
},
{
Event: "QueryPackagesSuccess",
Provider: beepProvider,
Args: "2.1.0",
},
{
Event: "ProviderAlreadyInstalled",
Provider: beepProvider,
Args: struct {
Version getproviders.Version
InCache bool
}{Version: getproviders.MustParseVersion("2.1.0"), InCache: true},
},
{
Event: "LinkFromCacheBegin",
Provider: beepProvider,
Args: struct {
Version string
CacheRoot string
}{
"2.1.0",
inst.globalCacheDir.BasePath(),
},
},
{
Event: "LinkFromCacheSuccess",
Provider: beepProvider,
Args: struct {
Version string
LocalDir string
}{
"2.1.0",
filepath.Join(dir.BasePath(), "/example.com/foo/beep/2.1.0/bleep_bloop"),
},
},
},
}
},
},
"successful initial install of one provider through a warm global cache with an incompatible checksum": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
},
nil,
),
LockFile: `
# This is approximating the awkward situation where the lock
# file was populated by someone who installed from a location
# other than the origin registry and so the set of checksums
# is incomplete. In this case we can't prove that our cache
# entry is valid and so we silently ignore the cache entry
# and try to install from upstream anyway, in the hope that
# this will give us an opportunity to access the origin
# registry and get a checksum that works for the current
# platform.
provider "example.com/foo/beep" {
version = "2.1.0"
constraints = ">= 1.0.0"
hashes = [
# NOTE: This is the correct checksum for the
# beepProviderDir package, but we're going to
# intentionally install from a different directory
# below so that the entry in the cache will not
# match this checksum.
"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=",
]
}
`,
Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
// This is another "beep provider" package directory that
// has a different checksum than the one in beepProviderDir.
// We're mimicking the situation where the lock file was
// originally built from beepProviderDir but the local system
// is running on a different platform and so its existing
// cache entry doesn't match the checksum.
beepProviderOtherPlatformDir := getproviders.PackageLocalDir("testdata/beep-provider-other-platform")
globalCacheDirPath := tmpDir(t)
globalCacheDir := NewDirWithPlatform(globalCacheDirPath, fakePlatform)
_, err := globalCacheDir.InstallPackage(
context.Background(),
getproviders.PackageMeta{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
TargetPlatform: fakePlatform,
Location: beepProviderOtherPlatformDir,
},
nil,
false,
)
if err != nil {
t.Fatalf("failed to populate global cache: %s", err)
}
inst.SetGlobalCacheDir(globalCacheDir)
},
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
}
if allLocked := locks.AllProviders(); len(allLocked) != 1 {
t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
}
gotLock := locks.Provider(beepProvider)
wantLock := depsfile.NewProviderLock(
beepProvider,
getproviders.MustParseVersion("2.1.0"),
getproviders.MustParseVersionConstraints(">= 2.0.0"),
[]getproviders.Hash{beepProviderHash},
)
if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
t.Errorf("wrong lock entry\n%s", diff)
}
gotEntry := dir.ProviderLatestVersion(beepProvider)
wantEntry := &CachedProvider{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
}
if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
t.Errorf("wrong cache entry\n%s", diff)
}
},
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
},
},
beepProvider: {
{
Event: "QueryPackagesBegin",
Provider: beepProvider,
Args: struct {
Constraints string
Locked bool
}{">= 2.0.0", true},
},
{
Event: "QueryPackagesSuccess",
Provider: beepProvider,
Args: "2.1.0",
},
{
Event: "FetchPackageMeta",
Provider: beepProvider,
Args: "2.1.0",
},
{
Event: "FetchPackageBegin",
Provider: beepProvider,
Args: struct {
Version string
Location getproviders.PackageLocation
InCache bool
}{
"2.1.0",
beepProviderDir,
true,
},
},
{
Event: "ProvidersLockUpdated",
Provider: beepProvider,
Args: struct {
Version string
Local []getproviders.Hash
Signed []getproviders.Hash
Prior []getproviders.Hash
}{
"2.1.0",
[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
nil,
[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
},
},
{
Event: "FetchPackageSuccess",
Provider: beepProvider,
Args: struct {
Version string
LocalDir string
AuthResult string
}{
"2.1.0",
filepath.Join(inst.globalCacheDir.BasePath(), "/example.com/foo/beep/2.1.0/bleep_bloop"),
"unauthenticated",
},
},
{
Event: "LinkFromCacheBegin",
Provider: beepProvider,
Args: struct {
Version string
CacheRoot string
}{
"2.1.0",
inst.globalCacheDir.BasePath(),
},
},
{
Event: "LinkFromCacheSuccess",
Provider: beepProvider,
Args: struct {
Version string
LocalDir string
}{
"2.1.0",
filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
},
},
},
}
},
},
"successful initial install of one provider through a warm global cache without a lock file entry but allowing the cache to break the lock file": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
},
nil,
),
LockFile: `
# (intentionally empty)
`,
Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
globalCacheDirPath := tmpDir(t)
globalCacheDir := NewDirWithPlatform(globalCacheDirPath, fakePlatform)
_, err := globalCacheDir.InstallPackage(
context.Background(),
getproviders.PackageMeta{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
nil,
false,
)
if err != nil {
t.Fatalf("failed to populate global cache: %s", err)
}
inst.SetGlobalCacheDir(globalCacheDir)
inst.SetGlobalCacheDirMayBreakDependencyLockFile(true)
},
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
}
if allLocked := locks.AllProviders(); len(allLocked) != 1 {
t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
}
gotLock := locks.Provider(beepProvider)
wantLock := depsfile.NewProviderLock(
beepProvider,
getproviders.MustParseVersion("2.1.0"),
getproviders.MustParseVersionConstraints(">= 2.0.0"),
[]getproviders.Hash{beepProviderHash},
)
if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
t.Errorf("wrong lock entry\n%s", diff)
}
gotEntry := dir.ProviderLatestVersion(beepProvider)
wantEntry := &CachedProvider{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
}
if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
t.Errorf("wrong cache entry\n%s", diff)
}
},
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
},
},
beepProvider: {
{
Event: "QueryPackagesBegin",
Provider: beepProvider,
Args: struct {
Constraints string
Locked bool
}{">= 2.0.0", false},
},
{
Event: "QueryPackagesSuccess",
Provider: beepProvider,
Args: "2.1.0",
},
{
Event: "FetchPackageMeta",
Provider: beepProvider,
Args: "2.1.0",
},
{
Event: "FetchPackageBegin",
Provider: beepProvider,
Args: struct {
Version string
Location getproviders.PackageLocation
InCache bool
}{"2.1.0", beepProviderDir, true},
},
{
Event: "ProvidersLockUpdated",
Provider: beepProvider,
Args: struct {
Version string
Local []getproviders.Hash
Signed []getproviders.Hash
Prior []getproviders.Hash
}{
"2.1.0",
[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
nil,
nil,
},
},
{
Event: "FetchPackageSuccess",
Provider: beepProvider,
Args: struct {
Version string
LocalDir string
AuthResult string
}{
"2.1.0",
filepath.Join(inst.globalCacheDir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
"unauthenticated",
},
},
{
Event: "LinkFromCacheBegin",
Provider: beepProvider,
Args: struct {
Version string
CacheRoot string
}{
"2.1.0",
inst.globalCacheDir.BasePath(),
},
},
{
Event: "LinkFromCacheSuccess",
Provider: beepProvider,
Args: struct {
Version string
LocalDir string
}{
"2.1.0",
filepath.Join(dir.BasePath(), "/example.com/foo/beep/2.1.0/bleep_bloop"),
},
},
},
}
},
},
"failing install of one provider through a warm global cache with an incorrect locked checksum while allowing the cache to break the lock file": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
},
nil,
),
LockFile: `
# The existing cache entry is valid only if it matches a
# checksum already recorded in the lock file, but this
# test is overriding that rule using a special setting.
provider "example.com/foo/beep" {
version = "2.1.0"
constraints = ">= 1.0.0"
hashes = [
"h1:wrong-not-matchy",
]
}
`,
Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
globalCacheDirPath := tmpDir(t)
globalCacheDir := NewDirWithPlatform(globalCacheDirPath, fakePlatform)
_, err := globalCacheDir.InstallPackage(
context.Background(),
getproviders.PackageMeta{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
nil,
false,
)
if err != nil {
t.Fatalf("failed to populate global cache: %s", err)
}
inst.SetGlobalCacheDir(globalCacheDir)
inst.SetGlobalCacheDirMayBreakDependencyLockFile(true)
},
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
if allCached := dir.AllAvailablePackages(); len(allCached) != 0 {
t.Errorf("wrong number of cache directory entries; want none\n%s", spew.Sdump(allCached))
}
if allLocked := locks.AllProviders(); len(allLocked) != 1 {
t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
}
gotLock := locks.Provider(beepProvider)
wantLock := depsfile.NewProviderLock(
// The lock file entry hasn't changed because the cache
// entry didn't match the existing lock file entry.
beepProvider,
getproviders.MustParseVersion("2.1.0"),
getproviders.MustParseVersionConstraints(">= 1.0.0"),
[]getproviders.Hash{"h1:wrong-not-matchy"},
)
if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
t.Errorf("wrong lock entry\n%s", diff)
}
// The provider wasn't installed into the local cache directory
// because that would make the local cache mismatch the
// lock file.
gotEntry := dir.ProviderLatestVersion(beepProvider)
wantEntry := (*CachedProvider)(nil)
if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
t.Errorf("wrong cache entry\n%s", diff)
}
},
WantErr: `doesn't match any of the checksums`,
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
},
},
beepProvider: {
{
Event: "QueryPackagesBegin",
Provider: beepProvider,
Args: struct {
Constraints string
Locked bool
}{">= 2.0.0", true},
},
{
Event: "QueryPackagesSuccess",
Provider: beepProvider,
Args: "2.1.0",
},
{
Event: "FetchPackageMeta",
Provider: beepProvider,
Args: "2.1.0",
},
{
Event: "FetchPackageBegin",
Provider: beepProvider,
Args: struct {
Version string
Location getproviders.PackageLocation
InCache bool
}{"2.1.0", beepProviderDir, true},
},
{
Event: "FetchPackageFailure",
Provider: beepProvider,
Args: struct {
Version string
Error string
}{
"2.1.0",
fmt.Sprintf(
"the local package for %s 2.1.0 doesn't match any of the checksums previously recorded in the dependency lock file (this might be because the available checksums are for packages targeting different platforms); for more information: https://opentofu.org/docs/language/files/dependency-lock/#checksum-verification",
beepProvider,
),
},
},
},
}
},
},
"successful reinstall of one previously-locked provider": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("1.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
},
nil,
),
LockFile: `
provider "example.com/foo/beep" {
version = "2.0.0"
constraints = ">= 2.0.0"
hashes = [
"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=",
]
}
`,
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
}
if allLocked := locks.AllProviders(); len(allLocked) != 1 {
t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
}
gotLock := locks.Provider(beepProvider)
wantLock := depsfile.NewProviderLock(
beepProvider,
getproviders.MustParseVersion("2.0.0"),
getproviders.MustParseVersionConstraints(">= 2.0.0"),
[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
)
if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
t.Errorf("wrong lock entry\n%s", diff)
}
gotEntry := dir.ProviderLatestVersion(beepProvider)
wantEntry := &CachedProvider{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.0.0"),
PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.0.0/bleep_bloop"),
}
if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
t.Errorf("wrong cache entry\n%s", diff)
}
},
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
},
},
beepProvider: {
{
Event: "QueryPackagesBegin",
Provider: beepProvider,
Args: struct {
Constraints string
Locked bool
}{">= 2.0.0", true},
},
{
Event: "QueryPackagesSuccess",
Provider: beepProvider,
Args: "2.0.0",
},
{
Event: "FetchPackageMeta",
Provider: beepProvider,
Args: "2.0.0",
},
{
Event: "FetchPackageBegin",
Provider: beepProvider,
Args: struct {
Version string
Location getproviders.PackageLocation
InCache bool
}{"2.0.0", beepProviderDir, false},
},
{
Event: "ProvidersLockUpdated",
Provider: beepProvider,
Args: struct {
Version string
Local []getproviders.Hash
Signed []getproviders.Hash
Prior []getproviders.Hash
}{
"2.0.0",
[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
nil,
[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
},
},
{
Event: "FetchPackageSuccess",
Provider: beepProvider,
Args: struct {
Version string
LocalDir string
AuthResult string
}{
"2.0.0",
filepath.Join(dir.BasePath(), "example.com/foo/beep/2.0.0/bleep_bloop"),
"unauthenticated",
},
},
},
}
},
},
"skipped install of one previously-locked and installed provider": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
},
nil,
),
LockFile: `
provider "example.com/foo/beep" {
version = "2.0.0"
constraints = ">= 2.0.0"
hashes = [
"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=",
]
}
`,
Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
_, err := dir.InstallPackage(
context.Background(),
getproviders.PackageMeta{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
nil,
false,
)
if err != nil {
t.Fatalf("installation to the test dir failed: %s", err)
}
},
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
}
if allLocked := locks.AllProviders(); len(allLocked) != 1 {
t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
}
gotLock := locks.Provider(beepProvider)
wantLock := depsfile.NewProviderLock(
beepProvider,
getproviders.MustParseVersion("2.0.0"),
getproviders.MustParseVersionConstraints(">= 2.0.0"),
[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
)
if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
t.Errorf("wrong lock entry\n%s", diff)
}
gotEntry := dir.ProviderLatestVersion(beepProvider)
wantEntry := &CachedProvider{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.0.0"),
PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.0.0/bleep_bloop"),
}
if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
t.Errorf("wrong cache entry\n%s", diff)
}
},
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
},
},
beepProvider: {
{
Event: "QueryPackagesBegin",
Provider: beepProvider,
Args: struct {
Constraints string
Locked bool
}{">= 2.0.0", true},
},
{
Event: "QueryPackagesSuccess",
Provider: beepProvider,
Args: "2.0.0",
},
{
Event: "ProviderAlreadyInstalled",
Provider: beepProvider,
Args: struct {
Version getproviders.Version
InCache bool
}{versions.Version{Major: 2, Minor: 0, Patch: 0}, false},
},
},
}
},
},
"successful upgrade of one previously-locked provider": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("1.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
},
nil,
),
LockFile: `
provider "example.com/foo/beep" {
version = "2.0.0"
constraints = ">= 2.0.0"
hashes = [
"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=",
]
}
`,
Mode: InstallUpgrades,
Reqs: getproviders.Requirements{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
}
if allLocked := locks.AllProviders(); len(allLocked) != 1 {
t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
}
gotLock := locks.Provider(beepProvider)
wantLock := depsfile.NewProviderLock(
beepProvider,
getproviders.MustParseVersion("2.1.0"),
getproviders.MustParseVersionConstraints(">= 2.0.0"),
[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
)
if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
t.Errorf("wrong lock entry\n%s", diff)
}
gotEntry := dir.ProviderLatestVersion(beepProvider)
wantEntry := &CachedProvider{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.1.0"),
PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
}
if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
t.Errorf("wrong cache entry\n%s", diff)
}
},
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
},
},
beepProvider: {
{
Event: "QueryPackagesBegin",
Provider: beepProvider,
Args: struct {
Constraints string
Locked bool
}{">= 2.0.0", false},
},
{
Event: "QueryPackagesSuccess",
Provider: beepProvider,
Args: "2.1.0",
},
{
Event: "FetchPackageMeta",
Provider: beepProvider,
Args: "2.1.0",
},
{
Event: "FetchPackageBegin",
Provider: beepProvider,
Args: struct {
Version string
Location getproviders.PackageLocation
InCache bool
}{"2.1.0", beepProviderDir, false},
},
{
Event: "ProvidersLockUpdated",
Provider: beepProvider,
Args: struct {
Version string
Local []getproviders.Hash
Signed []getproviders.Hash
Prior []getproviders.Hash
}{
"2.1.0",
[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
nil,
nil,
},
},
{
Event: "FetchPackageSuccess",
Provider: beepProvider,
Args: struct {
Version string
LocalDir string
AuthResult string
}{
"2.1.0",
filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
"unauthenticated",
},
},
},
}
},
},
"successful install of a built-in provider": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{},
nil,
),
Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
inst.SetBuiltInProviderTypes([]string{"terraform"})
},
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
terraformProvider: nil,
},
Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
// Built-in providers are neither included in the cache
// directory nor mentioned in the lock file, because they
// are compiled directly into the OpenTofu executable.
if allCached := dir.AllAvailablePackages(); len(allCached) != 0 {
t.Errorf("wrong number of cache directory entries; want none\n%s", spew.Sdump(allCached))
}
if allLocked := locks.AllProviders(); len(allLocked) != 0 {
t.Errorf("wrong number of provider lock entries; want none\n%s", spew.Sdump(allLocked))
}
},
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
terraformProvider: constraints.IntersectionSpec(nil),
},
},
},
terraformProvider: {
{
Event: "BuiltInProviderAvailable",
Provider: terraformProvider,
},
},
}
},
},
"remove no-longer-needed provider from lock file": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("1.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
},
nil,
),
LockFile: `
provider "example.com/foo/beep" {
version = "1.0.0"
constraints = ">= 1.0.0"
hashes = [
"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=",
]
}
provider "example.com/foo/obsolete" {
version = "2.0.0"
constraints = ">= 2.0.0"
hashes = [
"no:irrelevant",
]
}
`,
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
},
Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
}
if allLocked := locks.AllProviders(); len(allLocked) != 1 {
t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
}
gotLock := locks.Provider(beepProvider)
wantLock := depsfile.NewProviderLock(
beepProvider,
getproviders.MustParseVersion("1.0.0"),
getproviders.MustParseVersionConstraints(">= 1.0.0"),
[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
)
if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
t.Errorf("wrong lock entry\n%s", diff)
}
gotEntry := dir.ProviderLatestVersion(beepProvider)
wantEntry := &CachedProvider{
Provider: beepProvider,
Version: getproviders.MustParseVersion("1.0.0"),
PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/1.0.0/bleep_bloop"),
}
if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
t.Errorf("wrong cache entry\n%s", diff)
}
},
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
},
},
},
// Note: intentionally no entries for example.com/foo/obsolete
// here, because it's no longer needed and therefore not
// installed.
beepProvider: {
{
Event: "QueryPackagesBegin",
Provider: beepProvider,
Args: struct {
Constraints string
Locked bool
}{">= 1.0.0", true},
},
{
Event: "QueryPackagesSuccess",
Provider: beepProvider,
Args: "1.0.0",
},
{
Event: "FetchPackageMeta",
Provider: beepProvider,
Args: "1.0.0",
},
{
Event: "FetchPackageBegin",
Provider: beepProvider,
Args: struct {
Version string
Location getproviders.PackageLocation
InCache bool
}{"1.0.0", beepProviderDir, false},
},
{
Event: "ProvidersLockUpdated",
Provider: beepProvider,
Args: struct {
Version string
Local []getproviders.Hash
Signed []getproviders.Hash
Prior []getproviders.Hash
}{
"1.0.0",
[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
nil,
[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
},
},
{
Event: "FetchPackageSuccess",
Provider: beepProvider,
Args: struct {
Version string
LocalDir string
AuthResult string
}{
"1.0.0",
filepath.Join(dir.BasePath(), "example.com/foo/beep/1.0.0/bleep_bloop"),
"unauthenticated",
},
},
},
}
},
},
"failed install of a non-existing built-in provider": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{},
nil,
),
Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
// NOTE: We're intentionally not calling
// inst.SetBuiltInProviderTypes to make the "terraform"
// built-in provider available here, so requests for it
// should fail.
},
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
terraformProvider: nil,
},
WantErr: `some providers could not be installed:
- terraform.io/builtin/terraform: this OpenTofu release has no built-in provider named "terraform"`,
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
terraformProvider: constraints.IntersectionSpec(nil),
},
},
},
terraformProvider: {
{
Event: "BuiltInProviderFailure",
Provider: terraformProvider,
Args: `this OpenTofu release has no built-in provider named "terraform"`,
},
},
}
},
},
"failed install when a built-in provider has a version constraint": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{},
nil,
),
Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
inst.SetBuiltInProviderTypes([]string{"terraform"})
},
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
terraformProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
},
WantErr: `some providers could not be installed:
- terraform.io/builtin/terraform: built-in providers do not support explicit version constraints`,
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
terraformProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
},
},
},
terraformProvider: {
{
Event: "BuiltInProviderFailure",
Provider: terraformProvider,
Args: `built-in providers do not support explicit version constraints`,
},
},
}
},
},
"locked version is excluded by new version constraint": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("1.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
},
nil,
),
LockFile: `
provider "example.com/foo/beep" {
version = "1.0.0"
constraints = ">= 1.0.0"
hashes = [
"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=",
]
}
`,
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
if allCached := dir.AllAvailablePackages(); len(allCached) != 0 {
t.Errorf("wrong number of cache directory entries; want none\n%s", spew.Sdump(allCached))
}
if allLocked := locks.AllProviders(); len(allLocked) != 1 {
t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
}
gotLock := locks.Provider(beepProvider)
wantLock := depsfile.NewProviderLock(
beepProvider,
getproviders.MustParseVersion("1.0.0"),
getproviders.MustParseVersionConstraints(">= 1.0.0"),
[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
)
if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
t.Errorf("wrong lock entry\n%s", diff)
}
},
WantErr: `some providers could not be installed:
- example.com/foo/beep: locked provider example.com/foo/beep 1.0.0 does not match configured version constraint >= 2.0.0; must use tofu init -upgrade to allow selection of new versions`,
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
},
},
beepProvider: {
{
Event: "QueryPackagesBegin",
Provider: beepProvider,
Args: struct {
Constraints string
Locked bool
}{">= 2.0.0", true},
},
{
Event: "QueryPackagesFailure",
Provider: beepProvider,
Args: `locked provider example.com/foo/beep 1.0.0 does not match configured version constraint >= 2.0.0; must use tofu init -upgrade to allow selection of new versions`,
},
},
}
},
},
"locked version is no longer available": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("1.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("2.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
},
nil,
),
LockFile: `
provider "example.com/foo/beep" {
version = "1.2.0"
constraints = ">= 1.0.0"
hashes = [
"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=",
]
}
`,
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
},
Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
if allCached := dir.AllAvailablePackages(); len(allCached) != 0 {
t.Errorf("wrong number of cache directory entries; want none\n%s", spew.Sdump(allCached))
}
if allLocked := locks.AllProviders(); len(allLocked) != 1 {
t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
}
gotLock := locks.Provider(beepProvider)
wantLock := depsfile.NewProviderLock(
beepProvider,
getproviders.MustParseVersion("1.2.0"),
getproviders.MustParseVersionConstraints(">= 1.0.0"),
[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
)
if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
t.Errorf("wrong lock entry\n%s", diff)
}
},
WantErr: `some providers could not be installed:
- example.com/foo/beep: the previously-selected version 1.2.0 is no longer available`,
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
},
},
},
beepProvider: {
{
Event: "QueryPackagesBegin",
Provider: beepProvider,
Args: struct {
Constraints string
Locked bool
}{">= 1.0.0", true},
},
{
Event: "QueryPackagesFailure",
Provider: beepProvider,
Args: `the previously-selected version 1.2.0 is no longer available`,
},
},
}
},
},
"no versions match the version constraint": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("1.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
},
nil,
),
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
WantErr: `some providers could not be installed:
- example.com/foo/beep: no available releases match the given constraints >= 2.0.0`,
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
},
},
},
beepProvider: {
{
Event: "QueryPackagesBegin",
Provider: beepProvider,
Args: struct {
Constraints string
Locked bool
}{">= 2.0.0", false},
},
{
Event: "QueryPackagesFailure",
Provider: beepProvider,
Args: `no available releases match the given constraints >= 2.0.0`,
},
},
}
},
},
"version exists but doesn't support the current platform": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("1.0.0"),
TargetPlatform: wrongPlatform,
Location: beepProviderDir,
},
},
nil,
),
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
},
WantErr: `some providers could not be installed:
- example.com/foo/beep: provider example.com/foo/beep 1.0.0 is not available for bleep_bloop`,
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
},
},
},
beepProvider: {
{
Event: "QueryPackagesBegin",
Provider: beepProvider,
Args: struct {
Constraints string
Locked bool
}{">= 1.0.0", false},
},
{
Event: "QueryPackagesSuccess",
Provider: beepProvider,
Args: "1.0.0",
},
{
Event: "FetchPackageMeta",
Provider: beepProvider,
Args: "1.0.0",
},
{
Event: "FetchPackageFailure",
Provider: beepProvider,
Args: struct {
Version string
Error string
}{
"1.0.0",
"provider example.com/foo/beep 1.0.0 is not available for bleep_bloop",
},
},
},
}
},
},
"available package doesn't match locked hash": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("1.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
},
nil,
),
LockFile: `
provider "example.com/foo/beep" {
version = "1.0.0"
constraints = ">= 1.0.0"
hashes = [
"h1:does-not-match",
]
}
`,
Mode: InstallNewProvidersOnly,
Reqs: getproviders.Requirements{
beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
},
WantErr: `some providers could not be installed:
- example.com/foo/beep: the local package for example.com/foo/beep 1.0.0 doesn't match any of the checksums previously recorded in the dependency lock file (this might be because the available checksums are for packages targeting different platforms); for more information: https://opentofu.org/docs/language/files/dependency-lock/#checksum-verification`,
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
},
},
},
beepProvider: {
{
Event: "QueryPackagesBegin",
Provider: beepProvider,
Args: struct {
Constraints string
Locked bool
}{">= 1.0.0", true},
},
{
Event: "QueryPackagesSuccess",
Provider: beepProvider,
Args: "1.0.0",
},
{
Event: "FetchPackageMeta",
Provider: beepProvider,
Args: "1.0.0",
},
{
Event: "FetchPackageBegin",
Provider: beepProvider,
Args: struct {
Version string
Location getproviders.PackageLocation
InCache bool
}{"1.0.0", beepProviderDir, false},
},
{
Event: "FetchPackageFailure",
Provider: beepProvider,
Args: struct {
Version string
Error string
}{
"1.0.0",
`the local package for example.com/foo/beep 1.0.0 doesn't match any of the checksums previously recorded in the dependency lock file (this might be because the available checksums are for packages targeting different platforms); for more information: https://opentofu.org/docs/language/files/dependency-lock/#checksum-verification`,
},
},
},
}
},
},
"force mode ignores hashes": {
Source: getproviders.NewMockSource(
[]getproviders.PackageMeta{
{
Provider: beepProvider,
Version: getproviders.MustParseVersion("1.0.0"),
TargetPlatform: fakePlatform,
Location: beepProviderDir,
},
},
nil,
),
LockFile: `
provider "example.com/foo/beep" {
version = "1.0.0"
constraints = ">= 1.0.0"
hashes = [
"h1:does-not-match",
]
}
`,
Mode: InstallNewProvidersForce,
Reqs: getproviders.Requirements{
beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
},
Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
}
if allLocked := locks.AllProviders(); len(allLocked) != 1 {
t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
}
gotLock := locks.Provider(beepProvider)
wantLock := depsfile.NewProviderLock(
beepProvider,
getproviders.MustParseVersion("1.0.0"),
getproviders.MustParseVersionConstraints(">= 1.0.0"),
[]getproviders.Hash{beepProviderHash, "h1:does-not-match"},
)
if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
t.Errorf("wrong lock entry\n%s", diff)
}
gotEntry := dir.ProviderLatestVersion(beepProvider)
wantEntry := &CachedProvider{
Provider: beepProvider,
Version: getproviders.MustParseVersion("1.0.0"),
PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/1.0.0/bleep_bloop"),
}
if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
t.Errorf("wrong cache entry\n%s", diff)
}
},
WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
return map[addrs.Provider][]*testInstallerEventLogItem{
noProvider: {
{
Event: "PendingProviders",
Args: map[addrs.Provider]getproviders.VersionConstraints{
beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
},
},
},
beepProvider: {
{
Event: "QueryPackagesBegin",
Provider: beepProvider,
Args: struct {
Constraints string
Locked bool
}{">= 1.0.0", true},
},
{
Event: "QueryPackagesSuccess",
Provider: beepProvider,
Args: "1.0.0",
},
{
Event: "FetchPackageMeta",
Provider: beepProvider,
Args: "1.0.0",
},
{
Event: "FetchPackageBegin",
Provider: beepProvider,
Args: struct {
Version string
Location getproviders.PackageLocation
InCache bool
}{"1.0.0", beepProviderDir, false},
},
{
Event: "ProvidersLockUpdated",
Provider: beepProvider,
Args: struct {
Version string
Local []getproviders.Hash
Signed []getproviders.Hash
Prior []getproviders.Hash
}{
"1.0.0",
[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
nil,
[]getproviders.Hash{"h1:does-not-match"},
},
},
{
Event: "FetchPackageSuccess",
Provider: beepProvider,
Args: struct {
Version string
LocalDir string
AuthResult string
}{
"1.0.0",
filepath.Join(dir.BasePath(), "example.com/foo/beep/1.0.0/bleep_bloop"),
"unauthenticated",
},
},
},
}
},
},
}
ctx := context.Background()
for name, test := range tests {
t.Run(name, func(t *testing.T) {
if test.Check == nil && test.WantEvents == nil && test.WantErr == "" {
t.Fatalf("invalid test: must set at least one of Check, WantEvents, or WantErr")
}
outputDir := NewDirWithPlatform(tmpDir(t), fakePlatform)
source := test.Source
if source == nil {
source = getproviders.NewMockSource(nil, nil)
}
inst := NewInstaller(outputDir, source)
if test.Prepare != nil {
test.Prepare(t, inst, outputDir)
} /* boop */
locks, lockDiags := depsfile.LoadLocksFromBytes([]byte(test.LockFile), "test.lock.hcl")
if lockDiags.HasErrors() {
t.Fatalf("invalid lock file: %s", lockDiags.Err().Error())
}
providerEvents := make(map[addrs.Provider][]*testInstallerEventLogItem)
eventsCh := make(chan *testInstallerEventLogItem)
var newLocks *depsfile.Locks
var instErr error
go func(ch chan *testInstallerEventLogItem) {
events := installerLogEventsForTests(ch)
ctx := events.OnContext(ctx)
newLocks, instErr = inst.EnsureProviderVersions(ctx, locks, test.Reqs, test.Mode)
close(eventsCh) // exits the event loop below
}(eventsCh)
for evt := range eventsCh {
// We do the event collection in the main goroutine, rather than
// running the installer itself in the main goroutine, so that
// we can safely t.Log in here without violating the testing.T
// usage rules.
if evt.Provider == (addrs.Provider{}) {
t.Logf("%s(%s)", evt.Event, spew.Sdump(evt.Args))
} else {
t.Logf("%s: %s(%s)", evt.Provider, evt.Event, spew.Sdump(evt.Args))
}
providerEvents[evt.Provider] = append(providerEvents[evt.Provider], evt)
}
if test.WantErr != "" {
if instErr == nil {
t.Errorf("succeeded; want error\nwant: %s", test.WantErr)
} else if got, want := instErr.Error(), test.WantErr; !strings.Contains(got, want) {
t.Errorf("wrong error\ngot: %s\nwant substring: %s", got, want)
}
} else if instErr != nil {
t.Errorf("unexpected error\ngot: %s", instErr.Error())
}
if test.Check != nil {
test.Check(t, outputDir, newLocks)
}
if test.WantEvents != nil {
wantEvents := test.WantEvents(inst, outputDir)
if diff := cmp.Diff(wantEvents, providerEvents, cmp.AllowUnexported(getproviders.PackageAuthenticationResult{})); diff != "" {
t.Errorf("wrong installer events\n%s", diff)
}
}
})
}
}
func TestEnsureProviderVersions_local_source(t *testing.T) {
// create filesystem source using the test provider cache dir
source := getproviders.NewFilesystemMirrorSource("testdata/cachedir")
// create a temporary workdir
tmpDirPath := t.TempDir()
// set up the installer using the temporary directory and filesystem source
platform := getproviders.Platform{OS: "linux", Arch: "amd64"}
dir := NewDirWithPlatform(tmpDirPath, platform)
installer := NewInstaller(dir, source)
tests := map[string]struct {
provider string
version string
wantHash getproviders.Hash // getproviders.NilHash if not expected to be installed
err string
}{
"install-unpacked": {
provider: "null",
version: "2.0.0",
wantHash: getproviders.HashScheme1.New("qjsREM4DqEWECD43FcPqddZ9oxCG+IaMTxvWPciS05g="),
},
"invalid-zip-file": {
provider: "null",
version: "2.1.0",
wantHash: getproviders.NilHash,
err: "zip: not a valid zip file",
},
"version-constraint-unmet": {
provider: "null",
version: "2.2.0",
wantHash: getproviders.NilHash,
err: "no available releases match the given constraints 2.2.0",
},
"missing-executable": {
provider: "missing/executable",
version: "2.0.0",
wantHash: getproviders.NilHash, // installation fails for a provider with no executable
err: "provider binary not found: could not find executable file starting with terraform-provider-executable",
},
"unspecified-version": {
provider: "null",
version: "0.0.0",
wantHash: getproviders.NilHash,
err: "0.0.0 is not a valid provider version. \nIf the version 0.0.0 is intended to represent a non-published provider, consider using dev_overrides - https://opentofu.org/docs/cli/config/config-file/#development-overrides-for-provider-developers",
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
provider := addrs.MustParseProviderSourceString(test.provider)
versionConstraint := getproviders.MustParseVersionConstraints(test.version)
version := getproviders.MustParseVersion(test.version)
reqs := getproviders.Requirements{
provider: versionConstraint,
}
newLocks, err := installer.EnsureProviderVersions(t.Context(), depsfile.NewLocks(), reqs, InstallNewProvidersOnly)
gotProviderlocks := newLocks.AllProviders()
wantProviderLocks := map[addrs.Provider]*depsfile.ProviderLock{
provider: depsfile.NewProviderLock(
provider,
version,
getproviders.MustParseVersionConstraints("= 2.0.0"),
[]getproviders.Hash{
test.wantHash,
},
),
}
if test.wantHash == getproviders.NilHash {
wantProviderLocks = map[addrs.Provider]*depsfile.ProviderLock{}
}
if diff := cmp.Diff(wantProviderLocks, gotProviderlocks, depsfile.ProviderLockComparer); diff != "" {
t.Errorf("wrong selected\n%s", diff)
}
if test.err == "" && err == nil {
return
}
switch err := err.(type) {
case InstallerError:
providerError, ok := err.ProviderErrors[provider]
if !ok {
t.Fatalf("did not get error for provider %s", provider)
}
if got := providerError.Error(); got != test.err {
t.Fatalf("wrong result\ngot: %s\nwant: %s\n", got, test.err)
}
default:
t.Fatalf("wrong error type. Expected InstallerError, got %T", err)
}
})
}
}
// This test only verifies protocol errors and does not try for successful
// installation (at the time of writing, the test files aren't signed so the
// signature verification fails); that's left to the e2e tests.
func TestEnsureProviderVersions_protocol_errors(t *testing.T) {
source, _, close := testRegistrySource(t)
defer close()
// create a temporary workdir
tmpDirPath := t.TempDir()
version0 := getproviders.MustParseVersionConstraints("0.1.0") // supports protocol version 1.0
version1 := getproviders.MustParseVersion("1.2.0") // this is the expected result in tests with a match
version2 := getproviders.MustParseVersionConstraints("2.0") // supports protocol version 99
// set up the installer using the temporary directory and mock source
platform := getproviders.Platform{OS: "gameboy", Arch: "lr35902"}
dir := NewDirWithPlatform(tmpDirPath, platform)
installer := NewInstaller(dir, source)
tests := map[string]struct {
provider addrs.Provider
inputVersion getproviders.VersionConstraints
wantVersion getproviders.Version
}{
"too old": {
addrs.MustParseProviderSourceString("example.com/awesomesauce/happycloud"),
version0,
version1,
},
"too new": {
addrs.MustParseProviderSourceString("example.com/awesomesauce/happycloud"),
version2,
version1,
},
"unsupported": {
addrs.MustParseProviderSourceString("example.com/weaksauce/unsupported-protocol"),
version0,
getproviders.UnspecifiedVersion,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
reqs := getproviders.Requirements{
test.provider: test.inputVersion,
}
_, err := installer.EnsureProviderVersions(t.Context(), depsfile.NewLocks(), reqs, InstallNewProvidersOnly)
switch err := err.(type) {
case nil:
t.Fatalf("expected error, got success")
case InstallerError:
providerError, ok := err.ProviderErrors[test.provider]
if !ok {
t.Fatalf("did not get error for provider %s", test.provider)
}
switch providerError := providerError.(type) {
case getproviders.ErrProtocolNotSupported:
if !providerError.Suggestion.Same(test.wantVersion) {
t.Fatalf("wrong result\ngot: %s\nwant: %s\n", providerError.Suggestion, test.wantVersion)
}
default:
t.Fatalf("wrong error type. Expected ErrProtocolNotSupported, got %T", err)
}
default:
t.Fatalf("wrong error type. Expected InstallerError, got %T", err)
}
})
}
}
// testServices starts up a local HTTP server running a fake provider registry
// service and returns a service discovery object pre-configured to consider
// the host "example.com" to be served by the fake registry service.
//
// The returned discovery object also knows the hostname "not.example.com"
// which does not have a provider registry at all and "too-new.example.com"
// which has a "providers.v99" service that is inoperable but could be useful
// to test the error reporting for detecting an unsupported protocol version.
// It also knows fails.example.com but it refers to an endpoint that doesn't
// correctly speak HTTP, to simulate a protocol error.
//
// The second return value is a function to call at the end of a test function
// to shut down the test server. After you call that function, the discovery
// object becomes useless.
func testServices(t *testing.T) (services *disco.Disco, baseURL string, cleanup func()) {
server := httptest.NewServer(http.HandlerFunc(fakeRegistryHandler))
services = disco.New()
services.ForceHostServices(svchost.Hostname("example.com"), map[string]interface{}{
"providers.v1": server.URL + "/providers/v1/",
})
services.ForceHostServices(svchost.Hostname("not.example.com"), map[string]interface{}{})
services.ForceHostServices(svchost.Hostname("too-new.example.com"), map[string]interface{}{
// This service doesn't actually work; it's here only to be
// detected as "too new" by the discovery logic.
"providers.v99": server.URL + "/providers/v99/",
})
services.ForceHostServices(svchost.Hostname("fails.example.com"), map[string]interface{}{
"providers.v1": server.URL + "/fails-immediately/",
})
// We'll also permit registry.opentofu.org here just because it's our
// default and has some unique features that are not allowed on any other
// hostname. It behaves the same as example.com, which should be preferred
// if you're not testing something specific to the default registry in order
// to ensure that most things are hostname-agnostic.
services.ForceHostServices(svchost.Hostname("registry.opentofu.org"), map[string]interface{}{
"providers.v1": server.URL + "/providers/v1/",
})
return services, server.URL, func() {
server.Close()
}
}
// testRegistrySource is a wrapper around testServices that uses the created
// discovery object to produce a Source instance that is ready to use with the
// fake registry services.
//
// As with testServices, the second return value is a function to call at the end
// of your test in order to shut down the test server.
func testRegistrySource(t *testing.T) (source *getproviders.RegistrySource, baseURL string, cleanup func()) {
services, baseURL, close := testServices(t)
source = getproviders.NewRegistrySource(services, nil)
return source, baseURL, close
}
func fakeRegistryHandler(resp http.ResponseWriter, req *http.Request) {
write := func(data []byte) {
if _, err := resp.Write(data); err != nil {
panic(err)
}
}
path := req.URL.EscapedPath()
if strings.HasPrefix(path, "/fails-immediately/") {
// Here we take over the socket and just close it immediately, to
// simulate one possible way a server might not be an HTTP server.
hijacker, ok := resp.(http.Hijacker)
if !ok {
// Not hijackable, so we'll just fail normally.
// If this happens, tests relying on this will fail.
resp.WriteHeader(500)
write([]byte(`cannot hijack`))
return
}
conn, _, err := hijacker.Hijack()
if err != nil {
resp.WriteHeader(500)
write([]byte(`hijack failed`))
return
}
conn.Close()
return
}
if strings.HasPrefix(path, "/pkg/") {
switch path {
case "/pkg/awesomesauce/happycloud_1.2.0.zip":
write([]byte("some zip file"))
case "/pkg/awesomesauce/happycloud_1.2.0_SHA256SUMS":
write([]byte("000000000000000000000000000000000000000000000000000000000000f00d happycloud_1.2.0.zip\n"))
case "/pkg/awesomesauce/happycloud_1.2.0_SHA256SUMS.sig":
write([]byte("GPG signature"))
default:
resp.WriteHeader(404)
write([]byte("unknown package file download"))
}
return
}
if !strings.HasPrefix(path, "/providers/v1/") {
resp.WriteHeader(404)
write([]byte(`not a provider registry endpoint`))
return
}
pathParts := strings.Split(path, "/")[3:]
if len(pathParts) < 2 {
resp.WriteHeader(404)
write([]byte(`unexpected number of path parts`))
return
}
log.Printf("[TRACE] fake provider registry request for %#v", pathParts)
if len(pathParts) == 2 {
switch pathParts[0] + "/" + pathParts[1] {
case "-/legacy":
// NOTE: This legacy lookup endpoint is specific to
// registry.opentofu.org and not expected to work on any other
// registry host.
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(200)
write([]byte(`{"namespace":"legacycorp"}`))
default:
resp.WriteHeader(404)
write([]byte(`unknown namespace or provider type for direct lookup`))
}
}
if len(pathParts) < 3 {
resp.WriteHeader(404)
write([]byte(`unexpected number of path parts`))
return
}
if pathParts[2] == "versions" {
if len(pathParts) != 3 {
resp.WriteHeader(404)
write([]byte(`extraneous path parts`))
return
}
switch pathParts[0] + "/" + pathParts[1] {
case "awesomesauce/happycloud":
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(200)
// Note that these version numbers are intentionally misordered
// so we can test that the client-side code places them in the
// correct order (lowest precedence first).
write([]byte(`{"versions":[{"version":"0.1.0","protocols":["1.0"]},{"version":"2.0.0","protocols":["99.0"]},{"version":"1.2.0","protocols":["5.0"]}, {"version":"1.0.0","protocols":["5.0"]}]}`))
case "weaksauce/unsupported-protocol":
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(200)
write([]byte(`{"versions":[{"version":"0.1.0","protocols":["0.1"]}]}`))
case "weaksauce/no-versions":
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(200)
write([]byte(`{"versions":[]}`))
default:
resp.WriteHeader(404)
write([]byte(`unknown namespace or provider type`))
}
return
}
if len(pathParts) == 6 && pathParts[3] == "download" {
switch pathParts[0] + "/" + pathParts[1] {
case "awesomesauce/happycloud":
if pathParts[4] == "nonexist" {
resp.WriteHeader(404)
write([]byte(`unsupported OS`))
return
}
version := pathParts[2]
body := map[string]interface{}{
"protocols": []string{"99.0"},
"os": pathParts[4],
"arch": pathParts[5],
"filename": "happycloud_" + version + ".zip",
"shasum": "000000000000000000000000000000000000000000000000000000000000f00d",
"download_url": "/pkg/awesomesauce/happycloud_" + version + ".zip",
"shasums_url": "/pkg/awesomesauce/happycloud_" + version + "_SHA256SUMS",
"shasums_signature_url": "/pkg/awesomesauce/happycloud_" + version + "_SHA256SUMS.sig",
"signing_keys": map[string]interface{}{
"gpg_public_keys": []map[string]interface{}{
{
"ascii_armor": getproviders.TestingPublicKey,
},
},
},
}
enc, err := json.Marshal(body)
if err != nil {
resp.WriteHeader(500)
write([]byte("failed to encode body"))
}
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(200)
write(enc)
case "weaksauce/unsupported-protocol":
var protocols []string
version := pathParts[2]
switch version {
case "0.1.0":
protocols = []string{"1.0"}
case "2.0.0":
protocols = []string{"99.0"}
default:
protocols = []string{"5.0"}
}
body := map[string]interface{}{
"protocols": protocols,
"os": pathParts[4],
"arch": pathParts[5],
"filename": "happycloud_" + version + ".zip",
"shasum": "000000000000000000000000000000000000000000000000000000000000f00d",
"download_url": "/pkg/awesomesauce/happycloud_" + version + ".zip",
"shasums_url": "/pkg/awesomesauce/happycloud_" + version + "_SHA256SUMS",
"shasums_signature_url": "/pkg/awesomesauce/happycloud_" + version + "_SHA256SUMS.sig",
"signing_keys": map[string]interface{}{
"gpg_public_keys": []map[string]interface{}{
{
"ascii_armor": getproviders.TestingPublicKey,
},
},
},
}
enc, err := json.Marshal(body)
if err != nil {
resp.WriteHeader(500)
write([]byte("failed to encode body"))
}
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(200)
write(enc)
default:
resp.WriteHeader(404)
write([]byte(`unknown namespace/provider/version/architecture`))
}
return
}
resp.WriteHeader(404)
write([]byte(`unrecognized path scheme`))
}
// In order to be able to compare the recorded temp dir paths, we need to
// normalize the path to match what the installer would report.
func tmpDir(t *testing.T) string {
unlinked, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatal(err)
}
return filepath.Clean(unlinked)
}