vendor: moby/client v0.4.0, moby/api v1.54.1
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
+29
-3
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -13,6 +13,7 @@ type NodeListOptions struct {
|
||||
Filters Filters
|
||||
}
|
||||
|
||||
// NodeListResult holds the result of [Client.NodeList].
|
||||
type NodeListResult struct {
|
||||
Items []swarm.Node
|
||||
}
|
||||
|
||||
+2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user