Skip to content

Commit 8d1b1e4

Browse files
authored
[grid] Minimum Docker API 1.44 for Docker Engine v29+ in Dynamic Grid (#16591)
1 parent 940e15f commit 8d1b1e4

22 files changed

+1112
-54
lines changed

java/src/org/openqa/selenium/docker/Docker.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,22 @@ public class Docker {
2727
private static final Logger LOG = Logger.getLogger(Docker.class.getName());
2828
protected final HttpHandler client;
2929
private volatile Optional<DockerProtocol> dockerClient;
30+
private final String apiVersion;
3031

3132
public Docker(HttpHandler client) {
33+
this(client, null);
34+
}
35+
36+
/**
37+
* Creates a Docker client with an optional API version override.
38+
*
39+
* @param client HTTP client for Docker communication
40+
* @param apiVersion Optional API version to use (e.g., "1.41" or "1.44"). If null, the version
41+
* will be auto-detected.
42+
*/
43+
public Docker(HttpHandler client, String apiVersion) {
3244
this.client = Require.nonNull("HTTP client", client);
45+
this.apiVersion = apiVersion;
3346
this.dockerClient = Optional.empty();
3447
}
3548

@@ -83,7 +96,11 @@ private Optional<DockerProtocol> getDocker() {
8396

8497
synchronized (this) {
8598
if (!dockerClient.isPresent()) {
86-
dockerClient = new VersionCommand(client).getDockerProtocol();
99+
VersionCommand versionCommand = new VersionCommand(client);
100+
dockerClient =
101+
apiVersion != null
102+
? versionCommand.getDockerProtocol(apiVersion)
103+
: versionCommand.getDockerProtocol();
87104
}
88105
}
89106

java/src/org/openqa/selenium/docker/VersionCommand.java

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import java.util.Map;
2626
import java.util.Optional;
2727
import java.util.function.Function;
28-
import org.openqa.selenium.docker.v1_41.V141Docker;
28+
import org.openqa.selenium.docker.client.DockerClient;
2929
import org.openqa.selenium.internal.Require;
3030
import org.openqa.selenium.json.Json;
3131
import org.openqa.selenium.json.JsonException;
@@ -38,15 +38,50 @@ class VersionCommand {
3838

3939
private static final Json JSON = new Json();
4040
// Insertion order matters, and is preserved by ImmutableMap.
41+
// Map Docker API versions to their implementations
42+
// 1.44 is the default for Docker Engine 29.0.0+
43+
// 1.41 is maintained for backward compatibility with legacy engines
44+
// Both use the same generic implementation with different API version strings
4145
private static final Map<Version, Function<HttpHandler, DockerProtocol>> SUPPORTED_VERSIONS =
42-
ImmutableMap.of(new Version("1.40"), V141Docker::new);
46+
ImmutableMap.of(
47+
new Version("1.44"), client -> new DockerClient(client, "1.44"),
48+
new Version("1.41"), client -> new DockerClient(client, "1.41"),
49+
new Version("1.40"), client -> new DockerClient(client, "1.40"));
4350

4451
private final HttpHandler handler;
4552

4653
public VersionCommand(HttpHandler handler) {
4754
this.handler = Require.nonNull("HTTP client", handler);
4855
}
4956

57+
/**
58+
* Gets the Docker protocol implementation for a user-specified API version. This allows users to
59+
* override the automatic version detection and force a specific API version (e.g., 1.41 for
60+
* legacy Docker engines).
61+
*
62+
* @param requestedVersion The API version to use (e.g., "1.41" or "1.44")
63+
* @return Optional containing the DockerProtocol implementation if the version is supported
64+
*/
65+
public Optional<DockerProtocol> getDockerProtocol(String requestedVersion) {
66+
if (requestedVersion == null || requestedVersion.isEmpty()) {
67+
return getDockerProtocol();
68+
}
69+
70+
Version version = new Version(requestedVersion);
71+
Function<HttpHandler, DockerProtocol> factory = SUPPORTED_VERSIONS.get(version);
72+
73+
if (factory != null) {
74+
return Optional.of(factory.apply(handler));
75+
}
76+
77+
// If exact version not found, try to find a compatible version
78+
return SUPPORTED_VERSIONS.entrySet().stream()
79+
.filter(entry -> entry.getKey().equalTo(version))
80+
.map(Map.Entry::getValue)
81+
.map(func -> func.apply(handler))
82+
.findFirst();
83+
}
84+
5085
public Optional<DockerProtocol> getDockerProtocol() {
5186
try {
5287
HttpResponse res = handler.execute(new HttpRequest(GET, "/version"));
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.openqa.selenium.docker.client;
19+
20+
import java.util.logging.Logger;
21+
22+
/**
23+
* Factory for creating API version-specific adapters.
24+
*
25+
* <p>This factory selects the appropriate adapter based on the Docker API version:
26+
*
27+
* <ul>
28+
* <li>API v1.40-1.43: Uses {@link V140Adapter}
29+
* <li>API v1.44+: Uses {@link V144Adapter}
30+
* </ul>
31+
*
32+
* <p>The factory uses version comparison to determine which adapter to use, ensuring that future
33+
* API versions (e.g., 1.45, 1.46) automatically use the most appropriate adapter.
34+
*/
35+
class AdapterFactory {
36+
37+
private static final Logger LOG = Logger.getLogger(AdapterFactory.class.getName());
38+
39+
/**
40+
* Creates an appropriate adapter for the given API version.
41+
*
42+
* @param apiVersion The Docker API version (e.g., "1.40", "1.44")
43+
* @return An adapter suitable for the specified API version
44+
* @throws IllegalArgumentException if apiVersion is null or empty
45+
*/
46+
public static ApiVersionAdapter createAdapter(String apiVersion) {
47+
if (apiVersion == null || apiVersion.trim().isEmpty()) {
48+
throw new IllegalArgumentException("API version cannot be null or empty");
49+
}
50+
51+
// API v1.44+ uses the new adapter
52+
if (compareVersions(apiVersion, "1.44") >= 0) {
53+
LOG.fine("Using V144Adapter for API version " + apiVersion);
54+
return new V144Adapter(apiVersion);
55+
}
56+
57+
// API v1.40-1.43 uses the legacy adapter
58+
LOG.fine("Using V140Adapter for API version " + apiVersion);
59+
return new V140Adapter(apiVersion);
60+
}
61+
62+
/**
63+
* Compares two version strings in the format "major.minor".
64+
*
65+
* @param version1 First version string (e.g., "1.44")
66+
* @param version2 Second version string (e.g., "1.44")
67+
* @return negative if version1 < version2, zero if equal, positive if version1 > version2
68+
*/
69+
private static int compareVersions(String version1, String version2) {
70+
String[] parts1 = version1.split("\\.");
71+
String[] parts2 = version2.split("\\.");
72+
73+
int major1 = Integer.parseInt(parts1[0]);
74+
int minor1 = parts1.length > 1 ? Integer.parseInt(parts1[1]) : 0;
75+
76+
int major2 = Integer.parseInt(parts2[0]);
77+
int minor2 = parts2.length > 1 ? Integer.parseInt(parts2[1]) : 0;
78+
79+
if (major1 != major2) {
80+
return major1 - major2;
81+
}
82+
return minor1 - minor2;
83+
}
84+
85+
/**
86+
* Determines if the given API version supports multiple network endpoints.
87+
*
88+
* @param apiVersion The Docker API version
89+
* @return true if multiple networks are supported, false otherwise
90+
*/
91+
public static boolean supportsMultipleNetworks(String apiVersion) {
92+
return createAdapter(apiVersion).supportsMultipleNetworks();
93+
}
94+
95+
/**
96+
* Determines if the given API version includes the VirtualSize field.
97+
*
98+
* @param apiVersion The Docker API version
99+
* @return true if VirtualSize field is present, false otherwise
100+
*/
101+
public static boolean hasVirtualSizeField(String apiVersion) {
102+
return createAdapter(apiVersion).hasVirtualSizeField();
103+
}
104+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.openqa.selenium.docker.client;
19+
20+
import java.util.Map;
21+
22+
/**
23+
* Adapter interface for handling API version-specific differences in Docker Engine API.
24+
*
25+
* <p>Different Docker API versions may have subtle differences in request/response formats, field
26+
* names, or behavior. Adapters provide a way to normalize these differences and ensure consistent
27+
* behavior across API versions.
28+
*
29+
* <p>Key differences handled by adapters:
30+
*
31+
* <ul>
32+
* <li>v1.40-1.43: Uses {@code VirtualSize} field in image responses
33+
* <li>v1.44+: {@code VirtualSize} removed, use {@code Size} field instead
34+
* <li>v1.44+: Support for multiple network endpoints in container creation
35+
* <li>v1.44+: Enhanced validation for network creation
36+
* </ul>
37+
*/
38+
interface ApiVersionAdapter {
39+
40+
/**
41+
* Adapts an image response from the Docker API to normalize field names and structure.
42+
*
43+
* <p>Example: In API v1.44+, {@code VirtualSize} was removed. This method ensures that {@code
44+
* Size} is always available regardless of API version.
45+
*
46+
* @param response The raw image response from Docker API
47+
* @return Adapted response with normalized field names
48+
*/
49+
Map<String, Object> adaptImageResponse(Map<String, Object> response);
50+
51+
/**
52+
* Adapts a container creation request to match the target API version's expectations.
53+
*
54+
* <p>Example: API v1.44+ supports multiple network endpoints, while earlier versions only support
55+
* a single network. This method ensures the request is formatted correctly.
56+
*
57+
* @param request The container creation request
58+
* @return Adapted request compatible with the target API version
59+
*/
60+
Map<String, Object> adaptContainerCreateRequest(Map<String, Object> request);
61+
62+
/**
63+
* Adapts a container inspection response to normalize field names and structure.
64+
*
65+
* <p>Example: Some fields may be deprecated or renamed across versions. This method ensures
66+
* consistent field names in the response.
67+
*
68+
* @param response The raw container inspection response from Docker API
69+
* @return Adapted response with normalized field names
70+
*/
71+
Map<String, Object> adaptContainerInspectResponse(Map<String, Object> response);
72+
73+
/**
74+
* Returns whether this API version supports multiple network endpoints in container creation.
75+
*
76+
* @return true if multiple networks are supported, false otherwise
77+
*/
78+
boolean supportsMultipleNetworks();
79+
80+
/**
81+
* Returns whether this API version includes the VirtualSize field in image responses.
82+
*
83+
* @return true if VirtualSize field is present, false otherwise
84+
*/
85+
boolean hasVirtualSizeField();
86+
87+
/**
88+
* Returns the API version this adapter is designed for.
89+
*
90+
* @return API version string (e.g., "1.40", "1.44")
91+
*/
92+
String getApiVersion();
93+
}

java/src/org/openqa/selenium/docker/v1_41/CreateContainer.java renamed to java/src/org/openqa/selenium/docker/client/CreateContainer.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18-
package org.openqa.selenium.docker.v1_41;
18+
package org.openqa.selenium.docker.client;
1919

20-
import static org.openqa.selenium.docker.v1_41.V141Docker.DOCKER_API_VERSION;
20+
import static org.openqa.selenium.docker.client.DockerClient.DOCKER_API_VERSION;
2121
import static org.openqa.selenium.json.Json.JSON_UTF_8;
2222
import static org.openqa.selenium.json.Json.MAP_TYPE;
2323
import static org.openqa.selenium.remote.http.Contents.asJson;
@@ -45,20 +45,38 @@ class CreateContainer {
4545
private static final Logger LOG = Logger.getLogger(CreateContainer.class.getName());
4646
private final DockerProtocol protocol;
4747
private final HttpHandler client;
48+
private final String apiVersion;
49+
private final ApiVersionAdapter adapter;
4850

4951
public CreateContainer(DockerProtocol protocol, HttpHandler client) {
52+
this(protocol, client, DOCKER_API_VERSION, AdapterFactory.createAdapter(DOCKER_API_VERSION));
53+
}
54+
55+
public CreateContainer(DockerProtocol protocol, HttpHandler client, String apiVersion) {
56+
this(protocol, client, apiVersion, AdapterFactory.createAdapter(apiVersion));
57+
}
58+
59+
public CreateContainer(
60+
DockerProtocol protocol, HttpHandler client, String apiVersion, ApiVersionAdapter adapter) {
5061
this.protocol = Require.nonNull("Protocol", protocol);
5162
this.client = Require.nonNull("HTTP client", client);
63+
this.apiVersion = Require.nonNull("API version", apiVersion);
64+
this.adapter = Require.nonNull("API version adapter", adapter);
5265
}
5366

5467
public Container apply(ContainerConfig info) {
5568
this.protocol.getImage(info.getImage().getName());
69+
70+
// Convert ContainerConfig to JSON and adapt for API version
71+
Map<String, Object> requestJson = JSON.toType(JSON.toJson(info), MAP_TYPE);
72+
Map<String, Object> adaptedRequest = adapter.adaptContainerCreateRequest(requestJson);
73+
5674
HttpResponse res =
5775
DockerMessages.throwIfNecessary(
5876
client.execute(
59-
new HttpRequest(POST, String.format("/v%s/containers/create", DOCKER_API_VERSION))
77+
new HttpRequest(POST, String.format("/v%s/containers/create", apiVersion))
6078
.addHeader("Content-Type", JSON_UTF_8)
61-
.setContent(asJson(info))),
79+
.setContent(asJson(adaptedRequest))),
6280
"Unable to create container: ",
6381
info);
6482

0 commit comments

Comments
 (0)