Merge pull request #6226 from thaJeztah/bump_engine

vendor: docker/docker, moby/moby/api and moby/moby/client 4faedf2bec36
This commit is contained in:
Sebastiaan van Stijn
2025-07-31 18:10:08 +02:00
committed by GitHub
35 changed files with 424 additions and 322 deletions

View File

@ -8,7 +8,7 @@ import (
"sync"
"github.com/docker/cli/cli/command"
"github.com/moby/moby/api/stdcopy"
"github.com/moby/moby/api/pkg/stdcopy"
"github.com/moby/moby/client"
"github.com/moby/term"
"github.com/sirupsen/logrus"

View File

@ -7,7 +7,7 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/moby/moby/api/stdcopy"
"github.com/moby/moby/api/pkg/stdcopy"
"github.com/moby/moby/api/types/container"
"github.com/spf13/cobra"
)

View File

@ -414,8 +414,8 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
publishOpts := copts.publish.GetSlice()
var (
ports map[nat.Port]struct{}
portBindings map[nat.Port][]nat.PortBinding
ports map[container.PortRangeProto]struct{}
portBindings map[container.PortRangeProto][]container.PortBinding
convertedOpts []string
)

View File

@ -10,7 +10,6 @@ import (
"testing"
"time"
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/container"
networktypes "github.com/moby/moby/api/types/network"
"github.com/spf13/pflag"
@ -439,7 +438,7 @@ func TestParseWithExpose(t *testing.T) {
"8080-NaN/tcp": `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
"1234567890-8080/tcp": `invalid range format for --expose: 1234567890-8080/tcp, error: strconv.ParseUint: parsing "1234567890": value out of range`,
}
valids := map[string][]nat.Port{
valids := map[string][]container.PortRangeProto{
"8080/tcp": {"8080/tcp"},
"8080/udp": {"8080/udp"},
"8080/ncp": {"8080/ncp"},
@ -473,7 +472,7 @@ func TestParseWithExpose(t *testing.T) {
if len(config.ExposedPorts) != 2 {
t.Fatalf("Expected 2 exposed ports, got %v", config.ExposedPorts)
}
ports := []nat.Port{"80/tcp", "81/tcp"}
ports := []container.PortRangeProto{"80/tcp", "81/tcp"}
for _, port := range ports {
if _, ok := config.ExposedPorts[port]; !ok {
t.Fatalf("Expected %v, got %v", ports, config.ExposedPorts)

View File

@ -11,8 +11,8 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/go-connections/nat"
"github.com/fvbommel/sortorder"
"github.com/moby/moby/api/types/container"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@ -67,7 +67,7 @@ func runPort(ctx context.Context, dockerCli command.Cli, opts *portOptions) erro
if _, err = strconv.ParseUint(port, 10, 16); err != nil {
return errors.Wrapf(err, "Error: invalid port (%s)", port)
}
frontends, exists := c.NetworkSettings.Ports[nat.Port(port+"/"+proto)]
frontends, exists := c.NetworkSettings.Ports[container.PortRangeProto(port+"/"+proto)]
if !exists || len(frontends) == 0 {
return errors.Errorf("Error: No public port '%s' published for %s", opts.port, opts.container)
}

View File

@ -5,7 +5,6 @@ import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/container"
"gotest.tools/v3/assert"
"gotest.tools/v3/golden"
@ -47,19 +46,19 @@ func TestNewPortCommandOutput(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
inspectFunc: func(string) (container.InspectResponse, error) {
ci := container.InspectResponse{NetworkSettings: &container.NetworkSettings{}}
ci.NetworkSettings.Ports = nat.PortMap{
"80/tcp": make([]nat.PortBinding, len(tc.ips)),
"443/tcp": make([]nat.PortBinding, len(tc.ips)),
"443/udp": make([]nat.PortBinding, len(tc.ips)),
ci.NetworkSettings.Ports = container.PortMap{
"80/tcp": make([]container.PortBinding, len(tc.ips)),
"443/tcp": make([]container.PortBinding, len(tc.ips)),
"443/udp": make([]container.PortBinding, len(tc.ips)),
}
for i, ip := range tc.ips {
ci.NetworkSettings.Ports["80/tcp"][i] = nat.PortBinding{
ci.NetworkSettings.Ports["80/tcp"][i] = container.PortBinding{
HostIP: ip, HostPort: "3456",
}
ci.NetworkSettings.Ports["443/tcp"][i] = nat.PortBinding{
ci.NetworkSettings.Ports["443/tcp"][i] = container.PortBinding{
HostIP: ip, HostPort: "4567",
}
ci.NetworkSettings.Ports["443/udp"][i] = nat.PortBinding{
ci.NetworkSettings.Ports["443/udp"][i] = container.PortBinding{
HostIP: ip, HostPort: "5678",
}
}

View File

@ -16,12 +16,13 @@ import (
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/notary"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/moby/moby/api/types"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/api/types/jsonstream"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"github.com/moby/moby/client/pkg/jsonmessage"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/pflag"
"gotest.tools/v3/assert"
@ -256,9 +257,11 @@ func TestRunPullTermination(t *testing.T) {
TimeNano: time.Now().UnixNano(),
Time: time.Now().Unix(),
Progress: &jsonmessage.JSONProgress{
Current: int64(i),
Total: 100,
Start: 0,
Progress: jsonstream.Progress{
Current: int64(i),
Total: 100,
Start: 0,
},
},
}))
time.Sleep(100 * time.Millisecond)

View File

@ -20,9 +20,9 @@ import (
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/jsonstream"
"github.com/docker/cli/opts"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
"github.com/moby/go-archive"
"github.com/moby/moby/api/pkg/progress"
"github.com/moby/moby/api/pkg/streamformatter"
buildtypes "github.com/moby/moby/api/types/build"
"github.com/moby/moby/api/types/container"
registrytypes "github.com/moby/moby/api/types/registry"

View File

@ -17,10 +17,10 @@ import (
"time"
"github.com/docker/cli/cli/command/image/build/internal/git"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
"github.com/moby/go-archive"
"github.com/moby/go-archive/compression"
"github.com/moby/moby/api/pkg/progress"
"github.com/moby/moby/api/pkg/streamformatter"
"github.com/moby/patternmatcher"
"github.com/pkg/errors"
)

View File

@ -16,7 +16,7 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/idresolver"
"github.com/docker/cli/internal/logdetails"
"github.com/moby/moby/api/stdcopy"
"github.com/moby/moby/api/pkg/stdcopy"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"

View File

@ -12,8 +12,8 @@ import (
"time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
"github.com/moby/moby/api/pkg/progress"
"github.com/moby/moby/api/pkg/streamformatter"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"

View File

@ -5,7 +5,7 @@ import (
"strconv"
"testing"
"github.com/docker/docker/pkg/progress"
"github.com/moby/moby/api/pkg/progress"
"github.com/moby/moby/api/types/swarm"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"

View File

@ -8,8 +8,8 @@ import (
"os/signal"
"time"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
"github.com/moby/moby/api/pkg/progress"
"github.com/moby/moby/api/pkg/streamformatter"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"github.com/opencontainers/go-digest"

View File

@ -24,6 +24,7 @@ import (
"github.com/docker/go-units"
"github.com/go-viper/mapstructure/v2"
"github.com/google/shlex"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/versions"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@ -934,7 +935,7 @@ func toServicePortConfigs(value string) ([]any, error) {
for _, key := range keys {
// Reuse ConvertPortToPortConfig so that it is consistent
portConfig, err := swarmopts.ConvertPortToPortConfig(nat.Port(key), portBindings)
portConfig, err := swarmopts.ConvertPortToPortConfig(container.PortRangeProto(key), portBindings)
if err != nil {
return nil, err
}

View File

@ -4,13 +4,14 @@ import (
"context"
"io"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/cli/cli/streams"
"github.com/moby/moby/api/types/jsonstream"
"github.com/moby/moby/client/pkg/jsonmessage"
)
type (
Stream = jsonmessage.Stream
JSONError = jsonstream.Error
JSONMessage = jsonmessage.JSONMessage
JSONError = jsonmessage.JSONError
JSONProgress = jsonmessage.JSONProgress
)
@ -46,7 +47,7 @@ func WithAuxCallback(cb func(JSONMessage)) Options {
// "context aware" and appropriately returns why the function was canceled.
//
// It returns an error if the context is canceled, but not if the input reader / stream is closed.
func Display(ctx context.Context, in io.Reader, stream Stream, opts ...Options) error {
func Display(ctx context.Context, in io.Reader, stream *streams.Out, opts ...Options) error {
if ctx.Err() != nil {
return ctx.Err()
}

View File

@ -9,6 +9,7 @@ import (
"time"
"github.com/docker/cli/cli/streams"
"github.com/moby/moby/api/types/jsonstream"
"gotest.tools/v3/assert"
)
@ -35,9 +36,11 @@ func TestDisplay(t *testing.T) {
TimeNano: time.Now().UnixNano(),
Time: time.Now().Unix(),
Progress: &JSONProgress{
Current: int64(i),
Total: 100,
Start: 0,
Progress: jsonstream.Progress{
Current: int64(i),
Total: 100,
Start: 0,
},
},
})
if err != nil {

View File

@ -10,6 +10,7 @@ import (
"strings"
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/swarm"
"github.com/sirupsen/logrus"
)
@ -150,28 +151,31 @@ func (p *PortOpt) Value() []swarm.PortConfig {
// ConvertPortToPortConfig converts ports to the swarm type
func ConvertPortToPortConfig(
port nat.Port,
portBindings map[nat.Port][]nat.PortBinding,
portRangeProto container.PortRangeProto,
portBindings map[container.PortRangeProto][]container.PortBinding,
) ([]swarm.PortConfig, error) {
ports := []swarm.PortConfig{}
proto, port := nat.SplitProtoPort(string(portRangeProto))
portInt, _ := strconv.ParseUint(port, 10, 16)
proto = strings.ToLower(proto)
for _, binding := range portBindings[port] {
var ports []swarm.PortConfig
for _, binding := range portBindings[portRangeProto] {
if p := net.ParseIP(binding.HostIP); p != nil && !p.IsUnspecified() {
// TODO(thaJeztah): use context-logger, so that this output can be suppressed (in tests).
logrus.Warnf("ignoring IP-address (%s:%s) service will listen on '0.0.0.0'", net.JoinHostPort(binding.HostIP, binding.HostPort), port)
logrus.Warnf("ignoring IP-address (%s:%s) service will listen on '0.0.0.0'", net.JoinHostPort(binding.HostIP, binding.HostPort), portRangeProto)
}
startHostPort, endHostPort, err := nat.ParsePortRange(binding.HostPort)
if err != nil && binding.HostPort != "" {
return nil, fmt.Errorf("invalid hostport binding (%s) for port (%s)", binding.HostPort, port.Port())
return nil, fmt.Errorf("invalid hostport binding (%s) for port (%s)", binding.HostPort, port)
}
for i := startHostPort; i <= endHostPort; i++ {
ports = append(ports, swarm.PortConfig{
// TODO Name: ?
Protocol: swarm.PortConfigProtocol(strings.ToLower(port.Proto())),
TargetPort: uint32(port.Int()),
Protocol: swarm.PortConfigProtocol(proto),
TargetPort: uint32(portInt),
PublishedPort: uint32(i),
PublishMode: swarm.PortConfigPublishModeIngress,
})

View File

@ -5,7 +5,7 @@ import (
"os"
"testing"
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/swarm"
"github.com/sirupsen/logrus"
"gotest.tools/v3/assert"
@ -347,7 +347,7 @@ func TestConvertPortToPortConfigWithIP(t *testing.T) {
logrus.SetOutput(&b)
for _, tc := range testCases {
t.Run(tc.value, func(t *testing.T) {
_, err := ConvertPortToPortConfig("80/tcp", map[nat.Port][]nat.PortBinding{
_, err := ConvertPortToPortConfig("80/tcp", map[container.PortRangeProto][]container.PortBinding{
"80/tcp": {{HostIP: tc.value, HostPort: "2345"}},
})
assert.NilError(t, err)

View File

@ -8,8 +8,8 @@ go 1.23.0
replace (
// FIXME(thaJeztah): temporarily need to pin on commits, otherwise go modules won't resolve until these are tagged.
github.com/moby/moby/api => github.com/moby/moby/api v0.0.0-20250729125042-2574c2b2e917
github.com/moby/moby/client => github.com/moby/moby/client v0.0.0-20250729125042-2574c2b2e917
github.com/moby/moby/api => github.com/moby/moby/api v0.0.0-20250731152656-4faedf2bec36
github.com/moby/moby/client => github.com/moby/moby/client v0.0.0-20250731152656-4faedf2bec36
)
require (
@ -22,7 +22,7 @@ require (
github.com/distribution/reference v0.6.0
github.com/docker/cli-docs-tool v0.10.0
github.com/docker/distribution v2.8.3+incompatible
github.com/docker/docker v28.2.3-0.20250729125042-2574c2b2e917+incompatible // master (v29.0-dev)
github.com/docker/docker v28.2.3-0.20250731152656-4faedf2bec36+incompatible // master (v29.0-dev)
github.com/docker/docker-credential-helpers v0.9.3
github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0

View File

@ -57,8 +57,8 @@ github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09f
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v28.2.3-0.20250729125042-2574c2b2e917+incompatible h1:PmHpYwC9kywp7yqeiUlJYvQaOXoYh7Xy5C1N7Z+urVo=
github.com/docker/docker v28.2.3-0.20250729125042-2574c2b2e917+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.2.3-0.20250731152656-4faedf2bec36+incompatible h1:V1yo9808DrQaHES6Q3hW5Rtd44+mrc4oKulmzPgRDAM=
github.com/docker/docker v28.2.3-0.20250731152656-4faedf2bec36+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
@ -172,10 +172,10 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/moby/api v0.0.0-20250729125042-2574c2b2e917 h1:kUXvd5/UxuA9rOrBBJ79GKKuBD/HR76jJBZed/bg4+E=
github.com/moby/moby/api v0.0.0-20250729125042-2574c2b2e917/go.mod h1:UXRe3/H6L2SB5SrOqAhx5tsC0RqrhqtBoY8PFxjyp5k=
github.com/moby/moby/client v0.0.0-20250729125042-2574c2b2e917 h1:roWTPpD2XjM1Z8uYZO4HAD1pUKSpm1X5S3JqNsWYnVE=
github.com/moby/moby/client v0.0.0-20250729125042-2574c2b2e917/go.mod h1:aJbR/d6IN3wIBmj3XPFEY5E5Ndd6F57YQsqivzmCyO4=
github.com/moby/moby/api v0.0.0-20250731152656-4faedf2bec36 h1:2o6bmPZvLOYqE6mQGqnyt+556TMpa26ZtSxvgSpT+98=
github.com/moby/moby/api v0.0.0-20250731152656-4faedf2bec36/go.mod h1:GNQ0zU3WJGeJIcrLPE3xiQnsnLElvfqXQZZZiOqWuiE=
github.com/moby/moby/client v0.0.0-20250731152656-4faedf2bec36 h1:o32ntpp76dDd5Hf9+XL09+qs06120KvZl+Ogt+RkYG0=
github.com/moby/moby/client v0.0.0-20250731152656-4faedf2bec36/go.mod h1:+TFnycF6RuAUAq1tGTjZiSXuRcywpKUGo2dLkJ6tC48=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/swarmkit/v2 v2.0.0 h1:jkWQKQaJ4ltA61/mC9UdPe1McLma55RUcacTO+pPweY=

View File

@ -1,164 +0,0 @@
// Package streamformatter provides helper functions to format a stream.
package streamformatter
import (
"encoding/json"
"fmt"
"io"
"sync"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/progress"
)
const streamNewline = "\r\n"
type jsonProgressFormatter struct{}
func appendNewline(source []byte) []byte {
return append(source, []byte(streamNewline)...)
}
// FormatStatus formats the specified objects according to the specified format (and id).
func FormatStatus(id, format string, a ...interface{}) []byte {
str := fmt.Sprintf(format, a...)
b, err := json.Marshal(&jsonmessage.JSONMessage{ID: id, Status: str})
if err != nil {
return FormatError(err)
}
return appendNewline(b)
}
// FormatError formats the error as a JSON object
func FormatError(err error) []byte {
jsonError, ok := err.(*jsonmessage.JSONError)
if !ok {
jsonError = &jsonmessage.JSONError{Message: err.Error()}
}
if b, err := json.Marshal(&jsonmessage.JSONMessage{Error: jsonError, ErrorMessage: err.Error()}); err == nil {
return appendNewline(b)
}
return []byte(`{"error":"format error"}` + streamNewline)
}
func (sf *jsonProgressFormatter) formatStatus(id, format string, a ...interface{}) []byte {
return FormatStatus(id, format, a...)
}
// formatProgress formats the progress information for a specified action.
func (sf *jsonProgressFormatter) formatProgress(id, action string, progress *jsonmessage.JSONProgress, aux interface{}) []byte {
if progress == nil {
progress = &jsonmessage.JSONProgress{}
}
var auxJSON *json.RawMessage
if aux != nil {
auxJSONBytes, err := json.Marshal(aux)
if err != nil {
return nil
}
auxJSON = new(json.RawMessage)
*auxJSON = auxJSONBytes
}
b, err := json.Marshal(&jsonmessage.JSONMessage{
Status: action,
ProgressMessage: progress.String(),
Progress: progress,
ID: id,
Aux: auxJSON,
})
if err != nil {
return nil
}
return appendNewline(b)
}
type rawProgressFormatter struct{}
func (sf *rawProgressFormatter) formatStatus(id, format string, a ...interface{}) []byte {
return []byte(fmt.Sprintf(format, a...) + streamNewline)
}
func (sf *rawProgressFormatter) formatProgress(id, action string, progress *jsonmessage.JSONProgress, aux interface{}) []byte {
if progress == nil {
progress = &jsonmessage.JSONProgress{}
}
endl := "\r"
if progress.String() == "" {
endl += "\n"
}
return []byte(action + " " + progress.String() + endl)
}
// NewProgressOutput returns a progress.Output object that can be passed to
// progress.NewProgressReader.
func NewProgressOutput(out io.Writer) progress.Output {
return &progressOutput{sf: &rawProgressFormatter{}, out: out, newLines: true}
}
// NewJSONProgressOutput returns a progress.Output that formats output
// using JSON objects
func NewJSONProgressOutput(out io.Writer, newLines bool) progress.Output {
return &progressOutput{sf: &jsonProgressFormatter{}, out: out, newLines: newLines}
}
type formatProgress interface {
formatStatus(id, format string, a ...interface{}) []byte
formatProgress(id, action string, progress *jsonmessage.JSONProgress, aux interface{}) []byte
}
type progressOutput struct {
sf formatProgress
out io.Writer
newLines bool
mu sync.Mutex
}
// WriteProgress formats progress information from a ProgressReader.
func (out *progressOutput) WriteProgress(prog progress.Progress) error {
var formatted []byte
if prog.Message != "" {
formatted = out.sf.formatStatus(prog.ID, prog.Message)
} else {
jsonProgress := jsonmessage.JSONProgress{Current: prog.Current, Total: prog.Total, HideCounts: prog.HideCounts, Units: prog.Units}
formatted = out.sf.formatProgress(prog.ID, prog.Action, &jsonProgress, prog.Aux)
}
out.mu.Lock()
defer out.mu.Unlock()
_, err := out.out.Write(formatted)
if err != nil {
return err
}
if out.newLines && prog.LastUpdate {
_, err = out.out.Write(out.sf.formatStatus("", ""))
return err
}
return nil
}
// AuxFormatter is a streamFormatter that writes aux progress messages
type AuxFormatter struct {
io.Writer
}
// Emit emits the given interface as an aux progress message
func (sf *AuxFormatter) Emit(id string, aux interface{}) error {
auxJSONBytes, err := json.Marshal(aux)
if err != nil {
return err
}
auxJSON := new(json.RawMessage)
*auxJSON = auxJSONBytes
msgJSON, err := json.Marshal(&jsonmessage.JSONMessage{ID: id, Aux: auxJSON})
if err != nil {
return err
}
msgJSON = appendNewline(msgJSON)
n, err := sf.Writer.Write(msgJSON)
if n != len(msgJSON) {
return io.ErrShortWrite
}
return err
}

View File

@ -0,0 +1,247 @@
// Package streamformatter provides helper functions to format a stream.
package streamformatter
import (
"encoding/json"
"fmt"
"io"
"strings"
"sync"
"time"
"github.com/docker/go-units"
"github.com/moby/moby/api/pkg/progress"
"github.com/moby/moby/api/types/jsonstream"
)
// jsonMessage defines a message struct. It describes
// the created time, where it from, status, ID of the
// message. It's used for docker events.
//
// It is a reduced set of [jsonmessage.JSONMessage].
type jsonMessage struct {
Stream string `json:"stream,omitempty"`
Status string `json:"status,omitempty"`
Progress *jsonstream.Progress `json:"progressDetail,omitempty"`
ID string `json:"id,omitempty"`
Error *jsonstream.Error `json:"errorDetail,omitempty"`
Aux *json.RawMessage `json:"aux,omitempty"` // Aux contains out-of-band data, such as digests for push signing and image id after building.
// ErrorMessage contains errors encountered during the operation.
//
// Deprecated: this field is deprecated since docker v0.6.0 / API v1.4. Use [Error.Message] instead. This field will be omitted in a future release.
ErrorMessage string `json:"error,omitempty"` // deprecated
}
const streamNewline = "\r\n"
type jsonProgressFormatter struct{}
func appendNewline(source []byte) []byte {
return append(source, []byte(streamNewline)...)
}
// FormatStatus formats the specified objects according to the specified format (and id).
func FormatStatus(id, format string, a ...interface{}) []byte {
str := fmt.Sprintf(format, a...)
b, err := json.Marshal(&jsonMessage{ID: id, Status: str})
if err != nil {
return FormatError(err)
}
return appendNewline(b)
}
// FormatError formats the error as a JSON object
func FormatError(err error) []byte {
jsonError, ok := err.(*jsonstream.Error)
if !ok {
jsonError = &jsonstream.Error{Message: err.Error()}
}
if b, err := json.Marshal(&jsonMessage{Error: jsonError, ErrorMessage: err.Error()}); err == nil {
return appendNewline(b)
}
return []byte(`{"error":"format error"}` + streamNewline)
}
func (sf *jsonProgressFormatter) formatStatus(id, format string, a ...interface{}) []byte {
return FormatStatus(id, format, a...)
}
// formatProgress formats the progress information for a specified action.
func (sf *jsonProgressFormatter) formatProgress(id, action string, progress *jsonstream.Progress, aux interface{}) []byte {
if progress == nil {
progress = &jsonstream.Progress{}
}
var auxJSON *json.RawMessage
if aux != nil {
auxJSONBytes, err := json.Marshal(aux)
if err != nil {
return nil
}
auxJSON = new(json.RawMessage)
*auxJSON = auxJSONBytes
}
b, err := json.Marshal(&jsonMessage{
Status: action,
Progress: progress,
ID: id,
Aux: auxJSON,
})
if err != nil {
return nil
}
return appendNewline(b)
}
type rawProgressFormatter struct{}
func (sf *rawProgressFormatter) formatStatus(id, format string, a ...interface{}) []byte {
return []byte(fmt.Sprintf(format, a...) + streamNewline)
}
func rawProgressString(p *jsonstream.Progress) string {
if p == nil || (p.Current <= 0 && p.Total <= 0) {
return ""
}
if p.Total <= 0 {
switch p.Units {
case "":
return fmt.Sprintf("%8v", units.HumanSize(float64(p.Current)))
default:
return fmt.Sprintf("%d %s", p.Current, p.Units)
}
}
percentage := int(float64(p.Current)/float64(p.Total)*100) / 2
if percentage > 50 {
percentage = 50
}
numSpaces := 0
if 50-percentage > 0 {
numSpaces = 50 - percentage
}
pbBox := fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces))
var numbersBox string
switch {
case p.HideCounts:
case p.Units == "": // no units, use bytes
current := units.HumanSize(float64(p.Current))
total := units.HumanSize(float64(p.Total))
numbersBox = fmt.Sprintf("%8v/%v", current, total)
if p.Current > p.Total {
// remove total display if the reported current is wonky.
numbersBox = fmt.Sprintf("%8v", current)
}
default:
numbersBox = fmt.Sprintf("%d/%d %s", p.Current, p.Total, p.Units)
if p.Current > p.Total {
// remove total display if the reported current is wonky.
numbersBox = fmt.Sprintf("%d %s", p.Current, p.Units)
}
}
var timeLeftBox string
if p.Current > 0 && p.Start > 0 && percentage < 50 {
fromStart := time.Since(time.Unix(p.Start, 0))
perEntry := fromStart / time.Duration(p.Current)
left := time.Duration(p.Total-p.Current) * perEntry
timeLeftBox = " " + left.Round(time.Second).String()
}
return pbBox + numbersBox + timeLeftBox
}
func (sf *rawProgressFormatter) formatProgress(id, action string, progress *jsonstream.Progress, aux interface{}) []byte {
if progress == nil {
progress = &jsonstream.Progress{}
}
endl := "\r"
out := rawProgressString(progress)
if out == "" {
endl += "\n"
}
return []byte(action + " " + out + endl)
}
// NewProgressOutput returns a progress.Output object that can be passed to
// progress.NewProgressReader.
func NewProgressOutput(out io.Writer) progress.Output {
return &progressOutput{sf: &rawProgressFormatter{}, out: out, newLines: true}
}
// NewJSONProgressOutput returns a progress.Output that formats output
// using JSON objects
func NewJSONProgressOutput(out io.Writer, newLines bool) progress.Output {
return &progressOutput{sf: &jsonProgressFormatter{}, out: out, newLines: newLines}
}
type formatProgress interface {
formatStatus(id, format string, a ...interface{}) []byte
formatProgress(id, action string, progress *jsonstream.Progress, aux interface{}) []byte
}
type progressOutput struct {
sf formatProgress
out io.Writer
newLines bool
mu sync.Mutex
}
// WriteProgress formats progress information from a ProgressReader.
func (out *progressOutput) WriteProgress(prog progress.Progress) error {
var formatted []byte
if prog.Message != "" {
formatted = out.sf.formatStatus(prog.ID, prog.Message)
} else {
jsonProgress := jsonstream.Progress{
Current: prog.Current,
Total: prog.Total,
HideCounts: prog.HideCounts,
Units: prog.Units,
}
formatted = out.sf.formatProgress(prog.ID, prog.Action, &jsonProgress, prog.Aux)
}
out.mu.Lock()
defer out.mu.Unlock()
_, err := out.out.Write(formatted)
if err != nil {
return err
}
if out.newLines && prog.LastUpdate {
_, err = out.out.Write(out.sf.formatStatus("", ""))
return err
}
return nil
}
// AuxFormatter is a streamFormatter that writes aux progress messages
type AuxFormatter struct {
io.Writer
}
// Emit emits the given interface as an aux progress message
func (sf *AuxFormatter) Emit(id string, aux interface{}) error {
auxJSONBytes, err := json.Marshal(aux)
if err != nil {
return err
}
auxJSON := new(json.RawMessage)
*auxJSON = auxJSONBytes
msgJSON, err := json.Marshal(&jsonMessage{ID: id, Aux: auxJSON})
if err != nil {
return err
}
msgJSON = appendNewline(msgJSON)
n, err := sf.Writer.Write(msgJSON)
if n != len(msgJSON) {
return io.ErrShortWrite
}
return err
}

View File

@ -3,8 +3,6 @@ package streamformatter
import (
"encoding/json"
"io"
"github.com/docker/docker/pkg/jsonmessage"
)
type streamWriter struct {
@ -22,7 +20,7 @@ func (sw *streamWriter) Write(buf []byte) (int, error) {
}
func (sw *streamWriter) format(buf []byte) []byte {
msg := &jsonmessage.JSONMessage{Stream: sw.lineFormat(buf)}
msg := &jsonMessage{Stream: sw.lineFormat(buf)}
b, err := json.Marshal(msg)
if err != nil {
return FormatError(err)

View File

@ -3,7 +3,6 @@ package container
import (
"time"
"github.com/docker/go-connections/nat"
dockerspec "github.com/moby/docker-image-spec/specs-go/v1"
"github.com/moby/moby/api/types/strslice"
)
@ -48,7 +47,7 @@ type Config struct {
AttachStdin bool // Attach the standard input, makes possible user interaction
AttachStdout bool // Attach the standard output
AttachStderr bool // Attach the standard error
ExposedPorts nat.PortSet `json:",omitempty"` // List of exposed ports
ExposedPorts PortSet `json:",omitempty"` // List of exposed ports
Tty bool // Attach standard streams to a tty, including stdin if it is not closed.
OpenStdin bool // Open stdin
StdinOnce bool // If true, close stdin after the 1 attached client disconnects.

View File

@ -5,7 +5,6 @@ import (
"fmt"
"strings"
"github.com/docker/go-connections/nat"
"github.com/docker/go-units"
"github.com/moby/moby/api/types/blkiodev"
"github.com/moby/moby/api/types/mount"
@ -427,7 +426,7 @@ type HostConfig struct {
ContainerIDFile string // File (path) where the containerId is written
LogConfig LogConfig // Configuration of the logs for this container
NetworkMode NetworkMode // Network mode to use for the container
PortBindings nat.PortMap // Port mapping between the exposed port (container) and the host
PortBindings PortMap // Port mapping between the exposed port (container) and the host
RestartPolicy RestartPolicy // Restart policy to be used for the container
AutoRemove bool // Automatically remove container when it exits
VolumeDriver string // Name of the volume driver used to mount volumes

View File

@ -0,0 +1,24 @@
package container
import "github.com/docker/go-connections/nat"
// PortRangeProto is a string containing port number and protocol in the format "80/tcp",
// or a port range and protocol in the format "80-83/tcp".
//
// It is currently an alias for [nat.Port] but may become a concrete type in a future release.
type PortRangeProto = nat.Port
// PortSet is a collection of structs indexed by [HostPort].
//
// It is currently an alias for [nat.PortSet] but may become a concrete type in a future release.
type PortSet = nat.PortSet
// PortBinding represents a binding between a Host IP address and a [HostPort].
//
// It is currently an alias for [nat.PortBinding] but may become a concrete type in a future release.
type PortBinding = nat.PortBinding
// PortMap is a collection of [PortBinding] indexed by [HostPort].
//
// It is currently an alias for [nat.PortMap] but may become a concrete type in a future release.
type PortMap = nat.PortMap

View File

@ -1,7 +1,6 @@
package container
import (
"github.com/docker/go-connections/nat"
"github.com/moby/moby/api/types/network"
)
@ -14,10 +13,10 @@ type NetworkSettings struct {
// NetworkSettingsBase holds networking state for a container when inspecting it.
type NetworkSettingsBase struct {
Bridge string // Bridge contains the name of the default bridge interface iff it was set through the daemon --bridge flag.
SandboxID string // SandboxID uniquely represents a container's network stack
SandboxKey string // SandboxKey identifies the sandbox
Ports nat.PortMap // Ports is a collection of PortBinding indexed by Port
Bridge string // Bridge contains the name of the default bridge interface iff it was set through the daemon --bridge flag.
SandboxID string // SandboxID uniquely represents a container's network stack
SandboxKey string // SandboxKey identifies the sandbox
Ports PortMap // Ports is a collection of PortBinding indexed by Port
// HairpinMode specifies if hairpin NAT should be enabled on the virtual interface
//

View File

@ -0,0 +1,12 @@
package jsonstream
// Error wraps a concrete Code and Message, Code is
// an integer error code, Message is the error message.
type Error struct {
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
func (e *Error) Error() string {
return e.Message
}

View File

@ -0,0 +1,10 @@
package jsonstream
// Progress describes a progress message in a JSON stream.
type Progress struct {
Current int64 `json:"current,omitempty"` // Current is the current status and value of the progress made towards Total.
Total int64 `json:"total,omitempty"` // Total is the end value describing when we made 100% progress for an operation.
Start int64 `json:"start,omitempty"` // Start is the initial value for the operation.
HideCounts bool `json:"hidecounts,omitempty"` // HideCounts. if true, hides the progress count indicator (xB/yB).
Units string `json:"units,omitempty"` // Units is the unit to print for progress. It defaults to "bytes" if empty.
}

View File

@ -52,9 +52,11 @@ type AuthConfig struct {
//
// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5
func EncodeAuthConfig(authConfig AuthConfig) (string, error) {
if authConfig == (AuthConfig{}) {
return "", nil
}
// Older daemons (or registries) may not handle an empty string,
// which resulted in an "io.EOF" when unmarshaling or decoding.
//
// FIXME(thaJeztah): find exactly what code-paths are impacted by this.
// if authConfig == (AuthConfig{}) { return "", nil }
buf, err := json.Marshal(authConfig)
if err != nil {
return "", errInvalidParameter{err}

View File

@ -8,37 +8,17 @@ import (
"time"
"github.com/docker/go-units"
"github.com/moby/moby/api/types/jsonstream"
"github.com/moby/term"
"github.com/morikuni/aec"
)
// RFC3339NanoFixed is time.RFC3339Nano with nanoseconds padded using zeros to
// ensure the formatted time isalways the same number of characters.
const RFC3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
// JSONError wraps a concrete Code and Message, Code is
// an integer error code, Message is the error message.
type JSONError struct {
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
func (e *JSONError) Error() string {
return e.Message
}
// JSONProgress describes a progress message in a JSON stream.
type JSONProgress struct {
// Current is the current status and value of the progress made towards Total.
Current int64 `json:"current,omitempty"`
// Total is the end value describing when we made 100% progress for an operation.
Total int64 `json:"total,omitempty"`
// Start is the initial value for the operation.
Start int64 `json:"start,omitempty"`
// HideCounts. if true, hides the progress count indicator (xB/yB).
HideCounts bool `json:"hidecounts,omitempty"`
// Units is the unit to print for progress. It defaults to "bytes" if empty.
Units string `json:"units,omitempty"`
jsonstream.Progress
// terminalFd is the fd of the current terminal, if any. It is used
// to get the terminal width.
@ -142,40 +122,44 @@ func (p *JSONProgress) width() int {
// the created time, where it from, status, ID of the
// message. It's used for docker events.
type JSONMessage struct {
Stream string `json:"stream,omitempty"`
Status string `json:"status,omitempty"`
Progress *JSONProgress `json:"progressDetail,omitempty"`
// ProgressMessage is a pre-formatted presentation of [Progress].
//
// Deprecated: this field is deprecated since docker v0.7.1 / API v1.8. Use the information in [Progress] instead. This field will be omitted in a future release.
ProgressMessage string `json:"progress,omitempty"`
ID string `json:"id,omitempty"`
From string `json:"from,omitempty"`
Time int64 `json:"time,omitempty"`
TimeNano int64 `json:"timeNano,omitempty"`
Error *JSONError `json:"errorDetail,omitempty"`
// ErrorMessage contains errors encountered during the operation.
//
// Deprecated: this field is deprecated since docker v0.6.0 / API v1.4. Use [Error.Message] instead. This field will be omitted in a future release.
ErrorMessage string `json:"error,omitempty"` // deprecated
// Aux contains out-of-band data, such as digests for push signing and image id after building.
Aux *json.RawMessage `json:"aux,omitempty"`
Stream string `json:"stream,omitempty"`
Status string `json:"status,omitempty"`
Progress *JSONProgress `json:"progressDetail,omitempty"`
ID string `json:"id,omitempty"`
From string `json:"from,omitempty"`
Time int64 `json:"time,omitempty"`
TimeNano int64 `json:"timeNano,omitempty"`
Error *jsonstream.Error `json:"errorDetail,omitempty"`
Aux *json.RawMessage `json:"aux,omitempty"` // Aux contains out-of-band data, such as digests for push signing and image id after building.
}
// We can probably use [aec.EmptyBuilder] for managing the output, but
// currently we're doing it all manually, so defining some consts for
// the basics we use.
//
// [aec.EmptyBuilder]: https://pkg.go.dev/github.com/morikuni/aec#EmptyBuilder
const (
ansiEraseLine = "\x1b[2K" // Erase entire line
ansiCursorUpFmt = "\x1b[%dA" // Move cursor up N lines
ansiCursorDownFmt = "\x1b[%dB" // Move cursor down N lines
)
func clearLine(out io.Writer) {
eraseMode := aec.EraseModes.All
cl := aec.EraseLine(eraseMode)
fmt.Fprint(out, cl)
_, _ = out.Write([]byte(ansiEraseLine))
}
func cursorUp(out io.Writer, l uint) {
fmt.Fprint(out, aec.Up(l))
if l == 0 {
return
}
_, _ = fmt.Fprintf(out, ansiCursorUpFmt, l)
}
func cursorDown(out io.Writer, l uint) {
fmt.Fprint(out, aec.Down(l))
if l == 0 {
return
}
_, _ = fmt.Fprintf(out, ansiCursorDownFmt, l)
}
// Display prints the JSONMessage to out. If isTerminal is true, it erases
@ -189,29 +173,27 @@ func (jm *JSONMessage) Display(out io.Writer, isTerminal bool) error {
if isTerminal && jm.Stream == "" && jm.Progress != nil {
clearLine(out)
endl = "\r"
fmt.Fprint(out, endl)
_, _ = fmt.Fprint(out, endl)
} else if jm.Progress != nil && jm.Progress.String() != "" { // disable progressbar in non-terminal
return nil
}
if jm.TimeNano != 0 {
fmt.Fprintf(out, "%s ", time.Unix(0, jm.TimeNano).Format(RFC3339NanoFixed))
_, _ = fmt.Fprintf(out, "%s ", time.Unix(0, jm.TimeNano).Format(RFC3339NanoFixed))
} else if jm.Time != 0 {
fmt.Fprintf(out, "%s ", time.Unix(jm.Time, 0).Format(RFC3339NanoFixed))
_, _ = fmt.Fprintf(out, "%s ", time.Unix(jm.Time, 0).Format(RFC3339NanoFixed))
}
if jm.ID != "" {
fmt.Fprintf(out, "%s: ", jm.ID)
_, _ = fmt.Fprintf(out, "%s: ", jm.ID)
}
if jm.From != "" {
fmt.Fprintf(out, "(from %s) ", jm.From)
_, _ = fmt.Fprintf(out, "(from %s) ", jm.From)
}
if jm.Progress != nil && isTerminal {
fmt.Fprintf(out, "%s %s%s", jm.Status, jm.Progress.String(), endl)
} else if jm.ProgressMessage != "" { // deprecated
fmt.Fprintf(out, "%s %s%s", jm.Status, jm.ProgressMessage, endl)
_, _ = fmt.Fprintf(out, "%s %s%s", jm.Status, jm.Progress.String(), endl)
} else if jm.Stream != "" {
fmt.Fprintf(out, "%s%s", jm.Stream, endl)
_, _ = fmt.Fprintf(out, "%s%s", jm.Stream, endl)
} else {
fmt.Fprintf(out, "%s%s\n", jm.Status, endl)
_, _ = fmt.Fprintf(out, "%s%s\n", jm.Status, endl)
}
return nil
}
@ -258,7 +240,7 @@ func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr,
if jm.Progress != nil {
jm.Progress.terminalFd = terminalFd
}
if jm.ID != "" && (jm.Progress != nil || jm.ProgressMessage != "") {
if jm.ID != "" && jm.Progress != nil {
line, ok := ids[jm.ID]
if !ok {
// NOTE: This approach of using len(id) to
@ -270,7 +252,7 @@ func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr,
line = uint(len(ids))
ids[jm.ID] = line
if isTerminal {
fmt.Fprintf(out, "\n")
_, _ = fmt.Fprintf(out, "\n")
}
}
diff = uint(len(ids)) - line
@ -295,20 +277,3 @@ func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr,
}
return nil
}
// Stream is an io.Writer for output with utilities to get the output's file
// descriptor and to detect whether it's a terminal.
//
// it is subset of the streams.Out type in
// https://pkg.go.dev/github.com/docker/cli@v20.10.17+incompatible/cli/streams#Out
type Stream interface {
io.Writer
FD() uintptr
IsTerminal() bool
}
// DisplayJSONMessagesToStream prints json messages to the output Stream. It is
// used by the Docker CLI to print JSONMessage streams.
func DisplayJSONMessagesToStream(in io.Reader, stream Stream, auxCallback func(JSONMessage)) error {
return DisplayJSONMessagesStream(in, stream, stream.FD(), stream.IsTerminal(), auxCallback)
}

19
vendor/modules.txt vendored
View File

@ -65,12 +65,9 @@ github.com/docker/distribution/registry/client/transport
github.com/docker/distribution/registry/storage/cache
github.com/docker/distribution/registry/storage/cache/memory
github.com/docker/distribution/uuid
# github.com/docker/docker v28.2.3-0.20250729125042-2574c2b2e917+incompatible
# github.com/docker/docker v28.2.3-0.20250731152656-4faedf2bec36+incompatible
## explicit
github.com/docker/docker/pkg/jsonmessage
github.com/docker/docker/pkg/process
github.com/docker/docker/pkg/progress
github.com/docker/docker/pkg/streamformatter
# github.com/docker/docker-credential-helpers v0.9.3
## explicit; go 1.21
github.com/docker/docker-credential-helpers/client
@ -174,9 +171,11 @@ github.com/moby/docker-image-spec/specs-go/v1
github.com/moby/go-archive
github.com/moby/go-archive/compression
github.com/moby/go-archive/tarheader
# github.com/moby/moby/api v0.0.0 => github.com/moby/moby/api v0.0.0-20250729125042-2574c2b2e917
# github.com/moby/moby/api v0.0.0 => github.com/moby/moby/api v0.0.0-20250731152656-4faedf2bec36
## explicit; go 1.23.0
github.com/moby/moby/api/stdcopy
github.com/moby/moby/api/pkg/progress
github.com/moby/moby/api/pkg/stdcopy
github.com/moby/moby/api/pkg/streamformatter
github.com/moby/moby/api/types
github.com/moby/moby/api/types/auxprogress
github.com/moby/moby/api/types/blkiodev
@ -187,6 +186,7 @@ github.com/moby/moby/api/types/container
github.com/moby/moby/api/types/events
github.com/moby/moby/api/types/filters
github.com/moby/moby/api/types/image
github.com/moby/moby/api/types/jsonstream
github.com/moby/moby/api/types/mount
github.com/moby/moby/api/types/network
github.com/moby/moby/api/types/registry
@ -197,9 +197,10 @@ github.com/moby/moby/api/types/system
github.com/moby/moby/api/types/time
github.com/moby/moby/api/types/versions
github.com/moby/moby/api/types/volume
# github.com/moby/moby/client v0.0.0 => github.com/moby/moby/client v0.0.0-20250729125042-2574c2b2e917
# github.com/moby/moby/client v0.0.0 => github.com/moby/moby/client v0.0.0-20250731152656-4faedf2bec36
## explicit; go 1.23.0
github.com/moby/moby/client
github.com/moby/moby/client/pkg/jsonmessage
github.com/moby/moby/client/pkg/stringid
# github.com/moby/patternmatcher v0.6.0
## explicit; go 1.19
@ -571,5 +572,5 @@ gotest.tools/v3/skip
# tags.cncf.io/container-device-interface v0.8.0
## explicit; go 1.20
tags.cncf.io/container-device-interface/pkg/parser
# github.com/moby/moby/api => github.com/moby/moby/api v0.0.0-20250729125042-2574c2b2e917
# github.com/moby/moby/client => github.com/moby/moby/client v0.0.0-20250729125042-2574c2b2e917
# github.com/moby/moby/api => github.com/moby/moby/api v0.0.0-20250731152656-4faedf2bec36
# github.com/moby/moby/client => github.com/moby/moby/client v0.0.0-20250731152656-4faedf2bec36