package discovery import ( "io/ioutil" "log" "path/filepath" "runtime" "strings" ) const machineName = runtime.GOOS + "_" + runtime.GOARCH // 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, machineName, dirs) } func findPluginPaths(kind string, machineName string, dirs []string) []string { prefix := "terraform-" + kind + "-" ret := make([]string, 0, len(dirs)) for _, baseDir := range dirs { baseItems, err := ioutil.ReadDir(baseDir) if err != nil { // Ignore missing dirs, non-dirs, etc continue } log.Printf("[DEBUG] checking for plugins in %q", baseDir) for _, item := range baseItems { fullName := item.Name() if fullName == machineName && item.Mode().IsDir() { // Current-style $GOOS-$GOARCH directory prefix machineDir := filepath.Join(baseDir, machineName) machineItems, err := ioutil.ReadDir(machineDir) if err != nil { continue } log.Printf("[DEBUG] checking for plugins in %q", machineDir) for _, item := range machineItems { fullName := item.Name() if !strings.HasPrefix(fullName, prefix) { continue } // New-style paths must have a version segment in filename if !strings.Contains(strings.ToLower(fullName), "_v") { continue } absPath, err := filepath.Abs(filepath.Join(machineDir, fullName)) if err != nil { continue } log.Printf("[DEBUG] found plugin %q", fullName) ret = append(ret, filepath.Clean(absPath)) } continue } // FIXME: we pass in GOOS_GOARCH paths directly, so these may not be "legacy" if strings.HasPrefix(fullName, prefix) { // Legacy style with files directly in the base directory absPath, err := filepath.Abs(filepath.Join(baseDir, fullName)) if err != nil { continue } log.Printf("[DEBUG] found legacy plugin %q", 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 } parts := strings.SplitN(baseName, "_v", 2) name := parts[0] version := "0.0.0" 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 }