Merge pull request #6551 from thaJeztah/remove_legacy_api_versions

remove API-version compatibility for API < v1.44
This commit is contained in:
Sebastiaan van Stijn
2025-10-10 22:53:01 +02:00
committed by GitHub
33 changed files with 150 additions and 540 deletions

View File

@ -12,7 +12,6 @@ import (
"github.com/docker/cli/internal/prompt"
"github.com/docker/cli/opts"
"github.com/docker/go-units"
"github.com/moby/moby/api/types/versions"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
@ -112,17 +111,9 @@ type cancelledErr struct{ error }
func (cancelledErr) Cancelled() {}
type errNotImplemented struct{ error }
func (errNotImplemented) NotImplemented() {}
// pruneFn prunes the build cache for use in "docker system prune" and
// returns the amount of space reclaimed and a detailed output string.
func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) {
if ver := dockerCLI.Client().ClientVersion(); ver != "" && versions.LessThan(ver, "1.31") {
// Not supported on older daemons.
return 0, "", errNotImplemented{errors.New("builder prune requires API version 1.31 or greater")}
}
if !options.Confirmed {
// Dry-run: perform validation and produce confirmation before pruning.
var confirmMsg string

View File

@ -164,7 +164,7 @@ func TestInitializeFromClient(t *testing.T) {
{
doc: "successful ping",
pingFunc: func() (types.Ping, error) {
return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil
return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.44"}, nil
},
expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"},
negotiated: true,
@ -179,7 +179,7 @@ func TestInitializeFromClient(t *testing.T) {
{
doc: "failed ping, with API version",
pingFunc: func() (types.Ping, error) {
return types.Ping{APIVersion: "v1.33"}, errors.New("failed")
return types.Ping{APIVersion: "v1.44"}, errors.New("failed")
},
expectedServer: ServerInfo{HasExperimental: true},
negotiated: true,

View File

@ -25,7 +25,6 @@ import (
"github.com/docker/cli/internal/jsonstream"
"github.com/docker/cli/opts"
"github.com/moby/moby/api/types/mount"
"github.com/moby/moby/api/types/versions"
"github.com/moby/moby/client"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/cobra"
@ -85,7 +84,8 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
flags.Bool("help", false, "Print usage")
// TODO(thaJeztah): consider adding platform as "image create option" on containerOptions
addPlatformFlag(flags, &options.platform)
flags.StringVar(&options.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable")
_ = flags.SetAnnotation("platform", "version", []string{"1.32"})
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms())
flags.BoolVar(&options.untrusted, "disable-content-trust", !trust.Enabled(), "Skip image verification")
@ -306,11 +306,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
}
var platform *ocispec.Platform
// Engine API version 1.41 first introduced the option to specify platform on
// create. It will produce an error if you try to set a platform on older API
// versions, so check the API version here to maintain backwards
// compatibility for CLI users.
if options.platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") {
if options.platform != "" {
p, err := platforms.Parse(options.platform)
if err != nil {
return "", invalidParameter(fmt.Errorf("error parsing specified platform: %w", err))

View File

@ -141,16 +141,6 @@ type containerOptions struct {
Args []string
}
// addPlatformFlag adds "--platform" to a set of flags for API version 1.32 and
// later, using the value of "DOCKER_DEFAULT_PLATFORM" (if set) as a default.
//
// It should not be used for new uses, which may have a different API version
// requirement.
func addPlatformFlag(flags *pflag.FlagSet, target *string) {
flags.StringVar(target, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable")
_ = flags.SetAnnotation("platform", "version", []string{"1.32"})
}
// addFlags adds all command line flags that will be used by parse to the FlagSet
func addFlags(flags *pflag.FlagSet) *containerOptions {
copts := &containerOptions{
@ -659,7 +649,6 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
Cmd: runCmd,
Image: copts.Image,
Volumes: volumes,
MacAddress: copts.macAddress,
Entrypoint: entrypoint,
WorkingDir: copts.workingDir,
Labels: opts.ConvertKVStringsToMap(labels),
@ -730,25 +719,17 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
config.StdinOnce = true
}
networkingConfig := &network.NetworkingConfig{
EndpointsConfig: make(map[string]*network.EndpointSettings),
}
networkingConfig.EndpointsConfig, err = parseNetworkOpts(copts)
epCfg, err := parseNetworkOpts(copts)
if err != nil {
return nil, err
}
// Put the endpoint-specific MacAddress of the "main" network attachment into the container Config for backward
// compatibility with older daemons.
if nw, ok := networkingConfig.EndpointsConfig[hostConfig.NetworkMode.NetworkName()]; ok {
config.MacAddress = nw.MacAddress //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
}
return &containerConfig{
Config: config,
HostConfig: hostConfig,
NetworkingConfig: networkingConfig,
Config: config,
HostConfig: hostConfig,
NetworkingConfig: &network.NetworkingConfig{
EndpointsConfig: epCfg,
},
}, nil
}

View File

@ -351,11 +351,7 @@ func TestParseWithMacAddress(t *testing.T) {
if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" {
t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err)
}
config, hostConfig, nwConfig := mustParse(t, validMacAddress)
if config.MacAddress != "92:d0:c6:0a:29:33" { //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
t.Fatalf("Expected the config to have '92:d0:c6:0a:29:33' as container-wide MacAddress, got '%v'",
config.MacAddress) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
}
_, hostConfig, nwConfig := mustParse(t, validMacAddress)
defaultNw := hostConfig.NetworkMode.NetworkName()
if nwConfig.EndpointsConfig[defaultNw].MacAddress != "92:d0:c6:0a:29:33" {
t.Fatalf("Expected the default endpoint to have the MacAddress '92:d0:c6:0a:29:33' set, got '%v'", nwConfig.EndpointsConfig[defaultNw].MacAddress)
@ -576,7 +572,6 @@ func TestParseNetworkConfig(t *testing.T) {
name string
flags []string
expected map[string]*networktypes.EndpointSettings
expectedCfg container.Config
expectedHostCfg container.HostConfig
expectedErr string
}{
@ -680,7 +675,6 @@ func TestParseNetworkConfig(t *testing.T) {
MacAddress: "02:32:1c:23:00:04",
},
},
expectedCfg: container.Config{MacAddress: "02:32:1c:23:00:04"},
expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
},
{
@ -698,7 +692,6 @@ func TestParseNetworkConfig(t *testing.T) {
MacAddress: "52:0f:f3:dc:50:10",
},
},
expectedCfg: container.Config{MacAddress: "52:0f:f3:dc:50:10"},
expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
},
{
@ -745,7 +738,7 @@ func TestParseNetworkConfig(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
config, hConfig, nwConfig, err := parseRun(tc.flags)
_, hConfig, nwConfig, err := parseRun(tc.flags)
if tc.expectedErr != "" {
assert.Error(t, err, tc.expectedErr)
@ -753,7 +746,6 @@ func TestParseNetworkConfig(t *testing.T) {
}
assert.NilError(t, err)
assert.DeepEqual(t, config.MacAddress, tc.expectedCfg.MacAddress) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
assert.DeepEqual(t, hConfig.NetworkMode, tc.expectedHostCfg.NetworkMode)
assert.DeepEqual(t, nwConfig.EndpointsConfig, tc.expected, cmpopts.EquateComparable(netip.Addr{}))
})

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"os"
"strings"
"syscall"
@ -70,7 +71,8 @@ func newRunCommand(dockerCLI command.Cli) *cobra.Command {
flags.Bool("help", false, "Print usage")
// TODO(thaJeztah): consider adding platform as "image create option" on containerOptions
addPlatformFlag(flags, &options.platform)
flags.StringVar(&options.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable")
_ = flags.SetAnnotation("platform", "version", []string{"1.32"})
flags.BoolVar(&options.untrusted, "disable-content-trust", !trust.Enabled(), "Skip image verification")
copts = addFlags(flags)

View File

@ -61,7 +61,7 @@ func TestRunLabel(t *testing.T) {
ID: "id",
}, nil
},
Version: "1.36",
Version: client.MaxAPIVersion,
})
cmd := newRunCommand(fakeCLI)
cmd.SetArgs([]string{"--detach=true", "--label", "foo", "busybox"})
@ -103,8 +103,8 @@ func TestRunAttach(t *testing.T) {
return responseChan, errChan
},
// use new (non-legacy) wait API
// see: 38591f20d07795aaef45d400df89ca12f29c603b
Version: "1.30",
// see: https://github.com/docker/cli/commit/38591f20d07795aaef45d400df89ca12f29c603b
Version: client.MaxAPIVersion,
}, func(fc *test.FakeCli) {
fc.SetOut(streams.NewOut(tty))
fc.SetIn(streams.NewIn(tty))
@ -180,8 +180,8 @@ func TestRunAttachTermination(t *testing.T) {
return responseChan, errChan
},
// use new (non-legacy) wait API
// see: 38591f20d07795aaef45d400df89ca12f29c603b
Version: "1.30",
// see: https://github.com/docker/cli/commit/38591f20d07795aaef45d400df89ca12f29c603b
Version: client.MaxAPIVersion,
}, func(fc *test.FakeCli) {
fc.SetOut(streams.NewOut(tty))
fc.SetIn(streams.NewIn(tty))
@ -262,7 +262,7 @@ func TestRunPullTermination(t *testing.T) {
attachCh <- struct{}{}
return respReader, nil
},
Version: "1.30",
Version: client.MaxAPIVersion,
})
cmd := newRunCommand(fakeCLI)

View File

@ -126,8 +126,6 @@ func (c *buildCacheContext) Parent() string {
var parent string
if len(c.v.Parents) > 0 {
parent = strings.Join(c.v.Parents, ", ")
} else {
parent = c.v.Parent //nolint:staticcheck // Ignore SA1019: Field was deprecated in API v1.42, but kept for backward compatibility
}
if c.trunc {
return TruncateID(parent)

View File

@ -48,7 +48,8 @@ func newImportCommand(dockerCLI command.Cli) *cobra.Command {
options.changes = dockeropts.NewListOpts(nil)
flags.VarP(&options.changes, "change", "c", "Apply Dockerfile instruction to the created image")
flags.StringVarP(&options.message, "message", "m", "", "Set commit message for imported image")
addPlatformFlag(flags, &options.platform)
flags.StringVar(&options.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable")
_ = flags.SetAnnotation("platform", "version", []string{"1.32"})
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms())
return cmd

View File

@ -1,17 +0,0 @@
package image
import (
"os"
"github.com/spf13/pflag"
)
// addPlatformFlag adds "--platform" to a set of flags for API version 1.32 and
// later, using the value of "DOCKER_DEFAULT_PLATFORM" (if set) as a default.
//
// It should not be used for new uses, which may have a different API version
// requirement.
func addPlatformFlag(flags *pflag.FlagSet, target *string) {
flags.StringVar(target, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable")
_ = flags.SetAnnotation("platform", "version", []string{"1.32"})
}

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"os"
"github.com/distribution/reference"
"github.com/docker/cli/cli"
@ -54,10 +55,9 @@ func newPullCommand(dockerCLI command.Cli) *cobra.Command {
flags.BoolVarP(&opts.all, "all-tags", "a", false, "Download all tagged images in the repository")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress verbose output")
addPlatformFlag(flags, &opts.platform)
flags.BoolVar(&opts.untrusted, "disable-content-trust", !trust.Enabled(), "Skip image verification")
flags.StringVar(&opts.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable")
_ = flags.SetAnnotation("platform", "version", []string{"1.32"})
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms())
return cmd

View File

@ -9,7 +9,6 @@ import (
"github.com/docker/cli/cli/command/completion"
cliopts "github.com/docker/cli/opts"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/versions"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@ -132,9 +131,7 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
}
// query registry if flag disabling it was not set
if !opts.noResolveImage && versions.GreaterThanOrEqualTo(apiClient.ClientVersion(), "1.30") {
createOpts.QueryRegistry = true
}
createOpts.QueryRegistry = !opts.noResolveImage
response, err := apiClient.ServiceCreate(ctx, service, createOpts)
if err != nil {
@ -147,7 +144,7 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
_, _ = fmt.Fprintln(dockerCLI.Out(), response.ID)
if opts.detach || versions.LessThan(apiClient.ClientVersion(), "1.29") {
if opts.detach {
return nil
}

View File

@ -8,7 +8,6 @@ import (
"github.com/docker/cli/cli/command/formatter"
flagsHelper "github.com/docker/cli/cli/flags"
"github.com/docker/cli/opts"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
@ -43,44 +42,19 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command {
}
func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) error {
var (
apiClient = dockerCLI.Client()
err error
)
listOpts := client.ServiceListOptions{
apiClient := dockerCLI.Client()
services, err := apiClient.ServiceList(ctx, client.ServiceListOptions{
Filters: options.filter.Value(),
// When not running "quiet", also get service status (number of running
// and desired tasks). Note that this is only supported on API v1.41 and
// up; older API versions ignore this option, and we will have to collect
// the information manually below.
Status: !options.quiet,
}
services, err := apiClient.ServiceList(ctx, listOpts)
})
if err != nil {
return err
}
if listOpts.Status {
// Now that a request was made, we know what API version was used (either
// through configuration, or after client and daemon negotiated a version).
// If API version v1.41 or up was used; the daemon should already have done
// the legwork for us, and we don't have to calculate the number of desired
// and running tasks. On older API versions, we need to do some extra requests
// to get that information.
//
// So theoretically, this step can be skipped based on API version, however,
// some of our unit tests don't set the API version, and there may be other
// situations where the client uses the "default" version. To account for
// these situations, we do a quick check for services that do not have
// a ServiceStatus set, and perform a lookup for those.
services, err = AppendServiceStatus(ctx, apiClient, services)
if err != nil {
return err
}
}
format := options.format
if len(format) == 0 {
if len(dockerCLI.ConfigFile().ServicesFormat) > 0 && !options.quiet {
@ -96,94 +70,3 @@ func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) er
}
return ListFormatWrite(servicesCtx, services)
}
// AppendServiceStatus propagates the ServiceStatus field for "services".
//
// If API version v1.41 or up is used, this information is already set by the
// daemon. On older API versions, we need to do some extra requests to get
// that information. Theoretically, this function can be skipped based on API
// version, however, some of our unit tests don't set the API version, and
// there may be other situations where the client uses the "default" version.
// To take these situations into account, we do a quick check for services
// that don't have ServiceStatus set, and perform a lookup for those.
func AppendServiceStatus(ctx context.Context, c client.APIClient, services []swarm.Service) ([]swarm.Service, error) {
status := map[string]*swarm.ServiceStatus{}
taskFilter := make(client.Filters)
for i, s := range services {
// there is no need in this switch to check for job modes. jobs are not
// supported until after ServiceStatus was introduced.
switch {
case s.ServiceStatus != nil:
// Server already returned service-status, so we don't
// have to look-up tasks for this service.
continue
case s.Spec.Mode.Replicated != nil:
// For replicated services, set the desired number of tasks;
// that way we can present this information in case we're unable
// to get a list of tasks from the server.
services[i].ServiceStatus = &swarm.ServiceStatus{DesiredTasks: *s.Spec.Mode.Replicated.Replicas}
status[s.ID] = &swarm.ServiceStatus{}
taskFilter.Add("service", s.ID)
case s.Spec.Mode.Global != nil:
// No such thing as number of desired tasks for global services
services[i].ServiceStatus = &swarm.ServiceStatus{}
status[s.ID] = &swarm.ServiceStatus{}
taskFilter.Add("service", s.ID)
default:
// Unknown task type
}
}
if len(status) == 0 {
// All services have their ServiceStatus set, so we're done
return services, nil
}
tasks, err := c.TaskList(ctx, client.TaskListOptions{Filters: taskFilter})
if err != nil {
return nil, err
}
if len(tasks) == 0 {
return services, nil
}
activeNodes, err := getActiveNodes(ctx, c)
if err != nil {
return nil, err
}
for _, task := range tasks {
if status[task.ServiceID] == nil {
// This should not happen in practice; either all services have
// a ServiceStatus set, or none of them.
continue
}
// TODO: this should only be needed for "global" services. Replicated
// services have `Spec.Mode.Replicated.Replicas`, which should give this value.
if task.DesiredState != swarm.TaskStateShutdown {
status[task.ServiceID].DesiredTasks++
}
if _, nodeActive := activeNodes[task.NodeID]; nodeActive && task.Status.State == swarm.TaskStateRunning {
status[task.ServiceID].RunningTasks++
}
}
for i, service := range services {
if s := status[service.ID]; s != nil {
services[i].ServiceStatus = s
}
}
return services, nil
}
func getActiveNodes(ctx context.Context, c client.NodeAPIClient) (map[string]struct{}, error) {
nodes, err := c.NodeList(ctx, client.NodeListOptions{})
if err != nil {
return nil, err
}
activeNodes := make(map[string]struct{})
for _, n := range nodes {
if n.Status.State != swarm.NodeStateDown {
activeNodes[n.ID] = struct{}{}
}
}
return activeNodes, nil
}

View File

@ -10,7 +10,6 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/builders"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/versions"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
@ -63,59 +62,12 @@ func TestServiceListServiceStatus(t *testing.T) {
cluster: &cluster{}, // force an empty cluster
expected: []listResponse{},
},
{
// Services are running, but no active nodes were found. On API v1.40
// and below, this will cause looking up the "running" tasks to fail,
// as well as looking up "desired" tasks for global services.
doc: "API v1.40 no active nodes",
opts: clusterOpts{
apiVersion: "1.40",
activeNodes: 0,
runningTasks: 2,
desiredTasks: 4,
},
expected: []listResponse{
{ID: "replicated", Replicas: "0/4"},
{ID: "global", Replicas: "0/0"},
{ID: "none-id", Replicas: "0/0"},
},
},
{
doc: "API v1.40 3 active nodes, 1 task running",
opts: clusterOpts{
apiVersion: "1.40",
activeNodes: 3,
runningTasks: 1,
desiredTasks: 2,
},
expected: []listResponse{
{ID: "replicated", Replicas: "1/2"},
{ID: "global", Replicas: "1/3"},
{ID: "none-id", Replicas: "0/0"},
},
},
{
doc: "API v1.40 3 active nodes, all tasks running",
opts: clusterOpts{
apiVersion: "1.40",
activeNodes: 3,
runningTasks: 3,
desiredTasks: 3,
},
expected: []listResponse{
{ID: "replicated", Replicas: "3/3"},
{ID: "global", Replicas: "3/3"},
{ID: "none-id", Replicas: "0/0"},
},
},
{
// Services are running, but no active nodes were found. On API v1.41
// and up, the ServiceStatus is sent by the daemon, so this should not
// affect the results.
doc: "API v1.41 no active nodes",
doc: "no active nodes",
opts: clusterOpts{
apiVersion: "1.41",
activeNodes: 0,
runningTasks: 2,
desiredTasks: 4,
@ -127,9 +79,8 @@ func TestServiceListServiceStatus(t *testing.T) {
},
},
{
doc: "API v1.41 3 active nodes, 1 task running",
doc: "active nodes, 1 task running",
opts: clusterOpts{
apiVersion: "1.41",
activeNodes: 3,
runningTasks: 1,
desiredTasks: 2,
@ -141,9 +92,8 @@ func TestServiceListServiceStatus(t *testing.T) {
},
},
{
doc: "API v1.41 3 active nodes, all tasks running",
doc: "active nodes, all tasks running",
opts: clusterOpts{
apiVersion: "1.41",
activeNodes: 3,
runningTasks: 3,
desiredTasks: 3,
@ -174,7 +124,7 @@ func TestServiceListServiceStatus(t *testing.T) {
}
cli := test.NewFakeCli(&fakeClient{
serviceListFunc: func(ctx context.Context, options client.ServiceListOptions) ([]swarm.Service, error) {
if !options.Status || versions.LessThan(tc.opts.apiVersion, "1.41") {
if !options.Status {
// Don't return "ServiceStatus" if not requested, or on older API versions
for i := range tc.cluster.services {
tc.cluster.services[i].ServiceStatus = nil
@ -214,7 +164,6 @@ func TestServiceListServiceStatus(t *testing.T) {
}
type clusterOpts struct {
apiVersion string
activeNodes uint64
desiredTasks uint64
runningTasks uint64

View File

@ -6,7 +6,6 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/moby/moby/api/types/versions"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
@ -54,7 +53,7 @@ func runRollback(ctx context.Context, dockerCLI command.Cli, options *serviceOpt
_, _ = fmt.Fprintln(dockerCLI.Out(), serviceID)
if options.detach || versions.LessThan(apiClient.ClientVersion(), "1.29") {
if options.detach {
return nil
}

View File

@ -48,7 +48,8 @@ func TestRollback(t *testing.T) {
})
cmd := newRollbackCommand(cli)
cmd.SetArgs(tc.args)
cmd.Flags().Set("quiet", "true")
assert.NilError(t, cmd.Flags().Set("quiet", "true"))
assert.NilError(t, cmd.Flags().Set("detach", "true"))
cmd.SetOut(io.Discard)
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Equal(strings.TrimSpace(cli.ErrBuffer().String()), tc.expectedDockerCliErr))

View File

@ -9,7 +9,6 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/moby/moby/api/types/versions"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
@ -83,7 +82,7 @@ func runScale(ctx context.Context, dockerCLI command.Cli, options *scaleOptions,
serviceIDs = append(serviceIDs, serviceID)
}
if len(serviceIDs) > 0 && !options.detach && versions.GreaterThanOrEqualTo(dockerCLI.Client().ClientVersion(), "1.29") {
if len(serviceIDs) > 0 && !options.detach {
for _, serviceID := range serviceIDs {
if err := WaitOnService(ctx, dockerCLI, serviceID, false); err != nil {
errs = append(errs, fmt.Errorf("%s: %v", serviceID, err))

View File

@ -22,7 +22,6 @@ import (
"github.com/moby/moby/api/types/mount"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/versions"
"github.com/moby/moby/client"
"github.com/moby/swarmkit/v2/api/defaults"
"github.com/spf13/cobra"
@ -165,14 +164,6 @@ func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
return err
}
// There are two ways to do user-requested rollback. The old way is
// client-side, but with a sufficiently recent daemon we prefer
// server-side, because it will honor the rollback parameters.
var (
clientSideRollback bool
serverSideRollback bool
)
spec := &service.Spec
if rollback {
// Rollback can't be combined with other flags.
@ -188,20 +179,10 @@ func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
if otherFlagsPassed {
return errors.New("other flags may not be combined with --rollback")
}
if versions.LessThan(apiClient.ClientVersion(), "1.28") {
clientSideRollback = true
spec = service.PreviousSpec
if spec == nil {
return errors.New("service does not have a previous specification to roll back to")
}
} else {
serverSideRollback = true
}
}
updateOpts := client.ServiceUpdateOptions{}
if serverSideRollback {
if rollback {
updateOpts.Rollback = "previous"
}
@ -214,9 +195,7 @@ func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
if err := resolveServiceImageDigestContentTrust(dockerCLI, spec); err != nil {
return err
}
if !options.noResolveImage && versions.GreaterThanOrEqualTo(apiClient.ClientVersion(), "1.30") {
updateOpts.QueryRegistry = true
}
updateOpts.QueryRegistry = !options.noResolveImage
}
updatedSecrets, err := getUpdatedSecrets(ctx, apiClient, flags, spec.TaskTemplate.ContainerSpec.Secrets)
@ -243,8 +222,7 @@ func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
if err != nil {
return err
}
switch {
case sendAuth:
if sendAuth {
// Retrieve encoded auth token from the image reference
// This would be the old image if it didn't change in this update
image := spec.TaskTemplate.ContainerSpec.Image
@ -253,9 +231,7 @@ func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
return err
}
updateOpts.EncodedRegistryAuth = encodedAuth
case clientSideRollback:
updateOpts.RegistryAuthFrom = swarm.RegistryAuthFromPreviousSpec
default:
} else {
updateOpts.RegistryAuthFrom = swarm.RegistryAuthFromSpec
}
@ -270,7 +246,7 @@ func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
_, _ = fmt.Fprintln(dockerCLI.Out(), serviceID)
if options.detach || versions.LessThan(apiClient.ClientVersion(), "1.29") {
if options.detach {
return nil
}

View File

@ -14,8 +14,6 @@ import (
type fakeClient struct {
client.Client
version string
services []string
networks []string
secrets []string
@ -49,8 +47,8 @@ func (*fakeClient) ServerVersion(context.Context) (types.Version, error) {
}, nil
}
func (cli *fakeClient) ClientVersion() string {
return cli.version
func (*fakeClient) ClientVersion() string {
return client.MaxAPIVersion
}
func (cli *fakeClient) ServiceList(_ context.Context, options client.ServiceListOptions) ([]swarm.Service, error) {

View File

@ -10,7 +10,6 @@ import (
"github.com/docker/cli/cli/compose/convert"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/versions"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
@ -81,13 +80,6 @@ func runDeploy(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
return fmt.Errorf("invalid option %s for flag --resolve-image", opts.resolveImage)
}
// client side image resolution should not be done when the supported
// server version is older than 1.30
if versions.LessThan(dockerCLI.Client().ClientVersion(), "1.30") {
// TODO(thaJeztah): should this error if "opts.ResolveImage" is already other (unsupported) values?
opts.resolveImage = resolveImageNever
}
if opts.detach && !flags.Changed("detach") {
_, _ = fmt.Fprintln(dockerCLI.Err(), "Since --detach=false was not specified, tasks will be created in the background.\n"+
"In a future release, --detach=false will become the default.")

View File

@ -10,7 +10,6 @@ import (
"github.com/docker/cli/cli/command"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/versions"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
@ -61,20 +60,14 @@ func runRemove(ctx context.Context, dockerCli command.Cli, opts removeOptions) e
return err
}
var secrets []swarm.Secret
if versions.GreaterThanOrEqualTo(apiClient.ClientVersion(), "1.25") {
secrets, err = getStackSecrets(ctx, apiClient, namespace)
if err != nil {
return err
}
secrets, err := getStackSecrets(ctx, apiClient, namespace)
if err != nil {
return err
}
var configs []swarm.Config
if versions.GreaterThanOrEqualTo(apiClient.ClientVersion(), "1.30") {
configs, err = getStackConfigs(ctx, apiClient, namespace)
if err != nil {
return err
}
configs, err := getStackConfigs(ctx, apiClient, namespace)
if err != nil {
return err
}
if len(services)+len(networks)+len(secrets)+len(configs) == 0 {

View File

@ -11,7 +11,7 @@ import (
is "gotest.tools/v3/assert/cmp"
)
func fakeClientForRemoveStackTest(version string) *fakeClient {
func fakeClientForRemoveStackTest() *fakeClient {
allServices := []string{
objectName("foo", "service1"),
objectName("foo", "service2"),
@ -33,7 +33,6 @@ func fakeClientForRemoveStackTest(version string) *fakeClient {
objectName("bar", "config1"),
}
return &fakeClient{
version: version,
services: allServices,
networks: allNetworks,
secrets: allSecrets,
@ -50,40 +49,16 @@ func TestRemoveWithEmptyName(t *testing.T) {
assert.ErrorContains(t, cmd.Execute(), `invalid stack name: "' '"`)
}
func TestRemoveStackVersion124DoesNotRemoveConfigsOrSecrets(t *testing.T) {
client := fakeClientForRemoveStackTest("1.24")
cmd := newRemoveCommand(test.NewFakeCli(client))
func TestRemoveStackRemovesEverything(t *testing.T) {
apiClient := fakeClientForRemoveStackTest()
cmd := newRemoveCommand(test.NewFakeCli(apiClient))
cmd.SetArgs([]string{"foo", "bar"})
assert.NilError(t, cmd.Execute())
assert.Check(t, is.DeepEqual(buildObjectIDs(client.services), client.removedServices))
assert.Check(t, is.DeepEqual(buildObjectIDs(client.networks), client.removedNetworks))
assert.Check(t, is.Len(client.removedSecrets, 0))
assert.Check(t, is.Len(client.removedConfigs, 0))
}
func TestRemoveStackVersion125DoesNotRemoveConfigs(t *testing.T) {
client := fakeClientForRemoveStackTest("1.25")
cmd := newRemoveCommand(test.NewFakeCli(client))
cmd.SetArgs([]string{"foo", "bar"})
assert.NilError(t, cmd.Execute())
assert.Check(t, is.DeepEqual(buildObjectIDs(client.services), client.removedServices))
assert.Check(t, is.DeepEqual(buildObjectIDs(client.networks), client.removedNetworks))
assert.Check(t, is.DeepEqual(buildObjectIDs(client.secrets), client.removedSecrets))
assert.Check(t, is.Len(client.removedConfigs, 0))
}
func TestRemoveStackVersion130RemovesEverything(t *testing.T) {
client := fakeClientForRemoveStackTest("1.30")
cmd := newRemoveCommand(test.NewFakeCli(client))
cmd.SetArgs([]string{"foo", "bar"})
assert.NilError(t, cmd.Execute())
assert.Check(t, is.DeepEqual(buildObjectIDs(client.services), client.removedServices))
assert.Check(t, is.DeepEqual(buildObjectIDs(client.networks), client.removedNetworks))
assert.Check(t, is.DeepEqual(buildObjectIDs(client.secrets), client.removedSecrets))
assert.Check(t, is.DeepEqual(buildObjectIDs(client.configs), client.removedConfigs))
assert.Check(t, is.DeepEqual(buildObjectIDs(apiClient.services), apiClient.removedServices))
assert.Check(t, is.DeepEqual(buildObjectIDs(apiClient.networks), apiClient.removedNetworks))
assert.Check(t, is.DeepEqual(buildObjectIDs(apiClient.secrets), apiClient.removedSecrets))
assert.Check(t, is.DeepEqual(buildObjectIDs(apiClient.configs), apiClient.removedConfigs))
}
func TestRemoveStackSkipEmpty(t *testing.T) {
@ -100,7 +75,6 @@ func TestRemoveStackSkipEmpty(t *testing.T) {
allConfigIDs := buildObjectIDs(allConfigs)
apiClient := &fakeClient{
version: "1.30",
services: allServices,
networks: allNetworks,
secrets: allSecrets,
@ -141,7 +115,6 @@ func TestRemoveContinueAfterError(t *testing.T) {
removedServices := []string{}
apiClient := &fakeClient{
version: "1.30",
services: allServices,
networks: allNetworks,
secrets: allSecrets,

View File

@ -21,8 +21,6 @@ func TestStackServicesErrors(t *testing.T) {
args []string
flags map[string]string
serviceListFunc func(options client.ServiceListOptions) ([]swarm.Service, error)
nodeListFunc func(options client.NodeListOptions) ([]swarm.Node, error)
taskListFunc func(options client.TaskListOptions) ([]swarm.Task, error)
expectedError string
}{
{
@ -32,29 +30,6 @@ func TestStackServicesErrors(t *testing.T) {
},
expectedError: "error getting services",
},
{
args: []string{"foo"},
serviceListFunc: func(options client.ServiceListOptions) ([]swarm.Service, error) {
return []swarm.Service{*builders.Service(builders.GlobalService())}, nil
},
nodeListFunc: func(options client.NodeListOptions) ([]swarm.Node, error) {
return nil, errors.New("error getting nodes")
},
taskListFunc: func(options client.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*builders.Task()}, nil
},
expectedError: "error getting nodes",
},
{
args: []string{"foo"},
serviceListFunc: func(options client.ServiceListOptions) ([]swarm.Service, error) {
return []swarm.Service{*builders.Service(builders.GlobalService())}, nil
},
taskListFunc: func(options client.TaskListOptions) ([]swarm.Task, error) {
return nil, errors.New("error getting tasks")
},
expectedError: "error getting tasks",
},
{
args: []string{"foo"},
flags: map[string]string{
@ -71,8 +46,6 @@ func TestStackServicesErrors(t *testing.T) {
t.Run(tc.expectedError, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
serviceListFunc: tc.serviceListFunc,
nodeListFunc: tc.nodeListFunc,
taskListFunc: tc.taskListFunc,
})
cmd := newServicesCommand(cli)
cmd.SetArgs(tc.args)

View File

@ -3,44 +3,16 @@ package stack
import (
"context"
"github.com/docker/cli/cli/command/service"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
)
// getServices is the swarm implementation of listing stack services
func getServices(ctx context.Context, apiClient client.APIClient, opts serviceListOptions) ([]swarm.Service, error) {
listOpts := client.ServiceListOptions{
return apiClient.ServiceList(ctx, client.ServiceListOptions{
Filters: getStackFilterFromOpt(opts.namespace, opts.filter),
// When not running "quiet", also get service status (number of running
// and desired tasks). Note that this is only supported on API v1.41 and
// up; older API versions ignore this option, and we will have to collect
// the information manually below.
// and desired tasks).
Status: !opts.quiet,
}
services, err := apiClient.ServiceList(ctx, listOpts)
if err != nil {
return nil, err
}
if listOpts.Status {
// Now that a request was made, we know what API version was used (either
// through configuration, or after client and daemon negotiated a version).
// If API version v1.41 or up was used; the daemon should already have done
// the legwork for us, and we don't have to calculate the number of desired
// and running tasks. On older API versions, we need to do some extra requests
// to get that information.
//
// So theoretically, this step can be skipped based on API version, however,
// some of our unit tests don't set the API version, and there may be other
// situations where the client uses the "default" version. To account for
// these situations, we do a quick check for services that do not have
// a ServiceStatus set, and perform a lookup for those.
services, err = service.AppendServiceStatus(ctx, apiClient, services)
if err != nil {
return nil, err
}
}
return services, nil
})
}

View File

@ -22,24 +22,8 @@ import (
_ "github.com/docker/cli/cli/command/volume"
)
func TestPrunePromptPre131DoesNotIncludeBuildCache(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{version: "1.30"})
cmd := newPruneCommand(cli)
cmd.SetArgs([]string{})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
assert.ErrorContains(t, cmd.Execute(), "system prune has been cancelled")
expected := `WARNING! This will remove:
- all stopped containers
- all networks not used by at least one container
- all dangling images
Are you sure you want to continue? [y/N] `
assert.Check(t, is.Equal(expected, cli.OutBuffer().String()))
}
func TestPrunePromptFilters(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{version: "1.31"})
cli := test.NewFakeCli(&fakeClient{version: "1.51"})
cli.SetConfigFile(&configfile.ConfigFile{
PruneFilters: []string{"label!=never=remove-me", "label=remove=me"},
})

View File

@ -11,7 +11,6 @@ import (
"github.com/docker/cli/internal/prompt"
"github.com/docker/cli/opts"
"github.com/docker/go-units"
"github.com/moby/moby/api/types/versions"
"github.com/spf13/cobra"
)
@ -72,16 +71,11 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
warning := unusedVolumesWarning
if versions.GreaterThanOrEqualTo(dockerCli.CurrentVersion(), "1.42") {
if options.all {
if _, ok := pruneFilters["all"]; ok {
return 0, "", invalidParamErr{errors.New("conflicting options: cannot specify both --all and --filter all=1")}
}
pruneFilters.Add("all", "true")
warning = allVolumesWarning
if options.all {
if _, ok := pruneFilters["all"]; ok {
return 0, "", invalidParamErr{errors.New("conflicting options: cannot specify both --all and --filter all=1")}
}
} else {
// API < v1.42 removes all volumes (anonymous and named) by default.
pruneFilters.Add("all", "true")
warning = allVolumesWarning
}
if !options.force {

View File

@ -16,7 +16,6 @@ import (
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/versions"
"github.com/moby/moby/client"
)
@ -44,7 +43,7 @@ func Services(
return nil, fmt.Errorf("service %s: %w", service.Name, err)
}
serviceSpec, err := Service(apiClient.ClientVersion(), namespace, service, config.Networks, config.Volumes, secrets, configs)
serviceSpec, err := Service(namespace, service, config.Networks, config.Volumes, secrets, configs)
if err != nil {
return nil, fmt.Errorf("service %s: %w", service.Name, err)
}
@ -56,7 +55,6 @@ func Services(
// Service converts a ServiceConfig into a swarm ServiceSpec
func Service(
apiVersion string,
namespace Namespace,
service composetypes.ServiceConfig,
networkConfigs map[string]composetypes.NetworkConfig,
@ -161,6 +159,7 @@ func Service(
Preferences: getPlacementPreference(service.Deploy.Placement.Preferences),
MaxReplicas: service.Deploy.Placement.MaxReplicas,
},
Networks: networks,
},
EndpointSpec: endpoint,
Mode: mode,
@ -171,18 +170,6 @@ func Service(
// add an image label to serviceSpec
serviceSpec.Labels[LabelImage] = service.Image
// ServiceSpec.Networks is deprecated and should not have been used by
// this package. It is possible to update TaskTemplate.Networks, but it
// is not possible to update ServiceSpec.Networks. Unfortunately, we
// can't unconditionally start using TaskTemplate.Networks, because that
// will break with older daemons that don't support migrating from
// ServiceSpec.Networks to TaskTemplate.Networks. So which field to use
// is conditional on daemon version.
if versions.LessThan(apiVersion, "1.29") {
serviceSpec.Networks = networks //nolint:staticcheck // ignore SA1019: field is deprecated.
} else {
serviceSpec.TaskTemplate.Networks = networks
}
return serviceSpec, nil
}
@ -670,7 +657,6 @@ func toNetipAddrSlice(ips []string) []netip.Addr {
func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpecConfig, refs []*swarm.ConfigReference) (*swarm.CredentialSpec, error) {
var o []string
// Config was added in API v1.40
if spec.Config != "" {
o = append(o, `"Config"`)
}

View File

@ -495,7 +495,7 @@ func TestServiceConvertsIsolation(t *testing.T) {
src := composetypes.ServiceConfig{
Isolation: "hyperv",
}
result, err := Service("1.35", Namespace{name: "foo"}, src, nil, nil, nil, nil)
result, err := Service(Namespace{name: "foo"}, src, nil, nil, nil, nil)
assert.NilError(t, err)
assert.Check(t, is.Equal(container.IsolationHyperV, result.TaskTemplate.ContainerSpec.Isolation))
}
@ -692,7 +692,7 @@ func TestConvertServiceCapAddAndCapDrop(t *testing.T) {
}
for _, tc := range tests {
t.Run(tc.title, func(t *testing.T) {
result, err := Service("1.41", Namespace{name: "foo"}, tc.in, nil, nil, nil, nil)
result, err := Service(Namespace{name: "foo"}, tc.in, nil, nil, nil, nil)
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityAdd, tc.out.CapAdd))
assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityDrop, tc.out.CapDrop))

View File

@ -195,22 +195,22 @@ line in the `Engine` section:
```console
Client: Docker Engine - Community
Version: 23.0.3
API version: 1.42
Go version: go1.19.7
Git commit: 3e7cbfd
Built: Tue Apr 4 22:05:41 2023
OS/Arch: darwin/amd64
Context: default
Version: 28.5.1
API version: 1.51
Go version: go1.24.8
Git commit: e180ab8
Built: Wed Oct 8 12:16:17 2025
OS/Arch: darwin/arm64
Context: desktop-linux
Server: Docker Engine - Community
Engine:
Version: 23.0.3
API version: 1.42 (minimum version 1.12)
Go version: go1.19.7
Git commit: 59118bf
Built: Tue Apr 4 22:05:41 2023
OS/Arch: linux/amd64
Version: 28.5.1
API version: 1.51 (minimum version 1.24)
Go version: go1.24.8
Git commit: f8215cc
Built: Wed Oct 8 12:18:25 2025
OS/Arch: linux/arm64
Experimental: true
[...]
```

View File

@ -38,29 +38,29 @@ machine running Docker Desktop:
$ docker version
Client: Docker Engine - Community
Version: 23.0.3
API version: 1.42
Go version: go1.19.7
Git commit: 3e7cbfd
Built: Tue Apr 4 22:05:41 2023
OS/Arch: darwin/amd64
Context: default
Version: 28.5.1
API version: 1.51
Go version: go1.24.8
Git commit: e180ab8
Built: Wed Oct 8 12:16:17 2025
OS/Arch: darwin/arm64
Context: remote-test-server
Server: Docker Desktop 4.19.0 (12345)
Engine:
Version: 23.0.3
API version: 1.42 (minimum version 1.12)
Go version: go1.19.7
Git commit: 59118bf
Built: Tue Apr 4 22:05:41 2023
Version: 27.5.1
API version: 1.47 (minimum version 1.24)
Go version: go1.22.11
Git commit: 4c9b3b0
Built: Wed Jan 22 13:41:24 2025
OS/Arch: linux/amd64
Experimental: false
Experimental: true
containerd:
Version: 1.6.20
GitCommit: 2806fc1057397dbaeefbea0e4e17bddfbd388f38
Version: v1.7.25
GitCommit: bcc810d6b9066471b0b6fa75f557a15a1cbf31bb
runc:
Version: 1.1.5
GitCommit: v1.1.5-0-gf19387a
Version: 1.2.4
GitCommit: v1.2.4-0-g6c52b3f
docker-init:
Version: 0.19.0
GitCommit: de40ad0
@ -83,31 +83,32 @@ remote-test-server
$ docker version
Client: Docker Engine - Community
Version: 23.0.3
API version: 1.40 (downgraded from 1.42)
Go version: go1.19.7
Git commit: 3e7cbfd
Built: Tue Apr 4 22:05:41 2023
OS/Arch: darwin/amd64
Version: 28.5.1
API version: 1.51
Go version: go1.24.8
Git commit: e180ab8
Built: Wed Oct 8 12:16:17 2025
OS/Arch: darwin/arm64
Context: remote-test-server
Server: Docker Engine - Community
Engine:
Version: 19.03.8
API version: 1.40 (minimum version 1.12)
Go version: go1.12.17
Git commit: afacb8b
Built: Wed Mar 11 01:29:16 2020
Version: 27.5.1
API version: 1.47 (minimum version 1.24)
Go version: go1.22.11
Git commit: 4c9b3b0
Built: Wed Jan 22 13:41:24 2025
OS/Arch: linux/amd64
Experimental: true
containerd:
Version: v1.2.13
GitCommit: 7ad184331fa3e55e52b890ea95e65ba581ae3429
Version: v1.7.25
GitCommit: bcc810d6b9066471b0b6fa75f557a15a1cbf31bb
runc:
Version: 1.0.0-rc10
GitCommit: dc9208a3303feef5b3839f4323d9beb36df0a9dd
Version: 1.2.4
GitCommit: v1.2.4-0-g6c52b3f
docker-init:
Version: 0.18.0
GitCommit: fec3683
Version: 0.19.0
GitCommit: de40ad0
```
### API version and version negotiation
@ -117,14 +118,14 @@ CLI is connecting with. When connecting with the Docker Engine, the Docker CLI
and Docker Engine perform API version negotiation, and select the highest API
version that is supported by both the Docker CLI and the Docker Engine.
For example, if the CLI is connecting with Docker Engine version 19.03, it downgrades
to API version 1.40 (refer to the [API version matrix](https://docs.docker.com/reference/api/engine/#api-version-matrix)
For example, if the CLI is connecting with Docker Engine version 27.5, it downgrades
to API version 1.47 (refer to the [API version matrix](https://docs.docker.com/reference/api/engine/#api-version-matrix)
to learn about the supported API versions for Docker Engine):
```console
$ docker version --format '{{.Client.APIVersion}}'
1.40
1.47
```
Be aware that API version can also be overridden using the `DOCKER_API_VERSION`
@ -135,14 +136,14 @@ variable removes the override, and re-enables API version negotiation:
```console
$ env | grep DOCKER_API_VERSION
DOCKER_API_VERSION=1.39
DOCKER_API_VERSION=1.50
$ docker version --format '{{.Client.APIVersion}}'
1.39
1.50
$ unset DOCKER_API_VERSION
$ docker version --format '{{.Client.APIVersion}}'
1.42
1.51
```
## Examples
@ -159,7 +160,7 @@ page for details of the format.
```console
$ docker version --format '{{.Server.Version}}'
23.0.3
28.5.1
```
### Get the client API version
@ -169,7 +170,7 @@ The following example prints the API version that is used by the client:
```console
$ docker version --format '{{.Client.APIVersion}}'
1.42
1.51
```
The version shown is the API version that is negotiated between the client
@ -181,5 +182,5 @@ above for more information.
```console
$ docker version --format '{{json .}}'
{"Client":"Version":"23.0.3","ApiVersion":"1.42", ...}
{"Client":"Version":"28.5.1","ApiVersion":"1.51", ...}
```

View File

@ -210,7 +210,6 @@ func TestRunWithCgroupNamespace(t *testing.T) {
func TestMountSubvolume(t *testing.T) {
skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.45"))
volName := "test-volume-" + t.Name()
icmd.RunCommand("docker", "volume", "create", volName).Assert(t, icmd.Success)

View File

@ -16,7 +16,6 @@ import (
"github.com/docker/cli/e2e/testutils"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/environment"
"github.com/moby/moby/api/types/versions"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
"gotest.tools/v3/poll"
@ -144,7 +143,6 @@ func TestPromptExitCode(t *testing.T) {
run: func(t *testing.T) icmd.Cmd {
t.Helper()
t.Skip("flaky test: see https://github.com/docker/cli/issues/6248")
skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.44"))
const plugin = "registry:5000/plugin-install-test:latest"
@ -162,7 +160,6 @@ func TestPromptExitCode(t *testing.T) {
run: func(t *testing.T) icmd.Cmd {
t.Helper()
t.Skip("flaky test: see https://github.com/docker/cli/issues/6248")
skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.44"))
const plugin = "registry:5000/plugin-upgrade-test"

View File

@ -17,29 +17,29 @@ machine running Docker Desktop:
$ docker version
Client: Docker Engine - Community
Version: 23.0.3
API version: 1.42
Go version: go1.19.7
Git commit: 3e7cbfd
Built: Tue Apr 4 22:05:41 2023
OS/Arch: darwin/amd64
Context: default
Version: 28.5.1
API version: 1.51
Go version: go1.24.8
Git commit: e180ab8
Built: Wed Oct 8 12:16:17 2025
OS/Arch: darwin/arm64
Context: desktop-linux
Server: Docker Desktop 4.19.0 (12345)
Server: Docker Desktop 4.49.0 (12345)
Engine:
Version: 23.0.3
API version: 1.42 (minimum version 1.12)
Go version: go1.19.7
Git commit: 59118bf
Built: Tue Apr 4 22:05:41 2023
OS/Arch: linux/amd64
Version: 28.5.1
API version: 1.51 (minimum version 1.24)
Go version: go1.24.8
Git commit: f8215cc
Built: Wed Oct 8 12:18:25 2025
OS/Arch: linux/arm64
Experimental: false
containerd:
Version: 1.6.20
GitCommit: 2806fc1057397dbaeefbea0e4e17bddfbd388f38
Version: 1.7.27
GitCommit: 05044ec0a9a75232cad458027ca83437aae3f4da
runc:
Version: 1.1.5
GitCommit: v1.1.5-0-gf19387a
Version: 1.2.5
GitCommit: v1.2.5-0-g59923ef
docker-init:
Version: 0.19.0
GitCommit: de40ad0
@ -47,11 +47,11 @@ machine running Docker Desktop:
Get server version:
$ docker version --format '{{.Server.Version}}'
23.0.3
28.5.1
Dump raw data:
To view all available fields, you can use the format `{{json .}}`.
$ docker version --format '{{json .}}'
{"Client":"Version":"23.0.3","ApiVersion":"1.42", ...}
{"Client":"Version":"28.5.1","ApiVersion":"1.51", ...}