github.com/cspotcode/docker-cli@v20.10.0-rc1.0.20201201121459-3faad7acc5b8+incompatible/cli-plugins/manager/manager.go (about)

     1  package manager
     2  
     3  import (
     4  	"io/ioutil"
     5  	"os"
     6  	"os/exec"
     7  	"path/filepath"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/docker/cli/cli/command"
    12  	"github.com/docker/cli/cli/config"
    13  	"github.com/fvbommel/sortorder"
    14  	"github.com/spf13/cobra"
    15  )
    16  
    17  // ReexecEnvvar is the name of an ennvar which is set to the command
    18  // used to originally invoke the docker CLI when executing a
    19  // plugin. Assuming $PATH and $CWD remain unchanged this should allow
    20  // the plugin to re-execute the original CLI.
    21  const ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
    22  
    23  // errPluginNotFound is the error returned when a plugin could not be found.
    24  type errPluginNotFound string
    25  
    26  func (e errPluginNotFound) NotFound() {}
    27  
    28  func (e errPluginNotFound) Error() string {
    29  	return "Error: No such CLI plugin: " + string(e)
    30  }
    31  
    32  type notFound interface{ NotFound() }
    33  
    34  // IsNotFound is true if the given error is due to a plugin not being found.
    35  func IsNotFound(err error) bool {
    36  	if e, ok := err.(*pluginError); ok {
    37  		err = e.Cause()
    38  	}
    39  	_, ok := err.(notFound)
    40  	return ok
    41  }
    42  
    43  func getPluginDirs(dockerCli command.Cli) ([]string, error) {
    44  	var pluginDirs []string
    45  
    46  	if cfg := dockerCli.ConfigFile(); cfg != nil {
    47  		pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
    48  	}
    49  	pluginDir, err := config.Path("cli-plugins")
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  
    54  	pluginDirs = append(pluginDirs, pluginDir)
    55  	pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
    56  	return pluginDirs, nil
    57  }
    58  
    59  func addPluginCandidatesFromDir(res map[string][]string, d string) error {
    60  	dentries, err := ioutil.ReadDir(d)
    61  	if err != nil {
    62  		return err
    63  	}
    64  	for _, dentry := range dentries {
    65  		switch dentry.Mode() & os.ModeType {
    66  		case 0, os.ModeSymlink:
    67  			// Regular file or symlink, keep going
    68  		default:
    69  			// Something else, ignore.
    70  			continue
    71  		}
    72  		name := dentry.Name()
    73  		if !strings.HasPrefix(name, NamePrefix) {
    74  			continue
    75  		}
    76  		name = strings.TrimPrefix(name, NamePrefix)
    77  		var err error
    78  		if name, err = trimExeSuffix(name); err != nil {
    79  			continue
    80  		}
    81  		res[name] = append(res[name], filepath.Join(d, dentry.Name()))
    82  	}
    83  	return nil
    84  }
    85  
    86  // listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority.
    87  func listPluginCandidates(dirs []string) (map[string][]string, error) {
    88  	result := make(map[string][]string)
    89  	for _, d := range dirs {
    90  		// Silently ignore any directories which we cannot
    91  		// Stat (e.g. due to permissions or anything else) or
    92  		// which is not a directory.
    93  		if fi, err := os.Stat(d); err != nil || !fi.IsDir() {
    94  			continue
    95  		}
    96  		if err := addPluginCandidatesFromDir(result, d); err != nil {
    97  			// Silently ignore paths which don't exist.
    98  			if os.IsNotExist(err) {
    99  				continue
   100  			}
   101  			return nil, err // Or return partial result?
   102  		}
   103  	}
   104  	return result, nil
   105  }
   106  
   107  // ListPlugins produces a list of the plugins available on the system
   108  func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
   109  	pluginDirs, err := getPluginDirs(dockerCli)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	candidates, err := listPluginCandidates(pluginDirs)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	var plugins []Plugin
   120  	for _, paths := range candidates {
   121  		if len(paths) == 0 {
   122  			continue
   123  		}
   124  		c := &candidate{paths[0]}
   125  		p, err := newPlugin(c, rootcmd)
   126  		if err != nil {
   127  			return nil, err
   128  		}
   129  		if !IsNotFound(p.Err) {
   130  			p.ShadowedPaths = paths[1:]
   131  			plugins = append(plugins, p)
   132  		}
   133  	}
   134  
   135  	sort.Slice(plugins, func(i, j int) bool {
   136  		return sortorder.NaturalLess(plugins[i].Name, plugins[j].Name)
   137  	})
   138  
   139  	return plugins, nil
   140  }
   141  
   142  // PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin.
   143  // The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
   144  // The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow.
   145  func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
   146  	// This uses the full original args, not the args which may
   147  	// have been provided by cobra to our caller. This is because
   148  	// they lack e.g. global options which we must propagate here.
   149  	args := os.Args[1:]
   150  	if !pluginNameRe.MatchString(name) {
   151  		// We treat this as "not found" so that callers will
   152  		// fallback to their "invalid" command path.
   153  		return nil, errPluginNotFound(name)
   154  	}
   155  	exename := addExeSuffix(NamePrefix + name)
   156  	pluginDirs, err := getPluginDirs(dockerCli)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  
   161  	for _, d := range pluginDirs {
   162  		path := filepath.Join(d, exename)
   163  
   164  		// We stat here rather than letting the exec tell us
   165  		// ENOENT because the latter does not distinguish a
   166  		// file not existing from its dynamic loader or one of
   167  		// its libraries not existing.
   168  		if _, err := os.Stat(path); os.IsNotExist(err) {
   169  			continue
   170  		}
   171  
   172  		c := &candidate{path: path}
   173  		plugin, err := newPlugin(c, rootcmd)
   174  		if err != nil {
   175  			return nil, err
   176  		}
   177  		if plugin.Err != nil {
   178  			// TODO: why are we not returning plugin.Err?
   179  			return nil, errPluginNotFound(name)
   180  		}
   181  		cmd := exec.Command(plugin.Path, args...)
   182  		// Using dockerCli.{In,Out,Err}() here results in a hang until something is input.
   183  		// See: - https://github.com/golang/go/issues/10338
   184  		//      - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab
   185  		// os.Stdin is a *os.File which avoids this behaviour. We don't need the functionality
   186  		// of the wrappers here anyway.
   187  		cmd.Stdin = os.Stdin
   188  		cmd.Stdout = os.Stdout
   189  		cmd.Stderr = os.Stderr
   190  
   191  		cmd.Env = os.Environ()
   192  		cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0])
   193  
   194  		return cmd, nil
   195  	}
   196  	return nil, errPluginNotFound(name)
   197  }