From e98e4a71110fd33852bb755a9b8b4ebc9df904db Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 2 Nov 2016 17:43:32 -0700 Subject: [PATCH] always add but hide experimental cmds and flags Signed-off-by: Victor Vieux update cobra and use Tags Signed-off-by: Victor Vieux allow client to talk to an older server Signed-off-by: Victor Vieux --- api/server/middleware/user_agent.go | 47 ------------------- api/server/middleware/version.go | 4 +- api/server/middleware/version_test.go | 6 --- api/server/server.go | 2 +- api/types/types.go | 7 +++ cli/command/checkpoint/cmd.go | 7 ++- cli/command/cli.go | 44 ++++++++++++----- cli/command/container/cmd.go | 5 +- cli/command/container/exec.go | 1 + cli/command/container/prune.go | 1 + cli/command/image/build.go | 3 +- cli/command/image/cmd.go | 6 +-- cli/command/image/prune.go | 1 + cli/command/network/cmd.go | 5 +- cli/command/network/prune.go | 1 + cli/command/node/cmd.go | 5 +- cli/command/plugin/cmd.go | 5 +- cli/command/service/cmd.go | 5 +- cli/command/stack/cmd.go | 7 ++- cli/command/stack/deploy.go | 2 +- cli/command/swarm/cmd.go | 5 +- cli/command/system/cmd.go | 6 +-- cli/command/system/df.go | 1 + cli/command/system/prune.go | 1 + cli/command/system/version.go | 8 +++- cli/command/volume/cmd.go | 5 +- cli/command/volume/prune.go | 1 + cli/command/volume/remove.go | 2 +- client/client.go | 17 +++++-- client/container_create.go | 5 ++ client/container_exec.go | 5 ++ client/container_prune.go | 4 ++ client/errors.go | 11 +++++ client/image_build.go | 7 ++- client/image_prune.go | 4 ++ client/interface.go | 2 +- client/ping.go | 29 ++++++++---- client/request.go | 3 ++ client/volume_prune.go | 4 ++ client/volume_remove.go | 7 ++- cmd/docker/docker.go | 27 +++++++---- cmd/dockerd/daemon.go | 3 -- docs/reference/api/docker_remote_api.md | 1 + docs/reference/api/docker_remote_api_v1.18.md | 4 +- docs/reference/api/docker_remote_api_v1.20.md | 4 +- docs/reference/api/docker_remote_api_v1.22.md | 4 +- docs/reference/api/docker_remote_api_v1.24.md | 2 - docs/reference/api/docker_remote_api_v1.25.md | 2 - integration-cli/docker_api_test.go | 33 +------------ integration-cli/docker_cli_config_test.go | 2 + runconfig/opts/parse.go | 1 + 51 files changed, 192 insertions(+), 182 deletions(-) delete mode 100644 api/server/middleware/user_agent.go diff --git a/api/server/middleware/user_agent.go b/api/server/middleware/user_agent.go deleted file mode 100644 index be1c3fb4c4..0000000000 --- a/api/server/middleware/user_agent.go +++ /dev/null @@ -1,47 +0,0 @@ -package middleware - -import ( - "net/http" - "strings" - - "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/server/httputils" - "github.com/docker/docker/api/types/versions" - "golang.org/x/net/context" -) - -// UserAgentMiddleware is a middleware that -// validates the client user-agent. -type UserAgentMiddleware struct { - serverVersion string -} - -// NewUserAgentMiddleware creates a new UserAgentMiddleware -// with the server version. -func NewUserAgentMiddleware(s string) UserAgentMiddleware { - return UserAgentMiddleware{ - serverVersion: s, - } -} - -// WrapHandler returns a new handler function wrapping the previous one in the request chain. -func (u UserAgentMiddleware) WrapHandler(handler func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error) func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - ctx = context.WithValue(ctx, httputils.UAStringKey, r.Header.Get("User-Agent")) - - if strings.Contains(r.Header.Get("User-Agent"), "Docker-Client/") { - userAgent := strings.Split(r.Header.Get("User-Agent"), "/") - - // v1.20 onwards includes the GOOS of the client after the version - // such as Docker/1.7.0 (linux) - if len(userAgent) == 2 && strings.Contains(userAgent[1], " ") { - userAgent[1] = strings.Split(userAgent[1], " ")[0] - } - - if len(userAgent) == 2 && !versions.Equal(u.serverVersion, userAgent[1]) { - logrus.Debugf("Client and server don't have the same version (client: %s, server: %s)", userAgent[1], u.serverVersion) - } - } - return handler(ctx, w, r, vars) - } -} diff --git a/api/server/middleware/version.go b/api/server/middleware/version.go index b6f62b212b..11014659e5 100644 --- a/api/server/middleware/version.go +++ b/api/server/middleware/version.go @@ -36,15 +36,13 @@ func (v VersionMiddleware) WrapHandler(handler func(ctx context.Context, w http. apiVersion = v.defaultVersion } - if versions.GreaterThan(apiVersion, v.defaultVersion) { - return errors.NewBadRequestError(fmt.Errorf("client is newer than server (client API version: %s, server API version: %s)", apiVersion, v.defaultVersion)) - } if versions.LessThan(apiVersion, v.minVersion) { return errors.NewBadRequestError(fmt.Errorf("client version %s is too old. Minimum supported API version is %s, please upgrade your client to a newer version", apiVersion, v.minVersion)) } header := fmt.Sprintf("Docker/%s (%s)", v.serverVersion, runtime.GOOS) w.Header().Set("Server", header) + w.Header().Set("API-Version", v.defaultVersion) ctx = context.WithValue(ctx, "api-version", apiVersion) return handler(ctx, w, r, vars) } diff --git a/api/server/middleware/version_test.go b/api/server/middleware/version_test.go index 90dee7138c..9e72efd78d 100644 --- a/api/server/middleware/version_test.go +++ b/api/server/middleware/version_test.go @@ -54,10 +54,4 @@ func TestVersionMiddlewareWithErrors(t *testing.T) { if !strings.Contains(err.Error(), "client version 0.1 is too old. Minimum supported API version is 1.2.0") { t.Fatalf("Expected too old client error, got %v", err) } - - vars["version"] = "100000" - err = h(ctx, resp, req, vars) - if !strings.Contains(err.Error(), "client is newer than server") { - t.Fatalf("Expected client newer than server error, got %v", err) - } } diff --git a/api/server/server.go b/api/server/server.go index ccf6748d98..5d6a5d8682 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -128,7 +128,7 @@ func (s *Server) makeHTTPHandler(handler httputils.APIFunc) http.HandlerFunc { // apply to all requests. Data that is specific to the // immediate function being called should still be passed // as 'args' on the function call. - ctx := context.Background() + ctx := context.WithValue(context.Background(), httputils.UAStringKey, r.Header.Get("User-Agent")) handlerFunc := s.handlerWithGlobalMiddlewares(handler) vars := mux.Vars(r) diff --git a/api/types/types.go b/api/types/types.go index e5f83ec717..5591646b69 100644 --- a/api/types/types.go +++ b/api/types/types.go @@ -128,6 +128,13 @@ type ContainerProcessList struct { Titles []string } +// Info contains response of Remote API: +// GET "/_ping" +type Ping struct { + APIVersion string + Experimental bool +} + // Version contains response of Remote API: // GET "/version" type Version struct { diff --git a/cli/command/checkpoint/cmd.go b/cli/command/checkpoint/cmd.go index 7f9e537779..f186232a4d 100644 --- a/cli/command/checkpoint/cmd.go +++ b/cli/command/checkpoint/cmd.go @@ -1,8 +1,6 @@ package checkpoint import ( - "fmt" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" @@ -15,9 +13,10 @@ func NewCheckpointCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage checkpoints", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, - Tags: map[string]string{"experimental": ""}, + Tags: map[string]string{"experimental": "", "version": "1.25"}, } cmd.AddCommand( newCreateCommand(dockerCli), diff --git a/cli/command/cli.go b/cli/command/cli.go index 33a26c4c64..ef9de2edf1 100644 --- a/cli/command/cli.go +++ b/cli/command/cli.go @@ -10,6 +10,7 @@ import ( "runtime" "github.com/docker/docker/api" + "github.com/docker/docker/api/types/versions" cliflags "github.com/docker/docker/cli/flags" "github.com/docker/docker/cliconfig" "github.com/docker/docker/cliconfig/configfile" @@ -32,21 +33,24 @@ type Streams interface { // DockerCli represents the docker command line client. // Instances of the client can be returned from NewDockerCli. type DockerCli struct { - configFile *configfile.ConfigFile - in *InStream - out *OutStream - err io.Writer - keyFile string - client client.APIClient + configFile *configfile.ConfigFile + in *InStream + out *OutStream + err io.Writer + keyFile string + client client.APIClient + hasExperimental bool + defaultVersion string } -// HasExperimental returns true if experimental features are accessible +// HasExperimental returns true if experimental features are accessible. func (cli *DockerCli) HasExperimental() bool { - if cli.client == nil { - return false - } - enabled, _ := cli.client.Ping(context.Background()) - return enabled + return cli.hasExperimental +} + +// DefaultVersion returns api.defaultVersion of DOCKER_API_VERSION if specified. +func (cli *DockerCli) DefaultVersion() string { + return cli.defaultVersion } // Client returns the APIClient @@ -93,12 +97,28 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { if err != nil { return err } + + cli.defaultVersion = cli.client.ClientVersion() + if opts.Common.TrustKey == "" { cli.keyFile = filepath.Join(cliconfig.ConfigDir(), cliflags.DefaultTrustKeyFile) } else { cli.keyFile = opts.Common.TrustKey } + if ping, err := cli.client.Ping(context.Background()); err == nil { + cli.hasExperimental = ping.Experimental + + // since the new header was added in 1.25, assume server is 1.24 if header is not present. + if ping.APIVersion == "" { + ping.APIVersion = "1.24" + } + + // if server version is lower than the current cli, downgrade + if versions.LessThan(ping.APIVersion, cli.client.ClientVersion()) { + cli.client.UpdateClientVersion(ping.APIVersion) + } + } return nil } diff --git a/cli/command/container/cmd.go b/cli/command/container/cmd.go index f06b863b58..075f936bd9 100644 --- a/cli/command/container/cmd.go +++ b/cli/command/container/cmd.go @@ -1,8 +1,6 @@ package container import ( - "fmt" - "github.com/spf13/cobra" "github.com/docker/docker/cli" @@ -16,7 +14,8 @@ func NewContainerCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage containers", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, } cmd.AddCommand( diff --git a/cli/command/container/exec.go b/cli/command/container/exec.go index 48964693b2..84eba113cf 100644 --- a/cli/command/container/exec.go +++ b/cli/command/container/exec.go @@ -59,6 +59,7 @@ func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringVarP(&opts.user, "user", "u", "", "Username or UID (format: [:])") flags.BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the command") flags.VarP(opts.env, "env", "e", "Set environment variables") + flags.SetAnnotation("env", "version", []string{"1.25"}) return cmd } diff --git a/cli/command/container/prune.go b/cli/command/container/prune.go index 99a97f6cd8..ec6b0e3147 100644 --- a/cli/command/container/prune.go +++ b/cli/command/container/prune.go @@ -35,6 +35,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) return nil }, + Tags: map[string]string{"version": "1.25"}, } flags := cmd.Flags() diff --git a/cli/command/image/build.go b/cli/command/image/build.go index 604888b6fa..ebec87d641 100644 --- a/cli/command/image/build.go +++ b/cli/command/image/build.go @@ -113,6 +113,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer") flags.SetAnnotation("squash", "experimental", nil) + flags.SetAnnotation("squash", "version", []string{"1.25"}) return cmd } @@ -144,7 +145,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { progBuff io.Writer buildBuff io.Writer ) - + specifiedContext := options.context progBuff = dockerCli.Out() buildBuff = dockerCli.Out() diff --git a/cli/command/image/cmd.go b/cli/command/image/cmd.go index 6f8e7b7d4b..dc98257438 100644 --- a/cli/command/image/cmd.go +++ b/cli/command/image/cmd.go @@ -1,8 +1,6 @@ package image import ( - "fmt" - "github.com/spf13/cobra" "github.com/docker/docker/cli" @@ -16,7 +14,8 @@ func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage images", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, } cmd.AddCommand( @@ -33,6 +32,5 @@ func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command { newInspectCommand(dockerCli), NewPruneCommand(dockerCli), ) - return cmd } diff --git a/cli/command/image/prune.go b/cli/command/image/prune.go index 46bd56cb10..ea84cda877 100644 --- a/cli/command/image/prune.go +++ b/cli/command/image/prune.go @@ -36,6 +36,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) return nil }, + Tags: map[string]string{"version": "1.25"}, } flags := cmd.Flags() diff --git a/cli/command/network/cmd.go b/cli/command/network/cmd.go index 77c8e4908e..c2a7e83dd8 100644 --- a/cli/command/network/cmd.go +++ b/cli/command/network/cmd.go @@ -1,8 +1,6 @@ package network import ( - "fmt" - "github.com/spf13/cobra" "github.com/docker/docker/cli" @@ -16,7 +14,8 @@ func NewNetworkCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage networks", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, } cmd.AddCommand( diff --git a/cli/command/network/prune.go b/cli/command/network/prune.go index 00e05d3bdf..f2f8cc20c4 100644 --- a/cli/command/network/prune.go +++ b/cli/command/network/prune.go @@ -33,6 +33,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { } return nil }, + Tags: map[string]string{"version": "1.25"}, } flags := cmd.Flags() diff --git a/cli/command/node/cmd.go b/cli/command/node/cmd.go index c7d0cf8181..d70ee81789 100644 --- a/cli/command/node/cmd.go +++ b/cli/command/node/cmd.go @@ -1,8 +1,6 @@ package node import ( - "fmt" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" apiclient "github.com/docker/docker/client" @@ -17,7 +15,8 @@ func NewNodeCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage Swarm nodes", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, } cmd.AddCommand( diff --git a/cli/command/plugin/cmd.go b/cli/command/plugin/cmd.go index c78f43a8d4..03d01c8882 100644 --- a/cli/command/plugin/cmd.go +++ b/cli/command/plugin/cmd.go @@ -1,8 +1,6 @@ package plugin import ( - "fmt" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" @@ -15,7 +13,8 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage plugins", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, Tags: map[string]string{"experimental": ""}, } diff --git a/cli/command/service/cmd.go b/cli/command/service/cmd.go index 9f342e1342..f4f7d00f91 100644 --- a/cli/command/service/cmd.go +++ b/cli/command/service/cmd.go @@ -1,8 +1,6 @@ package service import ( - "fmt" - "github.com/spf13/cobra" "github.com/docker/docker/cli" @@ -16,7 +14,8 @@ func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage services", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, } cmd.AddCommand( diff --git a/cli/command/stack/cmd.go b/cli/command/stack/cmd.go index 70afec9c6d..4189504403 100644 --- a/cli/command/stack/cmd.go +++ b/cli/command/stack/cmd.go @@ -1,8 +1,6 @@ package stack import ( - "fmt" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" @@ -15,9 +13,10 @@ func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage Docker stacks", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, - Tags: map[string]string{"experimental": ""}, + Tags: map[string]string{"experimental": "", "version": "1.25"}, } cmd.AddCommand( newConfigCommand(dockerCli), diff --git a/cli/command/stack/deploy.go b/cli/command/stack/deploy.go index b0f6b455a8..435a9193b4 100644 --- a/cli/command/stack/deploy.go +++ b/cli/command/stack/deploy.go @@ -36,7 +36,7 @@ func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { opts.namespace = strings.TrimSuffix(args[0], ".dab") return runDeploy(dockerCli, opts) }, - Tags: map[string]string{"experimental": ""}, + Tags: map[string]string{"experimental": "", "version": "1.25"}, } flags := cmd.Flags() diff --git a/cli/command/swarm/cmd.go b/cli/command/swarm/cmd.go index 9f9df53950..f0a6bcdeb8 100644 --- a/cli/command/swarm/cmd.go +++ b/cli/command/swarm/cmd.go @@ -1,8 +1,6 @@ package swarm import ( - "fmt" - "github.com/spf13/cobra" "github.com/docker/docker/cli" @@ -16,7 +14,8 @@ func NewSwarmCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage Swarm", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, } cmd.AddCommand( diff --git a/cli/command/system/cmd.go b/cli/command/system/cmd.go index 46caa2491c..9cd74b5d4b 100644 --- a/cli/command/system/cmd.go +++ b/cli/command/system/cmd.go @@ -1,8 +1,6 @@ package system import ( - "fmt" - "github.com/spf13/cobra" "github.com/docker/docker/cli" @@ -16,7 +14,8 @@ func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage Docker", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, } cmd.AddCommand( @@ -25,5 +24,6 @@ func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command { NewDiskUsageCommand(dockerCli), NewPruneCommand(dockerCli), ) + return cmd } diff --git a/cli/command/system/df.go b/cli/command/system/df.go index 293946c188..9f712484aa 100644 --- a/cli/command/system/df.go +++ b/cli/command/system/df.go @@ -23,6 +23,7 @@ func NewDiskUsageCommand(dockerCli *command.DockerCli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runDiskUsage(dockerCli, opts) }, + Tags: map[string]string{"version": "1.25"}, } flags := cmd.Flags() diff --git a/cli/command/system/prune.go b/cli/command/system/prune.go index c79bc6910e..92dddbdca6 100644 --- a/cli/command/system/prune.go +++ b/cli/command/system/prune.go @@ -26,6 +26,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runPrune(dockerCli, opts) }, + Tags: map[string]string{"version": "1.25"}, } flags := cmd.Flags() diff --git a/cli/command/system/version.go b/cli/command/system/version.go index 6040c79361..00a84a3cbc 100644 --- a/cli/command/system/version.go +++ b/cli/command/system/version.go @@ -1,6 +1,7 @@ package system import ( + "fmt" "runtime" "time" @@ -70,10 +71,15 @@ func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error { Status: "Template parsing error: " + err.Error()} } + APIVersion := dockerCli.Client().ClientVersion() + if defaultAPIVersion := dockerCli.DefaultVersion(); APIVersion != defaultAPIVersion { + APIVersion = fmt.Sprintf("%s (downgraded from %s)", APIVersion, defaultAPIVersion) + } + vd := types.VersionResponse{ Client: &types.Version{ Version: dockerversion.Version, - APIVersion: dockerCli.Client().ClientVersion(), + APIVersion: APIVersion, GoVersion: runtime.Version(), GitCommit: dockerversion.GitCommit, BuildTime: dockerversion.BuildTime, diff --git a/cli/command/volume/cmd.go b/cli/command/volume/cmd.go index f35181ffaf..39e4b7f46e 100644 --- a/cli/command/volume/cmd.go +++ b/cli/command/volume/cmd.go @@ -1,8 +1,6 @@ package volume import ( - "fmt" - "github.com/spf13/cobra" "github.com/docker/docker/cli" @@ -17,7 +15,8 @@ func NewVolumeCommand(dockerCli *command.DockerCli) *cobra.Command { Long: volumeDescription, Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, } cmd.AddCommand( diff --git a/cli/command/volume/prune.go b/cli/command/volume/prune.go index a4bb0092d6..ac9c94451a 100644 --- a/cli/command/volume/prune.go +++ b/cli/command/volume/prune.go @@ -35,6 +35,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) return nil }, + Tags: map[string]string{"version": "1.25"}, } flags := cmd.Flags() diff --git a/cli/command/volume/remove.go b/cli/command/volume/remove.go index 213ad26ab5..f464bb3e1a 100644 --- a/cli/command/volume/remove.go +++ b/cli/command/volume/remove.go @@ -34,7 +34,7 @@ func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.force, "force", "f", false, "Force the removal of one or more volumes") - + flags.SetAnnotation("force", "version", []string{"1.25"}) return cmd } diff --git a/client/client.go b/client/client.go index 3b97720e00..76a1ac07c0 100644 --- a/client/client.go +++ b/client/client.go @@ -79,6 +79,8 @@ type Client struct { version string // custom http headers configured by users. customHTTPHeaders map[string]string + // manualOverride is set to true when the version was set by users. + manualOverride bool } // NewEnvClient initializes a new API client based on environment variables. @@ -111,13 +113,19 @@ func NewEnvClient() (*Client, error) { if host == "" { host = DefaultDockerHost } - version := os.Getenv("DOCKER_API_VERSION") if version == "" { version = DefaultVersion } - return NewClient(host, version, client, nil) + cli, err := NewClient(host, version, client, nil) + if err != nil { + return cli, err + } + if version != "" { + cli.manualOverride = true + } + return cli, nil } // NewClient initializes a new API client for the given host and API version. @@ -211,7 +219,10 @@ func (cli *Client) ClientVersion() string { // UpdateClientVersion updates the version string associated with this // instance of the Client. func (cli *Client) UpdateClientVersion(v string) { - cli.version = v + if !cli.manualOverride { + cli.version = v + } + } // ParseHost verifies that the given host strings is valid. diff --git a/client/container_create.go b/client/container_create.go index c042b17468..9f627aafa6 100644 --- a/client/container_create.go +++ b/client/container_create.go @@ -20,6 +20,11 @@ type configWrapper struct { // It can be associated with a name, but it's not mandatory. func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error) { var response container.ContainerCreateCreatedBody + + if err := cli.NewVersionError("1.25", "stop timeout"); config != nil && config.StopTimeout != nil && err != nil { + return response, err + } + query := url.Values{} if containerName != "" { query.Set("name", containerName) diff --git a/client/container_exec.go b/client/container_exec.go index f6df722918..0665c54fbd 100644 --- a/client/container_exec.go +++ b/client/container_exec.go @@ -10,6 +10,11 @@ import ( // ContainerExecCreate creates a new exec configuration to run an exec process. func (cli *Client) ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.IDResponse, error) { var response types.IDResponse + + if err := cli.NewVersionError("1.25", "env"); len(config.Env) != 0 && err != nil { + return response, err + } + resp, err := cli.post(ctx, "/containers/"+container+"/exec", nil, config, nil) if err != nil { return response, err diff --git a/client/container_prune.go b/client/container_prune.go index 0d8bd3292c..3eabe71a7f 100644 --- a/client/container_prune.go +++ b/client/container_prune.go @@ -12,6 +12,10 @@ import ( func (cli *Client) ContainersPrune(ctx context.Context, cfg types.ContainersPruneConfig) (types.ContainersPruneReport, error) { var report types.ContainersPruneReport + if err := cli.NewVersionError("1.25", "container prune"); err != nil { + return report, err + } + serverResp, err := cli.post(ctx, "/containers/prune", nil, cfg, nil) if err != nil { return report, err diff --git a/client/errors.go b/client/errors.go index ad1dadabb6..53e2065332 100644 --- a/client/errors.go +++ b/client/errors.go @@ -3,6 +3,8 @@ package client import ( "errors" "fmt" + + "github.com/docker/docker/api/types/versions" ) // ErrConnectionFailed is an error raised when the connection between the client and the server failed. @@ -206,3 +208,12 @@ func IsErrPluginPermissionDenied(err error) bool { _, ok := err.(pluginPermissionDenied) return ok } + +// NewVersionError returns an error if the APIVersion required +// if less than the current supported version +func (cli *Client) NewVersionError(APIrequired, feature string) error { + if versions.LessThan(cli.version, APIrequired) { + return fmt.Errorf("%q requires API version %s, but the Docker server is version %s", feature, APIrequired, cli.version) + } + return nil +} diff --git a/client/image_build.go b/client/image_build.go index 4d611d5430..0049e4e290 100644 --- a/client/image_build.go +++ b/client/image_build.go @@ -21,7 +21,7 @@ var headerRegexp = regexp.MustCompile(`\ADocker/.+\s\((.+)\)\z`) // The Body in the response implement an io.ReadCloser and it's up to the caller to // close it. func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { - query, err := imageBuildOptionsToQuery(options) + query, err := cli.imageBuildOptionsToQuery(options) if err != nil { return types.ImageBuildResponse{}, err } @@ -47,7 +47,7 @@ func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, optio }, nil } -func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) { +func (cli *Client) imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) { query := url.Values{ "t": options.Tags, "securityopt": options.SecurityOpt, @@ -76,6 +76,9 @@ func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, erro } if options.Squash { + if err := cli.NewVersionError("1.25", "squash"); err != nil { + return query, err + } query.Set("squash", "1") } diff --git a/client/image_prune.go b/client/image_prune.go index f6752e5043..d5e69d5b19 100644 --- a/client/image_prune.go +++ b/client/image_prune.go @@ -12,6 +12,10 @@ import ( func (cli *Client) ImagesPrune(ctx context.Context, cfg types.ImagesPruneConfig) (types.ImagesPruneReport, error) { var report types.ImagesPruneReport + if err := cli.NewVersionError("1.25", "image prune"); err != nil { + return report, err + } + serverResp, err := cli.post(ctx, "/images/prune", nil, cfg, nil) if err != nil { return report, err diff --git a/client/interface.go b/client/interface.go index a78cb759cd..99b06709b5 100644 --- a/client/interface.go +++ b/client/interface.go @@ -129,7 +129,7 @@ type SystemAPIClient interface { Info(ctx context.Context) (types.Info, error) RegistryLogin(ctx context.Context, auth types.AuthConfig) (registry.AuthenticateOKBody, error) DiskUsage(ctx context.Context) (types.DiskUsage, error) - Ping(ctx context.Context) (bool, error) + Ping(ctx context.Context) (types.Ping, error) } // VolumeAPIClient defines API client methods for the volumes diff --git a/client/ping.go b/client/ping.go index 5e99e1bba1..22dcda24fd 100644 --- a/client/ping.go +++ b/client/ping.go @@ -1,19 +1,30 @@ package client -import "golang.org/x/net/context" +import ( + "fmt" -// Ping pings the server and return the value of the "Docker-Experimental" header -func (cli *Client) Ping(ctx context.Context) (bool, error) { - serverResp, err := cli.get(ctx, "/_ping", nil, nil) + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// Ping pings the server and return the value of the "Docker-Experimental" & "API-Version" headers +func (cli *Client) Ping(ctx context.Context) (types.Ping, error) { + var ping types.Ping + req, err := cli.buildRequest("GET", fmt.Sprintf("%s/_ping", cli.basePath), nil, nil) if err != nil { - return false, err + return ping, err + } + serverResp, err := cli.doRequest(ctx, req) + if err != nil { + return ping, err } defer ensureReaderClosed(serverResp) - exp := serverResp.header.Get("Docker-Experimental") - if exp != "true" { - return false, nil + ping.APIVersion = serverResp.header.Get("API-Version") + + if serverResp.header.Get("Docker-Experimental") == "true" { + ping.Experimental = true } - return true, nil + return ping, nil } diff --git a/client/request.go b/client/request.go index c73464b54d..ac05363655 100644 --- a/client/request.go +++ b/client/request.go @@ -214,6 +214,9 @@ func (cli *Client) addHeaders(req *http.Request, headers headers) *http.Request // Add CLI Config's HTTP Headers BEFORE we set the Docker headers // then the user can't change OUR headers for k, v := range cli.customHTTPHeaders { + if versions.LessThan(cli.version, "1.25") && k == "User-Agent" { + continue + } req.Header.Set(k, v) } diff --git a/client/volume_prune.go b/client/volume_prune.go index e7ea7b591d..ea4e234a30 100644 --- a/client/volume_prune.go +++ b/client/volume_prune.go @@ -12,6 +12,10 @@ import ( func (cli *Client) VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error) { var report types.VolumesPruneReport + if err := cli.NewVersionError("1.25", "volume prune"); err != nil { + return report, err + } + serverResp, err := cli.post(ctx, "/volumes/prune", nil, cfg, nil) if err != nil { return report, err diff --git a/client/volume_remove.go b/client/volume_remove.go index 3d5aeff252..6c26575b49 100644 --- a/client/volume_remove.go +++ b/client/volume_remove.go @@ -3,14 +3,17 @@ package client import ( "net/url" + "github.com/docker/docker/api/types/versions" "golang.org/x/net/context" ) // VolumeRemove removes a volume from the docker host. func (cli *Client) VolumeRemove(ctx context.Context, volumeID string, force bool) error { query := url.Values{} - if force { - query.Set("force", "1") + if versions.GreaterThanOrEqualTo(cli.version, "1.25") { + if force { + query.Set("force", "1") + } } resp, err := cli.delete(ctx, "/volumes/"+volumeID, query, nil) ensureReaderClosed(resp) diff --git a/cmd/docker/docker.go b/cmd/docker/docker.go index 56c5a89895..18cd0e833c 100644 --- a/cmd/docker/docker.go +++ b/cmd/docker/docker.go @@ -5,6 +5,7 @@ import ( "os" "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" @@ -47,16 +48,15 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { cli.SetupRootCommand(cmd) cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) { - var err error if dockerCli.Client() == nil { // flags must be the top-level command flags, not cmd.Flags() opts.Common.SetDefaultOptions(flags) dockerPreRun(opts) - err = dockerCli.Initialize(opts) - } - if err != nil || !dockerCli.HasExperimental() { - hideExperimentalFeatures(ccmd) + dockerCli.Initialize(opts) } + + hideUnsupportedFeatures(ccmd, dockerCli.Client().ClientVersion(), dockerCli.HasExperimental()) + if err := ccmd.Help(); err != nil { ccmd.Println(err) } @@ -123,18 +123,29 @@ func dockerPreRun(opts *cliflags.ClientOptions) { } } -func hideExperimentalFeatures(cmd *cobra.Command) { - // hide flags +func hideUnsupportedFeatures(cmd *cobra.Command, clientVersion string, hasExperimental bool) { cmd.Flags().VisitAll(func(f *pflag.Flag) { + // hide experimental flags if _, ok := f.Annotations["experimental"]; ok { f.Hidden = true } + + // hide flags not supported by the server + if flagVersion, ok := f.Annotations["version"]; ok && len(flagVersion) == 1 && versions.LessThan(clientVersion, flagVersion[0]) { + f.Hidden = true + } + }) for _, subcmd := range cmd.Commands() { - // hide subcommands + // hide experimental subcommands 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 + } } } diff --git a/cmd/dockerd/daemon.go b/cmd/dockerd/daemon.go index a1ed8bab2a..38ad14b475 100644 --- a/cmd/dockerd/daemon.go +++ b/cmd/dockerd/daemon.go @@ -480,9 +480,6 @@ func (cli *DaemonCli) initMiddlewares(s *apiserver.Server, cfg *apiserver.Config s.UseMiddleware(c) } - u := middleware.NewUserAgentMiddleware(v) - s.UseMiddleware(u) - if err := validateAuthzPlugins(cli.Config.AuthorizationPlugins, cli.d.PluginStore); err != nil { return fmt.Errorf("Error validating authorization plugin: %v", err) } diff --git a/docs/reference/api/docker_remote_api.md b/docs/reference/api/docker_remote_api.md index 2edaaa7d26..3877f745bd 100644 --- a/docs/reference/api/docker_remote_api.md +++ b/docs/reference/api/docker_remote_api.md @@ -162,6 +162,7 @@ This section lists each version from latest to oldest. Each listing includes a * `POST /volumes/prune` prunes unused volumes. * `POST /networks/prune` prunes unused networks. * Every API response now includes a `Docker-Experimental` header specifying if experimental features are enabled (value can be `true` or `false`). +* Every API response now includes a `API-Version` header specifying the default API version of the server. * The `hostConfig` option now accepts the fields `CpuRealtimePeriod` and `CpuRtRuntime` to allocate cpu runtime to rt tasks when `CONFIG_RT_GROUP_SCHED` is enabled in the kernel. * The `SecurityOptions` field within the `GET /info` response now includes `userns` if user namespaces are enabled in the daemon. * `GET /nodes` and `GET /node/(id or name)` now return `Addr` as part of a node's `Status`, which is the address that that node connects to the manager from. diff --git a/docs/reference/api/docker_remote_api_v1.18.md b/docs/reference/api/docker_remote_api_v1.18.md index db84a8409a..8af4badee9 100644 --- a/docs/reference/api/docker_remote_api_v1.18.md +++ b/docs/reference/api/docker_remote_api_v1.18.md @@ -15,7 +15,7 @@ keywords: "API, Docker, rcli, REST, documentation" # Docker Remote API v1.18 -## 1. Brief introduction +# 1. Brief introduction - The Remote API has replaced `rcli`. - The daemon listens on `unix:///var/run/docker.sock` but you can @@ -23,8 +23,6 @@ keywords: "API, Docker, rcli, REST, documentation" - The API tends to be REST, but for some complex commands, like `attach` or `pull`, the HTTP connection is hijacked to transport `STDOUT`, `STDIN` and `STDERR`. - - When the client API version is newer than the daemon's, these calls return an HTTP - `400 Bad Request` error message. # 2. Endpoints diff --git a/docs/reference/api/docker_remote_api_v1.20.md b/docs/reference/api/docker_remote_api_v1.20.md index 34c10714d2..f66ef3e8e3 100644 --- a/docs/reference/api/docker_remote_api_v1.20.md +++ b/docs/reference/api/docker_remote_api_v1.20.md @@ -15,7 +15,7 @@ keywords: "API, Docker, rcli, REST, documentation" # Docker Remote API v1.20 -## 1. Brief introduction +# 1. Brief introduction - The Remote API has replaced `rcli`. - The daemon listens on `unix:///var/run/docker.sock` but you can @@ -23,8 +23,6 @@ keywords: "API, Docker, rcli, REST, documentation" - The API tends to be REST. However, for some complex commands, like `attach` or `pull`, the HTTP connection is hijacked to transport `stdout`, `stdin` and `stderr`. - - When the client API version is newer than the daemon's, these calls return an HTTP - `400 Bad Request` error message. # 2. Endpoints diff --git a/docs/reference/api/docker_remote_api_v1.22.md b/docs/reference/api/docker_remote_api_v1.22.md index 25c29695b5..900ffe1660 100644 --- a/docs/reference/api/docker_remote_api_v1.22.md +++ b/docs/reference/api/docker_remote_api_v1.22.md @@ -15,7 +15,7 @@ keywords: "API, Docker, rcli, REST, documentation" # Docker Remote API v1.22 -## 1. Brief introduction +# 1. Brief introduction - The Remote API has replaced `rcli`. - The daemon listens on `unix:///var/run/docker.sock` but you can @@ -23,8 +23,6 @@ keywords: "API, Docker, rcli, REST, documentation" - The API tends to be REST. However, for some complex commands, like `attach` or `pull`, the HTTP connection is hijacked to transport `stdout`, `stdin` and `stderr`. - - When the client API version is newer than the daemon's, these calls return an HTTP - `400 Bad Request` error message. # 2. Endpoints diff --git a/docs/reference/api/docker_remote_api_v1.24.md b/docs/reference/api/docker_remote_api_v1.24.md index a27fa5fd6c..b9d12404f6 100644 --- a/docs/reference/api/docker_remote_api_v1.24.md +++ b/docs/reference/api/docker_remote_api_v1.24.md @@ -23,8 +23,6 @@ keywords: "API, Docker, rcli, REST, documentation" - The API tends to be REST. However, for some complex commands, like `attach` or `pull`, the HTTP connection is hijacked to transport `stdout`, `stdin` and `stderr`. - - When the client API version is newer than the daemon's, these calls return an HTTP - `400 Bad Request` error message. # 2. Errors diff --git a/docs/reference/api/docker_remote_api_v1.25.md b/docs/reference/api/docker_remote_api_v1.25.md index d7d3c121e1..99884f11a1 100644 --- a/docs/reference/api/docker_remote_api_v1.25.md +++ b/docs/reference/api/docker_remote_api_v1.25.md @@ -23,8 +23,6 @@ keywords: "API, Docker, rcli, REST, documentation" - The API tends to be REST. However, for some complex commands, like `attach` or `pull`, the HTTP connection is hijacked to transport `stdout`, `stdin` and `stderr`. - - When the client API version is newer than the daemon's, these calls return an HTTP - `400 Bad Request` error message. # 2. Errors diff --git a/integration-cli/docker_api_test.go b/integration-cli/docker_api_test.go index 45cd3a2580..3b38ba96f2 100644 --- a/integration-cli/docker_api_test.go +++ b/integration-cli/docker_api_test.go @@ -4,11 +4,9 @@ import ( "fmt" "net/http" "net/http/httptest" - "net/http/httputil" "runtime" "strconv" "strings" - "time" "github.com/docker/docker/api" "github.com/docker/docker/pkg/integration/checker" @@ -34,36 +32,6 @@ func (s *DockerSuite) TestAPIGetEnabledCORS(c *check.C) { //c.Assert(res.Header.Get("Access-Control-Allow-Headers"), check.Equals, "Origin, X-Requested-With, Content-Type, Accept, X-Registry-Auth") } -func (s *DockerSuite) TestAPIVersionStatusCode(c *check.C) { - conn, err := sockConn(time.Duration(10*time.Second), "") - c.Assert(err, checker.IsNil) - - client := httputil.NewClientConn(conn, nil) - defer client.Close() - - req, err := http.NewRequest("GET", "/v999.0/version", nil) - c.Assert(err, checker.IsNil) - req.Header.Set("User-Agent", "Docker-Client/999.0 (os)") - - res, err := client.Do(req) - c.Assert(res.StatusCode, checker.Equals, http.StatusBadRequest) -} - -func (s *DockerSuite) TestAPIClientVersionNewerThanServer(c *check.C) { - v := strings.Split(api.DefaultVersion, ".") - vMinInt, err := strconv.Atoi(v[1]) - c.Assert(err, checker.IsNil) - vMinInt++ - v[1] = strconv.Itoa(vMinInt) - version := strings.Join(v, ".") - - status, body, err := sockRequest("GET", "/v"+version+"/version", nil) - c.Assert(err, checker.IsNil) - c.Assert(status, checker.Equals, http.StatusBadRequest) - expected := fmt.Sprintf("client is newer than server (client API version: %s, server API version: %s)", version, api.DefaultVersion) - c.Assert(getErrorMessage(c, body), checker.Equals, expected) -} - func (s *DockerSuite) TestAPIClientVersionOldNotSupported(c *check.C) { if daemonPlatform != runtime.GOOS { c.Skip("Daemon platform doesn't match test platform") @@ -90,6 +58,7 @@ func (s *DockerSuite) TestAPIDockerAPIVersion(c *check.C) { server := httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("API-Version", api.DefaultVersion) url := r.URL.Path svrVersion = url })) diff --git a/integration-cli/docker_cli_config_test.go b/integration-cli/docker_cli_config_test.go index bd184628f3..1d5e5ad3db 100644 --- a/integration-cli/docker_cli_config_test.go +++ b/integration-cli/docker_cli_config_test.go @@ -9,6 +9,7 @@ import ( "path/filepath" "runtime" + "github.com/docker/docker/api" "github.com/docker/docker/dockerversion" "github.com/docker/docker/pkg/homedir" "github.com/docker/docker/pkg/integration/checker" @@ -25,6 +26,7 @@ func (s *DockerSuite) TestConfigHTTPHeader(c *check.C) { server := httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("API-Version", api.DefaultVersion) headers = r.Header })) defer server.Close() diff --git a/runconfig/opts/parse.go b/runconfig/opts/parse.go index b1771112cc..5a53923709 100644 --- a/runconfig/opts/parse.go +++ b/runconfig/opts/parse.go @@ -168,6 +168,7 @@ func AddFlags(flags *pflag.FlagSet) *ContainerOptions { flags.StringVar(&copts.restartPolicy, "restart", "no", "Restart policy to apply when a container exits") flags.StringVar(&copts.stopSignal, "stop-signal", signal.DefaultStopSignal, fmt.Sprintf("Signal to stop a container, %v by default", signal.DefaultStopSignal)) flags.IntVar(&copts.stopTimeout, "stop-timeout", 0, "Timeout (in seconds) to stop a container") + flags.SetAnnotation("stop-timeout", "version", []string{"1.25"}) flags.Var(copts.sysctls, "sysctl", "Sysctl options") flags.BoolVarP(&copts.tty, "tty", "t", false, "Allocate a pseudo-TTY") flags.Var(copts.ulimits, "ulimit", "Ulimit options")