diff --git a/extensions/container-image/container-image-docker/deployment/pom.xml b/extensions/container-image/container-image-docker/deployment/pom.xml index 32e5e5f7394c5..0985cbc99c3b0 100644 --- a/extensions/container-image/container-image-docker/deployment/pom.xml +++ b/extensions/container-image/container-image-docker/deployment/pom.xml @@ -22,6 +22,11 @@ io.quarkus quarkus-container-image-deployment + + org.assertj + assertj-core + test + @@ -41,4 +46,4 @@ - \ No newline at end of file + diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerFileBaseInformationProvider.java b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerFileBaseInformationProvider.java new file mode 100644 index 0000000000000..262cffd8c8a95 --- /dev/null +++ b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerFileBaseInformationProvider.java @@ -0,0 +1,47 @@ +package io.quarkus.container.image.docker.deployment; + +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; + +interface DockerFileBaseInformationProvider { + + Optional determine(Path dockerFile); + + static DockerFileBaseInformationProvider impl() { + return new DockerFileBaseInformationProvider() { + + private final List delegates = List.of(new UbiMinimalBaseProvider(), + new RedHatOpenJDKRuntimeBaseProvider()); + + @Override + public Optional determine(Path dockerFile) { + for (DockerFileBaseInformationProvider delegate : delegates) { + Optional result = delegate.determine(dockerFile); + if (result.isPresent()) { + return result; + } + } + return Optional.empty(); + } + }; + } + + class DockerFileBaseInformation { + private final int javaVersion; + private final String baseImage; + + public DockerFileBaseInformation(String baseImage, int javaVersion) { + this.javaVersion = javaVersion; + this.baseImage = baseImage; + } + + public int getJavaVersion() { + return javaVersion; + } + + public String getBaseImage() { + return baseImage; + } + } +} diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java index 0b1e06203ea31..0f8d083c3b24c 100644 --- a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java +++ b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java @@ -34,6 +34,7 @@ import io.quarkus.deployment.pkg.PackageConfig; import io.quarkus.deployment.pkg.builditem.AppCDSResultBuildItem; import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; +import io.quarkus.deployment.pkg.builditem.CompiledJavaVersionBuildItem; import io.quarkus.deployment.pkg.builditem.JarBuildItem; import io.quarkus.deployment.pkg.builditem.NativeImageBuildItem; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; @@ -63,6 +64,7 @@ public void dockerBuildFromJar(DockerConfig dockerConfig, ContainerImageConfig containerImageConfig, OutputTargetBuildItem out, ContainerImageInfoBuildItem containerImageInfo, + CompiledJavaVersionBuildItem compiledJavaVersion, Optional buildRequest, Optional pushRequest, @SuppressWarnings("unused") Optional appCDSResult, // ensure docker build will be performed after AppCDS creation @@ -84,6 +86,19 @@ public void dockerBuildFromJar(DockerConfig dockerConfig, throw new RuntimeException("Unable to build docker image. Please check your docker installation"); } + var dockerfilePaths = getDockerfilePaths(dockerConfig, false, packageConfig, out); + var dockerFileBaseInformationProvider = DockerFileBaseInformationProvider.impl(); + var dockerFileBaseInformation = dockerFileBaseInformationProvider.determine(dockerfilePaths.getDockerfilePath()); + + if ((compiledJavaVersion.getJavaVersion().isJava17OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) + && dockerFileBaseInformation.isPresent() && (dockerFileBaseInformation.get().getJavaVersion() < 17)) { + throw new IllegalStateException( + String.format( + "The project is built with Java 17 or higher, but the selected Dockerfile (%s) is using a lower Java version in the base image (%s). Please ensure you are using the proper base image in the Dockerfile.", + dockerfilePaths.getDockerfilePath().toAbsolutePath(), + dockerFileBaseInformation.get().getBaseImage())); + } + log.info("Building docker image for jar."); ImageIdReader reader = new ImageIdReader(); diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProvider.java b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProvider.java new file mode 100644 index 0000000000000..1a955893486e1 --- /dev/null +++ b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProvider.java @@ -0,0 +1,42 @@ +package io.quarkus.container.image.docker.deployment; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** + * Can extract information from Dockerfile that uses {@code registry.access.redhat.com/ubi8/openjdk-$d-runtime:$d.$d} as the + * base image + */ +class RedHatOpenJDKRuntimeBaseProvider + implements DockerFileBaseInformationProvider { + + @Override + public Optional determine(Path dockerFile) { + try (Stream lines = Files.lines(dockerFile)) { + Optional fromOpt = lines.filter(l -> l.startsWith("FROM")).findFirst(); + if (fromOpt.isPresent()) { + String fromLine = fromOpt.get(); + String baseImage = fromLine.substring(4).trim(); + Pattern pattern = Pattern.compile(".*ubi8/openjdk-(\\w+)-runtime.*"); + Matcher matcher = pattern.matcher(baseImage); + if (matcher.find()) { + String match = matcher.group(1); + try { + return Optional.of(new DockerFileBaseInformationProvider.DockerFileBaseInformation(baseImage, + Integer.parseInt(match))); + } catch (NumberFormatException ignored) { + + } + } + } + } catch (IOException ignored) { + + } + return Optional.empty(); + } +} diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProvider.java b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProvider.java new file mode 100644 index 0000000000000..1ad6adc24f6a7 --- /dev/null +++ b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProvider.java @@ -0,0 +1,59 @@ +package io.quarkus.container.image.docker.deployment; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** + * Can extract information from Dockerfile that uses {@code registry.access.redhat.com/ubi8/ubi-minimal:$d.$d} as the + * base image + */ +class UbiMinimalBaseProvider + implements DockerFileBaseInformationProvider { + + public static final String UBI_MINIMAL_PREFIX = "registry.access.redhat.com/ubi8/ubi-minimal"; + + @Override + public Optional determine(Path dockerFile) { + AtomicInteger state = new AtomicInteger(0); //0: 'FROM' not yet encountered, 1: matching 'FROM' found, 2: ARG JAVA_PACKAGE found, 3: non matching 'FROM' found, 4: exception occurred + AtomicReference baseImage = new AtomicReference<>(null); + AtomicInteger javaVersion = new AtomicInteger(0); + try (Stream lines = Files.lines(dockerFile)) { + lines.takeWhile(s -> state.get() < 2).forEach(s -> { + if (s.startsWith("FROM")) { + String image = s.substring(4).trim(); + if (image.startsWith(UBI_MINIMAL_PREFIX)) { + baseImage.set(image); + state.set(1); + } else { + state.set(3); + } + } else if (s.startsWith("ARG JAVA_PACKAGE")) { + Pattern pattern = Pattern.compile("ARG JAVA_PACKAGE=java-(\\w+)-openjdk-headless"); + Matcher matcher = pattern.matcher(s); + if (matcher.find()) { + String match = matcher.group(1); + try { + javaVersion.set(Integer.parseInt(match)); + state.set(2); + } catch (NumberFormatException ignored) { + state.set(4); + } + } + } + }); + } catch (IOException ignored) { + state.set(4); + } + if (state.get() == 2) { + return Optional.of(new DockerFileBaseInformation(baseImage.get(), javaVersion.get())); + } + return Optional.empty(); + } +} diff --git a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java new file mode 100644 index 0000000000000..c8f058b7a0412 --- /dev/null +++ b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java @@ -0,0 +1,41 @@ +package io.quarkus.container.image.docker.deployment; + +import static io.quarkus.container.image.docker.deployment.TestUtil.getPath; +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +class RedHatOpenJDKRuntimeBaseProviderTest { + + private final DockerFileBaseInformationProvider sut = new RedHatOpenJDKRuntimeBaseProvider(); + + @Test + void testImageWithJava11() { + Path path = getPath("openjdk-11-runtime"); + var result = sut.determine(path); + assertThat(result).hasValueSatisfying(v -> { + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-11-runtime:1.10"); + assertThat(v.getJavaVersion()).isEqualTo(11); + }); + } + + @Test + void testImageWithJava17() { + Path path = getPath("openjdk-17-runtime"); + var result = sut.determine(path); + assertThat(result).hasValueSatisfying(v -> { + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-17-runtime"); + assertThat(v.getJavaVersion()).isEqualTo(17); + }); + } + + @Test + void testUnhandled() { + Path path = getPath("ubi-java11"); + var result = sut.determine(path); + assertThat(result).isEmpty(); + } + +} diff --git a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/TestUtil.java b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/TestUtil.java new file mode 100644 index 0000000000000..ad45ce736c77a --- /dev/null +++ b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/TestUtil.java @@ -0,0 +1,19 @@ +package io.quarkus.container.image.docker.deployment; + +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; + +final class TestUtil { + + private TestUtil() { + } + + static Path getPath(String filename) { + try { + return Paths.get(Thread.currentThread().getContextClassLoader().getResource(filename).toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } +} diff --git a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProviderTest.java b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProviderTest.java new file mode 100644 index 0000000000000..bd263fe871e24 --- /dev/null +++ b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/UbiMinimalBaseProviderTest.java @@ -0,0 +1,41 @@ +package io.quarkus.container.image.docker.deployment; + +import static io.quarkus.container.image.docker.deployment.TestUtil.getPath; +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +class UbiMinimalBaseProviderTest { + + private final DockerFileBaseInformationProvider sut = new UbiMinimalBaseProvider(); + + @Test + void testImageWithJava11() { + Path path = getPath("ubi-java11"); + var result = sut.determine(path); + assertThat(result).hasValueSatisfying(v -> { + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/ubi-minimal:8.3"); + assertThat(v.getJavaVersion()).isEqualTo(11); + }); + } + + @Test + void testImageWithJava17() { + Path path = getPath("ubi-java17"); + var result = sut.determine(path); + assertThat(result).hasValueSatisfying(v -> { + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/ubi-minimal"); + assertThat(v.getJavaVersion()).isEqualTo(17); + }); + } + + @Test + void testUnhandled() { + Path path = getPath("openjdk-11-runtime"); + var result = sut.determine(path); + assertThat(result).isEmpty(); + } + +} diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-11-runtime b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-11-runtime new file mode 100644 index 0000000000000..5e97afd82d60e --- /dev/null +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-11-runtime @@ -0,0 +1,17 @@ +FROM registry.access.redhat.com/ubi8/openjdk-11-runtime:1.10 + +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' + +# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. +ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 target/quarkus-app/*.jar /deployments/ +COPY --chown=185 target/quarkus-app/app/ /deployments/app/ +COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 + +ENTRYPOINT [ "java", "-jar", "/deployments/quarkus-run.jar" ] diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime new file mode 100644 index 0000000000000..7593800a82311 --- /dev/null +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime @@ -0,0 +1,18 @@ +# Use Java 17 base image +FROM registry.access.redhat.com/ubi8/openjdk-17-runtime + +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' + +# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. +ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 target/quarkus-app/*.jar /deployments/ +COPY --chown=185 target/quarkus-app/app/ /deployments/app/ +COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 + +ENTRYPOINT [ "java", "-jar", "/deployments/quarkus-run.jar" ] diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java11 b/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java11 new file mode 100644 index 0000000000000..963a2bcf88249 --- /dev/null +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java11 @@ -0,0 +1,31 @@ +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.3 + +ARG JAVA_PACKAGE=java-11-openjdk-headless +ARG RUN_JAVA_VERSION=1.3.8 +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' +# Install java and the run-java script +# Also set up permissions for user `1001` +RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \ + && microdnf update \ + && microdnf clean all \ + && mkdir /deployments \ + && chown 1001 /deployments \ + && chmod "g+rwX" /deployments \ + && chown 1001:root /deployments \ + && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \ + && chown 1001 /deployments/run-java.sh \ + && chmod 540 /deployments/run-java.sh \ + && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security + +# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. +ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=1001 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=1001 target/quarkus-app/*.jar /deployments/ +COPY --chown=1001 target/quarkus-app/app/ /deployments/app/ +COPY --chown=1001 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 1001 + +ENTRYPOINT [ "/deployments/run-java.sh" ] diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java17 b/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java17 new file mode 100644 index 0000000000000..5ae6e1e2f3ac4 --- /dev/null +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/ubi-java17 @@ -0,0 +1,31 @@ +FROM registry.access.redhat.com/ubi8/ubi-minimal + +ARG JAVA_PACKAGE=java-17-openjdk-headless +ARG RUN_JAVA_VERSION=1.3.8 +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' +# Install java and the run-java script +# Also set up permissions for user `1001` +RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \ + && microdnf update \ + && microdnf clean all \ + && mkdir /deployments \ + && chown 1001 /deployments \ + && chmod "g+rwX" /deployments \ + && chown 1001:root /deployments \ + && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \ + && chown 1001 /deployments/run-java.sh \ + && chmod 540 /deployments/run-java.sh \ + && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security + +# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. +ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=1001 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=1001 target/quarkus-app/*.jar /deployments/ +COPY --chown=1001 target/quarkus-app/app/ /deployments/app/ +COPY --chown=1001 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 1001 + +ENTRYPOINT [ "/deployments/run-java.sh" ]