瀏覽代碼

client: negotiate api version before handling version-specific code

We try to perform API-version negotiation as lazy as possible (and only execute
when we are about to make an API request). However, some code requires API-version
dependent handling (to set options, or remove options based on the version of the
API we're using).

Currently this code depended on the caller code to perform API negotiation (or
to configure the API version) first, which may not happen, and because of that
we may be missing options (or set options that are not supported on older API
versions).

This patch:

- splits the code that triggered API-version negotiation to a separate
  Client.checkVersion() function.
- updates NewVersionError to accept a context
- updates NewVersionError to perform API-version negotiation (if enabled)
- updates various Client functions to manually trigger API-version negotiation

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Sebastiaan van Stijn 1 年之前
父節點
當前提交
e6907243af

+ 1 - 1
client/build_prune.go

@@ -13,7 +13,7 @@ import (
 
 
 // BuildCachePrune requests the daemon to delete unused cache data
 // BuildCachePrune requests the daemon to delete unused cache data
 func (cli *Client) BuildCachePrune(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
 func (cli *Client) BuildCachePrune(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
-	if err := cli.NewVersionError("1.31", "build prune"); err != nil {
+	if err := cli.NewVersionError(ctx, "1.31", "build prune"); err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 

+ 11 - 3
client/client.go

@@ -254,13 +254,21 @@ func (cli *Client) Close() error {
 	return nil
 	return nil
 }
 }
 
 
+// checkVersion manually triggers API version negotiation (if configured).
+// This allows for version-dependent code to use the same version as will
+// be negotiated when making the actual requests, and for which cases
+// we cannot do the negotiation lazily.
+func (cli *Client) checkVersion(ctx context.Context) {
+	if cli.negotiateVersion && !cli.negotiated {
+		cli.NegotiateAPIVersion(ctx)
+	}
+}
+
 // getAPIPath returns the versioned request path to call the API.
 // getAPIPath returns the versioned request path to call the API.
 // It appends the query parameters to the path if they are not empty.
 // It appends the query parameters to the path if they are not empty.
 func (cli *Client) getAPIPath(ctx context.Context, p string, query url.Values) string {
 func (cli *Client) getAPIPath(ctx context.Context, p string, query url.Values) string {
 	var apiPath string
 	var apiPath string
-	if cli.negotiateVersion && !cli.negotiated {
-		cli.NegotiateAPIVersion(ctx)
-	}
+	cli.checkVersion(ctx)
 	if cli.version != "" {
 	if cli.version != "" {
 		v := strings.TrimPrefix(cli.version, "v")
 		v := strings.TrimPrefix(cli.version, "v")
 		apiPath = path.Join(cli.basePath, "/v"+v, p)
 		apiPath = path.Join(cli.basePath, "/v"+v, p)

+ 1 - 1
client/config_create.go

@@ -11,7 +11,7 @@ import (
 // ConfigCreate creates a new config.
 // ConfigCreate creates a new config.
 func (cli *Client) ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
 func (cli *Client) ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
 	var response types.ConfigCreateResponse
 	var response types.ConfigCreateResponse
-	if err := cli.NewVersionError("1.30", "config create"); err != nil {
+	if err := cli.NewVersionError(ctx, "1.30", "config create"); err != nil {
 		return response, err
 		return response, err
 	}
 	}
 	resp, err := cli.post(ctx, "/configs/create", nil, config, nil)
 	resp, err := cli.post(ctx, "/configs/create", nil, config, nil)

+ 1 - 1
client/config_inspect.go

@@ -14,7 +14,7 @@ func (cli *Client) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.C
 	if id == "" {
 	if id == "" {
 		return swarm.Config{}, nil, objectNotFoundError{object: "config", id: id}
 		return swarm.Config{}, nil, objectNotFoundError{object: "config", id: id}
 	}
 	}
-	if err := cli.NewVersionError("1.30", "config inspect"); err != nil {
+	if err := cli.NewVersionError(ctx, "1.30", "config inspect"); err != nil {
 		return swarm.Config{}, nil, err
 		return swarm.Config{}, nil, err
 	}
 	}
 	resp, err := cli.get(ctx, "/configs/"+id, nil, nil)
 	resp, err := cli.get(ctx, "/configs/"+id, nil, nil)

+ 1 - 1
client/config_list.go

@@ -12,7 +12,7 @@ import (
 
 
 // ConfigList returns the list of configs.
 // ConfigList returns the list of configs.
 func (cli *Client) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
 func (cli *Client) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
-	if err := cli.NewVersionError("1.30", "config list"); err != nil {
+	if err := cli.NewVersionError(ctx, "1.30", "config list"); err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 	query := url.Values{}
 	query := url.Values{}

+ 1 - 1
client/config_remove.go

@@ -4,7 +4,7 @@ import "context"
 
 
 // ConfigRemove removes a config.
 // ConfigRemove removes a config.
 func (cli *Client) ConfigRemove(ctx context.Context, id string) error {
 func (cli *Client) ConfigRemove(ctx context.Context, id string) error {
-	if err := cli.NewVersionError("1.30", "config remove"); err != nil {
+	if err := cli.NewVersionError(ctx, "1.30", "config remove"); err != nil {
 		return err
 		return err
 	}
 	}
 	resp, err := cli.delete(ctx, "/configs/"+id, nil, nil)
 	resp, err := cli.delete(ctx, "/configs/"+id, nil, nil)

+ 1 - 1
client/config_update.go

@@ -9,7 +9,7 @@ import (
 
 
 // ConfigUpdate attempts to update a config
 // ConfigUpdate attempts to update a config
 func (cli *Client) ConfigUpdate(ctx context.Context, id string, version swarm.Version, config swarm.ConfigSpec) error {
 func (cli *Client) ConfigUpdate(ctx context.Context, id string, version swarm.Version, config swarm.ConfigSpec) error {
-	if err := cli.NewVersionError("1.30", "config update"); err != nil {
+	if err := cli.NewVersionError(ctx, "1.30", "config update"); err != nil {
 		return err
 		return err
 	}
 	}
 	query := url.Values{}
 	query := url.Values{}

+ 10 - 3
client/container_create.go

@@ -23,13 +23,20 @@ type configWrapper struct {
 func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) {
 func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) {
 	var response container.CreateResponse
 	var response container.CreateResponse
 
 
-	if err := cli.NewVersionError("1.25", "stop timeout"); config != nil && config.StopTimeout != nil && err != nil {
+	// Make sure we negotiated (if the client is configured to do so),
+	// as code below contains API-version specific handling of options.
+	//
+	// Normally, version-negotiation (if enabled) would not happen until
+	// the API request is made.
+	cli.checkVersion(ctx)
+
+	if err := cli.NewVersionError(ctx, "1.25", "stop timeout"); config != nil && config.StopTimeout != nil && err != nil {
 		return response, err
 		return response, err
 	}
 	}
-	if err := cli.NewVersionError("1.41", "specify container image platform"); platform != nil && err != nil {
+	if err := cli.NewVersionError(ctx, "1.41", "specify container image platform"); platform != nil && err != nil {
 		return response, err
 		return response, err
 	}
 	}
-	if err := cli.NewVersionError("1.44", "specify health-check start interval"); config != nil && config.Healthcheck != nil && config.Healthcheck.StartInterval != 0 && err != nil {
+	if err := cli.NewVersionError(ctx, "1.44", "specify health-check start interval"); config != nil && config.Healthcheck != nil && config.Healthcheck.StartInterval != 0 && err != nil {
 		return response, err
 		return response, err
 	}
 	}
 
 

+ 8 - 1
client/container_exec.go

@@ -13,7 +13,14 @@ import (
 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 {
+	// Make sure we negotiated (if the client is configured to do so),
+	// as code below contains API-version specific handling of options.
+	//
+	// Normally, version-negotiation (if enabled) would not happen until
+	// the API request is made.
+	cli.checkVersion(ctx)
+
+	if err := cli.NewVersionError(ctx, "1.25", "env"); len(config.Env) != 0 && err != nil {
 		return response, err
 		return response, err
 	}
 	}
 	if versions.LessThan(cli.ClientVersion(), "1.42") {
 	if versions.LessThan(cli.ClientVersion(), "1.42") {

+ 1 - 1
client/container_prune.go

@@ -13,7 +13,7 @@ import (
 func (cli *Client) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) {
 func (cli *Client) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) {
 	var report types.ContainersPruneReport
 	var report types.ContainersPruneReport
 
 
-	if err := cli.NewVersionError("1.25", "container prune"); err != nil {
+	if err := cli.NewVersionError(ctx, "1.25", "container prune"); err != nil {
 		return report, err
 		return report, err
 	}
 	}
 
 

+ 10 - 2
client/container_restart.go

@@ -17,8 +17,16 @@ func (cli *Client) ContainerRestart(ctx context.Context, containerID string, opt
 	if options.Timeout != nil {
 	if options.Timeout != nil {
 		query.Set("t", strconv.Itoa(*options.Timeout))
 		query.Set("t", strconv.Itoa(*options.Timeout))
 	}
 	}
-	if options.Signal != "" && versions.GreaterThanOrEqualTo(cli.version, "1.42") {
-		query.Set("signal", options.Signal)
+	if options.Signal != "" {
+		// Make sure we negotiated (if the client is configured to do so),
+		// as code below contains API-version specific handling of options.
+		//
+		// Normally, version-negotiation (if enabled) would not happen until
+		// the API request is made.
+		cli.checkVersion(ctx)
+		if versions.GreaterThanOrEqualTo(cli.version, "1.42") {
+			query.Set("signal", options.Signal)
+		}
 	}
 	}
 	resp, err := cli.post(ctx, "/containers/"+containerID+"/restart", query, nil, nil)
 	resp, err := cli.post(ctx, "/containers/"+containerID+"/restart", query, nil, nil)
 	ensureReaderClosed(resp)
 	ensureReaderClosed(resp)

+ 10 - 2
client/container_stop.go

@@ -21,8 +21,16 @@ func (cli *Client) ContainerStop(ctx context.Context, containerID string, option
 	if options.Timeout != nil {
 	if options.Timeout != nil {
 		query.Set("t", strconv.Itoa(*options.Timeout))
 		query.Set("t", strconv.Itoa(*options.Timeout))
 	}
 	}
-	if options.Signal != "" && versions.GreaterThanOrEqualTo(cli.version, "1.42") {
-		query.Set("signal", options.Signal)
+	if options.Signal != "" {
+		// Make sure we negotiated (if the client is configured to do so),
+		// as code below contains API-version specific handling of options.
+		//
+		// Normally, version-negotiation (if enabled) would not happen until
+		// the API request is made.
+		cli.checkVersion(ctx)
+		if versions.GreaterThanOrEqualTo(cli.version, "1.42") {
+			query.Set("signal", options.Signal)
+		}
 	}
 	}
 	resp, err := cli.post(ctx, "/containers/"+containerID+"/stop", query, nil, nil)
 	resp, err := cli.post(ctx, "/containers/"+containerID+"/stop", query, nil, nil)
 	ensureReaderClosed(resp)
 	ensureReaderClosed(resp)

+ 6 - 0
client/container_wait.go

@@ -30,6 +30,12 @@ const containerWaitErrorMsgLimit = 2 * 1024 /* Max: 2KiB */
 // synchronize ContainerWait with other calls, such as specifying a
 // synchronize ContainerWait with other calls, such as specifying a
 // "next-exit" condition before issuing a ContainerStart request.
 // "next-exit" condition before issuing a ContainerStart request.
 func (cli *Client) ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
 func (cli *Client) ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
+	// Make sure we negotiated (if the client is configured to do so),
+	// as code below contains API-version specific handling of options.
+	//
+	// Normally, version-negotiation (if enabled) would not happen until
+	// the API request is made.
+	cli.checkVersion(ctx)
 	if versions.LessThan(cli.ClientVersion(), "1.30") {
 	if versions.LessThan(cli.ClientVersion(), "1.30") {
 		return cli.legacyContainerWait(ctx, containerID)
 		return cli.legacyContainerWait(ctx, containerID)
 	}
 	}

+ 1 - 1
client/distribution_inspect.go

@@ -17,7 +17,7 @@ func (cli *Client) DistributionInspect(ctx context.Context, image, encodedRegist
 		return distributionInspect, objectNotFoundError{object: "distribution", id: image}
 		return distributionInspect, objectNotFoundError{object: "distribution", id: image}
 	}
 	}
 
 
-	if err := cli.NewVersionError("1.30", "distribution inspect"); err != nil {
+	if err := cli.NewVersionError(ctx, "1.30", "distribution inspect"); err != nil {
 		return distributionInspect, err
 		return distributionInspect, err
 	}
 	}
 
 

+ 13 - 3
client/errors.go

@@ -1,6 +1,7 @@
 package client // import "github.com/docker/docker/client"
 package client // import "github.com/docker/docker/client"
 
 
 import (
 import (
+	"context"
 	"fmt"
 	"fmt"
 
 
 	"github.com/docker/docker/api/types/versions"
 	"github.com/docker/docker/api/types/versions"
@@ -48,9 +49,18 @@ func (e objectNotFoundError) Error() string {
 	return fmt.Sprintf("Error: No such %s: %s", e.object, e.id)
 	return fmt.Sprintf("Error: No such %s: %s", e.object, e.id)
 }
 }
 
 
-// NewVersionError returns an error if the APIVersion required
-// if less than the current supported version
-func (cli *Client) NewVersionError(APIrequired, feature string) error {
+// NewVersionError returns an error if the APIVersion required is less than the
+// current supported version.
+//
+// It performs API-version negotiation if the Client is configured with this
+// option, otherwise it assumes the latest API version is used.
+func (cli *Client) NewVersionError(ctx context.Context, APIrequired, feature string) error {
+	// Make sure we negotiated (if the client is configured to do so),
+	// as code below contains API-version specific handling of options.
+	//
+	// Normally, version-negotiation (if enabled) would not happen until
+	// the API request is made.
+	cli.checkVersion(ctx)
 	if cli.version != "" && versions.LessThan(cli.version, APIrequired) {
 	if cli.version != "" && versions.LessThan(cli.version, APIrequired) {
 		return fmt.Errorf("%q requires API version %s, but the Docker daemon API version is %s", feature, APIrequired, cli.version)
 		return fmt.Errorf("%q requires API version %s, but the Docker daemon API version is %s", feature, APIrequired, cli.version)
 	}
 	}

+ 4 - 4
client/image_build.go

@@ -18,7 +18,7 @@ import (
 // The Body in the response implements an io.ReadCloser and it's up to the caller to
 // The Body in the response implements 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 := cli.imageBuildOptionsToQuery(options)
+	query, err := cli.imageBuildOptionsToQuery(ctx, options)
 	if err != nil {
 	if err != nil {
 		return types.ImageBuildResponse{}, err
 		return types.ImageBuildResponse{}, err
 	}
 	}
@@ -43,7 +43,7 @@ func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, optio
 	}, nil
 	}, nil
 }
 }
 
 
-func (cli *Client) imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) {
+func (cli *Client) imageBuildOptionsToQuery(ctx context.Context, options types.ImageBuildOptions) (url.Values, error) {
 	query := url.Values{
 	query := url.Values{
 		"t":           options.Tags,
 		"t":           options.Tags,
 		"securityopt": options.SecurityOpt,
 		"securityopt": options.SecurityOpt,
@@ -73,7 +73,7 @@ func (cli *Client) imageBuildOptionsToQuery(options types.ImageBuildOptions) (ur
 	}
 	}
 
 
 	if options.Squash {
 	if options.Squash {
-		if err := cli.NewVersionError("1.25", "squash"); err != nil {
+		if err := cli.NewVersionError(ctx, "1.25", "squash"); err != nil {
 			return query, err
 			return query, err
 		}
 		}
 		query.Set("squash", "1")
 		query.Set("squash", "1")
@@ -123,7 +123,7 @@ func (cli *Client) imageBuildOptionsToQuery(options types.ImageBuildOptions) (ur
 		query.Set("session", options.SessionID)
 		query.Set("session", options.SessionID)
 	}
 	}
 	if options.Platform != "" {
 	if options.Platform != "" {
-		if err := cli.NewVersionError("1.32", "platform"); err != nil {
+		if err := cli.NewVersionError(ctx, "1.32", "platform"); err != nil {
 			return query, err
 			return query, err
 		}
 		}
 		query.Set("platform", strings.ToLower(options.Platform))
 		query.Set("platform", strings.ToLower(options.Platform))

+ 7 - 0
client/image_list.go

@@ -12,6 +12,13 @@ import (
 
 
 // ImageList returns a list of images in the docker host.
 // ImageList returns a list of images in the docker host.
 func (cli *Client) ImageList(ctx context.Context, options types.ImageListOptions) ([]types.ImageSummary, error) {
 func (cli *Client) ImageList(ctx context.Context, options types.ImageListOptions) ([]types.ImageSummary, error) {
+	// Make sure we negotiated (if the client is configured to do so),
+	// as code below contains API-version specific handling of options.
+	//
+	// Normally, version-negotiation (if enabled) would not happen until
+	// the API request is made.
+	cli.checkVersion(ctx)
+
 	var images []types.ImageSummary
 	var images []types.ImageSummary
 	query := url.Values{}
 	query := url.Values{}
 
 

+ 1 - 1
client/image_prune.go

@@ -13,7 +13,7 @@ import (
 func (cli *Client) ImagesPrune(ctx context.Context, pruneFilters filters.Args) (types.ImagesPruneReport, error) {
 func (cli *Client) ImagesPrune(ctx context.Context, pruneFilters filters.Args) (types.ImagesPruneReport, error) {
 	var report types.ImagesPruneReport
 	var report types.ImagesPruneReport
 
 
-	if err := cli.NewVersionError("1.25", "image prune"); err != nil {
+	if err := cli.NewVersionError(ctx, "1.25", "image prune"); err != nil {
 		return report, err
 		return report, err
 	}
 	}
 
 

+ 7 - 0
client/network_create.go

@@ -10,6 +10,13 @@ import (
 
 
 // NetworkCreate creates a new network in the docker host.
 // NetworkCreate creates a new network in the docker host.
 func (cli *Client) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) {
 func (cli *Client) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) {
+	// Make sure we negotiated (if the client is configured to do so),
+	// as code below contains API-version specific handling of options.
+	//
+	// Normally, version-negotiation (if enabled) would not happen until
+	// the API request is made.
+	cli.checkVersion(ctx)
+
 	networkCreateRequest := types.NetworkCreateRequest{
 	networkCreateRequest := types.NetworkCreateRequest{
 		NetworkCreate: options,
 		NetworkCreate: options,
 		Name:          name,
 		Name:          name,

+ 1 - 1
client/network_prune.go

@@ -13,7 +13,7 @@ import (
 func (cli *Client) NetworksPrune(ctx context.Context, pruneFilters filters.Args) (types.NetworksPruneReport, error) {
 func (cli *Client) NetworksPrune(ctx context.Context, pruneFilters filters.Args) (types.NetworksPruneReport, error) {
 	var report types.NetworksPruneReport
 	var report types.NetworksPruneReport
 
 
-	if err := cli.NewVersionError("1.25", "network prune"); err != nil {
+	if err := cli.NewVersionError(ctx, "1.25", "network prune"); err != nil {
 		return report, err
 		return report, err
 	}
 	}
 
 

+ 1 - 1
client/plugin_upgrade.go

@@ -14,7 +14,7 @@ import (
 
 
 // PluginUpgrade upgrades a plugin
 // PluginUpgrade upgrades a plugin
 func (cli *Client) PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (rc io.ReadCloser, err error) {
 func (cli *Client) PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (rc io.ReadCloser, err error) {
-	if err := cli.NewVersionError("1.26", "plugin upgrade"); err != nil {
+	if err := cli.NewVersionError(ctx, "1.26", "plugin upgrade"); err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 	query := url.Values{}
 	query := url.Values{}

+ 1 - 1
client/secret_create.go

@@ -11,7 +11,7 @@ import (
 // SecretCreate creates a new secret.
 // SecretCreate creates a new secret.
 func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) {
 func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) {
 	var response types.SecretCreateResponse
 	var response types.SecretCreateResponse
-	if err := cli.NewVersionError("1.25", "secret create"); err != nil {
+	if err := cli.NewVersionError(ctx, "1.25", "secret create"); err != nil {
 		return response, err
 		return response, err
 	}
 	}
 	resp, err := cli.post(ctx, "/secrets/create", nil, secret, nil)
 	resp, err := cli.post(ctx, "/secrets/create", nil, secret, nil)

+ 1 - 1
client/secret_inspect.go

@@ -11,7 +11,7 @@ import (
 
 
 // SecretInspectWithRaw returns the secret information with raw data
 // SecretInspectWithRaw returns the secret information with raw data
 func (cli *Client) SecretInspectWithRaw(ctx context.Context, id string) (swarm.Secret, []byte, error) {
 func (cli *Client) SecretInspectWithRaw(ctx context.Context, id string) (swarm.Secret, []byte, error) {
-	if err := cli.NewVersionError("1.25", "secret inspect"); err != nil {
+	if err := cli.NewVersionError(ctx, "1.25", "secret inspect"); err != nil {
 		return swarm.Secret{}, nil, err
 		return swarm.Secret{}, nil, err
 	}
 	}
 	if id == "" {
 	if id == "" {

+ 1 - 1
client/secret_list.go

@@ -12,7 +12,7 @@ import (
 
 
 // SecretList returns the list of secrets.
 // SecretList returns the list of secrets.
 func (cli *Client) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
 func (cli *Client) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
-	if err := cli.NewVersionError("1.25", "secret list"); err != nil {
+	if err := cli.NewVersionError(ctx, "1.25", "secret list"); err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 	query := url.Values{}
 	query := url.Values{}

+ 1 - 1
client/secret_remove.go

@@ -4,7 +4,7 @@ import "context"
 
 
 // SecretRemove removes a secret.
 // SecretRemove removes a secret.
 func (cli *Client) SecretRemove(ctx context.Context, id string) error {
 func (cli *Client) SecretRemove(ctx context.Context, id string) error {
-	if err := cli.NewVersionError("1.25", "secret remove"); err != nil {
+	if err := cli.NewVersionError(ctx, "1.25", "secret remove"); err != nil {
 		return err
 		return err
 	}
 	}
 	resp, err := cli.delete(ctx, "/secrets/"+id, nil, nil)
 	resp, err := cli.delete(ctx, "/secrets/"+id, nil, nil)

+ 1 - 1
client/secret_update.go

@@ -9,7 +9,7 @@ import (
 
 
 // SecretUpdate attempts to update a secret.
 // SecretUpdate attempts to update a secret.
 func (cli *Client) SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error {
 func (cli *Client) SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error {
-	if err := cli.NewVersionError("1.25", "secret update"); err != nil {
+	if err := cli.NewVersionError(ctx, "1.25", "secret update"); err != nil {
 		return err
 		return err
 	}
 	}
 	query := url.Values{}
 	query := url.Values{}

+ 7 - 0
client/service_create.go

@@ -20,6 +20,13 @@ import (
 func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options types.ServiceCreateOptions) (types.ServiceCreateResponse, error) {
 func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options types.ServiceCreateOptions) (types.ServiceCreateResponse, error) {
 	var response types.ServiceCreateResponse
 	var response types.ServiceCreateResponse
 
 
+	// Make sure we negotiated (if the client is configured to do so),
+	// as code below contains API-version specific handling of options.
+	//
+	// Normally, version-negotiation (if enabled) would not happen until
+	// the API request is made.
+	cli.checkVersion(ctx)
+
 	// Make sure containerSpec is not nil when no runtime is set or the runtime is set to container
 	// Make sure containerSpec is not nil when no runtime is set or the runtime is set to container
 	if service.TaskTemplate.ContainerSpec == nil && (service.TaskTemplate.Runtime == "" || service.TaskTemplate.Runtime == swarm.RuntimeContainer) {
 	if service.TaskTemplate.ContainerSpec == nil && (service.TaskTemplate.Runtime == "" || service.TaskTemplate.Runtime == swarm.RuntimeContainer) {
 		service.TaskTemplate.ContainerSpec = &swarm.ContainerSpec{}
 		service.TaskTemplate.ContainerSpec = &swarm.ContainerSpec{}

+ 7 - 0
client/service_update.go

@@ -16,6 +16,13 @@ import (
 // It should be the value as set *before* the update. You can find this value in the Meta field
 // It should be the value as set *before* the update. You can find this value in the Meta field
 // of swarm.Service, which can be found using ServiceInspectWithRaw.
 // of swarm.Service, which can be found using ServiceInspectWithRaw.
 func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) {
 func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) {
+	// Make sure we negotiated (if the client is configured to do so),
+	// as code below contains API-version specific handling of options.
+	//
+	// Normally, version-negotiation (if enabled) would not happen until
+	// the API request is made.
+	cli.checkVersion(ctx)
+
 	var (
 	var (
 		query    = url.Values{}
 		query    = url.Values{}
 		response = types.ServiceUpdateResponse{}
 		response = types.ServiceUpdateResponse{}

+ 1 - 1
client/volume_prune.go

@@ -13,7 +13,7 @@ import (
 func (cli *Client) VolumesPrune(ctx context.Context, pruneFilters filters.Args) (types.VolumesPruneReport, error) {
 func (cli *Client) VolumesPrune(ctx context.Context, pruneFilters filters.Args) (types.VolumesPruneReport, error) {
 	var report types.VolumesPruneReport
 	var report types.VolumesPruneReport
 
 
-	if err := cli.NewVersionError("1.25", "volume prune"); err != nil {
+	if err := cli.NewVersionError(ctx, "1.25", "volume prune"); err != nil {
 		return report, err
 		return report, err
 	}
 	}
 
 

+ 8 - 2
client/volume_remove.go

@@ -10,8 +10,14 @@ import (
 // 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 versions.GreaterThanOrEqualTo(cli.version, "1.25") {
-		if force {
+	if force {
+		// Make sure we negotiated (if the client is configured to do so),
+		// as code below contains API-version specific handling of options.
+		//
+		// Normally, version-negotiation (if enabled) would not happen until
+		// the API request is made.
+		cli.checkVersion(ctx)
+		if versions.GreaterThanOrEqualTo(cli.version, "1.25") {
 			query.Set("force", "1")
 			query.Set("force", "1")
 		}
 		}
 	}
 	}

+ 1 - 1
client/volume_update.go

@@ -11,7 +11,7 @@ import (
 // VolumeUpdate updates a volume. This only works for Cluster Volumes, and
 // VolumeUpdate updates a volume. This only works for Cluster Volumes, and
 // only some fields can be updated.
 // only some fields can be updated.
 func (cli *Client) VolumeUpdate(ctx context.Context, volumeID string, version swarm.Version, options volume.UpdateOptions) error {
 func (cli *Client) VolumeUpdate(ctx context.Context, volumeID string, version swarm.Version, options volume.UpdateOptions) error {
-	if err := cli.NewVersionError("1.42", "volume update"); err != nil {
+	if err := cli.NewVersionError(ctx, "1.42", "volume update"); err != nil {
 		return err
 		return err
 	}
 	}