Skip to content

Commit

Permalink
Merge pull request #14635 from jonathan-meier/native_image_build_remo…
Browse files Browse the repository at this point in the history
…te_docker

Containerized native image build on remote docker daemons (issue #1610)
  • Loading branch information
geoand authored Feb 22, 2021
2 parents 4055900 + a7e8a55 commit 0c3d785
Show file tree
Hide file tree
Showing 8 changed files with 424 additions and 211 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ public class NativeConfig {
@ConfigItem
public boolean containerBuild;

/**
* If this build is done using a remote docker daemon.
*/
@ConfigItem
public boolean remoteContainerBuild;

/**
* The docker image to use to do the image build
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package io.quarkus.deployment.pkg.steps;

import static io.quarkus.deployment.pkg.steps.LinuxIDUtil.getLinuxID;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;

import org.apache.commons.lang3.SystemUtils;
import org.jboss.logging.Logger;

import io.quarkus.deployment.pkg.NativeConfig;
import io.quarkus.deployment.util.FileUtil;
import io.quarkus.deployment.util.ProcessUtil;

public abstract class NativeImageBuildContainerRunner extends NativeImageBuildRunner {

private static final Logger log = Logger.getLogger(NativeImageBuildContainerRunner.class);

private final NativeConfig nativeConfig;
protected final NativeConfig.ContainerRuntime containerRuntime;
private final String[] baseContainerRuntimeArgs;
protected final String outputPath;

public NativeImageBuildContainerRunner(NativeConfig nativeConfig, Path outputDir) {
this.nativeConfig = nativeConfig;
containerRuntime = nativeConfig.containerRuntime.orElseGet(NativeImageBuildContainerRunner::detectContainerRuntime);
log.infof("Using %s to run the native image builder", containerRuntime.getExecutableName());

List<String> containerRuntimeArgs = new ArrayList<>();
Collections.addAll(containerRuntimeArgs, "--env", "LANG=C");

outputPath = outputDir == null ? null : outputDir.toAbsolutePath().toString();

if (SystemUtils.IS_OS_LINUX) {
String uid = getLinuxID("-ur");
String gid = getLinuxID("-gr");
if (uid != null && gid != null && !uid.isEmpty() && !gid.isEmpty()) {
Collections.addAll(containerRuntimeArgs, "--user", uid + ":" + gid);
if (containerRuntime == NativeConfig.ContainerRuntime.PODMAN) {
// Needed to avoid AccessDeniedExceptions
containerRuntimeArgs.add("--userns=keep-id");
}
}
}
this.baseContainerRuntimeArgs = containerRuntimeArgs.toArray(new String[0]);
}

@Override
public void setup(boolean processInheritIODisabled) {
if (containerRuntime == NativeConfig.ContainerRuntime.DOCKER
|| containerRuntime == NativeConfig.ContainerRuntime.PODMAN) {
// we pull the docker image in order to give users an indication of which step the process is at
// it's not strictly necessary we do this, however if we don't the subsequent version command
// will appear to block and no output will be shown
log.info("Checking image status " + nativeConfig.builderImage);
Process pullProcess = null;
try {
final ProcessBuilder pb = new ProcessBuilder(
Arrays.asList(containerRuntime.getExecutableName(), "pull", nativeConfig.builderImage));
pullProcess = ProcessUtil.launchProcess(pb, processInheritIODisabled);
pullProcess.waitFor();
} catch (IOException | InterruptedException e) {
throw new RuntimeException("Failed to pull builder image " + nativeConfig.builderImage, e);
} finally {
if (pullProcess != null) {
pullProcess.destroy();
}
}
}
}

@Override
protected String[] getGraalVMVersionCommand(List<String> args) {
return buildCommand("run", Collections.singletonList("--rm"), args);
}

@Override
protected String[] getBuildCommand(List<String> args) {
return buildCommand("run", getContainerRuntimeBuildArgs(), args);
}

protected List<String> getContainerRuntimeBuildArgs() {
List<String> containerRuntimeArgs = new ArrayList<>();
nativeConfig.containerRuntimeOptions.ifPresent(containerRuntimeArgs::addAll);
if (nativeConfig.debugBuildProcess && nativeConfig.publishDebugBuildProcessPort) {
// publish the debug port onto the host if asked for
containerRuntimeArgs.add("--publish=" + NativeImageBuildStep.DEBUG_BUILD_PROCESS_PORT + ":"
+ NativeImageBuildStep.DEBUG_BUILD_PROCESS_PORT);
}
return containerRuntimeArgs;
}

protected String[] buildCommand(String dockerCmd, List<String> containerRuntimeArgs, List<String> command) {
return Stream
.of(Stream.of(containerRuntime.getExecutableName()), Stream.of(dockerCmd), Stream.of(baseContainerRuntimeArgs),
containerRuntimeArgs.stream(), Stream.of(nativeConfig.builderImage), command.stream())
.flatMap(Function.identity()).toArray(String[]::new);
}

/**
* @return {@link NativeConfig.ContainerRuntime#DOCKER} if it's available, or {@link NativeConfig.ContainerRuntime#PODMAN}
* if the podman
* executable exists in the environment or if the docker executable is an alias to podman
* @throws IllegalStateException if no container runtime was found to build the image
*/
private static NativeConfig.ContainerRuntime detectContainerRuntime() {
// Docker version 19.03.14, build 5eb3275d40
String dockerVersionOutput = getVersionOutputFor(NativeConfig.ContainerRuntime.DOCKER);
boolean dockerAvailable = dockerVersionOutput.contains("Docker version");
// Check if Podman is installed
// podman version 2.1.1
String podmanVersionOutput = getVersionOutputFor(NativeConfig.ContainerRuntime.PODMAN);
boolean podmanAvailable = podmanVersionOutput.startsWith("podman version");
if (dockerAvailable) {
// Check if "docker" is an alias to "podman"
if (dockerVersionOutput.equals(podmanVersionOutput)) {
return NativeConfig.ContainerRuntime.PODMAN;
}
return NativeConfig.ContainerRuntime.DOCKER;
} else if (podmanAvailable) {
return NativeConfig.ContainerRuntime.PODMAN;
} else {
throw new IllegalStateException("No container runtime was found to run the native image builder");
}
}

private static String getVersionOutputFor(NativeConfig.ContainerRuntime containerRuntime) {
Process versionProcess = null;
try {
ProcessBuilder pb = new ProcessBuilder(containerRuntime.getExecutableName(), "--version")
.redirectErrorStream(true);
versionProcess = pb.start();
versionProcess.waitFor();
return new String(FileUtil.readFileContents(versionProcess.getInputStream()), StandardCharsets.UTF_8);
} 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());
return "";
} finally {
if (versionProcess != null) {
versionProcess.destroy();
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.quarkus.deployment.pkg.steps;

import java.nio.file.Path;
import java.util.Collections;
import java.util.List;

import org.apache.commons.lang3.SystemUtils;

import io.quarkus.deployment.pkg.NativeConfig;
import io.quarkus.deployment.util.FileUtil;

public class NativeImageBuildLocalContainerRunner extends NativeImageBuildContainerRunner {

public NativeImageBuildLocalContainerRunner(NativeConfig nativeConfig, Path outputDir) {
super(nativeConfig, outputDir);
}

@Override
protected List<String> getContainerRuntimeBuildArgs() {
List<String> containerRuntimeArgs = super.getContainerRuntimeBuildArgs();
String volumeOutputPath = outputPath;
if (SystemUtils.IS_OS_WINDOWS) {
volumeOutputPath = FileUtil.translateToVolumePath(volumeOutputPath);
}
Collections.addAll(containerRuntimeArgs, "--rm", "-v",
volumeOutputPath + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH + ":z");
return containerRuntimeArgs;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.quarkus.deployment.pkg.steps;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.stream.Stream;

import io.quarkus.deployment.util.ProcessUtil;

public class NativeImageBuildLocalRunner extends NativeImageBuildRunner {

private final String nativeImageExecutable;

public NativeImageBuildLocalRunner(String nativeImageExecutable) {
this.nativeImageExecutable = nativeImageExecutable;
}

@Override
public void cleanupServer(File outputDir, boolean processInheritIODisabled) throws InterruptedException, IOException {
final ProcessBuilder pb = new ProcessBuilder(nativeImageExecutable, "--server-shutdown");
pb.directory(outputDir);
final Process process = ProcessUtil.launchProcess(pb, processInheritIODisabled);
process.waitFor();
}

@Override
protected String[] getGraalVMVersionCommand(List<String> args) {
return buildCommand(args);
}

@Override
protected String[] getBuildCommand(List<String> args) {
return buildCommand(args);
}

private String[] buildCommand(List<String> args) {
return Stream.concat(Stream.of(nativeImageExecutable), args.stream()).toArray(String[]::new);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.quarkus.deployment.pkg.steps;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.util.List;

import io.quarkus.deployment.pkg.NativeConfig;

public class NativeImageBuildRemoteContainerRunner extends NativeImageBuildContainerRunner {

private final String nativeImageName;
private String containerId;

public NativeImageBuildRemoteContainerRunner(NativeConfig nativeConfig, Path outputDir, String nativeImageName) {
super(nativeConfig, outputDir);
this.nativeImageName = nativeImageName;
}

@Override
protected void preBuild(List<String> buildArgs) throws InterruptedException, IOException {
List<String> containerRuntimeArgs = getContainerRuntimeBuildArgs();
String[] createContainerCommand = buildCommand("create", containerRuntimeArgs, buildArgs);
Process createContainerProcess = new ProcessBuilder(createContainerCommand).start();
createContainerProcess.waitFor();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(createContainerProcess.getInputStream()))) {
containerId = reader.readLine();
}
String[] copyCommand = new String[] { containerRuntime.getExecutableName(), "cp", outputPath + "/.",
containerId + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH };
Process copyProcess = new ProcessBuilder(copyCommand).start();
copyProcess.waitFor();
super.preBuild(buildArgs);
}

@Override
protected String[] getBuildCommand(List<String> args) {
return new String[] { containerRuntime.getExecutableName(), "start", "--attach", containerId };
}

@Override
protected void postBuild() throws InterruptedException, IOException {
String[] copyCommand = new String[] { containerRuntime.getExecutableName(), "cp",
containerId + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH + "/" + nativeImageName, outputPath };
Process copyProcess = new ProcessBuilder(copyCommand).start();
copyProcess.waitFor();
String[] removeCommand = new String[] { containerRuntime.getExecutableName(), "container", "rm", "--volumes",
containerId };
Process removeProcess = new ProcessBuilder(removeCommand).start();
removeProcess.waitFor();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package io.quarkus.deployment.pkg.steps;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import io.quarkus.deployment.pkg.steps.NativeImageBuildStep.GraalVM;
import io.quarkus.deployment.util.ProcessUtil;

public abstract class NativeImageBuildRunner {

public GraalVM.Version getGraalVMVersion() {
final GraalVM.Version graalVMVersion;
try {
String[] versionCommand = getGraalVMVersionCommand(Collections.singletonList("--version"));
Process versionProcess = new ProcessBuilder(versionCommand)
.redirectErrorStream(true)
.start();
versionProcess.waitFor();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(versionProcess.getInputStream(), StandardCharsets.UTF_8))) {
graalVMVersion = GraalVM.Version.of(reader.lines());
}
} catch (Exception e) {
throw new RuntimeException("Failed to get GraalVM version", e);
}
return graalVMVersion;
}

public void setup(boolean processInheritIODisabled) {
}

public void cleanupServer(File outputDir, boolean processInheritIODisabled) throws InterruptedException, IOException {
}

public int build(List<String> args, Path outputDir, boolean processInheritIODisabled)
throws InterruptedException, IOException {
preBuild(args);
try {
CountDownLatch errorReportLatch = new CountDownLatch(1);
final ProcessBuilder processBuilder = new ProcessBuilder(getBuildCommand(args))
.directory(outputDir.toFile());
final Process process = ProcessUtil.launchProcessStreamStdOut(processBuilder, processInheritIODisabled);
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(new ErrorReplacingProcessReader(process.getErrorStream(), outputDir.resolve("reports").toFile(),
errorReportLatch));
executor.shutdown();
errorReportLatch.await();
return process.waitFor();
} finally {
postBuild();
}
}

protected abstract String[] getGraalVMVersionCommand(List<String> args);

protected abstract String[] getBuildCommand(List<String> args);

protected void preBuild(List<String> buildArgs) throws IOException, InterruptedException {
}

protected void postBuild() throws InterruptedException, IOException {
}

}
Loading

0 comments on commit 0c3d785

Please sign in to comment.