github.com/itscaro/cli@v0.0.0-20190705081621-c9db0fe93829/cli-plugins/manager/manager.go (about)

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