Skip to content

Commit 5c58a03

Browse files
committed
Container runtime detection cache and rootless check
With this change, the detected container runtime is cached per JVM run so as it is not checked again regardless of the classloader loading the ContainerRuntimeUtil class. The rootless attribute of such runtime is lazily fetched once as needed.
1 parent 8a8613b commit 5c58a03

File tree

3 files changed

+127
-83
lines changed

3 files changed

+127
-83
lines changed

core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java

Lines changed: 7 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11

22
package io.quarkus.deployment;
33

4-
import java.io.File;
4+
import static io.quarkus.runtime.util.ContainerRuntimeUtil.ContainerRuntime.UNAVAILABLE;
5+
56
import java.io.IOException;
67
import java.lang.reflect.InvocationTargetException;
78
import java.lang.reflect.Method;
@@ -11,23 +12,20 @@
1112
import java.net.URISyntaxException;
1213
import java.nio.file.Files;
1314
import java.nio.file.Path;
14-
import java.time.Duration;
1515
import java.util.List;
1616
import java.util.Optional;
1717
import java.util.function.BooleanSupplier;
1818
import java.util.function.Supplier;
1919

20-
import org.eclipse.microprofile.config.ConfigProvider;
2120
import org.jboss.logging.Logger;
2221

2322
import io.quarkus.deployment.console.StartupLogCompressor;
24-
import io.quarkus.deployment.util.ExecUtil;
23+
import io.quarkus.runtime.util.ContainerRuntimeUtil;
2524

