Skip to content

Commit

Permalink
[grid] Dynamic Grid working properly on Linux
Browse files Browse the repository at this point in the history
Docker Networking is a little different in
Linux and the approach of mapping a port and
relying on the Docker daemon url is not enough,
we need to use the container's IP and there is
no need to map a port to the host for that.

And after testing this approach in different OS,
we can make this behaviour the default for
Dynamic Grid, so we do not need to use the PortProber
when we run inside Docker.
  • Loading branch information
diemol committed Mar 21, 2021
1 parent f0ca78c commit 909239c
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 53 deletions.
70 changes: 32 additions & 38 deletions java/server/src/org/openqa/selenium/grid/docker/DockerOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,6 @@ public Map<Capabilities, Collection<SessionFactory>> getDockerSessionFactories(
throw new DockerException("Unable to reach the Docker daemon at " + getDockerUri());
}

DockerAssetsPath assetsPath = getAssetsPath(docker);

List<String> allConfigs = config.getAll(DOCKER_SECTION, "configs")
.orElseThrow(() -> new DockerException("Unable to find docker configs"));

Expand All @@ -141,10 +139,18 @@ public Map<Capabilities, Collection<SessionFactory>> getDockerSessionFactories(
kinds.put(imageName, stereotype);
}

// If Selenium Server is running inside a Docker container, we can inspect that container
// to get the information from it.
// Since Docker 1.12, the env var HOSTNAME has the container id (unless the user overwrites it)
String hostname = HostIdentifier.getHostName();
Optional<ContainerInfo> info = docker.inspect(new ContainerId(hostname));

DockerAssetsPath assetsPath = getAssetsPath(info);
String networkName = getDockerNetworkName(info);

loadImages(docker, kinds.keySet().toArray(new String[0]));
Image videoImage = getVideoImage(docker);
loadImages(docker, videoImage.getName());
String networkName = getDockerNetworkName(docker);

