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  }