Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Container runtime detection cached in sys prop, container-docker extension #31857

Merged
merged 5 commits into from
Mar 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem;
import io.quarkus.deployment.steps.MainClassBuildStep;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.util.ContainerRuntimeUtil;
import io.quarkus.runtime.util.ContainerRuntimeUtil.ContainerRuntime;
import io.quarkus.utilities.JavaBinFinder;

public class AppCDSBuildStep {
Expand All @@ -39,7 +39,6 @@ public class AppCDSBuildStep {
public static final String CLASSES_LIST_FILE_NAME = "classes.lst";
private static final String CONTAINER_IMAGE_BASE_BUILD_DIR = "/tmp/quarkus";
private static final String CONTAINER_IMAGE_APPCDS_DIR = CONTAINER_IMAGE_BASE_BUILD_DIR + "/appcds";
public static final ContainerRuntimeUtil.ContainerRuntime CONTAINER_RUNTIME = detectContainerRuntime(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid doing the autodetection in static fields.


@BuildStep(onlyIf = AppCDSRequired.class)
public void requested(OutputTargetBuildItem outputTarget, BuildProducer<AppCDSRequestedBuildItem> producer)
Expand Down Expand Up @@ -204,12 +203,10 @@ private Path createClassesList(JarBuildItem jarResult,
// generate the classes file on the host
private List<String> dockerRunCommands(OutputTargetBuildItem outputTarget, String containerImage,
String containerWorkingDir) {
if (CONTAINER_RUNTIME == ContainerRuntimeUtil.ContainerRuntime.UNAVAILABLE) {
throw new IllegalStateException("No container runtime was found. "
+ "Make sure you have either Docker or Podman installed in your environment.");
}
ContainerRuntime containerRuntime = detectContainerRuntime(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exception will be thrown automatically if required is true.


List<String> command = new ArrayList<>(10);
command.add(CONTAINER_RUNTIME.getExecutableName());
command.add(containerRuntime.getExecutableName());
command.add("run");
command.add("-v");
command.add(outputTarget.getOutputDirectory().toAbsolutePath().toString() + ":" + CONTAINER_IMAGE_BASE_BUILD_DIR
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.quarkus.deployment.pkg.steps;

import static io.quarkus.deployment.pkg.steps.AppCDSBuildStep.CONTAINER_RUNTIME;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not involved something that has nothing to do with this test.

import static org.assertj.core.api.Assertions.assertThat;

import java.nio.file.Path;
Expand All @@ -19,10 +18,8 @@ class NativeImageBuildContainerRunnerTest {
@DisabledIfSystemProperty(named = "avoid-containers", matches = "true")
@Test
void testBuilderImageBeingPickedUp() {
if (CONTAINER_RUNTIME == ContainerRuntimeUtil.ContainerRuntime.UNAVAILABLE) {
throw new IllegalStateException("No container runtime was found. "
+ "Make sure you have either Docker or Podman installed in your environment.");
}
ContainerRuntimeUtil.ContainerRuntime containerRuntime = ContainerRuntimeUtil.detectContainerRuntime(true);

NativeConfig nativeConfig = new NativeConfig();
nativeConfig.containerRuntime = Optional.empty();
boolean found;
Expand All @@ -31,7 +28,7 @@ void testBuilderImageBeingPickedUp() {

nativeConfig.builderImage = "graalvm";
localRunner = new NativeImageBuildLocalContainerRunner(nativeConfig, Path.of("/tmp"));
command = localRunner.buildCommand(CONTAINER_RUNTIME.getExecutableName(), Collections.emptyList(),
command = localRunner.buildCommand(containerRuntime.getExecutableName(), Collections.emptyList(),
Collections.emptyList());
found = false;
for (String part : command) {
Expand All @@ -44,7 +41,7 @@ void testBuilderImageBeingPickedUp() {

nativeConfig.builderImage = "mandrel";
localRunner = new NativeImageBuildLocalContainerRunner(nativeConfig, Path.of("/tmp"));
command = localRunner.buildCommand(CONTAINER_RUNTIME.getExecutableName(), Collections.emptyList(),
command = localRunner.buildCommand(containerRuntime.getExecutableName(), Collections.emptyList(),
Collections.emptyList());
found = false;
for (String part : command) {
Expand All @@ -57,7 +54,7 @@ void testBuilderImageBeingPickedUp() {

nativeConfig.builderImage = "RandomString";
localRunner = new NativeImageBuildLocalContainerRunner(nativeConfig, Path.of("/tmp"));
command = localRunner.buildCommand(CONTAINER_RUNTIME.getExecutableName(), Collections.emptyList(),
command = localRunner.buildCommand(containerRuntime.getExecutableName(), Collections.emptyList(),
Collections.emptyList());
found = false;
for (String part : command) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Collectors;

Expand All @@ -19,18 +17,14 @@
public final class ContainerRuntimeUtil {

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

/**
* Static variable is not used because the class gets loaded by different classloaders at
* runtime and the container runtime would be detected again and again unnecessarily.
*/
static final Path CONTAINER_RUNTIME = Path.of(System.getProperty("java.io.tmpdir"), "quarkus_container_runtime.txt");
private static final String CONTAINER_RUNTIME_SYS_PROP = "quarkus-local-container-runtime";

private ContainerRuntimeUtil() {
}
Expand All @@ -47,86 +41,85 @@ public static ContainerRuntime detectContainerRuntime() {
}

public static ContainerRuntime detectContainerRuntime(boolean required) {
final ContainerRuntime containerRuntime = loadConfig();
final ContainerRuntime containerRuntime = loadContainerRuntimeFromSystemProperty();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a simple name change for clarity.

if (containerRuntime != null) {
return containerRuntime;
} else {
// Docker version 19.03.14, build 5eb3275d40
final String dockerVersionOutput = getVersionOutputFor(ContainerRuntime.DOCKER);
boolean dockerAvailable = dockerVersionOutput.contains("Docker version");
String dockerVersionOutput;
boolean dockerAvailable;
// Check if Podman is installed
// podman version 2.1.1
final String podmanVersionOutput = getVersionOutputFor(ContainerRuntime.PODMAN);
boolean podmanAvailable = podmanVersionOutput.startsWith("podman version");
if (DOCKER_EXECUTABLE != null) {
if (DOCKER_EXECUTABLE.trim().equalsIgnoreCase("docker") && dockerAvailable) {
storeConfig(ContainerRuntime.DOCKER);
return ContainerRuntime.DOCKER;
} else if (DOCKER_EXECUTABLE.trim().equalsIgnoreCase("podman") && podmanAvailable) {
storeConfig(ContainerRuntime.PODMAN);
return ContainerRuntime.PODMAN;
} else {
log.warn("quarkus.native.container-runtime config property must be set to either podman or docker " +
"and the executable must be available. Ignoring it.");
String podmanVersionOutput;
boolean podmanAvailable;
if (CONTAINER_EXECUTABLE != null) {
if (CONTAINER_EXECUTABLE.trim().equalsIgnoreCase("docker")) {
dockerVersionOutput = getVersionOutputFor(ContainerRuntime.DOCKER);
dockerAvailable = dockerVersionOutput.contains("Docker version");
if (dockerAvailable) {
storeContainerRuntimeInSystemProperty(ContainerRuntime.DOCKER);
return ContainerRuntime.DOCKER;
}
}
if (CONTAINER_EXECUTABLE.trim().equalsIgnoreCase("podman")) {
podmanVersionOutput = getVersionOutputFor(ContainerRuntime.PODMAN);
podmanAvailable = podmanVersionOutput.startsWith("podman version");
if (podmanAvailable) {
storeContainerRuntimeInSystemProperty(ContainerRuntime.PODMAN);
return ContainerRuntime.PODMAN;
}
}
log.warn("quarkus.native.container-runtime config property must be set to either podman or docker " +
"and the executable must be available. Ignoring it.");
}
dockerVersionOutput = getVersionOutputFor(ContainerRuntime.DOCKER);
dockerAvailable = dockerVersionOutput.contains("Docker version");
if (dockerAvailable) {
// Check if "docker" is an alias to "podman"
if (dockerVersionOutput.equals(podmanVersionOutput)) {
storeConfig(ContainerRuntime.PODMAN);
if (dockerVersionOutput.startsWith("podman version")) {
storeContainerRuntimeInSystemProperty(ContainerRuntime.PODMAN);
return ContainerRuntime.PODMAN;
}
storeConfig(ContainerRuntime.DOCKER);
storeContainerRuntimeInSystemProperty(ContainerRuntime.DOCKER);
return ContainerRuntime.DOCKER;
} else if (podmanAvailable) {
storeConfig(ContainerRuntime.PODMAN);
}
podmanVersionOutput = getVersionOutputFor(ContainerRuntime.PODMAN);
podmanAvailable = podmanVersionOutput.startsWith("podman version");
if (podmanAvailable) {
storeContainerRuntimeInSystemProperty(ContainerRuntime.PODMAN);
return ContainerRuntime.PODMAN;
} else {
if (required) {
throw new IllegalStateException("No container runtime was found. "
+ "Make sure you have either Docker or Podman installed in your environment.");
} else {
storeConfig(ContainerRuntime.UNAVAILABLE);
return ContainerRuntime.UNAVAILABLE;
}
}

storeContainerRuntimeInSystemProperty(ContainerRuntime.UNAVAILABLE);

if (required) {
throw new IllegalStateException("No container runtime was found. "
+ "Make sure you have either Docker or Podman installed in your environment.");
}

return ContainerRuntime.UNAVAILABLE;
}
}

private static ContainerRuntime loadConfig() {
try {
if (Files.isReadable(CONTAINER_RUNTIME)) {
final String runtime = Files.readString(CONTAINER_RUNTIME, StandardCharsets.UTF_8);
if (ContainerRuntime.DOCKER.name().equalsIgnoreCase(runtime)) {
return ContainerRuntime.DOCKER;
} else if (ContainerRuntime.PODMAN.name().equalsIgnoreCase(runtime)) {
return ContainerRuntime.PODMAN;
} else if (ContainerRuntime.UNAVAILABLE.name().equalsIgnoreCase(runtime)) {
return ContainerRuntime.UNAVAILABLE;
} else {
log.warnf("The file %s contains an unknown value %s. Ignoring it.",
CONTAINER_RUNTIME.toAbsolutePath(), runtime);
return null;
}
} else {
return null;
}
} catch (IOException e) {
log.warnf("Error reading file %s. Ignoring it. See: %s",
CONTAINER_RUNTIME.toAbsolutePath(), e);
private static ContainerRuntime loadContainerRuntimeFromSystemProperty() {
final String runtime = System.getProperty(CONTAINER_RUNTIME_SYS_PROP);

if (runtime == null) {
return null;
}
}

private static void storeConfig(ContainerRuntime containerRuntime) {
try {
Files.writeString(CONTAINER_RUNTIME, containerRuntime.name(), StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
CONTAINER_RUNTIME.toFile().deleteOnExit();
} catch (IOException e) {
log.warnf("Error writing to file %s. Ignoring it. See: %s",
CONTAINER_RUNTIME.toAbsolutePath(), e);
ContainerRuntime containerRuntime = ContainerRuntime.valueOf(runtime);

if (containerRuntime == null) {
log.warnf("System property %s contains an unknown value %s. Ignoring it.",
CONTAINER_RUNTIME_SYS_PROP, runtime);
}

return containerRuntime;
}

private static void storeContainerRuntimeInSystemProperty(ContainerRuntime containerRuntime) {
System.setProperty(CONTAINER_RUNTIME_SYS_PROP, containerRuntime.name());
}

private static String getVersionOutputFor(ContainerRuntime containerRuntime) {
Expand All @@ -135,8 +128,14 @@ private static String getVersionOutputFor(ContainerRuntime containerRuntime) {
final ProcessBuilder pb = new ProcessBuilder(containerRuntime.getExecutableName(), "--version")
.redirectErrorStream(true);
versionProcess = pb.start();
versionProcess.waitFor();
return new String(versionProcess.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
final int timeoutS = 10;
if (versionProcess.waitFor(timeoutS, TimeUnit.SECONDS)) {
return new String(versionProcess.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
} else {
log.debugf("Failure. It took command %s more than %d seconds to execute.", containerRuntime.getExecutableName(),
timeoutS);
return "";
}
} catch (IOException | InterruptedException e) {
// If an exception is thrown in the process, just return an empty String
log.debugf(e, "Failure to read version output from %s", containerRuntime.getExecutableName());
Expand Down Expand Up @@ -200,6 +199,10 @@ public enum ContainerRuntime {
private Boolean rootless;

public String getExecutableName() {
if (this == UNAVAILABLE) {
throw new IllegalStateException("Cannot get an executable name when no container runtime is available");
}

return this.name().toLowerCase();
}

Expand All @@ -216,5 +219,15 @@ public boolean isRootless() {
}
return rootless;
}

public static ContainerRuntime of(String value) {
for (ContainerRuntime containerRuntime : values()) {
if (containerRuntime.name().equalsIgnoreCase(value)) {
return containerRuntime;
}
}

return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ public class DockerConfig {

/**
* Name of binary used to execute the docker commands.
* This setting can override the global container runtime detection.
*/
@ConfigItem(defaultValue = "docker")
public String executableName;
@ConfigItem
public Optional<String> executableName;

/**
* Configuration for Docker Buildx options
Expand Down
Loading