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 }