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