Browse code

always add but hide experimental cmds and flags

Signed-off-by: Victor Vieux <vieux@docker.com>

update cobra and use Tags

Signed-off-by: Victor Vieux <vieux@docker.com>

allow client to talk to an older server

Signed-off-by: Victor Vieux <vieux@docker.com>

Victor Vieux authored on 2016/11/03 09:43:32
Showing 51 changed files
1 1
deleted file mode 100644
... ...
@@ -1,47 +0,0 @@
1
-package middleware
2
-
3
-import (
4
-	"net/http"
5
-	"strings"
6
-
7
-	"github.com/Sirupsen/logrus"
8
-	"github.com/docker/docker/api/server/httputils"
9
-	"github.com/docker/docker/api/types/versions"
10
-	"golang.org/x/net/context"
11
-)
12
-
13
-// UserAgentMiddleware is a middleware that
14
-// validates the client user-agent.
15
-type UserAgentMiddleware struct {
16
-	serverVersion string
17
-}
18
-
19
-// NewUserAgentMiddleware creates a new UserAgentMiddleware
20
-// with the server version.
21
-func NewUserAgentMiddleware(s string) UserAgentMiddleware {
22
-	return UserAgentMiddleware{
23
-		serverVersion: s,
24
-	}
25
-}
26
-
27
-// WrapHandler returns a new handler function wrapping the previous one in the request chain.
28
-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 {
29
-	return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
30
-		ctx = context.WithValue(ctx, httputils.UAStringKey, r.Header.Get("User-Agent"))
31
-
32
-		if strings.Contains(r.Header.Get("User-Agent"), "Docker-Client/") {
33
-			userAgent := strings.Split(r.Header.Get("User-Agent"), "/")
34
-
35
-			// v1.20 onwards includes the GOOS of the client after the version
36
-			// such as Docker/1.7.0 (linux)
37
-			if len(userAgent) == 2 && strings.Contains(userAgent[1], " ") {
38
-				userAgent[1] = strings.Split(userAgent[1], " ")[0]
39
-			}
40
-
41
-			if len(userAgent) == 2 && !versions.Equal(u.serverVersion, userAgent[1]) {
42
-				logrus.Debugf("Client and server don't have the same version (client: %s, server: %s)", userAgent[1], u.serverVersion)
43
-			}
44
-		}
45
-		return handler(ctx, w, r, vars)
46
-	}
47
-}
... ...
@@ -36,15 +36,13 @@ func (v VersionMiddleware) WrapHandler(handler func(ctx context.Context, w http.
36 36
 			apiVersion = v.defaultVersion
37 37
 		}
38 38
 
39
-		if versions.GreaterThan(apiVersion, v.defaultVersion) {
40
-			return errors.NewBadRequestError(fmt.Errorf("client is newer than server (client API version: %s, server API version: %s)", apiVersion, v.defaultVersion))
41
-		}
42 39
 		if versions.LessThan(apiVersion, v.minVersion) {
43 40
 			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))
44 41
 		}
45 42
 
46 43
 		header := fmt.Sprintf("Docker/%s (%s)", v.serverVersion, runtime.GOOS)
47 44
 		w.Header().Set("Server", header)
45
+		w.Header().Set("API-Version", v.defaultVersion)
48 46
 		ctx = context.WithValue(ctx, "api-version", apiVersion)
49 47
 		return handler(ctx, w, r, vars)
50 48
 	}
... ...
@@ -54,10 +54,4 @@ func TestVersionMiddlewareWithErrors(t *testing.T) {
54 54
 	if !strings.Contains(err.Error(), "client version 0.1 is too old. Minimum supported API version is 1.2.0") {
55 55
 		t.Fatalf("Expected too old client error, got %v", err)
56 56
 	}
57
-
58
-	vars["version"] = "100000"
59
-	err = h(ctx, resp, req, vars)
60
-	if !strings.Contains(err.Error(), "client is newer than server") {
61
-		t.Fatalf("Expected client newer than server error, got %v", err)
62
-	}
63 57
 }
