From 24d32e9ca2aa6f8a23bb85cb2d0a982fabf9a406 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Tue, 20 Oct 2020 14:53:53 -0700 Subject: [PATCH] providercache: More exhaustive testing of the main installer We previously had some tests for some happy paths and a few specific failures into an empty directory with no existing locks, but we didn't have tests for the installer respecting existing lock file entries. This is a start on a more exhaustive set of tests for the installer, aiming to visit as many of the possible codepaths as we can reasonably test using this mocking strategy. (Some other codepaths require different underlying source implementations, etc, so we'll have to visit those in other tests separately.) --- internal/providercache/installer.go | 2 +- .../providercache/installer_events_test.go | 184 +++ internal/providercache/installer_test.go | 1203 +++++++++++++++++ .../beep-provider/terraform-provider-beep | 2 + 4 files changed, 1390 insertions(+), 1 deletion(-) create mode 100644 internal/providercache/installer_events_test.go create mode 100644 internal/providercache/testdata/beep-provider/terraform-provider-beep diff --git a/internal/providercache/installer.go b/internal/providercache/installer.go index 0fb8eeae95..db54a06242 100644 --- a/internal/providercache/installer.go +++ b/internal/providercache/installer.go @@ -576,5 +576,5 @@ func (err InstallerError) Error() string { providerErr := err.ProviderErrors[addr] fmt.Fprintf(&b, "- %s: %s\n", addr, providerErr) } - return b.String() + return strings.TrimSpace(b.String()) } diff --git a/internal/providercache/installer_events_test.go b/internal/providercache/installer_events_test.go new file mode 100644 index 0000000000..ab70326308 --- /dev/null +++ b/internal/providercache/installer_events_test.go @@ -0,0 +1,184 @@ +package providercache + +import ( + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/internal/getproviders" +) + +type testInstallerEventLogItem struct { + // The name of the event that occurred, using the same names as the + // fields of InstallerEvents. + Event string + + // Most events relate to a specific provider. For the few event types + // that don't, this will be a zero-value Provider. + Provider addrs.Provider + + // The type of Args will vary by event, but it should always be something + // that can be deterministically compared using the go-cmp package. + Args interface{} +} + +// installerLogEventsForTests is a test helper that produces an InstallerEvents +// that writes event notifications (*testInstallerEventLogItem values) to +// the given channel as they occur. +// +// The caller must keep reading from the read side of the given channel +// throughout any installer operation using the returned InstallerEvents. +// It's the caller's responsibility to close the channel if needed and +// clean up any goroutines it started to process the events. +// +// The exact sequence of events emitted for an installer operation might +// change in future, if e.g. we introduce new event callbacks to the +// InstallerEvents struct. Tests using this mechanism may therefore need to +// be updated to reflect such changes. +// +// (The channel-based approach here is so that the control flow for event +// processing will belong to the caller and thus it can safely use its +// testing.T object(s) to emit log lines without non-test-case frames in the +// call stack.) +func installerLogEventsForTests(into chan<- *testInstallerEventLogItem) *InstallerEvents { + return &InstallerEvents{ + PendingProviders: func(reqs map[addrs.Provider]getproviders.VersionConstraints) { + into <- &testInstallerEventLogItem{ + Event: "PendingProviders", + Args: reqs, + } + }, + ProviderAlreadyInstalled: func(provider addrs.Provider, selectedVersion getproviders.Version) { + into <- &testInstallerEventLogItem{ + Event: "ProviderAlreadyInstalled", + Provider: provider, + Args: selectedVersion, + } + }, + BuiltInProviderAvailable: func(provider addrs.Provider) { + into <- &testInstallerEventLogItem{ + Event: "BuiltInProviderAvailable", + Provider: provider, + } + }, + BuiltInProviderFailure: func(provider addrs.Provider, err error) { + into <- &testInstallerEventLogItem{ + Event: "BuiltInProviderFailure", + Provider: provider, + Args: err.Error(), // stringified to guarantee cmp-ability + } + }, + QueryPackagesBegin: func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints, locked bool) { + into <- &testInstallerEventLogItem{ + Event: "QueryPackagesBegin", + Provider: provider, + Args: struct { + Constraints string + Locked bool + }{getproviders.VersionConstraintsString(versionConstraints), locked}, + } + }, + QueryPackagesSuccess: func(provider addrs.Provider, selectedVersion getproviders.Version) { + into <- &testInstallerEventLogItem{ + Event: "QueryPackagesSuccess", + Provider: provider, + Args: selectedVersion.String(), + } + }, + QueryPackagesFailure: func(provider addrs.Provider, err error) { + into <- &testInstallerEventLogItem{ + Event: "QueryPackagesFailure", + Provider: provider, + Args: err.Error(), // stringified to guarantee cmp-ability + } + }, + QueryPackagesWarning: func(provider addrs.Provider, warns []string) { + into <- &testInstallerEventLogItem{ + Event: "QueryPackagesWarning", + Provider: provider, + Args: warns, + } + }, + LinkFromCacheBegin: func(provider addrs.Provider, version getproviders.Version, cacheRoot string) { + into <- &testInstallerEventLogItem{ + Event: "LinkFromCacheBegin", + Provider: provider, + Args: struct { + Version string + CacheRoot string + }{version.String(), cacheRoot}, + } + }, + LinkFromCacheSuccess: func(provider addrs.Provider, version getproviders.Version, localDir string) { + into <- &testInstallerEventLogItem{ + Event: "LinkFromCacheSuccess", + Provider: provider, + Args: struct { + Version string + LocalDir string + }{version.String(), localDir}, + } + }, + LinkFromCacheFailure: func(provider addrs.Provider, version getproviders.Version, err error) { + into <- &testInstallerEventLogItem{ + Event: "LinkFromCacheFailure", + Provider: provider, + Args: struct { + Version string + Error string + }{version.String(), err.Error()}, + } + }, + FetchPackageMeta: func(provider addrs.Provider, version getproviders.Version) { + into <- &testInstallerEventLogItem{ + Event: "FetchPackageMeta", + Provider: provider, + Args: version.String(), + } + }, + FetchPackageBegin: func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) { + into <- &testInstallerEventLogItem{ + Event: "FetchPackageBegin", + Provider: provider, + Args: struct { + Version string + Location getproviders.PackageLocation + }{version.String(), location}, + } + }, + FetchPackageSuccess: func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult) { + into <- &testInstallerEventLogItem{ + Event: "FetchPackageSuccess", + Provider: provider, + Args: struct { + Version string + LocalDir string + AuthResult string + }{version.String(), localDir, authResult.String()}, + } + }, + FetchPackageFailure: func(provider addrs.Provider, version getproviders.Version, err error) { + into <- &testInstallerEventLogItem{ + Event: "FetchPackageFailure", + Provider: provider, + Args: struct { + Version string + Error string + }{version.String(), err.Error()}, + } + }, + ProvidersFetched: func(authResults map[addrs.Provider]*getproviders.PackageAuthenticationResult) { + into <- &testInstallerEventLogItem{ + Event: "ProvidersFetched", + Args: authResults, + } + }, + HashPackageFailure: func(provider addrs.Provider, version getproviders.Version, err error) { + into <- &testInstallerEventLogItem{ + Event: "HashPackageFailure", + Provider: provider, + Args: struct { + Version string + Error string + }{version.String(), err.Error()}, + } + }, + } +} diff --git a/internal/providercache/installer_test.go b/internal/providercache/installer_test.go index 9e338fa744..c0598c6ee2 100644 --- a/internal/providercache/installer_test.go +++ b/internal/providercache/installer_test.go @@ -8,9 +8,12 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "strings" "testing" + "github.com/apparentlymart/go-versions/versions/constraints" + "github.com/davecgh/go-spew/spew" "github.com/google/go-cmp/cmp" svchost "github.com/hashicorp/terraform-svchost" "github.com/hashicorp/terraform-svchost/disco" @@ -19,6 +22,1206 @@ import ( "github.com/hashicorp/terraform/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 := addrs.MustParseProviderSourceString("example.com/foo/beep") + beepProviderDir := getproviders.PackageLocalDir("testdata/beep-provider") + fakePlatform := getproviders.Platform{OS: "bleep", Arch: "bloop"} + wrongPlatform := getproviders.Platform{OS: "wrong", Arch: "wrong"} + beepProviderHash := getproviders.HashScheme1.New("2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=") + 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"), + }, + }, + { + Event: "ProvidersFetched", + Args: map[addrs.Provider]*getproviders.PackageAuthenticationResult{ + beepProvider: nil, + }, + }, + }, + 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 + }{"2.1.0", beepProviderDir}, + }, + { + 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 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 := t.TempDir() + 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"), + }, + }, + { + Event: "ProvidersFetched", + Args: map[addrs.Provider]*getproviders.PackageAuthenticationResult{ + beepProvider: nil, + }, + }, + }, + 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 + }{"2.1.0", beepProviderDir}, + }, + { + Event: "FetchPackageSuccess", + Provider: beepProvider, + Args: struct { + Version string + LocalDir string + AuthResult string + }{ + "2.1.0", + // NOTE: With global cache enabled, the fetch + // goes into the global cache dir and + // we then to it from the local cache dir. + filepath.Join(inst.globalCacheDir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"), + "unauthenticated", + }, + }, + }, + } + }, + }, + "successful initial install of one provider through a warm 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 := t.TempDir() + globalCacheDir := NewDirWithPlatform(globalCacheDirPath, fakePlatform) + _, err := globalCacheDir.InstallPackage( + context.Background(), + getproviders.PackageMeta{ + Provider: beepProvider, + Version: getproviders.MustParseVersion("2.1.0"), + TargetPlatform: fakePlatform, + Location: beepProviderDir, + }, + nil, + ) + 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", + }, + { + 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 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"), + }, + }, + { + Event: "ProvidersFetched", + Args: map[addrs.Provider]*getproviders.PackageAuthenticationResult{ + beepProvider: nil, + }, + }, + }, + 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 + }{"2.0.0", beepProviderDir}, + }, + { + 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", + }, + }, + }, + } + }, + }, + "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"), + }, + }, + { + Event: "ProvidersFetched", + Args: map[addrs.Provider]*getproviders.PackageAuthenticationResult{ + beepProvider: nil, + }, + }, + }, + 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 + }{"2.1.0", beepProviderDir}, + }, + { + 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 Terraform 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, + }, + }, + } + }, + }, + "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 Terraform 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 Terraform 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 terraform 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 terraform 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"), + }, + 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"), + }, + }, + { + Event: "ProvidersFetched", + Args: map[addrs.Provider]*getproviders.PackageAuthenticationResult{ + beepProvider: nil, + }, + }, + }, + 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 + }{"1.0.0", beepProviderDir}, + }, + { + // FIXME: This ending in success with "unauthenticated" + // is technically okay within the interface as stated + // but doesn't really match our intent of treating + // a mismatch error against the lockfile as + // an error. We should make this an error in future. + 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(t.TempDir(), 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) + } + + 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; got != want { + t.Errorf("wrong error\ngot: %s\nwant: %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); 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") diff --git a/internal/providercache/testdata/beep-provider/terraform-provider-beep b/internal/providercache/testdata/beep-provider/terraform-provider-beep new file mode 100644 index 0000000000..e0841fd8c1 --- /dev/null +++ b/internal/providercache/testdata/beep-provider/terraform-provider-beep @@ -0,0 +1,2 @@ +This is not a real provider executable. It's just here to give the installer +something to copy in some of our installer test cases.