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 }