Skip to content

Commit ecc58fd

Browse files
giffletdiemol
andauthored
Add docker device mapping configuration (#10645)
* Add docker device mapping configuration Sometimes is needed to map a device file to a docker container to make possible some tasks like virtualize a VM or something else. Because of this, I believe it is desirable to have a way to configure which device files should be available in containers. In this commit, we can define which device files should be mapped in containers through the standard selenium grid configuration file. * Device mapping configuration added do selenium grid cli options * Added tests to cover the device mapping processing and validation * Trying to fix the regex backtracking issue * Fix code smells reported by Sonar Co-authored-by: Diego Molina <[email protected]>
1 parent 582073d commit ecc58fd

11 files changed

Lines changed: 342 additions & 14 deletions

File tree

java/src/org/openqa/selenium/docker/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ java_library(
77
visibility = [
88
"//java/src/org/openqa/selenium/grid/node/docker:__pkg__",
99
"//java/test/org/openqa/selenium/docker:__subpackages__",
10+
"//java/test/org/openqa/selenium/grid/node/docker:__pkg__",
1011
],
1112
deps = [
1213
"//java/src/org/openqa/selenium:core",

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

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.openqa.selenium.docker;
1919

2020
import com.google.common.collect.HashMultimap;
21+
import com.google.common.collect.ImmutableList;
2122
import com.google.common.collect.ImmutableMap;
2223
import com.google.common.collect.Multimap;
2324

@@ -39,27 +40,28 @@ public class ContainerConfig {
3940
private final Multimap<String, Map<String, Object>> portBindings;
4041
private final Map<String, String> envVars;
4142
private final Map<String, String> volumeBinds;
43+
private final List<Device> devices;
4244
private final String networkName;
4345
private final boolean autoRemove;
4446
private final long shmSize;
4547

46-
4748
public ContainerConfig(Image image,
4849
Multimap<String, Map<String, Object>> portBindings,
4950
Map<String, String> envVars, Map<String, String> volumeBinds,
50-
String networkName, long shmSize) {
51+
List<Device> devices, String networkName, long shmSize) {
5152
this.image = image;
5253
this.portBindings = portBindings;
5354
this.envVars = envVars;
5455
this.volumeBinds = volumeBinds;
56+
this.devices = devices;
5557
this.networkName = networkName;
5658
this.autoRemove = true;
5759
this.shmSize = shmSize;
5860
}
5961

6062
public static ContainerConfig image(Image image) {
6163
return new ContainerConfig(image, HashMultimap.create(), ImmutableMap.of(), ImmutableMap.of(),
62-
DEFAULT_DOCKER_NETWORK, DEFAULT_SHM_SIZE);
64+
ImmutableList.of(), DEFAULT_DOCKER_NETWORK, DEFAULT_SHM_SIZE);
6365
}
6466

6567
public ContainerConfig map(Port containerPort, Port hostPort) {
@@ -76,36 +78,43 @@ public ContainerConfig map(Port containerPort, Port hostPort) {
7678
containerPort.getPort() + "/" + containerPort.getProtocol(),
7779
ImmutableMap.of("HostPort", String.valueOf(hostPort.getPort()), "HostIp", ""));
7880

79-
return new ContainerConfig(image, updatedBindings, envVars, volumeBinds, networkName,
80-
shmSize);
81+
return new ContainerConfig(image, updatedBindings, envVars, volumeBinds, devices,
82+
networkName, shmSize);
8183
}
8284

8385
public ContainerConfig env(Map<String, String> envVars) {
8486
Require.nonNull("Container env vars", envVars);
8587

86-
return new ContainerConfig(image, portBindings, envVars, volumeBinds, networkName,
87-
shmSize);
88+
return new ContainerConfig(image, portBindings, envVars, volumeBinds, devices,
89+
networkName, shmSize);
8890
}
8991

9092
public ContainerConfig bind(Map<String, String> volumeBinds) {
9193
Require.nonNull("Container volume binds", volumeBinds);
9294

93-
return new ContainerConfig(image, portBindings, envVars, volumeBinds, networkName,
94-
shmSize);
95+
return new ContainerConfig(image, portBindings, envVars, volumeBinds, devices,
96+
networkName, shmSize);
9597
}
9698

9799
public ContainerConfig network(String networkName) {
98100
Require.nonNull("Container network name", networkName);
99101

100-
return new ContainerConfig(image, portBindings, envVars, volumeBinds, networkName,
102+
return new ContainerConfig(image, portBindings, envVars, volumeBinds, devices, networkName,
101103
shmSize);
102104
}
103105

104106
public ContainerConfig shmMemorySize(long shmSize) {
105-
return new ContainerConfig(image, portBindings, envVars, volumeBinds, networkName,
107+
return new ContainerConfig(image, portBindings, envVars, volumeBinds, devices, networkName,
106108
shmSize);
107109
}
108110

111+
public ContainerConfig devices(List<Device> devices) {
112+
Require.nonNull("Container device files", devices);
113+
114+
return new ContainerConfig(image, portBindings, envVars, volumeBinds, devices, networkName,
115+
shmSize);
116+
}
117+
109118
@Override
110119
public String toString() {
111120
return "ContainerConfig{" +
@@ -114,6 +123,7 @@ public String toString() {
114123
", envVars=" + envVars +
115124
", volumeBinds=" + volumeBinds +
116125
", networkName=" + networkName +
126+
", devices=" + devices +
117127
", autoRemove=" + autoRemove +
118128
", shmSize=" + shmSize +
119129
'}';
@@ -128,12 +138,21 @@ private Map<String, Object> toJson() {
128138
.map(key -> String.format("%s:%s", key, this.volumeBinds.get(key)))
129139
.collect(Collectors.toList());
130140

141+
List<Map<String, String>> devicesMapping = this.devices.stream()
142+
.map(device -> ImmutableMap.of(
143+
"PathOnHost", device.getPathOnHost(),
144+
"PathInContainer", device.getPathInContainer(),
145+
"CgroupPermissions", device.getCgroupPermissions()
146+
))
147+
.collect(Collectors.toList());
148+
131149
Map<String, Object> hostConfig = ImmutableMap.of(
132150
"PortBindings", portBindings.asMap(),
133151
"AutoRemove", autoRemove,
134152
"NetworkMode", networkName,
135153
"ShmSize", shmSize,
136-
"Binds", volumeBinds);
154+
"Binds", volumeBinds,
155+
"Devices", devicesMapping);
137156

138157
return ImmutableMap.of(
139158
"Image", image.getId(),
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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;
19+
20+
import java.util.Objects;
21+
22+
public class Device {
23+
24+
private final String pathOnHost;
25+
26+
private final String pathInContainer;
27+
28+
private final String cgroupPermissions;
29+
30+
private Device(String pathOnHost, String pathInContainer, String cgroupPermissions) {
31+
this.pathOnHost = pathOnHost;
32+
this.pathInContainer = pathInContainer;
33+
this.cgroupPermissions = cgroupPermissions;
34+
}
35+
36+
public static Device device(String pathOnHost, String pathInContainer, String cgroupPermissions) {
37+
if (Objects.isNull(cgroupPermissions) || cgroupPermissions.trim().length() == 0) {
38+
cgroupPermissions = "crw";
39+
}
40+
return new Device(pathOnHost, pathInContainer, cgroupPermissions);
41+
}
42+
43+
public String getPathOnHost() {
44+
return pathOnHost;
45+
}
46+
47+
public String getPathInContainer() {
48+
return pathInContainer;
49+
}
50+
51+
public String getCgroupPermissions() {
52+
return cgroupPermissions;
53+
}
54+
55+
@Override
56+
public boolean equals(Object o) {
57+
if (this == o) {
58+
return true;
59+
}
60+
if (o == null || getClass() != o.getClass()) {
61+
return false;
62+
}
63+
Device device = (Device) o;
64+
return Objects.equals(pathOnHost, device.pathOnHost) && Objects.equals(
65+
pathInContainer, device.pathInContainer) && Objects.equals(cgroupPermissions,
66+
device.cgroupPermissions);
67+
}
68+
69+
@Override
70+
public int hashCode() {
71+
return Objects.hash(pathOnHost, pathInContainer, cgroupPermissions);
72+
}
73+
74+
@Override
75+
public String toString() {
76+
return "Device{" +
77+
"pathOnHost='" + pathOnHost + '\'' +
78+
", pathInContainer='" + pathInContainer + '\'' +
79+
", cgroupPermissions='" + cgroupPermissions + '\'' +
80+
'}';
81+
}
82+
}

java/src/org/openqa/selenium/grid/node/docker/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ java_library(
88
"//java/src/org/openqa/selenium/grid/commands:__pkg__",
99
"//java/src/org/openqa/selenium/grid/node/httpd:__pkg__",
1010
"//java/src/org/openqa/selenium/grid/node/local:__pkg__",
11+
"//java/test/org/openqa/selenium/grid/node/docker:__pkg__",
1112
],
1213
deps = [
1314
"//java:auto-service",

java/src/org/openqa/selenium/grid/node/docker/DockerFlags.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,20 @@ public class DockerFlags implements HasRoles {
7070
example = "[\"selenium/standalone-firefox:latest\", \"{\\\"browserName\\\": \\\"firefox\\\"}\"]")
7171
private List<String> images2Capabilities;
7272

73+
@Parameter(
74+
names = {"--docker-devices"},
75+
description = "Exposes devices to a container. Each device mapping declaration must have " +
76+
" at least the path of the device in both host and container separated by a colon like " +
77+
"in this example: /device/path/in/host:/device/path/in/container",
78+
arity = 1,
79+
variableArity = true,
80+
splitter = NonSplittingSplitter.class)
81+
@ConfigValue(
82+
section = DockerOptions.DOCKER_SECTION,
83+
name = "devices",
84+
example = "[\"/dev/kvm:/dev/kvm\"]")
85+
private List<String> devices;
86+
7387
@Parameter(
7488
names = {"--docker-video-image"},
7589
description = "Docker image to be used when video recording is enabled"

java/src/org/openqa/selenium/grid/node/docker/DockerOptions.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,15 @@
2121
import com.google.common.collect.ImmutableMultimap;
2222
import com.google.common.collect.Multimap;
2323

24+
import java.util.ArrayList;
25+
import java.util.Collections;
26+
import java.util.regex.Matcher;
27+
import java.util.regex.Pattern;
2428
import org.openqa.selenium.Capabilities;
2529
import org.openqa.selenium.Platform;
2630
import org.openqa.selenium.docker.ContainerId;
2731
import org.openqa.selenium.docker.ContainerInfo;
32+
import org.openqa.selenium.docker.Device;
2833
import org.openqa.selenium.docker.Docker;
2934
import org.openqa.selenium.docker.DockerException;
3035
import org.openqa.selenium.docker.Image;
@@ -51,6 +56,7 @@
5156
import java.util.logging.Logger;
5257

5358
import static org.openqa.selenium.Platform.WINDOWS;
59+
import static org.openqa.selenium.docker.Device.device;
5460

5561
public class DockerOptions {
5662

@@ -143,6 +149,8 @@ public Map<Capabilities, Collection<SessionFactory>> getDockerSessionFactories(
143149
kinds.put(imageName, stereotype);
144150
}
145151

152+
List<Device> devicesMapping = getDevicesMapping();
153+
146154
// If Selenium Server is running inside a Docker container, we can inspect that container
147155
// to get the information from it.
148156
// Since Docker 1.12, the env var HOSTNAME has the container id (unless the user overwrites it)
@@ -173,6 +181,7 @@ public Map<Capabilities, Collection<SessionFactory>> getDockerSessionFactories(
173181
getDockerUri(),
174182
image,
175183
caps,
184+
devicesMapping,
176185
videoImage,
177186
assetsPath,
178187
networkName,
@@ -187,6 +196,28 @@ public Map<Capabilities, Collection<SessionFactory>> getDockerSessionFactories(
187196
return factories.build().asMap();
188197
}
189198

199+
protected List<Device> getDevicesMapping() {
200+
Pattern linuxDeviceMappingWithDefaultPermissionsPattern = Pattern.compile("^([\\w\\/-]+):([\\w\\/-]+)$");
201+
Pattern linuxDeviceMappingWithPermissionsPattern = Pattern.compile("^([\\w\\/-]+):([\\w\\/-]+):([\\w]+)$");
202+
203+
List<String> devices = config.getAll(DOCKER_SECTION, "devices")
204+
.orElseGet(Collections::emptyList);
205+
206+
List<Device> deviceMapping = new ArrayList<>();
207+
for (int i = 0; i < devices.size(); i++) {
208+
String deviceMappingDefined = devices.get(i).trim();
209+
Matcher matcher = linuxDeviceMappingWithDefaultPermissionsPattern.matcher(deviceMappingDefined);
210+
211+
if (matcher.matches()) {
212+
deviceMapping.add(device(matcher.group(1), matcher.group(2), null));
213+
} else if ((matcher = linuxDeviceMappingWithPermissionsPattern.matcher(
214+
deviceMappingDefined)).matches()) {
215+
deviceMapping.add(device(matcher.group(1), matcher.group(2), matcher.group(3)));
216+
}
217+
}
218+
return deviceMapping;
219+
}
220+
190221
private Image getVideoImage(Docker docker) {
191222
String videoImage = config.get(DOCKER_SECTION, "video-image").orElse(DEFAULT_VIDEO_IMAGE);
192223
return docker.getImage(videoImage);

java/src/org/openqa/selenium/grid/node/docker/DockerSessionFactory.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package org.openqa.selenium.grid.node.docker;
1919

20+
import java.util.List;
2021
import org.openqa.selenium.Capabilities;
2122
import org.openqa.selenium.Dimension;
2223
import org.openqa.selenium.ImmutableCapabilities;
@@ -28,6 +29,7 @@
2829
import org.openqa.selenium.docker.Container;
2930
import org.openqa.selenium.docker.ContainerConfig;
3031
import org.openqa.selenium.docker.ContainerInfo;
32+
import org.openqa.selenium.docker.Device;
3133
import org.openqa.selenium.docker.Docker;
3234
import org.openqa.selenium.docker.Image;
3335
import org.openqa.selenium.docker.Port;
@@ -96,6 +98,7 @@ public class DockerSessionFactory implements SessionFactory {
9698
private final URI dockerUri;
9799
private final Image browserImage;
98100
private final Capabilities stereotype;
101+
private final List<Device> devices;
99102
private final Image videoImage;
100103
private final DockerAssetsPath assetsPath;
101104
private final String networkName;
@@ -110,6 +113,7 @@ public DockerSessionFactory(
110113
URI dockerUri,
111114
Image browserImage,
112115
Capabilities stereotype,
116+
List<Device> devices,
113117
Image videoImage,
114118
DockerAssetsPath assetsPath,
115119
String networkName,
@@ -123,6 +127,7 @@ public DockerSessionFactory(
123127
this.networkName = Require.nonNull("Docker network name", networkName);
124128
this.stereotype = ImmutableCapabilities.copyOf(
125129
Require.nonNull("Stereotype", stereotype));
130+
this.devices = Require.nonNull("Container devices", devices);
126131
this.videoImage = videoImage;
127132
this.assetsPath = assetsPath;
128133
this.runningInDocker = runningInDocker;
@@ -287,7 +292,8 @@ private Container createBrowserContainer(int port, Capabilities sessionCapabilit
287292
ContainerConfig containerConfig = image(browserImage)
288293
.env(browserContainerEnvVars)
289294
.shmMemorySize(browserContainerShmMemorySize)
290-
.network(networkName);
295+
.network(networkName)
296+
.devices(devices);
291297
if (!runningInDocker) {
292298
containerConfig = containerConfig.map(Port.tcp(4444), Port.tcp(port));
293299
}

java/test/org/openqa/selenium/docker/BootstrapTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public void shouldReportDockerIsUnsupportedIfServerReturns404() {
6161
}
6262

6363
@Test
64-
public void shouldReportDockerIsUnsupportIfRequestCausesAnIoException() {
64+
public void shouldReportDockerIsUnsupportedIfRequestCausesAnIoException() {
6565
HttpHandler client = req -> { throw new UncheckedIOException(new IOException("Eeek!")); };
6666

6767
boolean isSupported = new Docker(client).isSupported();

0 commit comments

Comments
 (0)