Repository: moby/moby
File: client/client.go (Go)

Code before:
		cli.negotiateLock.Lock()
		defer cli.negotiateLock.Unlock()

		ping, err := cli.Ping(ctx)
		if err != nil {
			// FIXME(thaJeztah): Ping returns an error when failing to connect to the API; we should not swallow the error here, and instead returning it.
			return
		}
		// FIXME(thaJeztah): we should not swallow the error here, and instead returning it.
		_ = cli.negotiateAPIVersion(ping.APIVersion)
	}
}

// NegotiateAPIVersionPing downgrades the client's API version to match the
// APIVersion in the ping response. If the API version in pingResponse is higher
// than the maximum version supported by the client, it uses the client's maximum
// version.
//
// 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.
//
// If the API server's ping response does not contain an API version, it falls
// back to the oldest API version supported.
func (cli *Client) NegotiateAPIVersionPing(pingResponse PingResult) {
	// TODO(thaJeztah): should this take a "Ping" option? It only consumes the version. This method should be removed overall and not be exported.
	if !cli.manualOverride {
		// Avoid concurrent modification of version-related fields
		cli.negotiateLock.Lock()
		defer cli.negotiateLock.Unlock()

		// FIXME(thaJeztah): we should not swallow the error here, and instead returning it.
		_ = cli.negotiateAPIVersion(pingResponse.APIVersion)
	}
}

// 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.
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))
	}

	// if the client is not initialized with a version, start with the latest supported version
	if cli.version == "" {

Review comment (suggestion):
I have some ideas to get rid of this, but will work on that for a follow-up;

- Make `Ping` also perform negotiation if the CLI is configured to do so
- Optionally, add a `PingOptions` struct to force negotiation

With that, we could potentially;

- Remove `NegotiateAPIVersionPing`
- Remove `NegotiateAPIVersion` (which would now become `Ping()` with the `NegotiateAPIVersion` option enabled



Code after:
		cli.negotiateLock.Lock()
		defer cli.negotiateLock.Unlock()

		ping, err := cli.Ping(ctx, PingOptions{})
		if err != nil {
			// FIXME(thaJeztah): Ping returns an error when failing to connect to the API; we should not swallow the error here, and instead returning it.
			return
		}
		// FIXME(thaJeztah): we should not swallow the error here, and instead returning it.
		_ = cli.negotiateAPIVersion(ping.APIVersion)
	}
}

// NegotiateAPIVersionPing downgrades the client's API version to match the
// APIVersion in the ping response. If the API version in pingResponse is higher
// than the maximum version supported by the client, it uses the client's maximum
// version.
//
// 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.
//
// If the API server's ping response does not contain an API version, it falls
// back to the oldest API version supported.
func (cli *Client) NegotiateAPIVersionPing(pingResponse PingResult) {
	// TODO(thaJeztah): should this take a "Ping" option? It only consumes the version. This method should be removed overall and not be exported.
	if !cli.manualOverride {
		// Avoid concurrent modification of version-related fields
		cli.negotiateLock.Lock()
		defer cli.negotiateLock.Unlock()

		// FIXME(thaJeztah): we should not swallow the error here, and instead returning it.
		_ = cli.negotiateAPIVersion(pingResponse.APIVersion)
	}
}

// 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.
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))
	}

	// if the client is not initialized with a version, start with the latest supported version
	if cli.version == "" {