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 <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2025-11-27 11:19:57 +01:00
parent 848dcad809
commit 1abfbf298c
14 changed files with 294 additions and 147 deletions

View File

@ -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")

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
}

View File

@ -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

View File

@ -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=

View File

@ -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)
}
}
```

View File

@ -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

View File

@ -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 "<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,
// 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
}
}

View File

@ -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

View File

@ -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.

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
// 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(&params).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 &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) {
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()
}

View File

@ -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) {

View File

@ -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)
}
}
}

View File

@ -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 ("<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
// be used for query-parameters for filtering / selecting platforms.
func encodePlatforms(platform ...ocispec.Platform) ([]string, error) {

2
vendor/modules.txt vendored
View File

@ -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