Merge pull request #6551 from thaJeztah/remove_legacy_api_versions
remove API-version compatibility for API < v1.44
This commit is contained in:
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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{}))
|
||||
})
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"})
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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.")
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@ -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"},
|
||||
})
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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"`)
|
||||
}
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
[...]
|
||||
```
|
||||
|
||||
@ -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", ...}
|
||||
```
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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", ...}
|
||||
|
||||
Reference in New Issue
Block a user