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