github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/providercache/cached_provider.go (about) 1 package providercache 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "path/filepath" 7 "strings" 8 9 "github.com/eliastor/durgaform/internal/addrs" 10 "github.com/eliastor/durgaform/internal/getproviders" 11 ) 12 13 // CachedProvider represents a provider package in a cache directory. 14 type CachedProvider struct { 15 // Provider and Version together identify the specific provider version 16 // this cache entry represents. 17 Provider addrs.Provider 18 Version getproviders.Version 19 20 // PackageDir is the local filesystem path to the root directory where 21 // the provider's distribution archive was unpacked. 22 // 23 // The path always uses slashes as path separators, even on Windows, so 24 // that the results are consistent between platforms. Windows accepts 25 // both slashes and backslashes as long as the separators are consistent 26 // within a particular path string. 27 PackageDir string 28 } 29 30 // PackageLocation returns the package directory given in the PackageDir field 31 // as a getproviders.PackageLocation implementation. 32 // 33 // Because cached providers are always in the unpacked structure, the result is 34 // always of the concrete type getproviders.PackageLocalDir. 35 func (cp *CachedProvider) PackageLocation() getproviders.PackageLocalDir { 36 return getproviders.PackageLocalDir(cp.PackageDir) 37 } 38 39 // Hash computes a hash of the contents of the package directory associated 40 // with the receiving cached provider, using whichever hash algorithm is 41 // the current default. 42 // 43 // If you need a specific version of hash rather than just whichever one is 44 // current default, call that version's corresponding method (e.g. HashV1) 45 // directly instead. 46 func (cp *CachedProvider) Hash() (getproviders.Hash, error) { 47 return getproviders.PackageHash(cp.PackageLocation()) 48 } 49 50 // MatchesHash returns true if the package on disk matches the given hash, 51 // or false otherwise. If it cannot traverse the package directory and read 52 // all of the files in it, or if the hash is in an unsupported format, 53 // MatchesHash returns an error. 54 // 55 // MatchesHash may accept hashes in a number of different formats. Over time 56 // the set of supported formats may grow and shrink. 57 func (cp *CachedProvider) MatchesHash(want getproviders.Hash) (bool, error) { 58 return getproviders.PackageMatchesHash(cp.PackageLocation(), want) 59 } 60 61 // MatchesAnyHash returns true if the package on disk matches the given hash, 62 // or false otherwise. If it cannot traverse the package directory and read 63 // all of the files in it, MatchesAnyHash returns an error. 64 // 65 // Unlike the singular MatchesHash, MatchesAnyHash considers unsupported hash 66 // formats as successfully non-matching, rather than returning an error. 67 func (cp *CachedProvider) MatchesAnyHash(allowed []getproviders.Hash) (bool, error) { 68 return getproviders.PackageMatchesAnyHash(cp.PackageLocation(), allowed) 69 } 70 71 // HashV1 computes a hash of the contents of the package directory associated 72 // with the receiving cached provider using hash algorithm 1. 73 // 74 // The hash covers the paths to files in the directory and the contents of 75 // those files. It does not cover other metadata about the files, such as 76 // permissions. 77 // 78 // This function is named "HashV1" in anticipation of other hashing algorithms 79 // being added (in a backward-compatible way) in future. The result from 80 // HashV1 always begins with the prefix "h1:" so that callers can distinguish 81 // the results of potentially multiple different hash algorithms in future. 82 func (cp *CachedProvider) HashV1() (getproviders.Hash, error) { 83 return getproviders.PackageHashV1(cp.PackageLocation()) 84 } 85 86 // ExecutableFile inspects the cached provider's unpacked package directory for 87 // something that looks like it's intended to be the executable file for the 88 // plugin. 89 // 90 // This is a bit messy and heuristic-y because historically Durgaform used the 91 // filename itself for local filesystem discovery, allowing some variance in 92 // the filenames to capture extra metadata, whereas now we're using the 93 // directory structure leading to the executable instead but need to remain 94 // compatible with the executable names bundled into existing provider packages. 95 // 96 // It will return an error if it can't find a file following the expected 97 // convention in the given directory. 98 // 99 // If found, the path always uses slashes as path separators, even on Windows, 100 // so that the results are consistent between platforms. Windows accepts both 101 // slashes and backslashes as long as the separators are consistent within a 102 // particular path string. 103 func (cp *CachedProvider) ExecutableFile() (string, error) { 104 infos, err := ioutil.ReadDir(cp.PackageDir) 105 if err != nil { 106 // If the directory itself doesn't exist or isn't readable then we 107 // can't access an executable in it. 108 return "", fmt.Errorf("could not read package directory: %s", err) 109 } 110 111 // For a provider named e.g. tf.example.com/awesomecorp/happycloud, we 112 // expect an executable file whose name starts with 113 // "durgaform-provider-happycloud", followed by zero or more additional 114 // characters. If there _are_ additional characters then the first one 115 // must be an underscore or a period, like in thse examples: 116 // - durgaform-provider-happycloud_v1.0.0 117 // - durgaform-provider-happycloud.exe 118 // 119 // We don't require the version in the filename to match because the 120 // executable's name is no longer authoritative, but packages of "official" 121 // providers may continue to use versioned executable names for backward 122 // compatibility with Durgaform 0.12. 123 // 124 // We also presume that providers packaged for Windows will include the 125 // necessary .exe extension on their filenames but do not explicitly check 126 // for that. If there's a provider package for Windows that has a file 127 // without that suffix then it will be detected as an executable but then 128 // we'll presumably fail later trying to run it. 129 wantPrefix := "durgaform-provider-" + cp.Provider.Type 130 131 // We'll visit all of the directory entries and take the first (in 132 // name-lexical order) that looks like a plausible provider executable 133 // name. A package with multiple files meeting these criteria is degenerate 134 // but we will tolerate it by ignoring the subsequent entries. 135 for _, info := range infos { 136 if info.IsDir() { 137 continue // A directory can never be an executable 138 } 139 name := info.Name() 140 if !strings.HasPrefix(name, wantPrefix) { 141 continue 142 } 143 remainder := name[len(wantPrefix):] 144 if len(remainder) > 0 && (remainder[0] != '_' && remainder[0] != '.') { 145 continue // subsequent characters must be delimited by _ or . 146 } 147 return filepath.ToSlash(filepath.Join(cp.PackageDir, name)), nil 148 } 149 150 return "", fmt.Errorf("could not find executable file starting with %s", wantPrefix) 151 }