github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/plugin/discovery/find.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package discovery 5 6 import ( 7 "io/ioutil" 8 "log" 9 "os" 10 "path/filepath" 11 "strings" 12 ) 13 14 // FindPlugins looks in the given directories for files whose filenames 15 // suggest that they are plugins of the given kind (e.g. "provider") and 16 // returns a PluginMetaSet representing the discovered potential-plugins. 17 // 18 // Currently this supports two different naming schemes. The current 19 // standard naming scheme is a subdirectory called $GOOS-$GOARCH containing 20 // files named terraform-$KIND-$NAME-V$VERSION. The legacy naming scheme is 21 // files directly in the given directory whose names are like 22 // terraform-$KIND-$NAME. 23 // 24 // Only one plugin will be returned for each unique plugin (name, version) 25 // pair, with preference given to files found in earlier directories. 26 // 27 // This is a convenience wrapper around FindPluginPaths and ResolvePluginsPaths. 28 func FindPlugins(kind string, dirs []string) PluginMetaSet { 29 return ResolvePluginPaths(FindPluginPaths(kind, dirs)) 30 } 31 32 // FindPluginPaths looks in the given directories for files whose filenames 33 // suggest that they are plugins of the given kind (e.g. "provider"). 34 // 35 // The return value is a list of absolute paths that appear to refer to 36 // plugins in the given directories, based only on what can be inferred 37 // from the naming scheme. The paths returned are ordered such that files 38 // in later dirs appear after files in earlier dirs in the given directory 39 // list. Within the same directory plugins are returned in a consistent but 40 // undefined order. 41 func FindPluginPaths(kind string, dirs []string) []string { 42 // This is just a thin wrapper around findPluginPaths so that we can 43 // use the latter in tests with a fake machineName so we can use our 44 // test fixtures. 45 return findPluginPaths(kind, dirs) 46 } 47 48 func findPluginPaths(kind string, dirs []string) []string { 49 prefix := "terraform-" + kind + "-" 50 51 ret := make([]string, 0, len(dirs)) 52 53 for _, dir := range dirs { 54 items, err := ioutil.ReadDir(dir) 55 if err != nil { 56 // Ignore missing dirs, non-dirs, etc 57 continue 58 } 59 60 log.Printf("[DEBUG] checking for %s in %q", kind, dir) 61 62 for _, item := range items { 63 fullName := item.Name() 64 65 if !strings.HasPrefix(fullName, prefix) { 66 continue 67 } 68 69 // New-style paths must have a version segment in filename 70 if strings.Contains(strings.ToLower(fullName), "_v") { 71 absPath, err := filepath.Abs(filepath.Join(dir, fullName)) 72 if err != nil { 73 log.Printf("[ERROR] plugin filepath error: %s", err) 74 continue 75 } 76 77 // Check that the file we found is usable 78 if !pathIsFile(absPath) { 79 log.Printf("[ERROR] ignoring non-file %s", absPath) 80 continue 81 } 82 83 log.Printf("[DEBUG] found %s %q", kind, fullName) 84 ret = append(ret, filepath.Clean(absPath)) 85 continue 86 } 87 88 // Legacy style with files directly in the base directory 89 absPath, err := filepath.Abs(filepath.Join(dir, fullName)) 90 if err != nil { 91 log.Printf("[ERROR] plugin filepath error: %s", err) 92 continue 93 } 94 95 // Check that the file we found is usable 96 if !pathIsFile(absPath) { 97 log.Printf("[ERROR] ignoring non-file %s", absPath) 98 continue 99 } 100 101 log.Printf("[WARN] found legacy %s %q", kind, fullName) 102 103 ret = append(ret, filepath.Clean(absPath)) 104 } 105 } 106 107 return ret 108 } 109 110 // Returns true if and only if the given path refers to a file or a symlink 111 // to a file. 112 func pathIsFile(path string) bool { 113 info, err := os.Stat(path) 114 if err != nil { 115 return false 116 } 117 118 return !info.IsDir() 119 } 120 121 // ResolvePluginPaths takes a list of paths to plugin executables (as returned 122 // by e.g. FindPluginPaths) and produces a PluginMetaSet describing the 123 // referenced plugins. 124 // 125 // If the same combination of plugin name and version appears multiple times, 126 // the earlier reference will be preferred. Several different versions of 127 // the same plugin name may be returned, in which case the methods of 128 // PluginMetaSet can be used to filter down. 129 func ResolvePluginPaths(paths []string) PluginMetaSet { 130 s := make(PluginMetaSet) 131 132 type nameVersion struct { 133 Name string 134 Version string 135 } 136 found := make(map[nameVersion]struct{}) 137 138 for _, path := range paths { 139 baseName := strings.ToLower(filepath.Base(path)) 140 if !strings.HasPrefix(baseName, "terraform-") { 141 // Should never happen with reasonable input 142 continue 143 } 144 145 baseName = baseName[10:] 146 firstDash := strings.Index(baseName, "-") 147 if firstDash == -1 { 148 // Should never happen with reasonable input 149 continue 150 } 151 152 baseName = baseName[firstDash+1:] 153 if baseName == "" { 154 // Should never happen with reasonable input 155 continue 156 } 157 158 // Trim the .exe suffix used on Windows before we start wrangling 159 // the remainder of the path. 160 baseName = strings.TrimSuffix(baseName, ".exe") 161 162 parts := strings.SplitN(baseName, "_v", 2) 163 name := parts[0] 164 version := VersionZero 165 if len(parts) == 2 { 166 version = parts[1] 167 } 168 169 // Auto-installed plugins contain an extra name portion representing 170 // the expected plugin version, which we must trim off. 171 if underX := strings.Index(version, "_x"); underX != -1 { 172 version = version[:underX] 173 } 174 175 if _, ok := found[nameVersion{name, version}]; ok { 176 // Skip duplicate versions of the same plugin 177 // (We do this during this step because after this we will be 178 // dealing with sets and thus lose our ordering with which to 179 // decide preference.) 180 continue 181 } 182 183 s.Add(PluginMeta{ 184 Name: name, 185 Version: VersionStr(version), 186 Path: path, 187 }) 188 found[nameVersion{name, version}] = struct{}{} 189 } 190 191 return s 192 }