2018-12-11 09:50:04 -05:00
|
|
|
package manager
|
|
|
|
|
|
|
|
import (
|
2022-03-30 09:27:25 -04:00
|
|
|
"fmt"
|
2024-02-28 15:36:45 -05:00
|
|
|
"net/url"
|
2022-03-30 09:27:25 -04:00
|
|
|
"os"
|
2024-02-28 15:36:45 -05:00
|
|
|
"strings"
|
2023-03-27 22:08:07 -04:00
|
|
|
"sync"
|
2022-03-30 09:27:25 -04:00
|
|
|
|
2018-12-11 09:50:04 -05:00
|
|
|
"github.com/docker/cli/cli/command"
|
|
|
|
"github.com/spf13/cobra"
|
2024-02-13 16:40:55 -05:00
|
|
|
"go.opentelemetry.io/otel/attribute"
|
2018-12-11 09:50:04 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// CommandAnnotationPlugin is added to every stub command added by
|
|
|
|
// AddPluginCommandStubs with the value "true" and so can be
|
|
|
|
// used to distinguish plugin stubs from regular commands.
|
|
|
|
CommandAnnotationPlugin = "com.docker.cli.plugin"
|
|
|
|
|
|
|
|
// CommandAnnotationPluginVendor is added to every stub command
|
|
|
|
// added by AddPluginCommandStubs and contains the vendor of
|
|
|
|
// that plugin.
|
|
|
|
CommandAnnotationPluginVendor = "com.docker.cli.plugin.vendor"
|
|
|
|
|
2019-02-15 11:27:22 -05:00
|
|
|
// CommandAnnotationPluginVersion is added to every stub command
|
|
|
|
// added by AddPluginCommandStubs and contains the version of
|
|
|
|
// that plugin.
|
|
|
|
CommandAnnotationPluginVersion = "com.docker.cli.plugin.version"
|
|
|
|
|
2018-12-11 09:50:04 -05:00
|
|
|
// CommandAnnotationPluginInvalid is added to any stub command
|
|
|
|
// added by AddPluginCommandStubs for an invalid command (that
|
|
|
|
// is, one which failed it's candidate test) and contains the
|
|
|
|
// reason for the failure.
|
|
|
|
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"
|
2024-02-13 16:40:55 -05:00
|
|
|
|
|
|
|
// CommandAnnotationPluginCommandPath is added to overwrite the
|
|
|
|
// command path for a plugin invocation.
|
|
|
|
CommandAnnotationPluginCommandPath = "com.docker.cli.plugin.command_path"
|
2018-12-11 09:50:04 -05:00
|
|
|
)
|
|
|
|
|
2023-03-27 22:08:07 -04:00
|
|
|
var pluginCommandStubsOnce sync.Once
|
|
|
|
|
2019-01-30 05:46:31 -05:00
|
|
|
// AddPluginCommandStubs adds a stub cobra.Commands for each valid and invalid
|
|
|
|
// plugin. The command stubs will have several annotations added, see
|
|
|
|
// `CommandAnnotationPlugin*`.
|
2023-03-27 22:08:07 -04:00
|
|
|
func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err error) {
|
|
|
|
pluginCommandStubsOnce.Do(func() {
|
|
|
|
var plugins []Plugin
|
|
|
|
plugins, err = ListPlugins(dockerCli, rootCmd)
|
|
|
|
if err != nil {
|
|
|
|
return
|
2018-12-11 09:50:04 -05:00
|
|
|
}
|
2023-03-27 22:08:07 -04:00
|
|
|
for _, p := range plugins {
|
|
|
|
p := p
|
|
|
|
vendor := p.Vendor
|
|
|
|
if vendor == "" {
|
|
|
|
vendor = "unknown"
|
|
|
|
}
|
|
|
|
annotations := map[string]string{
|
|
|
|
CommandAnnotationPlugin: "true",
|
|
|
|
CommandAnnotationPluginVendor: vendor,
|
|
|
|
CommandAnnotationPluginVersion: p.Version,
|
|
|
|
}
|
|
|
|
if p.Err != nil {
|
|
|
|
annotations[CommandAnnotationPluginInvalid] = p.Err.Error()
|
|
|
|
}
|
|
|
|
rootCmd.AddCommand(&cobra.Command{
|
|
|
|
Use: p.Name,
|
|
|
|
Short: p.ShortDescription,
|
|
|
|
Run: func(_ *cobra.Command, _ []string) {},
|
|
|
|
Annotations: annotations,
|
|
|
|
DisableFlagParsing: true,
|
|
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
|
|
flags := rootCmd.PersistentFlags()
|
|
|
|
flags.SetOutput(nil)
|
|
|
|
perr := flags.Parse(args)
|
|
|
|
if perr != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if flags.Changed("help") {
|
|
|
|
cmd.HelpFunc()(rootCmd, args)
|
|
|
|
return nil
|
|
|
|
}
|
cli: improve output and consistency for unknown (sub)commands
Before this patch, output for invalid top-level and sub-commands differed.
For top-level commands, the CLI would print an error-message and a suggestion
to use `--help`. For missing *subcommands*, we would hit a different code-path,
and different output, which includes full "usage" / "help" output.
While it is a common convention to show usage output, and may have been
a nice gesture when docker was still young and only had a few commands
and options ("you did something wrong; here's an overview of what you
can use"), that's no longer the case, and many commands have a _very_
long output.
The result of this is that the error message, which is the relevant
information in this case - "You mis-typed something" - is lost in the
output, and hard to find (sometimes even requiring scrolling back).
The output is also confusing, because it _looks_ like something ran
successfully (most of the output is not about the error!).
Even further; the suggested resolution (try `--help` to see the correct
options) is rather redundant, because running teh command with `--help`
produces _exactly_ the same output as was just showh, baring the error
message. As a fun fact, due to the usage output being printed, the
output even contains not one, but _two_ "call to actions";
- `See 'docker volume --help'.` (under the erro message)
- `Run 'docker volume COMMAND --help' for more information on a command.`
(under the usage output)
In short; the output is too verbose, confusing, and doesn't provide
a good UX. Let's reduce the output produced so that the focus is on the
important information.
This patch:
- Changes the usage to the short-usage.
- Changes the error-message to mention the _full_ command instead of only
the command after `docker` (so `docker no-such-command` instead of
`no-such-command`).
- Prefixes the error message with the binary / root-command name
(usually `docker:`); this is something we can still decide on, but
it's a pattern we already use in some places. The motivation for this
is that `docker` commands can often produce output that's a combination
of output from the CLI itself, output from the daemon, and even output
from the container. The `docker:` prefix helps to distinguish where
the message originated from (the `docker` CLI in this case).
- Adds an empty line between the error-message and the "call to action"
(`Run 'docker volume --help'...` in the example below). This helps
separating the error message ("unkown flag") from the call-to-action.
Before this patch:
Unknown top-level command:
docker nosuchcommand foo
docker: 'nosuchcommand' is not a docker command.
See 'docker --help'
Unknown sub-command:
docker volume nosuchcommand foo
Usage: docker volume COMMAND
Manage volumes
Commands:
create Create a volume
inspect Display detailed information on one or more volumes
ls List volumes
prune Remove unused local volumes
rm Remove one or more volumes
update Update a volume (cluster volumes only)
Run 'docker volume COMMAND --help' for more information on a command.
After this patch:
Unknown top-level command:
docker nosuchcommand foo
docker: unknown command: docker nosuchcommand
Run 'docker --help' for more information
Unknown sub-command:
docker volume nosuchcommand foo
docker: unknown command: 'docker volume nosuchcommand'
Usage: docker volume COMMAND
Run 'docker volume --help' for more information
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-04 18:57:07 -04:00
|
|
|
return fmt.Errorf("docker: unknown command: docker %s\n\nRun 'docker --help' for more information", cmd.Name())
|
2023-03-27 22:08:07 -04:00
|
|
|
},
|
|
|
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
|
|
// Delegate completion to plugin
|
|
|
|
cargs := []string{p.Path, cobra.ShellCompRequestCmd, p.Name}
|
|
|
|
cargs = append(cargs, args...)
|
|
|
|
cargs = append(cargs, toComplete)
|
|
|
|
os.Args = cargs
|
|
|
|
runCommand, runErr := PluginRunCommand(dockerCli, p.Name, cmd)
|
|
|
|
if runErr != nil {
|
|
|
|
return nil, cobra.ShellCompDirectiveError
|
|
|
|
}
|
|
|
|
runErr = runCommand.Run()
|
|
|
|
if runErr == nil {
|
|
|
|
os.Exit(0) // plugin already rendered complete data
|
|
|
|
}
|
2022-03-30 09:27:25 -04:00
|
|
|
return nil, cobra.ShellCompDirectiveError
|
2023-03-27 22:08:07 -04:00
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return err
|
2018-12-11 09:50:04 -05:00
|
|
|
}
|
2024-02-13 16:40:55 -05:00
|
|
|
|
|
|
|
const (
|
|
|
|
dockerCliAttributePrefix = attribute.Key("docker.cli")
|
|
|
|
|
|
|
|
cobraCommandPath = attribute.Key("cobra.command_path")
|
|
|
|
)
|
|
|
|
|
|
|
|
func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Set {
|
|
|
|
commandPath := cmd.Annotations[CommandAnnotationPluginCommandPath]
|
|
|
|
if commandPath == "" {
|
|
|
|
commandPath = fmt.Sprintf("%s %s", cmd.CommandPath(), plugin.Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
attrSet := attribute.NewSet(
|
|
|
|
cobraCommandPath.String(commandPath),
|
|
|
|
)
|
|
|
|
|
|
|
|
kvs := make([]attribute.KeyValue, 0, attrSet.Len())
|
|
|
|
for iter := attrSet.Iter(); iter.Next(); {
|
|
|
|
attr := iter.Attribute()
|
|
|
|
kvs = append(kvs, attribute.KeyValue{
|
|
|
|
Key: dockerCliAttributePrefix + "." + attr.Key,
|
|
|
|
Value: attr.Value,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return attribute.NewSet(kvs...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func appendPluginResourceAttributesEnvvar(env []string, cmd *cobra.Command, plugin Plugin) []string {
|
|
|
|
if attrs := getPluginResourceAttributes(cmd, plugin); attrs.Len() > 0 {
|
2024-02-28 15:36:45 -05:00
|
|
|
// values in environment variables need to be in baggage format
|
|
|
|
// otel/baggage package can be used after update to v1.22, currently it encodes incorrectly
|
|
|
|
attrsSlice := make([]string, attrs.Len())
|
|
|
|
for iter := attrs.Iter(); iter.Next(); {
|
|
|
|
i, v := iter.IndexedAttribute()
|
|
|
|
attrsSlice[i] = string(v.Key) + "=" + url.PathEscape(v.Value.AsString())
|
|
|
|
}
|
|
|
|
env = append(env, ResourceAttributesEnvvar+"="+strings.Join(attrsSlice, ","))
|
2024-02-13 16:40:55 -05:00
|
|
|
}
|
|
|
|
return env
|
|
|
|
}
|