... ...
@@ -128,7 +128,7 @@ func (s *Server) makeHTTPHandler(handler httputils.APIFunc) http.HandlerFunc {
128 128
 		// apply to all requests. Data that is specific to the
129 129
 		// immediate function being called should still be passed
130 130
 		// as 'args' on the function call.
131
-		ctx := context.Background()
131
+		ctx := context.WithValue(context.Background(), httputils.UAStringKey, r.Header.Get("User-Agent"))
132 132
 		handlerFunc := s.handlerWithGlobalMiddlewares(handler)
133 133
 
134 134
 		vars := mux.Vars(r)
... ...
@@ -128,6 +128,13 @@ type ContainerProcessList struct {
128 128
 	Titles    []string
129 129
 }
130 130
 
131
+// Info contains response of Remote API:
132
+// GET "/_ping"
133
+type Ping struct {
134
+	APIVersion   string
135
+	Experimental bool
136
+}
137
+
131 138
 // Version contains response of Remote API:
132 139
 // GET "/version"
133 140
 type Version struct {
... ...
@@ -1,8 +1,6 @@
1 1
 package checkpoint
2 2
 
3 3
 import (
4
-	"fmt"
5
-
6 4
 	"github.com/docker/docker/cli"
7 5
 	"github.com/docker/docker/cli/command"
8 6
 	"github.com/spf13/cobra"
... ...
@@ -15,9 +13,10 @@ func NewCheckpointCommand(dockerCli *command.DockerCli) *cobra.Command {
15 15
 		Short: "Manage checkpoints",
16 16
 		Args:  cli.NoArgs,
17 17
 		Run: func(cmd *cobra.Command, args []string) {
18
-			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
18
+			cmd.SetOutput(dockerCli.Err())
19
+			cmd.HelpFunc()(cmd, args)
19 20
 		},
20
-		Tags: map[string]string{"experimental": ""},
21
+		Tags: map[string]string{"experimental": "", "version": "1.25"},
21 22
 	}
22 23
 	cmd.AddCommand(
23 24
 		newCreateCommand(dockerCli),
... ...
@@ -10,6 +10,7 @@ import (
10 10
 	"runtime"
11 11
 
12 12
 	"github.com/docker/docker/api"
13
+	"github.com/docker/docker/api/types/versions"
13 14
 	cliflags "github.com/docker/docker/cli/flags"
14 15
 	"github.com/docker/docker/cliconfig"
15 16
 	"github.com/docker/docker/cliconfig/configfile"
... ...
@@ -32,21 +33,24 @@ type Streams interface {
32 32
 // DockerCli represents the docker command line client.
33 33
 // Instances of the client can be returned from NewDockerCli.
34 34
 type DockerCli struct {
35
-	configFile *configfile.ConfigFile
36
-	in         *InStream
37
-	out        *OutStream
38
-	err        io.Writer
39
-	keyFile    string
40
-	client     client.APIClient
35
+	configFile      *configfile.ConfigFile
36
+	in              *InStream
37
+	out             *OutStream
38
+	err             io.Writer
39
+	keyFile         string
40
+	client          client.APIClient
41
+	hasExperimental bool
42
+	defaultVersion  string
41 43
 }
42 44
 
43
-// HasExperimental returns true if experimental features are accessible
45
+// HasExperimental returns true if experimental features are accessible.
44 46
 func (cli *DockerCli) HasExperimental() bool {
45
-	if cli.client == nil {
46
-		return false
47
-	}
48
-	enabled, _ := cli.client.Ping(context.Background())
49
-	return enabled
47
+	return cli.hasExperimental
48
+}
49
+
50
+// DefaultVersion returns api.defaultVersion of DOCKER_API_VERSION if specified.
51
+func (cli *DockerCli) DefaultVersion() string {
52
+	return cli.defaultVersion
50 53
 }
51 54
 
52 55
 // Client returns the APIClient
... ...
@@ -93,12 +97,28 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
93 93
 	if err != nil {
94 94
 		return err
95 95
 	}
96
+
97
+	cli.defaultVersion = cli.client.ClientVersion()
98
+
96 99
 	if opts.Common.TrustKey == "" {
97 100
 		cli.keyFile = filepath.Join(cliconfig.ConfigDir(), cliflags.DefaultTrustKeyFile)
98 101
 	} else {
99 102
 		cli.keyFile = opts.Common.TrustKey
100 103
 	}
101 104
 
105
+	if ping, err := cli.client.Ping(context.Background()); err == nil {
106
+		cli.hasExperimental = ping.Experimental
107
+
108
+		// since the new header was added in 1.25, assume server is 1.24 if header is not present.
109
+		if ping.APIVersion == "" {
110
+			ping.APIVersion = "1.24"
111
+		}
112
+
113
+		// if server version is lower than the current cli, downgrade
114
+		if versions.LessThan(ping.APIVersion, cli.client.ClientVersion()) {
115
+			cli.client.UpdateClientVersion(ping.APIVersion)
116
+		}
117
+	}
102 118
 	return nil
103 119
 }
104 120
 
... ...
@@ -1,8 +1,6 @@
1 1
 package container
2 2
 
3 3
 import (
4
-	"fmt"
5
-
6 4
 	"github.com/spf13/cobra"
7 5
 
8 6
 	"github.com/docker/docker/cli"
... ...
@@ -16,7 +14,8 @@ func NewContainerCommand(dockerCli *command.DockerCli) *cobra.Command {
16 16
 		Short: "Manage containers",
17 17
 		Args:  cli.NoArgs,
18 18
 		Run: func(cmd *cobra.Command, args []string) {
19
-			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
19
+			cmd.SetOutput(dockerCli.Err())
20
+			cmd.HelpFunc()(cmd, args)
20 21
 		},
21 22
 	}
22 23
 	cmd.AddCommand(
... ...
@@ -59,6 +59,7 @@ func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command {
59 59
 	flags.StringVarP(&opts.user, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
60 60
 	flags.BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the command")
61 61
 	flags.VarP(opts.env, "env", "e", "Set environment variables")
62
+	flags.SetAnnotation("env", "version", []string{"1.25"})
62 63
 
63 64
 	return cmd
64 65
 }
... ...
@@ -35,6 +35,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
35 35
 			fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
36 36
 			return nil
37 37
 		},
38
+		Tags: map[string]string{"version": "1.25"},
38 39
 	}
39 40
 
40 41
 	flags := cmd.Flags()
... ...
@@ -113,6 +113,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command {
113 113
 
114 114
 	flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer")
115 115
 	flags.SetAnnotation("squash", "experimental", nil)
116
+	flags.SetAnnotation("squash", "version", []string{"1.25"})
116 117
 
117 118
 	return cmd
118 119
 }
... ...
@@ -144,7 +145,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
144 144
 		progBuff      io.Writer
145 145
 		buildBuff     io.Writer
146 146
 	)
147
-	
147
+
148 148
 	specifiedContext := options.context
149 149
 	progBuff = dockerCli.Out()
150 150
 	buildBuff = dockerCli.Out()
... ...
@@ -1,8 +1,6 @@
1 1
 package image
2 2
 
3 3
 import (
4
-	"fmt"
5
-
6 4
 	"github.com/spf13/cobra"
7 5
 
8 6
 	"github.com/docker/docker/cli"
... ...
@@ -16,7 +14,8 @@ func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command {
16 16
 		Short: "Manage images",
17 17
 		Args:  cli.NoArgs,
18 18
 		Run: func(cmd *cobra.Command, args []string) {
19
-			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
19
+			cmd.SetOutput(dockerCli.Err())
20
+			cmd.HelpFunc()(cmd, args)
20 21
 		},
21 22
 	}
22 23
 	cmd.AddCommand(
... ...
@@ -33,6 +32,5 @@ func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command {
33 33
 		newInspectCommand(dockerCli),
34 34
 		NewPruneCommand(dockerCli),
35 35
 	)
36
-
37 36
 	return cmd
38 37
 }
... ...
@@ -36,6 +36,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
36 36
 			fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
37 37
 			return nil
38 38
 		},
39
+		Tags: map[string]string{"version": "1.25"},
39 40
 	}
40 41
 
41 42
 	flags := cmd.Flags()
... ...
@@ -1,8 +1,6 @@
1 1
 package network
2 2
 
3 3
 import (
4
-	"fmt"
5
-
6 4
 	"github.com/spf13/cobra"
7 5
 
8 6
 	"github.com/docker/docker/cli"
... ...
@@ -16,7 +14,8 @@ func NewNetworkCommand(dockerCli *command.DockerCli) *cobra.Command {
16 16
 		Short: "Manage networks",
17 17
 		Args:  cli.NoArgs,
18 18
 		Run: func(cmd *cobra.Command, args []string) {
19
-			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
19
+			cmd.SetOutput(dockerCli.Err())
20
+			cmd.HelpFunc()(cmd, args)
20 21
 		},
21 22
 	}
22 23
 	cmd.AddCommand(
... ...
@@ -33,6 +33,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
33 33
 			}
34 34
 			return nil
35 35
 		},
36
+		Tags: map[string]string{"version": "1.25"},
36 37
 	}
37 38
 
38 39
 	flags := cmd.Flags()
... ...
@@ -1,8 +1,6 @@
1 1
 package node
2 2
 
3 3
 import (
4
-	"fmt"
5
-
6 4
 	"github.com/docker/docker/cli"
7 5
 	"github.com/docker/docker/cli/command"
8 6
 	apiclient "github.com/docker/docker/client"
... ...
@@ -17,7 +15,8 @@ func NewNodeCommand(dockerCli *command.DockerCli) *cobra.Command {
17 17
 		Short: "Manage Swarm nodes",
18 18
 		Args:  cli.NoArgs,
19 19
 		Run: func(cmd *cobra.Command, args []string) {
20
-			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
20
+			cmd.SetOutput(dockerCli.Err())
21
+			cmd.HelpFunc()(cmd, args)
21 22
 		},
22 23
 	}
23 24
 	cmd.AddCommand(
... ...
@@ -1,8 +1,6 @@
1 1
 package plugin
2 2
 
3 3
 import (
4
-	"fmt"
5
-
6 4
 	"github.com/docker/docker/cli"
7 5
 	"github.com/docker/docker/cli/command"
8 6
 	"github.com/spf13/cobra"
... ...
@@ -15,7 +13,8 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command {
15 15
 		Short: "Manage plugins",
16 16
 		Args:  cli.NoArgs,
17 17
 		Run: func(cmd *cobra.Command, args []string) {
18
-			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
18
+			cmd.SetOutput(dockerCli.Err())
19
+			cmd.HelpFunc()(cmd, args)
19 20
 		},
20 21
 		Tags: map[string]string{"experimental": ""},
21 22
 	}
... ...
@@ -1,8 +1,6 @@
1 1
 package service
2 2
 
3 3
 import (
4
-	"fmt"
5
-
6 4
 	"github.com/spf13/cobra"
7 5
 
8 6
 	"github.com/docker/docker/cli"
... ...
@@ -16,7 +14,8 @@ func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command {
16 16
 		Short: "Manage services",
17 17
 		Args:  cli.NoArgs,
18 18
 		Run: func(cmd *cobra.Command, args []string) {
19
-			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
19
+			cmd.SetOutput(dockerCli.Err())
20
+			cmd.HelpFunc()(cmd, args)
20 21
 		},
21 22
 	}
22 23
 	cmd.AddCommand(
... ...
@@ -1,8 +1,6 @@
1 1
 package stack
2 2
 
3 3
 import (
4
-	"fmt"
5
-
6 4
 	"github.com/docker/docker/cli"
7 5
 	"github.com/docker/docker/cli/command"
8 6
 	"github.com/spf13/cobra"
... ...
@@ -15,9 +13,10 @@ func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command {
15 15
 		Short: "Manage Docker stacks",
16 16
 		Args:  cli.NoArgs,
17 17
 		Run: func(cmd *cobra.Command, args []string) {
18
-			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
18
+			cmd.SetOutput(dockerCli.Err())
19
+			cmd.HelpFunc()(cmd, args)
19 20
 		},
20
-		Tags: map[string]string{"experimental": ""},
21
+		Tags: map[string]string{"experimental": "", "version": "1.25"},
21 22
 	}
22 23
 	cmd.AddCommand(
23 24
 		newConfigCommand(dockerCli),
... ...
@@ -36,7 +36,7 @@ func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command {
36 36
 			opts.namespace = strings.TrimSuffix(args[0], ".dab")
37 37
 			return runDeploy(dockerCli, opts)
38 38
 		},
39
-		Tags: map[string]string{"experimental": ""},
39
+		Tags: map[string]string{"experimental": "", "version": "1.25"},
40 40
 	}
41 41
 
42 42
 	flags := cmd.Flags()
... ...
@@ -1,8 +1,6 @@
1 1
 package swarm
2 2
 
3 3
 import (
4
-	"fmt"
5
-
6 4
 	"github.com/spf13/cobra"
7 5
 
8 6
 	"github.com/docker/docker/cli"
... ...
@@ -16,7 +14,8 @@ func NewSwarmCommand(dockerCli *command.DockerCli) *cobra.Command {
16 16
 		Short: "Manage Swarm",
17 17
 		Args:  cli.NoArgs,
18 18
 		Run: func(cmd *cobra.Command, args []string) {
19
-			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
19
+			cmd.SetOutput(dockerCli.Err())
20
+			cmd.HelpFunc()(cmd, args)
20 21
 		},
21 22
 	}
22 23
 	cmd.AddCommand(
... ...
@@ -1,8 +1,6 @@
1 1
 package system
2 2
 
3 3
 import (
4
-	"fmt"
5
-
6 4
 	"github.com/spf13/cobra"
7 5
 
8 6
 	"github.com/docker/docker/cli"
... ...
@@ -16,7 +14,8 @@ func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command {
16 16
 		Short: "Manage Docker",
17 17
 		Args:  cli.NoArgs,
18 18
 		Run: func(cmd *cobra.Command, args []string) {
19
-			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
19
+			cmd.SetOutput(dockerCli.Err())
20
+			cmd.HelpFunc()(cmd, args)
20 21
 		},
21 22
 	}
22 23
 	cmd.AddCommand(
... ...
@@ -25,5 +24,6 @@ func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command {
25 25
 		NewDiskUsageCommand(dockerCli),
26 26
 		NewPruneCommand(dockerCli),
27 27
 	)
28
+
28 29
 	return cmd
29 30
 }
... ...
@@ -23,6 +23,7 @@ func NewDiskUsageCommand(dockerCli *command.DockerCli) *cobra.Command {
23 23
 		RunE: func(cmd *cobra.Command, args []string) error {
24 24
 			return runDiskUsage(dockerCli, opts)
25 25
 		},
26
+		Tags: map[string]string{"version": "1.25"},
26 27
 	}
27 28
 
28 29
 	flags := cmd.Flags()
... ...
@@ -26,6 +26,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
26 26
 		RunE: func(cmd *cobra.Command, args []string) error {
27 27
 			return runPrune(dockerCli, opts)
28 28
 		},
29
+		Tags: map[string]string{"version": "1.25"},
29 30
 	}
30 31
 
31 32
 	flags := cmd.Flags()
... ...
@@ -1,6 +1,7 @@
1 1
 package system
2 2
 
3 3
 import (
4
+	"fmt"
4 5
 	"runtime"
5 6
 	"time"
6 7
 
... ...
@@ -70,10 +71,15 @@ func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error {
70 70
 			Status: "Template parsing error: " + err.Error()}
71 71
 	}
72 72
 
73
+	APIVersion := dockerCli.Client().ClientVersion()
74
+	if defaultAPIVersion := dockerCli.DefaultVersion(); APIVersion != defaultAPIVersion {
75
+		APIVersion = fmt.Sprintf("%s (downgraded from %s)", APIVersion, defaultAPIVersion)
76
+	}
77
+
73 78
 	vd := types.VersionResponse{
74 79
 		Client: &types.Version{
75 80
 			Version:    dockerversion.Version,
76
-			APIVersion: dockerCli.Client().ClientVersion(),
81
+			APIVersion: APIVersion,
77 82
 			GoVersion:  runtime.Version(),
78 83
 			GitCommit:  dockerversion.GitCommit,
79 84
 			BuildTime:  dockerversion.BuildTime,
... ...
@@ -1,8 +1,6 @@
1 1
 package volume
2 2
 
3 3
 import (
4
-	"fmt"
5
-
6 4
 	"github.com/spf13/cobra"
7 5
 
8 6
 	"github.com/docker/docker/cli"
... ...
@@ -17,7 +15,8 @@ func NewVolumeCommand(dockerCli *command.DockerCli) *cobra.Command {
17 17
 		Long:  volumeDescription,
18 18
 		Args:  cli.NoArgs,
19 19
 		Run: func(cmd *cobra.Command, args []string) {
20
-			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
20
+			cmd.SetOutput(dockerCli.Err())
21
+			cmd.HelpFunc()(cmd, args)
21 22
 		},
22 23
 	}
23 24
 	cmd.AddCommand(
... ...
@@ -35,6 +35,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
35 35
 			fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
36 36
 			return nil
37 37
 		},
38
+		Tags: map[string]string{"version": "1.25"},
38 39
 	}
39 40
 
40 41
 	flags := cmd.Flags()
... ...
@@ -34,7 +34,7 @@ func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
34 34
 
35 35
 	flags := cmd.Flags()
36 36
 	flags.BoolVarP(&opts.force, "force", "f", false, "Force the removal of one or more volumes")
37
-
37
+	flags.SetAnnotation("force", "version", []string{"1.25"})
38 38
 	return cmd
39 39
 }
40 40
 
... ...
@@ -79,6 +79,8 @@ type Client struct {
79 79
 	version string
80 80
 	// custom http headers configured by users.
81 81
 	customHTTPHeaders map[string]string
82
+	// manualOverride is set to true when the version was set by users.
83
+	manualOverride bool
82 84
 }
83 85
 
84 86
 // NewEnvClient initializes a new API client based on environment variables.
... ...
@@ -111,13 +113,19 @@ func NewEnvClient() (*Client, error) {
111 111
 	if host == "" {
112 112
 		host = DefaultDockerHost
113 113
 	}
114
-
115 114
 	version := os.Getenv("DOCKER_API_VERSION")
116 115
 	if version == "" {
117 116
 		version = DefaultVersion
118 117
 	}
119 118
 
120
-	return NewClient(host, version, client, nil)
119
+	cli, err := NewClient(host, version, client, nil)
120
+	if err != nil {
121
+		return cli, err
122
+	}
123
+	if version != "" {
124
+		cli.manualOverride = true
125
+	}
126
+	return cli, nil
121 127
 }
122 128
 
123 129
 // NewClient initializes a new API client for the given host and API version.
... ...
@@ -211,7 +219,10 @@ func (cli *Client) ClientVersion() string {
211 211
 // UpdateClientVersion updates the version string associated with this
212 212
 // instance of the Client.
213 213
 func (cli *Client) UpdateClientVersion(v string) {
214
-	cli.version = v
214
+	if !cli.manualOverride {
215
+		cli.version = v
216
+	}
217
+
215 218
 }
216 219
 
217 220
 // ParseHost verifies that the given host strings is valid.
... ...
@@ -20,6 +20,11 @@ type configWrapper struct {
20 20
 // It can be associated with a name, but it's not mandatory.
21 21
 func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error) {
22 22
 	var response container.ContainerCreateCreatedBody
23
+
24
+	if err := cli.NewVersionError("1.25", "stop timeout"); config != nil && config.StopTimeout != nil && err != nil {
25
+		return response, err
26
+	}
27
+
23 28
 	query := url.Values{}
24 29
 	if containerName != "" {
25 30
 		query.Set("name", containerName)
... ...
@@ -10,6 +10,11 @@ import (
10 10
 // ContainerExecCreate creates a new exec configuration to run an exec process.
11 11
 func (cli *Client) ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.IDResponse, error) {
12 12
 	var response types.IDResponse
13
+
14
+	if err := cli.NewVersionError("1.25", "env"); len(config.Env) != 0 && err != nil {
15
+		return response, err
16
+	}
17
+
13 18
 	resp, err := cli.post(ctx, "/containers/"+container+"/exec", nil, config, nil)
14 19
 	if err != nil {
15 20
 		return response, err
... ...
@@ -12,6 +12,10 @@ import (
12 12
 func (cli *Client) ContainersPrune(ctx context.Context, cfg types.ContainersPruneConfig) (types.ContainersPruneReport, error) {
13 13
 	var report types.ContainersPruneReport
14 14
 
15
+	if err := cli.NewVersionError("1.25", "container prune"); err != nil {
16
+		return report, err
17
+	}
18
+
15 19
 	serverResp, err := cli.post(ctx, "/containers/prune", nil, cfg, nil)
16 20
 	if err != nil {
17 21
 		return report, err
... ...
@@ -3,6 +3,8 @@ package client
3 3
 import (
4 4
 	"errors"
5 5
 	"fmt"
6
+
7
+	"github.com/docker/docker/api/types/versions"
6 8
 )
7 9
 
8 10
 // ErrConnectionFailed is an error raised when the connection between the client and the server failed.
... ...
@@ -206,3 +208,12 @@ func IsErrPluginPermissionDenied(err error) bool {
206 206
 	_, ok := err.(pluginPermissionDenied)
207 207
 	return ok
208 208
 }
209
+
210
+// NewVersionError returns an error if the APIVersion required
211
+// if less than the current supported version
212
+func (cli *Client) NewVersionError(APIrequired, feature string) error {
213
+	if versions.LessThan(cli.version, APIrequired) {
214
+		return fmt.Errorf("%q requires API version %s, but the Docker server is version %s", feature, APIrequired, cli.version)
215
+	}
216
+	return nil
217
+}
... ...
@@ -21,7 +21,7 @@ var headerRegexp = regexp.MustCompile(`\ADocker/.+\s\((.+)\)\z`)
21 21
 // The Body in the response implement an io.ReadCloser and it's up to the caller to
22 22
 // close it.
23 23
 func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
24
-	query, err := imageBuildOptionsToQuery(options)
24
+	query, err := cli.imageBuildOptionsToQuery(options)
25 25
 	if err != nil {
26 26
 		return types.ImageBuildResponse{}, err
27 27
 	}
... ...
@@ -47,7 +47,7 @@ func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, optio
47 47
 	}, nil
48 48
 }
49 49
 
50
-func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) {
50
+func (cli *Client) imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) {
51 51
 	query := url.Values{
52 52
 		"t":           options.Tags,
53 53
 		"securityopt": options.SecurityOpt,
... ...
@@ -76,6 +76,9 @@ func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, erro
76 76
 	}
77 77
 
78 78
 	if options.Squash {
79
+		if err := cli.NewVersionError("1.25", "squash"); err != nil {
80
+			return query, err
81
+		}
79 82
 		query.Set("squash", "1")
80 83
 	}
81 84
 
... ...
@@ -12,6 +12,10 @@ import (
12 12
 func (cli *Client) ImagesPrune(ctx context.Context, cfg types.ImagesPruneConfig) (types.ImagesPruneReport, error) {
13 13
 	var report types.ImagesPruneReport
14 14
 
15
+	if err := cli.NewVersionError("1.25", "image prune"); err != nil {
16
+		return report, err
17
+	}
18
+
15 19
 	serverResp, err := cli.post(ctx, "/images/prune", nil, cfg, nil)
16 20
 	if err != nil {
17 21
 		return report, err
... ...
@@ -129,7 +129,7 @@ type SystemAPIClient interface {
129 129
 	Info(ctx context.Context) (types.Info, error)
130 130
 	RegistryLogin(ctx context.Context, auth types.AuthConfig) (registry.AuthenticateOKBody, error)
131 131
 	DiskUsage(ctx context.Context) (types.DiskUsage, error)
132
-	Ping(ctx context.Context) (bool, error)
132
+	Ping(ctx context.Context) (types.Ping, error)
133 133
 }
134 134
 
135 135
 // VolumeAPIClient defines API client methods for the volumes
... ...
@@ -1,19 +1,30 @@
1 1
 package client
2 2
 
3
-import "golang.org/x/net/context"
3
+import (
4
+	"fmt"
4 5
 
5
-// Ping pings the server and return the value of the "Docker-Experimental" header
6
-func (cli *Client) Ping(ctx context.Context) (bool, error) {
7
-	serverResp, err := cli.get(ctx, "/_ping", nil, nil)
6
+	"github.com/docker/docker/api/types"
7
+	"golang.org/x/net/context"
8
+)
9
+
10
+// Ping pings the server and return the value of the "Docker-Experimental" & "API-Version" headers
11
+func (cli *Client) Ping(ctx context.Context) (types.Ping, error) {
12
+	var ping types.Ping
13
+	req, err := cli.buildRequest("GET", fmt.Sprintf("%s/_ping", cli.basePath), nil, nil)
14
+	if err != nil {
15
+		return ping, err
16
+	}
17
+	serverResp, err := cli.doRequest(ctx, req)
8 18
 	if err != nil {
9
-		return false, err
19
+		return ping, err
10 20
 	}
11 21
 	defer ensureReaderClosed(serverResp)
12 22
 
13
-	exp := serverResp.header.Get("Docker-Experimental")
14
-	if exp != "true" {
15
-		return false, nil
23
+	ping.APIVersion = serverResp.header.Get("API-Version")
24
+
25
+	if serverResp.header.Get("Docker-Experimental") == "true" {
26
+		ping.Experimental = true
16 27
 	}
17 28
 
18
-	return true, nil
29
+	return ping, nil
19 30
 }
... ...
@@ -214,6 +214,9 @@ func (cli *Client) addHeaders(req *http.Request, headers headers) *http.Request
214 214
 	// Add CLI Config's HTTP Headers BEFORE we set the Docker headers
215 215
 	// then the user can't change OUR headers
216 216
 	for k, v := range cli.customHTTPHeaders {
217
+		if versions.LessThan(cli.version, "1.25") && k == "User-Agent" {
218
+			continue
219
+		}
217 220
 		req.Header.Set(k, v)
218 221
 	}
219 222
 
... ...
@@ -12,6 +12,10 @@ import (
12 12
 func (cli *Client) VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error) {
13 13
 	var report types.VolumesPruneReport
14 14
 
15
+	if err := cli.NewVersionError("1.25", "volume prune"); err != nil {
16
+		return report, err
17
+	}
18
+
15 19
 	serverResp, err := cli.post(ctx, "/volumes/prune", nil, cfg, nil)
16 20
 	if err != nil {
17 21
 		return report, err
... ...
@@ -3,14 +3,17 @@ package client
3 3
 import (
4 4
 	"net/url"
5 5
 
6
+	"github.com/docker/docker/api/types/versions"
6 7
 	"golang.org/x/net/context"
7 8
 )
8 9
 
9 10
 // VolumeRemove removes a volume from the docker host.
10 11
 func (cli *Client) VolumeRemove(ctx context.Context, volumeID string, force bool) error {
11 12
 	query := url.Values{}
12
-	if force {
13
-		query.Set("force", "1")
13
+	if versions.GreaterThanOrEqualTo(cli.version, "1.25") {
14
+		if force {
15
+			query.Set("force", "1")
16
+		}
14 17
 	}
15 18
 	resp, err := cli.delete(ctx, "/volumes/"+volumeID, query, nil)
16 19
 	ensureReaderClosed(resp)
... ...
@@ -5,6 +5,7 @@ import (
5 5
 	"os"
6 6
 
7 7
 	"github.com/Sirupsen/logrus"
8
+	"github.com/docker/docker/api/types/versions"
8 9
 	"github.com/docker/docker/cli"
9 10
 	"github.com/docker/docker/cli/command"
10 11
 	"github.com/docker/docker/cli/command/commands"
... ...
@@ -47,16 +48,15 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
47 47
 	cli.SetupRootCommand(cmd)
48 48
 
49 49
 	cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) {
50
-		var err error
51 50
 		if dockerCli.Client() == nil {
52 51
 			// flags must be the top-level command flags, not cmd.Flags()
53 52
 			opts.Common.SetDefaultOptions(flags)
54 53
 			dockerPreRun(opts)
55
-			err = dockerCli.Initialize(opts)
56
-		}
57
-		if err != nil || !dockerCli.HasExperimental() {
58
-			hideExperimentalFeatures(ccmd)
54
+			dockerCli.Initialize(opts)
59 55
 		}
56
+
57
+		hideUnsupportedFeatures(ccmd, dockerCli.Client().ClientVersion(), dockerCli.HasExperimental())
58
+
60 59
 		if err := ccmd.Help(); err != nil {
61 60
 			ccmd.Println(err)
62 61
 		}
... ...
@@ -123,18 +123,29 @@ func dockerPreRun(opts *cliflags.ClientOptions) {
123 123
 	}
124 124
 }
125 125
 
126
-func hideExperimentalFeatures(cmd *cobra.Command) {
127
-	// hide flags
126
+func hideUnsupportedFeatures(cmd *cobra.Command, clientVersion string, hasExperimental bool) {
128 127
 	cmd.Flags().VisitAll(func(f *pflag.Flag) {
128
+		// hide experimental flags
129 129
 		if _, ok := f.Annotations["experimental"]; ok {
130 130
 			f.Hidden = true
131 131
 		}
132
+
133
+		// hide flags not supported by the server
134
+		if flagVersion, ok := f.Annotations["version"]; ok && len(flagVersion) == 1 && versions.LessThan(clientVersion, flagVersion[0]) {
135
+			f.Hidden = true
136
+		}
137
+
132 138
 	})
133 139
 
134 140
 	for _, subcmd := range cmd.Commands() {
135
-		// hide subcommands
141
+		// hide experimental subcommands
136 142
 		if _, ok := subcmd.Tags["experimental"]; ok {
137 143
 			subcmd.Hidden = true
138 144
 		}
145
+
146
+		// hide subcommands not supported by the server
147
+		if subcmdVersion, ok := subcmd.Tags["version"]; ok && versions.LessThan(clientVersion, subcmdVersion) {
148
+			subcmd.Hidden = true
149
+		}
139 150
 	}
140 151
 }
... ...
@@ -480,9 +480,6 @@ func (cli *DaemonCli) initMiddlewares(s *apiserver.Server, cfg *apiserver.Config
480 480
 		s.UseMiddleware(c)
481 481
 	}
482 482
 
483
-	u := middleware.NewUserAgentMiddleware(v)
484
-	s.UseMiddleware(u)
485
-
486 483
 	if err := validateAuthzPlugins(cli.Config.AuthorizationPlugins, cli.d.PluginStore); err != nil {
487 484
 		return fmt.Errorf("Error validating authorization plugin: %v", err)
488 485
 	}
... ...
@@ -162,6 +162,7 @@ This section lists each version from latest to oldest.  Each listing includes a
162 162
 * `POST /volumes/prune` prunes unused volumes.
163 163
 * `POST /networks/prune` prunes unused networks.
164 164
 * Every API response now includes a `Docker-Experimental` header specifying if experimental features are enabled (value can be `true` or `false`).
165
+* Every API response now includes a `API-Version` header specifying the default API version of the server.
165 166
 * 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.
166 167
 * The `SecurityOptions` field within the `GET /info` response now includes `userns` if user namespaces are enabled in the daemon.
167 168
 * `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.
... ...
@@ -15,7 +15,7 @@ keywords: "API, Docker, rcli, REST, documentation"
15 15
 
16 16
 # Docker Remote API v1.18
17 17
 
18
-## 1. Brief introduction
18
+# 1. Brief introduction
19 19
 
20 20
  - The Remote API has replaced `rcli`.
21 21
  - The daemon listens on `unix:///var/run/docker.sock` but you can
... ...
@@ -23,8 +23,6 @@ keywords: "API, Docker, rcli, REST, documentation"
23 23
  - The API tends to be REST, but for some complex commands, like `attach`
24 24
    or `pull`, the HTTP connection is hijacked to transport `STDOUT`,
25 25
    `STDIN` and `STDERR`.
26
- - When the client API version is newer than the daemon's, these calls return an HTTP
27
-   `400 Bad Request` error message.
28 26
 
29 27
 # 2. Endpoints
30 28
 
... ...
@@ -15,7 +15,7 @@ keywords: "API, Docker, rcli, REST, documentation"
15 15
 
16 16
 # Docker Remote API v1.20
17 17
 
18
-## 1. Brief introduction
18
+# 1. Brief introduction
19 19
 
20 20
  - The Remote API has replaced `rcli`.
21 21
  - The daemon listens on `unix:///var/run/docker.sock` but you can
... ...
@@ -23,8 +23,6 @@ keywords: "API, Docker, rcli, REST, documentation"
23 23
  - The API tends to be REST. However, for some complex commands, like `attach`
24 24
    or `pull`, the HTTP connection is hijacked to transport `stdout`,
25 25
    `stdin` and `stderr`.
26
- - When the client API version is newer than the daemon's, these calls return an HTTP
27
-   `400 Bad Request` error message.
28 26
 
29 27
 # 2. Endpoints
30 28
 
... ...
@@ -15,7 +15,7 @@ keywords: "API, Docker, rcli, REST, documentation"
15 15
 
16 16
 # Docker Remote API v1.22
17 17
 
18
-## 1. Brief introduction
18
+# 1. Brief introduction
19 19
 
20 20
  - The Remote API has replaced `rcli`.
21 21
  - The daemon listens on `unix:///var/run/docker.sock` but you can
... ...
@@ -23,8 +23,6 @@ keywords: "API, Docker, rcli, REST, documentation"
23 23
  - The API tends to be REST. However, for some complex commands, like `attach`
24 24
    or `pull`, the HTTP connection is hijacked to transport `stdout`,
25 25
    `stdin` and `stderr`.
26
- - When the client API version is newer than the daemon's, these calls return an HTTP
27
-   `400 Bad Request` error message.
28 26
 
29 27
 # 2. Endpoints
30 28
 
... ...
@@ -23,8 +23,6 @@ keywords: "API, Docker, rcli, REST, documentation"
23 23
  - The API tends to be REST. However, for some complex commands, like `attach`
24 24
    or `pull`, the HTTP connection is hijacked to transport `stdout`,
25 25
    `stdin` and `stderr`.
26
- - When the client API version is newer than the daemon's, these calls return an HTTP
27
-   `400 Bad Request` error message.
28 26
 
29 27
 # 2. Errors
30 28
 
... ...
@@ -23,8 +23,6 @@ keywords: "API, Docker, rcli, REST, documentation"
23 23
  - The API tends to be REST. However, for some complex commands, like `attach`
24 24
    or `pull`, the HTTP connection is hijacked to transport `stdout`,
25 25
    `stdin` and `stderr`.
26
- - When the client API version is newer than the daemon's, these calls return an HTTP
27
-   `400 Bad Request` error message.
28 26
 
29 27
 # 2. Errors
30 28
 
... ...
@@ -4,11 +4,9 @@ import (
4 4
 	"fmt"
5 5
 	"net/http"
6 6
 	"net/http/httptest"
7
-	"net/http/httputil"
8 7
 	"runtime"
9 8
 	"strconv"
10 9
 	"strings"
11
-	"time"
12 10
 
13 11
 	"github.com/docker/docker/api"
14 12
 	"github.com/docker/docker/pkg/integration/checker"
... ...
@@ -34,36 +32,6 @@ func (s *DockerSuite) TestAPIGetEnabledCORS(c *check.C) {
34 34
 	//c.Assert(res.Header.Get("Access-Control-Allow-Headers"), check.Equals, "Origin, X-Requested-With, Content-Type, Accept, X-Registry-Auth")
35 35
 }
36 36
 
37
-func (s *DockerSuite) TestAPIVersionStatusCode(c *check.C) {
38
-	conn, err := sockConn(time.Duration(10*time.Second), "")
39
-	c.Assert(err, checker.IsNil)
40
-
41
-	client := httputil.NewClientConn(conn, nil)
42
-	defer client.Close()
43
-
44
-	req, err := http.NewRequest("GET", "/v999.0/version", nil)
45
-	c.Assert(err, checker.IsNil)
46
-	req.Header.Set("User-Agent", "Docker-Client/999.0 (os)")
47
-
48
-	res, err := client.Do(req)
49
-	c.Assert(res.StatusCode, checker.Equals, http.StatusBadRequest)
50
-}
51
-
52
-func (s *DockerSuite) TestAPIClientVersionNewerThanServer(c *check.C) {
53
-	v := strings.Split(api.DefaultVersion, ".")
54
-	vMinInt, err := strconv.Atoi(v[1])
55
-	c.Assert(err, checker.IsNil)
56
-	vMinInt++
57
-	v[1] = strconv.Itoa(vMinInt)
58
-	version := strings.Join(v, ".")
59
-
60
-	status, body, err := sockRequest("GET", "/v"+version+"/version", nil)
61
-	c.Assert(err, checker.IsNil)
62
-	c.Assert(status, checker.Equals, http.StatusBadRequest)
63
-	expected := fmt.Sprintf("client is newer than server (client API version: %s, server API version: %s)", version, api.DefaultVersion)
64
-	c.Assert(getErrorMessage(c, body), checker.Equals, expected)
65
-}
66
-
67 37
 func (s *DockerSuite) TestAPIClientVersionOldNotSupported(c *check.C) {
68 38
 	if daemonPlatform != runtime.GOOS {
69 39
 		c.Skip("Daemon platform doesn't match test platform")
... ...
@@ -90,6 +58,7 @@ func (s *DockerSuite) TestAPIDockerAPIVersion(c *check.C) {
90 90
 
91 91
 	server := httptest.NewServer(http.HandlerFunc(
92 92
 		func(w http.ResponseWriter, r *http.Request) {
93
+			w.Header().Set("API-Version", api.DefaultVersion)
93 94
 			url := r.URL.Path
94 95
 			svrVersion = url
95 96
 		}))
... ...
@@ -9,6 +9,7 @@ import (
9 9
 	"path/filepath"
10 10
 	"runtime"
11 11
 
12
+	"github.com/docker/docker/api"
12 13
 	"github.com/docker/docker/dockerversion"
13 14
 	"github.com/docker/docker/pkg/homedir"
14 15
 	"github.com/docker/docker/pkg/integration/checker"
... ...
@@ -25,6 +26,7 @@ func (s *DockerSuite) TestConfigHTTPHeader(c *check.C) {
25 25
 
26 26
 	server := httptest.NewServer(http.HandlerFunc(
27 27
 		func(w http.ResponseWriter, r *http.Request) {
28
+			w.Header().Set("API-Version", api.DefaultVersion)
28 29
 			headers = r.Header
29 30
 		}))
30 31
 	defer server.Close()
... ...
@@ -168,6 +168,7 @@ func AddFlags(flags *pflag.FlagSet) *ContainerOptions {
168 168
 	flags.StringVar(&copts.restartPolicy, "restart", "no", "Restart policy to apply when a container exits")
169 169
 	flags.StringVar(&copts.stopSignal, "stop-signal", signal.DefaultStopSignal, fmt.Sprintf("Signal to stop a container, %v by default", signal.DefaultStopSignal))
170 170
 	flags.IntVar(&copts.stopTimeout, "stop-timeout", 0, "Timeout (in seconds) to stop a container")
171
+	flags.SetAnnotation("stop-timeout", "version", []string{"1.25"})
171 172
 	flags.Var(copts.sysctls, "sysctl", "Sysctl options")
172 173
 	flags.BoolVarP(&copts.tty, "tty", "t", false, "Allocate a pseudo-TTY")
173 174
 	flags.Var(copts.ulimits, "ulimit", "Ulimit options")