From 1abfbf298c649f563bb6f977cbf4b6d537c1c180 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 27 Nov 2025 11:19:57 +0100 Subject: [PATCH] vendor: github.com/moby/moby/client v0.2.1 full diff: https://github.com/moby/moby/compare/client/v0.1.0...v0.2.1 Signed-off-by: Sebastiaan van Stijn --- cli/command/cli_test.go | 4 +- cli/context/docker/load.go | 2 +- vendor.mod | 2 +- vendor.sum | 4 +- vendor/github.com/moby/moby/client/README.md | 17 ++- vendor/github.com/moby/moby/client/client.go | 82 ++++++++------ .../client/{options.go => client_options.go} | 101 +++++++++++++----- vendor/github.com/moby/moby/client/envvars.go | 2 +- vendor/github.com/moby/moby/client/ping.go | 33 ++++-- vendor/github.com/moby/moby/client/request.go | 84 +++++++++------ .../moby/moby/client/service_create.go | 56 +++++----- .../moby/moby/client/service_update.go | 10 +- vendor/github.com/moby/moby/client/utils.go | 42 ++++++++ vendor/modules.txt | 2 +- 14 files changed, 294 insertions(+), 147 deletions(-) rename vendor/github.com/moby/moby/client/{options.go => client_options.go} (75%) diff --git a/cli/command/cli_test.go b/cli/command/cli_test.go index 5213cbf28..27eb103a1 100644 --- a/cli/command/cli_test.go +++ b/cli/command/cli_test.go @@ -120,8 +120,8 @@ func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) { } func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) { - const customVersion = "v3.3.3" - const expectedVersion = "3.3.3" + const customVersion = "v3.3" + const expectedVersion = "3.3" t.Setenv("DOCKER_API_VERSION", customVersion) t.Setenv("DOCKER_HOST", ":2375") diff --git a/cli/context/docker/load.go b/cli/context/docker/load.go index 906647e0c..a5c44f934 100644 --- a/cli/context/docker/load.go +++ b/cli/context/docker/load.go @@ -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 } diff --git a/vendor.mod b/vendor.mod index 4b7ffa196..bb8e52227 100644 --- a/vendor.mod +++ b/vendor.mod @@ -29,7 +29,7 @@ require ( github.com/mattn/go-runewidth v0.0.19 github.com/moby/go-archive v0.1.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/patternmatcher v0.6.0 github.com/moby/swarmkit/v2 v2.1.1 github.com/moby/sys/atomicwriter v0.1.0 diff --git a/vendor.sum b/vendor.sum index fecdf0cd0..72557b6ec 100644 --- a/vendor.sum +++ b/vendor.sum @@ -115,8 +115,8 @@ github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg= 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.1.0/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE= +github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k= +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/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/swarmkit/v2 v2.1.1 h1:yvTJ8MMCc3f0qTA44J6R59EZ5yZawdYopkpuLk4+ICU= diff --git a/vendor/github.com/moby/moby/client/README.md b/vendor/github.com/moby/moby/client/README.md index c8c5b96f9..115e604db 100644 --- a/vendor/github.com/moby/moby/client/README.md +++ b/vendor/github.com/moby/moby/client/README.md @@ -23,19 +23,28 @@ import ( ) 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 { panic(err) } 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 { panic(err) } - for _, ctr := range containers { - fmt.Printf("%s %s (status: %s)\n", ctr.ID, ctr.Image, ctr.Status) + // Print each container's ID, status and the image it was created from. + 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) } } ``` diff --git a/vendor/github.com/moby/moby/client/client.go b/vendor/github.com/moby/moby/client/client.go index 3d1186144..0567ba11d 100644 --- a/vendor/github.com/moby/moby/client/client.go +++ b/vendor/github.com/moby/moby/client/client.go @@ -8,10 +8,8 @@ https://docs.docker.com/reference/api/engine/ You use the library by constructing a client object using [New] and calling methods on it. The client can be configured from environment -variables by passing the [FromEnv] option, and the [WithAPIVersionNegotiation] -option to allow downgrading the API version used when connecting with an older -daemon version. Other options cen be configured manually by passing any of -the available [Opt] options. +variables by passing the [FromEnv] option. Other options can be configured +manually by passing any of the available [Opt] options. 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 // API-version negotiation to allow downgrading the API 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 { log.Fatal(err) } @@ -103,18 +101,16 @@ import ( const DummyHost = "api.moby.localhost" // MaxAPIVersion is the highest REST API version supported by the client. -// If API-version negotiation is enabled (see [WithAPIVersionNegotiation], -// [Client.NegotiateAPIVersion]), the client may downgrade its API version. -// Similarly, the [WithVersion] and [WithVersionFromEnv] allow overriding -// the version. +// If API-version negotiation is enabled, the client may downgrade its API version. +// Similarly, the [WithAPIVersion] and [WithAPIVersionFromEnv] options allow +// overriding the version and disable API-version negotiation. // // This version may be lower than the version of the api library module used. const MaxAPIVersion = "1.52" -// fallbackAPIVersion is the version to fall back to if API-version negotiation -// fails. API versions below this version are not supported by the client, -// and not considered when negotiating. -const fallbackAPIVersion = "1.44" +// MinAPIVersion is the minimum API version supported by the client. API versions +// below this version are not considered when performing API-version negotiation. +const MinAPIVersion = "1.44" // Ensure that Client always implements APIClient. 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 // the order they're provided, which allows modifying the defaults when creating // the client. For example, the following initializes a client that configures -// itself with values from environment variables ([FromEnv]), and has automatic -// API version negotiation enabled ([WithAPIVersionNegotiation]). +// itself with values from environment variables ([FromEnv]). +// +// 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( // 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 { // 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 @@ -278,7 +285,7 @@ func (cli *Client) Close() error { // be negotiated when making the actual requests, and for which cases // we cannot do the negotiation lazily. func (cli *Client) checkVersion(ctx context.Context) error { - if cli.manualOverride || !cli.negotiateVersion || cli.negotiated.Load() { + if cli.negotiated.Load() { return nil } _, err := cli.Ping(ctx, PingOptions{ @@ -306,36 +313,47 @@ func (cli *Client) ClientVersion() string { } // negotiateAPIVersion updates the version to match the API version from -// the ping response. It falls back to the lowest version supported if the -// 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. +// the ping response. +// +// 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 { - pingVersion = strings.TrimPrefix(pingVersion, "v") - if pingVersion == "" { - // TODO(thaJeztah): consider returning an error on empty value or not falling back; see https://github.com/moby/moby/pull/51119#discussion_r2413148487 - pingVersion = fallbackAPIVersion - } 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)) + var err error + pingVersion, err = parseAPIVersion(pingVersion) + if err != nil { + return err + } + + 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 cli.version == "" { - cli.version = MaxAPIVersion + negotiatedVersion := cli.version + if negotiatedVersion == "" { + negotiatedVersion = MaxAPIVersion } // if server version is lower than the client version, downgrade - if versions.LessThan(pingVersion, cli.version) { - cli.version = pingVersion + if versions.LessThan(pingVersion, negotiatedVersion) { + negotiatedVersion = pingVersion } // Store the results, so that automatic API version negotiation (if enabled) // won't be performed on the next request. - if cli.negotiateVersion { - cli.negotiated.Store(true) - } + cli.setAPIVersion(negotiatedVersion) 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 func (cli *Client) DaemonHost() string { return cli.host diff --git a/vendor/github.com/moby/moby/client/options.go b/vendor/github.com/moby/moby/client/client_options.go similarity index 75% rename from vendor/github.com/moby/moby/client/options.go rename to vendor/github.com/moby/moby/client/client_options.go index 1b1e92bbb..295d29918 100644 --- a/vendor/github.com/moby/moby/client/options.go +++ b/vendor/github.com/moby/moby/client/client_options.go @@ -38,14 +38,22 @@ type clientConfig struct { userAgent *string // custom HTTP headers configured by users. 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 - // the API version to use when making requests. API version negotiation is - // performed on the first request, after which negotiated is set to "true" - // so that subsequent requests do not re-negotiate. - negotiateVersion bool + // manualAPIVersion contains the API version set by users. This field + // will only be non-empty if a valid-formed version was set through + // [WithAPIVersion]. + // + // 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 []otelhttp.Option @@ -56,7 +64,7 @@ type Opt func(*clientConfig) error // FromEnv configures the client with values from environment variables. It // is the equivalent of using the [WithTLSClientConfigFromEnv], [WithHostFromEnv], -// and [WithVersionFromEnv] options. +// and [WithAPIVersionFromEnv] options. // // FromEnv uses the following environment variables: // @@ -71,7 +79,7 @@ func FromEnv(c *clientConfig) error { ops := []Opt{ WithTLSClientConfigFromEnv(), WithHostFromEnv(), - WithVersionFromEnv(), + WithAPIVersionFromEnv(), } for _, op := range ops { 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 -// version is provided, the value is ignored to allow version negotiation -// (see [WithAPIVersionNegotiation]). +// WithAPIVersion overrides the client's API version with the specified one, +// and disables API version negotiation. If an empty version is provided, +// this option is ignored to allow version negotiation. The given version +// should be formatted "." (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, -// and callers should verify if the version is in the correct format and -// lower than the maximum supported version as defined by [MaxAPIVersion]. -func WithVersion(version string) Opt { +// 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 WithAPIVersion(version string) Opt { return func(c *clientConfig) error { - if v := strings.TrimPrefix(version, "v"); v != "" { - c.version = v - c.manualOverride = true + version = strings.TrimSpace(version) + 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.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 } @@ -260,25 +309,21 @@ func WithVersion(version string) Opt { // WithVersionFromEnv 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. // -// WithVersion does not validate if the client supports the given version, -// and callers should verify if the version is in the correct format and -// lower than the maximum supported version as defined by [MaxAPIVersion]. +// Deprecated: use [WithAPIVersionFromEnv] instead. func WithVersionFromEnv() Opt { - return func(c *clientConfig) error { - return WithVersion(os.Getenv(EnvOverrideAPIVersion))(c) - } + return WithAPIVersionFromEnv() } // WithAPIVersionNegotiation enables automatic API version negotiation for the client. // With this option enabled, 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. +// +// Deprecated: API-version negotiation is now enabled by default. Use [WithAPIVersion] +// or [WithAPIVersionFromEnv] to disable API version negotiation. func WithAPIVersionNegotiation() Opt { return func(c *clientConfig) error { - c.negotiateVersion = true return nil } } diff --git a/vendor/github.com/moby/moby/client/envvars.go b/vendor/github.com/moby/moby/client/envvars.go index 2b0e3f6b5..a02295d16 100644 --- a/vendor/github.com/moby/moby/client/envvars.go +++ b/vendor/github.com/moby/moby/client/envvars.go @@ -13,7 +13,7 @@ const ( // be used to override the API version to use. Value must be // 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. // // This environment variable should be used for debugging purposes only, as diff --git a/vendor/github.com/moby/moby/client/ping.go b/vendor/github.com/moby/moby/client/ping.go index 15c54faa0..d315e4b98 100644 --- a/vendor/github.com/moby/moby/client/ping.go +++ b/vendor/github.com/moby/moby/client/ping.go @@ -20,7 +20,7 @@ type PingOptions struct { // // If a manual override is in place, either through the "DOCKER_API_VERSION" // ([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 // client did not get a successful ping response, it assumes it is connected with @@ -29,9 +29,8 @@ type PingOptions struct { NegotiateAPIVersion bool // ForceNegotiate forces the client to re-negotiate the API version, even if - // API-version negotiation already happened. This option cannot be - // used if the client is configured with a fixed version using (using - // [WithVersion] or [WithVersionFromEnv]). + // API-version negotiation already happened or it the client is configured + // with a fixed version (using [WithAPIVersion] or [WithAPIVersionFromEnv]). // // This option has no effect if NegotiateAPIVersion is not set. ForceNegotiate bool @@ -72,10 +71,12 @@ type SwarmStatus struct { // for other non-success status codes, failing to connect to the API, or failing // to parse the API response. 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) } - if !options.NegotiateAPIVersion && !cli.negotiateVersion { + if cli.negotiated.Load() && !options.ForceNegotiate { + // API version was already negotiated or manually set. return cli.ping(ctx) } @@ -85,10 +86,19 @@ func (cli *Client) Ping(ctx context.Context, options PingOptions) (PingResult, e ping, err := cli.ping(ctx) if err != nil { - return cli.ping(ctx) + return ping, err } 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 } @@ -112,10 +122,15 @@ func (cli *Client) ping(ctx context.Context) (PingResult, error) { // response-body to get error details from. return newPingResult(resp), nil } + // close to allow reusing connection. + ensureReaderClosed(resp) // HEAD failed or returned a non-OK status; fallback to GET. - req.Method = http.MethodGet - resp, err = cli.doRequest(req) + req2, err := cli.buildRequest(ctx, http.MethodGet, path.Join(cli.basePath, "/_ping"), nil, nil) + if err != nil { + return PingResult{}, err + } + resp, err = cli.doRequest(req2) defer ensureReaderClosed(resp) if err != nil { // Failed to connect. diff --git a/vendor/github.com/moby/moby/client/request.go b/vendor/github.com/moby/moby/client/request.go index f5c2b956a..50c12eb15 100644 --- a/vendor/github.com/moby/moby/client/request.go +++ b/vendor/github.com/moby/moby/client/request.go @@ -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 // header. If body is nil, or a nil-interface, a "nil" body is returned without // 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) { - 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) if err != nil { 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{} if headers != nil { hdr = headers.Clone() } + // TODO(thaJeztah): should this return an error if a different Content-Type is already set? hdr.Set("Content-Type", "application/json") return jsonBody, hdr, nil } @@ -110,9 +101,6 @@ func (cli *Client) buildRequest(ctx context.Context, method, path string, body i req.Host = DummyHost } - if body != nil && req.Header.Get("Content-Type") == "" { - req.Header.Set("Content-Type", "text/plain") - } return req, nil } @@ -248,7 +236,11 @@ func checkResponseErr(serverResp *http.Response) (retErr error) { if statusMsg == "" { 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 bodyR := &io.LimitedReader{ 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) { - var params bytes.Buffer - if data != nil { - if err := json.NewEncoder(¶ms).Encode(data); err != nil { - return nil, err + switch x := data.(type) { + case nil: + return http.NoBody, nil + 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 ¶ms, 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) { - if response != nil && response.Body != nil { - // 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() + if response == nil || response.Body == nil { + return } + 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() } diff --git a/vendor/github.com/moby/moby/client/service_create.go b/vendor/github.com/moby/moby/client/service_create.go index 204d21816..319bca6f4 100644 --- a/vendor/github.com/moby/moby/client/service_create.go +++ b/vendor/github.com/moby/moby/client/service_create.go @@ -59,16 +59,18 @@ func (cli *Client) ServiceCreate(ctx context.Context, options ServiceCreateOptio options.Spec.TaskTemplate.ContainerSpec.Image = taggedImg } if options.QueryRegistry { - resolveWarning := resolveContainerSpecImage(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth) - warnings = append(warnings, resolveWarning) + if warning := resolveContainerSpecImage(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth); warning != "" { + warnings = append(warnings, warning) + } } case options.Spec.TaskTemplate.PluginSpec != nil: if taggedImg := imageWithTagString(options.Spec.TaskTemplate.PluginSpec.Remote); taggedImg != "" { options.Spec.TaskTemplate.PluginSpec.Remote = taggedImg } if options.QueryRegistry { - resolveWarning := resolvePluginSpecRemote(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth) - warnings = append(warnings, resolveWarning) + if warning := resolvePluginSpecRemote(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth); warning != "" { + 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 { - var warning string - if img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.ContainerSpec.Image, encodedAuth); err != nil { - warning = 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 - } + img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.ContainerSpec.Image, encodedAuth) + if err != nil { + return digestWarning(taskSpec.ContainerSpec.Image) } - 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 { - var warning string - if img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.PluginSpec.Remote, encodedAuth); err != nil { - warning = 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 - } + img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.PluginSpec.Remote, encodedAuth) + if err != nil { + return digestWarning(taskSpec.PluginSpec.Remote) } - 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) { diff --git a/vendor/github.com/moby/moby/client/service_update.go b/vendor/github.com/moby/moby/client/service_update.go index 9e6b52781..2505fe4b8 100644 --- a/vendor/github.com/moby/moby/client/service_update.go +++ b/vendor/github.com/moby/moby/client/service_update.go @@ -82,16 +82,18 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, options options.Spec.TaskTemplate.ContainerSpec.Image = taggedImg } if options.QueryRegistry { - resolveWarning := resolveContainerSpecImage(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth) - warnings = append(warnings, resolveWarning) + if warning := resolveContainerSpecImage(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth); warning != "" { + warnings = append(warnings, warning) + } } case options.Spec.TaskTemplate.PluginSpec != nil: if taggedImg := imageWithTagString(options.Spec.TaskTemplate.PluginSpec.Remote); taggedImg != "" { options.Spec.TaskTemplate.PluginSpec.Remote = taggedImg } if options.QueryRegistry { - resolveWarning := resolvePluginSpecRemote(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth) - warnings = append(warnings, resolveWarning) + if warning := resolvePluginSpecRemote(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth); warning != "" { + warnings = append(warnings, warning) + } } } diff --git a/vendor/github.com/moby/moby/client/utils.go b/vendor/github.com/moby/moby/client/utils.go index 6a5fc46ea..f2ba4744c 100644 --- a/vendor/github.com/moby/moby/client/utils.go +++ b/vendor/github.com/moby/moby/client/utils.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net/http" + "strconv" "strings" "sync" @@ -32,6 +33,47 @@ func trimID(objType, id string) (string, error) { return id, nil } +// parseAPIVersion checks v to be a well-formed (".") +// 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 .") + } + 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, err := strconv.Atoi(majVer) + if err != nil { + return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("invalid major version: must be formatted .") + } + minor, err = strconv.Atoi(minVer) + if err != nil { + return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("invalid minor version: must be formatted .") + } + return major, minor, nil +} + // encodePlatforms marshals the given platform(s) to JSON format, to // be used for query-parameters for filtering / selecting platforms. func encodePlatforms(platform ...ocispec.Platform) ([]string, error) { diff --git a/vendor/modules.txt b/vendor/modules.txt index b39636bcd..f744d2da0 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -189,7 +189,7 @@ 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.1.0 +# github.com/moby/moby/client v0.2.1 ## explicit; go 1.24.0 github.com/moby/moby/client github.com/moby/moby/client/internal