...

Source file src/github.com/docker/cli/cli-plugins/manager/manager.go

Documentation: github.com/docker/cli/cli-plugins/manager

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

View as plain text