diff --git a/internal/getproviders/mock_source.go b/internal/getproviders/mock_source.go new file mode 100644 index 0000000000..edf89e0dfc --- /dev/null +++ b/internal/getproviders/mock_source.go @@ -0,0 +1,186 @@ +package getproviders + +import ( + "archive/zip" + "fmt" + "io/ioutil" + "os" + + "github.com/hashicorp/terraform/addrs" +) + +// MockSource is an in-memory-only, statically-configured source intended for +// use only in unit tests of other subsystems that consume provider sources. +// +// The MockSource also tracks calls to it in case a calling test wishes to +// assert that particular calls were made. +// +// This should not be used outside of unit test code. +type MockSource struct { + packages []PackageMeta + calls [][]interface{} +} + +var _ Source = (*MockSource)(nil) + +// NewMockSource creates and returns a MockSource with the given packages. +// +// The given packages don't necessarily need to refer to objects that actually +// exist on disk or over the network, unless the calling test is planning to +// use (directly or indirectly) the results for further provider installation +// actions. +func NewMockSource(packages []PackageMeta) *MockSource { + return &MockSource{ + packages: packages, + } +} + +// AvailableVersions returns all of the versions of the given provider that +// are available in the fixed set of packages that were passed to +// NewMockSource when creating the receiving source. +func (s *MockSource) AvailableVersions(provider addrs.Provider) (VersionList, error) { + s.calls = append(s.calls, []interface{}{"AvailableVersions", provider}) + var ret VersionList + for _, pkg := range s.packages { + if pkg.Provider == provider { + ret = append(ret, pkg.Version) + } + } + if len(ret) == 0 { + // In this case, we'll behave like a registry that doesn't know about + // this provider at all, rather than just returning an empty result. + return nil, ErrProviderNotKnown{provider} + } + ret.Sort() + return ret, nil +} + +// PackageMeta returns the first package from the list given to NewMockSource +// when creating the receiver that has the given provider, version, and +// target platform. +// +// If none of the packages match, it returns ErrPlatformNotSupported to +// simulate the situation where a provider release isn't available for a +// particular platform. +// +// Note that if the list of packages passed to NewMockSource contains more +// than one with the same provider, version, and target this function will +// always return the first one in the list, which may not match the behavior +// of other sources in an equivalent situation because it's a degenerate case +// with undefined results. +func (s *MockSource) PackageMeta(provider addrs.Provider, version Version, target Platform) (PackageMeta, error) { + s.calls = append(s.calls, []interface{}{"PackageMeta", provider, version, target}) + + for _, pkg := range s.packages { + if pkg.Provider != provider { + continue + } + if pkg.Version != version { + // (We're using strict equality rather than precedence here, + // because this is an exact version specification. The caller + // should consider precedence when selecting a version in the + // AvailableVersions response, and pass the exact selected + // version here.) + continue + } + if pkg.TargetPlatform != target { + continue + } + return pkg, nil + } + + // If we fall out here then nothing matched at all, so we'll treat that + // as "platform not supported" for consistency with RegistrySource. + return PackageMeta{}, ErrPlatformNotSupported{ + Provider: provider, + Version: version, + Platform: target, + } +} + +// CallLog returns a list of calls to other methods of the receiever that have +// been called since it was created, in case a calling test wishes to verify +// a particular sequence of operations. +// +// The result is a slice of slices where the first element of each inner slice +// is the name of the method that was called, and then any subsequent elements +// are positional arguments passed to that method. +// +// Callers are forbidden from modifying any objects accessible via the returned +// value. +func (s *MockSource) CallLog() [][]interface{} { + return s.calls +} + +// FakePackageMeta constructs and returns a PackageMeta that carries the given +// metadata but has fake location information that is likely to fail if +// attempting to install from it. +func FakePackageMeta(provider addrs.Provider, version Version, target Platform) PackageMeta { + return PackageMeta{ + Provider: provider, + Version: version, + TargetPlatform: target, + + // Some fake but somewhat-realistic-looking other metadata. This + // points nowhere, so will fail if attempting to actually use it. + Filename: fmt.Sprintf("terraform-provider-%s_%s_%s.zip", provider.Type, version.String(), target.String()), + Location: PackageHTTPURL(fmt.Sprintf("https://fake.invalid/terraform-provider-%s_%s.zip", provider.Type, version.String())), + } +} + +// FakeInstallablePackageMeta constructs and returns a PackageMeta that points +// to a temporary archive file that could actually be installed in principle. +// +// Installing it will not produce a working provider though: just a fake file +// posing as an executable. +// +// It's the caller's responsibility to call the close callback returned +// alongside the result in order to clean up the temporary file. The caller +// should call the callback even if this function returns an error, because +// some error conditions leave a partially-created file on disk. +func FakeInstallablePackageMeta(provider addrs.Provider, version Version, target Platform) (PackageMeta, func(), error) { + f, err := ioutil.TempFile("", "terraform-getproviders-fake-package-") + if err != nil { + return PackageMeta{}, func() {}, err + } + + // After this point, all of our return paths should include this as the + // close callback. + close := func() { + f.Close() + os.Remove(f.Name()) + } + + execFilename := fmt.Sprintf("terraform-provider-%s_%s", provider.Type, version.String()) + if target.OS == "windows" { + // For a little more (technically unnecessary) realism... + execFilename += ".exe" + } + + zw := zip.NewWriter(f) + fw, err := zw.Create(execFilename) + if err != nil { + return PackageMeta{}, close, fmt.Errorf("failed to add %s to mock zip file: %s", execFilename, err) + } + fmt.Fprintf(fw, "This is a fake provider package for %s %s %s, not a real provider.\n", provider, version, target) + err = zw.Close() + if err != nil { + return PackageMeta{}, close, fmt.Errorf("failed to close the mock zip file: %s", err) + } + + meta := PackageMeta{ + Provider: provider, + Version: version, + TargetPlatform: target, + + Location: PackageLocalArchive(f.Name()), + + // This is a fake filename that mimics what a real registry might + // indicate as a good filename for this package, in case some caller + // intends to use it to name a local copy of the temporary file. + // (At the time of writing, no caller actually does that, but who + // knows what the future holds?) + Filename: fmt.Sprintf("terraform-provider-%s_%s_%s.zip", provider.Type, version.String(), target.String()), + } + return meta, close, nil +}