package discovery import ( "io/ioutil" "log" "path/filepath" "strings" ) // FindPlugins looks in the given directories for files whose filenames // suggest that they are plugins of the given kind (e.g. "provider") and // returns a PluginMetaSet representing the discovered potential-plugins. // // Currently this supports two different naming schemes. The current // standard naming scheme is a subdirectory called $GOOS-$GOARCH containing // files named terraform-$KIND-$NAME-V$VERSION. The legacy naming scheme is // files directly in the given directory whose names are like // terraform-$KIND-$NAME. // // Only one plugin will be returned for each unique plugin (name, version) // pair, with preference given to files found in earlier directories. // // This is a convenience wrapper around FindPluginPaths and ResolvePluginsPaths. func FindPlugins(kind string, dirs []string) PluginMetaSet { return ResolvePluginPaths(FindPluginPaths(kind, dirs)) } // FindPluginPaths looks in the given directories for files whose filenames // suggest that they are plugins of the given kind (e.g. "provider"). // // The return value is a list of absolute paths that appear to refer to // plugins in the given directories, based only on what can be inferred // from the naming scheme. The paths returned are ordered such that files // in later dirs appear after files in earlier dirs in the given directory // list. Within the same directory plugins are returned in a consistent but // undefined order. func FindPluginPaths(kind string, dirs []string) []string { // This is just a thin wrapper around findPluginPaths so that we can // use the latter in tests with a fake machineName so we can use our // test fixtures. return findPluginPaths(kind, dirs) } func findPluginPaths(kind string, dirs []string) []string { prefix := "terraform-" + kind + "-" ret := make([]string, 0, len(dirs)) for _, dir := range dirs { items, err := ioutil.ReadDir(dir) if err != nil { // Ignore missing dirs, non-dirs, etc continue } log.Printf("[DEBUG] checking for %s in %q", kind, dir) for _, item := range items { fullName := item.Name() if !strings.HasPrefix(fullName, prefix) { log.Printf("[DEBUG] skipping %q, not a %s", fullName, kind) continue } // New-style paths must have a version segment in filename if strings.Contains(strings.ToLower(fullName), "_v") { absPath, err := filepath.Abs(filepath.Join(dir, fullName)) if err != nil { log.Printf("[ERROR] plugin filepath error: %s", err) continue } log.Printf("[DEBUG] found %s %q", kind, fullName) ret = append(ret, filepath.Clean(absPath)) continue } // Legacy style with files directly in the base directory absPath, err := filepath.Abs(filepath.Join(dir, fullName)) if err != nil { log.Printf("[ERROR] plugin filepath error: %s", err) continue } log.Printf("[WARNING] found legacy %s %q", kind, fullName) ret = append(ret, filepath.Clean(absPath)) } } return ret } // ResolvePluginPaths takes a list of paths to plugin executables (as returned // by e.g. FindPluginPaths) and produces a PluginMetaSet describing the // referenced plugins. // // If the same combination of plugin name and version appears multiple times, // the earlier reference will be preferred. Several different versions of // the same plugin name may be returned, in which case the methods of // PluginMetaSet can be used to filter down. func ResolvePluginPaths(paths []string) PluginMetaSet { s := make(PluginMetaSet) type nameVersion struct { Name string Version string } found := make(map[nameVersion]struct{}) for _, path := range paths { baseName := strings.ToLower(filepath.Base(path)) if !strings.HasPrefix(baseName, "terraform-") { // Should never happen with reasonable input continue } baseName = baseName[10:] firstDash := strings.Index(baseName, "-") if firstDash == -1 { // Should never happen with reasonable input continue } baseName = baseName[firstDash+1:] if baseName == "" { // Should never happen with reasonable input continue } // Trim the .exe suffix used on Windows before we start wrangling // the remainder of the path. if strings.HasSuffix(baseName, ".exe") { baseName = baseName[:len(baseName)-4] } parts := strings.SplitN(baseName, "_v", 2) name := parts[0] version := VersionZero if len(parts) == 2 { version = parts[1] } // Auto-installed plugins contain an extra name portion representing // the expected plugin version, which we must trim off. if underX := strings.Index(version, "_x"); underX != -1 { version = version[:underX] } if _, ok := found[nameVersion{name, version}]; ok { // Skip duplicate versions of the same plugin // (We do this during this step because after this we will be // dealing with sets and thus lose our ordering with which to // decide preference.) continue } s.Add(PluginMeta{ Name: name, Version: VersionStr(version), Path: path, }) found[nameVersion{name, version}] = struct{}{} } return s }