github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/providercache/dir.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package providercache
     5  
     6  import (
     7  	"log"
     8  	"path/filepath"
     9  	"sort"
    10  
    11  	"github.com/terramate-io/tf/addrs"
    12  	"github.com/terramate-io/tf/getproviders"
    13  )
    14  
    15  // Dir represents a single local filesystem directory containing cached
    16  // provider plugin packages that can be both read from (to find providers to
    17  // use for operations) and written to (during provider installation).
    18  //
    19  // The contents of a cache directory follow the same naming conventions as a
    20  // getproviders.FilesystemMirrorSource, except that the packages are always
    21  // kept in the "unpacked" form (a directory containing the contents of the
    22  // original distribution archive) so that they are ready for direct execution.
    23  //
    24  // A Dir also pays attention only to packages for the current host platform,
    25  // silently ignoring any cached packages for other platforms.
    26  //
    27  // Various Dir methods return values that are technically mutable due to the
    28  // restrictions of the Go typesystem, but callers are not permitted to mutate
    29  // any part of the returned data structures.
    30  type Dir struct {
    31  	baseDir        string
    32  	targetPlatform getproviders.Platform
    33  
    34  	// metaCache is a cache of the metadata of relevant packages available in
    35  	// the cache directory last time we scanned it. This can be nil to indicate
    36  	// that the cache is cold. The cache will be invalidated (set back to nil)
    37  	// by any operation that modifies the contents of the cache directory.
    38  	//
    39  	// We intentionally don't make effort to detect modifications to the
    40  	// directory made by other codepaths because the contract for NewDir
    41  	// explicitly defines using the same directory for multiple purposes
    42  	// as undefined behavior.
    43  	metaCache map[addrs.Provider][]CachedProvider
    44  }
    45  
    46  // NewDir creates and returns a new Dir object that will read and write
    47  // provider plugins in the given filesystem directory.
    48  //
    49  // If two instances of Dir are concurrently operating on a particular base
    50  // directory, or if a Dir base directory is also used as a filesystem mirror
    51  // source directory, the behavior is undefined.
    52  func NewDir(baseDir string) *Dir {
    53  	return &Dir{
    54  		baseDir:        baseDir,
    55  		targetPlatform: getproviders.CurrentPlatform,
    56  	}
    57  }
    58  
    59  // NewDirWithPlatform is a variant of NewDir that allows selecting a specific
    60  // target platform, rather than taking the current one where this code is
    61  // running.
    62  //
    63  // This is primarily intended for portable unit testing and not particularly
    64  // useful in "real" callers.
    65  func NewDirWithPlatform(baseDir string, platform getproviders.Platform) *Dir {
    66  	return &Dir{
    67  		baseDir:        baseDir,
    68  		targetPlatform: platform,
    69  	}
    70  }
    71  
    72  // BasePath returns the filesystem path of the base directory of this
    73  // cache directory.
    74  func (d *Dir) BasePath() string {
    75  	return filepath.Clean(d.baseDir)
    76  }
    77  
    78  // AllAvailablePackages returns a description of all of the packages already
    79  // present in the directory. The cache entries are grouped by the provider
    80  // they relate to and then sorted by version precedence, with highest
    81  // precedence first.
    82  //
    83  // This function will return an empty result both when the directory is empty
    84  // and when scanning the directory produces an error.
    85  //
    86  // The caller is forbidden from modifying the returned data structure in any
    87  // way, even though the Go type system permits it.
    88  func (d *Dir) AllAvailablePackages() map[addrs.Provider][]CachedProvider {
    89  	if err := d.fillMetaCache(); err != nil {
    90  		log.Printf("[WARN] Failed to scan provider cache directory %s: %s", d.baseDir, err)
    91  		return nil
    92  	}
    93  
    94  	return d.metaCache
    95  }
    96  
    97  // ProviderVersion returns the cache entry for the requested provider version,
    98  // or nil if the requested provider version isn't present in the cache.
    99  func (d *Dir) ProviderVersion(provider addrs.Provider, version getproviders.Version) *CachedProvider {
   100  	if err := d.fillMetaCache(); err != nil {
   101  		return nil
   102  	}
   103  
   104  	for _, entry := range d.metaCache[provider] {
   105  		// We're intentionally comparing exact version here, so if either
   106  		// version number contains build metadata and they don't match then
   107  		// this will not return true. The rule of ignoring build metadata
   108  		// applies only for handling version _constraints_ and for deciding
   109  		// version precedence.
   110  		if entry.Version == version {
   111  			return &entry
   112  		}
   113  	}
   114  
   115  	return nil
   116  }
   117  
   118  // ProviderLatestVersion returns the cache entry for the latest
   119  // version of the requested provider already available in the cache, or nil if
   120  // there are no versions of that provider available.
   121  func (d *Dir) ProviderLatestVersion(provider addrs.Provider) *CachedProvider {
   122  	if err := d.fillMetaCache(); err != nil {
   123  		return nil
   124  	}
   125  
   126  	entries := d.metaCache[provider]
   127  	if len(entries) == 0 {
   128  		return nil
   129  	}
   130  
   131  	return &entries[0]
   132  }
   133  
   134  func (d *Dir) fillMetaCache() error {
   135  	// For d.metaCache we consider nil to be different than a non-nil empty
   136  	// map, so we can distinguish between having scanned and got an empty
   137  	// result vs. not having scanned successfully at all yet.
   138  	if d.metaCache != nil {
   139  		log.Printf("[TRACE] providercache.fillMetaCache: using cached result from previous scan of %s", d.baseDir)
   140  		return nil
   141  	}
   142  	log.Printf("[TRACE] providercache.fillMetaCache: scanning directory %s", d.baseDir)
   143  
   144  	allData, err := getproviders.SearchLocalDirectory(d.baseDir)
   145  	if err != nil {
   146  		log.Printf("[TRACE] providercache.fillMetaCache: error while scanning directory %s: %s", d.baseDir, err)
   147  		return err
   148  	}
   149  
   150  	// The getproviders package just returns everything it found, but we're
   151  	// interested only in a subset of the results:
   152  	// - those that are for the current platform
   153  	// - those that are in the "unpacked" form, ready to execute
   154  	// ...so we'll filter in these ways while we're constructing our final
   155  	// map to save as the cache.
   156  	//
   157  	// We intentionally always make a non-nil map, even if it might ultimately
   158  	// be empty, because we use that to recognize that the cache is populated.
   159  	data := make(map[addrs.Provider][]CachedProvider)
   160  
   161  	for providerAddr, metas := range allData {
   162  		for _, meta := range metas {
   163  			if meta.TargetPlatform != d.targetPlatform {
   164  				log.Printf("[TRACE] providercache.fillMetaCache: ignoring %s because it is for %s, not %s", meta.Location, meta.TargetPlatform, d.targetPlatform)
   165  				continue
   166  			}
   167  			if _, ok := meta.Location.(getproviders.PackageLocalDir); !ok {
   168  				// PackageLocalDir indicates an unpacked provider package ready
   169  				// to execute.
   170  				log.Printf("[TRACE] providercache.fillMetaCache: ignoring %s because it is not an unpacked directory", meta.Location)
   171  				continue
   172  			}
   173  
   174  			packageDir := filepath.Clean(string(meta.Location.(getproviders.PackageLocalDir)))
   175  
   176  			log.Printf("[TRACE] providercache.fillMetaCache: including %s as a candidate package for %s %s", meta.Location, providerAddr, meta.Version)
   177  			data[providerAddr] = append(data[providerAddr], CachedProvider{
   178  				Provider:   providerAddr,
   179  				Version:    meta.Version,
   180  				PackageDir: filepath.ToSlash(packageDir),
   181  			})
   182  		}
   183  	}
   184  
   185  	// After we've built our lists per provider, we'll also sort them by
   186  	// version precedence so that the newest available version is always at
   187  	// index zero. If there are two versions that differ only in build metadata
   188  	// then it's undefined but deterministic which one we will select here,
   189  	// because we're preserving the order returned by SearchLocalDirectory
   190  	// in that case..
   191  	for _, entries := range data {
   192  		sort.SliceStable(entries, func(i, j int) bool {
   193  			// We're using GreaterThan rather than LessThan here because we
   194  			// want these in _decreasing_ order of precedence.
   195  			return entries[i].Version.GreaterThan(entries[j].Version)
   196  		})
   197  	}
   198  
   199  	d.metaCache = data
   200  	return nil
   201  }