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 }