1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

Merge pull request #27745 from vieux/cli_backward_compose_api

allow client to talk to an older server
This commit is contained in:
Victor Vieux 2016-11-08 18:27:23 -08:00 committed by GitHub
commit d3c780bb90
51 changed files with 192 additions and 182 deletions

View file

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

View file

@ -36,15 +36,13 @@ func (v VersionMiddleware) WrapHandler(handler func(ctx context.Context, w http.
apiVersion = v.defaultVersion 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) { 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)) 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) header := fmt.Sprintf("Docker/%s (%s)", v.serverVersion, runtime.GOOS)
w.Header().Set("Server", header) w.Header().Set("Server", header)
w.Header().Set("API-Version", v.defaultVersion)
ctx = context.WithValue(ctx, "api-version", apiVersion) ctx = context.WithValue(ctx, "api-version", apiVersion)
return handler(ctx, w, r, vars) return handler(ctx, w, r, vars)
} }

View file

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

View file

@ -128,7 +128,7 @@ func (s *Server) makeHTTPHandler(handler httputils.APIFunc) http.HandlerFunc {
// apply to all requests. Data that is specific to the // apply to all requests. Data that is specific to the
// immediate function being called should still be passed // immediate function being called should still be passed
// as 'args' on the function call. // 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) handlerFunc := s.handlerWithGlobalMiddlewares(handler)
vars := mux.Vars(r) vars := mux.Vars(r)

View file

@ -128,6 +128,13 @@ type ContainerProcessList struct {
Titles []string Titles []string
} }
// Info contains response of Remote API:
// GET "/_ping"
type Ping struct {
APIVersion string
Experimental bool
}
// Version contains response of Remote API: // Version contains response of Remote API:
// GET "/version" // GET "/version"
type Version struct { type Version struct {

View file

@ -1,8 +1,6 @@
package checkpoint package checkpoint
import ( import (
"fmt"
"github.com/docker/docker/cli" "github.com/docker/docker/cli"
"github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -15,9 +13,10 @@ func NewCheckpointCommand(dockerCli *command.DockerCli) *cobra.Command {
Short: "Manage checkpoints", Short: "Manage checkpoints",
Args: cli.NoArgs, Args: cli.NoArgs,
Run: func(cmd *cobra.Command, args []string) { 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( cmd.AddCommand(
newCreateCommand(dockerCli), newCreateCommand(dockerCli),

View file

@ -10,6 +10,7 @@ import (
"runtime" "runtime"
"github.com/docker/docker/api" "github.com/docker/docker/api"
"github.com/docker/docker/api/types/versions"
cliflags "github.com/docker/docker/cli/flags" cliflags "github.com/docker/docker/cli/flags"
"github.com/docker/docker/cliconfig" "github.com/docker/docker/cliconfig"
"github.com/docker/docker/cliconfig/configfile" "github.com/docker/docker/cliconfig/configfile"
@ -32,21 +33,24 @@ type Streams interface {
// DockerCli represents the docker command line client. // DockerCli represents the docker command line client.
// Instances of the client can be returned from NewDockerCli. // Instances of the client can be returned from NewDockerCli.
type DockerCli struct { type DockerCli struct {
configFile *configfile.ConfigFile configFile *configfile.ConfigFile
in *InStream in *InStream
out *OutStream out *OutStream
err io.Writer err io.Writer
keyFile string keyFile string
client client.APIClient 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 { func (cli *DockerCli) HasExperimental() bool {
if cli.client == nil { return cli.hasExperimental
return false }
}
enabled, _ := cli.client.Ping(context.Background()) // DefaultVersion returns api.defaultVersion of DOCKER_API_VERSION if specified.
return enabled func (cli *DockerCli) DefaultVersion() string {
return cli.defaultVersion
} }
// Client returns the APIClient // Client returns the APIClient
@ -93,12 +97,28 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
if err != nil { if err != nil {
return err return err
} }
cli.defaultVersion = cli.client.ClientVersion()
if opts.Common.TrustKey == "" { if opts.Common.TrustKey == "" {
cli.keyFile = filepath.Join(cliconfig.ConfigDir(), cliflags.DefaultTrustKeyFile) cli.keyFile = filepath.Join(cliconfig.ConfigDir(), cliflags.DefaultTrustKeyFile)
} else { } else {
cli.keyFile = opts.Common.TrustKey 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 return nil
} }

View file

@ -1,8 +1,6 @@
package container package container
import ( import (
"fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/docker/cli" "github.com/docker/docker/cli"
@ -16,7 +14,8 @@ func NewContainerCommand(dockerCli *command.DockerCli) *cobra.Command {
Short: "Manage containers", Short: "Manage containers",
Args: cli.NoArgs, Args: cli.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) cmd.SetOutput(dockerCli.Err())
cmd.HelpFunc()(cmd, args)
}, },
} }
cmd.AddCommand( cmd.AddCommand(

View file

@ -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.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.BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the command")
flags.VarP(opts.env, "env", "e", "Set environment variables") flags.VarP(opts.env, "env", "e", "Set environment variables")
flags.SetAnnotation("env", "version", []string{"1.25"})
return cmd return cmd
} }

View file

@ -35,6 +35,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
return nil return nil
}, },
Tags: map[string]string{"version": "1.25"},
} }
flags := cmd.Flags() flags := cmd.Flags()

View file

@ -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.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer")
flags.SetAnnotation("squash", "experimental", nil) flags.SetAnnotation("squash", "experimental", nil)
flags.SetAnnotation("squash", "version", []string{"1.25"})
return cmd return cmd
} }
@ -144,7 +145,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
progBuff io.Writer progBuff io.Writer
buildBuff io.Writer buildBuff io.Writer
) )
specifiedContext := options.context specifiedContext := options.context
progBuff = dockerCli.Out() progBuff = dockerCli.Out()
buildBuff = dockerCli.Out() buildBuff = dockerCli.Out()

View file

@ -1,8 +1,6 @@
package image package image
import ( import (
"fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/docker/cli" "github.com/docker/docker/cli"
@ -16,7 +14,8 @@ func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command {
Short: "Manage images", Short: "Manage images",
Args: cli.NoArgs, Args: cli.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) cmd.SetOutput(dockerCli.Err())
cmd.HelpFunc()(cmd, args)
}, },
} }
cmd.AddCommand( cmd.AddCommand(
@ -33,6 +32,5 @@ func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command {
newInspectCommand(dockerCli), newInspectCommand(dockerCli),
NewPruneCommand(dockerCli), NewPruneCommand(dockerCli),
) )
return cmd return cmd
} }

