diff --git a/cmd/docker/daemon_none.go b/cmd/docker/daemon_none.go new file mode 100644 index 00000000..6fbd0001 --- /dev/null +++ b/cmd/docker/daemon_none.go @@ -0,0 +1,29 @@ +// +build !daemon + +package main + +import ( + "fmt" + "runtime" + "strings" + + "github.com/spf13/cobra" +) + +func newDaemonCommand() *cobra.Command { + return &cobra.Command{ + Use: "daemon", + Hidden: true, + Args: cobra.ArbitraryArgs, + DisableFlagParsing: true, + RunE: func(cmd *cobra.Command, args []string) error { + return runDaemon() + }, + } +} + +func runDaemon() error { + return fmt.Errorf( + "`docker daemon` is not supported on %s. Please run `dockerd` directly", + strings.Title(runtime.GOOS)) +} diff --git a/cmd/docker/daemon_none_test.go b/cmd/docker/daemon_none_test.go new file mode 100644 index 00000000..af0fcfd6 --- /dev/null +++ b/cmd/docker/daemon_none_test.go @@ -0,0 +1,17 @@ +// +build !daemon + +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDaemonCommand(t *testing.T) { + cmd := newDaemonCommand() + cmd.SetArgs([]string{"--version"}) + err := cmd.Execute() + + assert.EqualError(t, err, "Please run `dockerd`") +} diff --git a/cmd/docker/daemon_unit_test.go b/cmd/docker/daemon_unit_test.go new file mode 100644 index 00000000..ffd8a5e2 --- /dev/null +++ b/cmd/docker/daemon_unit_test.go @@ -0,0 +1,30 @@ +// +build daemon + +package main + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func stubRun(cmd *cobra.Command, args []string) error { + return nil +} + +func TestDaemonCommandHelp(t *testing.T) { + cmd := newDaemonCommand() + cmd.RunE = stubRun + cmd.SetArgs([]string{"--help"}) + err := cmd.Execute() + assert.NoError(t, err) +} + +func TestDaemonCommand(t *testing.T) { + cmd := newDaemonCommand() + cmd.RunE = stubRun + cmd.SetArgs([]string{"--containerd", "/foo"}) + err := cmd.Execute() + assert.NoError(t, err) +} diff --git a/cmd/docker/daemon_unix.go b/cmd/docker/daemon_unix.go new file mode 100644 index 00000000..6ec6b625 --- /dev/null +++ b/cmd/docker/daemon_unix.go @@ -0,0 +1,79 @@ +// +build daemon + +package main + +import ( + "fmt" + + "os" + "os/exec" + "path/filepath" + "syscall" + + "github.com/spf13/cobra" +) + +const daemonBinary = "dockerd" + +func newDaemonCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "daemon", + Hidden: true, + Args: cobra.ArbitraryArgs, + DisableFlagParsing: true, + RunE: func(cmd *cobra.Command, args []string) error { + return runDaemon() + }, + Deprecated: "and will be removed in Docker 17.12. Please run `dockerd` directly.", + } + cmd.SetHelpFunc(helpFunc) + return cmd +} + +// CmdDaemon execs dockerd with the same flags +func runDaemon() error { + // Use os.Args[1:] so that "global" args are passed to dockerd + return execDaemon(stripDaemonArg(os.Args[1:])) +} + +func execDaemon(args []string) error { + binaryPath, err := findDaemonBinary() + if err != nil { + return err + } + + return syscall.Exec( + binaryPath, + append([]string{daemonBinary}, args...), + os.Environ()) +} + +func helpFunc(cmd *cobra.Command, args []string) { + if err := execDaemon([]string{"--help"}); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + } +} + +// findDaemonBinary looks for the path to the dockerd binary starting with +// the directory of the current executable (if one exists) and followed by $PATH +func findDaemonBinary() (string, error) { + execDirname := filepath.Dir(os.Args[0]) + if execDirname != "" { + binaryPath := filepath.Join(execDirname, daemonBinary) + if _, err := os.Stat(binaryPath); err == nil { + return binaryPath, nil + } + } + + return exec.LookPath(daemonBinary) +} + +// stripDaemonArg removes the `daemon` argument from the list +func stripDaemonArg(args []string) []string { + for i, arg := range args { + if arg == "daemon" { + return append(args[:i], args[i+1:]...) + } + } + return args +} diff --git a/cmd/docker/docker.go b/cmd/docker/docker.go new file mode 100644 index 00000000..96283deb --- /dev/null +++ b/cmd/docker/docker.go @@ -0,0 +1,310 @@ +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/client" + "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) + }, + } + 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); 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); err != nil { + ccmd.Println(err) + return + } + + hideUnsupportedFeatures(ccmd, dockerCli) + + 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); 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() + } +} + +type versionDetails interface { + Client() client.APIClient + ServerInfo() command.ServerInfo +} + +func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) { + clientVersion := details.Client().ClientVersion() + osType := details.ServerInfo().OSType + hasExperimental := details.ServerInfo().HasExperimental + + 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, details versionDetails) error { + clientVersion := details.Client().ClientVersion() + osType := details.ServerInfo().OSType + hasExperimental := details.ServerInfo().HasExperimental + + // 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 +} diff --git a/cmd/docker/docker_test.go b/cmd/docker/docker_test.go new file mode 100644 index 00000000..88afb41e --- /dev/null +++ b/cmd/docker/docker_test.go @@ -0,0 +1,32 @@ +package main + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/debug" + "github.com/stretchr/testify/assert" +) + +func TestClientDebugEnabled(t *testing.T) { + defer debug.Disable() + + cmd := newDockerCommand(&command.DockerCli{}) + cmd.Flags().Set("debug", "true") + + err := cmd.PersistentPreRunE(cmd, []string{}) + assert.NoError(t, err) + assert.Equal(t, "1", os.Getenv("DEBUG")) + assert.Equal(t, logrus.DebugLevel, logrus.GetLevel()) +} + +func TestExitStatusForInvalidSubcommandWithHelpFlag(t *testing.T) { + discard := ioutil.Discard + cmd := newDockerCommand(command.NewDockerCli(os.Stdin, discard, discard)) + cmd.SetArgs([]string{"help", "invalid"}) + err := cmd.Execute() + assert.EqualError(t, err, "unknown help topic: invalid") +} diff --git a/cmd/docker/docker_windows.go b/cmd/docker/docker_windows.go new file mode 100644 index 00000000..9bc507e2 --- /dev/null +++ b/cmd/docker/docker_windows.go @@ -0,0 +1,18 @@ +package main + +import ( + "sync/atomic" + + _ "github.com/docker/docker/autogen/winresources/docker" +) + +//go:cgo_import_dynamic main.dummy CommandLineToArgvW%2 "shell32.dll" + +var dummy uintptr + +func init() { + // Ensure that this import is not removed by the linker. This is used to + // ensure that shell32.dll is loaded by the system loader, preventing + // go#15286 from triggering on Nano Server TP5. + atomic.LoadUintptr(&dummy) +}