opentofu/internal/providercache/dir.go
Martin Atkins b3f5c7f1e6 command/init: Read, respect, and update provider dependency locks
This changes the approach used by the provider installer to remember
between runs which selections it has previously made, using the lock file
format implemented in internal/depsfile.

This means that version constraints in the configuration are considered
only for providers we've not seen before or when -upgrade mode is active.
2020-10-09 09:26:23 -07:00

199 lines
7.5 KiB
Go

package providercache
import (
"log"
"path/filepath"
"sort"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/internal/getproviders"
)
// Dir represents a single local filesystem directory containing cached
// provider plugin packages that can be both read from (to find providers to
// use for operations) and written to (during provider installation).
//
// The contents of a cache directory follow the same naming conventions as a
// getproviders.FilesystemMirrorSource, except that the packages are always
// kept in the "unpacked" form (a directory containing the contents of the
// original distribution archive) so that they are ready for direct execution.
//
// A Dir also pays attention only to packages for the current host platform,
// silently ignoring any cached packages for other platforms.
//
// Various Dir methods return values that are technically mutable due to the
// restrictions of the Go typesystem, but callers are not permitted to mutate
// any part of the returned data structures.
type Dir struct {
baseDir string
targetPlatform getproviders.Platform
// metaCache is a cache of the metadata of relevant packages available in
// the cache directory last time we scanned it. This can be nil to indicate
// that the cache is cold. The cache will be invalidated (set back to nil)
// by any operation that modifies the contents of the cache directory.
//
// We intentionally don't make effort to detect modifications to the
// directory made by other codepaths because the contract for NewDir
// explicitly defines using the same directory for multiple purposes
// as undefined behavior.
metaCache map[addrs.Provider][]CachedProvider
}
// NewDir creates and returns a new Dir object that will read and write
// provider plugins in the given filesystem directory.
//
// If two instances of Dir are concurrently operating on a particular base
// directory, or if a Dir base directory is also used as a filesystem mirror
// source directory, the behavior is undefined.
func NewDir(baseDir string) *Dir {
return &Dir{
baseDir: baseDir,
targetPlatform: getproviders.CurrentPlatform,
}
}
// NewDirWithPlatform is a variant of NewDir that allows selecting a specific
// target platform, rather than taking the current one where this code is
// running.
//
// This is primarily intended for portable unit testing and not particularly
// useful in "real" callers, with the exception of terraform-bundle.
func NewDirWithPlatform(baseDir string, platform getproviders.Platform) *Dir {
return &Dir{
baseDir: baseDir,
targetPlatform: platform,
}
}
// BasePath returns the filesystem path of the base directory of this
// cache directory.
func (d *Dir) BasePath() string {
return filepath.Clean(d.baseDir)
}
// AllAvailablePackages returns a description of all of the packages already
// present in the directory. The cache entries are grouped by the provider
// they relate to and then sorted by version precedence, with highest
// precedence first.
//
// This function will return an empty result both when the directory is empty
// and when scanning the directory produces an error.
//
// The caller is forbidden from modifying the returned data structure in any
// way, even though the Go type system permits it.
func (d *Dir) AllAvailablePackages() map[addrs.Provider][]CachedProvider {
if err := d.fillMetaCache(); err != nil {
log.Printf("[WARN] Failed to scan provider cache directory %s: %s", d.baseDir, err)
return nil
}
return d.metaCache
}
// ProviderVersion returns the cache entry for the requested provider version,
// or nil if the requested provider version isn't present in the cache.
func (d *Dir) ProviderVersion(provider addrs.Provider, version getproviders.Version) *CachedProvider {
if err := d.fillMetaCache(); err != nil {
return nil
}
for _, entry := range d.metaCache[provider] {
// We're intentionally comparing exact version here, so if either
// version number contains build metadata and they don't match then
// this will not return true. The rule of ignoring build metadata
// applies only for handling version _constraints_ and for deciding
// version precedence.
if entry.Version == version {
return &entry
}
}
return nil
}
// ProviderLatestVersion returns the cache entry for the latest
// version of the requested provider already available in the cache, or nil if
// there are no versions of that provider available.
func (d *Dir) ProviderLatestVersion(provider addrs.Provider) *CachedProvider {
if err := d.fillMetaCache(); err != nil {
return nil
}
entries := d.metaCache[provider]
if len(entries) == 0 {
return nil
}
return &entries[0]
}
func (d *Dir) fillMetaCache() error {
// For d.metaCache we consider nil to be different than a non-nil empty
// map, so we can distinguish between having scanned and got an empty
// result vs. not having scanned successfully at all yet.
if d.metaCache != nil {
log.Printf("[TRACE] providercache.fillMetaCache: using cached result from previous scan of %s", d.baseDir)
return nil
}
log.Printf("[TRACE] providercache.fillMetaCache: scanning directory %s", d.baseDir)
allData, err := getproviders.SearchLocalDirectory(d.baseDir)
if err != nil {
log.Printf("[TRACE] providercache.fillMetaCache: error while scanning directory %s: %s", d.baseDir, err)
return err
}
// The getproviders package just returns everything it found, but we're
// interested only in a subset of the results:
// - those that are for the current platform
// - those that are in the "unpacked" form, ready to execute
// ...so we'll filter in these ways while we're constructing our final
// map to save as the cache.
//
// We intentionally always make a non-nil map, even if it might ultimately
// be empty, because we use that to recognize that the cache is populated.
data := make(map[addrs.Provider][]CachedProvider)
for providerAddr, metas := range allData {
for _, meta := range metas {
if meta.TargetPlatform != d.targetPlatform {
log.Printf("[TRACE] providercache.fillMetaCache: ignoring %s because it is for %s, not %s", meta.Location, meta.TargetPlatform, d.targetPlatform)
continue
}
if _, ok := meta.Location.(getproviders.PackageLocalDir); !ok {
// PackageLocalDir indicates an unpacked provider package ready
// to execute.
log.Printf("[TRACE] providercache.fillMetaCache: ignoring %s because it is not an unpacked directory", meta.Location)
continue
}
packageDir := filepath.Clean(string(meta.Location.(getproviders.PackageLocalDir)))
log.Printf("[TRACE] providercache.fillMetaCache: including %s as a candidate package for %s %s", meta.Location, providerAddr, meta.Version)
data[providerAddr] = append(data[providerAddr], CachedProvider{
Provider: providerAddr,
Version: meta.Version,
PackageDir: filepath.ToSlash(packageDir),
})
}
}
// After we've built our lists per provider, we'll also sort them by
// version precedence so that the newest available version is always at
// index zero. If there are two versions that differ only in build metadata
// then it's undefined but deterministic which one we will select here,
// because we're preserving the order returned by SearchLocalDirectory
// in that case..
for _, entries := range data {
sort.SliceStable(entries, func(i, j int) bool {
// We're using GreaterThan rather than LessThan here because we
// want these in _decreasing_ order of precedence.
return entries[i].Version.GreaterThan(entries[j].Version)
})
}
d.metaCache = data
return nil
}