github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/providercache/installer.go (about) 1 package providercache 2 3 import ( 4 "context" 5 "fmt" 6 "sort" 7 "strings" 8 9 "github.com/apparentlymart/go-versions/versions" 10 11 "github.com/eliastor/durgaform/internal/addrs" 12 copydir "github.com/eliastor/durgaform/internal/copy" 13 "github.com/eliastor/durgaform/internal/depsfile" 14 "github.com/eliastor/durgaform/internal/getproviders" 15 ) 16 17 // Installer is the main type in this package, representing a provider installer 18 // with a particular configuration-specific cache directory and an optional 19 // global cache directory. 20 type Installer struct { 21 // targetDir is the cache directory we're ultimately aiming to get the 22 // requested providers installed into. 23 targetDir *Dir 24 25 // source is the provider source that the installer will use to discover 26 // what provider versions are available for installation and to 27 // find the source locations for any versions that are not already 28 // available via one of the cache directories. 29 source getproviders.Source 30 31 // globalCacheDir is an optional additional directory that will, if 32 // provided, be treated as a read-through cache when retrieving new 33 // provider versions. That is, new packages are fetched into this 34 // directory first and then linked into targetDir, which allows sharing 35 // both the disk space and the download time for a particular provider 36 // version between different configurations on the same system. 37 globalCacheDir *Dir 38 39 // builtInProviderTypes is an optional set of types that should be 40 // considered valid to appear in the special durgaform.io/builtin/... 41 // namespace, which we use for providers that are built in to Durgaform 42 // and thus do not need any separate installation step. 43 builtInProviderTypes []string 44 45 // unmanagedProviderTypes is a set of provider addresses that should be 46 // considered implemented, but that Durgaform does not manage the 47 // lifecycle for, and therefore does not need to worry about the 48 // installation of. 49 unmanagedProviderTypes map[addrs.Provider]struct{} 50 } 51 52 // NewInstaller constructs and returns a new installer with the given target 53 // directory and provider source. 54 // 55 // A newly-created installer does not have a global cache directory configured, 56 // but a caller can make a follow-up call to SetGlobalCacheDir to provide 57 // one prior to taking any installation actions. 58 // 59 // The target directory MUST NOT also be an input consulted by the given source, 60 // or the result is undefined. 61 func NewInstaller(targetDir *Dir, source getproviders.Source) *Installer { 62 return &Installer{ 63 targetDir: targetDir, 64 source: source, 65 } 66 } 67 68 // Clone returns a new Installer which has the a new target directory but 69 // the same optional global cache directory, the same installation sources, 70 // and the same built-in/unmanaged providers. The result can be mutated further 71 // using the various setter methods without affecting the original. 72 func (i *Installer) Clone(targetDir *Dir) *Installer { 73 // For now all of our setter methods just overwrite field values in 74 // their entirety, rather than mutating things on the other side of 75 // the shared pointers, and so we can safely just shallow-copy the 76 // root. We might need to be more careful here if in future we add 77 // methods that allow deeper mutations through the stored pointers. 78 ret := *i 79 ret.targetDir = targetDir 80 return &ret 81 } 82 83 // ProviderSource returns the getproviders.Source that the installer would 84 // use for installing any new providers. 85 func (i *Installer) ProviderSource() getproviders.Source { 86 return i.source 87 } 88 89 // SetGlobalCacheDir activates a second tier of caching for the receiving 90 // installer, with the given directory used as a read-through cache for 91 // installation operations that need to retrieve new packages. 92 // 93 // The global cache directory for an installer must never be the same as its 94 // target directory, and must not be used as one of its provider sources. 95 // If these overlap then undefined behavior will result. 96 func (i *Installer) SetGlobalCacheDir(cacheDir *Dir) { 97 // A little safety check to catch straightforward mistakes where the 98 // directories overlap. Better to panic early than to do 99 // possibly-distructive actions on the cache directory downstream. 100 if same, err := copydir.SameFile(i.targetDir.baseDir, cacheDir.baseDir); err == nil && same { 101 panic(fmt.Sprintf("global cache directory %s must not match the installation target directory %s", cacheDir.baseDir, i.targetDir.baseDir)) 102 } 103 i.globalCacheDir = cacheDir 104 } 105 106 // HasGlobalCacheDir returns true if someone has previously called 107 // SetGlobalCacheDir to configure a global cache directory for this installer. 108 func (i *Installer) HasGlobalCacheDir() bool { 109 return i.globalCacheDir != nil 110 } 111 112 // SetBuiltInProviderTypes tells the receiver to consider the type names in the 113 // given slice to be valid as providers in the special special 114 // durgaform.io/builtin/... namespace that we use for providers that are 115 // built in to Durgaform and thus do not need a separate installation step. 116 // 117 // If a caller requests installation of a provider in that namespace, the 118 // installer will treat it as a no-op if its name exists in this list, but 119 // will produce an error if it does not. 120 // 121 // The default, if this method isn't called, is for there to be no valid 122 // builtin providers. 123 // 124 // Do not modify the buffer under the given slice after passing it to this 125 // method. 126 func (i *Installer) SetBuiltInProviderTypes(types []string) { 127 i.builtInProviderTypes = types 128 } 129 130 // SetUnmanagedProviderTypes tells the receiver to consider the providers 131 // indicated by the passed addrs.Providers as unmanaged. Durgaform does not 132 // need to control the lifecycle of these providers, and they are assumed to be 133 // running already when Durgaform is started. Because these are essentially 134 // processes, not binaries, Durgaform will not do any work to ensure presence 135 // or versioning of these binaries. 136 func (i *Installer) SetUnmanagedProviderTypes(types map[addrs.Provider]struct{}) { 137 i.unmanagedProviderTypes = types 138 } 139 140 // EnsureProviderVersions compares the given provider requirements with what 141 // is already available in the installer's target directory and then takes 142 // appropriate installation actions to ensure that suitable packages 143 // are available in the target cache directory. 144 // 145 // The given mode modifies how the operation will treat providers that already 146 // have acceptable versions available in the target cache directory. See the 147 // documentation for InstallMode and the InstallMode values for more 148 // information. 149 // 150 // The given context can be used to cancel the overall installation operation 151 // (causing any operations in progress to fail with an error), and can also 152 // include an InstallerEvents value for optional intermediate progress 153 // notifications. 154 // 155 // If a given InstallerEvents subscribes to notifications about installation 156 // failures then those notifications will be redundant with the ones included 157 // in the final returned error value so callers should show either one or the 158 // other, and not both. 159 func (i *Installer) EnsureProviderVersions(ctx context.Context, locks *depsfile.Locks, reqs getproviders.Requirements, mode InstallMode) (*depsfile.Locks, error) { 160 errs := map[addrs.Provider]error{} 161 evts := installerEventsForContext(ctx) 162 163 // We'll work with a copy of the given locks, so we can modify it and 164 // return the updated locks without affecting the caller's object. 165 // We'll add, replace, or remove locks in here during our work so that the 166 // final locks file reflects what the installer has selected. 167 locks = locks.DeepCopy() 168 169 if cb := evts.PendingProviders; cb != nil { 170 cb(reqs) 171 } 172 173 // Step 1: Which providers might we need to fetch a new version of? 174 // This produces the subset of requirements we need to ask the provider 175 // source about. If we're in the normal (non-upgrade) mode then we'll 176 // just ask the source to confirm the continued existence of what 177 // was locked, or otherwise we'll find the newest version matching the 178 // configured version constraint. 179 mightNeed := map[addrs.Provider]getproviders.VersionSet{} 180 locked := map[addrs.Provider]bool{} 181 for provider, versionConstraints := range reqs { 182 if provider.IsBuiltIn() { 183 // Built in providers do not require installation but we'll still 184 // verify that the requested provider name is valid. 185 valid := false 186 for _, name := range i.builtInProviderTypes { 187 if name == provider.Type { 188 valid = true 189 break 190 } 191 } 192 var err error 193 if valid { 194 if len(versionConstraints) == 0 { 195 // Other than reporting an event for the outcome of this 196 // provider, we'll do nothing else with it: it's just 197 // automatically available for use. 198 if cb := evts.BuiltInProviderAvailable; cb != nil { 199 cb(provider) 200 } 201 } else { 202 // A built-in provider is not permitted to have an explicit 203 // version constraint, because we can only use the version 204 // that is built in to the current Durgaform release. 205 err = fmt.Errorf("built-in providers do not support explicit version constraints") 206 } 207 } else { 208 err = fmt.Errorf("this Durgaform release has no built-in provider named %q", provider.Type) 209 } 210 if err != nil { 211 errs[provider] = err 212 if cb := evts.BuiltInProviderFailure; cb != nil { 213 cb(provider, err) 214 } 215 } 216 continue 217 } 218 if _, ok := i.unmanagedProviderTypes[provider]; ok { 219 // unmanaged providers do not require installation 220 continue 221 } 222 acceptableVersions := versions.MeetingConstraints(versionConstraints) 223 if !mode.forceQueryAllProviders() { 224 // If we're not forcing potential changes of version then an 225 // existing selection from the lock file takes priority over 226 // the currently-configured version constraints. 227 if lock := locks.Provider(provider); lock != nil { 228 if !acceptableVersions.Has(lock.Version()) { 229 err := fmt.Errorf( 230 "locked provider %s %s does not match configured version constraint %s; must use durgaform init -upgrade to allow selection of new versions", 231 provider, lock.Version(), getproviders.VersionConstraintsString(versionConstraints), 232 ) 233 errs[provider] = err 234 // This is a funny case where we're returning an error 235 // before we do any querying at all. To keep the event 236 // stream consistent without introducing an extra event 237 // type, we'll emit an artificial QueryPackagesBegin for 238 // this provider before we indicate that it failed using 239 // QueryPackagesFailure. 240 if cb := evts.QueryPackagesBegin; cb != nil { 241 cb(provider, versionConstraints, true) 242 } 243 if cb := evts.QueryPackagesFailure; cb != nil { 244 cb(provider, err) 245 } 246 continue 247 } 248 acceptableVersions = versions.Only(lock.Version()) 249 locked[provider] = true 250 } 251 } 252 mightNeed[provider] = acceptableVersions 253 } 254 255 // Step 2: Query the provider source for each of the providers we selected 256 // in the first step and select the latest available version that is 257 // in the set of acceptable versions. 258 // 259 // This produces a set of packages to install to our cache in the next step. 260 need := map[addrs.Provider]getproviders.Version{} 261 NeedProvider: 262 for provider, acceptableVersions := range mightNeed { 263 if err := ctx.Err(); err != nil { 264 // If our context has been cancelled or reached a timeout then 265 // we'll abort early, because subsequent operations against 266 // that context will fail immediately anyway. 267 return nil, err 268 } 269 270 if cb := evts.QueryPackagesBegin; cb != nil { 271 cb(provider, reqs[provider], locked[provider]) 272 } 273 available, warnings, err := i.source.AvailableVersions(ctx, provider) 274 if err != nil { 275 // TODO: Consider retrying a few times for certain types of 276 // source errors that seem likely to be transient. 277 errs[provider] = err 278 if cb := evts.QueryPackagesFailure; cb != nil { 279 cb(provider, err) 280 } 281 // We will take no further actions for this provider. 282 continue 283 } 284 if len(warnings) > 0 { 285 if cb := evts.QueryPackagesWarning; cb != nil { 286 cb(provider, warnings) 287 } 288 } 289 available.Sort() // put the versions in increasing order of precedence 290 for i := len(available) - 1; i >= 0; i-- { // walk backwards to consider newer versions first 291 if acceptableVersions.Has(available[i]) { 292 need[provider] = available[i] 293 if cb := evts.QueryPackagesSuccess; cb != nil { 294 cb(provider, available[i]) 295 } 296 continue NeedProvider 297 } 298 } 299 // If we get here then the source has no packages that meet the given 300 // version constraint, which we model as a query error. 301 if locked[provider] { 302 // This situation should be a rare one: it suggests that a 303 // version was previously available but was yanked for some 304 // reason. 305 lock := locks.Provider(provider) 306 err = fmt.Errorf("the previously-selected version %s is no longer available", lock.Version()) 307 } else { 308 err = fmt.Errorf("no available releases match the given constraints %s", getproviders.VersionConstraintsString(reqs[provider])) 309 } 310 errs[provider] = err 311 if cb := evts.QueryPackagesFailure; cb != nil { 312 cb(provider, err) 313 } 314 } 315 316 // Step 3: For each provider version we've decided we need to install, 317 // install its package into our target cache (possibly via the global cache). 318 authResults := map[addrs.Provider]*getproviders.PackageAuthenticationResult{} // record auth results for all successfully fetched providers 319 targetPlatform := i.targetDir.targetPlatform // we inherit this to behave correctly in unit tests 320 for provider, version := range need { 321 if err := ctx.Err(); err != nil { 322 // If our context has been cancelled or reached a timeout then 323 // we'll abort early, because subsequent operations against 324 // that context will fail immediately anyway. 325 return nil, err 326 } 327 328 lock := locks.Provider(provider) 329 var preferredHashes []getproviders.Hash 330 if lock != nil && lock.Version() == version { // hash changes are expected if the version is also changing 331 preferredHashes = lock.PreferredHashes() 332 } 333 334 // If our target directory already has the provider version that fulfills the lock file, carry on 335 if installed := i.targetDir.ProviderVersion(provider, version); installed != nil { 336 if len(preferredHashes) > 0 { 337 if matches, _ := installed.MatchesAnyHash(preferredHashes); matches { 338 if cb := evts.ProviderAlreadyInstalled; cb != nil { 339 cb(provider, version) 340 } 341 continue 342 } 343 } 344 } 345 346 if i.globalCacheDir != nil { 347 // Step 3a: If our global cache already has this version available then 348 // we'll just link it in. 349 if cached := i.globalCacheDir.ProviderVersion(provider, version); cached != nil { 350 if cb := evts.LinkFromCacheBegin; cb != nil { 351 cb(provider, version, i.globalCacheDir.baseDir) 352 } 353 if _, err := cached.ExecutableFile(); err != nil { 354 err := fmt.Errorf("provider binary not found: %s", err) 355 errs[provider] = err 356 if cb := evts.LinkFromCacheFailure; cb != nil { 357 cb(provider, version, err) 358 } 359 continue 360 } 361 362 err := i.targetDir.LinkFromOtherCache(cached, preferredHashes) 363 if err != nil { 364 errs[provider] = err 365 if cb := evts.LinkFromCacheFailure; cb != nil { 366 cb(provider, version, err) 367 } 368 continue 369 } 370 // We'll fetch what we just linked to make sure it actually 371 // did show up there. 372 new := i.targetDir.ProviderVersion(provider, version) 373 if new == nil { 374 err := fmt.Errorf("after linking %s from provider cache at %s it is still not detected in the target directory; this is a bug in Durgaform", provider, i.globalCacheDir.baseDir) 375 errs[provider] = err 376 if cb := evts.LinkFromCacheFailure; cb != nil { 377 cb(provider, version, err) 378 } 379 continue 380 } 381 382 // The LinkFromOtherCache call above should've verified that 383 // the package matches one of the hashes previously recorded, 384 // if any. We'll now augment those hashes with one freshly 385 // calculated from the package we just linked, which allows 386 // the lock file to gradually transition to recording newer hash 387 // schemes when they become available. 388 var priorHashes []getproviders.Hash 389 if lock != nil && lock.Version() == version { 390 // If the version we're installing is identical to the 391 // one we previously locked then we'll keep all of the 392 // hashes we saved previously and add to it. Otherwise 393 // we'll be starting fresh, because each version has its 394 // own set of packages and thus its own hashes. 395 priorHashes = append(priorHashes, preferredHashes...) 396 397 // NOTE: The behavior here is unfortunate when a particular 398 // provider version was already cached on the first time 399 // the current configuration requested it, because that 400 // means we don't currently get the opportunity to fetch 401 // and verify the checksums for the new package from 402 // upstream. That's currently unavoidable because upstream 403 // checksums are in the "ziphash" format and so we can't 404 // verify them against our cache directory's unpacked 405 // packages: we'd need to go fetch the package from the 406 // origin and compare against it, which would defeat the 407 // purpose of the global cache. 408 // 409 // If we fetch from upstream on the first encounter with 410 // a particular provider then we'll end up in the other 411 // codepath below where we're able to also include the 412 // checksums from the origin registry. 413 } 414 newHash, err := cached.Hash() 415 if err != nil { 416 err := fmt.Errorf("after linking %s from provider cache at %s, failed to compute a checksum for it: %s", provider, i.globalCacheDir.baseDir, err) 417 errs[provider] = err 418 if cb := evts.LinkFromCacheFailure; cb != nil { 419 cb(provider, version, err) 420 } 421 continue 422 } 423 // The hashes slice gets deduplicated in the lock file 424 // implementation, so we don't worry about potentially 425 // creating a duplicate here. 426 var newHashes []getproviders.Hash 427 newHashes = append(newHashes, priorHashes...) 428 newHashes = append(newHashes, newHash) 429 locks.SetProvider(provider, version, reqs[provider], newHashes) 430 if cb := evts.ProvidersLockUpdated; cb != nil { 431 // We want to ensure that newHash and priorHashes are 432 // sorted. newHash is a single value, so it's definitely 433 // sorted. priorHashes are pulled from the lock file, so 434 // are also already sorted. 435 cb(provider, version, []getproviders.Hash{newHash}, nil, priorHashes) 436 } 437 438 if cb := evts.LinkFromCacheSuccess; cb != nil { 439 cb(provider, version, new.PackageDir) 440 } 441 continue // Don't need to do full install, then. 442 } 443 } 444 445 // Step 3b: Get the package metadata for the selected version from our 446 // provider source. 447 // 448 // This is the step where we might detect and report that the provider 449 // isn't available for the current platform. 450 if cb := evts.FetchPackageMeta; cb != nil { 451 cb(provider, version) 452 } 453 meta, err := i.source.PackageMeta(ctx, provider, version, targetPlatform) 454 if err != nil { 455 errs[provider] = err 456 if cb := evts.FetchPackageFailure; cb != nil { 457 cb(provider, version, err) 458 } 459 continue 460 } 461 462 // Step 3c: Retrieve the package indicated by the metadata we received, 463 // either directly into our target directory or via the global cache 464 // directory. 465 if cb := evts.FetchPackageBegin; cb != nil { 466 cb(provider, version, meta.Location) 467 } 468 var installTo, linkTo *Dir 469 if i.globalCacheDir != nil { 470 installTo = i.globalCacheDir 471 linkTo = i.targetDir 472 } else { 473 installTo = i.targetDir 474 linkTo = nil // no linking needed 475 } 476 477 allowedHashes := preferredHashes 478 if mode.forceInstallChecksums() { 479 allowedHashes = []getproviders.Hash{} 480 } 481 482 authResult, err := installTo.InstallPackage(ctx, meta, allowedHashes) 483 if err != nil { 484 // TODO: Consider retrying for certain kinds of error that seem 485 // likely to be transient. For now, we just treat all errors equally. 486 errs[provider] = err 487 if cb := evts.FetchPackageFailure; cb != nil { 488 cb(provider, version, err) 489 } 490 continue 491 } 492 new := installTo.ProviderVersion(provider, version) 493 if new == nil { 494 err := fmt.Errorf("after installing %s it is still not detected in the target directory; this is a bug in Durgaform", provider) 495 errs[provider] = err 496 if cb := evts.FetchPackageFailure; cb != nil { 497 cb(provider, version, err) 498 } 499 continue 500 } 501 if _, err := new.ExecutableFile(); err != nil { 502 err := fmt.Errorf("provider binary not found: %s", err) 503 errs[provider] = err 504 if cb := evts.FetchPackageFailure; cb != nil { 505 cb(provider, version, err) 506 } 507 continue 508 } 509 if linkTo != nil { 510 // We skip emitting the "LinkFromCache..." events here because 511 // it's simpler for the caller to treat them as mutually exclusive. 512 // We can just subsume the linking step under the "FetchPackage..." 513 // series here (and that's why we use FetchPackageFailure below). 514 // We also don't do a hash check here because we already did that 515 // as part of the installTo.InstallPackage call above. 516 err := linkTo.LinkFromOtherCache(new, nil) 517 if err != nil { 518 errs[provider] = err 519 if cb := evts.FetchPackageFailure; cb != nil { 520 cb(provider, version, err) 521 } 522 continue 523 } 524 } 525 authResults[provider] = authResult 526 527 // The InstallPackage call above should've verified that 528 // the package matches one of the hashes previously recorded, 529 // if any. We'll now augment those hashes with a new set populated 530 // with the hashes returned by the upstream source and from the 531 // package we've just installed, which allows the lock file to 532 // gradually transition to newer hash schemes when they become 533 // available. 534 // 535 // This is assuming that if a package matches both a hash we saw before 536 // _and_ a new hash then the new hash is a valid substitute for 537 // the previous hash. 538 // 539 // The hashes slice gets deduplicated in the lock file 540 // implementation, so we don't worry about potentially 541 // creating duplicates here. 542 var priorHashes []getproviders.Hash 543 if lock != nil && lock.Version() == version { 544 // If the version we're installing is identical to the 545 // one we previously locked then we'll keep all of the 546 // hashes we saved previously and add to it. Otherwise 547 // we'll be starting fresh, because each version has its 548 // own set of packages and thus its own hashes. 549 priorHashes = append(priorHashes, preferredHashes...) 550 } 551 newHash, err := new.Hash() 552 if err != nil { 553 err := fmt.Errorf("after installing %s, failed to compute a checksum for it: %s", provider, err) 554 errs[provider] = err 555 if cb := evts.FetchPackageFailure; cb != nil { 556 cb(provider, version, err) 557 } 558 continue 559 } 560 561 var signedHashes []getproviders.Hash 562 if authResult.SignedByAnyParty() { 563 // We'll trust new hashes from upstream only if they were verified 564 // as signed by a suitable key. Otherwise, we'd record only 565 // a new hash we just calculated ourselves from the bytes on disk, 566 // and so the hashes would cover only the current platform. 567 signedHashes = append(signedHashes, meta.AcceptableHashes()...) 568 } 569 570 var newHashes []getproviders.Hash 571 newHashes = append(newHashes, newHash) 572 newHashes = append(newHashes, priorHashes...) 573 newHashes = append(newHashes, signedHashes...) 574 575 locks.SetProvider(provider, version, reqs[provider], newHashes) 576 if cb := evts.ProvidersLockUpdated; cb != nil { 577 // newHash and priorHashes are already sorted. 578 // But we do need to sort signedHashes so we can reason about it 579 // sensibly. 580 sort.Slice(signedHashes, func(i, j int) bool { 581 return string(signedHashes[i]) < string(signedHashes[j]) 582 }) 583 584 cb(provider, version, []getproviders.Hash{newHash}, signedHashes, priorHashes) 585 } 586 587 if cb := evts.FetchPackageSuccess; cb != nil { 588 cb(provider, version, new.PackageDir, authResult) 589 } 590 } 591 592 // Emit final event for fetching if any were successfully fetched 593 if cb := evts.ProvidersFetched; cb != nil && len(authResults) > 0 { 594 cb(authResults) 595 } 596 597 // Finally, if the lock structure contains locks for any providers that 598 // are no longer needed by this configuration, we'll remove them. This 599 // is important because we will not have installed those providers 600 // above and so a lock file still containing them would make the working 601 // directory invalid: not every provider in the lock file is available 602 // for use. 603 for providerAddr := range locks.AllProviders() { 604 if _, ok := reqs[providerAddr]; !ok { 605 locks.RemoveProvider(providerAddr) 606 } 607 } 608 609 if len(errs) > 0 { 610 return locks, InstallerError{ 611 ProviderErrors: errs, 612 } 613 } 614 return locks, nil 615 } 616 617 // InstallMode customizes the details of how an install operation treats 618 // providers that have versions already cached in the target directory. 619 type InstallMode rune 620 621 const ( 622 // InstallNewProvidersOnly is an InstallMode that causes the installer 623 // to accept any existing version of a requested provider that is already 624 // cached as long as it's in the given version sets, without checking 625 // whether new versions are available that are also in the given version 626 // sets. 627 InstallNewProvidersOnly InstallMode = 'N' 628 629 // InstallNewProvidersForce is an InstallMode that follows the same 630 // logic as InstallNewProvidersOnly except it does not verify existing 631 // checksums but force installs new checksums for all given providers. 632 InstallNewProvidersForce InstallMode = 'F' 633 634 // InstallUpgrades is an InstallMode that causes the installer to check 635 // all requested providers to see if new versions are available that 636 // are also in the given version sets, even if a suitable version of 637 // a given provider is already available. 638 InstallUpgrades InstallMode = 'U' 639 ) 640 641 func (m InstallMode) forceQueryAllProviders() bool { 642 return m == InstallUpgrades 643 } 644 645 func (m InstallMode) forceInstallChecksums() bool { 646 return m == InstallNewProvidersForce 647 } 648 649 // InstallerError is an error type that may be returned (but is not guaranteed) 650 // from Installer.EnsureProviderVersions to indicate potentially several 651 // separate failed installation outcomes for different providers included in 652 // the overall request. 653 type InstallerError struct { 654 ProviderErrors map[addrs.Provider]error 655 } 656 657 func (err InstallerError) Error() string { 658 addrs := make([]addrs.Provider, 0, len(err.ProviderErrors)) 659 for addr := range err.ProviderErrors { 660 addrs = append(addrs, addr) 661 } 662 sort.Slice(addrs, func(i, j int) bool { 663 return addrs[i].LessThan(addrs[j]) 664 }) 665 var b strings.Builder 666 b.WriteString("some providers could not be installed:\n") 667 for _, addr := range addrs { 668 providerErr := err.ProviderErrors[addr] 669 fmt.Fprintf(&b, "- %s: %s\n", addr, providerErr) 670 } 671 return strings.TrimSpace(b.String()) 672 }