View file

@ -36,6 +36,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
return nil return nil
}, },
Tags: map[string]string{"version": "1.25"},
} }
flags := cmd.Flags() flags := cmd.Flags()

View file

@ -1,8 +1,6 @@
package network package network
import ( import (
"fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/docker/cli" "github.com/docker/docker/cli"
@ -16,7 +14,8 @@ func NewNetworkCommand(dockerCli *command.DockerCli) *cobra.Command {
Short: "Manage networks", Short: "Manage networks",
Args: cli.NoArgs, Args: cli.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) cmd.SetOutput(dockerCli.Err())
cmd.HelpFunc()(cmd, args)
}, },
} }
cmd.AddCommand( cmd.AddCommand(

View file

@ -33,6 +33,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
} }
return nil return nil
}, },
Tags: map[string]string{"version": "1.25"},
} }
flags := cmd.Flags() flags := cmd.Flags()

View file

@ -1,8 +1,6 @@
package node package node
import ( import (
"fmt"
"github.com/docker/docker/cli" "github.com/docker/docker/cli"
"github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command"
apiclient "github.com/docker/docker/client" apiclient "github.com/docker/docker/client"
@ -17,7 +15,8 @@ func NewNodeCommand(dockerCli *command.DockerCli) *cobra.Command {
Short: "Manage Swarm nodes", Short: "Manage Swarm nodes",
Args: cli.NoArgs, Args: cli.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) cmd.SetOutput(dockerCli.Err())
cmd.HelpFunc()(cmd, args)
}, },
} }
cmd.AddCommand( cmd.AddCommand(

View file

@ -1,8 +1,6 @@
package plugin package plugin
import ( import (
"fmt"
"github.com/docker/docker/cli" "github.com/docker/docker/cli"
"github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -15,7 +13,8 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command {
Short: "Manage plugins", Short: "Manage plugins",
Args: cli.NoArgs, Args: cli.NoArgs,
Run: func(cmd *cobra.Command, args []string) { 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": ""},
} }

View file

@ -1,8 +1,6 @@
package service package service
import ( import (
"fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/docker/cli" "github.com/docker/docker/cli"
@ -16,7 +14,8 @@ func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command {
Short: "Manage services", Short: "Manage services",
Args: cli.NoArgs, Args: cli.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) cmd.SetOutput(dockerCli.Err())
cmd.HelpFunc()(cmd, args)
}, },
} }
cmd.AddCommand( cmd.AddCommand(

View file

@ -1,8 +1,6 @@
package stack package stack
import ( import (
"fmt"
"github.com/docker/docker/cli" "github.com/docker/docker/cli"
"github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -15,9 +13,10 @@ func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command {
Short: "Manage Docker stacks", Short: "Manage Docker stacks",
Args: cli.NoArgs, Args: cli.NoArgs,
Run: func(cmd *cobra.Command, args []string) { 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( cmd.AddCommand(
newConfigCommand(dockerCli), newConfigCommand(dockerCli),

View file

@ -36,7 +36,7 @@ func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command {
opts.namespace = strings.TrimSuffix(args[0], ".dab") opts.namespace = strings.TrimSuffix(args[0], ".dab")
return runDeploy(dockerCli, opts) return runDeploy(dockerCli, opts)
}, },
Tags: map[string]string{"experimental": ""}, Tags: map[string]string{"experimental": "", "version": "1.25"},
} }
flags := cmd.Flags() flags := cmd.Flags()

View file

@ -1,8 +1,6 @@
package swarm package swarm
import ( import (
"fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/docker/cli" "github.com/docker/docker/cli"
@ -16,7 +14,8 @@ func NewSwarmCommand(dockerCli *command.DockerCli) *cobra.Command {
Short: "Manage Swarm", Short: "Manage Swarm",
Args: cli.NoArgs, Args: cli.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) cmd.SetOutput(dockerCli.Err())
cmd.HelpFunc()(cmd, args)
}, },
} }
cmd.AddCommand( cmd.AddCommand(

View file

@ -1,8 +1,6 @@
package system package system
import ( import (
"fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/docker/cli" "github.com/docker/docker/cli"
@ -16,7 +14,8 @@ func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command {
Short: "Manage Docker", Short: "Manage Docker",
Args: cli.NoArgs, Args: cli.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) cmd.SetOutput(dockerCli.Err())
cmd.HelpFunc()(cmd, args)
}, },
} }
cmd.AddCommand( cmd.AddCommand(
@ -25,5 +24,6 @@ func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command {
NewDiskUsageCommand(dockerCli), NewDiskUsageCommand(dockerCli),
NewPruneCommand(dockerCli), NewPruneCommand(dockerCli),
) )
return cmd return cmd
} }

View file

@ -23,6 +23,7 @@ func NewDiskUsageCommand(dockerCli *command.DockerCli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runDiskUsage(dockerCli, opts) return runDiskUsage(dockerCli, opts)
}, },
Tags: map[string]string{"version": "1.25"},
} }
flags := cmd.Flags() flags := cmd.Flags()

