2018-12-11 14:03:47 +00:00
package manager
import (
2023-04-01 15:40:32 +02:00
"context"
2018-12-11 14:03:47 +00:00
"os"
2023-05-26 02:03:45 +02:00
"os/exec"
2018-12-11 14:03:47 +00: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 14:26:08 +01:00
"sort"
2018-12-11 14:50:04 +00:00
"strings"
2023-04-01 15:40:32 +02:00
"sync"
2018-12-11 14:03:47 +00:00
2025-03-04 23:16:33 +01:00
"github.com/docker/cli/cli-plugins/metadata"
2018-12-11 14:03:47 +00:00
"github.com/docker/cli/cli/config"
2024-01-11 18:15:30 +01:00
"github.com/docker/cli/cli/config/configfile"
2020-08-28 14:35:09 +02:00
"github.com/fvbommel/sortorder"
2018-12-11 14:03:47 +00:00
"github.com/spf13/cobra"
2023-04-01 15:40:32 +02:00
"golang.org/x/sync/errgroup"
2018-12-11 14:03:47 +00:00
)
2024-02-13 15:40:55 -06:00
const (
// 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.
2025-03-04 23:16:33 +01:00
ReexecEnvvar = metadata . ReexecEnvvar
2024-02-13 15:40:55 -06:00
// ResourceAttributesEnvvar is the name of the envvar that includes additional
// resource attributes for OTEL.
cli/command: un-export ResourceAttributesEnvvar, DockerCliAttributePrefix
These utility functions were added in 8890a1c9292ed5f2bf45df44a0e57d345f563d31,
and are all related to OTEL. The ResourceAttributesEnvvar const defines
the "OTEL_RESOURCE_ATTRIBUTES" environment-variable to use, which is part
of the [OpenTelemetry specification], so should be considered a well-known
env-var, and not up to us to define a const for. These code-changes were not
yet included in a release, so we don't have to deprecate.
This patch:
- Moves the utility functions to the telemetry files, so that all code related
to OpenTelemetry is together.
- Un-exports the ResourceAttributesEnvvar to reduce our public API.
- Un-exports the DockerCliAttributePrefix to reduce depdency on cli/command
in CLI-plugins, but adds a TODO to move telemetry-related code to a common
(internal) package.
- Deprecates the cli-plugins/manager.ResourceAttributesEnvvar const. This
const has no known consumers, so we could skip deprecation, but just in
case some codebase uses this.
[OpenTelemetry specification]: https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-03-03 14:21:45 +01:00
//
// Deprecated: The "OTEL_RESOURCE_ATTRIBUTES" env-var is part of the OpenTelemetry specification; users should define their own const for this. This const will be removed in the next release.
ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES"
2024-02-13 15:40:55 -06:00
)
2019-01-31 17:50:58 +00:00
2018-12-11 14:03:47 +00:00
// errPluginNotFound is the error returned when a plugin could not be found.
type errPluginNotFound string
2025-02-17 14:07:32 +01:00
func ( errPluginNotFound ) NotFound ( ) { }
2018-12-11 14:03:47 +00:00
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 16:50:12 +00:00
if e , ok := err . ( * pluginError ) ; ok {
err = e . Cause ( )
}
2018-12-11 14:03:47 +00:00
_ , ok := err . ( notFound )
return ok
}
2024-06-19 14:02:40 +02:00
// getPluginDirs returns the platform-specific locations to search for plugins
// in order of preference.
//
// Plugin-discovery is performed in the following order of preference:
//
// 1. The "cli-plugins" directory inside the CLIs [config.Path] (usually "~/.docker/cli-plugins").
// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs].
// 3. Platform-specific defaultSystemPluginDirs.
//
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
2025-03-18 11:47:48 +01:00
func getPluginDirs ( cfg * configfile . ConfigFile ) [ ] string {
2018-12-11 14:03:47 +00:00
var pluginDirs [ ] string
2024-01-11 18:15:30 +01:00
if cfg != nil {
2018-12-11 14:03:47 +00:00
pluginDirs = append ( pluginDirs , cfg . CLIPluginsExtraDirs ... )
}
2025-03-18 11:47:48 +01:00
pluginDir := filepath . Join ( config . Dir ( ) , "cli-plugins" )
2019-03-07 14:28:42 +00:00
pluginDirs = append ( pluginDirs , pluginDir )
2018-12-11 14:03:47 +00:00
pluginDirs = append ( pluginDirs , defaultSystemPluginDirs ... )
2025-03-18 11:47:48 +01:00
return pluginDirs
2018-12-11 14:03:47 +00:00
}
2024-11-28 14:24:40 +01:00
func addPluginCandidatesFromDir ( res map [ string ] [ ] string , d string ) {
2022-02-25 16:01:20 +01:00
dentries , err := os . ReadDir ( d )
2024-11-28 12:45:19 +01:00
// Silently ignore any directories which we cannot list (e.g. due to
// permissions or anything else) or which is not a directory
2018-12-11 14:50:04 +00:00
if err != nil {
2024-11-28 14:24:40 +01:00
return
2018-12-11 14:50:04 +00:00
}
for _ , dentry := range dentries {
2025-05-19 19:10:53 +02:00
switch dentry . Type ( ) & os . ModeType { //nolint:exhaustive,nolintlint // no need to include all possible file-modes in this list
2018-12-11 14:50:04 +00:00
case 0 , os . ModeSymlink :
// Regular file or symlink, keep going
default :
// Something else, ignore.
continue
}
name := dentry . Name ( )
2025-03-04 23:16:33 +01:00
if ! strings . HasPrefix ( name , metadata . NamePrefix ) {
2018-12-11 14:50:04 +00:00
continue
}
2025-03-04 23:16:33 +01:00
name = strings . TrimPrefix ( name , metadata . NamePrefix )
2019-01-14 17:53:19 +00:00
var err error
if name , err = trimExeSuffix ( name ) ; err != nil {
continue
2018-12-11 14:50:04 +00:00
}
res [ name ] = append ( res [ name ] , filepath . Join ( d , dentry . Name ( ) ) )
}
}
// listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority.
2024-11-28 14:24:40 +01:00
func listPluginCandidates ( dirs [ ] string ) map [ string ] [ ] string {
2018-12-11 14:50:04 +00:00
result := make ( map [ string ] [ ] string )
for _ , d := range dirs {
2024-11-28 14:24:40 +01:00
addPluginCandidatesFromDir ( result , d )
2018-12-11 14:50:04 +00:00
}
2024-11-28 14:24:40 +01:00
return result
2018-12-11 14:50:04 +00:00
}
2022-02-03 10:37:55 +01:00
// GetPlugin returns a plugin on the system by its name
2025-03-04 22:45:51 +01:00
func GetPlugin ( name string , dockerCLI config . Provider , rootcmd * cobra . Command ) ( * Plugin , error ) {
2025-03-18 11:47:48 +01:00
pluginDirs := getPluginDirs ( dockerCLI . ConfigFile ( ) )
2025-03-04 22:45:51 +01:00
return getPlugin ( name , pluginDirs , rootcmd )
}
2022-02-03 10:37:55 +01:00
2025-03-04 22:45:51 +01:00
func getPlugin ( name string , pluginDirs [ ] string , rootcmd * cobra . Command ) ( * Plugin , error ) {
2024-11-28 14:24:40 +01:00
candidates := listPluginCandidates ( pluginDirs )
2022-02-03 10:37:55 +01:00
if paths , ok := candidates [ name ] ; ok {
if len ( paths ) == 0 {
return nil , errPluginNotFound ( name )
}
c := & candidate { paths [ 0 ] }
2023-04-01 15:40:32 +02:00
p , err := newPlugin ( c , rootcmd . Commands ( ) )
2022-02-03 10:37:55 +01: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 14:50:04 +00:00
// ListPlugins produces a list of the plugins available on the system
2025-03-04 22:45:51 +01:00
func ListPlugins ( dockerCli config . Provider , rootcmd * cobra . Command ) ( [ ] Plugin , error ) {
2025-03-18 11:47:48 +01:00
pluginDirs := getPluginDirs ( dockerCli . ConfigFile ( ) )
2024-11-28 14:24:40 +01:00
candidates := listPluginCandidates ( pluginDirs )
2025-03-18 11:46:37 +01:00
if len ( candidates ) == 0 {
return nil , nil
}
2018-12-11 14:50:04 +00:00
var plugins [ ] Plugin
2023-04-01 15:40:32 +02:00
var mu sync . Mutex
2025-03-18 11:13:29 +01:00
ctx := rootcmd . Context ( )
if ctx == nil {
// Fallback, mostly for tests that pass a bare cobra.command
ctx = context . Background ( )
}
eg , _ := errgroup . WithContext ( ctx )
2023-04-01 15:40:32 +02:00
cmds := rootcmd . Commands ( )
2018-12-11 14:50:04 +00:00
for _ , paths := range candidates {
2023-04-01 15:40:32 +02: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 14:50:04 +00: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 14:26:08 +01:00
sort . Slice ( plugins , func ( i , j int ) bool {
return sortorder . NaturalLess ( plugins [ i ] . Name , plugins [ j ] . Name )
} )
2018-12-11 14:50:04 +00:00
return plugins , nil
}
2018-12-11 14:03:47 +00: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.
2025-03-04 22:45:51 +01:00
func PluginRunCommand ( dockerCli config . Provider , name string , rootcmd * cobra . Command ) ( * exec . Cmd , error ) {
2018-12-11 14:03:47 +00:00
// 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 )
}
2025-03-04 23:16:33 +01:00
exename := addExeSuffix ( metadata . NamePrefix + name )
2025-03-18 11:47:48 +01:00
pluginDirs := getPluginDirs ( dockerCli . ConfigFile ( ) )
2019-03-07 14:28:42 +00:00
for _ , d := range pluginDirs {
2018-12-11 14:03:47 +00: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 15:40:32 +02:00
plugin , err := newPlugin ( c , rootcmd . Commands ( ) )
2018-12-11 14:03:47 +00:00
if err != nil {
return nil , err
}
if plugin . Err != nil {
2019-04-17 22:09:29 +00:00
// TODO: why are we not returning plugin.Err?
2018-12-11 14:03:47 +00:00
return nil , errPluginNotFound ( name )
}
2024-10-30 15:30:46 +01:00
cmd := exec . Command ( plugin . Path , args ... ) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
2018-12-11 14:03:47 +00:00
// 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
2025-03-04 23:16:33 +01:00
cmd . Env = append ( cmd . Environ ( ) , metadata . ReexecEnvvar + "=" + os . Args [ 0 ] )
2024-02-13 15:40:55 -06:00
cmd . Env = appendPluginResourceAttributesEnvvar ( cmd . Env , rootcmd , plugin )
2019-01-31 17:50:58 +00:00
2018-12-11 14:03:47 +00:00
return cmd , nil
}
return nil , errPluginNotFound ( name )
}
2022-09-29 22:43:47 +02:00
// IsPluginCommand checks if the given cmd is a plugin-stub.
func IsPluginCommand ( cmd * cobra . Command ) bool {
2025-03-04 23:33:21 +01:00
return cmd . Annotations [ metadata . CommandAnnotationPlugin ] == "true"
2022-09-29 22:43:47 +02:00
}