2625
public class IsDockerWorking implements BooleanSupplier {
2726

2827
private static final Logger LOGGER = Logger.getLogger(IsDockerWorking.class.getName());
2928
public static final int DOCKER_HOST_CHECK_TIMEOUT = 1000;
30-
public static final int DOCKER_CMD_CHECK_TIMEOUT = 3000;
3129

3230
private final List<Strategy> strategies;
3331

@@ -36,8 +34,7 @@ public IsDockerWorking() {
3634
}
3735

3836
public IsDockerWorking(boolean silent) {
39-
this.strategies = List.of(new TestContainersStrategy(silent), new DockerHostStrategy(),
40-
new DockerBinaryStrategy(silent));
37+
this.strategies = List.of(new TestContainersStrategy(silent), new DockerHostStrategy(), new DockerBinaryStrategy());
4138
}
4239

4340
@Override
@@ -170,41 +167,11 @@ public Result get() {
170167

171168
private static class DockerBinaryStrategy implements Strategy {
172169

173-
private final boolean silent;
174-
private final String binary;
175-
176-
private DockerBinaryStrategy(boolean silent) {
177-
this.silent = silent;
178-
this.binary = ConfigProvider.getConfig().getOptionalValue("quarkus.native.container-runtime", String.class)
179-
.orElse("docker");
180-
}
181-
182170
@Override
183171
public Result get() {
184-
try {
185-
if (!ExecUtil.execSilentWithTimeout(Duration.ofMillis(DOCKER_CMD_CHECK_TIMEOUT), binary, "-v")) {
186-
LOGGER.warnf("'%s -v' returned an error code. Make sure your Docker binary is correct", binary);
187-
return Result.UNKNOWN;
188-
}
189-
} catch (Exception e) {
190-
LOGGER.warnf("No %s binary found or general error: %s", binary, e);
191-
return Result.UNKNOWN;
192-
}
193-
194-
try {
195-
OutputFilter filter = new OutputFilter();
196-
if (ExecUtil.execWithTimeout(new File("."), filter, Duration.ofMillis(DOCKER_CMD_CHECK_TIMEOUT),
197-
binary, "version", "--format", "'{{.Server.Version}}'")) {
198-
LOGGER.debugf("Docker daemon found. Version: %s", filter.getOutput());
199-
return Result.AVAILABLE;
200-
} else {
201-
if (!silent) {
202-
LOGGER.warn("Could not determine version of Docker daemon");
203-
}
204-
return Result.UNAVAILABLE;
205-
}
206-
} catch (Exception e) {
207-
LOGGER.warn("Unexpected error occurred while determining Docker daemon version", e);
172+
if (ContainerRuntimeUtil.detectContainerRuntime(false) != UNAVAILABLE) {
173+
return Result.AVAILABLE;
174+
} else {
208175
return Result.UNKNOWN;
209176
}
210177
}

core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package io.quarkus.deployment.pkg.steps;
22

33
import static io.quarkus.deployment.pkg.steps.LinuxIDUtil.getLinuxID;
4+
import static io.quarkus.runtime.util.ContainerRuntimeUtil.ContainerRuntime.DOCKER;
5+
import static io.quarkus.runtime.util.ContainerRuntimeUtil.ContainerRuntime.PODMAN;
46

57
import java.nio.file.Path;
68
import java.util.ArrayList;
@@ -12,24 +14,21 @@
1214

1315
import io.quarkus.deployment.pkg.NativeConfig;
1416
import io.quarkus.deployment.util.FileUtil;
15-
import io.quarkus.runtime.util.ContainerRuntimeUtil;
1617

1718
public class NativeImageBuildLocalContainerRunner extends NativeImageBuildContainerRunner {
1819

1920
public NativeImageBuildLocalContainerRunner(NativeConfig nativeConfig, Path outputDir) {
2021
super(nativeConfig, outputDir);
2122
if (SystemUtils.IS_OS_LINUX) {
22-
ArrayList<String> containerRuntimeArgs = new ArrayList<>(Arrays.asList(baseContainerRuntimeArgs));
23-
if (containerRuntime == ContainerRuntimeUtil.ContainerRuntime.DOCKER
24-
&& containerRuntime.isRootless()) {
23+
final ArrayList<String> containerRuntimeArgs = new ArrayList<>(Arrays.asList(baseContainerRuntimeArgs));
24+
if (containerRuntime == DOCKER && containerRuntime.isRootless()) {
2525
Collections.addAll(containerRuntimeArgs, "--user", String.valueOf(0));
2626
} else {
2727
String uid = getLinuxID("-ur");
2828
String gid = getLinuxID("-gr");
2929
if (uid != null && gid != null && !uid.isEmpty() && !gid.isEmpty()) {
3030
Collections.addAll(containerRuntimeArgs, "--user", uid + ":" + gid);
31-
if (containerRuntime == ContainerRuntimeUtil.ContainerRuntime.PODMAN
32-
&& containerRuntime.isRootless()) {
31+
if (containerRuntime == PODMAN && containerRuntime.isRootless()) {
3332
// Needed to avoid AccessDeniedExceptions
3433
containerRuntimeArgs.add("--userns=keep-id");
3534
}
@@ -41,16 +40,19 @@ public NativeImageBuildLocalContainerRunner(NativeConfig nativeConfig, Path outp
4140

4241
@Override
4342
protected List<String> getContainerRuntimeBuildArgs() {
44-
List<String> containerRuntimeArgs = super.getContainerRuntimeBuildArgs();
45-
String volumeOutputPath = outputPath;
43+
final List<String> containerRuntimeArgs = super.getContainerRuntimeBuildArgs();
44+
final String volumeOutputPath;
4645
if (SystemUtils.IS_OS_WINDOWS) {
47-
volumeOutputPath = FileUtil.translateToVolumePath(volumeOutputPath);
46+
volumeOutputPath = FileUtil.translateToVolumePath(outputPath);
47+
} else {
48+
volumeOutputPath = outputPath;
4849
}
4950

50-
String selinuxBindOption = ":z";
51-
if (SystemUtils.IS_OS_MAC
52-
&& ContainerRuntimeUtil.detectContainerRuntime() == ContainerRuntimeUtil.ContainerRuntime.PODMAN) {
51+
final String selinuxBindOption;
52+
if (SystemUtils.IS_OS_MAC && containerRuntime == PODMAN) {
5353
selinuxBindOption = "";
54+
} else {
55+
selinuxBindOption = ":z";
5456
}
5557

5658
Collections.addAll(containerRuntimeArgs, "-v",

core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java

Lines changed: 106 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import java.io.InputStream;
66
import java.io.InputStreamReader;
77
import java.nio.charset.StandardCharsets;
8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
10+
import java.nio.file.StandardOpenOption;
811
import java.util.function.Predicate;
912
import java.util.stream.Collectors;
1013

@@ -18,53 +21,118 @@ public final class ContainerRuntimeUtil {
1821
private static final Logger log = Logger.getLogger(ContainerRuntimeUtil.class);
1922
private static final String DOCKER_EXECUTABLE = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class)
2023
.getOptionalValue("quarkus.native.container-runtime", String.class).orElse(null);
24+
/*
25+
* Caching the value in a file helps us only as much as one JVM execution is concerned.
26+
* Test suite's pom.xml sets things like <argLine>-Djava.io.tmpdir="${project.build.directory}"</argLine>,
27+
* so the file could appear in /tmp/ or C:\Users\karm\AppData\Local\Temp\ or in fact in
28+
* quarkus/integration-tests/something/target.
29+
* There is no point in reaching it in `Path.of(Paths.get("").toAbsolutePath().toString(), "target",
30+
* "quarkus_container_runtime.txt")`
31+
* as the file is deleted between JVM executions anyway.
32+
*/
33+
static final Path CONTAINER_RUNTIME = Path.of(System.getProperty("java.io.tmpdir"), "quarkus_container_runtime.txt");
2134

2235
private ContainerRuntimeUtil() {
2336
}
2437

2538
/**
2639
* @return {@link ContainerRuntime#DOCKER} if it's available, or {@link ContainerRuntime#PODMAN}
2740
* if the podman
28-
* executable exists in the environment or if the docker executable is an alias to podman
41+
* executable exists in the environment or if the docker executable is an alias to podman,
42+
* or {@link ContainerRuntime#UNAVAILABLE} if no container runtime is available and the required arg is false.
2943
* @throws IllegalStateException if no container runtime was found to build the image
3044
*/
3145
public static ContainerRuntime detectContainerRuntime() {
32-
// Docker version 19.03.14, build 5eb3275d40
33-
String dockerVersionOutput = getVersionOutputFor(ContainerRuntime.DOCKER);
34-
boolean dockerAvailable = dockerVersionOutput.contains("Docker version");
35-
// Check if Podman is installed
36-
// podman version 2.1.1
37-
String podmanVersionOutput = getVersionOutputFor(ContainerRuntime.PODMAN);
38-
boolean podmanAvailable = podmanVersionOutput.startsWith("podman version");
39-
if (DOCKER_EXECUTABLE != null) {
40-
if (DOCKER_EXECUTABLE.trim().equalsIgnoreCase("docker") && dockerAvailable) {
46+
return detectContainerRuntime(true);
47+
}
48+
49+
public static ContainerRuntime detectContainerRuntime(boolean required) {
50+
final ContainerRuntime containerRuntime = loadConfig();
51+
if (containerRuntime != null) {
52+
return containerRuntime;
53+
} else {
54+
// Docker version 19.03.14, build 5eb3275d40
55+
final String dockerVersionOutput = getVersionOutputFor(ContainerRuntime.DOCKER);
56+
boolean dockerAvailable = dockerVersionOutput.contains("Docker version");
57+
// Check if Podman is installed
58+
// podman version 2.1.1
59+
final String podmanVersionOutput = getVersionOutputFor(ContainerRuntime.PODMAN);
60+
boolean podmanAvailable = podmanVersionOutput.startsWith("podman version");
61+
if (DOCKER_EXECUTABLE != null) {
62+
if (DOCKER_EXECUTABLE.trim().equalsIgnoreCase("docker") && dockerAvailable) {
63+
storeConfig(ContainerRuntime.DOCKER);
64+
return ContainerRuntime.DOCKER;
65+
} else if (DOCKER_EXECUTABLE.trim().equalsIgnoreCase("podman") && podmanAvailable) {
66+
storeConfig(ContainerRuntime.PODMAN);
67+
return ContainerRuntime.PODMAN;
68+
} else {
69+
log.warn("quarkus.native.container-runtime config property must be set to either podman or docker " +
70+
"and the executable must be available. Ignoring it.");
71+
}
72+
}
73+
if (dockerAvailable) {
74+
// Check if "docker" is an alias to "podman"
75+
if (dockerVersionOutput.equals(podmanVersionOutput)) {
76+
storeConfig(ContainerRuntime.PODMAN);
77+
return ContainerRuntime.PODMAN;
78+
}
79+
storeConfig(ContainerRuntime.DOCKER);
4180
return ContainerRuntime.DOCKER;
42-
} else if (DOCKER_EXECUTABLE.trim().equalsIgnoreCase("podman") && podmanAvailable) {
81+
} else if (podmanAvailable) {
82+
storeConfig(ContainerRuntime.PODMAN);
4383
return ContainerRuntime.PODMAN;
4484
} else {
45-
log.warn("quarkus.native.container-runtime config property must be set to either podman or docker " +
46-
"and the executable must be available. Ignoring it.");
85+
if (required) {
86+
throw new IllegalStateException("No container runtime was found. "
87+
+ "Make sure you have either Docker or Podman installed in your environment.");
88+
} else {
89+
storeConfig(ContainerRuntime.UNAVAILABLE);
90+
return ContainerRuntime.UNAVAILABLE;
91+
}
4792
}
4893
}
94+
}
4995

50-
if (dockerAvailable) {
51-
// Check if "docker" is an alias to "podman"
52-
if (dockerVersionOutput.equals(podmanVersionOutput)) {
53-
return ContainerRuntime.PODMAN;
96+
private static ContainerRuntime loadConfig() {
97+
try {
98+
if (Files.isReadable(CONTAINER_RUNTIME)) {
99+
final String runtime = Files.readString(CONTAINER_RUNTIME, StandardCharsets.UTF_8);
100+
if (ContainerRuntime.DOCKER.name().equalsIgnoreCase(runtime)) {
101+
return ContainerRuntime.DOCKER;
102+
} else if (ContainerRuntime.PODMAN.name().equalsIgnoreCase(runtime)) {
103+
return ContainerRuntime.PODMAN;
104+
} else if (ContainerRuntime.UNAVAILABLE.name().equalsIgnoreCase(runtime)) {
105+
return ContainerRuntime.UNAVAILABLE;
106+
} else {
107+
log.warnf("The file %s contains an unknown value %s. Ignoring it.",
108+
CONTAINER_RUNTIME.toAbsolutePath(), runtime);
109+
return null;
110+
}
111+
} else {
112+
return null;
54113
}
55-
return ContainerRuntime.DOCKER;
56-
} else if (podmanAvailable) {
57-
return ContainerRuntime.PODMAN;
58-
} else {
59-
throw new IllegalStateException("No container runtime was found to. "
60-
+ "Make sure you have Docker or Podman installed in your environment.");
114+
} catch (IOException e) {
115+
log.warnf("Error reading file %s. Ignoring it. See: %s",
116+
CONTAINER_RUNTIME.toAbsolutePath(), e);
117+
return null;
118+
}
119+
}
120+
121+
private static void storeConfig(ContainerRuntime containerRuntime) {
122+
try {
123+
Files.writeString(CONTAINER_RUNTIME, containerRuntime.name(), StandardCharsets.UTF_8,
124+
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
125+
CONTAINER_RUNTIME.toFile().deleteOnExit();
126+
} catch (IOException e) {
127+
log.warnf("Error writing to file %s. Ignoring it. See: %s",
128+
CONTAINER_RUNTIME.toAbsolutePath(), e);
61129
}
62130
}
63131

64132
private static String getVersionOutputFor(ContainerRuntime containerRuntime) {
65133
Process versionProcess = null;
66134
try {
67-
ProcessBuilder pb = new ProcessBuilder(containerRuntime.getExecutableName(), "--version")
135+
final ProcessBuilder pb = new ProcessBuilder(containerRuntime.getExecutableName(), "--version")
68136
.redirectErrorStream(true);
69137
versionProcess = pb.start();
70138
versionProcess.waitFor();
@@ -100,7 +168,7 @@ private static boolean getRootlessStateFor(ContainerRuntime containerRuntime) {
100168
bufferedReader.lines().collect(Collectors.joining(System.lineSeparator())));
101169
return false;
102170
} else {
103-
Predicate<String> stringPredicate;
171+
final Predicate<String> stringPredicate;
104172
// Docker includes just "rootless" under SecurityOptions, while podman includes "rootless: <boolean>"
105173
if (containerRuntime == ContainerRuntime.DOCKER) {
106174
stringPredicate = line -> line.trim().equals("rootless");
@@ -126,19 +194,26 @@ private static boolean getRootlessStateFor(ContainerRuntime containerRuntime) {
126194
*/
127195
public enum ContainerRuntime {
128196
DOCKER,
129-
PODMAN;
197+
PODMAN,
198+
UNAVAILABLE;
130199

131-
private final boolean rootless;
132-
133-
ContainerRuntime() {
134-
this.rootless = getRootlessStateFor(this);
135-
}
200+
private Boolean rootless;
136201

137202
public String getExecutableName() {
138203
return this.name().toLowerCase();
139204
}
140205

141206
public boolean isRootless() {
207+
if (rootless != null) {
208+
return rootless;
209+
} else {
210+
if (this != ContainerRuntime.UNAVAILABLE) {
211+
rootless = getRootlessStateFor(this);
212+
} else {
213+
throw new IllegalStateException("No container runtime was found. "
214+
+ "Make sure you have either Docker or Podman installed in your environment.");
215+
}
216+
}
142217
return rootless;
143218
}
144219
}

0 commit comments

Comments
 (0)