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

Containerized native image build on remote docker daemons (issue #1610) #14635

Merged
merged 7 commits into from
Feb 22, 2021
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