int maxContainerCount = Runtime.getRuntime().availableProcessors();
ImmutableMultimap.Builder<Capabilities, SessionFactory> factories = ImmutableMultimap.builder();
Expand All @@ -162,7 +168,8 @@ public Map<Capabilities, Collection<SessionFactory>> getDockerSessionFactories(
caps,
videoImage,
assetsPath,
networkName));
networkName,
info.isPresent()));
}
LOG.info(String.format(
"Mapping %s to docker image %s %d times",
Expand All @@ -178,53 +185,40 @@ private Image getVideoImage(Docker docker) {
return docker.getImage(videoImage);
}

private String getDockerNetworkName(Docker docker) {
// Selenium Server is running inside a Docker container, we will inspect that container
// to get the mounted volume and use that. If no volume was mounted, no assets will be saved.
// Since Docker 1.12, the env var HOSTNAME has the container id (unless the user overwrites it)
String hostname = HostIdentifier.getHostName();
Optional<ContainerInfo> info = docker.inspect(new ContainerId(hostname));
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private String getDockerNetworkName(Optional<ContainerInfo> info) {
if (info.isPresent()) {
return info.get().getNetworkName();
}
return DEFAULT_DOCKER_NETWORK;
}

private DockerAssetsPath getAssetsPath(Docker docker) {
Optional<String> assetsPath = config.get(DOCKER_SECTION, "assets-path");
if (assetsPath.isPresent()) {
// We assume the user is not running the Selenium Server inside a Docker container
// Therefore, we have access to the assets path on the host
return new DockerAssetsPath(assetsPath.get(), assetsPath.get());
}
// Selenium Server is running inside a Docker container, we will inspect that container
// to get the mounted volume and use that. If no volume was mounted, no assets will be saved.
// Since Docker 1.12, the env var HOSTNAME has the container id (unless the user overwrites it)
String hostname = HostIdentifier.getHostName();

Optional<ContainerInfo> info = docker.inspect(new ContainerId(hostname));
if (!info.isPresent()) {
return null;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private DockerAssetsPath getAssetsPath(Optional<ContainerInfo> info) {
if (info.isPresent()) {
Optional<Map<String, Object>> mountedVolume = info.get().getMountedVolumes()
.stream()
.filter(
mounted ->
DEFAULT_ASSETS_PATH.equalsIgnoreCase(String.valueOf(mounted.get("Destination"))))
.findFirst();

if (mountedVolume.isPresent()) {
String hostPath = String.valueOf(mountedVolume.get().get("Source"));
return new DockerAssetsPath(hostPath, DEFAULT_ASSETS_PATH);
}
}

Optional<Map<String, Object>> mountedVolume = info.get().getMountedVolumes()
.stream()
.filter(
mounted ->
DEFAULT_ASSETS_PATH.equalsIgnoreCase(String.valueOf(mounted.get("Destination"))))
.findFirst();

if (mountedVolume.isPresent()) {
String hostPath = String.valueOf(mountedVolume.get().get("Source"));
return new DockerAssetsPath(hostPath, DEFAULT_ASSETS_PATH);
}
Optional<String> assetsPath = config.get(DOCKER_SECTION, "assets-path");
// We assume the user is not running the Selenium Server inside a Docker container
// Therefore, we have access to the assets path on the host
return assetsPath.map(path -> new DockerAssetsPath(path, path)).orElse(null);

return null;
}

private void loadImages(Docker docker, String... imageNames) {
CompletableFuture<Void> cd = CompletableFuture.allOf(
Arrays.stream(imageNames)
Arrays.stream(imageNames)
.map(name -> CompletableFuture.supplyAsync(() -> docker.getImage(name)))
.toArray(CompletableFuture[]::new));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.docker.Container;
import org.openqa.selenium.docker.ContainerConfig;
import org.openqa.selenium.docker.ContainerInfo;
import org.openqa.selenium.docker.Docker;
import org.openqa.selenium.docker.Image;
Expand Down Expand Up @@ -94,6 +95,7 @@ public class DockerSessionFactory implements SessionFactory {
private final Image videoImage;
private final DockerAssetsPath assetsPath;
private final String networkName;
private final boolean runningInDocker;

public DockerSessionFactory(
Tracer tracer,
Expand All @@ -104,7 +106,8 @@ public DockerSessionFactory(
Capabilities stereotype,
Image videoImage,
DockerAssetsPath assetsPath,
String networkName) {
String networkName,
boolean runningInDocker) {
this.tracer = Require.nonNull("Tracer", tracer);
this.clientFactory = Require.nonNull("HTTP client", clientFactory);
this.docker = Require.nonNull("Docker command", docker);
Expand All @@ -115,6 +118,7 @@ public DockerSessionFactory(
Require.nonNull("Stereotype", stereotype));
this.videoImage = videoImage;
this.assetsPath = assetsPath;
this.runningInDocker = runningInDocker;
}

@Override
Expand All @@ -130,23 +134,27 @@ public boolean test(Capabilities capabilities) {
@Override
public Either<WebDriverException, ActiveSession> apply(CreateSessionRequest sessionRequest) {
LOG.info("Starting session for " + sessionRequest.getCapabilities());
int port = PortProber.findFreePort();
URL remoteAddress = getUrl(port);

HttpClient client = clientFactory.createClient(remoteAddress);
int port = runningInDocker ? 4444 : PortProber.findFreePort();
try (Span span = tracer.getCurrentContext().createSpan("docker_session_factory.apply")) {
Map<String, EventAttributeValue> attributeMap = new HashMap<>();
attributeMap.put(AttributeKey.LOGGER_CLASS.getKey(),
EventAttribute.setValue(this.getClass().getName()));
LOG.info("Creating container, mapping container port 4444 to " + port);
String logMessage = runningInDocker ? "Creating container..." :
"Creating container, mapping container port 4444 to " + port;
LOG.info(logMessage);
Container container = createBrowserContainer(port, sessionRequest.getCapabilities());
container.start();
ContainerInfo containerInfo = container.inspect();

String containerIp = containerInfo.getIp();
URL remoteAddress = getUrl(port, containerIp);
HttpClient client = clientFactory.createClient(remoteAddress);

attributeMap.put("docker.browser.image", EventAttribute.setValue(browserImage.toString()));
attributeMap.put("container.port", EventAttribute.setValue(port));
attributeMap.put("container.id", EventAttribute.setValue(container.getId().toString()));
attributeMap.put("container.ip", EventAttribute.setValue(containerInfo.getIp()));
attributeMap.put("container.ip", EventAttribute.setValue(containerIp));
attributeMap.put("docker.server.url", EventAttribute.setValue(remoteAddress.toString()));

LOG.info(
Expand Down Expand Up @@ -212,7 +220,7 @@ public Either<WebDriverException, ActiveSession> apply(CreateSessionRequest sess
String containerPath = path.get().getContainerPath(id);
saveSessionCapabilities(mergedCapabilities, containerPath);
String hostPath = path.get().getHostPath(id);
videoContainer = startVideoContainer(mergedCapabilities, containerInfo.getIp(), hostPath);
videoContainer = startVideoContainer(mergedCapabilities, containerIp, hostPath);
}

Dialect downstream = sessionRequest.getDownstreamDialects().contains(result.getDialect()) ?
Expand Down Expand Up @@ -249,11 +257,14 @@ public Either<WebDriverException, ActiveSession> apply(CreateSessionRequest sess
private Container createBrowserContainer(int port, Capabilities sessionCapabilities) {
Map<String, String> browserContainerEnvVars = getBrowserContainerEnvVars(sessionCapabilities);
Map<String, String> devShmMount = Collections.singletonMap("/dev/shm", "/dev/shm");
return docker.create(image(browserImage)
.env(browserContainerEnvVars)
.bind(devShmMount)
.map(Port.tcp(4444), Port.tcp(port))
.network(networkName));
ContainerConfig containerConfig = image(browserImage)
.env(browserContainerEnvVars)
.bind(devShmMount)
.network(networkName);
if (!runningInDocker) {
containerConfig = containerConfig.map(Port.tcp(4444), Port.tcp(port));
}
return docker.create(containerConfig);
}

private Map<String, String> getBrowserContainerEnvVars(Capabilities sessionRequestCapabilities) {
Expand Down Expand Up @@ -365,11 +376,15 @@ private void waitForServerToStart(HttpClient client, Duration duration) {
});
}

private URL getUrl(int port) {
private URL getUrl(int port, String containerIp) {
try {
String host = "localhost";
if (dockerUri.getScheme().startsWith("tcp") || dockerUri.getScheme().startsWith("http")) {
host = dockerUri.getHost();
if (runningInDocker) {
host = containerIp;
} else {
if (dockerUri.getScheme().startsWith("tcp") || dockerUri.getScheme().startsWith("http")) {
host = dockerUri.getHost();
}
}
return new URL(String.format("http://%s:%s/wd/hub", host, port));
} catch (MalformedURLException e) {
Expand Down

0 comments on commit 909239c

Please sign in to comment.