The current error-handling only checked for version annotations
on the subcommand itself, but did not check the top-level command.
This patch always traverses the command path (parents), and
prints an error if the command is not supported.
Before this change:
$ docker service
Usage: docker service COMMAND
Manage services
Options:
--help Print usage
Commands:
create Create a new service
inspect Display detailed information on one or more services
ls List services
ps List the tasks of one or more services
rm Remove one or more services
scale Scale one or multiple replicated services
update Update a service
Run 'docker service COMMAND --help' for more information on a command.
$ docker service ls
ID NAME MODE REPLICAS IMAGE
After this change:
$ DOCKER_API_VERSION=1.12 docker service
docker service requires API version 1.24, but the Docker daemon API version is 1.12
$ DOCKER_API_VERSION=1.12 docker service ls
docker service ls requires API version 1.24, but the Docker daemon API version is 1.12
$ DOCKER_API_VERSION=1.24 docker plugin --help
docker plugin requires API version 1.25, but the Docker daemon API version is 1.24
$ DOCKER_API_VERSION=1.25 docker plugin upgrade --help
docker plugin upgrade requires API version 1.26, but the Docker daemon API version is 1.25
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Upstream-commit: 15d63f1c5ba40a0937a935c866c384c953c79b16
Component: engine
297 lines
9.1 KiB
Go
297 lines
9.1 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
"github.com/docker/docker/api/types/versions"
|
|
"github.com/docker/docker/cli"
|
|
"github.com/docker/docker/cli/command"
|
|
"github.com/docker/docker/cli/command/commands"
|
|
cliconfig "github.com/docker/docker/cli/config"
|
|
"github.com/docker/docker/cli/debug"
|
|
cliflags "github.com/docker/docker/cli/flags"
|
|
"github.com/docker/docker/dockerversion"
|
|
"github.com/docker/docker/pkg/term"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/pflag"
|
|
)
|
|
|
|
func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
|
|
opts := cliflags.NewClientOptions()
|
|
var flags *pflag.FlagSet
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "docker [OPTIONS] COMMAND [ARG...]",
|
|
Short: "A self-sufficient runtime for containers",
|
|
SilenceUsage: true,
|
|
SilenceErrors: true,
|
|
TraverseChildren: true,
|
|
Args: noArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if opts.Version {
|
|
showVersion()
|
|
return nil
|
|
}
|
|
return dockerCli.ShowHelp(cmd, args)
|
|
},
|
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
|
// daemon command is special, we redirect directly to another binary
|
|
if cmd.Name() == "daemon" {
|
|
return nil
|
|
}
|
|
// flags must be the top-level command flags, not cmd.Flags()
|
|
opts.Common.SetDefaultOptions(flags)
|
|
dockerPreRun(opts)
|
|
if err := dockerCli.Initialize(opts); err != nil {
|
|
return err
|
|
}
|
|
return isSupported(cmd, dockerCli.Client().ClientVersion(), dockerCli.OSType(), dockerCli.HasExperimental())
|
|
},
|
|
}
|
|
cli.SetupRootCommand(cmd)
|
|
|
|
flags = cmd.Flags()
|
|
flags.BoolVarP(&opts.Version, "version", "v", false, "Print version information and quit")
|
|
flags.StringVar(&opts.ConfigDir, "config", cliconfig.Dir(), "Location of client config files")
|
|
opts.Common.InstallFlags(flags)
|
|
|
|
setFlagErrorFunc(dockerCli, cmd, flags, opts)
|
|
|
|
setHelpFunc(dockerCli, cmd, flags, opts)
|
|
|
|
cmd.SetOutput(dockerCli.Out())
|
|
cmd.AddCommand(newDaemonCommand())
|
|
commands.AddCommands(cmd, dockerCli)
|
|
|
|
setValidateArgs(dockerCli, cmd, flags, opts)
|
|
|
|
return cmd
|
|
}
|
|
|
|
func setFlagErrorFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) {
|
|
// When invoking `docker stack --nonsense`, we need to make sure FlagErrorFunc return appropriate
|
|
// output if the feature is not supported.
|
|
// As above cli.SetupRootCommand(cmd) have already setup the FlagErrorFunc, we will add a pre-check before the FlagErrorFunc
|
|
// is called.
|
|
flagErrorFunc := cmd.FlagErrorFunc()
|
|
cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
|
|
initializeDockerCli(dockerCli, flags, opts)
|
|
if err := isSupported(cmd, dockerCli.Client().ClientVersion(), dockerCli.OSType(), dockerCli.HasExperimental()); err != nil {
|
|
return err
|
|
}
|
|
return flagErrorFunc(cmd, err)
|
|
})
|
|
}
|
|
|
|
func setHelpFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) {
|
|
cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) {
|
|
initializeDockerCli(dockerCli, flags, opts)
|
|
if err := isSupported(ccmd, dockerCli.Client().ClientVersion(), dockerCli.OSType(), dockerCli.HasExperimental()); err != nil {
|
|
ccmd.Println(err)
|
|
return
|
|
}
|
|
|
|
hideUnsupportedFeatures(ccmd, dockerCli.Client().ClientVersion(), dockerCli.OSType(), dockerCli.HasExperimental())
|
|
|
|
if err := ccmd.Help(); err != nil {
|
|
ccmd.Println(err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func setValidateArgs(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) {
|
|
// The Args is handled by ValidateArgs in cobra, which does not allows a pre-hook.
|
|
// As a result, here we replace the existing Args validation func to a wrapper,
|
|
// where the wrapper will check to see if the feature is supported or not.
|
|
// The Args validation error will only be returned if the feature is supported.
|
|
visitAll(cmd, func(ccmd *cobra.Command) {
|
|
// if there is no tags for a command or any of its parent,
|
|
// there is no need to wrap the Args validation.
|
|
if !hasTags(ccmd) {
|
|
return
|
|
}
|
|
|
|
if ccmd.Args == nil {
|
|
return
|
|
}
|
|
|
|
cmdArgs := ccmd.Args
|
|
ccmd.Args = func(cmd *cobra.Command, args []string) error {
|
|
initializeDockerCli(dockerCli, flags, opts)
|
|
if err := isSupported(cmd, dockerCli.Client().ClientVersion(), dockerCli.OSType(), dockerCli.HasExperimental()); err != nil {
|
|
return err
|
|
}
|
|
return cmdArgs(cmd, args)
|
|
}
|
|
})
|
|
}
|
|
|
|
func initializeDockerCli(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *cliflags.ClientOptions) {
|
|
if dockerCli.Client() == nil { // when using --help, PersistentPreRun is not called, so initialization is needed.
|
|
// flags must be the top-level command flags, not cmd.Flags()
|
|
opts.Common.SetDefaultOptions(flags)
|
|
dockerPreRun(opts)
|
|
dockerCli.Initialize(opts)
|
|
}
|
|
}
|
|
|
|
// visitAll will traverse all commands from the root.
|
|
// This is different from the VisitAll of cobra.Command where only parents
|
|
// are checked.
|
|
func visitAll(root *cobra.Command, fn func(*cobra.Command)) {
|
|
for _, cmd := range root.Commands() {
|
|
visitAll(cmd, fn)
|
|
}
|
|
fn(root)
|
|
}
|
|
|
|
func noArgs(cmd *cobra.Command, args []string) error {
|
|
if len(args) == 0 {
|
|
return nil
|
|
}
|
|
return fmt.Errorf(
|
|
"docker: '%s' is not a docker command.\nSee 'docker --help'", args[0])
|
|
}
|
|
|
|
func main() {
|
|
// Set terminal emulation based on platform as required.
|
|
stdin, stdout, stderr := term.StdStreams()
|
|
logrus.SetOutput(stderr)
|
|
|
|
dockerCli := command.NewDockerCli(stdin, stdout, stderr)
|
|
cmd := newDockerCommand(dockerCli)
|
|
|
|
if err := cmd.Execute(); err != nil {
|
|
if sterr, ok := err.(cli.StatusError); ok {
|
|
if sterr.Status != "" {
|
|
fmt.Fprintln(stderr, sterr.Status)
|
|
}
|
|
// StatusError should only be used for errors, and all errors should
|
|
// have a non-zero exit status, so never exit with 0
|
|
if sterr.StatusCode == 0 {
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(sterr.StatusCode)
|
|
}
|
|
fmt.Fprintln(stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func showVersion() {
|
|
fmt.Printf("Docker version %s, build %s\n", dockerversion.Version, dockerversion.GitCommit)
|
|
}
|
|
|
|
func dockerPreRun(opts *cliflags.ClientOptions) {
|
|
cliflags.SetLogLevel(opts.Common.LogLevel)
|
|
|
|
if opts.ConfigDir != "" {
|
|
cliconfig.SetDir(opts.ConfigDir)
|
|
}
|
|
|
|
if opts.Common.Debug {
|
|
debug.Enable()
|
|
}
|
|
}
|
|
|
|
func hideUnsupportedFeatures(cmd *cobra.Command, clientVersion, osType string, hasExperimental bool) {
|
|
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
|
// hide experimental flags
|
|
if !hasExperimental {
|
|
if _, ok := f.Annotations["experimental"]; ok {
|
|
f.Hidden = true
|
|
}
|
|
}
|
|
|
|
// hide flags not supported by the server
|
|
if !isOSTypeSupported(f, osType) || !isVersionSupported(f, clientVersion) {
|
|
f.Hidden = true
|
|
}
|
|
})
|
|
|
|
for _, subcmd := range cmd.Commands() {
|
|
// hide experimental subcommands
|
|
if !hasExperimental {
|
|
if _, ok := subcmd.Tags["experimental"]; ok {
|
|
subcmd.Hidden = true
|
|
}
|
|
}
|
|
|
|
// hide subcommands not supported by the server
|
|
if subcmdVersion, ok := subcmd.Tags["version"]; ok && versions.LessThan(clientVersion, subcmdVersion) {
|
|
subcmd.Hidden = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func isSupported(cmd *cobra.Command, clientVersion, osType string, hasExperimental bool) error {
|
|
// Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack`
|
|
for curr := cmd; curr != nil; curr = curr.Parent() {
|
|
if cmdVersion, ok := curr.Tags["version"]; ok && versions.LessThan(clientVersion, cmdVersion) {
|
|
return fmt.Errorf("%s requires API version %s, but the Docker daemon API version is %s", cmd.CommandPath(), cmdVersion, clientVersion)
|
|
}
|
|
if _, ok := curr.Tags["experimental"]; ok && !hasExperimental {
|
|
return fmt.Errorf("%s is only supported on a Docker daemon with experimental features enabled", cmd.CommandPath())
|
|
}
|
|
}
|
|
|
|
errs := []string{}
|
|
|
|
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
|
if f.Changed {
|
|
if !isVersionSupported(f, clientVersion) {
|
|
errs = append(errs, fmt.Sprintf("\"--%s\" requires API version %s, but the Docker daemon API version is %s", f.Name, getFlagAnnotation(f, "version"), clientVersion))
|
|
return
|
|
}
|
|
if !isOSTypeSupported(f, osType) {
|
|
errs = append(errs, fmt.Sprintf("\"--%s\" requires the Docker daemon to run on %s, but the Docker daemon is running on %s", f.Name, getFlagAnnotation(f, "ostype"), osType))
|
|
return
|
|
}
|
|
if _, ok := f.Annotations["experimental"]; ok && !hasExperimental {
|
|
errs = append(errs, fmt.Sprintf("\"--%s\" is only supported on a Docker daemon with experimental features enabled", f.Name))
|
|
}
|
|
}
|
|
})
|
|
if len(errs) > 0 {
|
|
return errors.New(strings.Join(errs, "\n"))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getFlagAnnotation(f *pflag.Flag, annotation string) string {
|
|
if value, ok := f.Annotations[annotation]; ok && len(value) == 1 {
|
|
return value[0]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func isVersionSupported(f *pflag.Flag, clientVersion string) bool {
|
|
if v := getFlagAnnotation(f, "version"); v != "" {
|
|
return versions.GreaterThanOrEqualTo(clientVersion, v)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isOSTypeSupported(f *pflag.Flag, osType string) bool {
|
|
if v := getFlagAnnotation(f, "ostype"); v != "" && osType != "" {
|
|
return osType == v
|
|
}
|
|
return true
|
|
}
|
|
|
|
// hasTags return true if any of the command's parents has tags
|
|
func hasTags(cmd *cobra.Command) bool {
|
|
for curr := cmd; curr != nil; curr = curr.Parent() {
|
|
if len(curr.Tags) > 0 {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|