Просмотр исходного кода

Merge pull request #27745 from vieux/cli_backward_compose_api

allow client to talk to an older server
Victor Vieux 8 лет назад
Родитель
Сommit
d3c780bb90
51 измененных файлов с 192 добавлено и 182 удалено
  1. 0 47
      api/server/middleware/user_agent.go
  2. 1 3
      api/server/middleware/version.go
  3. 0 6
      api/server/middleware/version_test.go
  4. 1 1
      api/server/server.go
  5. 7 0
      api/types/types.go
  6. 3 4
      cli/command/checkpoint/cmd.go
  7. 32 12
      cli/command/cli.go
  8. 2 3
      cli/command/container/cmd.go
  9. 1 0
      cli/command/container/exec.go
  10. 1 0
      cli/command/container/prune.go
  11. 2 1
      cli/command/image/build.go
  12. 2 4
      cli/command/image/cmd.go
  13. 1 0
      cli/command/image/prune.go
  14. 2 3
      cli/command/network/cmd.go
  15. 1 0
      cli/command/network/prune.go
  16. 2 3
      cli/command/node/cmd.go
  17. 2 3
      cli/command/plugin/cmd.go
  18. 2 3
      cli/command/service/cmd.go
  19. 3 4
      cli/command/stack/cmd.go
  20. 1 1
      cli/command/stack/deploy.go
  21. 2 3
      cli/command/swarm/cmd.go
  22. 3 3
      cli/command/system/cmd.go
  23. 1 0
      cli/command/system/df.go
  24. 1 0
      cli/command/system/prune.go
  25. 7 1
      cli/command/system/version.go
  26. 2 3
      cli/command/volume/cmd.go
  27. 1 0
      cli/command/volume/prune.go
  28. 1 1
      cli/command/volume/remove.go
  29. 14 3
      client/client.go
  30. 5 0
      client/container_create.go
  31. 5 0
      client/container_exec.go
  32. 4 0
      client/container_prune.go
  33. 11 0
      client/errors.go
  34. 5 2
      client/image_build.go
  35. 4 0
      client/image_prune.go
  36. 1 1
      client/interface.go
  37. 20 9
      client/ping.go
  38. 3 0
      client/request.go
  39. 4 0
      client/volume_prune.go
  40. 5 2
      client/volume_remove.go
  41. 19 8
      cmd/docker/docker.go
  42. 0 3
      cmd/dockerd/daemon.go
  43. 1 0
      docs/reference/api/docker_remote_api.md
  44. 1 3
      docs/reference/api/docker_remote_api_v1.18.md
  45. 1 3
      docs/reference/api/docker_remote_api_v1.20.md
  46. 1 3
      docs/reference/api/docker_remote_api_v1.22.md
  47. 0 2
      docs/reference/api/docker_remote_api_v1.24.md
  48. 0 2
      docs/reference/api/docker_remote_api_v1.25.md
  49. 1 32
      integration-cli/docker_api_test.go
  50. 2 0
      integration-cli/docker_cli_config_test.go
  51. 1 0
      runconfig/opts/parse.go

+ 0 - 47
api/server/middleware/user_agent.go

@@ -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)
-	}
-}

+ 1 - 3
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)
 	}

+ 0 - 6
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)
-	}
 }

+ 1 - 1
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)

+ 7 - 0
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 {

+ 3 - 4
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),

+ 32 - 12
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
 }
 

+ 2 - 3
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(

+ 1 - 0
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: <name|uid>[:<group|gid>])")
 	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
 }

+ 1 - 0
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()

+ 2 - 1
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()

+ 2 - 4
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
 }

+ 1 - 0
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()

+ 2 - 3
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(

+ 1 - 0
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()

+ 2 - 3
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(

+ 2 - 3
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": ""},
 	}

+ 2 - 3
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(

+ 3 - 4
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),

+ 1 - 1
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()

+ 2 - 3
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(

+ 3 - 3
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
 }

+ 1 - 0
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()

+ 1 - 0
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()

+ 7 - 1
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,

+ 2 - 3
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(

+ 1 - 0
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()

+ 1 - 1
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
 }
 

+ 14 - 3
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.

+ 5 - 0
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)

+ 5 - 0
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

+ 4 - 0
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

+ 11 - 0
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
+}

+ 5 - 2
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")
 	}
 

+ 4 - 0
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

+ 1 - 1
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

+ 20 - 9
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 ping, err
+	}
+	serverResp, err := cli.doRequest(ctx, req)
 	if err != nil {
-		return false, err
+		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
 }

+ 3 - 0
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)
 	}
 

+ 4 - 0
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

+ 5 - 2
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)

+ 19 - 8
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
+		}
 	}
 }

+ 0 - 3
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)
 	}

+ 1 - 0
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.

+ 1 - 3
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
 

+ 1 - 3
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
 

+ 1 - 3
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
 

+ 0 - 2
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
 

+ 0 - 2
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
 

+ 1 - 32
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
 		}))

+ 2 - 0
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()

+ 1 - 0
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")