github.com/opentofu/opentofu@v1.7.1/internal/getproviders/types.go (about)

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