1package getproviders
2
3import (
4	"archive/zip"
5	"context"
6	"crypto/sha256"
7	"fmt"
8	"io"
9	"io/ioutil"
10	"os"
11
12	"github.com/hashicorp/terraform/internal/addrs"
13)
14
15// MockSource is an in-memory-only, statically-configured source intended for
16// use only in unit tests of other subsystems that consume provider sources.
17//
18// The MockSource also tracks calls to it in case a calling test wishes to
19// assert that particular calls were made.
20//
21// This should not be used outside of unit test code.
22type MockSource struct {
23	packages []PackageMeta
24	warnings map[addrs.Provider]Warnings
25	calls    [][]interface{}
26}
27
28var _ Source = (*MockSource)(nil)
29
30// NewMockSource creates and returns a MockSource with the given packages.
31//
32// The given packages don't necessarily need to refer to objects that actually
33// exist on disk or over the network, unless the calling test is planning to
34// use (directly or indirectly) the results for further provider installation
35// actions.
36func NewMockSource(packages []PackageMeta, warns map[addrs.Provider]Warnings) *MockSource {
37	return &MockSource{
38		packages: packages,
39		warnings: warns,
40	}
41}
42
43// AvailableVersions returns all of the versions of the given provider that
44// are available in the fixed set of packages that were passed to
45// NewMockSource when creating the receiving source.
46func (s *MockSource) AvailableVersions(ctx context.Context, provider addrs.Provider) (VersionList, Warnings, error) {
47	s.calls = append(s.calls, []interface{}{"AvailableVersions", provider})
48	var ret VersionList
49	for _, pkg := range s.packages {
50		if pkg.Provider == provider {
51			ret = append(ret, pkg.Version)
52		}
53	}
54	var warns []string
55	if s.warnings != nil {
56		if warnings, ok := s.warnings[provider]; ok {
57			warns = warnings
58		}
59	}
60	if len(ret) == 0 {
61		// In this case, we'll behave like a registry that doesn't know about
62		// this provider at all, rather than just returning an empty result.
63		return nil, warns, ErrRegistryProviderNotKnown{provider}
64	}
65	ret.Sort()
66	return ret, warns, nil
67}
68
69// PackageMeta returns the first package from the list given to NewMockSource
70// when creating the receiver that has the given provider, version, and
71// target platform.
72//
73// If none of the packages match, it returns ErrPlatformNotSupported to
74// simulate the situation where a provider release isn't available for a
75// particular platform.
76//
77// Note that if the list of packages passed to NewMockSource contains more
78// than one with the same provider, version, and target this function will
79// always return the first one in the list, which may not match the behavior
80// of other sources in an equivalent situation because it's a degenerate case
81// with undefined results.
82func (s *MockSource) PackageMeta(ctx context.Context, provider addrs.Provider, version Version, target Platform) (PackageMeta, error) {
83	s.calls = append(s.calls, []interface{}{"PackageMeta", provider, version, target})
84
85	for _, pkg := range s.packages {
86		if pkg.Provider != provider {
87			continue
88		}
89		if pkg.Version != version {
90			// (We're using strict equality rather than precedence here,
91			// because this is an exact version specification. The caller
92			// should consider precedence when selecting a version in the
93			// AvailableVersions response, and pass the exact selected
94			// version here.)
95			continue
96		}
97		if pkg.TargetPlatform != target {
98			continue
99		}
100		return pkg, nil
101	}
102
103	// If we fall out here then nothing matched at all, so we'll treat that
104	// as "platform not supported" for consistency with RegistrySource.
105	return PackageMeta{}, ErrPlatformNotSupported{
106		Provider: provider,
107		Version:  version,
108		Platform: target,
109	}
110}
111
112// CallLog returns a list of calls to other methods of the receiever that have
113// been called since it was created, in case a calling test wishes to verify
114// a particular sequence of operations.
115//
116// The result is a slice of slices where the first element of each inner slice
117// is the name of the method that was called, and then any subsequent elements
118// are positional arguments passed to that method.
119//
120// Callers are forbidden from modifying any objects accessible via the returned
121// value.
122func (s *MockSource) CallLog() [][]interface{} {
123	return s.calls
124}
125
126// FakePackageMeta constructs and returns a PackageMeta that carries the given
127// metadata but has fake location information that is likely to fail if
128// attempting to install from it.
129func FakePackageMeta(provider addrs.Provider, version Version, protocols VersionList, target Platform) PackageMeta {
130	return PackageMeta{
131		Provider:         provider,
132		Version:          version,
133		ProtocolVersions: protocols,
134		TargetPlatform:   target,
135
136		// Some fake but somewhat-realistic-looking other metadata. This
137		// points nowhere, so will fail if attempting to actually use it.
138		Filename: fmt.Sprintf("terraform-provider-%s_%s_%s.zip", provider.Type, version.String(), target.String()),
139		Location: PackageHTTPURL(fmt.Sprintf("https://fake.invalid/terraform-provider-%s_%s.zip", provider.Type, version.String())),
140	}
141}
142
143// FakeInstallablePackageMeta constructs and returns a PackageMeta that points
144// to a temporary archive file that could actually be installed in principle.
145//
146// Installing it will not produce a working provider though: just a fake file
147// posing as an executable. The filename for the executable defaults to the
148// standard terraform-provider-NAME_X.Y.Z format, but can be overridden with
149// the execFilename argument.
150//
151// It's the caller's responsibility to call the close callback returned
152// alongside the result in order to clean up the temporary file. The caller
153// should call the callback even if this function returns an error, because
154// some error conditions leave a partially-created file on disk.
155func FakeInstallablePackageMeta(provider addrs.Provider, version Version, protocols VersionList, target Platform, execFilename string) (PackageMeta, func(), error) {
156	f, err := ioutil.TempFile("", "terraform-getproviders-fake-package-")
157	if err != nil {
158		return PackageMeta{}, func() {}, err
159	}
160
161	// After this point, all of our return paths should include this as the
162	// close callback.
163	close := func() {
164		f.Close()
165		os.Remove(f.Name())
166	}
167
168	if execFilename == "" {
169		execFilename = fmt.Sprintf("terraform-provider-%s_%s", provider.Type, version.String())
170		if target.OS == "windows" {
171			// For a little more (technically unnecessary) realism...
172			execFilename += ".exe"
173		}
174	}
175
176	zw := zip.NewWriter(f)
177	fw, err := zw.Create(execFilename)
178	if err != nil {
179		return PackageMeta{}, close, fmt.Errorf("failed to add %s to mock zip file: %s", execFilename, err)
180	}
181	fmt.Fprintf(fw, "This is a fake provider package for %s %s, not a real provider.\n", provider, version)
182	err = zw.Close()
183	if err != nil {
184		return PackageMeta{}, close, fmt.Errorf("failed to close the mock zip file: %s", err)
185	}
186
187	// Compute the SHA256 checksum of the generated file, to allow package
188	// authentication code to be exercised.
189	f.Seek(0, io.SeekStart)
190	h := sha256.New()
191	io.Copy(h, f)
192	checksum := [32]byte{}
193	h.Sum(checksum[:0])
194
195	meta := PackageMeta{
196		Provider:         provider,
197		Version:          version,
198		ProtocolVersions: protocols,
199		TargetPlatform:   target,
200
201		Location: PackageLocalArchive(f.Name()),
202
203		// This is a fake filename that mimics what a real registry might
204		// indicate as a good filename for this package, in case some caller
205		// intends to use it to name a local copy of the temporary file.
206		// (At the time of writing, no caller actually does that, but who
207		// knows what the future holds?)
208		Filename: fmt.Sprintf("terraform-provider-%s_%s_%s.zip", provider.Type, version.String(), target.String()),
209
210		Authentication: NewArchiveChecksumAuthentication(target, checksum),
211	}
212	return meta, close, nil
213}
214
215func (s *MockSource) ForDisplay(provider addrs.Provider) string {
216	return "mock source"
217}
218