github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/cli/cli-plugins/manager/manager.go (about)

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