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