package getproviders import ( "archive/zip" "context" "crypto/sha256" "fmt" "io" "io/ioutil" "os" "github.com/hashicorp/terraform/internal/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 warnings map[addrs.Provider]Warnings 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, warns map[addrs.Provider]Warnings) *MockSource { return &MockSource{ packages: packages, warnings: warns, } } // 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(ctx context.Context, provider addrs.Provider) (VersionList, Warnings, 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) } } var warns []string if s.warnings != nil { if warnings, ok := s.warnings[provider]; ok { warns = warnings } } 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, warns, ErrRegistryProviderNotKnown{provider} } ret.Sort() return ret, warns, 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(ctx context.Context, 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, protocols VersionList, target Platform) PackageMeta { return PackageMeta{ Provider: provider, Version: version, ProtocolVersions: protocols, 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. The filename for the executable defaults to the // standard terraform-provider-NAME_X.Y.Z format, but can be overridden with // the execFilename argument. // // 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, protocols VersionList, target Platform, execFilename string) (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()) } if execFilename == "" { 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, not a real provider.\n", provider, version) err = zw.Close() if err != nil { return PackageMeta{}, close, fmt.Errorf("failed to close the mock zip file: %s", err) } // Compute the SHA256 checksum of the generated file, to allow package // authentication code to be exercised. f.Seek(0, io.SeekStart) h := sha256.New() io.Copy(h, f) checksum := [32]byte{} h.Sum(checksum[:0]) meta := PackageMeta{ Provider: provider, Version: version, ProtocolVersions: protocols, 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()), Authentication: NewArchiveChecksumAuthentication(target, checksum), } return meta, close, nil } func (s *MockSource) ForDisplay(provider addrs.Provider) string { return "mock source" }