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.