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

Code before:
// 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 types.Ping) {
	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, err := parseAPIVersion(pingVersion)
	if err != nil {
		return err
	}
	if pingVersion == "" {
		pingVersion = fallbackAPIVersion
	}

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

	// if server version is lower than the client version, downgrade
	if versions.LessThan(pingVersion, cli.version) {
		cli.version = 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)
	}
	return nil
}


Review comment (suggestion):
From the name `parseAPIVersion` it's not clear that it's normalising the value and doing a min-version check ... maybe `validateAPIVersion`? (Or maybe just do it in-line here, but I guess you're planning to add more callers?)

Code after:
		// 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 == "" {
		cli.version = MaxAPIVersion
	}

	// if server version is lower than the client version, downgrade
	if versions.LessThan(pingVersion, cli.version) {
		cli.version = 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)
	}
	return nil
}

// DaemonHost returns the host address used by the client
func (cli *Client) DaemonHost() string {
	return cli.host
}

// ParseHostURL parses a url string, validates the string is a host url, and
// returns the parsed URL
func ParseHostURL(host string) (*url.URL, error) {
	proto, addr, ok := strings.Cut(host, "://")
	if !ok || addr == "" {
		return nil, fmt.Errorf("unable to parse docker host `%s`", host)