2018-12-11 09:03:47 -05:00
|
|
|
package manager
|
|
|
|
|
|
|
|
import (
|
2023-04-01 09:40:32 -04:00
|
|
|
"context"
|
2018-12-11 09:03:47 -05:00
|
|
|
"os"
|
2023-05-25 20:03:45 -04:00
|
|
|
"os/exec"
|
2018-12-11 09:03:47 -05:00
|
|
|
"path/filepath"
|
docker info: list CLI plugins alphabetically
Before this change, plugins were listed in a random order:
Client:
Debug Mode: false
Plugins:
doodle: Docker Doodles all around! 🐳 🎃 (thaJeztah, v0.0.1)
shell: Open a browser shell on the Docker Host. (thaJeztah, v0.0.1)
app: Docker Application (Docker Inc., v0.8.0)
buildx: Build with BuildKit (Docker Inc., v0.3.1-tp-docker)
With this change, plugins are listed alphabetically:
Client:
Debug Mode: false
Plugins:
app: Docker Application (Docker Inc., v0.8.0)
buildx: Build with BuildKit (Docker Inc., v0.3.1-tp-docker)
doodle: Docker Doodles all around! 🐳 🎃 (thaJeztah, v0.0.1)
shell: Open a browser shell on the Docker Host. (thaJeztah, v0.0.1)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-12-31 08:26:08 -05:00
|
|
|
"sort"
|
2018-12-11 09:50:04 -05:00
|
|
|
"strings"
|
2023-04-01 09:40:32 -04:00
|
|
|
"sync"
|
2018-12-11 09:03:47 -05:00
|
|
|
|
|
|
|
"github.com/docker/cli/cli/command"
|
|
|
|
"github.com/docker/cli/cli/config"
|
2020-08-28 08:35:09 -04:00
|
|
|
"github.com/fvbommel/sortorder"
|
2018-12-11 09:03:47 -05:00
|
|
|
"github.com/spf13/cobra"
|
2023-04-01 09:40:32 -04:00
|
|
|
"golang.org/x/sync/errgroup"
|
2018-12-11 09:03:47 -05:00
|
|
|
)
|
|
|
|
|
2019-01-31 12:50:58 -05:00
|
|
|
// ReexecEnvvar is the name of an ennvar which is set to the command
|
|
|
|
// used to originally invoke the docker CLI when executing a
|
|
|
|
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
|
|
|
|
// the plugin to re-execute the original CLI.
|
|
|
|
const ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
|
|
|
|
|
2018-12-11 09:03:47 -05:00
|
|
|
// errPluginNotFound is the error returned when a plugin could not be found.
|
|
|
|
type errPluginNotFound string
|
|
|
|
|
|
|
|
func (e errPluginNotFound) NotFound() {}
|
|
|
|
|
|
|
|
func (e errPluginNotFound) Error() string {
|
|
|
|
return "Error: No such CLI plugin: " + string(e)
|
|
|
|
}
|
|
|
|
|
|
|
|
type notFound interface{ NotFound() }
|
|
|
|
|
|
|
|
// IsNotFound is true if the given error is due to a plugin not being found.
|
|
|
|
func IsNotFound(err error) bool {
|
2019-05-21 12:50:12 -04:00
|
|
|
if e, ok := err.(*pluginError); ok {
|
|
|
|
err = e.Cause()
|
|
|
|
}
|
2018-12-11 09:03:47 -05:00
|
|
|
_, ok := err.(notFound)
|
|
|
|
return ok
|
|
|
|
}
|
|
|
|
|
2019-03-07 09:28:42 -05:00
|
|
|
func getPluginDirs(dockerCli command.Cli) ([]string, error) {
|
2018-12-11 09:03:47 -05:00
|
|
|
var pluginDirs []string
|
|
|
|
|
|
|
|
if cfg := dockerCli.ConfigFile(); cfg != nil {
|
|
|
|
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
|
|
|
|
}
|
2019-03-07 09:28:42 -05:00
|
|
|
pluginDir, err := config.Path("cli-plugins")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
pluginDirs = append(pluginDirs, pluginDir)
|
2018-12-11 09:03:47 -05:00
|
|
|
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
|
2019-03-07 09:28:42 -05:00
|
|
|
return pluginDirs, nil
|
2018-12-11 09:03:47 -05:00
|
|
|
}
|
|
|
|
|
2018-12-11 09:50:04 -05:00
|
|
|
func addPluginCandidatesFromDir(res map[string][]string, d string) error {
|
2022-02-25 10:01:20 -05:00
|
|
|
dentries, err := os.ReadDir(d)
|
2018-12-11 09:50:04 -05:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for _, dentry := range dentries {
|
2022-02-25 10:01:20 -05:00
|
|
|
switch dentry.Type() & os.ModeType {
|
2018-12-11 09:50:04 -05:00
|
|
|
case 0, os.ModeSymlink:
|
|
|
|
// Regular file or symlink, keep going
|
|
|
|
default:
|
|
|
|
// Something else, ignore.
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
name := dentry.Name()
|
|
|
|
if !strings.HasPrefix(name, NamePrefix) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
name = strings.TrimPrefix(name, NamePrefix)
|
2019-01-14 12:53:19 -05:00
|
|
|
var err error
|
|
|
|
if name, err = trimExeSuffix(name); err != nil {
|
|
|
|
continue
|
2018-12-11 09:50:04 -05:00
|
|
|
}
|
|
|
|
res[name] = append(res[name], filepath.Join(d, dentry.Name()))
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority.
|
|
|
|
func listPluginCandidates(dirs []string) (map[string][]string, error) {
|
|
|
|
result := make(map[string][]string)
|
|
|
|
for _, d := range dirs {
|
|
|
|
// Silently ignore any directories which we cannot
|
|
|
|
// Stat (e.g. due to permissions or anything else) or
|
|
|
|
// which is not a directory.
|
|
|
|
if fi, err := os.Stat(d); err != nil || !fi.IsDir() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if err := addPluginCandidatesFromDir(result, d); err != nil {
|
|
|
|
// Silently ignore paths which don't exist.
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
return nil, err // Or return partial result?
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
2022-02-03 04:37:55 -05:00
|
|
|
// GetPlugin returns a plugin on the system by its name
|
|
|
|
func GetPlugin(name string, dockerCli command.Cli, rootcmd *cobra.Command) (*Plugin, error) {
|
|
|
|
pluginDirs, err := getPluginDirs(dockerCli)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
candidates, err := listPluginCandidates(pluginDirs)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if paths, ok := candidates[name]; ok {
|
|
|
|
if len(paths) == 0 {
|
|
|
|
return nil, errPluginNotFound(name)
|
|
|
|
}
|
|
|
|
c := &candidate{paths[0]}
|
2023-04-01 09:40:32 -04:00
|
|
|
p, err := newPlugin(c, rootcmd.Commands())
|
2022-02-03 04:37:55 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if !IsNotFound(p.Err) {
|
|
|
|
p.ShadowedPaths = paths[1:]
|
|
|
|
}
|
|
|
|
return &p, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, errPluginNotFound(name)
|
|
|
|
}
|
|
|
|
|
2018-12-11 09:50:04 -05:00
|
|
|
// ListPlugins produces a list of the plugins available on the system
|
|
|
|
func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
|
2019-03-07 09:28:42 -05:00
|
|
|
pluginDirs, err := getPluginDirs(dockerCli)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
candidates, err := listPluginCandidates(pluginDirs)
|
2018-12-11 09:50:04 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var plugins []Plugin
|
2023-04-01 09:40:32 -04:00
|
|
|
var mu sync.Mutex
|
|
|
|
eg, _ := errgroup.WithContext(context.TODO())
|
|
|
|
cmds := rootcmd.Commands()
|
2018-12-11 09:50:04 -05:00
|
|
|
for _, paths := range candidates {
|
2023-04-01 09:40:32 -04:00
|
|
|
func(paths []string) {
|
|
|
|
eg.Go(func() error {
|
|
|
|
if len(paths) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
c := &candidate{paths[0]}
|
|
|
|
p, err := newPlugin(c, cmds)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if !IsNotFound(p.Err) {
|
|
|
|
p.ShadowedPaths = paths[1:]
|
|
|
|
mu.Lock()
|
|
|
|
defer mu.Unlock()
|
|
|
|
plugins = append(plugins, p)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}(paths)
|
|
|
|
}
|
|
|
|
if err := eg.Wait(); err != nil {
|
|
|
|
return nil, err
|
2018-12-11 09:50:04 -05:00
|
|
|
}
|
|
|
|
|
docker info: list CLI plugins alphabetically
Before this change, plugins were listed in a random order:
Client:
Debug Mode: false
Plugins:
doodle: Docker Doodles all around! 🐳 🎃 (thaJeztah, v0.0.1)
shell: Open a browser shell on the Docker Host. (thaJeztah, v0.0.1)
app: Docker Application (Docker Inc., v0.8.0)
buildx: Build with BuildKit (Docker Inc., v0.3.1-tp-docker)
With this change, plugins are listed alphabetically:
Client:
Debug Mode: false
Plugins:
app: Docker Application (Docker Inc., v0.8.0)
buildx: Build with BuildKit (Docker Inc., v0.3.1-tp-docker)
doodle: Docker Doodles all around! 🐳 🎃 (thaJeztah, v0.0.1)
shell: Open a browser shell on the Docker Host. (thaJeztah, v0.0.1)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-12-31 08:26:08 -05:00
|
|
|
sort.Slice(plugins, func(i, j int) bool {
|
|
|
|
return sortorder.NaturalLess(plugins[i].Name, plugins[j].Name)
|
|
|
|
})
|
|
|
|
|
2018-12-11 09:50:04 -05:00
|
|
|
return plugins, nil
|
|
|
|
}
|
|
|
|
|
2018-12-11 09:03:47 -05:00
|
|
|
// PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin.
|
|
|
|
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
|
|
|
|
// The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow.
|
|
|
|
func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
|
|
|
|
// This uses the full original args, not the args which may
|
|
|
|
// have been provided by cobra to our caller. This is because
|
|
|
|
// they lack e.g. global options which we must propagate here.
|
|
|
|
args := os.Args[1:]
|
|
|
|
if !pluginNameRe.MatchString(name) {
|
|
|
|
// We treat this as "not found" so that callers will
|
|
|
|
// fallback to their "invalid" command path.
|
|
|
|
return nil, errPluginNotFound(name)
|
|
|
|
}
|
2019-01-14 12:53:19 -05:00
|
|
|
exename := addExeSuffix(NamePrefix + name)
|
2019-03-07 09:28:42 -05:00
|
|
|
pluginDirs, err := getPluginDirs(dockerCli)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, d := range pluginDirs {
|
2018-12-11 09:03:47 -05:00
|
|
|
path := filepath.Join(d, exename)
|
|
|
|
|
|
|
|
// We stat here rather than letting the exec tell us
|
|
|
|
// ENOENT because the latter does not distinguish a
|
|
|
|
// file not existing from its dynamic loader or one of
|
|
|
|
// its libraries not existing.
|
|
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
c := &candidate{path: path}
|
2023-04-01 09:40:32 -04:00
|
|
|
plugin, err := newPlugin(c, rootcmd.Commands())
|
2018-12-11 09:03:47 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if plugin.Err != nil {
|
2019-04-17 18:09:29 -04:00
|
|
|
// TODO: why are we not returning plugin.Err?
|
2018-12-11 09:03:47 -05:00
|
|
|
return nil, errPluginNotFound(name)
|
|
|
|
}
|
|
|
|
cmd := exec.Command(plugin.Path, args...)
|
|
|
|
// Using dockerCli.{In,Out,Err}() here results in a hang until something is input.
|
|
|
|
// See: - https://github.com/golang/go/issues/10338
|
|
|
|
// - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab
|
|
|
|
// os.Stdin is a *os.File which avoids this behaviour. We don't need the functionality
|
|
|
|
// of the wrappers here anyway.
|
|
|
|
cmd.Stdin = os.Stdin
|
|
|
|
cmd.Stdout = os.Stdout
|
|
|
|
cmd.Stderr = os.Stderr
|
plugins: run plugin with new process group ID
Changes were made in 1554ac3b5f38147301c518c60da16f3f80c1717b to provide
a mechanism for the CLI to notify running plugin processes that they
should exit, in order to improve the general CLI/plugin UX. The current
implementation boils down to:
1. The CLI creates a socket
2. The CLI executes the plugin
3. The plugin connects to the socket
4. (When) the CLI receives a termination signal, it uses the socket to
notify the plugin that it should exit
5. The plugin's gets notified via the socket, and cancels it's `cmd.Context`,
which then gets handled appropriately
This change works in most cases and fixes the issue it sets out to solve
(see: https://github.com/docker/compose/pull/11292) however, in the case
where the user has a TTY attached and the plugin is not already handling
received signals, steps 4+ changes:
4. (When) the CLI receives a termination signal, before it can use the
socket to notify the plugin that it should exit, the plugin process
also receives a signal due to sharing the pgid with the CLI
Since we now have a proper "job control" mechanism, we can simplify the
scenarios by executing the plugins with their own process group id,
thereby removing the "double notification" issue and making it so that
plugins can handle the same whether attached to a TTY or not.
In order to make this change "plugin-binary" backwards-compatible, in
the case that a plugin does not connect to the socket, the CLI passes
the signal to the plugin process.
Co-authored-by: Bjorn Neergaard <bjorn.neergaard@docker.com>
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
Signed-off-by: Bjorn Neergaard <bjorn.neergaard@docker.com>
2024-01-08 06:59:30 -05:00
|
|
|
configureOSSpecificCommand(cmd)
|
2018-12-11 09:03:47 -05:00
|
|
|
|
2019-01-31 12:50:58 -05:00
|
|
|
cmd.Env = os.Environ()
|
|
|
|
cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0])
|
|
|
|
|
2018-12-11 09:03:47 -05:00
|
|
|
return cmd, nil
|
|
|
|
}
|
|
|
|
return nil, errPluginNotFound(name)
|
|
|
|
}
|
2022-09-29 16:43:47 -04:00
|
|
|
|
|
|
|
// IsPluginCommand checks if the given cmd is a plugin-stub.
|
|
|
|
func IsPluginCommand(cmd *cobra.Command) bool {
|
|
|
|
return cmd.Annotations[CommandAnnotationPlugin] == "true"
|
|
|
|
}
|