Merge pull request #3779 from thaJeztah/bump_moby

vendor: moby/client v0.4.0, moby/api v1.54.1
This commit is contained in:
Tõnis Tiigi
2026-04-03 11:36:47 -07:00
committed by GitHub
32 changed files with 502 additions and 105 deletions
+1 -1
View File
@@ -104,7 +104,7 @@ func (d *Driver) create(ctx context.Context, l progress.SubLogger) error {
return err
}
defer resp.Close()
return jsonmessage.DisplayJSONMessagesStream(resp, io.Discard, 0, false, nil)
return jsonmessage.DisplayStream(resp, io.Discard)
}); err != nil {
// image pulling failed, check if it exists in local image store.
// if not, return pulling error. otherwise log it.
+2 -2
View File
@@ -32,8 +32,8 @@ require (
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/moby/buildkit v0.29.0
github.com/moby/go-archive v0.2.0
github.com/moby/moby/api v1.54.0
github.com/moby/moby/client v0.3.0
github.com/moby/moby/api v1.54.1
github.com/moby/moby/client v0.4.0
github.com/moby/policy-helpers v0.0.0-20260324161837-b7c0b994300b
github.com/moby/sys/atomicwriter v0.1.0
github.com/moby/sys/mountinfo v0.7.2
+4 -4
View File
@@ -426,10 +426,10 @@ github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0=
github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs=
github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ=
github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=
github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=
github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/policy-helpers v0.0.0-20260324161837-b7c0b994300b h1:lvBBM2ACrsG5/O1G1caEwlh0XeqA89IQK3xq0Sh/5NI=
+29 -3
View File
@@ -77,8 +77,21 @@ func (p Port) Num() uint16 {
return p.num
}
// Port returns p's port number as a string.
//
// It returns an empty string for zero-values.
func (p Port) Port() string {
if p.proto == protoZero {
return ""
}
return strconv.Itoa(int(p.num))
}
// Proto returns p's network protocol.
func (p Port) Proto() IPProtocol {
if p.proto == protoZero {
return ""
}
return p.proto.Value()
}
@@ -93,7 +106,8 @@ func (p Port) IsValid() bool {
}
// String returns a string representation of the port in the format "<portnum>/<proto>".
// If the port is the zero value, it returns "invalid port".
// If the port is the zero value, it returns "invalid port", and users should
// check [PortRange.IsValid] or [PortRange.IsZero] before using this method.
func (p Port) String() string {
switch p.proto {
case protoZero:
@@ -232,6 +246,9 @@ func (pr PortRange) End() uint16 {
// Proto returns pr's network protocol.
func (pr PortRange) Proto() IPProtocol {
if pr.proto == protoZero {
return ""
}
return pr.proto.Value()
}
@@ -245,8 +262,12 @@ func (pr PortRange) IsValid() bool {
return pr.proto != protoZero
}
// String returns a string representation of the port range in the format "<start>-<end>/<proto>" or "<portnum>/<proto>" if start == end.
// If the port range is the zero value, it returns "invalid port range".
// String returns a string representation of the port range in the format
// "<start>-<end>/<proto>" or "<portnum>/<proto>" (if start == end).
//
// If the port range is the zero value, it returns "invalid port range",
// and users should check [PortRange.IsValid] or [PortRange.IsZero] before
// using this method.
func (pr PortRange) String() string {
switch pr.proto {
case protoZero:
@@ -307,6 +328,11 @@ func (pr PortRange) Range() PortRange {
// }
func (pr PortRange) All() iter.Seq[Port] {
return func(yield func(Port) bool) {
// Do not skip zero values here, because a zero-value means
// "map the port to an ephemeral host port".
//
// For example, "--port 80" is shorthand for "--port 0:80"
// ("--port <ephemeral port>:80").
for i := uint32(pr.Start()); i <= uint32(pr.End()); i++ {
if !yield(Port{num: uint16(i), proto: pr.proto}) {
return
+11 -6
View File
@@ -23,11 +23,16 @@ import (
)
func main() {
// Create a new client that handles common environment variables
// for configuration (DOCKER_HOST, DOCKER_API_VERSION), and does
// API-version negotiation to allow downgrading the API version
// when connecting with an older daemon version.
apiClient, err := client.New(client.FromEnv)
// Create a new client with "client.FromEnv" (configuring the client
// from commonly used environment variables such as DOCKER_HOST and
// DOCKER_API_VERSION) and set a custom User-Agent.
//
// API-version negotiation is enabled by default to allow downgrading
// the API version when connecting with an older daemon version.
apiClient, err := client.New(
client.FromEnv,
client.WithUserAgent("my-application/1.0.0"),
)
if err != nil {
panic(err)
}
@@ -49,4 +54,4 @@ func main() {
}
```
[Full documentation is available on pkg.go.dev.](https://pkg.go.dev/github.com/moby/moby/client)
Full documentation is available on [pkg.go.dev](https://pkg.go.dev/github.com/moby/moby/client).
+2
View File
@@ -5,8 +5,10 @@ import (
"net/url"
)
// BuildCancelOptions holds options for [Client.BuildCancel].
type BuildCancelOptions struct{}
// BuildCancelResult holds the result of [Client.BuildCancel].
type BuildCancelResult struct{}
// BuildCancel requests the daemon to cancel the ongoing build request
+23 -1
View File
@@ -59,6 +59,7 @@ import (
"net/http"
"net/url"
"path"
"runtime"
"slices"
"strings"
"sync"
@@ -67,6 +68,7 @@ import (
cerrdefs "github.com/containerd/errdefs"
"github.com/docker/go-connections/sockets"
"github.com/moby/moby/client/internal/mod"
"github.com/moby/moby/client/pkg/versions"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
@@ -113,6 +115,10 @@ const MaxAPIVersion = "1.54"
// below this version are not considered when performing API-version negotiation.
const MinAPIVersion = "1.40"
// defaultUserAgent returns the default User-Agent to use if none is set.
// It defaults to "moby-client/<module version> os/arch"
var defaultUserAgent = sync.OnceValue(userAgent)
// Ensure that Client always implements APIClient.
var _ APIClient = &Client{}
@@ -159,7 +165,9 @@ func CheckRedirect(_ *http.Request, via []*http.Request) error {
// NewClientWithOpts initializes a new API client.
//
// Deprecated: use New. This function will be removed in the next release.
// Deprecated: use [New]. This function will be removed in the next release.
//
//go:fix inline
func NewClientWithOpts(ops ...Opt) (*Client, error) {
return New(ops...)
}
@@ -207,6 +215,9 @@ func New(ops ...Opt) (*Client, error) {
cfg := &c.clientConfig
for _, op := range ops {
if op == nil {
continue
}
if err := op(cfg); err != nil {
return nil, err
}
@@ -431,3 +442,14 @@ func (cli *Client) dialer() func(context.Context) (net.Conn, error) {
}
}
}
func userAgent() string {
const defaultVersion = "v0.0.0+unknown"
const moduleName = "github.com/moby/moby/client"
version := defaultVersion
if v := mod.Version(moduleName); v != "" {
version = v
}
return "moby-client/" + version + " " + runtime.GOOS + "/" + runtime.GOARCH
}
+77 -25
View File
@@ -2,6 +2,7 @@ package client
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
@@ -11,6 +12,7 @@ import (
"strings"
"time"
cerrdefs "github.com/containerd/errdefs"
"github.com/docker/go-connections/sockets"
"github.com/docker/go-connections/tlsconfig"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
@@ -193,11 +195,23 @@ func WithUserAgent(ua string) Opt {
}
// WithHTTPHeaders appends custom HTTP headers to the client's default headers.
// It does not allow for built-in headers (such as "User-Agent", if set) to
// be overridden. Also see [WithUserAgent].
// It does not allow overriding built-in headers (such as "User-Agent").
// Also see [WithUserAgent].
//
// It replaces any existing custom headers. Keys are case-insensitive and
// canonicalized using [http.CanonicalHeaderKey]. If multiple entries map
// to the same canonical key, a [cerrdefs.ErrInvalidArgument] is returned.
func WithHTTPHeaders(headers map[string]string) Opt {
return func(c *clientConfig) error {
c.customHTTPHeaders = headers
c.customHTTPHeaders = make(map[string]string)
for k, v := range headers {
k = http.CanonicalHeaderKey(k)
_, ok := c.customHTTPHeaders[k]
if ok {
return cerrdefs.ErrInvalidArgument.WithMessage(fmt.Sprintf("duplicate custom HTTP header (%s)", k))
}
c.customHTTPHeaders[k] = v
}
return nil
}
}
@@ -210,56 +224,84 @@ func WithScheme(scheme string) Opt {
}
}
// WithTLSClientConfig applies a TLS config to the client transport.
func WithTLSClientConfig(cacertPath, certPath, keyPath string) Opt {
// WithTLSClientConfig configures the client's existing HTTP transport to use TLS.
// The minimum TLS version is TLS 1.2.
//
// If caFile is non-empty, it specifies the CA certificate file to use for
// server verification, and replaces the system root pool for that verification.
// If certFile is empty, the system root pool is used.
//
// If either certFile or keyFile is set, both must point to readable files
// containing a valid client certificate and unencrypted private key, or this
// option returns an error.
//
// If both certPath and keyPath are empty, no client certificate is configured.
// The connection will use TLS without client authentication (i.e., not mTLS).
func WithTLSClientConfig(caFile, certFile, keyFile string) Opt {
return func(c *clientConfig) error {
transport, ok := c.client.Transport.(*http.Transport)
if !ok {
return fmt.Errorf("cannot apply tls config to transport: %T", c.client.Transport)
return fmt.Errorf("cannot configure TLS: unsupported HTTP transport %T", c.client.Transport)
}
config, err := tlsconfig.Client(tlsconfig.Options{
CAFile: cacertPath,
CertFile: certPath,
KeyFile: keyPath,
CAFile: caFile,
CertFile: certFile,
KeyFile: keyFile,
ExclusiveRootPools: true,
MinVersion: tls.VersionTLS12,
})
if err != nil {
return fmt.Errorf("failed to create tls config: %w", err)
return fmt.Errorf("configure TLS: %w", err)
}
transport.TLSClientConfig = config
return nil
}
}
// WithTLSClientConfigFromEnv configures the client's TLS settings with the
// settings in the DOCKER_CERT_PATH ([EnvOverrideCertPath]) and DOCKER_TLS_VERIFY
// ([EnvTLSVerify]) environment variables. If DOCKER_CERT_PATH is not set or empty,
// TLS configuration is not modified.
// WithTLSClientConfigFromEnv configures the client for TLS using the
// DOCKER_CERT_PATH ([EnvOverrideCertPath]) and DOCKER_TLS_VERIFY
// ([EnvTLSVerify]) environment variables. The minimum TLS version is TLS 1.2.
//
// WithTLSClientConfigFromEnv uses the following environment variables:
// If DOCKER_CERT_PATH is unset or empty, this option leaves the client
// unchanged.
//
// - DOCKER_CERT_PATH ([EnvOverrideCertPath]) to specify the directory from
// which to load the TLS certificates ("ca.pem", "cert.pem", "key.pem").
// - DOCKER_TLS_VERIFY ([EnvTLSVerify]) to enable or disable TLS verification
// (off by default).
// When DOCKER_CERT_PATH is set, the following files are loaded from that
// directory:
//
// - "ca.pem" as the CA certificate
// - "cert.pem" as the client certificate
// - "key.pem" as the client private key
//
// These files must exist, be readable, and contain valid TLS material, or this
// option returns an error. A client certificate is always loaded from "cert.pem"
// and "key.pem" (mTLS is expected).
//
// If DOCKER_TLS_VERIFY is set to a non-empty value, server certificate
// verification is enabled. In that case, "ca.pem" is added to the system root
// pool used for verification.
//
// If DOCKER_TLS_VERIFY is unset or empty, server certificate verification is
// disabled.
func WithTLSClientConfigFromEnv() Opt {
return func(c *clientConfig) error {
dockerCertPath := os.Getenv(EnvOverrideCertPath)
if dockerCertPath == "" {
return nil
}
tlsc, err := tlsconfig.Client(tlsconfig.Options{
tlsConfig, err := tlsconfig.Client(tlsconfig.Options{
CAFile: filepath.Join(dockerCertPath, "ca.pem"),
CertFile: filepath.Join(dockerCertPath, "cert.pem"),
KeyFile: filepath.Join(dockerCertPath, "key.pem"),
InsecureSkipVerify: os.Getenv(EnvTLSVerify) == "",
MinVersion: tls.VersionTLS12,
})
if err != nil {
return err
return fmt.Errorf("configure TLS from %q: %w", EnvOverrideCertPath+"="+dockerCertPath, err)
}
// FIXME(thaJeztah): unlike WithTLSClientConfig, this option replaces the client's http.Client and transport; consider updating just the transport.
c.client = &http.Client{
Transport: &http.Transport{TLSClientConfig: tlsc},
Transport: &http.Transport{TLSClientConfig: tlsConfig},
CheckRedirect: CheckRedirect,
}
return nil
@@ -295,6 +337,8 @@ func WithAPIVersion(version string) Opt {
// WithVersion overrides the client version with the specified one.
//
// Deprecated: use [WithAPIVersion] instead.
//
//go:fix inline
func WithVersion(version string) Opt {
return WithAPIVersion(version)
}
@@ -328,6 +372,8 @@ func WithAPIVersionFromEnv() Opt {
// the DOCKER_API_VERSION ([EnvOverrideAPIVersion]) environment variable.
//
// Deprecated: use [WithAPIVersionFromEnv] instead.
//
//go:fix inline
func WithVersionFromEnv() Opt {
return WithAPIVersionFromEnv()
}
@@ -337,8 +383,11 @@ func WithVersionFromEnv() Opt {
// to use when making requests. API version negotiation is performed on the first
// request; subsequent requests do not re-negotiate.
//
// Deprecated: API-version negotiation is now enabled by default. Use [WithAPIVersion]
// or [WithAPIVersionFromEnv] to disable API version negotiation.
// Deprecated: API-version negotiation is now enabled by default and this options
// is now a no-op.
//
// Use [WithAPIVersion] or [WithAPIVersionFromEnv] to set a fixed API version
// instead of using automatic negotiation.
func WithAPIVersionNegotiation() Opt {
return func(c *clientConfig) error {
return nil
@@ -348,7 +397,10 @@ func WithAPIVersionNegotiation() Opt {
// WithTraceProvider sets the trace provider for the client.
// If this is not set then the global trace provider is used.
func WithTraceProvider(provider trace.TracerProvider) Opt {
return WithTraceOptions(otelhttp.WithTracerProvider(provider))
return func(c *clientConfig) error {
c.traceOpts = append(c.traceOpts, otelhttp.WithTracerProvider(provider))
return nil
}
}
// WithTraceOptions sets tracing span options for the client.
+2
View File
@@ -2,10 +2,12 @@ package client
import "context"
// ConfigRemoveOptions holds options for [Client.ConfigRemove].
type ConfigRemoveOptions struct {
// Add future optional parameters here
}
// ConfigRemoveResult holds the result of [Client.ConfigRemove].
type ConfigRemoveResult struct {
// Add future fields here
}
+1
View File
@@ -13,6 +13,7 @@ type ConfigUpdateOptions struct {
Spec swarm.ConfigSpec
}
// ConfigUpdateResult holds the result of [Client.ConfigUpdate].
type ConfigUpdateResult struct{}
// ConfigUpdate attempts to update a config
+5
View File
@@ -14,10 +14,12 @@ import (
"github.com/moby/moby/api/types/container"
)
// ContainerStatPathOptions holds options for [Client.ContainerStatPath].
type ContainerStatPathOptions struct {
Path string
}
// ContainerStatPathResult holds the result of [Client.ContainerStatPath].
type ContainerStatPathResult struct {
Stat container.PathStat
}
@@ -53,6 +55,7 @@ type CopyToContainerOptions struct {
CopyUIDGID bool
}
// CopyToContainerResult holds the result of [Client.CopyToContainer].
type CopyToContainerResult struct{}
// CopyToContainer copies content into the container filesystem.
@@ -83,10 +86,12 @@ func (cli *Client) CopyToContainer(ctx context.Context, containerID string, opti
return CopyToContainerResult{}, nil
}
// CopyFromContainerOptions holds options for [Client.CopyFromContainer].
type CopyFromContainerOptions struct {
SourcePath string
}
// CopyFromContainerResult holds the result of [Client.CopyFromContainer].
type CopyFromContainerResult struct {
Content io.ReadCloser
Stat container.PathStat
+2 -3
View File
@@ -2,6 +2,7 @@ package client
import (
"encoding/json"
"maps"
"net/url"
)
@@ -35,9 +36,7 @@ func (f Filters) Clone() Filters {
out := make(Filters, len(f))
for term, values := range f {
inner := make(map[string]bool, len(values))
for v, ok := range values {
inner[v] = ok
}
maps.Copy(inner, values)
out[term] = inner
}
return out
+1 -1
View File
@@ -23,7 +23,7 @@ func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, optio
return ImageBuildResult{}, err
}
buf, err := json.Marshal(options.AuthConfigs)
buf, err := json.Marshal(options.AuthConfigs) // #nosec G117 -- ignore "Marshaled struct field "Password" (JSON key "password") matches secret pattern"
if err != nil {
return ImageBuildResult{}, err
}
+1 -1
View File
@@ -39,7 +39,7 @@ func (cli *Client) ImageImport(ctx context.Context, source ImageImportSource, re
query.Set("message", options.Message)
}
if p := formatPlatform(options.Platform); p != "unknown" {
// TODO(thaJeztah): would we ever support mutiple platforms here? (would require multiple rootfs tars as well?)
// TODO(thaJeztah): would we ever support multiple platforms here? (would require multiple rootfs tars as well?)
query.Set("platform", p)
}
for _, change := range options.Changes {
+2
View File
@@ -9,11 +9,13 @@ import (
"github.com/distribution/reference"
)
// ImageTagOptions holds options for [Client.ImageTag].
type ImageTagOptions struct {
Source string
Target string
}
// ImageTagResult holds the result of [Client.ImageTag].
type ImageTagResult struct{}
// ImageTag tags an image in the docker host
+226
View File
@@ -0,0 +1,226 @@
// Package mod provides a small helper to extract a module's version
// from [debug.BuildInfo] without depending on [golang.org/x/mod].
//
// [golang.org/x/mod]: https://pkg.go.dev/golang.org/x/mod
package mod
import (
"fmt"
"runtime/debug"
"strconv"
"strings"
"sync"
)
var readBuildInfo = sync.OnceValues(debug.ReadBuildInfo)
// Version returns a best-effort version string for the given module path,
// similar to [mod.Version] in the daemon.
//
// If the module is present in [debug.BuildInfo] dependencies, its version
// is returned. Tagged versions are returned as-is (with "+incompatible"
// stripped). [Pseudo-versions] are normalized to:
//
// <base>+<revision>[+meta...][+dirty]
//
// Where "<base>" matches the behavior of [module.PseudoVersionBase] (i.e.,
// downgrade to the previous tag for non-prerelease Pseudo-versions).
//
// If the module is replaced (for example via go.work or replace directives),
// or no usable version information is available, Version returns an empty string.
//
// The returned value is intended for display purposes (e.g., in a default
// User-Agent), not for version comparison.
//
// [mod.Version]: https://pkg.go.dev/github.com/moby/moby/v2@v2.0.0-beta.7/daemon/internal/builder-next/worker/mod#Version
// [module.PseudoVersionBase]: https://pkg.go.dev/golang.org/x/mod@v0.34.0/module#PseudoVersionBase
// [Pseudo-versions]: https://cs.opensource.google/go/x/mod/+/refs/tags/v0.34.0:module/pseudo.go;l=5-33
func Version(name string) string {
bi, ok := readBuildInfo()
if !ok || bi == nil {
return ""
}
return moduleVersion(name, bi)
}
func moduleVersion(name string, bi *debug.BuildInfo) (modVersion string) {
if bi == nil {
return ""
}
// Check if we're the main module.
if v, ok := getVersion(name, &bi.Main); ok {
return v
}
// iterate over all dependencies and find name
for _, dep := range bi.Deps {
if v, ok := getVersion(name, dep); ok {
return v
}
}
return ""
}
func getVersion(name string, dep *debug.Module) (string, bool) {
if dep == nil || dep.Path != name {
return "", false
}
v := dep.Version
if dep.Replace != nil && dep.Replace.Version != "" {
v = dep.Replace.Version
}
if v == "" || v == "(devel)" {
return "", true
}
return normalize(v), true
}
// normalize converts a Go module version into a display-friendly form:
//
// - strips "+incompatible" unconditionally
// - if pseudo: vX.Y.Z[-pre][+rev][+meta...][+dirty]
// - if tagged: vX.Y.Z[-pre][+meta...][+dirty]
func normalize(v string) string {
base, metas, dirty := splitMetadata(v)
out := base
if base2, rev, undoPatch, ok := splitPseudo(base); ok {
if undoPatch {
// Downgrade the patch version that was raised by pseudo-versions:
//
// (2) vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456
if major, minor, patch, ok := parseSemVer(base2); ok && patch > 0 {
patch--
base2 = fmt.Sprintf("v%d.%d.%d", major, minor, patch)
}
}
// Go pseudo rev is typically 12, but be defensive.
if len(rev) > 12 {
rev = rev[:12]
}
out = base2 + "+" + rev
}
// Preserve other metadata (except for "+incompatible").
for _, m := range metas {
out += m
}
if dirty {
// +dirty goes last
out += "+dirty"
}
return out
}
func splitMetadata(v string) (base string, metas []string, dirty bool) {
base, meta, ok := strings.Cut(v, "+")
if !ok || meta == "" {
return base, nil, false
}
for m := range strings.SplitSeq(meta, "+") {
// drop incompatible, extract dirty, preserve everything else.
switch m {
case "incompatible", "":
// drop "+incompatible" and empty strings
case "dirty":
dirty = true
default:
metas = append(metas, "+"+m)
}
}
return base, metas, dirty
}
// splitPseudo splits a pseudo-version into base + revision, and reports whether
// it is a (Z+1) pseudo that needs patch undo.
//
// Supported (after stripping +incompatible/+dirty metadata):
//
// (1) vX.0.0-yyyymmddhhmmss-abcdef123456
// (2) vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456
// (4) vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456
func splitPseudo(v string) (base, rev string, undoPatch bool, ok bool) {
// Split off revision at the last '-'.
last := strings.LastIndexByte(v, '-')
if last < 0 || last+1 >= len(v) {
return "", "", false, false
}
rev = v[last+1:]
left := v[:last]
// First try the dot-joined timestamp forms:
// ...-0.<ts> (release pseudo; undoPatch)
// ....0.<ts> (prerelease pseudo; preserve prerelease)
if dot := strings.LastIndexByte(left, '.'); dot > 0 && dot+1 < len(left) {
ts := left[dot+1:]
if isTimestamp(ts) {
prefix := left[:dot] // ends with "-0" or ".0" for forms (2)/(4)
switch {
case strings.HasSuffix(prefix, "-0"):
// (2) vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456
return prefix[:len(prefix)-2], rev, true, true
case strings.HasSuffix(prefix, ".0"):
// (4) vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456
return prefix[:len(prefix)-2], rev, false, true
}
}
}
// Fall back to form (1): ...-<ts>-<rev>
//
// (1) vX.0.0-yyyymmddhhmmss-abcdef123456
if dash := strings.LastIndexByte(left, '-'); dash > 0 && dash+1 < len(left) {
ts := left[dash+1:]
if isTimestamp(ts) {
return left[:dash], rev, false, true
}
}
return "", "", false, false
}
// isTimestamp checks whether s is a timestamp ("yyyymmddhhmmss")
// component in a module version (vX.0.0-yyyymmddhhmmss-abcdef123456).
func isTimestamp(s string) bool {
if len(s) != 14 {
return false
}
for i := range len(s) {
c := s[i]
if c < '0' || c > '9' {
return false
}
}
return true
}
// parseSemVer parses "vX.Y.Z" into numeric components.
// It intentionally handles only the strict three-segment core form.
func parseSemVer(v string) (major, minor, patch int, ok bool) {
if len(v) < 2 || v[0] != 'v' {
return 0, 0, 0, false
}
parts := strings.Split(v[1:], ".")
if len(parts) != 3 {
return 0, 0, 0, false
}
var err error
major, err = strconv.Atoi(parts[0])
if err != nil {
return 0, 0, 0, false
}
minor, err = strconv.Atoi(parts[1])
if err != nil {
return 0, 0, 0, false
}
patch, err = strconv.Atoi(parts[2])
if err != nil {
return 0, 0, 0, false
}
return major, minor, patch, true
}
+1
View File
@@ -12,6 +12,7 @@ import (
// NodeInspectOptions holds parameters to inspect nodes with.
type NodeInspectOptions struct{}
// NodeInspectResult holds the result of [Client.NodeInspect].
type NodeInspectResult struct {
Node swarm.Node
Raw json.RawMessage
+1
View File
@@ -13,6 +13,7 @@ type NodeListOptions struct {
Filters Filters
}
// NodeListResult holds the result of [Client.NodeList].
type NodeListResult struct {
Items []swarm.Node
}
+2
View File
@@ -9,6 +9,8 @@ import (
type NodeRemoveOptions struct {
Force bool
}
// NodeRemoveResult holds the result of [Client.NodeRemove].
type NodeRemoveResult struct{}
// NodeRemove removes a Node.
+1
View File
@@ -13,6 +13,7 @@ type NodeUpdateOptions struct {
Spec swarm.NodeSpec
}
// NodeUpdateResult holds the result of [Client.NodeUpdate].
type NodeUpdateResult struct{}
// NodeUpdate updates a Node.
+77 -28
View File
@@ -20,6 +20,25 @@ var timeNow = time.Now // For overriding in tests.
// ensure the formatted time is always the same number of characters.
const RFC3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
// DisplayOpt configures behavior for [DisplayStream] and [DisplayMessages].
// Options are applied in order; if an option returns an error, processing
// stops and the error is returned to the caller.
type DisplayOpt func(*displayOpts) error
type displayOpts struct {
auxCallback func(jsonstream.Message)
}
// WithAuxCallback registers a callback that is invoked for auxiliary
// jsonstream messages as they are processed. The callback is optional;
// if not provided, auxiliary messages are ignored.
func WithAuxCallback(fn func(jsonstream.Message)) DisplayOpt {
return func(opts *displayOpts) error {
opts.auxCallback = fn
return nil
}
}
func RenderTUIProgress(p jsonstream.Progress, width uint16) string {
var (
pbBox string
@@ -37,16 +56,10 @@ func RenderTUIProgress(p jsonstream.Progress, width uint16) string {
}
}
percentage := int(float64(p.Current)/float64(p.Total)*100) / 2
if percentage > 50 {
percentage = 50
}
percentage := min(int(float64(p.Current)/float64(p.Total)*100)/2, 50)
if width > 110 {
// this number can't be negative gh#7136
numSpaces := 0
if 50-percentage > 0 {
numSpaces = 50 - percentage
}
numSpaces := max(50-percentage, 0)
pbBox = fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces))
}
@@ -146,10 +159,24 @@ func Display(jm jsonstream.Message, out io.Writer, isTerminal bool, width uint16
type JSONMessagesStream = iter.Seq2[jsonstream.Message, error]
// DisplayJSONMessagesStream reads a JSON message stream from in, and writes
// each [JSONMessage] to out.
// see DisplayJSONMessages for details
// DisplayJSONMessagesStream is like [DisplayStream], but allows the caller to
// explicitly provide the terminal file descriptor and whether out is a terminal.
func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, isTerminal bool, auxCallback func(jsonstream.Message)) error {
var opts []DisplayOpt
if auxCallback != nil {
opts = append(opts, WithAuxCallback(auxCallback))
}
return displayJSONMessagesStream(in, out, terminalFd, isTerminal, opts...)
}
// DisplayStream reads a JSON message stream from in, and writes each
// [jsonstream.Message] to out. See [DisplayMessages] for details.
func DisplayStream(in io.Reader, out io.Writer, opts ...DisplayOpt) error {
terminalFd, isTerminal := term.GetFdInfo(out)
return displayJSONMessagesStream(in, out, terminalFd, isTerminal, opts...)
}
func displayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, isTerminal bool, opts ...DisplayOpt) error {
dec := json.NewDecoder(in)
f := func(yield func(jsonstream.Message, error) bool) {
for {
@@ -164,26 +191,48 @@ func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr,
}
}
return DisplayJSONMessages(f, out, terminalFd, isTerminal, auxCallback)
return displayJSONMessages(f, out, terminalFd, isTerminal, opts...)
}
// DisplayJSONMessages writes each [JSONMessage] from stream to out.
// It returns an error if an invalid JSONMessage is received, or if
// a JSONMessage containers a non-zero [JSONMessage.Error].
//
// Presentation of the JSONMessage depends on whether a terminal is attached,
// and on the terminal width. Progress bars ([JSONProgress]) are suppressed
// on narrower terminals (< 110 characters).
//
// - isTerminal describes if out is a terminal, in which case it prints
// a newline ("\n") at the end of each line and moves the cursor while
// displaying.
// - terminalFd is the fd of the current terminal (if any), and used
// to get the terminal width.
// - auxCallback allows handling the [JSONMessage.Aux] field. It is
// called if a JSONMessage contains an Aux field, in which case
// DisplayJSONMessagesStream does not present the JSONMessage.
// DisplayJSONMessages is like [DisplayMessages], but allows the caller to
// explicitly provide the terminal file descriptor and whether out is a terminal.
func DisplayJSONMessages(messages iter.Seq2[jsonstream.Message, error], out io.Writer, terminalFd uintptr, isTerminal bool, auxCallback func(jsonstream.Message)) error {
var opts []DisplayOpt
if auxCallback != nil {
opts = append(opts, WithAuxCallback(auxCallback))
}
return displayJSONMessages(messages, out, terminalFd, isTerminal, opts...)
}
// DisplayMessages writes each [jsonstream.Message] from stream to out.
// It returns an error if an invalid [jsonstream.Message] is received, or if
// a message contains a non-zero [jsonstream.Message.Error].
//
// Presentation of the message depends on whether out is a terminal, and on the
// terminal width. Progress bars ([jsonstream.Progress]) are suppressed on
// narrower terminals (< 110 characters). If out is a terminal, it prints a
// newline ("\n") at the end of each line and moves the cursor while displaying.
//
// auxCallback allows handling the [jsonstream.Message.Aux] field. It is called
// if a message contains an Aux field, in which case DisplayMessages does not
// present the message.
func DisplayMessages(messages iter.Seq2[jsonstream.Message, error], out io.Writer, opts ...DisplayOpt) error {
terminalFd, isTerminal := term.GetFdInfo(out)
return displayJSONMessages(messages, out, terminalFd, isTerminal, opts...)
}
func displayJSONMessages(messages iter.Seq2[jsonstream.Message, error], out io.Writer, terminalFd uintptr, isTerminal bool, opts ...DisplayOpt) error {
var cfg displayOpts
for _, opt := range opts {
if opt == nil {
continue
}
if err := opt(&cfg); err != nil {
return err
}
}
auxCallback := cfg.auxCallback
ids := make(map[string]uint)
var width uint16 = 200
if isTerminal {
+1 -1
View File
@@ -21,7 +21,7 @@ func DecodeOptions(opts []string) []Option {
so := make([]Option, 0, len(opts))
for _, opt := range opts {
secopt := Option{}
for _, s := range strings.Split(opt, ",") {
for s := range strings.SplitSeq(opt, ",") {
k, v, _ := strings.Cut(s, "=")
if k == "" {
continue
+2 -5
View File
@@ -16,11 +16,8 @@ func compare(v1, v2 string) int {
otherTab = strings.Split(v2, ".")
)
maxVer := len(currTab)
if len(otherTab) > maxVer {
maxVer = len(otherTab)
}
for i := 0; i < maxVer; i++ {
maxVer := max(len(otherTab), len(currTab))
for i := range maxVer {
var currInt, otherInt int
if len(currTab) > i {
+12 -7
View File
@@ -128,7 +128,7 @@ func (cli *Client) sendRequest(ctx context.Context, method, path string, query u
// when failing to make a connection, On error, any Response can be ignored.
// A non-2xx status code doesn't cause an error.
func (cli *Client) doRequest(req *http.Request) (*http.Response, error) {
resp, err := cli.client.Do(req)
resp, err := cli.client.Do(req) // #nosec G704 -- ignore "SSRF via taint analysis"; API client intentionally sends caller-provided requests/URLs.
if err == nil {
return resp, nil
}
@@ -317,12 +317,17 @@ func (cli *Client) addHeaders(req *http.Request, headers http.Header) *http.Requ
req.Header[http.CanonicalHeaderKey(k)] = v
}
if cli.userAgent != nil {
if *cli.userAgent == "" {
req.Header.Del("User-Agent")
} else {
req.Header.Set("User-Agent", *cli.userAgent)
if cli.userAgent == nil {
// No custom User-Agent set: use the default.
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", defaultUserAgent())
}
} else if *cli.userAgent == "" {
// User-Agent set to empty value; remove User-Agent.
req.Header.Del("User-Agent")
} else {
// Custom User-Agent set.
req.Header.Set("User-Agent", *cli.userAgent)
}
return req
}
@@ -344,7 +349,7 @@ func jsonEncode(data any) (io.Reader, error) {
// encoding/json encodes a nil pointer as the JSON document `null`,
// irrespective of whether the type implements json.Marshaler or encoding.TextMarshaler.
// That is almost certainly not what the caller intended as the request body.
if v := reflect.ValueOf(data); v.Kind() == reflect.Ptr && v.IsNil() {
if v := reflect.ValueOf(data); v.Kind() == reflect.Pointer && v.IsNil() {
return http.NoBody, nil
}
+2
View File
@@ -2,10 +2,12 @@ package client
import "context"
// SecretRemoveOptions holds options for [Client.SecretRemove].
type SecretRemoveOptions struct {
// Add future optional parameters here
}
// SecretRemoveResult holds the result of [Client.SecretRemove].
type SecretRemoveResult struct {
// Add future fields here
}
+1
View File
@@ -13,6 +13,7 @@ type SecretUpdateOptions struct {
Spec swarm.SecretSpec
}
// SecretUpdateResult holds the result of [Client.SecretUpdate].
type SecretUpdateResult struct{}
// SecretUpdate attempts to update a secret.
+1 -1
View File
@@ -34,7 +34,7 @@ type ServiceLogsResult interface {
//
// The underlying [io.ReadCloser] is automatically closed if the context is canceled,
func (cli *Client) ServiceLogs(ctx context.Context, serviceID string, options ServiceLogsOptions) (ServiceLogsResult, error) {
// TODO(thaJeztah): this function needs documentation about the format of ths stream (similar to for container logs)
// TODO(thaJeztah): this function needs documentation about the format of the stream (similar to for container logs)
// TODO(thaJeztah): migrate CLI utilities to the client where suitable; https://github.com/docker/cli/blob/v29.0.0-rc.1/cli/command/service/logs.go#L73-L348
serviceID, err := trimID("service", serviceID)
+3 -10
View File
@@ -244,22 +244,15 @@ func imageDiskUsageFromLegacyAPI(du *legacyDiskUsage) ImagesDiskUsage {
Items: du.Images,
}
var used int64
for _, i := range idu.Items {
if i.Containers > 0 {
idu.ActiveCount++
if i.Size == -1 || i.SharedSize == -1 {
continue
}
used += (i.Size - i.SharedSize)
} else if i.Size != -1 && i.SharedSize != -1 {
// Only count reclaimable size if we have size information
idu.Reclaimable += (i.Size - i.SharedSize)
}
}
if idu.TotalCount > 0 {
idu.Reclaimable = idu.TotalSize - used
}
return idu
}
+2
View File
@@ -9,10 +9,12 @@ import (
"github.com/moby/moby/api/types/system"
)
// InfoOptions holds options for [Client.Info].
type InfoOptions struct {
// No options currently; placeholder for future use
}
// SystemInfoResult holds the result of [Client.Info].
type SystemInfoResult struct {
Info system.Info
}
+1 -1
View File
@@ -32,7 +32,7 @@ type TaskLogsResult interface {
//
// The underlying [io.ReadCloser] is automatically closed if the context is canceled,
func (cli *Client) TaskLogs(ctx context.Context, taskID string, options TaskLogsOptions) (TaskLogsResult, error) {
// TODO(thaJeztah): this function needs documentation about the format of ths stream (similar to for container logs)
// TODO(thaJeztah): this function needs documentation about the format of the stream (similar to for container logs)
// TODO(thaJeztah): migrate CLI utilities to the client where suitable; https://github.com/docker/cli/blob/v29.0.0-rc.1/cli/command/service/logs.go#L73-L348
query := url.Values{}
+1 -1
View File
@@ -136,7 +136,7 @@ func newCancelReadCloser(ctx context.Context, rc io.ReadCloser) io.ReadCloser {
rc: rc,
close: sync.OnceValue(rc.Close),
}
crc.stop = context.AfterFunc(ctx, func() { _ = crc.Close() })
crc.stop = context.AfterFunc(ctx, func() { _ = crc.close() })
return crc
}
+5 -4
View File
@@ -751,8 +751,8 @@ github.com/moby/go-archive/tarheader
# github.com/moby/locker v1.0.1
## explicit; go 1.13
github.com/moby/locker
# github.com/moby/moby/api v1.54.0
## explicit; go 1.24.0
# github.com/moby/moby/api v1.54.1
## explicit; go 1.24
github.com/moby/moby/api/pkg/authconfig
github.com/moby/moby/api/pkg/stdcopy
github.com/moby/moby/api/types
@@ -772,10 +772,11 @@ github.com/moby/moby/api/types/storage
github.com/moby/moby/api/types/swarm
github.com/moby/moby/api/types/system
github.com/moby/moby/api/types/volume
# github.com/moby/moby/client v0.3.0
## explicit; go 1.24.0
# github.com/moby/moby/client v0.4.0
## explicit; go 1.24
github.com/moby/moby/client
github.com/moby/moby/client/internal
github.com/moby/moby/client/internal/mod
github.com/moby/moby/client/internal/timestamp
github.com/moby/moby/client/pkg/jsonmessage
github.com/moby/moby/client/pkg/security