Skip to content

Commit

Permalink
Merge pull request #23212 from geoand/dockerfile-java17-warning
Browse files Browse the repository at this point in the history
Fail the building docker image when a Java version mismatch exists
  • Loading branch information
geoand authored Jan 26, 2022
2 parents 6600a9f + 769c605 commit d64d13b
Show file tree
Hide file tree
Showing 12 changed files with 367 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-container-image-deployment</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand All @@ -41,4 +46,4 @@
</plugins>
</build>

</project>
</project>
Original file line number Diff line number Diff line change
@@ -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<DockerFileBaseInformation> determine(Path dockerFile);

static DockerFileBaseInformationProvider impl() {
return new DockerFileBaseInformationProvider() {

private final List<DockerFileBaseInformationProvider> delegates = List.of(new UbiMinimalBaseProvider(),
new RedHatOpenJDKRuntimeBaseProvider());

@Override
public Optional<DockerFileBaseInformation> determine(Path dockerFile) {
for (DockerFileBaseInformationProvider delegate : delegates) {
Optional<DockerFileBaseInformation> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,6 +64,7 @@ public void dockerBuildFromJar(DockerConfig dockerConfig,
ContainerImageConfig containerImageConfig,
OutputTargetBuildItem out,
ContainerImageInfoBuildItem containerImageInfo,
CompiledJavaVersionBuildItem compiledJavaVersion,
Optional<ContainerImageBuildRequestBuildItem> buildRequest,
Optional<ContainerImagePushRequestBuildItem> pushRequest,
@SuppressWarnings("unused") Optional<AppCDSResultBuildItem> appCDSResult, // ensure docker build will be performed after AppCDS creation
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DockerFileBaseInformation> determine(Path dockerFile) {
try (Stream<String> lines = Files.lines(dockerFile)) {
Optional<String> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<DockerFileBaseInformation> 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<String> baseImage = new AtomicReference<>(null);
AtomicInteger javaVersion = new AtomicInteger(0);
try (Stream<String> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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();
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}

}
Original file line number Diff line number Diff line change
@@ -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" ]
Original file line number Diff line number Diff line change
@@ -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" ]
Original file line number Diff line number Diff line change
@@ -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" ]
Loading

0 comments on commit d64d13b

Please sign in to comment.