From ca10286cbbec7f1517bf1d3b01e052b0a1265ab4 Mon Sep 17 00:00:00 2001 From: Viet Nguyen Duc Date: Fri, 14 Nov 2025 22:19:21 +0700 Subject: [PATCH 1/2] [grid] Improve Docker client for Dynamic Grid Signed-off-by: Viet Nguyen Duc --- .../org/openqa/selenium/docker/Docker.java | 2 +- .../selenium/docker/VersionCommand.java | 13 +- .../docker/client/AdapterFactory.java | 15 +- .../docker/client/CreateContainer.java | 9 - .../selenium/docker/client/DockerClient.java | 5 - .../docker/client/GetContainerLogs.java | 5 - .../docker/client/InspectContainer.java | 5 - .../docker/client/IsContainerPresent.java | 5 - .../selenium/docker/client/ListImages.java | 7 +- .../selenium/docker/client/PullImage.java | 5 - .../docker/client/StartContainer.java | 5 - .../selenium/docker/client/StopContainer.java | 5 - .../selenium/docker/client/V140Adapter.java | 2 +- .../selenium/docker/client/V148Adapter.java | 260 ++++++++++++++++++ .../grid/node/docker/DockerFlags.java | 6 +- .../grid/node/docker/DockerOptions.java | 3 +- .../openqa/selenium/docker/BootstrapTest.java | 2 +- .../selenium/docker/VersionCommandTest.java | 98 ++++++- .../docker/client/ListImagesTest.java | 4 +- .../docker/client/V140AdapterTest.java | 4 +- .../docker/client/V148AdapterTest.java | 245 +++++++++++++++++ 21 files changed, 632 insertions(+), 73 deletions(-) create mode 100644 java/src/org/openqa/selenium/docker/client/V148Adapter.java create mode 100644 java/test/org/openqa/selenium/docker/client/V148AdapterTest.java diff --git a/java/src/org/openqa/selenium/docker/Docker.java b/java/src/org/openqa/selenium/docker/Docker.java index 1c55ba606746e..48fed70c2aada 100644 --- a/java/src/org/openqa/selenium/docker/Docker.java +++ b/java/src/org/openqa/selenium/docker/Docker.java @@ -37,7 +37,7 @@ public Docker(HttpHandler client) { * Creates a Docker client with an optional API version override. * * @param client HTTP client for Docker communication - * @param apiVersion Optional API version to use (e.g., "1.41" or "1.44"). If null, the version + * @param apiVersion Optional API version to use (e.g., "1.40" or "1.44"). If null, the version * will be auto-detected. */ public Docker(HttpHandler client, String apiVersion) { diff --git a/java/src/org/openqa/selenium/docker/VersionCommand.java b/java/src/org/openqa/selenium/docker/VersionCommand.java index d5ba5bc1e907f..2060a4808878e 100644 --- a/java/src/org/openqa/selenium/docker/VersionCommand.java +++ b/java/src/org/openqa/selenium/docker/VersionCommand.java @@ -39,13 +39,14 @@ class VersionCommand { private static final Json JSON = new Json(); // Insertion order matters, and is preserved by ImmutableMap. // Map Docker API versions to their implementations - // 1.44 is the default for Docker Engine 29.0.0+ - // 1.41 is maintained for backward compatibility with legacy engines - // Both use the same generic implementation with different API version strings + // 1.48 is for Docker Engine v28+ with multi-platform and gateway priority support + // 1.44 is for Docker Engine v25+ with multi-network and modern features + // 1.40 is maintained for backward compatibility with legacy engines (Docker v19.03+) + // All use the same generic implementation with version-specific adapters private static final Map> SUPPORTED_VERSIONS = ImmutableMap.of( + new Version("1.48"), client -> new DockerClient(client, "1.48"), new Version("1.44"), client -> new DockerClient(client, "1.44"), - new Version("1.41"), client -> new DockerClient(client, "1.41"), new Version("1.40"), client -> new DockerClient(client, "1.40")); private final HttpHandler handler; @@ -56,10 +57,10 @@ public VersionCommand(HttpHandler handler) { /** * Gets the Docker protocol implementation for a user-specified API version. This allows users to - * override the automatic version detection and force a specific API version (e.g., 1.41 for + * override the automatic version detection and force a specific API version (e.g., 1.40 for * legacy Docker engines). * - * @param requestedVersion The API version to use (e.g., "1.41" or "1.44") + * @param requestedVersion The API version to use (e.g., "1.40" or "1.44") * @return Optional containing the DockerProtocol implementation if the version is supported */ public Optional getDockerProtocol(String requestedVersion) { diff --git a/java/src/org/openqa/selenium/docker/client/AdapterFactory.java b/java/src/org/openqa/selenium/docker/client/AdapterFactory.java index 31634a1fce94a..fad9effca11c4 100644 --- a/java/src/org/openqa/selenium/docker/client/AdapterFactory.java +++ b/java/src/org/openqa/selenium/docker/client/AdapterFactory.java @@ -26,11 +26,12 @@ * *
    *
  • API v1.40-1.43: Uses {@link V140Adapter} - *
  • API v1.44+: Uses {@link V144Adapter} + *
  • API v1.44-1.47: Uses {@link V144Adapter} + *
  • API v1.48+: Uses {@link V148Adapter} *
* *

The factory uses version comparison to determine which adapter to use, ensuring that future - * API versions (e.g., 1.45, 1.46) automatically use the most appropriate adapter. + * API versions automatically use the most appropriate adapter. */ class AdapterFactory { @@ -39,7 +40,7 @@ class AdapterFactory { /** * Creates an appropriate adapter for the given API version. * - * @param apiVersion The Docker API version (e.g., "1.40", "1.44") + * @param apiVersion The Docker API version (e.g., "1.40", "1.44", "1.48") * @return An adapter suitable for the specified API version * @throws IllegalArgumentException if apiVersion is null or empty */ @@ -48,7 +49,13 @@ public static ApiVersionAdapter createAdapter(String apiVersion) { throw new IllegalArgumentException("API version cannot be null or empty"); } - // API v1.44+ uses the new adapter + // API v1.48+ uses the latest adapter with multi-platform and gateway priority support + if (compareVersions(apiVersion, "1.48") >= 0) { + LOG.fine("Using V148Adapter for API version " + apiVersion); + return new V148Adapter(apiVersion); + } + + // API v1.44-1.47 uses the v1.44 adapter if (compareVersions(apiVersion, "1.44") >= 0) { LOG.fine("Using V144Adapter for API version " + apiVersion); return new V144Adapter(apiVersion); diff --git a/java/src/org/openqa/selenium/docker/client/CreateContainer.java b/java/src/org/openqa/selenium/docker/client/CreateContainer.java index bf12da7ad4e40..56a47551be487 100644 --- a/java/src/org/openqa/selenium/docker/client/CreateContainer.java +++ b/java/src/org/openqa/selenium/docker/client/CreateContainer.java @@ -17,7 +17,6 @@ package org.openqa.selenium.docker.client; -import static org.openqa.selenium.docker.client.DockerClient.DOCKER_API_VERSION; import static org.openqa.selenium.json.Json.JSON_UTF_8; import static org.openqa.selenium.json.Json.MAP_TYPE; import static org.openqa.selenium.remote.http.Contents.asJson; @@ -48,14 +47,6 @@ class CreateContainer { private final String apiVersion; private final ApiVersionAdapter adapter; - public CreateContainer(DockerProtocol protocol, HttpHandler client) { - this(protocol, client, DOCKER_API_VERSION, AdapterFactory.createAdapter(DOCKER_API_VERSION)); - } - - public CreateContainer(DockerProtocol protocol, HttpHandler client, String apiVersion) { - this(protocol, client, apiVersion, AdapterFactory.createAdapter(apiVersion)); - } - public CreateContainer( DockerProtocol protocol, HttpHandler client, String apiVersion, ApiVersionAdapter adapter) { this.protocol = Require.nonNull("Protocol", protocol); diff --git a/java/src/org/openqa/selenium/docker/client/DockerClient.java b/java/src/org/openqa/selenium/docker/client/DockerClient.java index f798d0e87b50d..307d3c91c9445 100644 --- a/java/src/org/openqa/selenium/docker/client/DockerClient.java +++ b/java/src/org/openqa/selenium/docker/client/DockerClient.java @@ -34,7 +34,6 @@ public class DockerClient implements DockerProtocol { - static final String DOCKER_API_VERSION = "1.41"; private static final Logger LOG = Logger.getLogger(DockerClient.class.getName()); private final String apiVersion; private final ApiVersionAdapter adapter; @@ -47,10 +46,6 @@ public class DockerClient implements DockerProtocol { private final InspectContainer inspectContainer; private final GetContainerLogs containerLogs; - public DockerClient(HttpHandler client) { - this(client, DOCKER_API_VERSION); - } - public DockerClient(HttpHandler client, String apiVersion) { Require.nonNull("HTTP client", client); this.apiVersion = Require.nonNull("API version", apiVersion); diff --git a/java/src/org/openqa/selenium/docker/client/GetContainerLogs.java b/java/src/org/openqa/selenium/docker/client/GetContainerLogs.java index 4f70f574e59ae..f314240f62dc6 100644 --- a/java/src/org/openqa/selenium/docker/client/GetContainerLogs.java +++ b/java/src/org/openqa/selenium/docker/client/GetContainerLogs.java @@ -18,7 +18,6 @@ package org.openqa.selenium.docker.client; import static java.net.HttpURLConnection.HTTP_OK; -import static org.openqa.selenium.docker.client.DockerClient.DOCKER_API_VERSION; import static org.openqa.selenium.remote.http.HttpMethod.GET; import java.util.Arrays; @@ -38,10 +37,6 @@ class GetContainerLogs { private final HttpHandler client; private final String apiVersion; - public GetContainerLogs(HttpHandler client) { - this(client, DOCKER_API_VERSION); - } - public GetContainerLogs(HttpHandler client, String apiVersion) { this.client = Require.nonNull("HTTP client", client); this.apiVersion = Require.nonNull("API version", apiVersion); diff --git a/java/src/org/openqa/selenium/docker/client/InspectContainer.java b/java/src/org/openqa/selenium/docker/client/InspectContainer.java index aa857375ff860..e575b78bd8a01 100644 --- a/java/src/org/openqa/selenium/docker/client/InspectContainer.java +++ b/java/src/org/openqa/selenium/docker/client/InspectContainer.java @@ -18,7 +18,6 @@ package org.openqa.selenium.docker.client; import static java.net.HttpURLConnection.HTTP_OK; -import static org.openqa.selenium.docker.client.DockerClient.DOCKER_API_VERSION; import static org.openqa.selenium.json.Json.MAP_TYPE; import static org.openqa.selenium.remote.http.HttpMethod.GET; @@ -44,10 +43,6 @@ class InspectContainer { private final String apiVersion; private final ApiVersionAdapter adapter; - public InspectContainer(HttpHandler client) { - this(client, DOCKER_API_VERSION, AdapterFactory.createAdapter(DOCKER_API_VERSION)); - } - public InspectContainer(HttpHandler client, String apiVersion) { this(client, apiVersion, AdapterFactory.createAdapter(apiVersion)); } diff --git a/java/src/org/openqa/selenium/docker/client/IsContainerPresent.java b/java/src/org/openqa/selenium/docker/client/IsContainerPresent.java index 0adf1215ed1e8..d3bb3d7c01b0d 100644 --- a/java/src/org/openqa/selenium/docker/client/IsContainerPresent.java +++ b/java/src/org/openqa/selenium/docker/client/IsContainerPresent.java @@ -17,7 +17,6 @@ package org.openqa.selenium.docker.client; -import static org.openqa.selenium.docker.client.DockerClient.DOCKER_API_VERSION; import static org.openqa.selenium.remote.http.HttpMethod.GET; import org.openqa.selenium.docker.ContainerId; @@ -30,10 +29,6 @@ class IsContainerPresent { private final HttpHandler client; private final String apiVersion; - public IsContainerPresent(HttpHandler client) { - this(client, DOCKER_API_VERSION); - } - public IsContainerPresent(HttpHandler client, String apiVersion) { this.client = Require.nonNull("Http client", client); this.apiVersion = Require.nonNull("API version", apiVersion); diff --git a/java/src/org/openqa/selenium/docker/client/ListImages.java b/java/src/org/openqa/selenium/docker/client/ListImages.java index 13be1edd4aac8..3cfd2db79ddf0 100644 --- a/java/src/org/openqa/selenium/docker/client/ListImages.java +++ b/java/src/org/openqa/selenium/docker/client/ListImages.java @@ -18,7 +18,6 @@ package org.openqa.selenium.docker.client; import static com.google.common.collect.ImmutableSet.toImmutableSet; -import static org.openqa.selenium.docker.client.DockerClient.DOCKER_API_VERSION; import static org.openqa.selenium.json.Json.JSON_UTF_8; import static org.openqa.selenium.remote.http.Contents.string; import static org.openqa.selenium.remote.http.HttpMethod.GET; @@ -47,10 +46,6 @@ class ListImages { private final String apiVersion; private final ApiVersionAdapter adapter; - public ListImages(HttpHandler client) { - this(client, DOCKER_API_VERSION, AdapterFactory.createAdapter(DOCKER_API_VERSION)); - } - public ListImages(HttpHandler client, String apiVersion) { this(client, apiVersion, AdapterFactory.createAdapter(apiVersion)); } @@ -67,7 +62,7 @@ public Set apply(Reference reference) { String familiarName = reference.getFamiliarName(); Map filters = ImmutableMap.of("reference", ImmutableMap.of(familiarName, true)); - // https://docs.docker.com/engine/api/v1.41/#operation/ImageList + // https://docs.docker.com/engine/api/v1.40/#operation/ImageList HttpRequest req = new HttpRequest(GET, String.format("/v%s/images/json", apiVersion)) .addHeader("Content-Type", JSON_UTF_8) diff --git a/java/src/org/openqa/selenium/docker/client/PullImage.java b/java/src/org/openqa/selenium/docker/client/PullImage.java index 74349569fb5dd..80ff2a37619a7 100644 --- a/java/src/org/openqa/selenium/docker/client/PullImage.java +++ b/java/src/org/openqa/selenium/docker/client/PullImage.java @@ -17,7 +17,6 @@ package org.openqa.selenium.docker.client; -import static org.openqa.selenium.docker.client.DockerClient.DOCKER_API_VERSION; import static org.openqa.selenium.json.Json.JSON_UTF_8; import static org.openqa.selenium.json.Json.MAP_TYPE; import static org.openqa.selenium.remote.http.HttpMethod.POST; @@ -39,10 +38,6 @@ class PullImage { private final HttpHandler client; private final String apiVersion; - public PullImage(HttpHandler client) { - this(client, DOCKER_API_VERSION); - } - public PullImage(HttpHandler client, String apiVersion) { this.client = Require.nonNull("HTTP client", client); this.apiVersion = Require.nonNull("API version", apiVersion); diff --git a/java/src/org/openqa/selenium/docker/client/StartContainer.java b/java/src/org/openqa/selenium/docker/client/StartContainer.java index a2888edb63867..0deea19a5d27a 100644 --- a/java/src/org/openqa/selenium/docker/client/StartContainer.java +++ b/java/src/org/openqa/selenium/docker/client/StartContainer.java @@ -17,7 +17,6 @@ package org.openqa.selenium.docker.client; -import static org.openqa.selenium.docker.client.DockerClient.DOCKER_API_VERSION; import static org.openqa.selenium.docker.client.DockerMessages.throwIfNecessary; import static org.openqa.selenium.remote.http.HttpMethod.POST; @@ -30,10 +29,6 @@ class StartContainer { private final HttpHandler client; private final String apiVersion; - public StartContainer(HttpHandler client) { - this(client, DOCKER_API_VERSION); - } - public StartContainer(HttpHandler client, String apiVersion) { this.client = Require.nonNull("HTTP client", client); this.apiVersion = Require.nonNull("API version", apiVersion); diff --git a/java/src/org/openqa/selenium/docker/client/StopContainer.java b/java/src/org/openqa/selenium/docker/client/StopContainer.java index 230b570490c28..e0018b7ba786c 100644 --- a/java/src/org/openqa/selenium/docker/client/StopContainer.java +++ b/java/src/org/openqa/selenium/docker/client/StopContainer.java @@ -17,7 +17,6 @@ package org.openqa.selenium.docker.client; -import static org.openqa.selenium.docker.client.DockerClient.DOCKER_API_VERSION; import static org.openqa.selenium.docker.client.DockerMessages.throwIfNecessary; import static org.openqa.selenium.remote.http.HttpMethod.POST; @@ -31,10 +30,6 @@ class StopContainer { private final HttpHandler client; private final String apiVersion; - public StopContainer(HttpHandler client) { - this(client, DOCKER_API_VERSION); - } - public StopContainer(HttpHandler client, String apiVersion) { this.client = Require.nonNull("HTTP client", client); this.apiVersion = Require.nonNull("API version", apiVersion); diff --git a/java/src/org/openqa/selenium/docker/client/V140Adapter.java b/java/src/org/openqa/selenium/docker/client/V140Adapter.java index 45f90e0811442..c515a74a0f825 100644 --- a/java/src/org/openqa/selenium/docker/client/V140Adapter.java +++ b/java/src/org/openqa/selenium/docker/client/V140Adapter.java @@ -29,7 +29,7 @@ *

    *
  • Use {@code VirtualSize} field in image responses *
  • Support single network endpoint in container creation - *
  • Use {@code filter} parameter (deprecated in 1.41+, use {@code filters}) + *
  • Use {@code filter} parameter (deprecated in 1.40+, use {@code filters}) *
* *

This adapter normalizes responses to ensure compatibility with newer code that expects the diff --git a/java/src/org/openqa/selenium/docker/client/V148Adapter.java b/java/src/org/openqa/selenium/docker/client/V148Adapter.java new file mode 100644 index 0000000000000..2fee945471c2e --- /dev/null +++ b/java/src/org/openqa/selenium/docker/client/V148Adapter.java @@ -0,0 +1,260 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.openqa.selenium.docker.client; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +/** + * Adapter for Docker API version 1.48 and later. + * + *

Key features of API v1.48+: + * + *

    + *
  • Multi-platform image support with {@code platform} parameter + *
  • Image mount type support ({@code Mount.Type = "image"}) + *
  • Gateway priority ({@code GwPriority}) for network endpoints + *
  • Enhanced image manifest descriptors ({@code ImageManifestDescriptor}) + *
  • IPv4 IPAM control ({@code EnableIPv4} for networks) + *
  • Improved progress reporting (deprecated {@code error} and {@code progress} fields) + *
+ * + *

This adapter extends v1.44 functionality with v1.48-specific enhancements for better + * multi-platform support and network control. + */ +class V148Adapter implements ApiVersionAdapter { + + private static final Logger LOG = Logger.getLogger(V148Adapter.class.getName()); + private final String apiVersion; + + public V148Adapter(String apiVersion) { + this.apiVersion = apiVersion; + } + + @Override + public Map adaptImageResponse(Map response) { + if (response == null) { + return response; + } + + Map adapted = new HashMap<>(response); + + // v1.48+ includes ImageManifestDescriptor for multi-platform images + if (adapted.containsKey("ImageManifestDescriptor")) { + LOG.fine( + "Image response includes ImageManifestDescriptor (multi-platform support in API v" + + apiVersion + + ")"); + } + + // v1.48+ includes Descriptor field (OCI descriptor) + if (adapted.containsKey("Descriptor")) { + LOG.fine("Image response includes OCI Descriptor field (API v" + apiVersion + ")"); + } + + // Ensure VirtualSize is not present (removed in v1.44) + if (adapted.containsKey("VirtualSize")) { + LOG.warning( + "VirtualSize field found in API v" + + apiVersion + + " response. This field was removed in v1.44."); + adapted.remove("VirtualSize"); + } + + return adapted; + } + + @Override + public Map adaptContainerCreateRequest(Map request) { + if (request == null) { + return request; + } + + Map adapted = new HashMap<>(request); + + // v1.48+ supports Mount type "image" for mounting images inside containers + @SuppressWarnings("unchecked") + Map hostConfig = (Map) adapted.get("HostConfig"); + + if (hostConfig != null) { + @SuppressWarnings("unchecked") + Object mounts = hostConfig.get("Mounts"); + + if (mounts instanceof Iterable) { + for (Object mount : (Iterable) mounts) { + if (mount instanceof Map) { + @SuppressWarnings("unchecked") + Map mountMap = (Map) mount; + String type = (String) mountMap.get("Type"); + + if ("image".equals(type)) { + LOG.fine( + "Container creation includes image mount type (supported in API v" + + apiVersion + + "+)"); + } + } + } + } + } + + // v1.48+ supports GwPriority in NetworkingConfig for gateway priority + @SuppressWarnings("unchecked") + Map networkingConfig = (Map) adapted.get("NetworkingConfig"); + + if (networkingConfig != null) { + @SuppressWarnings("unchecked") + Map endpointsConfig = + (Map) networkingConfig.get("EndpointsConfig"); + + if (endpointsConfig != null) { + for (Map.Entry entry : endpointsConfig.entrySet()) { + if (entry.getValue() instanceof Map) { + @SuppressWarnings("unchecked") + Map endpointConfig = (Map) entry.getValue(); + + if (endpointConfig.containsKey("GwPriority")) { + LOG.fine( + "Network endpoint '" + + entry.getKey() + + "' includes GwPriority (supported in API v" + + apiVersion + + "+)"); + } + } + } + } + } + + return adapted; + } + + @Override + public Map adaptContainerInspectResponse(Map response) { + if (response == null) { + return response; + } + + Map adapted = new HashMap<>(response); + + // v1.48+ includes ImageManifestDescriptor + if (adapted.containsKey("ImageManifestDescriptor")) { + LOG.fine( + "Container inspect includes ImageManifestDescriptor (multi-platform support in API v" + + apiVersion + + ")"); + } + + // v1.48+ includes GwPriority in NetworkSettings + @SuppressWarnings("unchecked") + Map networkSettings = (Map) adapted.get("NetworkSettings"); + + if (networkSettings != null) { + @SuppressWarnings("unchecked") + Map networks = (Map) networkSettings.get("Networks"); + + if (networks != null) { + for (Map.Entry entry : networks.entrySet()) { + if (entry.getValue() instanceof Map) { + @SuppressWarnings("unchecked") + Map network = (Map) entry.getValue(); + + if (network.containsKey("GwPriority")) { + LOG.fine( + "Network '" + entry.getKey() + "' includes GwPriority (API v" + apiVersion + ")"); + } + } + } + } + + // Remove deprecated fields (should not be present in v1.48+) + String[] deprecatedFields = { + "HairpinMode", + "LinkLocalIPv6Address", + "LinkLocalIPv6PrefixLen", + "SecondaryIPAddresses", + "SecondaryIPv6Addresses", + "Bridge" // Deprecated in v1.51, removed in v1.52 + }; + + for (String field : deprecatedFields) { + if (networkSettings.containsKey(field)) { + LOG.fine( + "Removing deprecated field '" + + field + + "' from NetworkSettings (deprecated in earlier API versions)"); + networkSettings.remove(field); + } + } + } + + return adapted; + } + + @Override + public boolean supportsMultipleNetworks() { + return true; // v1.48+ supports multiple network endpoints (inherited from v1.44) + } + + @Override + public boolean hasVirtualSizeField() { + return false; // v1.48+ does not include VirtualSize (removed in v1.44) + } + + @Override + public String getApiVersion() { + return apiVersion; + } + + /** + * Checks if this adapter supports multi-platform image operations. + * + * @return true for v1.48+ which supports platform-specific operations + */ + public boolean supportsMultiPlatform() { + return true; + } + + /** + * Checks if this adapter supports image mount type. + * + * @return true for v1.48+ which supports mounting images inside containers + */ + public boolean supportsImageMountType() { + return true; + } + + /** + * Checks if this adapter supports gateway priority for network endpoints. + * + * @return true for v1.48+ which supports GwPriority + */ + public boolean supportsGatewayPriority() { + return true; + } + + /** + * Checks if this adapter supports IPv4 IPAM control. + * + * @return true for v1.48+ which supports EnableIPv4 for networks + */ + public boolean supportsIPv4Control() { + return true; + } +} diff --git a/java/src/org/openqa/selenium/grid/node/docker/DockerFlags.java b/java/src/org/openqa/selenium/grid/node/docker/DockerFlags.java index 51c77b0694d39..78b685c2c2dfd 100644 --- a/java/src/org/openqa/selenium/grid/node/docker/DockerFlags.java +++ b/java/src/org/openqa/selenium/grid/node/docker/DockerFlags.java @@ -57,9 +57,9 @@ public class DockerFlags implements HasRoles { @Parameter( names = {"--docker-api-version"}, description = - "Docker API version to use. Only supported values are 1.41 (for Docker Engine" - + " older than v25) and 1.44 (for Docker Engine v29+). Default is 1.44.") - @ConfigValue(section = DockerOptions.DOCKER_SECTION, name = "api-version", example = "1.41") + "Docker API version to use. Pin an API version instead of auto-detecting by" + + " implementation") + @ConfigValue(section = DockerOptions.DOCKER_SECTION, name = "api-version", example = "1.40") private String apiVersion; @Parameter( diff --git a/java/src/org/openqa/selenium/grid/node/docker/DockerOptions.java b/java/src/org/openqa/selenium/grid/node/docker/DockerOptions.java index 5ced3e3176487..193b8cd885240 100644 --- a/java/src/org/openqa/selenium/grid/node/docker/DockerOptions.java +++ b/java/src/org/openqa/selenium/grid/node/docker/DockerOptions.java @@ -65,7 +65,6 @@ public class DockerOptions { static final String DEFAULT_VIDEO_IMAGE = "false"; static final int DEFAULT_MAX_SESSIONS = Runtime.getRuntime().availableProcessors(); static final int DEFAULT_SERVER_START_TIMEOUT = 60; - static final String DEFAULT_DOCKER_API_VERSION = "1.44"; private static final String DEFAULT_DOCKER_NETWORK = "bridge"; private static final Logger LOG = Logger.getLogger(DockerOptions.class.getName()); private static final Json JSON = new Json(); @@ -117,7 +116,7 @@ private Duration getServerStartTimeout() { } private String getApiVersion() { - return config.get(DOCKER_SECTION, "api-version").orElse(DEFAULT_DOCKER_API_VERSION); + return config.get(DOCKER_SECTION, "api-version").orElse(null); } private boolean isEnabled(Docker docker) { diff --git a/java/test/org/openqa/selenium/docker/BootstrapTest.java b/java/test/org/openqa/selenium/docker/BootstrapTest.java index 92c2e758d2c6c..01da49d9526b7 100644 --- a/java/test/org/openqa/selenium/docker/BootstrapTest.java +++ b/java/test/org/openqa/selenium/docker/BootstrapTest.java @@ -81,7 +81,7 @@ void shouldComplainBitterlyIfNoSupportedVersionOfDockerProtocolIsFound() { .setContent( utf8String( "{\"message\":\"client version 1.50 is too new. Maximum supported API" - + " version is 1.41\"}")); + + " version is 1.44\"}")); boolean isSupported = new Docker(client).isSupported(); diff --git a/java/test/org/openqa/selenium/docker/VersionCommandTest.java b/java/test/org/openqa/selenium/docker/VersionCommandTest.java index e553f447584ed..6e8fc63eace6e 100644 --- a/java/test/org/openqa/selenium/docker/VersionCommandTest.java +++ b/java/test/org/openqa/selenium/docker/VersionCommandTest.java @@ -93,6 +93,102 @@ void shouldReturnADockerInstanceIfTheVersionOfTheApiSupportedIsOneSeleniumAlsoSu Optional maybeDocker = new VersionCommand(handler).getDockerProtocol(); assertThat(maybeDocker).isPresent(); - assertThat(maybeDocker.get().version()).isEqualTo("1.41"); + assertThat(maybeDocker.get().version()).isEqualTo("1.40"); + } + + @Test + void shouldSupportDockerEngine290WithApi152() { + // Docker Engine 29.0 uses API version 1.52 (max) and 1.44 (min) + // Should select 1.48 as it's within the supported range [1.44, 1.52] + HttpHandler handler = + req -> + new HttpResponse() + .addHeader("Content-Type", "application/json") + .setContent(utf8String("{\"ApiVersion\":\"1.52\",\"MinAPIVersion\":\"1.44\"}")); + + Optional maybeDocker = new VersionCommand(handler).getDockerProtocol(); + + assertThat(maybeDocker).isPresent(); + assertThat(maybeDocker.get().version()).isEqualTo("1.48"); + } + + @Test + void shouldSupportDockerEngine250WithApi144() { + // Docker Engine 25.0 uses API version 1.44 (max) and 1.24 (min) + // Should select 1.44 as it matches exactly + HttpHandler handler = + req -> + new HttpResponse() + .addHeader("Content-Type", "application/json") + .setContent(utf8String("{\"ApiVersion\":\"1.44\",\"MinAPIVersion\":\"1.24\"}")); + + Optional maybeDocker = new VersionCommand(handler).getDockerProtocol(); + + assertThat(maybeDocker).isPresent(); + assertThat(maybeDocker.get().version()).isEqualTo("1.44"); + } + + @Test + void shouldSupportDockerEngine2010WithApi140() { + // Docker Engine 20.10 uses API version 1.41 (max) and 1.12 (min) + // Should select 1.40 as it's within the supported range [1.12, 1.41] + HttpHandler handler = + req -> + new HttpResponse() + .addHeader("Content-Type", "application/json") + .setContent(utf8String("{\"ApiVersion\":\"1.41\",\"MinAPIVersion\":\"1.12\"}")); + + Optional maybeDocker = new VersionCommand(handler).getDockerProtocol(); + + assertThat(maybeDocker).isPresent(); + assertThat(maybeDocker.get().version()).isEqualTo("1.40"); + } + + @Test + void shouldSupportDockerEngine1903WithApi140() { + // Docker Engine 19.03 uses API version 1.40 (max) and 1.12 (min) + // Should select 1.40 as it matches exactly + HttpHandler handler = + req -> + new HttpResponse() + .addHeader("Content-Type", "application/json") + .setContent(utf8String("{\"ApiVersion\":\"1.40\",\"MinAPIVersion\":\"1.12\"}")); + + Optional maybeDocker = new VersionCommand(handler).getDockerProtocol(); + + assertThat(maybeDocker).isPresent(); + assertThat(maybeDocker.get().version()).isEqualTo("1.40"); + } + + @Test + void shouldSupportDockerEngine280WithApi148() { + // Docker Engine 28.0 uses API version 1.48 (max) and 1.24 (min) + // Should select 1.48 as it matches exactly + HttpHandler handler = + req -> + new HttpResponse() + .addHeader("Content-Type", "application/json") + .setContent(utf8String("{\"ApiVersion\":\"1.48\",\"MinAPIVersion\":\"1.24\"}")); + + Optional maybeDocker = new VersionCommand(handler).getDockerProtocol(); + + assertThat(maybeDocker).isPresent(); + assertThat(maybeDocker.get().version()).isEqualTo("1.48"); + } + + @Test + void shouldSupportDockerEngine290WithApi150() { + // Docker Engine 29.0 uses API version 1.50 (max) and 1.24 (min) + // Should select 1.48 as it's the highest supported version within range + HttpHandler handler = + req -> + new HttpResponse() + .addHeader("Content-Type", "application/json") + .setContent(utf8String("{\"ApiVersion\":\"1.50\",\"MinAPIVersion\":\"1.24\"}")); + + Optional maybeDocker = new VersionCommand(handler).getDockerProtocol(); + + assertThat(maybeDocker).isPresent(); + assertThat(maybeDocker.get().version()).isEqualTo("1.48"); } } diff --git a/java/test/org/openqa/selenium/docker/client/ListImagesTest.java b/java/test/org/openqa/selenium/docker/client/ListImagesTest.java index 50e5c44c2995a..5a117d06b77b0 100644 --- a/java/test/org/openqa/selenium/docker/client/ListImagesTest.java +++ b/java/test/org/openqa/selenium/docker/client/ListImagesTest.java @@ -36,8 +36,8 @@ class ListImagesTest { @Test - void shouldReturnImageIfTagIsPresentWithApi141() { - testListImages("1.41"); + void shouldReturnImageIfTagIsPresentWithApi140() { + testListImages("1.40"); } @Test diff --git a/java/test/org/openqa/selenium/docker/client/V140AdapterTest.java b/java/test/org/openqa/selenium/docker/client/V140AdapterTest.java index 0a0fe661eed1a..581139b6437cc 100644 --- a/java/test/org/openqa/selenium/docker/client/V140AdapterTest.java +++ b/java/test/org/openqa/selenium/docker/client/V140AdapterTest.java @@ -25,11 +25,11 @@ class V140AdapterTest { - private final V140Adapter adapter = new V140Adapter("1.41"); + private final V140Adapter adapter = new V140Adapter("1.40"); @Test void shouldReturnCorrectApiVersion() { - assertThat(adapter.getApiVersion()).isEqualTo("1.41"); + assertThat(adapter.getApiVersion()).isEqualTo("1.40"); } @Test diff --git a/java/test/org/openqa/selenium/docker/client/V148AdapterTest.java b/java/test/org/openqa/selenium/docker/client/V148AdapterTest.java new file mode 100644 index 0000000000000..393f808183722 --- /dev/null +++ b/java/test/org/openqa/selenium/docker/client/V148AdapterTest.java @@ -0,0 +1,245 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.openqa.selenium.docker.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class V148AdapterTest { + + @Test + void shouldReturnCorrectApiVersion() { + V148Adapter adapter = new V148Adapter("1.48"); + assertThat(adapter.getApiVersion()).isEqualTo("1.48"); + } + + @Test + void shouldSupportMultipleNetworks() { + V148Adapter adapter = new V148Adapter("1.48"); + assertThat(adapter.supportsMultipleNetworks()).isTrue(); + } + + @Test + void shouldNotHaveVirtualSizeField() { + V148Adapter adapter = new V148Adapter("1.48"); + assertThat(adapter.hasVirtualSizeField()).isFalse(); + } + + @Test + void shouldSupportMultiPlatform() { + V148Adapter adapter = new V148Adapter("1.48"); + assertThat(adapter.supportsMultiPlatform()).isTrue(); + } + + @Test + void shouldSupportImageMountType() { + V148Adapter adapter = new V148Adapter("1.48"); + assertThat(adapter.supportsImageMountType()).isTrue(); + } + + @Test + void shouldSupportGatewayPriority() { + V148Adapter adapter = new V148Adapter("1.48"); + assertThat(adapter.supportsGatewayPriority()).isTrue(); + } + + @Test + void shouldSupportIPv4Control() { + V148Adapter adapter = new V148Adapter("1.48"); + assertThat(adapter.supportsIPv4Control()).isTrue(); + } + + @Test + void shouldRemoveVirtualSizeFromImageResponse() { + V148Adapter adapter = new V148Adapter("1.48"); + + Map response = new HashMap<>(); + response.put("Id", "sha256:abc123"); + response.put("Size", 123456789L); + response.put("VirtualSize", 987654321L); // Should be removed + + Map adapted = adapter.adaptImageResponse(response); + + assertThat(adapted).containsKey("Size"); + assertThat(adapted).doesNotContainKey("VirtualSize"); + assertThat(adapted.get("Size")).isEqualTo(123456789L); + } + + @Test + void shouldHandleImageManifestDescriptorInImageResponse() { + V148Adapter adapter = new V148Adapter("1.48"); + + Map descriptor = Map.of("digest", "sha256:xyz789", "platform", "linux/amd64"); + + Map response = new HashMap<>(); + response.put("Id", "sha256:abc123"); + response.put("Size", 123456789L); + response.put("ImageManifestDescriptor", descriptor); + response.put("Descriptor", Map.of("mediaType", "application/vnd.oci.image.manifest.v1+json")); + + Map adapted = adapter.adaptImageResponse(response); + + assertThat(adapted).containsKey("ImageManifestDescriptor"); + assertThat(adapted).containsKey("Descriptor"); + } + + @Test + void shouldHandleImageMountTypeInContainerCreateRequest() { + V148Adapter adapter = new V148Adapter("1.48"); + + List> mounts = new ArrayList<>(); + Map imageMount = new HashMap<>(); + imageMount.put("Type", "image"); + imageMount.put("Source", "myimage:latest"); + imageMount.put("Target", "/mnt/image"); + mounts.add(imageMount); + + Map hostConfig = Map.of("Mounts", mounts); + Map request = Map.of("HostConfig", hostConfig); + + Map adapted = adapter.adaptContainerCreateRequest(request); + + assertThat(adapted).isNotNull(); + assertThat(adapted).containsKey("HostConfig"); + } + + @Test + void shouldHandleGatewayPriorityInContainerCreateRequest() { + V148Adapter adapter = new V148Adapter("1.48"); + + Map endpoint1 = new HashMap<>(); + endpoint1.put("IPAMConfig", Map.of("IPv4Address", "172.20.0.2")); + endpoint1.put("GwPriority", 100); + + Map endpoint2 = new HashMap<>(); + endpoint2.put("IPAMConfig", Map.of("IPv4Address", "172.21.0.2")); + endpoint2.put("GwPriority", 200); + + Map endpointsConfig = new HashMap<>(); + endpointsConfig.put("network1", endpoint1); + endpointsConfig.put("network2", endpoint2); + + Map networkingConfig = Map.of("EndpointsConfig", endpointsConfig); + Map request = Map.of("NetworkingConfig", networkingConfig); + + Map adapted = adapter.adaptContainerCreateRequest(request); + + assertThat(adapted).isNotNull(); + assertThat(adapted).containsKey("NetworkingConfig"); + } + + @Test + void shouldHandleImageManifestDescriptorInContainerInspectResponse() { + V148Adapter adapter = new V148Adapter("1.48"); + + Map descriptor = + Map.of( + "digest", "sha256:abc123", "platform", Map.of("architecture", "amd64", "os", "linux")); + + Map response = new HashMap<>(); + response.put("Id", "container123"); + response.put("ImageManifestDescriptor", descriptor); + + Map adapted = adapter.adaptContainerInspectResponse(response); + + assertThat(adapted).containsKey("ImageManifestDescriptor"); + assertThat(adapted.get("ImageManifestDescriptor")).isEqualTo(descriptor); + } + + @Test + void shouldHandleGatewayPriorityInContainerInspectResponse() { + V148Adapter adapter = new V148Adapter("1.48"); + + Map network1 = new HashMap<>(); + network1.put("IPAddress", "172.20.0.2"); + network1.put("GwPriority", 100); + + Map network2 = new HashMap<>(); + network2.put("IPAddress", "172.21.0.2"); + network2.put("GwPriority", 200); + + Map networks = new HashMap<>(); + networks.put("network1", network1); + networks.put("network2", network2); + + Map networkSettings = Map.of("Networks", networks); + Map response = Map.of("NetworkSettings", networkSettings); + + Map adapted = adapter.adaptContainerInspectResponse(response); + + assertThat(adapted).containsKey("NetworkSettings"); + @SuppressWarnings("unchecked") + Map adaptedNetworkSettings = + (Map) adapted.get("NetworkSettings"); + @SuppressWarnings("unchecked") + Map adaptedNetworks = + (Map) adaptedNetworkSettings.get("Networks"); + @SuppressWarnings("unchecked") + Map adaptedNetwork1 = (Map) adaptedNetworks.get("network1"); + + assertThat(adaptedNetwork1).containsKey("GwPriority"); + assertThat(adaptedNetwork1.get("GwPriority")).isEqualTo(100); + } + + @Test + void shouldRemoveDeprecatedFieldsFromContainerInspectResponse() { + V148Adapter adapter = new V148Adapter("1.48"); + + Map networkSettings = new HashMap<>(); + networkSettings.put("IPAddress", "172.17.0.2"); + networkSettings.put("HairpinMode", false); // Deprecated + networkSettings.put("LinkLocalIPv6Address", "fe80::1"); // Deprecated + networkSettings.put("Bridge", "docker0"); // Deprecated + + Map response = Map.of("NetworkSettings", networkSettings); + + Map adapted = adapter.adaptContainerInspectResponse(response); + + @SuppressWarnings("unchecked") + Map adaptedNetworkSettings = + (Map) adapted.get("NetworkSettings"); + + assertThat(adaptedNetworkSettings).containsKey("IPAddress"); + assertThat(adaptedNetworkSettings).doesNotContainKey("HairpinMode"); + assertThat(adaptedNetworkSettings).doesNotContainKey("LinkLocalIPv6Address"); + assertThat(adaptedNetworkSettings).doesNotContainKey("Bridge"); + } + + @Test + void shouldHandleNullImageResponse() { + V148Adapter adapter = new V148Adapter("1.48"); + assertThat(adapter.adaptImageResponse(null)).isNull(); + } + + @Test + void shouldHandleNullContainerCreateRequest() { + V148Adapter adapter = new V148Adapter("1.48"); + assertThat(adapter.adaptContainerCreateRequest(null)).isNull(); + } + + @Test + void shouldHandleNullContainerInspectResponse() { + V148Adapter adapter = new V148Adapter("1.48"); + assertThat(adapter.adaptContainerInspectResponse(null)).isNull(); + } +} From fe4ab2b681428ea30828131b4e6763eb608ceb26 Mon Sep 17 00:00:00 2001 From: Viet Nguyen Duc Date: Sat, 15 Nov 2025 01:52:40 +0700 Subject: [PATCH 2/2] Fix review comment Signed-off-by: Viet Nguyen Duc --- .../selenium/docker/client/V144Adapter.java | 9 +- .../selenium/docker/client/V148Adapter.java | 167 ++++++++++++++---- .../docker/client/V144AdapterTest.java | 29 +++ .../docker/client/V148AdapterTest.java | 34 +++- 4 files changed, 202 insertions(+), 37 deletions(-) diff --git a/java/src/org/openqa/selenium/docker/client/V144Adapter.java b/java/src/org/openqa/selenium/docker/client/V144Adapter.java index a6d2142dba809..cba4b625bc5a0 100644 --- a/java/src/org/openqa/selenium/docker/client/V144Adapter.java +++ b/java/src/org/openqa/selenium/docker/client/V144Adapter.java @@ -115,9 +115,14 @@ public Map adaptContainerInspectResponse(Map res // v1.44+ includes DNSNames field // Ensure deprecated fields are handled if present @SuppressWarnings("unchecked") - Map networkSettings = (Map) adapted.get("NetworkSettings"); + Map originalNetworkSettings = + (Map) adapted.get("NetworkSettings"); + + if (originalNetworkSettings != null) { + // Create defensive copy to avoid mutating the original response + Map networkSettings = new HashMap<>(originalNetworkSettings); + adapted.put("NetworkSettings", networkSettings); - if (networkSettings != null) { // Remove deprecated fields if present (they shouldn't be in v1.44+) String[] deprecatedFields = { "HairpinMode", diff --git a/java/src/org/openqa/selenium/docker/client/V148Adapter.java b/java/src/org/openqa/selenium/docker/client/V148Adapter.java index 2fee945471c2e..c23f73caa0aa6 100644 --- a/java/src/org/openqa/selenium/docker/client/V148Adapter.java +++ b/java/src/org/openqa/selenium/docker/client/V148Adapter.java @@ -55,19 +55,6 @@ public Map adaptImageResponse(Map response) { Map adapted = new HashMap<>(response); - // v1.48+ includes ImageManifestDescriptor for multi-platform images - if (adapted.containsKey("ImageManifestDescriptor")) { - LOG.fine( - "Image response includes ImageManifestDescriptor (multi-platform support in API v" - + apiVersion - + ")"); - } - - // v1.48+ includes Descriptor field (OCI descriptor) - if (adapted.containsKey("Descriptor")) { - LOG.fine("Image response includes OCI Descriptor field (API v" + apiVersion + ")"); - } - // Ensure VirtualSize is not present (removed in v1.44) if (adapted.containsKey("VirtualSize")) { LOG.warning( @@ -77,6 +64,46 @@ public Map adaptImageResponse(Map response) { adapted.remove("VirtualSize"); } + // Ensure Size field is present (required in v1.44+) + if (!adapted.containsKey("Size")) { + LOG.warning("Size field missing from image response in API v" + apiVersion); + } + + // v1.48+ includes ImageManifestDescriptor for multi-platform images + // Extract platform information for better observability + if (adapted.containsKey("ImageManifestDescriptor")) { + @SuppressWarnings("unchecked") + Map descriptor = (Map) adapted.get("ImageManifestDescriptor"); + if (descriptor != null && descriptor.containsKey("platform")) { + Object platformObj = descriptor.get("platform"); + if (platformObj instanceof Map) { + @SuppressWarnings("unchecked") + Map platform = (Map) platformObj; + String arch = (String) platform.get("architecture"); + String os = (String) platform.get("os"); + if (arch != null && os != null) { + LOG.fine( + String.format( + "Image is platform-specific: %s/%s (API v%s multi-platform support)", + os, arch, apiVersion)); + } + } + } + } + + // v1.48+ includes Descriptor field (OCI descriptor) + // Validate OCI descriptor structure + if (adapted.containsKey("Descriptor")) { + @SuppressWarnings("unchecked") + Map descriptor = (Map) adapted.get("Descriptor"); + if (descriptor != null) { + String mediaType = (String) descriptor.get("mediaType"); + if (mediaType != null) { + LOG.fine("Image includes OCI descriptor with mediaType: " + mediaType); + } + } + } + return adapted; } @@ -89,6 +116,7 @@ public Map adaptContainerCreateRequest(Map reque Map adapted = new HashMap<>(request); // v1.48+ supports Mount type "image" for mounting images inside containers + // Validate image mount configurations @SuppressWarnings("unchecked") Map hostConfig = (Map) adapted.get("HostConfig"); @@ -104,10 +132,23 @@ public Map adaptContainerCreateRequest(Map reque String type = (String) mountMap.get("Type"); if ("image".equals(type)) { - LOG.fine( - "Container creation includes image mount type (supported in API v" - + apiVersion - + "+)"); + // Validate required fields for image mounts + String source = (String) mountMap.get("Source"); + String target = (String) mountMap.get("Target"); + + if (source == null || source.isEmpty()) { + LOG.warning("Image mount missing required 'Source' field"); + } + if (target == null || target.isEmpty()) { + LOG.warning("Image mount missing required 'Target' field"); + } + + if (source != null && target != null) { + LOG.fine( + String.format( + "Mounting image '%s' at '%s' (API v%s+ image mount support)", + source, target, apiVersion)); + } } } } @@ -115,6 +156,7 @@ public Map adaptContainerCreateRequest(Map reque } // v1.48+ supports GwPriority in NetworkingConfig for gateway priority + // Validate and log gateway priority configuration @SuppressWarnings("unchecked") Map networkingConfig = (Map) adapted.get("NetworkingConfig"); @@ -123,22 +165,39 @@ public Map adaptContainerCreateRequest(Map reque Map endpointsConfig = (Map) networkingConfig.get("EndpointsConfig"); - if (endpointsConfig != null) { + if (endpointsConfig != null && endpointsConfig.size() > 1) { + // Track gateway priorities for multi-network containers + int highestPriority = Integer.MIN_VALUE; + String defaultGatewayNetwork = null; + for (Map.Entry entry : endpointsConfig.entrySet()) { if (entry.getValue() instanceof Map) { @SuppressWarnings("unchecked") Map endpointConfig = (Map) entry.getValue(); if (endpointConfig.containsKey("GwPriority")) { - LOG.fine( - "Network endpoint '" - + entry.getKey() - + "' includes GwPriority (supported in API v" - + apiVersion - + "+)"); + Object priorityObj = endpointConfig.get("GwPriority"); + int priority = priorityObj instanceof Number ? ((Number) priorityObj).intValue() : 0; + + if (priority > highestPriority) { + highestPriority = priority; + defaultGatewayNetwork = entry.getKey(); + } } } } + + if (defaultGatewayNetwork != null) { + LOG.fine( + String.format( + "Container will use '%s' as default gateway (priority: %d, API v%s+)", + defaultGatewayNetwork, highestPriority, apiVersion)); + } else { + LOG.fine( + String.format( + "Creating container with %d networks, no explicit gateway priority set", + endpointsConfig.size())); + } } } @@ -153,34 +212,74 @@ public Map adaptContainerInspectResponse(Map res Map adapted = new HashMap<>(response); - // v1.48+ includes ImageManifestDescriptor + // v1.48+ includes ImageManifestDescriptor with platform information + // Extract and expose platform details for better observability if (adapted.containsKey("ImageManifestDescriptor")) { - LOG.fine( - "Container inspect includes ImageManifestDescriptor (multi-platform support in API v" - + apiVersion - + ")"); + @SuppressWarnings("unchecked") + Map descriptor = (Map) adapted.get("ImageManifestDescriptor"); + if (descriptor != null && descriptor.containsKey("platform")) { + Object platformObj = descriptor.get("platform"); + if (platformObj instanceof Map) { + @SuppressWarnings("unchecked") + Map platform = (Map) platformObj; + String arch = (String) platform.get("architecture"); + String os = (String) platform.get("os"); + String digest = (String) descriptor.get("digest"); + + if (arch != null && os != null) { + LOG.fine( + String.format( + "Container running on %s/%s platform (digest: %s, API v%s+)", + os, + arch, + digest != null ? digest.substring(0, Math.min(12, digest.length())) : "unknown", + apiVersion)); + } + } + } } // v1.48+ includes GwPriority in NetworkSettings + // Identify which network is providing the default gateway @SuppressWarnings("unchecked") - Map networkSettings = (Map) adapted.get("NetworkSettings"); + Map originalNetworkSettings = + (Map) adapted.get("NetworkSettings"); + + if (originalNetworkSettings != null) { + // Create defensive copy to avoid mutating the original response + Map networkSettings = new HashMap<>(originalNetworkSettings); + adapted.put("NetworkSettings", networkSettings); - if (networkSettings != null) { @SuppressWarnings("unchecked") Map networks = (Map) networkSettings.get("Networks"); - if (networks != null) { + if (networks != null && networks.size() > 1) { + int highestPriority = Integer.MIN_VALUE; + String defaultGatewayNetwork = null; + for (Map.Entry entry : networks.entrySet()) { if (entry.getValue() instanceof Map) { @SuppressWarnings("unchecked") Map network = (Map) entry.getValue(); if (network.containsKey("GwPriority")) { - LOG.fine( - "Network '" + entry.getKey() + "' includes GwPriority (API v" + apiVersion + ")"); + Object priorityObj = network.get("GwPriority"); + int priority = priorityObj instanceof Number ? ((Number) priorityObj).intValue() : 0; + + if (priority > highestPriority) { + highestPriority = priority; + defaultGatewayNetwork = entry.getKey(); + } } } } + + if (defaultGatewayNetwork != null) { + LOG.fine( + String.format( + "Container using '%s' as default gateway (priority: %d)", + defaultGatewayNetwork, highestPriority)); + } } // Remove deprecated fields (should not be present in v1.48+) diff --git a/java/test/org/openqa/selenium/docker/client/V144AdapterTest.java b/java/test/org/openqa/selenium/docker/client/V144AdapterTest.java index 9b65f4885fca9..4d981086414f3 100644 --- a/java/test/org/openqa/selenium/docker/client/V144AdapterTest.java +++ b/java/test/org/openqa/selenium/docker/client/V144AdapterTest.java @@ -141,4 +141,33 @@ void shouldHandleContainerInspectResponseWithoutNetworkSettings() { assertThat(adapted.get("Id")).isEqualTo("abc123"); assertThat(adapted.containsKey("NetworkSettings")).isFalse(); } + + @Test + void shouldNotMutateOriginalResponseWhenRemovingDeprecatedFields() { + // Create original response with deprecated fields + Map originalNetworkSettings = new HashMap<>(); + originalNetworkSettings.put("IPAddress", "172.17.0.2"); + originalNetworkSettings.put("HairpinMode", false); + originalNetworkSettings.put("LinkLocalIPv6Address", "fe80::1"); + + Map originalResponse = new HashMap<>(); + originalResponse.put("Id", "container123"); + originalResponse.put("NetworkSettings", originalNetworkSettings); + + // Adapt the response + Map adapted = adapter.adaptContainerInspectResponse(originalResponse); + + // Verify original response is unchanged + assertThat(originalResponse.get("NetworkSettings")).isEqualTo(originalNetworkSettings); + assertThat(originalNetworkSettings).containsKey("HairpinMode"); + assertThat(originalNetworkSettings).containsKey("LinkLocalIPv6Address"); + + // Verify adapted response has deprecated fields removed + @SuppressWarnings("unchecked") + Map adaptedNetworkSettings = + (Map) adapted.get("NetworkSettings"); + assertThat(adaptedNetworkSettings).doesNotContainKey("HairpinMode"); + assertThat(adaptedNetworkSettings).doesNotContainKey("LinkLocalIPv6Address"); + assertThat(adaptedNetworkSettings).containsKey("IPAddress"); + } } diff --git a/java/test/org/openqa/selenium/docker/client/V148AdapterTest.java b/java/test/org/openqa/selenium/docker/client/V148AdapterTest.java index 393f808183722..ea74ec6084bf8 100644 --- a/java/test/org/openqa/selenium/docker/client/V148AdapterTest.java +++ b/java/test/org/openqa/selenium/docker/client/V148AdapterTest.java @@ -89,7 +89,8 @@ void shouldRemoveVirtualSizeFromImageResponse() { void shouldHandleImageManifestDescriptorInImageResponse() { V148Adapter adapter = new V148Adapter("1.48"); - Map descriptor = Map.of("digest", "sha256:xyz789", "platform", "linux/amd64"); + Map platform = Map.of("architecture", "amd64", "os", "linux"); + Map descriptor = Map.of("digest", "sha256:xyz789", "platform", platform); Map response = new HashMap<>(); response.put("Id", "sha256:abc123"); @@ -242,4 +243,35 @@ void shouldHandleNullContainerInspectResponse() { V148Adapter adapter = new V148Adapter("1.48"); assertThat(adapter.adaptContainerInspectResponse(null)).isNull(); } + + @Test + void shouldNotMutateOriginalResponseWhenRemovingDeprecatedFields() { + V148Adapter adapter = new V148Adapter("1.48"); + + // Create original response with deprecated fields + Map originalNetworkSettings = new HashMap<>(); + originalNetworkSettings.put("IPAddress", "172.17.0.2"); + originalNetworkSettings.put("HairpinMode", false); + originalNetworkSettings.put("Bridge", "docker0"); + + Map originalResponse = new HashMap<>(); + originalResponse.put("Id", "container123"); + originalResponse.put("NetworkSettings", originalNetworkSettings); + + // Adapt the response + Map adapted = adapter.adaptContainerInspectResponse(originalResponse); + + // Verify original response is unchanged + assertThat(originalResponse.get("NetworkSettings")).isEqualTo(originalNetworkSettings); + assertThat(originalNetworkSettings).containsKey("HairpinMode"); + assertThat(originalNetworkSettings).containsKey("Bridge"); + + // Verify adapted response has deprecated fields removed + @SuppressWarnings("unchecked") + Map adaptedNetworkSettings = + (Map) adapted.get("NetworkSettings"); + assertThat(adaptedNetworkSettings).doesNotContainKey("HairpinMode"); + assertThat(adaptedNetworkSettings).doesNotContainKey("Bridge"); + assertThat(adaptedNetworkSettings).containsKey("IPAddress"); + } }