github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/getproviders/types.go (about)

     1  package getproviders
     2  
     3  import (
     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/eliastor/durgaform/internal/addrs"
    13  )
    14  
    15  // Version represents a particular single version of a provider.
    16  type Version = versions.Version
    17  
    18  // UnspecifiedVersion is the zero value of Version, representing the absense
    19  // of a version number.
    20  var UnspecifiedVersion Version = versions.Unspecified
    21  
    22  // VersionList represents a list of versions. It is a []Version with some
    23  // extra methods for convenient filtering.
    24  type 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.
    29  type VersionSet = versions.Set
    30  
    31  // VersionConstraints represents a set of version constraints, which can
    32  // define the membership of a VersionSet by exclusion.
    33  type VersionConstraints = constraints.IntersectionSpec
    34  
    35  // Warnings represents a list of warnings returned by a Registry source.
    36  type 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.
    46  type 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.
    54  func (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.
    70  type 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.
    74  func 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.
    80  func 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.
    90  func 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.
    96  func 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.
   107  func 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.
   113  type Platform struct {
   114  	OS, Arch string
   115  }
   116  
   117  func (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.
   128  func (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.
   139  func ParsePlatform(str string) (Platform, error) {
   140  	parts := strings.Split(str, "_")
   141  	if len(parts) != 2 {
   142  		return Platform{}, fmt.Errorf("must be two words separated by an underscore")
   143  	}
   144  
   145  	os, arch := parts[0], parts[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.
   163  var 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 Durgaform before trying to use the
   175  // described package.
   176  type 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.
   212  func (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.
   232  func (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.
   243  func (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 Durgaform 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.
   272  func (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.
   283  type 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 Durgaform is running.
   292  type PackageLocalArchive string
   293  
   294  func (p PackageLocalArchive) packageLocation() {}
   295  func (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 Durgaform is running.
   301  type PackageLocalDir string
   302  
   303  func (p PackageLocalDir) packageLocation() {}
   304  func (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.
   308  type PackageHTTPURL string
   309  
   310  func (p PackageHTTPURL) packageLocation() {}
   311  func (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.
   315  type PackageMetaList []PackageMeta
   316  
   317  func (l PackageMetaList) Len() int {
   318  	return len(l)
   319  }
   320  
   321  func (l PackageMetaList) Less(i, j int) bool {
   322  	return l[i].LessThan(l[j])
   323  }
   324  
   325  func (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.
   333  func (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.
   342  func (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.
   359  func (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.
   372  func (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.
   384  func 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  	// Durgaform 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.
   495  var 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  
   509  func versionSelectionOperatorLess(i, j constraints.SelectionOp) bool {
   510  	iPrio := versionSelectionsBoundaryPriority[i]
   511  	jPrio := versionSelectionsBoundaryPriority[j]
   512  	return iPrio < jPrio
   513  }
   514  
   515  func 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  }