View file

@ -26,6 +26,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runPrune(dockerCli, opts) return runPrune(dockerCli, opts)
}, },
Tags: map[string]string{"version": "1.25"},
} }
flags := cmd.Flags() flags := cmd.Flags()

View file

@ -1,6 +1,7 @@
package system package system
import ( import (
"fmt"
"runtime" "runtime"
"time" "time"
@ -70,10 +71,15 @@ func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error {
Status: "Template parsing error: " + err.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{ vd := types.VersionResponse{
Client: &types.Version{ Client: &types.Version{
Version: dockerversion.Version, Version: dockerversion.Version,
APIVersion: dockerCli.Client().ClientVersion(), APIVersion: APIVersion,
GoVersion: runtime.Version(), GoVersion: runtime.Version(),
GitCommit: dockerversion.GitCommit, GitCommit: dockerversion.GitCommit,
BuildTime: dockerversion.BuildTime, BuildTime: dockerversion.BuildTime,

View file

@ -1,8 +1,6 @@
package volume package volume
import ( import (
"fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/docker/cli" "github.com/docker/docker/cli"
@ -17,7 +15,8 @@ func NewVolumeCommand(dockerCli *command.DockerCli) *cobra.Command {
Long: volumeDescription, Long: volumeDescription,
Args: cli.NoArgs, Args: cli.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) cmd.SetOutput(dockerCli.Err())
cmd.HelpFunc()(cmd, args)
}, },
} }
cmd.AddCommand( cmd.AddCommand(

View file

@ -35,6 +35,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
return nil return nil
}, },
Tags: map[string]string{"version": "1.25"},
} }
flags := cmd.Flags() flags := cmd.Flags()

View file

@ -34,7 +34,7 @@ func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVarP(&opts.force, "force", "f", false, "Force the removal of one or more volumes") flags.BoolVarP(&opts.force, "force", "f", false, "Force the removal of one or more volumes")
flags.SetAnnotation("force", "version", []string{"1.25"})
return cmd return cmd
} }

View file

@ -79,6 +79,8 @@ type Client struct {
version string version string
// custom http headers configured by users. // custom http headers configured by users.
customHTTPHeaders map[string]string 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. // NewEnvClient initializes a new API client based on environment variables.
@ -111,13 +113,19 @@ func NewEnvClient() (*Client, error) {
if host == "" { if host == "" {
host = DefaultDockerHost host = DefaultDockerHost
} }
version := os.Getenv("DOCKER_API_VERSION") version := os.Getenv("DOCKER_API_VERSION")
if version == "" { if version == "" {
version = DefaultVersion 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. // 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 // UpdateClientVersion updates the version string associated with this
// instance of the Client. // instance of the Client.
func (cli *Client) UpdateClientVersion(v string) { func (cli *Client) UpdateClientVersion(v string) {
cli.version = v if !cli.manualOverride {
cli.version = v
}
} }
// ParseHost verifies that the given host strings is valid. // ParseHost verifies that the given host strings is valid.

View file

@ -20,6 +20,11 @@ type configWrapper struct {
// It can be associated with a name, but it's not mandatory. // 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) { 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 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{} query := url.Values{}
if containerName != "" { if containerName != "" {
query.Set("name", containerName) query.Set("name", containerName)

View file

@ -10,6 +10,11 @@ import (
// ContainerExecCreate creates a new exec configuration to run an exec process. // 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) { func (cli *Client) ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.IDResponse, error) {
var response types.IDResponse 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) resp, err := cli.post(ctx, "/containers/"+container+"/exec", nil, config, nil)
if err != nil { if err != nil {
return response, err return response, err

View file

@ -12,6 +12,10 @@ import (
func (cli *Client) ContainersPrune(ctx context.Context, cfg types.ContainersPruneConfig) (types.ContainersPruneReport, error) { func (cli *Client) ContainersPrune(ctx context.Context, cfg types.ContainersPruneConfig) (types.ContainersPruneReport, error) {
var report types.ContainersPruneReport 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) serverResp, err := cli.post(ctx, "/containers/prune", nil, cfg, nil)
if err != nil { if err != nil {
return report, err return report, err

View file

@ -3,6 +3,8 @@ package client
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/docker/docker/api/types/versions"
) )
// ErrConnectionFailed is an error raised when the connection between the client and the server failed. // 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) _, ok := err.(pluginPermissionDenied)
return ok 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
}

View file

@ -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 // The Body in the response implement an io.ReadCloser and it's up to the caller to
// close it. // close it.
func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { 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 { if err != nil {
return types.ImageBuildResponse{}, err return types.ImageBuildResponse{}, err
} }
@ -47,7 +47,7 @@ func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, optio
}, nil }, nil
} }
func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) { func (cli *Client) imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) {
query := url.Values{ query := url.Values{
"t": options.Tags, "t": options.Tags,
"securityopt": options.SecurityOpt, "securityopt": options.SecurityOpt,
@ -76,6 +76,9 @@ func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, erro
} }
if options.Squash { if options.Squash {
if err := cli.NewVersionError("1.25", "squash"); err != nil {
return query, err
}
query.Set("squash", "1") query.Set("squash", "1")
} }

View file

@ -12,6 +12,10 @@ import (
func (cli *Client) ImagesPrune(ctx context.Context, cfg types.ImagesPruneConfig) (types.ImagesPruneReport, error) { func (cli *Client) ImagesPrune(ctx context.Context, cfg types.ImagesPruneConfig) (types.ImagesPruneReport, error) {
var report types.ImagesPruneReport 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) serverResp, err := cli.post(ctx, "/images/prune", nil, cfg, nil)
if err != nil { if err != nil {
return report, err return report, err

View file

@ -129,7 +129,7 @@ type SystemAPIClient interface {
Info(ctx context.Context) (types.Info, error) Info(ctx context.Context) (types.Info, error)
RegistryLogin(ctx context.Context, auth types.AuthConfig) (registry.AuthenticateOKBody, error) RegistryLogin(ctx context.Context, auth types.AuthConfig) (registry.AuthenticateOKBody, error)
DiskUsage(ctx context.Context) (types.DiskUsage, 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 // VolumeAPIClient defines API client methods for the volumes

View file

@ -1,19 +1,30 @@
package client package client
import "golang.org/x/net/context" import (
"fmt"
// Ping pings the server and return the value of the "Docker-Experimental" header "github.com/docker/docker/api/types"
func (cli *Client) Ping(ctx context.Context) (bool, error) { "golang.org/x/net/context"
serverResp, err := cli.get(ctx, "/_ping", nil, nil) )
// 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 { if err != nil {
return false, err return ping, err
}
serverResp, err := cli.doRequest(ctx, req)
if err != nil {
return ping, err
} }
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(serverResp)
exp := serverResp.header.Get("Docker-Experimental") ping.APIVersion = serverResp.header.Get("API-Version")
if exp != "true" {
return false, nil if serverResp.header.Get("Docker-Experimental") == "true" {
ping.Experimental = true
} }
return true, nil return ping, nil
} }

View file

@ -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 // Add CLI Config's HTTP Headers BEFORE we set the Docker headers
// then the user can't change OUR headers // then the user can't change OUR headers
for k, v := range cli.customHTTPHeaders { for k, v := range cli.customHTTPHeaders {
if versions.LessThan(cli.version, "1.25") && k == "User-Agent" {
continue
}
req.Header.Set(k, v) req.Header.Set(k, v)
} }

View file

@ -12,6 +12,10 @@ import (
func (cli *Client) VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error) { func (cli *Client) VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error) {
var report types.VolumesPruneReport 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) serverResp, err := cli.post(ctx, "/volumes/prune", nil, cfg, nil)
if err != nil { if err != nil {
return report, err return report, err

View file

@ -3,14 +3,17 @@ package client
import ( import (
"net/url" "net/url"
"github.com/docker/docker/api/types/versions"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
// VolumeRemove removes a volume from the docker host. // VolumeRemove removes a volume from the docker host.
func (cli *Client) VolumeRemove(ctx context.Context, volumeID string, force bool) error { func (cli *Client) VolumeRemove(ctx context.Context, volumeID string, force bool) error {
query := url.Values{} query := url.Values{}
if force { if versions.GreaterThanOrEqualTo(cli.version, "1.25") {
query.Set("force", "1") if force {
query.Set("force", "1")
}
} }
resp, err := cli.delete(ctx, "/volumes/"+volumeID, query, nil) resp, err := cli.delete(ctx, "/volumes/"+volumeID, query, nil)
ensureReaderClosed(resp) ensureReaderClosed(resp)

View file

@ -5,6 +5,7 @@ import (
"os" "os"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/cli" "github.com/docker/docker/cli"
"github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/command/commands" "github.com/docker/docker/cli/command/commands"
@ -47,16 +48,15 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
cli.SetupRootCommand(cmd) cli.SetupRootCommand(cmd)
cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) { cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) {
var err error
if dockerCli.Client() == nil { if dockerCli.Client() == nil {
// flags must be the top-level command flags, not cmd.Flags() // flags must be the top-level command flags, not cmd.Flags()
opts.Common.SetDefaultOptions(flags) opts.Common.SetDefaultOptions(flags)
dockerPreRun(opts) dockerPreRun(opts)
err = dockerCli.Initialize(opts) dockerCli.Initialize(opts)
}
if err != nil || !dockerCli.HasExperimental() {
hideExperimentalFeatures(ccmd)
} }
hideUnsupportedFeatures(ccmd, dockerCli.Client().ClientVersion(), dockerCli.HasExperimental())
if err := ccmd.Help(); err != nil { if err := ccmd.Help(); err != nil {
ccmd.Println(err) ccmd.Println(err)
} }
@ -123,18 +123,29 @@ func dockerPreRun(opts *cliflags.ClientOptions) {
} }
} }
func hideExperimentalFeatures(cmd *cobra.Command) { func hideUnsupportedFeatures(cmd *cobra.Command, clientVersion string, hasExperimental bool) {
// hide flags
cmd.Flags().VisitAll(func(f *pflag.Flag) { cmd.Flags().VisitAll(func(f *pflag.Flag) {
// hide experimental flags
if _, ok := f.Annotations["experimental"]; ok { if _, ok := f.Annotations["experimental"]; ok {
f.Hidden = true 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() { for _, subcmd := range cmd.Commands() {
// hide subcommands // hide experimental subcommands
if _, ok := subcmd.Tags["experimental"]; ok { if _, ok := subcmd.Tags["experimental"]; ok {
subcmd.Hidden = true subcmd.Hidden = true
} }
// hide subcommands not supported by the server
if subcmdVersion, ok := subcmd.Tags["version"]; ok && versions.LessThan(clientVersion, subcmdVersion) {
subcmd.Hidden = true
}
} }
} }

View file

@ -480,9 +480,6 @@ func (cli *DaemonCli) initMiddlewares(s *apiserver.Server, cfg *apiserver.Config
s.UseMiddleware(c) s.UseMiddleware(c)
} }
u := middleware.NewUserAgentMiddleware(v)
s.UseMiddleware(u)
if err := validateAuthzPlugins(cli.Config.AuthorizationPlugins, cli.d.PluginStore); err != nil { if err := validateAuthzPlugins(cli.Config.AuthorizationPlugins, cli.d.PluginStore); err != nil {
return fmt.Errorf("Error validating authorization plugin: %v", err) return fmt.Errorf("Error validating authorization plugin: %v", err)
} }

View file

@ -162,6 +162,7 @@ This section lists each version from latest to oldest. Each listing includes a
* `POST /volumes/prune` prunes unused volumes. * `POST /volumes/prune` prunes unused volumes.
* `POST /networks/prune` prunes unused networks. * `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 `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 `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. * 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. * `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.

View file

@ -15,7 +15,7 @@ keywords: "API, Docker, rcli, REST, documentation"
# Docker Remote API v1.18 # Docker Remote API v1.18
## 1. Brief introduction # 1. Brief introduction
- The Remote API has replaced `rcli`. - The Remote API has replaced `rcli`.
- The daemon listens on `unix:///var/run/docker.sock` but you can - 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` - The API tends to be REST, but for some complex commands, like `attach`
or `pull`, the HTTP connection is hijacked to transport `STDOUT`, or `pull`, the HTTP connection is hijacked to transport `STDOUT`,
`STDIN` and `STDERR`. `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 # 2. Endpoints

View file

@ -15,7 +15,7 @@ keywords: "API, Docker, rcli, REST, documentation"
# Docker Remote API v1.20 # Docker Remote API v1.20
## 1. Brief introduction # 1. Brief introduction
- The Remote API has replaced `rcli`. - The Remote API has replaced `rcli`.
- The daemon listens on `unix:///var/run/docker.sock` but you can - 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` - The API tends to be REST. However, for some complex commands, like `attach`
or `pull`, the HTTP connection is hijacked to transport `stdout`, or `pull`, the HTTP connection is hijacked to transport `stdout`,
`stdin` and `stderr`. `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 # 2. Endpoints

View file

@ -15,7 +15,7 @@ keywords: "API, Docker, rcli, REST, documentation"
# Docker Remote API v1.22 # Docker Remote API v1.22
## 1. Brief introduction # 1. Brief introduction
- The Remote API has replaced `rcli`. - The Remote API has replaced `rcli`.
- The daemon listens on `unix:///var/run/docker.sock` but you can - 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` - The API tends to be REST. However, for some complex commands, like `attach`
or `pull`, the HTTP connection is hijacked to transport `stdout`, or `pull`, the HTTP connection is hijacked to transport `stdout`,
`stdin` and `stderr`. `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 # 2. Endpoints

View file

@ -23,8 +23,6 @@ keywords: "API, Docker, rcli, REST, documentation"
- The API tends to be REST. However, for some complex commands, like `attach` - The API tends to be REST. However, for some complex commands, like `attach`
or `pull`, the HTTP connection is hijacked to transport `stdout`, or `pull`, the HTTP connection is hijacked to transport `stdout`,
`stdin` and `stderr`. `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 # 2. Errors

View file

@ -23,8 +23,6 @@ keywords: "API, Docker, rcli, REST, documentation"
- The API tends to be REST. However, for some complex commands, like `attach` - The API tends to be REST. However, for some complex commands, like `attach`
or `pull`, the HTTP connection is hijacked to transport `stdout`, or `pull`, the HTTP connection is hijacked to transport `stdout`,
`stdin` and `stderr`. `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 # 2. Errors

View file

@ -4,11 +4,9 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/http/httputil"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/docker/docker/api" "github.com/docker/docker/api"
"github.com/docker/docker/pkg/integration/checker" "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") //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) { func (s *DockerSuite) TestAPIClientVersionOldNotSupported(c *check.C) {
if daemonPlatform != runtime.GOOS { if daemonPlatform != runtime.GOOS {
c.Skip("Daemon platform doesn't match test platform") 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( server := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("API-Version", api.DefaultVersion)
url := r.URL.Path url := r.URL.Path
svrVersion = url svrVersion = url
})) }))

View file

@ -9,6 +9,7 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"github.com/docker/docker/api"
"github.com/docker/docker/dockerversion" "github.com/docker/docker/dockerversion"
"github.com/docker/docker/pkg/homedir" "github.com/docker/docker/pkg/homedir"
"github.com/docker/docker/pkg/integration/checker" "github.com/docker/docker/pkg/integration/checker"
@ -25,6 +26,7 @@ func (s *DockerSuite) TestConfigHTTPHeader(c *check.C) {
server := httptest.NewServer(http.HandlerFunc( server := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("API-Version", api.DefaultVersion)
headers = r.Header headers = r.Header
})) }))
defer server.Close() defer server.Close()

View file

@ -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.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.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.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.Var(copts.sysctls, "sysctl", "Sysctl options")
flags.BoolVarP(&copts.tty, "tty", "t", false, "Allocate a pseudo-TTY") flags.BoolVarP(&copts.tty, "tty", "t", false, "Allocate a pseudo-TTY")
flags.Var(copts.ulimits, "ulimit", "Ulimit options") flags.Var(copts.ulimits, "ulimit", "Ulimit options")