Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package com.github.dockerjava.core;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.dockerjava.api.exception.DockerClientException;
import com.github.dockerjava.api.model.Version;
import com.github.dockerjava.transport.DockerHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Objects;

/**
* Performs one-shot Docker Remote API version negotiation against a running daemon.
* <p>
* Mirrors the behaviour of {@code moby/client.NegotiateAPIVersion}: queries {@code GET /version},
* reads the daemon's {@code ApiVersion} and {@code MinAPIVersion}, and returns
* {@code min(daemon ApiVersion, latest version known to docker-java)}. If that result is below the
* daemon's {@code MinAPIVersion}, the daemon's minimum is used instead and a warning is logged —
* matching {@code moby}'s behaviour.
* <p>
* Callers normally don't invoke this directly: enable
* {@link DefaultDockerClientConfig.Builder#withApiVersionAutoNegotiation(boolean)} and the
* {@link DockerClientImpl#getInstance(DockerClientConfig, DockerHttpClient)} factory will run
* negotiation automatically. The class is exposed as a public utility for callers that want to
* negotiate without going through {@code DockerClientImpl} (for example, when constructing
* commands manually for tests).
*/
public final class ApiVersionNegotiator {

private static final Logger LOGGER = LoggerFactory.getLogger(ApiVersionNegotiator.class);

private ApiVersionNegotiator() {
}

/**
* Calls {@code GET /version} via {@code httpClient}, parses the response with {@code objectMapper},
* and returns the negotiated {@link RemoteApiVersion}.
*
* @throws DockerClientException if the daemon is unreachable, returns a non-2xx status, or returns
* a {@code /version} payload without a parseable {@code ApiVersion}.
*/
@Nonnull
public static RemoteApiVersion negotiate(@Nonnull DockerHttpClient httpClient, @Nonnull ObjectMapper objectMapper) {
Objects.requireNonNull(httpClient, "httpClient was not specified");
Objects.requireNonNull(objectMapper, "objectMapper was not specified");

Version daemonVersion = fetchDaemonVersion(httpClient, objectMapper);

RemoteApiVersion daemonApi = parseOrThrow(daemonVersion.getApiVersion(), "ApiVersion");
RemoteApiVersion clientMax = latestSupported();

RemoteApiVersion negotiated = daemonApi.isGreater(clientMax) ? clientMax : daemonApi;

String minApiVersionString = daemonVersion.getMinAPIVersion();
if (minApiVersionString != null && !minApiVersionString.isEmpty()) {
RemoteApiVersion daemonMin = RemoteApiVersion.parseConfigWithDefault(minApiVersionString);
if (daemonMin != RemoteApiVersion.UNKNOWN_VERSION && daemonMin.isGreater(negotiated)) {
LOGGER.warn(
"Negotiated API version {} is below the daemon's minimum supported version {}. "
+ "Pinning to the daemon's minimum; some docker-java features may not work as expected.",
negotiated.getVersion(), daemonMin.getVersion());
negotiated = daemonMin;
}
}

LOGGER.debug("API version auto-negotiation result: daemon={} min={} client-max={} -> {}",
daemonApi.getVersion(),
minApiVersionString,
clientMax.getVersion(),
negotiated.getVersion());

return negotiated;
}

private static Version fetchDaemonVersion(DockerHttpClient httpClient, ObjectMapper objectMapper) {
DockerHttpClient.Request request = DockerHttpClient.Request.builder()
.method(DockerHttpClient.Request.Method.GET)
.path("/version")
.build();

try (DockerHttpClient.Response response = httpClient.execute(request);
InputStream body = response.getBody()) {
int status = response.getStatusCode();
if (status < 200 || status >= 300) {
throw new DockerClientException(
"Failed to negotiate Docker API version: GET /version returned HTTP " + status);
}
return objectMapper.readValue(body, Version.class);
} catch (IOException e) {
throw new DockerClientException("Failed to negotiate Docker API version", e);
}
}

private static RemoteApiVersion parseOrThrow(String version, String field) {
if (version == null || version.isEmpty()) {
throw new DockerClientException("Daemon /version response did not include a " + field);
}
RemoteApiVersion parsed = RemoteApiVersion.parseConfigWithDefault(version);
if (parsed == RemoteApiVersion.UNKNOWN_VERSION) {
throw new DockerClientException("Daemon /version returned unparseable " + field + ": " + version);
}
return parsed;
}

/**
* The highest {@code VERSION_1_X} constant declared on {@link RemoteApiVersion}. Discovered by
* reflection so adding new constants does not require touching this class.
*/
static RemoteApiVersion latestSupported() {
RemoteApiVersion highest = null;
for (Field field : RemoteApiVersion.class.getDeclaredFields()) {
if (!Modifier.isStatic(field.getModifiers()) || !Modifier.isPublic(field.getModifiers())) {
continue;
}
if (!RemoteApiVersion.class.equals(field.getType())) {
continue;
}
try {
RemoteApiVersion candidate = (RemoteApiVersion) field.get(null);
if (candidate == null || candidate == RemoteApiVersion.UNKNOWN_VERSION) {
continue;
}
if (highest == null || candidate.isGreater(highest)) {
highest = candidate;
}
} catch (IllegalAccessException e) {
throw new IllegalStateException("Unable to read " + field.getName(), e);
}
}
if (highest == null) {
throw new IllegalStateException("No VERSION_1_X constants found on RemoteApiVersion");
}
return highest;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ public class DefaultDockerClientConfig implements Serializable, DockerClientConf

public static final String API_VERSION = "api.version";

public static final String API_VERSION_AUTO_NEGOTIATION = "api.version.auto.negotiation";

/**
* Environment variable counterpart of {@link #API_VERSION_AUTO_NEGOTIATION}.
* <p>
* Accepted truthy values are {@code true} (case-insensitive) and {@code 1}; anything else
* (or unset) is treated as {@code false}.
*/
public static final String DOCKER_API_VERSION_AUTO_NEGOTIATION = "DOCKER_API_VERSION_AUTO_NEGOTIATION";

public static final String REGISTRY_USERNAME = "registry.username";

public static final String REGISTRY_PASSWORD = "registry.password";
Expand All @@ -74,6 +84,8 @@ public class DefaultDockerClientConfig implements Serializable, DockerClientConf
CONFIG_KEYS.add(DOCKER_CONFIG);
CONFIG_KEYS.add(DOCKER_CERT_PATH);
CONFIG_KEYS.add(API_VERSION);
CONFIG_KEYS.add(API_VERSION_AUTO_NEGOTIATION);
CONFIG_KEYS.add(DOCKER_API_VERSION_AUTO_NEGOTIATION);
CONFIG_KEYS.add(REGISTRY_USERNAME);
CONFIG_KEYS.add(REGISTRY_PASSWORD);
CONFIG_KEYS.add(REGISTRY_EMAIL);
Expand All @@ -92,15 +104,25 @@ public class DefaultDockerClientConfig implements Serializable, DockerClientConf

private final RemoteApiVersion apiVersion;

private final boolean apiVersionAutoNegotiation;

private final DockerConfigFile dockerConfig;

DefaultDockerClientConfig(URI dockerHost, DockerConfigFile dockerConfigFile, String dockerConfigPath, String apiVersion,
String registryUrl, String registryUsername, String registryPassword, String registryEmail,
SSLConfig sslConfig) {
this(dockerHost, dockerConfigFile, dockerConfigPath, apiVersion, registryUrl, registryUsername, registryPassword,
registryEmail, sslConfig, false);
}

DefaultDockerClientConfig(URI dockerHost, DockerConfigFile dockerConfigFile, String dockerConfigPath, String apiVersion,
String registryUrl, String registryUsername, String registryPassword, String registryEmail,
SSLConfig sslConfig, boolean apiVersionAutoNegotiation) {
this.dockerHost = checkDockerHostScheme(dockerHost);
this.dockerConfig = dockerConfigFile;
this.dockerConfigPath = dockerConfigPath;
this.apiVersion = RemoteApiVersion.parseConfigWithDefault(apiVersion);
this.apiVersionAutoNegotiation = apiVersionAutoNegotiation;
this.sslConfig = sslConfig;
this.registryUsername = registryUsername;
this.registryPassword = registryPassword;
Expand Down Expand Up @@ -247,6 +269,11 @@ public RemoteApiVersion getApiVersion() {
return apiVersion;
}

@Override
public boolean isApiVersionAutoNegotiationEnabled() {
return apiVersionAutoNegotiation;
}

@Override
public String getRegistryUsername() {
return registryUsername;
Expand Down Expand Up @@ -338,6 +365,8 @@ public static class Builder {

private Boolean dockerTlsVerify;

private Boolean apiVersionAutoNegotiation;

private SSLConfig customSslConfig = null;

/**
Expand All @@ -351,11 +380,17 @@ public Builder withProperties(Properties p) {
withDockerHost(p.getProperty(DOCKER_HOST));
}

String autoNegotiation = p.getProperty(API_VERSION_AUTO_NEGOTIATION);
if (autoNegotiation == null) {
autoNegotiation = p.getProperty(DOCKER_API_VERSION_AUTO_NEGOTIATION);
}

return withDockerTlsVerify(p.getProperty(DOCKER_TLS_VERIFY))
.withDockerContext(p.getProperty(DOCKER_CONTEXT))
.withDockerConfig(p.getProperty(DOCKER_CONFIG))
.withDockerCertPath(p.getProperty(DOCKER_CERT_PATH))
.withApiVersion(p.getProperty(API_VERSION))
.withApiVersionAutoNegotiation(autoNegotiation)
.withRegistryUsername(p.getProperty(REGISTRY_USERNAME))
.withRegistryPassword(p.getProperty(REGISTRY_PASSWORD))
.withRegistryEmail(p.getProperty(REGISTRY_EMAIL))
Expand All @@ -381,6 +416,31 @@ public final Builder withApiVersion(String apiVersion) {
return this;
}

/**
* Opt the client in to one-shot API version auto-negotiation against the daemon's
* {@code /version} endpoint at construction time. See
* {@link DockerClientConfig#isApiVersionAutoNegotiationEnabled()}.
*/
public final Builder withApiVersionAutoNegotiation(boolean apiVersionAutoNegotiation) {
this.apiVersionAutoNegotiation = apiVersionAutoNegotiation;
return this;
}

/**
* String overload that matches the {@link #withDockerTlsVerify(String)} parsing semantics:
* {@code "true"} (case-insensitive) and {@code "1"} are truthy; anything else (or {@code null})
* is falsy. Used when wiring the flag in from a {@link Properties} source.
*/
public final Builder withApiVersionAutoNegotiation(String apiVersionAutoNegotiation) {
if (apiVersionAutoNegotiation != null) {
String trimmed = apiVersionAutoNegotiation.trim();
this.apiVersionAutoNegotiation = "true".equalsIgnoreCase(trimmed) || "1".equals(trimmed);
} else {
this.apiVersionAutoNegotiation = false;
}
return this;
}

public final Builder withRegistryUsername(String registryUsername) {
this.registryUsername = registryUsername;
return this;
Expand Down Expand Up @@ -488,7 +548,7 @@ public DefaultDockerClientConfig build() {
: URI.create(SystemUtils.IS_OS_WINDOWS ? WINDOWS_DEFAULT_DOCKER_HOST : DEFAULT_DOCKER_HOST);

return new DefaultDockerClientConfig(dockerHostUri, dockerConfigFile, dockerConfig, apiVersion, registryUrl, registryUsername,
registryPassword, registryEmail, sslConfig);
registryPassword, registryEmail, sslConfig, isTrue(apiVersionAutoNegotiation));
}

private DockerConfigFile readDockerConfig() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ static ObjectMapper getDefaultObjectMapper() {
default ObjectMapper getObjectMapper() {
return getDefaultObjectMapper();
}

/**
* Whether the client should query the daemon's {@code /version} endpoint at construction time
* and pin the API version to {@code min(daemon API version, latest API version supported by docker-java)}.
* <p>
* Mirrors {@code moby/client.NegotiateAPIVersion}.
* <p>
* Default {@code false}: callers that have not opted in keep the existing behaviour
* (explicit {@code withApiVersion(...)} or {@link RemoteApiVersion#UNKNOWN_VERSION}).
*/
default boolean isApiVersionAutoNegotiationEnabled() {
return false;
}
}

enum DefaultObjectMapperHolder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,23 @@ public static DockerClientImpl getInstance(DockerClientConfig dockerClientConfig
}

public static DockerClient getInstance(DockerClientConfig dockerClientConfig, DockerHttpClient dockerHttpClient) {
return new DockerClientImpl(dockerClientConfig)
DockerClientConfig effectiveConfig = maybeNegotiateApiVersion(dockerClientConfig, dockerHttpClient);
return new DockerClientImpl(effectiveConfig)
.withHttpClient(dockerHttpClient);
}

private static DockerClientConfig maybeNegotiateApiVersion(DockerClientConfig config, DockerHttpClient httpClient) {
if (!config.isApiVersionAutoNegotiationEnabled()) {
return config;
}
if (config.getApiVersion() != RemoteApiVersion.UNKNOWN_VERSION) {
// Explicit withApiVersion(...) wins, same precedence the Docker CLI uses.
return config;
}
RemoteApiVersion negotiated = ApiVersionNegotiator.negotiate(httpClient, config.getObjectMapper());
return new NegotiatedDockerClientConfig(config, negotiated);
}

/**
*
* @deprecated use {@link #getInstance(DockerClientConfig, DockerHttpClient)}
Expand Down
Loading
Loading