Skip to content

Commit

Permalink
Add support for AppCDS generation in Jib built container-image
Browse files Browse the repository at this point in the history
Fixes: #14607
  • Loading branch information
geoand committed Jan 28, 2021
1 parent 4312bfe commit ac073d7
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ public class PackageConfig {
* on its PATH.
* This flag is useful when the JVM to be used at runtime is not the same exact JVM version as the one used to build
* the jar.
* Note that this property is consulted only when {@code quarkus.package.create-appcds=true}.
* Note that this property is consulted only when {@code quarkus.package.create-appcds=true} and it requires having
* docker available during the build.
*/
@ConfigItem
public Optional<String> appcdsBuilderImage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
import io.quarkus.deployment.builditem.CapabilityBuildItem;
import io.quarkus.deployment.builditem.MainClassBuildItem;
import io.quarkus.deployment.pkg.PackageConfig;
import io.quarkus.deployment.pkg.builditem.AppCDSContainerImageBuildItem;
import io.quarkus.deployment.pkg.builditem.AppCDSResultBuildItem;
import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem;
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
import io.quarkus.deployment.pkg.builditem.JarBuildItem;
Expand All @@ -85,6 +87,20 @@ public CapabilityBuildItem capability() {
return new CapabilityBuildItem(Capability.CONTAINER_IMAGE_JIB);
}

// when AppCDS are enabled and a container image build via Jib has been requested,
// we want the AppCDS generation process to use the same JVM as the base image
// in order to make the AppCDS usable by the runtime JVM
@BuildStep(onlyIf = JibBuild.class)
public void appCDS(ContainerImageConfig containerImageConfig, JibConfig jibConfig,
BuildProducer<AppCDSContainerImageBuildItem> producer) {

if (!containerImageConfig.build && !containerImageConfig.push) {
return;
}

producer.produce(new AppCDSContainerImageBuildItem(jibConfig.baseJvmImage));
}

@BuildStep(onlyIf = { IsNormalNotRemoteDev.class, JibBuild.class }, onlyIfNot = NativeBuild.class)
public void buildFromJar(ContainerImageConfig containerImageConfig, JibConfig jibConfig,
PackageConfig packageConfig,
Expand All @@ -96,6 +112,7 @@ public void buildFromJar(ContainerImageConfig containerImageConfig, JibConfig ji
Optional<ContainerImageBuildRequestBuildItem> buildRequest,
Optional<ContainerImagePushRequestBuildItem> pushRequest,
List<ContainerImageLabelBuildItem> containerImageLabels,
Optional<AppCDSResultBuildItem> appCDSResult,
BuildProducer<ArtifactResultBuildItem> artifactResultProducer) {

if (!containerImageConfig.build && !containerImageConfig.push && !buildRequest.isPresent()
Expand All @@ -109,7 +126,8 @@ public void buildFromJar(ContainerImageConfig containerImageConfig, JibConfig ji
jibContainerBuilder = createContainerBuilderFromLegacyJar(jibConfig,
sourceJar, outputTarget, mainClass, containerImageLabels);
} else if (packageConfig.isFastJar()) {
jibContainerBuilder = createContainerBuilderFromFastJar(jibConfig, sourceJar, curateOutcome, containerImageLabels);
jibContainerBuilder = createContainerBuilderFromFastJar(jibConfig, sourceJar, curateOutcome, containerImageLabels,
appCDSResult);
} else {
throw new IllegalArgumentException(
"Package type '" + packageType + "' is not supported by the container-image-jib extension");
Expand Down Expand Up @@ -240,7 +258,8 @@ private Logger.Level toJBossLoggingLevel(LogEvent.Level level) {
*/
private JibContainerBuilder createContainerBuilderFromFastJar(JibConfig jibConfig,
JarBuildItem sourceJarBuildItem,
CurateOutcomeBuildItem curateOutcome, List<ContainerImageLabelBuildItem> containerImageLabels) {
CurateOutcomeBuildItem curateOutcome, List<ContainerImageLabelBuildItem> containerImageLabels,
Optional<AppCDSResultBuildItem> appCDSResult) {
Path componentsPath = sourceJarBuildItem.getPath().getParent();
Path appLibDir = componentsPath.resolve(JarResultBuildStep.LIB).resolve(JarResultBuildStep.MAIN);

Expand All @@ -250,9 +269,10 @@ private JibContainerBuilder createContainerBuilderFromFastJar(JibConfig jibConfi
if (jibConfig.jvmEntrypoint.isPresent()) {
entrypoint = jibConfig.jvmEntrypoint.get();
} else {
entrypoint = new ArrayList<>(3 + jibConfig.jvmArguments.size());
List<String> effectiveJvmArguments = determineEffectiveJvmArguments(jibConfig, appCDSResult);
entrypoint = new ArrayList<>(3 + effectiveJvmArguments.size());
entrypoint.add("java");
entrypoint.addAll(jibConfig.jvmArguments);
entrypoint.addAll(effectiveJvmArguments);
entrypoint.add("-jar");
entrypoint.add(JarResultBuildStep.QUARKUS_RUN_JAR);
}
Expand Down Expand Up @@ -325,18 +345,48 @@ private JibContainerBuilder createContainerBuilderFromFastJar(JibConfig jibConfi
// we need to manually create each layer
// the idea here is that the fast changing libraries are created in a later layer, thus when they do change,
// docker doesn't have to create an entire layer with all dependencies - only change the fast ones
jibContainerBuilder.addLayer(
Collections.singletonList(
componentsPath.resolve(JarResultBuildStep.LIB).resolve(JarResultBuildStep.BOOT_LIB)),
workDirInContainer.resolve(JarResultBuildStep.LIB));

FileEntriesLayer.Builder bootLibsLayerBuilder = FileEntriesLayer.builder();
Path bootLibPath = componentsPath.resolve(JarResultBuildStep.LIB).resolve(JarResultBuildStep.BOOT_LIB);
Files.list(bootLibPath).forEach(lib -> {
try {
AbsoluteUnixPath libPathInContainer = workDirInContainer.resolve(JarResultBuildStep.LIB)
.resolve(JarResultBuildStep.BOOT_LIB)
.resolve(lib.getFileName());
if (appCDSResult.isPresent()) {
// the boot lib jars need to preserve the modification time because otherwise AppCDS won't work
bootLibsLayerBuilder.addEntry(lib, libPathInContainer, Files.getLastModifiedTime(lib).toInstant());
} else {
bootLibsLayerBuilder.addEntry(lib, libPathInContainer);
}

} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
jibContainerBuilder.addFileEntriesLayer(bootLibsLayerBuilder.build());

jibContainerBuilder.addLayer(nonFastChangingLibPaths,
workDirInContainer.resolve(JarResultBuildStep.LIB).resolve(JarResultBuildStep.MAIN));
jibContainerBuilder.addLayer(new ArrayList<>(fastChangingLibPaths),
workDirInContainer.resolve(JarResultBuildStep.LIB).resolve(JarResultBuildStep.MAIN));
}

if (appCDSResult.isPresent()) {
jibContainerBuilder.addFileEntriesLayer(FileEntriesLayer.builder().addEntry(
componentsPath.resolve(JarResultBuildStep.QUARKUS_RUN_JAR),
workDirInContainer.resolve(JarResultBuildStep.QUARKUS_RUN_JAR),
Files.getLastModifiedTime(componentsPath.resolve(JarResultBuildStep.QUARKUS_RUN_JAR)).toInstant())
.build());
jibContainerBuilder
.addLayer(Collections.singletonList(appCDSResult.get().getAppCDS()), workDirInContainer);
} else {
jibContainerBuilder.addFileEntriesLayer(FileEntriesLayer.builder().addEntry(
componentsPath.resolve(JarResultBuildStep.QUARKUS_RUN_JAR),
workDirInContainer.resolve(JarResultBuildStep.QUARKUS_RUN_JAR)).build());
}

jibContainerBuilder
.addLayer(Collections.singletonList(componentsPath.resolve(JarResultBuildStep.QUARKUS_RUN_JAR)),
workDirInContainer)
.addLayer(Collections.singletonList(componentsPath.resolve(JarResultBuildStep.APP)), workDirInContainer)
.addLayer(Collections.singletonList(componentsPath.resolve(JarResultBuildStep.QUARKUS)), workDirInContainer)
.setWorkingDirectory(workDirInContainer)
Expand All @@ -356,6 +406,23 @@ private JibContainerBuilder createContainerBuilderFromFastJar(JibConfig jibConfi
}
}

private List<String> determineEffectiveJvmArguments(JibConfig jibConfig, Optional<AppCDSResultBuildItem> appCDSResult) {
List<String> effectiveJvmArguments = new ArrayList<>(jibConfig.jvmArguments);
if (appCDSResult.isPresent()) {
boolean containsAppCDSOptions = false;
for (String effectiveJvmArgument : effectiveJvmArguments) {
if (effectiveJvmArgument.startsWith("-XX:SharedArchiveFile")) {
containsAppCDSOptions = true;
break;
}
}
if (!containsAppCDSOptions) {
effectiveJvmArguments.add("-XX:SharedArchiveFile=" + appCDSResult.get().getAppCDS().getFileName().toString());
}
}
return effectiveJvmArguments;
}

private void setUser(JibConfig jibConfig, JibContainerBuilder jibContainerBuilder) {
jibConfig.user.ifPresent(jibContainerBuilder::setUser);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
invoker.goals=clean package -Dquarkus.container-image.build=true
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.acme</groupId>
<artifactId>container-build-jib-appcds</artifactId>
<version>0.1-SNAPSHOT</version>
<properties>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<surefire-plugin.version>3.0.0-M5</surefire-plugin.version>
<maven.compiler.source>1.8</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-bom</artifactId>
<version>@project.version@</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-container-image-jib</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>${native.surefire.skip}</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>native-image</goal>
</goals>
<configuration>
<enableHttpUrlHandler>true</enableHttpUrlHandler>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.acme;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/hello")
public class Hello {

@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Configuration file
# key = value
quarkus.package.type=fast-jar
quarkus.package.create-appcds=true
# -Xshare:on ensures that the AppCDS file will be used - if it can't, the JVM terminates
quarkus.jib.jvm-arguments=-Djava.util.logging.manager=org.jboss.logmanager.LogManager,-Xshare:on
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import io.quarkus.deployment.util.ExecUtil
import io.quarkus.runtime.util.JavaVersionUtil

import java.util.concurrent.ThreadLocalRandom

if (!JavaVersionUtil.java11OrHigher) {
// AppCDS don't work in Java 8
return
}

try {
ExecUtil.exec("docker", "version", "--format", "'{{.Server.Version}}'")
} catch (Exception ignored) {
return
}

String image = "${System.getProperty("user.name")}/container-build-jib-appcds:0.1-SNAPSHOT"
assert ExecUtil.exec("docker", "images", image)

String containerName = "container-build-jib-appcds-" + ThreadLocalRandom.current().nextInt(10000)
int maxTimesToCheck = 10
int i = 0
int hostPort = 12345
assert ExecUtil.exec("docker", "run", "-d", "-p", "$hostPort:8080", "--name", containerName, image)

while (true) {
try {
def response = "http://localhost:$hostPort/hello".toURL().text
assert response == "hello"
break
} catch (IOException e) {
try {
Thread.sleep(2000)
} catch (InterruptedException ignored) {
}
if ((i++) >= maxTimesToCheck) {
throw new RuntimeException("Unable to determine if container is running", e)
}
}
}
assert ExecUtil.exec("docker", "stop", containerName)
assert ExecUtil.exec("docker", "rm", containerName)
assert ExecUtil.exec("docker", "rmi", image)

0 comments on commit ac073d7

Please sign in to comment.