vendor: docker/cli v29.1.1, moby/client v0.2.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2025-12-02 15:52:11 +01:00
parent 4f77ae4858
commit 5efe75571f
13 changed files with 296 additions and 149 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ require (
github.com/creack/pty v1.1.24 github.com/creack/pty v1.1.24
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/distribution/reference v0.6.0 github.com/distribution/reference v0.6.0
github.com/docker/cli v29.0.1+incompatible github.com/docker/cli v29.1.1+incompatible
github.com/docker/cli-docs-tool v0.10.0 github.com/docker/cli-docs-tool v0.10.0
github.com/docker/docker v28.5.2+incompatible github.com/docker/docker v28.5.2+incompatible
github.com/docker/go-units v0.5.0 github.com/docker/go-units v0.5.0
@@ -32,7 +32,7 @@ require (
github.com/moby/buildkit v0.26.2 github.com/moby/buildkit v0.26.2
github.com/moby/go-archive v0.1.0 github.com/moby/go-archive v0.1.0
github.com/moby/moby/api v1.52.0 github.com/moby/moby/api v1.52.0
github.com/moby/moby/client v0.1.0 github.com/moby/moby/client v0.2.1
github.com/moby/sys/atomicwriter v0.1.0 github.com/moby/sys/atomicwriter v0.1.0
github.com/moby/sys/mountinfo v0.7.2 github.com/moby/sys/mountinfo v0.7.2
github.com/morikuni/aec v1.0.0 github.com/morikuni/aec v1.0.0
+4 -4
View File
@@ -98,8 +98,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v29.0.1+incompatible h1:EnvMEAR9Ro5xQEKbMitlabj5vCDY0vwcDyY/Lsow7FQ= github.com/docker/cli v29.1.1+incompatible h1:gGQk5qx62yPKRm3bUdKBzmDBSQzp17hlSLbV1F7jjys=
github.com/docker/cli v29.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli-docs-tool v0.10.0 h1:bOD6mKynPQgojQi3s2jgcUWGp/Ebqy1SeCr9VfKQLLU= github.com/docker/cli-docs-tool v0.10.0 h1:bOD6mKynPQgojQi3s2jgcUWGp/Ebqy1SeCr9VfKQLLU=
github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09fzRHP4aX1qwp1U= github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09fzRHP4aX1qwp1U=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
@@ -218,8 +218,8 @@ 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/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg= github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.1.0 h1:nt+hn6O9cyJQqq5UWnFGqsZRTS/JirUqzPjEl0Bdc/8= github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
github.com/moby/moby/client v0.1.0/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE= github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 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/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
+1 -1
View File
@@ -134,7 +134,7 @@ func (ep *Endpoint) ClientOpts() ([]client.Opt, error) {
} }
} }
result = append(result, client.WithVersionFromEnv(), client.WithAPIVersionNegotiation()) result = append(result, client.WithAPIVersionFromEnv())
return result, nil return result, nil
} }
+13 -4
View File
@@ -23,19 +23,28 @@ import (
) )
func main() { func main() {
apiClient, err := client.New(client.FromEnv, client.WithAPIVersionNegotiation()) // 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)
if err != nil { if err != nil {
panic(err) panic(err)
} }
defer apiClient.Close() defer apiClient.Close()
containers, err := apiClient.ContainerList(context.Background(), client.ContainerListOptions{All: true}) // List all containers (both stopped and running).
result, err := apiClient.ContainerList(context.Background(), client.ContainerListOptions{
All: true,
})
if err != nil { if err != nil {
panic(err) panic(err)
} }
for _, ctr := range containers { // Print each container's ID, status and the image it was created from.
fmt.Printf("%s %s (status: %s)\n", ctr.ID, ctr.Image, ctr.Status) fmt.Printf("%s %-22s %s\n", "ID", "STATUS", "IMAGE")
for _, ctr := range result.Items {
fmt.Printf("%s %-22s %s\n", ctr.ID, ctr.Status, ctr.Image)
} }
} }
``` ```
+50 -32
View File
@@ -8,10 +8,8 @@ https://docs.docker.com/reference/api/engine/
You use the library by constructing a client object using [New] You use the library by constructing a client object using [New]
and calling methods on it. The client can be configured from environment and calling methods on it. The client can be configured from environment
variables by passing the [FromEnv] option, and the [WithAPIVersionNegotiation] variables by passing the [FromEnv] option. Other options can be configured
option to allow downgrading the API version used when connecting with an older manually by passing any of the available [Opt] options.
daemon version. Other options cen be configured manually by passing any of
the available [Opt] options.
For example, to list running containers (the equivalent of "docker ps"): For example, to list running containers (the equivalent of "docker ps"):
@@ -30,7 +28,7 @@ For example, to list running containers (the equivalent of "docker ps"):
// for configuration (DOCKER_HOST, DOCKER_API_VERSION), and does // for configuration (DOCKER_HOST, DOCKER_API_VERSION), and does
// API-version negotiation to allow downgrading the API version // API-version negotiation to allow downgrading the API version
// when connecting with an older daemon version. // when connecting with an older daemon version.
apiClient, err := client.New(client.FromEnv, client.WithAPIVersionNegotiation()) apiClient, err := client.New(client.FromEnv)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -103,18 +101,16 @@ import (
const DummyHost = "api.moby.localhost" const DummyHost = "api.moby.localhost"
// MaxAPIVersion is the highest REST API version supported by the client. // MaxAPIVersion is the highest REST API version supported by the client.
// If API-version negotiation is enabled (see [WithAPIVersionNegotiation], // If API-version negotiation is enabled, the client may downgrade its API version.
// [Client.NegotiateAPIVersion]), the client may downgrade its API version. // Similarly, the [WithAPIVersion] and [WithAPIVersionFromEnv] options allow
// Similarly, the [WithVersion] and [WithVersionFromEnv] allow overriding // overriding the version and disable API-version negotiation.
// the version.
// //
// This version may be lower than the version of the api library module used. // This version may be lower than the version of the api library module used.
const MaxAPIVersion = "1.52" const MaxAPIVersion = "1.52"
// fallbackAPIVersion is the version to fall back to if API-version negotiation // MinAPIVersion is the minimum API version supported by the client. API versions
// fails. API versions below this version are not supported by the client, // below this version are not considered when performing API-version negotiation.
// and not considered when negotiating. const MinAPIVersion = "1.44"
const fallbackAPIVersion = "1.44"
// Ensure that Client always implements APIClient. // Ensure that Client always implements APIClient.
var _ APIClient = &Client{} var _ APIClient = &Client{}
@@ -174,8 +170,13 @@ func NewClientWithOpts(ops ...Opt) (*Client, error) {
// It takes an optional list of [Opt] functional arguments, which are applied in // It takes an optional list of [Opt] functional arguments, which are applied in
// the order they're provided, which allows modifying the defaults when creating // the order they're provided, which allows modifying the defaults when creating
// the client. For example, the following initializes a client that configures // the client. For example, the following initializes a client that configures
// itself with values from environment variables ([FromEnv]), and has automatic // itself with values from environment variables ([FromEnv]).
// API version negotiation enabled ([WithAPIVersionNegotiation]). //
// By default, the client automatically negotiates the API version to use when
// making requests. API version negotiation is performed on the first request;
// subsequent requests do not re-negotiate. Use [WithAPIVersion] or
// [WithAPIVersionFromEnv] to configure the client with a fixed API version
// and disable API version negotiation.
// //
// cli, err := client.New( // cli, err := client.New(
// client.FromEnv, // client.FromEnv,
@@ -213,6 +214,12 @@ func New(ops ...Opt) (*Client, error) {
} }
} }
if cfg.envAPIVersion != "" {
c.setAPIVersion(cfg.envAPIVersion)
} else if cfg.manualAPIVersion != "" {
c.setAPIVersion(cfg.manualAPIVersion)
}
if tr, ok := c.client.Transport.(*http.Transport); ok { if tr, ok := c.client.Transport.(*http.Transport); ok {
// Store the base transport before we wrap it in tracing libs below // Store the base transport before we wrap it in tracing libs below
// This is used, as an example, to close idle connections when the client is closed // This is used, as an example, to close idle connections when the client is closed
@@ -278,7 +285,7 @@ func (cli *Client) Close() error {
// be negotiated when making the actual requests, and for which cases // be negotiated when making the actual requests, and for which cases
// we cannot do the negotiation lazily. // we cannot do the negotiation lazily.
func (cli *Client) checkVersion(ctx context.Context) error { func (cli *Client) checkVersion(ctx context.Context) error {
if cli.manualOverride || !cli.negotiateVersion || cli.negotiated.Load() { if cli.negotiated.Load() {
return nil return nil
} }
_, err := cli.Ping(ctx, PingOptions{ _, err := cli.Ping(ctx, PingOptions{
@@ -306,36 +313,47 @@ func (cli *Client) ClientVersion() string {
} }
// negotiateAPIVersion updates the version to match the API version from // negotiateAPIVersion updates the version to match the API version from
// the ping response. It falls back to the lowest version supported if the // the ping response.
// API version is empty, or returns an error if the API version is lower than //
// the lowest supported API version, in which case the version is not modified. // It returns an error if version is invalid, or lower than the minimum
// supported API version in which case the client's API version is not
// updated, and negotiation is not marked as completed.
func (cli *Client) negotiateAPIVersion(pingVersion string) error { func (cli *Client) negotiateAPIVersion(pingVersion string) error {
pingVersion = strings.TrimPrefix(pingVersion, "v") var err error
if pingVersion == "" { pingVersion, err = parseAPIVersion(pingVersion)
// TODO(thaJeztah): consider returning an error on empty value or not falling back; see https://github.com/moby/moby/pull/51119#discussion_r2413148487 if err != nil {
pingVersion = fallbackAPIVersion return err
} else if versions.LessThan(pingVersion, fallbackAPIVersion) { }
return cerrdefs.ErrInvalidArgument.WithMessage(fmt.Sprintf("API version %s is not supported by this client: the minimum supported API version is %s", pingVersion, fallbackAPIVersion))
if versions.LessThan(pingVersion, MinAPIVersion) {
return cerrdefs.ErrInvalidArgument.WithMessage(fmt.Sprintf("API version %s is not supported by this client: the minimum supported API version is %s", pingVersion, MinAPIVersion))
} }
// if the client is not initialized with a version, start with the latest supported version // if the client is not initialized with a version, start with the latest supported version
if cli.version == "" { negotiatedVersion := cli.version
cli.version = MaxAPIVersion if negotiatedVersion == "" {
negotiatedVersion = MaxAPIVersion
} }
// if server version is lower than the client version, downgrade // if server version is lower than the client version, downgrade
if versions.LessThan(pingVersion, cli.version) { if versions.LessThan(pingVersion, negotiatedVersion) {
cli.version = pingVersion negotiatedVersion = pingVersion
} }
// Store the results, so that automatic API version negotiation (if enabled) // Store the results, so that automatic API version negotiation (if enabled)
// won't be performed on the next request. // won't be performed on the next request.
if cli.negotiateVersion { cli.setAPIVersion(negotiatedVersion)
cli.negotiated.Store(true)
}
return nil return nil
} }
// setAPIVersion sets the client's API version and marks API version negotiation
// as completed, so that automatic API version negotiation (if enabled) won't
// be performed on the next request.
func (cli *Client) setAPIVersion(version string) {
cli.version = version
cli.negotiated.Store(true)
}
// DaemonHost returns the host address used by the client // DaemonHost returns the host address used by the client
func (cli *Client) DaemonHost() string { func (cli *Client) DaemonHost() string {
return cli.host return cli.host
@@ -38,14 +38,22 @@ type clientConfig struct {
userAgent *string userAgent *string
// custom HTTP headers configured by users. // custom HTTP headers configured by users.
customHTTPHeaders map[string]string customHTTPHeaders map[string]string
// manualOverride is set to true when the version was set by users.
manualOverride bool
// negotiateVersion indicates if the client should automatically negotiate // manualAPIVersion contains the API version set by users. This field
// the API version to use when making requests. API version negotiation is // will only be non-empty if a valid-formed version was set through
// performed on the first request, after which negotiated is set to "true" // [WithAPIVersion].
// so that subsequent requests do not re-negotiate. //
negotiateVersion bool // If both manualAPIVersion and envAPIVersion are set, manualAPIVersion
// takes precedence. Either field disables API-version negotiation.
manualAPIVersion string
// envAPIVersion contains the API version set by users. This field
// will only be non-empty if a valid-formed version was set through
// [WithAPIVersionFromEnv].
//
// If both manualAPIVersion and envAPIVersion are set, manualAPIVersion
// takes precedence. Either field disables API-version negotiation.
envAPIVersion string
// traceOpts is a list of options to configure the tracing span. // traceOpts is a list of options to configure the tracing span.
traceOpts []otelhttp.Option traceOpts []otelhttp.Option
@@ -56,7 +64,7 @@ type Opt func(*clientConfig) error
// FromEnv configures the client with values from environment variables. It // FromEnv configures the client with values from environment variables. It
// is the equivalent of using the [WithTLSClientConfigFromEnv], [WithHostFromEnv], // is the equivalent of using the [WithTLSClientConfigFromEnv], [WithHostFromEnv],
// and [WithVersionFromEnv] options. // and [WithAPIVersionFromEnv] options.
// //
// FromEnv uses the following environment variables: // FromEnv uses the following environment variables:
// //
@@ -71,7 +79,7 @@ func FromEnv(c *clientConfig) error {
ops := []Opt{ ops := []Opt{
WithTLSClientConfigFromEnv(), WithTLSClientConfigFromEnv(),
WithHostFromEnv(), WithHostFromEnv(),
WithVersionFromEnv(), WithAPIVersionFromEnv(),
} }
for _, op := range ops { for _, op := range ops {
if err := op(c); err != nil { if err := op(c); err != nil {
@@ -241,18 +249,59 @@ func WithTLSClientConfigFromEnv() Opt {
} }
} }
// WithVersion overrides the client version with the specified one. If an empty // WithAPIVersion overrides the client's API version with the specified one,
// version is provided, the value is ignored to allow version negotiation // and disables API version negotiation. If an empty version is provided,
// (see [WithAPIVersionNegotiation]). // this option is ignored to allow version negotiation. The given version
// should be formatted "<major>.<minor>" (for example, "1.52"). It returns
// an error if the given value not in the correct format.
// //
// WithVersion does not validate if the client supports the given version, // WithAPIVersion does not validate if the client supports the given version,
// and callers should verify if the version is in the correct format and // and callers should verify if the version lower than the maximum supported
// lower than the maximum supported version as defined by [MaxAPIVersion]. // version as defined by [MaxAPIVersion].
func WithVersion(version string) Opt { //
// [WithAPIVersionFromEnv] takes precedence if [WithAPIVersion] and
// [WithAPIVersionFromEnv] are both set.
func WithAPIVersion(version string) Opt {
return func(c *clientConfig) error { return func(c *clientConfig) error {
if v := strings.TrimPrefix(version, "v"); v != "" { version = strings.TrimSpace(version)
c.version = v if val := strings.TrimPrefix(version, "v"); val != "" {
c.manualOverride = true ver, err := parseAPIVersion(val)
if err != nil {
return fmt.Errorf("invalid API version (%s): %w", version, err)
}
c.manualAPIVersion = ver
}
return nil
}
}
// WithVersion overrides the client version with the specified one.
//
// Deprecated: use [WithAPIVersion] instead.
func WithVersion(version string) Opt {
return WithAPIVersion(version)
}
// WithAPIVersionFromEnv overrides the client version with the version specified in
// the DOCKER_API_VERSION ([EnvOverrideAPIVersion]) environment variable.
// If DOCKER_API_VERSION is not set, or set to an empty value, the version
// is not modified.
//
// WithAPIVersion does not validate if the client supports the given version,
// and callers should verify if the version lower than the maximum supported
// version as defined by [MaxAPIVersion].
//
// [WithAPIVersionFromEnv] takes precedence if [WithAPIVersion] and
// [WithAPIVersionFromEnv] are both set.
func WithAPIVersionFromEnv() Opt {
return func(c *clientConfig) error {
version := strings.TrimSpace(os.Getenv(EnvOverrideAPIVersion))
if val := strings.TrimPrefix(version, "v"); val != "" {
ver, err := parseAPIVersion(val)
if err != nil {
return fmt.Errorf("invalid API version (%s): %w", version, err)
}
c.envAPIVersion = ver
} }
return nil return nil
} }
@@ -260,25 +309,21 @@ func WithVersion(version string) Opt {
// WithVersionFromEnv overrides the client version with the version specified in // WithVersionFromEnv overrides the client version with the version specified in
// the DOCKER_API_VERSION ([EnvOverrideAPIVersion]) environment variable. // the DOCKER_API_VERSION ([EnvOverrideAPIVersion]) environment variable.
// If DOCKER_API_VERSION is not set, or set to an empty value, the version
// is not modified.
// //
// WithVersion does not validate if the client supports the given version, // Deprecated: use [WithAPIVersionFromEnv] instead.
// and callers should verify if the version is in the correct format and
// lower than the maximum supported version as defined by [MaxAPIVersion].
func WithVersionFromEnv() Opt { func WithVersionFromEnv() Opt {
return func(c *clientConfig) error { return WithAPIVersionFromEnv()
return WithVersion(os.Getenv(EnvOverrideAPIVersion))(c)
}
} }
// WithAPIVersionNegotiation enables automatic API version negotiation for the client. // WithAPIVersionNegotiation enables automatic API version negotiation for the client.
// With this option enabled, the client automatically negotiates the API version // With this option enabled, the client automatically negotiates the API version
// to use when making requests. API version negotiation is performed on the first // to use when making requests. API version negotiation is performed on the first
// request; subsequent requests do not re-negotiate. // 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.
func WithAPIVersionNegotiation() Opt { func WithAPIVersionNegotiation() Opt {
return func(c *clientConfig) error { return func(c *clientConfig) error {
c.negotiateVersion = true
return nil return nil
} }
} }
+1 -1
View File
@@ -13,7 +13,7 @@ const (
// be used to override the API version to use. Value must be // be used to override the API version to use. Value must be
// formatted as MAJOR.MINOR, for example, "1.19". // formatted as MAJOR.MINOR, for example, "1.19".
// //
// This env-var is read by [FromEnv] and [WithVersionFromEnv] and when set to a // This env-var is read by [FromEnv] and [WithAPIVersionFromEnv] and when set to a
// non-empty value, takes precedence over API version negotiation. // non-empty value, takes precedence over API version negotiation.
// //
// This environment variable should be used for debugging purposes only, as // This environment variable should be used for debugging purposes only, as
+24 -9
View File
@@ -20,7 +20,7 @@ type PingOptions struct {
// //
// If a manual override is in place, either through the "DOCKER_API_VERSION" // If a manual override is in place, either through the "DOCKER_API_VERSION"
// ([EnvOverrideAPIVersion]) environment variable, or if the client is initialized // ([EnvOverrideAPIVersion]) environment variable, or if the client is initialized
// with a fixed version ([WithVersion]), no negotiation is performed. // with a fixed version ([WithAPIVersion]), no negotiation is performed.
// //
// If the API server's ping response does not contain an API version, or if the // If the API server's ping response does not contain an API version, or if the
// client did not get a successful ping response, it assumes it is connected with // client did not get a successful ping response, it assumes it is connected with
@@ -29,9 +29,8 @@ type PingOptions struct {
NegotiateAPIVersion bool NegotiateAPIVersion bool
// ForceNegotiate forces the client to re-negotiate the API version, even if // ForceNegotiate forces the client to re-negotiate the API version, even if
// API-version negotiation already happened. This option cannot be // API-version negotiation already happened or it the client is configured
// used if the client is configured with a fixed version using (using // with a fixed version (using [WithAPIVersion] or [WithAPIVersionFromEnv]).
// [WithVersion] or [WithVersionFromEnv]).
// //
// This option has no effect if NegotiateAPIVersion is not set. // This option has no effect if NegotiateAPIVersion is not set.
ForceNegotiate bool ForceNegotiate bool
@@ -72,10 +71,12 @@ type SwarmStatus struct {
// for other non-success status codes, failing to connect to the API, or failing // for other non-success status codes, failing to connect to the API, or failing
// to parse the API response. // to parse the API response.
func (cli *Client) Ping(ctx context.Context, options PingOptions) (PingResult, error) { func (cli *Client) Ping(ctx context.Context, options PingOptions) (PingResult, error) {
if cli.manualOverride { if !options.NegotiateAPIVersion {
// No API version negotiation needed; just return ping response.
return cli.ping(ctx) return cli.ping(ctx)
} }
if !options.NegotiateAPIVersion && !cli.negotiateVersion { if cli.negotiated.Load() && !options.ForceNegotiate {
// API version was already negotiated or manually set.
return cli.ping(ctx) return cli.ping(ctx)
} }
@@ -85,10 +86,19 @@ func (cli *Client) Ping(ctx context.Context, options PingOptions) (PingResult, e
ping, err := cli.ping(ctx) ping, err := cli.ping(ctx)
if err != nil { if err != nil {
return cli.ping(ctx) return ping, err
} }
if cli.negotiated.Load() && !options.ForceNegotiate { if cli.negotiated.Load() && !options.ForceNegotiate {
// API version was already negotiated or manually set.
//
// We check cli.negotiated again under lock, to account for race
// conditions with the check at the start of this function.
return ping, nil
}
if ping.APIVersion == "" {
cli.setAPIVersion(MaxAPIVersion)
return ping, nil return ping, nil
} }
@@ -112,10 +122,15 @@ func (cli *Client) ping(ctx context.Context) (PingResult, error) {
// response-body to get error details from. // response-body to get error details from.
return newPingResult(resp), nil return newPingResult(resp), nil
} }
// close to allow reusing connection.
ensureReaderClosed(resp)
// HEAD failed or returned a non-OK status; fallback to GET. // HEAD failed or returned a non-OK status; fallback to GET.
req.Method = http.MethodGet req2, err := cli.buildRequest(ctx, http.MethodGet, path.Join(cli.basePath, "/_ping"), nil, nil)
resp, err = cli.doRequest(req) if err != nil {
return PingResult{}, err
}
resp, err = cli.doRequest(req2)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
// Failed to connect. // Failed to connect.
+50 -34
View File
@@ -67,31 +67,22 @@ func (cli *Client) delete(ctx context.Context, path string, query url.Values, he
// prepareJSONRequest encodes the given body to JSON and returns it as an [io.Reader], and sets the Content-Type // prepareJSONRequest encodes the given body to JSON and returns it as an [io.Reader], and sets the Content-Type
// header. If body is nil, or a nil-interface, a "nil" body is returned without // header. If body is nil, or a nil-interface, a "nil" body is returned without
// error. // error.
//
// TODO(thaJeztah): should this return an error if a different Content-Type is already set?
// TODO(thaJeztah): is "nil" the appropriate approach for an empty body, or should we use [http.NoBody] (or similar)?
func prepareJSONRequest(body any, headers http.Header) (io.Reader, http.Header, error) { func prepareJSONRequest(body any, headers http.Header) (io.Reader, http.Header, error) {
if body == nil {
return nil, headers, nil
}
// 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.
//
// TODO(thaJeztah): consider moving this to jsonEncode, which would also allow returning an (empty) reader instead of nil.
if reflect.TypeOf(body).Kind() == reflect.Ptr && reflect.ValueOf(body).IsNil() {
return nil, headers, nil
}
jsonBody, err := jsonEncode(body) jsonBody, err := jsonEncode(body)
if err != nil { if err != nil {
return nil, headers, err return nil, headers, err
} }
if jsonBody == nil || jsonBody == http.NoBody {
// no content-type is set on empty requests.
return jsonBody, headers, nil
}
hdr := http.Header{} hdr := http.Header{}
if headers != nil { if headers != nil {
hdr = headers.Clone() hdr = headers.Clone()
} }
// TODO(thaJeztah): should this return an error if a different Content-Type is already set?
hdr.Set("Content-Type", "application/json") hdr.Set("Content-Type", "application/json")
return jsonBody, hdr, nil return jsonBody, hdr, nil
} }
@@ -110,9 +101,6 @@ func (cli *Client) buildRequest(ctx context.Context, method, path string, body i
req.Host = DummyHost req.Host = DummyHost
} }
if body != nil && req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "text/plain")
}
return req, nil return req, nil
} }
@@ -248,7 +236,11 @@ func checkResponseErr(serverResp *http.Response) (retErr error) {
if statusMsg == "" { if statusMsg == "" {
statusMsg = http.StatusText(serverResp.StatusCode) statusMsg = http.StatusText(serverResp.StatusCode)
} }
if serverResp.Body != nil { var reqMethod string
if serverResp.Request != nil {
reqMethod = serverResp.Request.Method
}
if serverResp.Body != nil && reqMethod != http.MethodHead {
bodyMax := 1 * 1024 * 1024 // 1 MiB bodyMax := 1 * 1024 * 1024 // 1 MiB
bodyR := &io.LimitedReader{ bodyR := &io.LimitedReader{
R: serverResp.Body, R: serverResp.Body,
@@ -333,25 +325,49 @@ func (cli *Client) addHeaders(req *http.Request, headers http.Header) *http.Requ
} }
func jsonEncode(data any) (io.Reader, error) { func jsonEncode(data any) (io.Reader, error) {
var params bytes.Buffer switch x := data.(type) {
if data != nil { case nil:
if err := json.NewEncoder(&params).Encode(data); err != nil { return http.NoBody, nil
return nil, err case io.Reader:
// http.NoBody or other readers
return x, nil
case json.RawMessage:
if len(x) == 0 {
return http.NoBody, nil
} }
return bytes.NewReader(x), nil
} }
return &params, nil
// 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() {
return http.NoBody, nil
}
b, err := json.Marshal(data)
if err != nil {
return nil, err
}
return bytes.NewReader(b), nil
} }
func ensureReaderClosed(response *http.Response) { func ensureReaderClosed(response *http.Response) {
if response != nil && response.Body != nil { if response == nil || response.Body == nil {
// Drain up to 512 bytes and close the body to let the Transport reuse the connection return
// see https://github.com/google/go-github/pull/317/files#r57536827
//
// TODO(thaJeztah): see if this optimization is still needed, or already implemented in stdlib,
// and check if context-cancellation should handle this as well. If still needed, consider
// wrapping response.Body, or returning a "closer()" from [Client.sendRequest] and related
// methods.
_, _ = io.CopyN(io.Discard, response.Body, 512)
_ = response.Body.Close()
} }
if response.ContentLength == 0 || (response.Request != nil && response.Request.Method == http.MethodHead) {
// No need to drain head requests or zero-length responses.
_ = response.Body.Close()
return
}
// Drain up to 512 bytes and close the body to let the Transport reuse the connection
// see https://github.com/google/go-github/pull/317/files#r57536827
//
// TODO(thaJeztah): see if this optimization is still needed, or already implemented in stdlib,
// and check if context-cancellation should handle this as well. If still needed, consider
// wrapping response.Body, or returning a "closer()" from [Client.sendRequest] and related
// methods.
_, _ = io.CopyN(io.Discard, response.Body, 512)
_ = response.Body.Close()
} }
+28 -28
View File
@@ -59,16 +59,18 @@ func (cli *Client) ServiceCreate(ctx context.Context, options ServiceCreateOptio
options.Spec.TaskTemplate.ContainerSpec.Image = taggedImg options.Spec.TaskTemplate.ContainerSpec.Image = taggedImg
} }
if options.QueryRegistry { if options.QueryRegistry {
resolveWarning := resolveContainerSpecImage(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth) if warning := resolveContainerSpecImage(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth); warning != "" {
warnings = append(warnings, resolveWarning) warnings = append(warnings, warning)
}
} }
case options.Spec.TaskTemplate.PluginSpec != nil: case options.Spec.TaskTemplate.PluginSpec != nil:
if taggedImg := imageWithTagString(options.Spec.TaskTemplate.PluginSpec.Remote); taggedImg != "" { if taggedImg := imageWithTagString(options.Spec.TaskTemplate.PluginSpec.Remote); taggedImg != "" {
options.Spec.TaskTemplate.PluginSpec.Remote = taggedImg options.Spec.TaskTemplate.PluginSpec.Remote = taggedImg
} }
if options.QueryRegistry { if options.QueryRegistry {
resolveWarning := resolvePluginSpecRemote(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth) if warning := resolvePluginSpecRemote(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth); warning != "" {
warnings = append(warnings, resolveWarning) warnings = append(warnings, warning)
}
} }
} }
@@ -93,35 +95,33 @@ func (cli *Client) ServiceCreate(ctx context.Context, options ServiceCreateOptio
} }
func resolveContainerSpecImage(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string { func resolveContainerSpecImage(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string {
var warning string img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.ContainerSpec.Image, encodedAuth)
if img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.ContainerSpec.Image, encodedAuth); err != nil { if err != nil {
warning = digestWarning(taskSpec.ContainerSpec.Image) return digestWarning(taskSpec.ContainerSpec.Image)
} else {
taskSpec.ContainerSpec.Image = img
if len(imgPlatforms) > 0 {
if taskSpec.Placement == nil {
taskSpec.Placement = &swarm.Placement{}
}
taskSpec.Placement.Platforms = imgPlatforms
}
} }
return warning taskSpec.ContainerSpec.Image = img
if len(imgPlatforms) > 0 {
if taskSpec.Placement == nil {
taskSpec.Placement = &swarm.Placement{}
}
taskSpec.Placement.Platforms = imgPlatforms
}
return ""
} }
func resolvePluginSpecRemote(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string { func resolvePluginSpecRemote(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string {
var warning string img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.PluginSpec.Remote, encodedAuth)
if img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.PluginSpec.Remote, encodedAuth); err != nil { if err != nil {
warning = digestWarning(taskSpec.PluginSpec.Remote) return digestWarning(taskSpec.PluginSpec.Remote)
} else {
taskSpec.PluginSpec.Remote = img
if len(imgPlatforms) > 0 {
if taskSpec.Placement == nil {
taskSpec.Placement = &swarm.Placement{}
}
taskSpec.Placement.Platforms = imgPlatforms
}
} }
return warning taskSpec.PluginSpec.Remote = img
if len(imgPlatforms) > 0 {
if taskSpec.Placement == nil {
taskSpec.Placement = &swarm.Placement{}
}
taskSpec.Placement.Platforms = imgPlatforms
}
return ""
} }
func imageDigestAndPlatforms(ctx context.Context, cli DistributionAPIClient, image, encodedAuth string) (string, []swarm.Platform, error) { func imageDigestAndPlatforms(ctx context.Context, cli DistributionAPIClient, image, encodedAuth string) (string, []swarm.Platform, error) {
+6 -4
View File
@@ -82,16 +82,18 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, options
options.Spec.TaskTemplate.ContainerSpec.Image = taggedImg options.Spec.TaskTemplate.ContainerSpec.Image = taggedImg
} }
if options.QueryRegistry { if options.QueryRegistry {
resolveWarning := resolveContainerSpecImage(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth) if warning := resolveContainerSpecImage(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth); warning != "" {
warnings = append(warnings, resolveWarning) warnings = append(warnings, warning)
}
} }
case options.Spec.TaskTemplate.PluginSpec != nil: case options.Spec.TaskTemplate.PluginSpec != nil:
if taggedImg := imageWithTagString(options.Spec.TaskTemplate.PluginSpec.Remote); taggedImg != "" { if taggedImg := imageWithTagString(options.Spec.TaskTemplate.PluginSpec.Remote); taggedImg != "" {
options.Spec.TaskTemplate.PluginSpec.Remote = taggedImg options.Spec.TaskTemplate.PluginSpec.Remote = taggedImg
} }
if options.QueryRegistry { if options.QueryRegistry {
resolveWarning := resolvePluginSpecRemote(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth) if warning := resolvePluginSpecRemote(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth); warning != "" {
warnings = append(warnings, resolveWarning) warnings = append(warnings, warning)
}
} }
} }
+42
View File
@@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strconv"
"strings" "strings"
"sync" "sync"
@@ -32,6 +33,47 @@ func trimID(objType, id string) (string, error) {
return id, nil return id, nil
} }
// parseAPIVersion checks v to be a well-formed ("<major>.<minor>")
// API version. It returns an error if the value is empty or does not
// have the correct format, but does not validate if the API version is
// within the supported range ([MinAPIVersion] <= v <= [MaxAPIVersion]).
//
// It returns version after normalizing, or an error if validation failed.
func parseAPIVersion(version string) (string, error) {
if strings.TrimPrefix(strings.TrimSpace(version), "v") == "" {
return "", cerrdefs.ErrInvalidArgument.WithMessage("value is empty")
}
major, minor, err := parseMajorMinor(version)
if err != nil {
return "", err
}
return fmt.Sprintf("%d.%d", major, minor), nil
}
// parseMajorMinor is a helper for parseAPIVersion.
func parseMajorMinor(v string) (major, minor int, _ error) {
if strings.HasPrefix(v, "v") {
return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("must be formatted <major>.<minor>")
}
if strings.TrimSpace(v) == "" {
return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("value is empty")
}
majVer, minVer, ok := strings.Cut(v, ".")
if !ok {
return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("must be formatted <major>.<minor>")
}
major, err := strconv.Atoi(majVer)
if err != nil {
return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("invalid major version: must be formatted <major>.<minor>")
}
minor, err = strconv.Atoi(minVer)
if err != nil {
return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("invalid minor version: must be formatted <major>.<minor>")
}
return major, minor, nil
}
// encodePlatforms marshals the given platform(s) to JSON format, to // encodePlatforms marshals the given platform(s) to JSON format, to
// be used for query-parameters for filtering / selecting platforms. // be used for query-parameters for filtering / selecting platforms.
func encodePlatforms(platform ...ocispec.Platform) ([]string, error) { func encodePlatforms(platform ...ocispec.Platform) ([]string, error) {
+2 -2
View File
@@ -219,7 +219,7 @@ github.com/davecgh/go-spew/spew
# github.com/distribution/reference v0.6.0 # github.com/distribution/reference v0.6.0
## explicit; go 1.20 ## explicit; go 1.20
github.com/distribution/reference github.com/distribution/reference
# github.com/docker/cli v29.0.1+incompatible # github.com/docker/cli v29.1.1+incompatible
## explicit ## explicit
github.com/docker/cli/cli github.com/docker/cli/cli
github.com/docker/cli/cli-plugins/metadata github.com/docker/cli/cli-plugins/metadata
@@ -543,7 +543,7 @@ github.com/moby/moby/api/types/storage
github.com/moby/moby/api/types/swarm github.com/moby/moby/api/types/swarm
github.com/moby/moby/api/types/system github.com/moby/moby/api/types/system
github.com/moby/moby/api/types/volume github.com/moby/moby/api/types/volume
# github.com/moby/moby/client v0.1.0 # github.com/moby/moby/client v0.2.1
## explicit; go 1.24.0 ## explicit; go 1.24.0
github.com/moby/moby/client github.com/moby/moby/client
github.com/moby/moby/client/internal github.com/moby/moby/client/internal