Files
opentf/internal/providercache/installer.go
Christian Mesh 11694a6ac0 Alternate approach to linking and locking the global cache (#2708)
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2025-05-08 15:26:46 -04:00

819 lines
31 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package providercache
import (
"context"
"fmt"
"log"
"slices"
"sort"
"strings"
"github.com/apparentlymart/go-versions/versions"
otelAttr "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/opentofu/opentofu/internal/addrs"
copydir "github.com/opentofu/opentofu/internal/copy"
"github.com/opentofu/opentofu/internal/depsfile"
"github.com/opentofu/opentofu/internal/getproviders"
"github.com/opentofu/opentofu/internal/tracing"
"github.com/opentofu/opentofu/internal/tracing/traceattrs"
)
// Installer is the main type in this package, representing a provider installer
// with a particular configuration-specific cache directory and an optional
// global cache directory.
type Installer struct {
// targetDir is the cache directory we're ultimately aiming to get the
// requested providers installed into.
targetDir *Dir
// source is the provider source that the installer will use to discover
// what provider versions are available for installation and to
// find the source locations for any versions that are not already
// available via one of the cache directories.
source getproviders.Source
// globalCacheDir is an optional additional directory that will, if
// provided, be treated as a read-through cache when retrieving new
// provider versions. That is, new packages are fetched into this
// directory first and then linked into targetDir, which allows sharing
// both the disk space and the download time for a particular provider
// version between different configurations on the same system.
globalCacheDir *Dir
// globalCacheDirMayBreakDependencyLockFile allows a temporary exception to
// the rule that an entry in globalCacheDir can normally only be used if
// its validity is already confirmed by an entry in the dependency lock
// file.
globalCacheDirMayBreakDependencyLockFile bool
// builtInProviderTypes is an optional set of types that should be
// considered valid to appear in the special terraform.io/builtin/...
// namespace, which we use for providers that are built in to OpenTofu
// and thus do not need any separate installation step.
builtInProviderTypes []string
// unmanagedProviderTypes is a set of provider addresses that should be
// considered implemented, but that OpenTofu does not manage the
// lifecycle for, and therefore does not need to worry about the
// installation of.
unmanagedProviderTypes map[addrs.Provider]struct{}
}
// NewInstaller constructs and returns a new installer with the given target
// directory and provider source.
//
// A newly-created installer does not have a global cache directory configured,
// but a caller can make a follow-up call to SetGlobalCacheDir to provide
// one prior to taking any installation actions.
//
// The target directory MUST NOT also be an input consulted by the given source,
// or the result is undefined.
func NewInstaller(targetDir *Dir, source getproviders.Source) *Installer {
return &Installer{
targetDir: targetDir,
source: source,
}
}
// Clone returns a new Installer which has the a new target directory but
// the same optional global cache directory, the same installation sources,
// and the same built-in/unmanaged providers. The result can be mutated further
// using the various setter methods without affecting the original.
func (i *Installer) Clone(targetDir *Dir) *Installer {
// For now all of our setter methods just overwrite field values in
// their entirety, rather than mutating things on the other side of
// the shared pointers, and so we can safely just shallow-copy the
// root. We might need to be more careful here if in future we add
// methods that allow deeper mutations through the stored pointers.
ret := *i
ret.targetDir = targetDir
return &ret
}
// ProviderSource returns the getproviders.Source that the installer would
// use for installing any new providers.
func (i *Installer) ProviderSource() getproviders.Source {
return i.source
}
// SetGlobalCacheDir activates a second tier of caching for the receiving
// installer, with the given directory used as a read-through cache for
// installation operations that need to retrieve new packages.
//
// The global cache directory for an installer must never be the same as its
// target directory, and must not be used as one of its provider sources.
// If these overlap then undefined behavior will result.
func (i *Installer) SetGlobalCacheDir(cacheDir *Dir) {
// A little safety check to catch straightforward mistakes where the
// directories overlap. Better to panic early than to do
// possibly-destructive actions on the cache directory downstream.
if same, err := copydir.SameFile(i.targetDir.baseDir, cacheDir.baseDir); err == nil && same {
panic(fmt.Sprintf("global cache directory %s must not match the installation target directory %s", cacheDir.baseDir, i.targetDir.baseDir))
}
i.globalCacheDir = cacheDir
}
// SetGlobalCacheDirMayBreakDependencyLockFile activates or deactivates our
// temporary exception to the rule that the global cache directory can be used
// only when entries are confirmed by existing entries in the dependency lock
// file.
//
// If this is set then if we install a provider for the first time from the
// cache then the dependency lock file will include only the checksum from
// the package in the global cache, which means the lock file won't be portable
// to OpenTofu running on another operating system or CPU architecture.
func (i *Installer) SetGlobalCacheDirMayBreakDependencyLockFile(mayBreak bool) {
i.globalCacheDirMayBreakDependencyLockFile = mayBreak
}
// HasGlobalCacheDir returns true if someone has previously called
// SetGlobalCacheDir to configure a global cache directory for this installer.
func (i *Installer) HasGlobalCacheDir() bool {
return i.globalCacheDir != nil
}
// SetBuiltInProviderTypes tells the receiver to consider the type names in the
// given slice to be valid as providers in the special special
// terraform.io/builtin/... namespace that we use for providers that are
// built in to OpenTofu and thus do not need a separate installation step.
//
// If a caller requests installation of a provider in that namespace, the
// installer will treat it as a no-op if its name exists in this list, but
// will produce an error if it does not.
//
// The default, if this method isn't called, is for there to be no valid
// builtin providers.
//
// Do not modify the buffer under the given slice after passing it to this
// method.
func (i *Installer) SetBuiltInProviderTypes(types []string) {
i.builtInProviderTypes = types
}
// SetUnmanagedProviderTypes tells the receiver to consider the providers
// indicated by the passed addrs.Providers as unmanaged. OpenTofu does not
// need to control the lifecycle of these providers, and they are assumed to be
// running already when OpenTofu is started. Because these are essentially
// processes, not binaries, OpenTofu will not do any work to ensure presence
// or versioning of these binaries.
func (i *Installer) SetUnmanagedProviderTypes(types map[addrs.Provider]struct{}) {
i.unmanagedProviderTypes = types
}
// EnsureProviderVersions compares the given provider requirements with what
// is already available in the installer's target directory and then takes
// appropriate installation actions to ensure that suitable packages
// are available in the target cache directory.
//
// The given mode modifies how the operation will treat providers that already
// have acceptable versions available in the target cache directory. See the
// documentation for InstallMode and the InstallMode values for more
// information.
//
// The given context can be used to cancel the overall installation operation
// (causing any operations in progress to fail with an error), and can also
// include an InstallerEvents value for optional intermediate progress
// notifications.
//
// If a given InstallerEvents subscribes to notifications about installation
// failures then those notifications will be redundant with the ones included
// in the final returned error value so callers should show either one or the
// other, and not both.
func (i *Installer) EnsureProviderVersions(ctx context.Context, locks *depsfile.Locks, reqs getproviders.Requirements, mode InstallMode) (*depsfile.Locks, error) {
ctx, span := tracing.Tracer().Start(ctx, "Install Providers") // TODO: Discuss span name
defer span.End()
evts := installerEventsForContext(ctx)
// Whenever possible we prefer to collect separate errors for each
// problematic provider and then report them all together at the end,
// because that can allow an operator to notice a systematic problem
// across multiple providers, such as a particular registry failing
// in the same way regardless of which provider is requested.
//
// The other functions we call below will gradually add errors here
// as appropriate. Those functions only return an err directly
// themselves in situations that are not related to any particular
// provider and so prevent us from continuing further at all.
errs := map[addrs.Provider]error{}
// We'll work with a copy of the given locks, so we can modify it and
// return the updated locks without affecting the caller's object.
// We'll add, replace, or remove locks in here during our work so that the
// final locks file reflects what the installer has selected.
locks = locks.DeepCopy()
if cb := evts.PendingProviders; cb != nil {
cb(reqs)
}
// Step 1: Which providers might we need to fetch a new version of?
// This produces the subset of requirements we need to ask the provider
// source about. If we're in the normal (non-upgrade) mode then we'll
// just ask the source to confirm the continued existence of what
// was locked, or otherwise we'll find the newest version matching the
// configured version constraint.
mightNeed, locked := i.ensureProviderVersionsMightNeed(ctx, locks, reqs, mode, errs)
// Step 2: Query the provider source for each of the providers we selected
// in the first step and select the latest available version that is
// in the set of acceptable versions.
//
// This produces a set of packages to install to our cache in the next step.
need, err := i.ensureProviderVersionsNeed(ctx, locks, reqs, mightNeed, locked, errs)
if err != nil {
return nil, err
}
// Step 3: For each provider version we've decided we need to install,
// install its package into our target cache (possibly via the global cache).
targetPlatform := i.targetDir.targetPlatform // we inherit this to behave correctly in unit tests
span.SetAttributes(otelAttr.String(traceattrs.TargetPlatform, targetPlatform.String()))
span.SetName("Install Providers - " + targetPlatform.String())
authResults, err := i.ensureProviderVersionsInstall(ctx, locks, reqs, mode, need, targetPlatform, errs)
if err != nil {
return nil, err
}
// Emit final event for fetching if any were successfully fetched
if cb := evts.ProvidersAuthenticated; cb != nil && len(authResults) > 0 {
cb(authResults)
}
// Finally, if the lock structure contains locks for any providers that
// are no longer needed by this configuration, we'll remove them. This
// is important because we will not have installed those providers
// above and so a lock file still containing them would make the working
// directory invalid: not every provider in the lock file is available
// for use.
for providerAddr := range locks.AllProviders() {
if _, ok := reqs[providerAddr]; !ok {
locks.RemoveProvider(providerAddr)
}
}
if len(errs) > 0 {
return locks, InstallerError{
ProviderErrors: errs,
}
}
return locks, nil
}
func (i *Installer) ensureProviderVersionsMightNeed(
ctx context.Context,
locks *depsfile.Locks,
reqs getproviders.Requirements,
mode InstallMode,
errs map[addrs.Provider]error,
) (
map[addrs.Provider]getproviders.VersionSet,
map[addrs.Provider]bool,
) {
evts := installerEventsForContext(ctx)
mightNeed := map[addrs.Provider]getproviders.VersionSet{}
locked := map[addrs.Provider]bool{}
for provider, versionConstraints := range reqs {
if provider.IsBuiltIn() {
// Built in providers do not require installation but we'll still
// verify that the requested provider name is valid.
valid := false
for _, name := range i.builtInProviderTypes {
if name == provider.Type {
valid = true
break
}
}
var err error
if valid {
if len(versionConstraints) == 0 {
// Other than reporting an event for the outcome of this
// provider, we'll do nothing else with it: it's just
// automatically available for use.
if cb := evts.BuiltInProviderAvailable; cb != nil {
cb(provider)
}
} else {
// A built-in provider is not permitted to have an explicit
// version constraint, because we can only use the version
// that is built in to the current OpenTofu release.
err = fmt.Errorf("built-in providers do not support explicit version constraints")
}
} else {
err = fmt.Errorf("this OpenTofu release has no built-in provider named %q", provider.Type)
}
if err != nil {
errs[provider] = err
if cb := evts.BuiltInProviderFailure; cb != nil {
cb(provider, err)
}
}
continue
}
if _, ok := i.unmanagedProviderTypes[provider]; ok {
// unmanaged providers do not require installation
continue
}
acceptableVersions := versions.MeetingConstraints(versionConstraints)
if !mode.forceQueryAllProviders() {
// If we're not forcing potential changes of version then an
// existing selection from the lock file takes priority over
// the currently-configured version constraints.
if lock := locks.Provider(provider); lock != nil {
if !acceptableVersions.Has(lock.Version()) {
err := fmt.Errorf(
"locked provider %s %s does not match configured version constraint %s; must use tofu init -upgrade to allow selection of new versions",
provider, lock.Version(), getproviders.VersionConstraintsString(versionConstraints),
)
errs[provider] = err
// This is a funny case where we're returning an error
// before we do any querying at all. To keep the event
// stream consistent without introducing an extra event
// type, we'll emit an artificial QueryPackagesBegin for
// this provider before we indicate that it failed using
// QueryPackagesFailure.
if cb := evts.QueryPackagesBegin; cb != nil {
cb(provider, versionConstraints, true)
}
if cb := evts.QueryPackagesFailure; cb != nil {
cb(provider, err)
}
continue
}
acceptableVersions = versions.Only(lock.Version())
locked[provider] = true
}
}
mightNeed[provider] = acceptableVersions
}
return mightNeed, locked
}
func (i *Installer) ensureProviderVersionsNeed(
ctx context.Context,
locks *depsfile.Locks,
reqs getproviders.Requirements,
mightNeed map[addrs.Provider]getproviders.VersionSet,
locked map[addrs.Provider]bool,
errs map[addrs.Provider]error,
) (map[addrs.Provider]getproviders.Version, error) {
evts := installerEventsForContext(ctx)
need := map[addrs.Provider]getproviders.Version{}
NeedProvider:
for provider, acceptableVersions := range mightNeed {
if err := ctx.Err(); err != nil {
// If our context has been cancelled or reached a timeout then
// we'll abort early, because subsequent operations against
// that context will fail immediately anyway.
return nil, err
}
if cb := evts.QueryPackagesBegin; cb != nil {
cb(provider, reqs[provider], locked[provider])
}
// Version 0.0.0 not supported
if err := checkUnspecifiedVersion(acceptableVersions); err != nil {
errs[provider] = err
if cb := evts.QueryPackagesFailure; cb != nil {
cb(provider, err)
}
continue
}
available, warnings, err := i.source.AvailableVersions(ctx, provider)
if err != nil {
errs[provider] = err
if cb := evts.QueryPackagesFailure; cb != nil {
cb(provider, err)
}
// We will take no further actions for this provider.
continue
}
if len(warnings) > 0 {
if cb := evts.QueryPackagesWarning; cb != nil {
cb(provider, warnings)
}
}
available.Sort() // put the versions in increasing order of precedence
for i := len(available) - 1; i >= 0; i-- { // walk backwards to consider newer versions first
if acceptableVersions.Has(available[i]) {
need[provider] = available[i]
if cb := evts.QueryPackagesSuccess; cb != nil {
cb(provider, available[i])
}
continue NeedProvider
}
}
// If we get here then the source has no packages that meet the given
// version constraint, which we model as a query error.
if locked[provider] {
// This situation should be a rare one: it suggests that a
// version was previously available but was yanked for some
// reason.
lock := locks.Provider(provider)
err = fmt.Errorf("the previously-selected version %s is no longer available", lock.Version())
} else {
err = fmt.Errorf("no available releases match the given constraints %s", getproviders.VersionConstraintsString(reqs[provider]))
log.Printf("[DEBUG] %s", err.Error())
log.Printf("[DEBUG] Available releases: %s", available)
}
errs[provider] = err
if cb := evts.QueryPackagesFailure; cb != nil {
cb(provider, err)
}
}
return need, nil
}
func (i *Installer) ensureProviderVersionsInstall(
ctx context.Context,
locks *depsfile.Locks,
reqs getproviders.Requirements,
mode InstallMode,
need map[addrs.Provider]getproviders.Version,
targetPlatform getproviders.Platform,
errs map[addrs.Provider]error,
) (map[addrs.Provider]*getproviders.PackageAuthenticationResult, error) {
authResults := map[addrs.Provider]*getproviders.PackageAuthenticationResult{} // record auth results for all successfully fetched providers
for provider, version := range need {
traceCtx, span := tracing.Tracer().Start(ctx,
fmt.Sprintf("Install Provider %q", provider.String()),
trace.WithAttributes(
otelAttr.String(traceattrs.ProviderAddress, provider.String()),
otelAttr.String(traceattrs.ProviderVersion, version.String()),
otelAttr.String(traceattrs.TargetPlatform, targetPlatform.String()),
),
)
if err := traceCtx.Err(); err != nil {
// If our context has been cancelled or reached a timeout then
// we'll abort early, because subsequent operations against
// that context will fail immediately anyway.
tracing.SetSpanError(span, err)
span.End()
return nil, err
}
authResult, err := i.ensureProviderVersionInstalled(traceCtx, locks, reqs, mode, provider, version, targetPlatform)
if authResult != nil {
authResults[provider] = authResult
}
if err != nil {
errs[provider] = err
}
span.End()
}
return authResults, nil
}
func (i *Installer) ensureProviderVersionInstalled(
ctx context.Context,
locks *depsfile.Locks,
reqs getproviders.Requirements,
mode InstallMode,
provider addrs.Provider,
version getproviders.Version,
targetPlatform getproviders.Platform,
) (*getproviders.PackageAuthenticationResult, error) {
evts := installerEventsForContext(ctx)
lock := locks.Provider(provider)
var preferredHashes []getproviders.Hash
if lock != nil && lock.Version() == version { // hash changes are expected if the version is also changing
preferredHashes = lock.PreferredHashes()
}
// If our target directory already has the provider version that fulfills the lock file, carry on
if installed := i.targetDir.ProviderVersion(provider, version); installed != nil {
if len(preferredHashes) > 0 {
if matches, _ := installed.MatchesAnyHash(preferredHashes); matches {
if cb := evts.ProviderAlreadyInstalled; cb != nil {
cb(provider, version, false)
}
// Even though the package is installed, the requirements in the lockfile may still need to be updated
locks.SetProvider(provider, version, reqs[provider], lock.AllHashes())
return nil, nil
}
}
}
var installTo, linkTo *Dir
if i.globalCacheDir != nil {
installTo = i.globalCacheDir
linkTo = i.targetDir
} else {
installTo = i.targetDir
linkTo = nil // no linking needed
}
result, err := i.ensureProviderVersionInDirectory(ctx, locks, reqs, mode, provider, version, targetPlatform, installTo)
if err != nil {
return result, err
}
if linkTo != nil {
if cb := evts.LinkFromCacheBegin; cb != nil {
cb(provider, version, installTo.BasePath())
}
// We don't do a hash check here because we already did that
// as part of the ensureProviderVersionInDirectory call above.
new := installTo.ProviderVersion(provider, version)
err := linkTo.LinkFromOtherCache(ctx, new, nil)
if err != nil {
if cb := evts.LinkFromCacheFailure; cb != nil {
cb(provider, version, err)
}
return nil, err
}
// We should now also find the package in the linkTo dir, which
// gives us the final value of "new" where the path points in to
// the true target directory, rather than possibly the global
// cache directory.
new = linkTo.ProviderVersion(provider, version)
if new == nil {
err := fmt.Errorf("after installing %s it is still not detected in %s; this is a bug in OpenTofu", provider, linkTo.BasePath())
if cb := evts.LinkFromCacheFailure; cb != nil {
cb(provider, version, err)
}
return nil, err
}
if _, err := new.ExecutableFile(); err != nil {
err := fmt.Errorf("provider binary not found: %w", err)
if cb := evts.LinkFromCacheFailure; cb != nil {
cb(provider, version, err)
}
return nil, err
}
if cb := evts.LinkFromCacheSuccess; cb != nil {
cb(provider, version, new.PackageDir)
}
}
return result, err
}
func (i *Installer) ensureProviderVersionInDirectory(
ctx context.Context,
locks *depsfile.Locks,
reqs getproviders.Requirements,
mode InstallMode,
provider addrs.Provider,
version getproviders.Version,
targetPlatform getproviders.Platform,
installTo *Dir,
) (*getproviders.PackageAuthenticationResult, error) {
evts := installerEventsForContext(ctx)
lock := locks.Provider(provider)
var preferredHashes []getproviders.Hash
if lock != nil && lock.Version() == version { // hash changes are expected if the version is also changing
preferredHashes = lock.PreferredHashes()
}
isGlobalCache := installTo == i.globalCacheDir
// If our target directory already has the provider version that fulfills the lock file, carry on
if installed := installTo.ProviderVersion(provider, version); installed != nil {
if len(preferredHashes) > 0 {
if matches, _ := installed.MatchesAnyHash(preferredHashes); matches {
if cb := evts.ProviderAlreadyInstalled; cb != nil {
cb(provider, version, isGlobalCache)
}
// Even though the package is installed, the requirements in the lockfile may still need to be updated
locks.SetProvider(provider, version, reqs[provider], lock.AllHashes())
return nil, nil
}
}
}
// Step 3b: Get the package metadata for the selected version from our
// provider source.
//
// This is the step where we might detect and report that the provider
// isn't available for the current platform.
if cb := evts.FetchPackageMeta; cb != nil {
cb(provider, version)
}
meta, err := i.source.PackageMeta(ctx, provider, version, targetPlatform)
if err != nil {
if cb := evts.FetchPackageFailure; cb != nil {
cb(provider, version, err)
}
return nil, err
}
// Step 3c: Retrieve the package indicated by the metadata we received,
// either directly into our target directory or via the global cache
// directory.
if cb := evts.FetchPackageBegin; cb != nil {
cb(provider, version, meta.Location, isGlobalCache)
}
allowedHashes := preferredHashes
if mode.forceInstallChecksums() {
allowedHashes = []getproviders.Hash{}
}
allowSkippingInstallWithoutHashes := i.globalCacheDirMayBreakDependencyLockFile && isGlobalCache
authResult, err := installTo.InstallPackage(ctx, meta, allowedHashes, allowSkippingInstallWithoutHashes)
if err != nil {
// TODO: Consider retrying for certain kinds of error that seem
// likely to be transient. For now, we just treat all errors equally.
if cb := evts.FetchPackageFailure; cb != nil {
cb(provider, version, err)
}
return nil, err
}
new := installTo.ProviderVersion(provider, version)
if new == nil {
err := fmt.Errorf("after installing %s it is still not detected in %s; this is a bug in OpenTofu", provider, installTo.BasePath())
if cb := evts.FetchPackageFailure; cb != nil {
cb(provider, version, err)
}
return nil, err
}
if _, err := new.ExecutableFile(); err != nil {
err := fmt.Errorf("provider binary not found: %w", err)
if cb := evts.FetchPackageFailure; cb != nil {
cb(provider, version, err)
}
return nil, err
}
// The InstallPackage call above should've verified that
// the package matches one of the hashes previously recorded,
// if any. We'll now augment those hashes with a new set populated
// with the hashes returned by the upstream source and from the
// package we've just installed, which allows the lock file to
// gradually transition to newer hash schemes when they become
// available.
//
// This is assuming that if a package matches both a hash we saw before
// _and_ a new hash then the new hash is a valid substitute for
// the previous hash.
//
// The hashes slice gets deduplicated in the lock file
// implementation, so we don't worry about potentially
// creating duplicates here.
var priorHashes []getproviders.Hash
if lock != nil && lock.Version() == version {
// If the version we're installing is identical to the
// one we previously locked then we'll keep all of the
// hashes we saved previously and add to it. Otherwise
// we'll be starting fresh, because each version has its
// own set of packages and thus its own hashes.
priorHashes = append(priorHashes, preferredHashes...)
}
newHash, err := new.Hash()
if err != nil {
err := fmt.Errorf("after installing %s, failed to compute a checksum for it: %w", provider, err)
if cb := evts.FetchPackageFailure; cb != nil {
cb(provider, version, err)
}
return authResult, err
}
// localHashes is the set of hashes that we were able to verify locally
// based on the data we downloaded.
localHashes := slices.Collect(authResult.HashesWithDisposition(func(hd *getproviders.HashDisposition) bool {
return hd.VerifiedLocally
}))
localHashes = append(localHashes, newHash) // the hash we calculated above was _also_ verified locally
// We have different rules for what subset of hashes we track in
// the dependency lock file depending on the provider. Refer to
// the documentation of the following function for more information.
signingRequired := getproviders.ShouldEnforceGPGValidationForProvider(provider)
signedHashes := slices.Collect(authResult.HashesWithDisposition(func(hd *getproviders.HashDisposition) bool {
if !signingRequired {
// When signing isn't required, we pretend that anything
// that was reported by the origin registry was "signed",
// just for the purposes of updating the lock file and
// reporting that lock file update to the UI layer through
// the evts object.
// Note that the "tofu init" UI relies on us pretending
// that these are "signed" to avoid generating its warning
// that the dependency lock file might be incomplete.
return hd.ReportedByRegistry
}
return hd.SignedByAnyGPGKeys()
}))
var newHashes []getproviders.Hash
newHashes = append(newHashes, newHash)
newHashes = append(newHashes, priorHashes...)
newHashes = append(newHashes, localHashes...)
newHashes = append(newHashes, signedHashes...)
locks.SetProvider(provider, version, reqs[provider], newHashes)
if cb := evts.ProvidersLockUpdated; cb != nil {
// priorHashes is already sorted, but we do need to sort
// the newly-generated localHashes and signedHashes.
sort.Slice(localHashes, func(i, j int) bool {
return localHashes[i].String() < localHashes[j].String()
})
sort.Slice(signedHashes, func(i, j int) bool {
return signedHashes[i].String() < signedHashes[j].String()
})
// these slices might also contain duplicates if the
// same hash was found in two different ways, so we'll
// adjust for that. This relies on the sorting above
// and modifies the underlying arrays in-place.
localHashes = slices.Compact(localHashes)
signedHashes = slices.Compact(signedHashes)
cb(provider, version, localHashes, signedHashes, priorHashes)
}
if cb := evts.FetchPackageSuccess; cb != nil {
cb(provider, version, new.PackageDir, authResult)
}
return authResult, nil
}
// checkUnspecifiedVersion Check the presence of version 0.0.0 and return an error with a tip
func checkUnspecifiedVersion(acceptableVersions versions.Set) error {
if !acceptableVersions.Exactly(versions.Unspecified) {
return nil
}
tip := "If the version 0.0.0 is intended to represent a non-published provider, consider using dev_overrides - https://opentofu.org/docs/cli/config/config-file/#development-overrides-for-provider-developers"
return fmt.Errorf("0.0.0 is not a valid provider version. \n%s", tip)
}
// InstallMode customizes the details of how an install operation treats
// providers that have versions already cached in the target directory.
type InstallMode rune
const (
// InstallNewProvidersOnly is an InstallMode that causes the installer
// to accept any existing version of a requested provider that is already
// cached as long as it's in the given version sets, without checking
// whether new versions are available that are also in the given version
// sets.
InstallNewProvidersOnly InstallMode = 'N'
// InstallNewProvidersForce is an InstallMode that follows the same
// logic as InstallNewProvidersOnly except it does not verify existing
// checksums but force installs new checksums for all given providers.
InstallNewProvidersForce InstallMode = 'F'
// InstallUpgrades is an InstallMode that causes the installer to check
// all requested providers to see if new versions are available that
// are also in the given version sets, even if a suitable version of
// a given provider is already available.
InstallUpgrades InstallMode = 'U'
)
func (m InstallMode) forceQueryAllProviders() bool {
return m == InstallUpgrades
}
func (m InstallMode) forceInstallChecksums() bool {
return m == InstallNewProvidersForce
}
// InstallerError is an error type that may be returned (but is not guaranteed)
// from Installer.EnsureProviderVersions to indicate potentially several
// separate failed installation outcomes for different providers included in
// the overall request.
type InstallerError struct {
ProviderErrors map[addrs.Provider]error
}
func (err InstallerError) Error() string {
addrs := make([]addrs.Provider, 0, len(err.ProviderErrors))
for addr := range err.ProviderErrors {
addrs = append(addrs, addr)
}
sort.Slice(addrs, func(i, j int) bool {
return addrs[i].LessThan(addrs[j])
})
var b strings.Builder
b.WriteString("some providers could not be installed:\n")
for _, addr := range addrs {
providerErr := err.ProviderErrors[addr]
fmt.Fprintf(&b, "- %s: %s\n", addr, providerErr)
}
return strings.TrimSpace(b.String())
}