Files
opentf/internal/providercache/installer.go
Martin Atkins 0503163e28 tracing: Centralize our OpenTelemetry package imports
OpenTelemetry has various Go packages split across several Go modules that
often need to be carefully upgraded together. And in particular, we are
using the "semconv" package in conjunction with the OpenTelemetry SDK's
"resource" package in a way that requires that they both agree on which
version of the OpenTelemetry Semantic Conventions are being followed.

To help avoid "dependency hell" situations when upgrading, this centralizes
all of our direct calls into the OpenTelemetry SDK and tracing API into
packages under internal/tracing, by exposing a few thin wrapper functions
that other packages can use to access the same functionality indirectly.

We only use a relatively small subset of the OpenTelemetry library surface
area, so we don't need too many of these reexports and they should not
represent a significant additional maintenance burden.

For the semconv and resource interaction in particular this also factors
that out into a separate helper function with a unit test, so we should
notice quickly whenever they become misaligned. This complements the
end-to-end test previously added in opentofu/opentofu#3447 to give us
faster feedback about this particular problem, while the end-to-end test
has the broader scope of making sure there aren't any errors at all when
initializing OpenTelemetry tracing.

Finally, this also replaces the constants we previously had in package
traceaddrs with functions that return attribute.KeyValue values directly.
This matches the API style used by the OpenTelemetry semconv packages, and
makes the calls to these helpers from elsewhere in the system a little
more concise.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-10-30 13:27:10 -07:00

817 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"
"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(traceattrs.OpenTofuTargetPlatform(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()),
tracing.SpanAttributes(
traceattrs.OpenTofuProviderAddress(provider.String()),
traceattrs.OpenTofuProviderVersion(version.String()),
traceattrs.OpenTofuTargetPlatform(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())
}