1package getproviders
2
3import (
4	"fmt"
5	"runtime"
6	"sort"
7	"strings"
8
9	"github.com/apparentlymart/go-versions/versions"
10	"github.com/apparentlymart/go-versions/versions/constraints"
11
12	"github.com/hashicorp/terraform/internal/addrs"
13)
14
15// Version represents a particular single version of a provider.
16type Version = versions.Version
17
18// UnspecifiedVersion is the zero value of Version, representing the absense
19// of a version number.
20var UnspecifiedVersion Version = versions.Unspecified
21
22// VersionList represents a list of versions. It is a []Version with some
23// extra methods for convenient filtering.
24type VersionList = versions.List
25
26// VersionSet represents a set of versions, usually describing the acceptable
27// versions that can be selected under a particular version constraint provided
28// by the end-user.
29type VersionSet = versions.Set
30
31// VersionConstraints represents a set of version constraints, which can
32// define the membership of a VersionSet by exclusion.
33type VersionConstraints = constraints.IntersectionSpec
34
35// Warnings represents a list of warnings returned by a Registry source.
36type Warnings = []string
37
38// Requirements gathers together requirements for many different providers
39// into a single data structure, as a convenient way to represent the full
40// set of requirements for a particular configuration or state or both.
41//
42// If an entry in a Requirements has a zero-length VersionConstraints then
43// that indicates that the provider is required but that any version is
44// acceptable. That's different than a provider being absent from the map
45// altogether, which means that it is not required at all.
46type Requirements map[addrs.Provider]VersionConstraints
47
48// Merge takes the requirements in the receiever and the requirements in the
49// other given value and produces a new set of requirements that combines
50// all of the requirements of both.
51//
52// The resulting requirements will permit only selections that both of the
53// source requirements would've allowed.
54func (r Requirements) Merge(other Requirements) Requirements {
55	ret := make(Requirements)
56	for addr, constraints := range r {
57		ret[addr] = constraints
58	}
59	for addr, constraints := range other {
60		ret[addr] = append(ret[addr], constraints...)
61	}
62	return ret
63}
64
65// Selections gathers together version selections for many different providers.
66//
67// This is the result of provider installation: a specific version selected
68// for each provider given in the requested Requirements, selected based on
69// the given version constraints.
70type Selections map[addrs.Provider]Version
71
72// ParseVersion parses a "semver"-style version string into a Version value,
73// which is the version syntax we use for provider versions.
74func ParseVersion(str string) (Version, error) {
75	return versions.ParseVersion(str)
76}
77
78// MustParseVersion is a variant of ParseVersion that panics if it encounters
79// an error while parsing.
80func MustParseVersion(str string) Version {
81	ret, err := ParseVersion(str)
82	if err != nil {
83		panic(err)
84	}
85	return ret
86}
87
88// ParseVersionConstraints parses a "Ruby-like" version constraint string
89// into a VersionConstraints value.
90func ParseVersionConstraints(str string) (VersionConstraints, error) {
91	return constraints.ParseRubyStyleMulti(str)
92}
93
94// MustParseVersionConstraints is a variant of ParseVersionConstraints that
95// panics if it encounters an error while parsing.
96func MustParseVersionConstraints(str string) VersionConstraints {
97	ret, err := ParseVersionConstraints(str)
98	if err != nil {
99		panic(err)
100	}
101	return ret
102}
103
104// MeetingConstraints returns a version set that contains all of the versions
105// that meet the given constraints, specified using the Spec type from the
106// constraints package.
107func MeetingConstraints(vc VersionConstraints) VersionSet {
108	return versions.MeetingConstraints(vc)
109}
110
111// Platform represents a target platform that a provider is or might be
112// available for.
113type Platform struct {
114	OS, Arch string
115}
116
117func (p Platform) String() string {
118	return p.OS + "_" + p.Arch
119}
120
121// LessThan returns true if the receiver should sort before the other given
122// Platform in an ordered list of platforms.
123//
124// The ordering is lexical first by OS and then by Architecture.
125// This ordering is primarily just to ensure that results of
126// functions in this package will be deterministic. The ordering is not
127// intended to have any semantic meaning and is subject to change in future.
128func (p Platform) LessThan(other Platform) bool {
129	switch {
130	case p.OS != other.OS:
131		return p.OS < other.OS
132	default:
133		return p.Arch < other.Arch
134	}
135}
136
137// ParsePlatform parses a string representation of a platform, like
138// "linux_amd64", or returns an error if the string is not valid.
139func ParsePlatform(str string) (Platform, error) {
140	underPos := strings.Index(str, "_")
141	if underPos < 1 || underPos >= len(str)-2 {
142		return Platform{}, fmt.Errorf("must be two words separated by an underscore")
143	}
144
145	os, arch := str[:underPos], str[underPos+1:]
146	if strings.ContainsAny(os, " \t\n\r") {
147		return Platform{}, fmt.Errorf("OS portion must not contain whitespace")
148	}
149	if strings.ContainsAny(arch, " \t\n\r") {
150		return Platform{}, fmt.Errorf("architecture portion must not contain whitespace")
151	}
152
153	return Platform{
154		OS:   os,
155		Arch: arch,
156	}, nil
157}
158
159// CurrentPlatform is the platform where the current program is running.
160//
161// If attempting to install providers for use on the same system where the
162// installation process is running, this is the right platform to use.
163var CurrentPlatform = Platform{
164	OS:   runtime.GOOS,
165	Arch: runtime.GOARCH,
166}
167
168// PackageMeta represents the metadata related to a particular downloadable
169// provider package targeting a single platform.
170//
171// Package findproviders does no signature verification or protocol version
172// compatibility checking of its own. A caller receving a PackageMeta must
173// verify that it has a correct signature and supports a protocol version
174// accepted by the current version of Terraform before trying to use the
175// described package.
176type PackageMeta struct {
177	Provider addrs.Provider
178	Version  Version
179
180	ProtocolVersions VersionList
181	TargetPlatform   Platform
182
183	Filename string
184	Location PackageLocation
185
186	// Authentication, if non-nil, is a request from the source that produced
187	// this meta for verification of the target package after it has been
188	// retrieved from the indicated Location.
189	//
190	// Different sources will support different authentication strategies --
191	// or possibly no strategies at all -- depending on what metadata they
192	// have available to them, such as checksums provided out-of-band by the
193	// original package author, expected signing keys, etc.
194	//
195	// If Authentication is non-nil then no authentication is requested.
196	// This is likely appropriate only for packages that are already available
197	// on the local system.
198	Authentication PackageAuthentication
199}
200
201// LessThan returns true if the receiver should sort before the given other
202// PackageMeta in a sorted list of PackageMeta.
203//
204// Sorting preference is given first to the provider address, then to the
205// taget platform, and the to the version number (using semver precedence).
206// Packages that differ only in semver build metadata have no defined
207// precedence and so will always return false.
208//
209// This ordering is primarily just to maximize the chance that results of
210// functions in this package will be deterministic. The ordering is not
211// intended to have any semantic meaning and is subject to change in future.
212func (m PackageMeta) LessThan(other PackageMeta) bool {
213	switch {
214	case m.Provider != other.Provider:
215		return m.Provider.LessThan(other.Provider)
216	case m.TargetPlatform != other.TargetPlatform:
217		return m.TargetPlatform.LessThan(other.TargetPlatform)
218	case m.Version != other.Version:
219		return m.Version.LessThan(other.Version)
220	default:
221		return false
222	}
223}
224
225// UnpackedDirectoryPath determines the path under the given base
226// directory where SearchLocalDirectory or the FilesystemMirrorSource would
227// expect to find an unpacked copy of the receiving PackageMeta.
228//
229// The result always uses forward slashes as path separator, even on Windows,
230// to produce a consistent result on all platforms. Windows accepts both
231// direction of slash as long as each individual path string is self-consistent.
232func (m PackageMeta) UnpackedDirectoryPath(baseDir string) string {
233	return UnpackedDirectoryPathForPackage(baseDir, m.Provider, m.Version, m.TargetPlatform)
234}
235
236// PackedFilePath determines the path under the given base
237// directory where SearchLocalDirectory or the FilesystemMirrorSource would
238// expect to find packed copy (a .zip archive) of the receiving PackageMeta.
239//
240// The result always uses forward slashes as path separator, even on Windows,
241// to produce a consistent result on all platforms. Windows accepts both
242// direction of slash as long as each individual path string is self-consistent.
243func (m PackageMeta) PackedFilePath(baseDir string) string {
244	return PackedFilePathForPackage(baseDir, m.Provider, m.Version, m.TargetPlatform)
245}
246
247// AcceptableHashes returns a set of hashes that could be recorded for
248// comparison to future results for the same provider version, to implement a
249// "trust on first use" scheme.
250//
251// The AcceptableHashes result is a platform-agnostic set of hashes, with the
252// intent that in most cases it will be used as an additional cross-check in
253// addition to a platform-specific hash check made during installation. However,
254// there are some situations (such as verifying an already-installed package
255// that's on local disk) where Terraform would check only against the results
256// of this function, meaning that it would in principle accept another
257// platform's package as a substitute for the correct platform. That's a
258// pragmatic compromise to allow lock files derived from the result of this
259// method to be portable across platforms.
260//
261// Callers of this method should typically also verify the package using the
262// object in the Authentication field, and consider how much trust to give
263// the result of this method depending on the authentication result: an
264// unauthenticated result or one that only verified a checksum could be
265// considered less trustworthy than one that checked the package against
266// a signature provided by the origin registry.
267//
268// The AcceptableHashes result is actually provided by the object in the
269// Authentication field. AcceptableHashes therefore returns an empty result
270// for a PackageMeta that has no authentication object, or has one that does
271// not make use of hashes.
272func (m PackageMeta) AcceptableHashes() []Hash {
273	auth, ok := m.Authentication.(PackageAuthenticationHashes)
274	if !ok {
275		return nil
276	}
277	return auth.AcceptableHashes()
278}
279
280// PackageLocation represents a location where a provider distribution package
281// can be obtained. A value of this type contains one of the following
282// concrete types: PackageLocalArchive, PackageLocalDir, or PackageHTTPURL.
283type PackageLocation interface {
284	packageLocation()
285	String() string
286}
287
288// PackageLocalArchive is the location of a provider distribution archive file
289// in the local filesystem. Its value is a local filesystem path using the
290// syntax understood by Go's standard path/filepath package on the operating
291// system where Terraform is running.
292type PackageLocalArchive string
293
294func (p PackageLocalArchive) packageLocation() {}
295func (p PackageLocalArchive) String() string   { return string(p) }
296
297// PackageLocalDir is the location of a directory containing an unpacked
298// provider distribution archive in the local filesystem. Its value is a local
299// filesystem path using the syntax understood by Go's standard path/filepath
300// package on the operating system where Terraform is running.
301type PackageLocalDir string
302
303func (p PackageLocalDir) packageLocation() {}
304func (p PackageLocalDir) String() string   { return string(p) }
305
306// PackageHTTPURL is a provider package location accessible via HTTP.
307// Its value is a URL string using either the http: scheme or the https: scheme.
308type PackageHTTPURL string
309
310func (p PackageHTTPURL) packageLocation() {}
311func (p PackageHTTPURL) String() string   { return string(p) }
312
313// PackageMetaList is a list of PackageMeta. It's just []PackageMeta with
314// some methods for convenient sorting and filtering.
315type PackageMetaList []PackageMeta
316
317func (l PackageMetaList) Len() int {
318	return len(l)
319}
320
321func (l PackageMetaList) Less(i, j int) bool {
322	return l[i].LessThan(l[j])
323}
324
325func (l PackageMetaList) Swap(i, j int) {
326	l[i], l[j] = l[j], l[i]
327}
328
329// Sort performs an in-place, stable sort on the contents of the list, using
330// the ordering given by method Less. This ordering is primarily to help
331// encourage deterministic results from functions and does not have any
332// semantic meaning.
333func (l PackageMetaList) Sort() {
334	sort.Stable(l)
335}
336
337// FilterPlatform constructs a new PackageMetaList that contains only the
338// elements of the receiver that are for the given target platform.
339//
340// Pass CurrentPlatform to filter only for packages targeting the platform
341// where this code is running.
342func (l PackageMetaList) FilterPlatform(target Platform) PackageMetaList {
343	var ret PackageMetaList
344	for _, m := range l {
345		if m.TargetPlatform == target {
346			ret = append(ret, m)
347		}
348	}
349	return ret
350}
351
352// FilterProviderExactVersion constructs a new PackageMetaList that contains
353// only the elements of the receiver that relate to the given provider address
354// and exact version.
355//
356// The version matching for this function is exact, including matching on
357// semver build metadata, because it's intended for handling a single exact
358// version selected by the caller from a set of available versions.
359func (l PackageMetaList) FilterProviderExactVersion(provider addrs.Provider, version Version) PackageMetaList {
360	var ret PackageMetaList
361	for _, m := range l {
362		if m.Provider == provider && m.Version == version {
363			ret = append(ret, m)
364		}
365	}
366	return ret
367}
368
369// FilterProviderPlatformExactVersion is a combination of both
370// FilterPlatform and FilterProviderExactVersion that filters by all three
371// criteria at once.
372func (l PackageMetaList) FilterProviderPlatformExactVersion(provider addrs.Provider, platform Platform, version Version) PackageMetaList {
373	var ret PackageMetaList
374	for _, m := range l {
375		if m.Provider == provider && m.Version == version && m.TargetPlatform == platform {
376			ret = append(ret, m)
377		}
378	}
379	return ret
380}
381
382// VersionConstraintsString returns a canonical string representation of
383// a VersionConstraints value.
384func VersionConstraintsString(spec VersionConstraints) string {
385	// (we have our own function for this because the upstream versions
386	// library prefers to use npm/cargo-style constraint syntax, but
387	// Terraform prefers Ruby-like. Maybe we can upstream a "RubyLikeString")
388	// function to do this later, but having this in here avoids blocking on
389	// that and this is the sort of thing that is unlikely to need ongoing
390	// maintenance because the version constraint syntax is unlikely to change.)
391	//
392	// ParseVersionConstraints allows some variations for convenience, but the
393	// return value from this function serves as the normalized form of a
394	// particular version constraint, which is the form we require in dependency
395	// lock files. Therefore the canonical forms produced here are a compatibility
396	// constraint for the dependency lock file parser.
397
398	if len(spec) == 0 {
399		return ""
400	}
401
402	// VersionConstraints values are typically assembled by combining together
403	// the version constraints from many separate declarations throughout
404	// a configuration, across many modules. As a consequence, they typically
405	// contain duplicates and the terms inside are in no particular order.
406	// For our canonical representation we'll both deduplicate the items
407	// and sort them into a consistent order.
408	sels := make(map[constraints.SelectionSpec]struct{})
409	for _, sel := range spec {
410		// The parser allows writing abbreviated version (such as 2) which
411		// end up being represented in memory with trailing unconstrained parts
412		// (for example 2.*.*). For the purpose of serialization with Ruby
413		// style syntax, these unconstrained parts can all be represented as 0
414		// with no loss of meaning, so we make that conversion here. Doing so
415		// allows us to deduplicate equivalent constraints, such as >= 2.0 and
416		// >= 2.0.0.
417		normalizedSel := constraints.SelectionSpec{
418			Operator: sel.Operator,
419			Boundary: sel.Boundary.ConstrainToZero(),
420		}
421		sels[normalizedSel] = struct{}{}
422	}
423	selsOrder := make([]constraints.SelectionSpec, 0, len(sels))
424	for sel := range sels {
425		selsOrder = append(selsOrder, sel)
426	}
427	sort.Slice(selsOrder, func(i, j int) bool {
428		is, js := selsOrder[i], selsOrder[j]
429		boundaryCmp := versionSelectionBoundaryCompare(is.Boundary, js.Boundary)
430		if boundaryCmp == 0 {
431			// The operator is the decider, then.
432			return versionSelectionOperatorLess(is.Operator, js.Operator)
433		}
434		return boundaryCmp < 0
435	})
436
437	var b strings.Builder
438	for i, sel := range selsOrder {
439		if i > 0 {
440			b.WriteString(", ")
441		}
442		switch sel.Operator {
443		case constraints.OpGreaterThan:
444			b.WriteString("> ")
445		case constraints.OpLessThan:
446			b.WriteString("< ")
447		case constraints.OpGreaterThanOrEqual:
448			b.WriteString(">= ")
449		case constraints.OpGreaterThanOrEqualPatchOnly, constraints.OpGreaterThanOrEqualMinorOnly:
450			// These two differ in how the version is written, not in the symbol.
451			b.WriteString("~> ")
452		case constraints.OpLessThanOrEqual:
453			b.WriteString("<= ")
454		case constraints.OpEqual:
455			b.WriteString("")
456		case constraints.OpNotEqual:
457			b.WriteString("!= ")
458		default:
459			// The above covers all of the operators we support during
460			// parsing, so we should not get here.
461			b.WriteString("??? ")
462		}
463
464		// We use a different constraint operator to distinguish between the
465		// two types of pessimistic constraint: minor-only and patch-only. For
466		// minor-only constraints, we always want to display only the major and
467		// minor version components, so we special-case that operator below.
468		//
469		// One final edge case is a minor-only constraint specified with only
470		// the major version, such as ~> 2. We treat this the same as ~> 2.0,
471		// because a major-only pessimistic constraint does not exist: it is
472		// logically identical to >= 2.0.0.
473		if sel.Operator == constraints.OpGreaterThanOrEqualMinorOnly {
474			// The minor-pessimistic syntax uses only two version components.
475			fmt.Fprintf(&b, "%s.%s", sel.Boundary.Major, sel.Boundary.Minor)
476		} else {
477			fmt.Fprintf(&b, "%s.%s.%s", sel.Boundary.Major, sel.Boundary.Minor, sel.Boundary.Patch)
478		}
479		if sel.Boundary.Prerelease != "" {
480			b.WriteString("-" + sel.Boundary.Prerelease)
481		}
482		if sel.Boundary.Metadata != "" {
483			b.WriteString("+" + sel.Boundary.Metadata)
484		}
485	}
486	return b.String()
487}
488
489// Our sort for selection operators is somewhat arbitrary and mainly motivated
490// by consistency rather than meaning, but this ordering does at least try
491// to make it so "simple" constraint sets will appear how a human might
492// typically write them, with the lower bounds first and the upper bounds
493// last. Weird mixtures of different sorts of constraints will likely seem
494// less intuitive, but they'd be unintuitive no matter the ordering.
495var versionSelectionsBoundaryPriority = map[constraints.SelectionOp]int{
496	// We skip zero here so that if we end up seeing an invalid
497	// operator (which the string function would render as "???")
498	// then it will have index zero and thus appear first.
499	constraints.OpGreaterThan:                 1,
500	constraints.OpGreaterThanOrEqual:          2,
501	constraints.OpEqual:                       3,
502	constraints.OpGreaterThanOrEqualPatchOnly: 4,
503	constraints.OpGreaterThanOrEqualMinorOnly: 5,
504	constraints.OpLessThanOrEqual:             6,
505	constraints.OpLessThan:                    7,
506	constraints.OpNotEqual:                    8,
507}
508
509func versionSelectionOperatorLess(i, j constraints.SelectionOp) bool {
510	iPrio := versionSelectionsBoundaryPriority[i]
511	jPrio := versionSelectionsBoundaryPriority[j]
512	return iPrio < jPrio
513}
514
515func versionSelectionBoundaryCompare(i, j constraints.VersionSpec) int {
516	// In the Ruby-style constraint syntax, unconstrained parts appear
517	// only for omitted portions of a version string, like writing
518	// "2" instead of "2.0.0". For sorting purposes we'll just
519	// consider those as zero, which also matches how we serialize them
520	// to strings.
521	i, j = i.ConstrainToZero(), j.ConstrainToZero()
522
523	// Once we've removed any unconstrained parts, we can safely
524	// convert to our main Version type so we can use its ordering.
525	iv := Version{
526		Major:      i.Major.Num,
527		Minor:      i.Minor.Num,
528		Patch:      i.Patch.Num,
529		Prerelease: versions.VersionExtra(i.Prerelease),
530		Metadata:   versions.VersionExtra(i.Metadata),
531	}
532	jv := Version{
533		Major:      j.Major.Num,
534		Minor:      j.Minor.Num,
535		Patch:      j.Patch.Num,
536		Prerelease: versions.VersionExtra(j.Prerelease),
537		Metadata:   versions.VersionExtra(j.Metadata),
538	}
539	if iv.Same(jv) {
540		// Although build metadata doesn't normally weigh in to
541		// precedence choices, we'll use it for our visual
542		// ordering just because we need to pick _some_ order.
543		switch {
544		case iv.Metadata.Raw() == jv.Metadata.Raw():
545			return 0
546		case iv.Metadata.LessThan(jv.Metadata):
547			return -1
548		default:
549			return 1 // greater, by elimination
550		}
551	}
552	switch {
553	case iv.LessThan(jv):
554		return -1
555	default:
556		return 1 // greater, by elimination
557	}
558}
559