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