From 98f1fd1162af6d3fe8ef935b063abacd031a5b1d Mon Sep 17 00:00:00 2001 From: Michal Karm Babacek Date: Sat, 25 Feb 2023 18:53:12 +0100 Subject: [PATCH 01/27] Enable Podman for Windows in-container tests Enables enforcing podman on systems where both podman and docker are available --- .../quarkus/deployment/IsDockerWorking.java | 4 +- .../deployment/pkg/steps/AppCDSBuildStep.java | 3 +- .../NativeImageBuildLocalContainerRunner.java | 5 +- .../NativeImageBuildContainerRunnerTest.java | 10 ++- .../runtime/util/ContainerRuntimeUtil.java | 15 ++++ .../DefaultDockerContainerLauncher.java | 73 ++++++++----------- .../common/DefaultNativeImageLauncher.java | 2 +- .../io/quarkus/test/common/LauncherUtil.java | 2 +- .../test/junit/IntegrationTestUtil.java | 4 +- 9 files changed, 65 insertions(+), 53 deletions(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java b/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java index 2b26c34844365..2fe3f196040a6 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java @@ -175,7 +175,7 @@ private static class DockerBinaryStrategy implements Strategy { private DockerBinaryStrategy(boolean silent) { this.silent = silent; - this.binary = ConfigProvider.getConfig().getOptionalValue("quarkus.docker.executable-name", String.class) + this.binary = ConfigProvider.getConfig().getOptionalValue("quarkus.native.container-runtime", String.class) .orElse("docker"); } @@ -194,7 +194,7 @@ public Result get() { try { OutputFilter filter = new OutputFilter(); if (ExecUtil.execWithTimeout(new File("."), filter, Duration.ofMillis(DOCKER_CMD_CHECK_TIMEOUT), - "docker", "version", "--format", "'{{.Server.Version}}'")) { + binary, "version", "--format", "'{{.Server.Version}}'")) { LOGGER.debugf("Docker daemon found. Version: %s", filter.getOutput()); return Result.AVAILABLE; } else { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java index 22c2714cb9a7f..f75c9026a9b66 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java @@ -1,6 +1,7 @@ package io.quarkus.deployment.pkg.steps; import static io.quarkus.deployment.pkg.steps.LinuxIDUtil.getLinuxID; +import static io.quarkus.deployment.pkg.steps.NativeImageBuildLocalContainerRunner.DOCKER_EXECUTABLE; import java.io.File; import java.io.IOException; @@ -202,7 +203,7 @@ private Path createClassesList(JarBuildItem jarResult, private List dockerRunCommands(OutputTargetBuildItem outputTarget, String containerImage, String containerWorkingDir) { List command = new ArrayList<>(10); - command.add("docker"); + command.add(DOCKER_EXECUTABLE); command.add("run"); command.add("-v"); command.add(outputTarget.getOutputDirectory().toAbsolutePath().toString() + ":" + CONTAINER_IMAGE_BASE_BUILD_DIR diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java index a7a8aa27725e3..f916ea37d08e2 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java @@ -9,7 +9,7 @@ import java.util.List; import org.apache.commons.lang3.SystemUtils; -import org.jboss.logging.Logger; +import org.eclipse.microprofile.config.ConfigProvider; import io.quarkus.deployment.pkg.NativeConfig; import io.quarkus.deployment.util.FileUtil; @@ -17,7 +17,8 @@ public class NativeImageBuildLocalContainerRunner extends NativeImageBuildContainerRunner { - private static final Logger LOGGER = Logger.getLogger(NativeImageBuildLocalContainerRunner.class.getName()); + public static final String DOCKER_EXECUTABLE = ConfigProvider.getConfig() + .getOptionalValue("quarkus.native.container-runtime", String.class).orElse("docker"); public NativeImageBuildLocalContainerRunner(NativeConfig nativeConfig, Path outputDir) { super(nativeConfig, outputDir); diff --git a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java index 17d9c77287d21..3d3bd861d108b 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java @@ -1,5 +1,6 @@ package io.quarkus.deployment.pkg.steps; +import static io.quarkus.deployment.pkg.steps.NativeImageBuildLocalContainerRunner.DOCKER_EXECUTABLE; import static org.assertj.core.api.Assertions.assertThat; import java.nio.file.Path; @@ -25,33 +26,36 @@ void testBuilderImageBeingPickedUp() { nativeConfig.builderImage = "graalvm"; localRunner = new NativeImageBuildLocalContainerRunner(nativeConfig, Path.of("/tmp")); - command = localRunner.buildCommand("docker", Collections.emptyList(), Collections.emptyList()); + command = localRunner.buildCommand(DOCKER_EXECUTABLE, Collections.emptyList(), Collections.emptyList()); found = false; for (String part : command) { if (part.contains("ubi-quarkus-graalvmce-builder-image")) { found = true; + break; } } assertThat(found).isTrue(); nativeConfig.builderImage = "mandrel"; localRunner = new NativeImageBuildLocalContainerRunner(nativeConfig, Path.of("/tmp")); - command = localRunner.buildCommand("docker", Collections.emptyList(), Collections.emptyList()); + command = localRunner.buildCommand(DOCKER_EXECUTABLE, Collections.emptyList(), Collections.emptyList()); found = false; for (String part : command) { if (part.contains("ubi-quarkus-mandrel-builder-image")) { found = true; + break; } } assertThat(found).isTrue(); nativeConfig.builderImage = "RandomString"; localRunner = new NativeImageBuildLocalContainerRunner(nativeConfig, Path.of("/tmp")); - command = localRunner.buildCommand("docker", Collections.emptyList(), Collections.emptyList()); + command = localRunner.buildCommand(DOCKER_EXECUTABLE, Collections.emptyList(), Collections.emptyList()); found = false; for (String part : command) { if (part.equals("RandomString")) { found = true; + break; } } assertThat(found).isTrue(); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java index 94cee990fc25b..c24572b5d3c3d 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java @@ -7,6 +7,7 @@ import java.nio.charset.StandardCharsets; import java.util.function.Predicate; +import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.logging.Logger; public final class ContainerRuntimeUtil { @@ -30,6 +31,20 @@ public static ContainerRuntime detectContainerRuntime() { // podman version 2.1.1 String podmanVersionOutput = getVersionOutputFor(ContainerRuntime.PODMAN); boolean podmanAvailable = podmanVersionOutput.startsWith("podman version"); + + final String executable = ConfigProvider.getConfig() + .getOptionalValue("quarkus.native.container-runtime", String.class).orElse(null); + if (executable != null) { + if (executable.trim().equalsIgnoreCase("docker") && dockerAvailable) { + return ContainerRuntime.DOCKER; + } else if (executable.trim().equalsIgnoreCase("podman") && podmanAvailable) { + return ContainerRuntime.PODMAN; + } else { + log.warn("quarkus.native.container-runtime config property must be set to either podman or docker " + + "and the executable must be available. Ignoring it."); + } + } + if (dockerAvailable) { // Check if "docker" is an alias to "podman" if (dockerVersionOutput.equals(podmanVersionOutput)) { diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java b/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java index 4918c94d6dacf..8203acbf65699 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java @@ -5,15 +5,12 @@ import static io.quarkus.test.common.LauncherUtil.waitForCapturedListeningData; import static io.quarkus.test.common.LauncherUtil.waitForStartedFunction; import static java.lang.ProcessBuilder.Redirect.DISCARD; -import static java.lang.ProcessBuilder.Redirect.PIPE; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; import java.net.ServerSocket; +import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -24,9 +21,8 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; -import org.apache.commons.io.IOUtils; -import org.apache.commons.io.input.TeeInputStream; import org.apache.commons.lang3.RandomStringUtils; +import org.jboss.logging.Logger; import io.quarkus.runtime.util.ContainerRuntimeUtil; import io.quarkus.test.common.http.TestHTTPResourceManager; @@ -34,6 +30,8 @@ public class DefaultDockerContainerLauncher implements DockerContainerArtifactLauncher { + private static final Logger log = Logger.getLogger(DefaultDockerContainerLauncher.class); + private int httpPort; private int httpsPort; private long waitTimeSeconds; @@ -43,16 +41,11 @@ public class DefaultDockerContainerLauncher implements DockerContainerArtifactLa private String containerImage; private boolean pullRequired; private Map additionalExposedPorts; - private final Map systemProps = new HashMap<>(); - private boolean isSsl; - - private String containerName; - + private final String containerName = "quarkus-integration-test-" + RandomStringUtils.random(5, true, false); private String containerRuntimeBinaryName; - - private ExecutorService executorService = Executors.newSingleThreadExecutor(); + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); @Override public void init(DockerContainerArtifactLauncher.DockerInitContext initContext) { @@ -78,7 +71,7 @@ public void start() throws IOException { containerRuntimeBinaryName = determineBinary(); if (pullRequired) { - System.out.println("Pulling container image '" + containerImage + "'"); + log.infof("Pulling container image '%s'", containerImage); try { int pullResult = new ProcessBuilder().redirectError(DISCARD).redirectOutput(DISCARD) .command(containerRuntimeBinaryName, "pull", containerImage).start().waitFor(); @@ -99,21 +92,21 @@ public void start() throws IOException { httpsPort = getRandomPort(); } - List args = new ArrayList<>(); + final List args = new ArrayList<>(); args.add(containerRuntimeBinaryName); args.add("run"); if (!argLine.isEmpty()) { args.addAll(argLine); } args.add("--name"); - containerName = "quarkus-integration-test-" + RandomStringUtils.random(5, true, false); args.add(containerName); + args.add("-i"); // Interactive, write logs to stdout args.add("--rm"); args.add("-p"); args.add(httpPort + ":" + httpPort); args.add("-p"); args.add(httpsPort + ":" + httpsPort); - for (var entry : additionalExposedPorts.entrySet()) { + for (Map.Entry entry : additionalExposedPorts.entrySet()) { args.add("-p"); args.add(entry.getKey() + ":" + entry.getValue()); } @@ -125,7 +118,7 @@ public void start() throws IOException { if (DefaultJarLauncher.HTTP_PRESENT) { args.addAll(toEnvVar("quarkus.http.port", "" + httpPort)); args.addAll(toEnvVar("quarkus.http.ssl-port", "" + httpsPort)); - // this won't be correct when using the random port but it's really only used by us for the rest client tests + // This won't be correct when using the random port, but it's really only used by us for the rest client tests // in the main module, since those tests hit the application itself args.addAll(toEnvVar("test.url", TestHTTPResourceManager.getUri())); } @@ -138,31 +131,31 @@ public void start() throws IOException { } args.add(containerImage); - Path logFile = PropertyTestUtil.getLogFilePath(); - Files.deleteIfExists(logFile); - Files.createDirectories(logFile.getParent()); - - Path containerLogFile = Paths.get("target", "container.log"); - Files.createDirectories(containerLogFile.getParent()); - FileOutputStream containerLogOutputStream = new FileOutputStream(containerLogFile.toFile(), true); + final Path logFile = PropertyTestUtil.getLogFilePath(); + try { + Files.deleteIfExists(logFile); + Files.createDirectories(logFile.getParent()); + } catch (FileSystemException e) { + log.warnf("Log file %s deletion failed, could happen on Windows, we can carry on.", logFile); + } - System.out.println("Executing \"" + String.join(" ", args) + "\""); + log.infof("Executing \"%s\"", String.join(" ", args)); - Function startedFunction = createStartedFunction(); + final Function startedFunction = createStartedFunction(); - // the idea here is to obtain the logs of the application simply by redirecting all its output the a file - // this is done in contrast with the JarLauncher and NativeImageLauncher because in the case of the container - // the log itself is written inside the container - Process quarkusProcess = new ProcessBuilder(args).redirectError(PIPE).redirectOutput(PIPE).start(); - InputStream tee = new TeeInputStream(quarkusProcess.getInputStream(), new FileOutputStream(logFile.toFile())); - executorService.submit(() -> IOUtils.copy(tee, containerLogOutputStream)); + // We rely on the container writing log to stdout. If it just writes to a logfile inside itself, we would have + // to mount /work/ directory to get quarkus.log. + final Process containerProcess = new ProcessBuilder(args) + .redirectErrorStream(true) + .redirectOutput(ProcessBuilder.Redirect.appendTo(logFile.toFile())) + .start(); if (startedFunction != null) { - IntegrationTestStartedNotifier.Result result = waitForStartedFunction(startedFunction, quarkusProcess, + final IntegrationTestStartedNotifier.Result result = waitForStartedFunction(startedFunction, containerProcess, waitTimeSeconds, logFile); isSsl = result.isSsl(); } else { - ListeningAddress result = waitForCapturedListeningData(quarkusProcess, logFile, waitTimeSeconds); + final ListeningAddress result = waitForCapturedListeningData(containerProcess, logFile, waitTimeSeconds); updateConfigForPort(result.getPort()); isSsl = result.isSsl(); } @@ -188,10 +181,7 @@ public void includeAsSysProps(Map systemProps) { private List toEnvVar(String property, String value) { if ((property != null) && (!property.isEmpty())) { - List result = new ArrayList<>(2); - result.add("--env"); - result.add(String.format("%s=%s", convertPropertyToEnvVar(property), value)); - return result; + return List.of("--env", String.format("%s=%s", convertPropertyToEnvVar(property), value)); } return Collections.emptyList(); } @@ -203,14 +193,13 @@ private String convertPropertyToEnvVar(String property) { @Override public void close() { try { - Process dockerStopProcess = new ProcessBuilder(containerRuntimeBinaryName, "stop", containerName) + final Process dockerStopProcess = new ProcessBuilder(containerRuntimeBinaryName, "stop", containerName) .redirectError(DISCARD) .redirectOutput(DISCARD).start(); dockerStopProcess.waitFor(10, TimeUnit.SECONDS); } catch (IOException | InterruptedException e) { - System.out.println("Unable to stop container '" + containerName + "'"); + log.errorf("Unable to stop container '%s'", containerName); } executorService.shutdown(); } - } diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/DefaultNativeImageLauncher.java b/test-framework/common/src/main/java/io/quarkus/test/common/DefaultNativeImageLauncher.java index 2680369cdbe12..9fa084ce4001e 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/DefaultNativeImageLauncher.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/DefaultNativeImageLauncher.java @@ -130,7 +130,7 @@ public void start(String[] programArgs, boolean handleIo) throws IOException { args.add("-Dtest.url=" + TestHTTPResourceManager.getUri()); } Path logFile = PropertyTestUtil.getLogFilePath(); - args.add("-Dquarkus.log.file.path=" + logFile.toAbsolutePath().toString()); + args.add("-Dquarkus.log.file.path=" + logFile.toAbsolutePath()); args.add("-Dquarkus.log.file.enable=true"); if (testProfile != null) { args.add("-Dquarkus.profile=" + testProfile); diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/LauncherUtil.java b/test-framework/common/src/main/java/io/quarkus/test/common/LauncherUtil.java index 76c2701fadded..e6fe3df54729d 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/LauncherUtil.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/LauncherUtil.java @@ -253,7 +253,7 @@ public void run() { long now = System.currentTimeMillis(); // if we have seen info that the app is started in the log a while ago - // or waiting the the next check interval will exceed the bailout time, it's time to finish waiting: + // or waiting the next check interval will exceed the bailout time, it's time to finish waiting: if (now + LOG_CHECK_INTERVAL > bailoutTime || now - 2 * LOG_CHECK_INTERVAL > timeStarted) { if (started) { dataDetermined(null, null); // no http, all is null diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java index 50b565e3b9f34..2defc2c8e42c5 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java @@ -34,6 +34,7 @@ import org.apache.commons.lang3.RandomStringUtils; import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.jandex.Index; import org.junit.jupiter.api.extension.ExtensionContext; @@ -64,7 +65,8 @@ public final class IntegrationTestUtil { public static final int DEFAULT_HTTPS_PORT = 8444; public static final long DEFAULT_WAIT_TIME_SECONDS = 60; - private static final String DOCKER_BINARY = "docker"; + private static final String DOCKER_BINARY = ConfigProvider.getConfig() + .getOptionalValue("quarkus.native.container-runtime", String.class).orElse("docker"); private IntegrationTestUtil() { } From 9340137459f7338e46cb1431c5cbde0f175305c7 Mon Sep 17 00:00:00 2001 From: Michal Karm Babacek Date: Mon, 27 Feb 2023 17:21:19 +0100 Subject: [PATCH 02/27] AWT in-container test --- integration-tests/awt/pom.xml | 41 ++++++++++++++++++- .../awt/src/main/docker/Dockerfile.native | 15 +++++++ 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 integration-tests/awt/src/main/docker/Dockerfile.native diff --git a/integration-tests/awt/pom.xml b/integration-tests/awt/pom.xml index 7f5c5ff6af75e..6f7ab12430a13 100644 --- a/integration-tests/awt/pom.xml +++ b/integration-tests/awt/pom.xml @@ -27,6 +27,10 @@ io.quarkus quarkus-resteasy-multipart + + io.quarkus + quarkus-container-image-docker + @@ -45,6 +49,8 @@ test + + io.quarkus @@ -59,7 +65,6 @@ - io.quarkus quarkus-resteasy-multipart-deployment @@ -99,7 +104,19 @@ - + + io.quarkus + quarkus-container-image-docker-deployment + ${project.version} + pom + test + + + * + * + + + @@ -128,6 +145,26 @@ false + + maven-failsafe-plugin + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + localhost + 8081 + + + + + diff --git a/integration-tests/awt/src/main/docker/Dockerfile.native b/integration-tests/awt/src/main/docker/Dockerfile.native new file mode 100644 index 0000000000000..8b7e7fd728806 --- /dev/null +++ b/integration-tests/awt/src/main/docker/Dockerfile.native @@ -0,0 +1,15 @@ +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.7 +# Dependencies for AWT +RUN microdnf install freetype fontconfig \ + && microdnf clean all +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*-runner /work/application +# Permissions fix for Windows +RUN chmod "ugo+x" /work/application +EXPOSE 8081 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] From 4f183f3f86c124bc909a95993dd8dd82a591d19c Mon Sep 17 00:00:00 2001 From: Michal Karm Babacek Date: Mon, 6 Mar 2023 10:41:20 +0100 Subject: [PATCH 03/27] Logs proper command when detecting rootless container runtime e.g. Before: ``` WARN [io.qua.run.uti.ContainerRuntimeUtil] (main) Command "docker" exited with error code 1. Rootless container runtime detection might not be reliable. ``` after: ``` WARN [io.qua.run.uti.ContainerRuntimeUtil] (main) Command "docker info" exited with error code 1. Rootless container runtime detection might not be reliable or the container service is not running at all. ``` plus with `-Dquarkus.log.level=DEBUG` it logs: ``` DEBUG [io.qua.run.uti.ContainerRuntimeUtil] (main) Command "docker info" output: Client: Context: default Debug Mode: false Plugins: buildx: Docker Buildx (Docker Inc.) Version: v0.10.2 Path: /usr/libexec/docker/cli-plugins/docker-buildx compose: Docker Compose (Docker Inc.) Version: v2.16.0 Path: /usr/libexec/docker/cli-plugins/docker-compose scan: Docker Scan (Docker Inc.) Version: v0.23.0 Path: /usr/libexec/docker/cli-plugins/docker-scan Server: ERROR: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? ``` --- .../runtime/util/ContainerRuntimeUtil.java | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java index c24572b5d3c3d..4fc6bc1a8eb4f 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java @@ -6,6 +6,7 @@ import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.function.Predicate; +import java.util.stream.Collectors; import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.logging.Logger; @@ -80,31 +81,37 @@ private static String getVersionOutputFor(ContainerRuntime containerRuntime) { private static boolean getRootlessStateFor(ContainerRuntime containerRuntime) { Process rootlessProcess = null; + ProcessBuilder pb = null; try { - ProcessBuilder pb = new ProcessBuilder(containerRuntime.getExecutableName(), "info") - .redirectErrorStream(true); + pb = new ProcessBuilder(containerRuntime.getExecutableName(), "info").redirectErrorStream(true); rootlessProcess = pb.start(); int exitCode = rootlessProcess.waitFor(); if (exitCode != 0) { log.warnf("Command \"%s\" exited with error code %d. " + - "Rootless container runtime detection might not be reliable.", - containerRuntime.getExecutableName(), exitCode); + "Rootless container runtime detection might not be reliable or the container service is not running at all.", + String.join(" ", pb.command()), exitCode); } try (InputStream inputStream = rootlessProcess.getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { - Predicate stringPredicate; - // Docker includes just "rootless" under SecurityOptions, while podman includes "rootless: " - if (containerRuntime == ContainerRuntime.DOCKER) { - stringPredicate = line -> line.trim().equals("rootless"); + if (exitCode != 0) { + log.debugf("Command \"%s\" output: %s", String.join(" ", pb.command()), + bufferedReader.lines().collect(Collectors.joining(System.lineSeparator()))); + return false; } else { - stringPredicate = line -> line.trim().equals("rootless: true"); + Predicate stringPredicate; + // Docker includes just "rootless" under SecurityOptions, while podman includes "rootless: " + if (containerRuntime == ContainerRuntime.DOCKER) { + stringPredicate = line -> line.trim().equals("rootless"); + } else { + stringPredicate = line -> line.trim().equals("rootless: true"); + } + return bufferedReader.lines().anyMatch(stringPredicate); } - return bufferedReader.lines().anyMatch(stringPredicate); } } catch (IOException | InterruptedException e) { // If an exception is thrown in the process, assume we are not running rootless (default docker installation) - log.debugf(e, "Failure to read info output from %s", containerRuntime.getExecutableName()); + log.debugf(e, "Failure to read info output from %s", String.join(" ", pb.command())); return false; } finally { if (rootlessProcess != null) { From aca49284cba632e654a9595ab92afc3dd7efc8ac Mon Sep 17 00:00:00 2001 From: Michal Karm Babacek Date: Tue, 7 Mar 2023 15:02:19 +0100 Subject: [PATCH 04/27] ConfigProvider.getConfig() different classloader issue --- .../deployment/pkg/steps/AppCDSBuildStep.java | 3 ++- .../quarkus/runtime/util/ContainerRuntimeUtil.java | 13 +++++++------ .../io/quarkus/test/junit/IntegrationTestUtil.java | 6 ++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java index f75c9026a9b66..7d9efb860968c 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java @@ -1,7 +1,7 @@ package io.quarkus.deployment.pkg.steps; import static io.quarkus.deployment.pkg.steps.LinuxIDUtil.getLinuxID; -import static io.quarkus.deployment.pkg.steps.NativeImageBuildLocalContainerRunner.DOCKER_EXECUTABLE; +import static io.quarkus.runtime.util.ContainerRuntimeUtil.detectContainerRuntime; import java.io.File; import java.io.IOException; @@ -38,6 +38,7 @@ public class AppCDSBuildStep { public static final String CLASSES_LIST_FILE_NAME = "classes.lst"; private static final String CONTAINER_IMAGE_BASE_BUILD_DIR = "/tmp/quarkus"; private static final String CONTAINER_IMAGE_APPCDS_DIR = CONTAINER_IMAGE_BASE_BUILD_DIR + "/appcds"; + public static final String DOCKER_EXECUTABLE = detectContainerRuntime().getExecutableName(); @BuildStep(onlyIf = AppCDSRequired.class) public void requested(OutputTargetBuildItem outputTarget, BuildProducer producer) diff --git a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java index 4fc6bc1a8eb4f..707d54fa963e8 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java @@ -11,9 +11,13 @@ import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.logging.Logger; +import io.smallrye.config.SmallRyeConfig; + public final class ContainerRuntimeUtil { private static final Logger log = Logger.getLogger(ContainerRuntimeUtil.class); + private static final String DOCKER_EXECUTABLE = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class) + .getOptionalValue("quarkus.native.container-runtime", String.class).orElse(null); private ContainerRuntimeUtil() { } @@ -32,13 +36,10 @@ public static ContainerRuntime detectContainerRuntime() { // podman version 2.1.1 String podmanVersionOutput = getVersionOutputFor(ContainerRuntime.PODMAN); boolean podmanAvailable = podmanVersionOutput.startsWith("podman version"); - - final String executable = ConfigProvider.getConfig() - .getOptionalValue("quarkus.native.container-runtime", String.class).orElse(null); - if (executable != null) { - if (executable.trim().equalsIgnoreCase("docker") && dockerAvailable) { + if (DOCKER_EXECUTABLE != null) { + if (DOCKER_EXECUTABLE.trim().equalsIgnoreCase("docker") && dockerAvailable) { return ContainerRuntime.DOCKER; - } else if (executable.trim().equalsIgnoreCase("podman") && podmanAvailable) { + } else if (DOCKER_EXECUTABLE.trim().equalsIgnoreCase("podman") && podmanAvailable) { return ContainerRuntime.PODMAN; } else { log.warn("quarkus.native.container-runtime config property must be set to either podman or docker " + diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java index 2defc2c8e42c5..3c74d966feb80 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java @@ -1,5 +1,6 @@ package io.quarkus.test.junit; +import static io.quarkus.runtime.util.ContainerRuntimeUtil.detectContainerRuntime; import static io.quarkus.test.common.PathTestHelper.getAppClassLocationForTestLocation; import static io.quarkus.test.common.PathTestHelper.getTestClassesLocation; import static java.lang.ProcessBuilder.Redirect.DISCARD; @@ -34,7 +35,6 @@ import org.apache.commons.lang3.RandomStringUtils; import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.jandex.Index; import org.junit.jupiter.api.extension.ExtensionContext; @@ -64,9 +64,7 @@ public final class IntegrationTestUtil { public static final int DEFAULT_PORT = 8081; public static final int DEFAULT_HTTPS_PORT = 8444; public static final long DEFAULT_WAIT_TIME_SECONDS = 60; - - private static final String DOCKER_BINARY = ConfigProvider.getConfig() - .getOptionalValue("quarkus.native.container-runtime", String.class).orElse("docker"); + private static final String DOCKER_BINARY = detectContainerRuntime().getExecutableName(); private IntegrationTestUtil() { } From d970257464eb2a4a4b4742a8f8f0863c8c1eef87 Mon Sep 17 00:00:00 2001 From: Michal Karm Babacek Date: Mon, 13 Mar 2023 17:08:35 +0100 Subject: [PATCH 05/27] Container runtime detection cache and rootless check With this change, the detected container runtime is cached per JVM run so as it is not checked again regardless of the classloader loading the ContainerRuntimeUtil class. The rootless attribute of such runtime is lazily fetched once as needed. (cherry picked from commit 5c58a03dddd36d54eaa46cb3fe5c228b4ce98d17) --- .../quarkus/deployment/IsDockerWorking.java | 47 +----- .../NativeImageBuildLocalContainerRunner.java | 26 ++-- .../runtime/util/ContainerRuntimeUtil.java | 137 ++++++++++++++---- 3 files changed, 127 insertions(+), 83 deletions(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java b/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java index 2fe3f196040a6..61cf180c5cb45 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/IsDockerWorking.java @@ -1,7 +1,8 @@ package io.quarkus.deployment; -import java.io.File; +import static io.quarkus.runtime.util.ContainerRuntimeUtil.ContainerRuntime.UNAVAILABLE; + import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -11,23 +12,20 @@ import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; -import java.time.Duration; import java.util.List; import java.util.Optional; import java.util.function.BooleanSupplier; import java.util.function.Supplier; -import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.logging.Logger; import io.quarkus.deployment.console.StartupLogCompressor; -import io.quarkus.deployment.util.ExecUtil; +import io.quarkus.runtime.util.ContainerRuntimeUtil; public class IsDockerWorking implements BooleanSupplier { private static final Logger LOGGER = Logger.getLogger(IsDockerWorking.class.getName()); public static final int DOCKER_HOST_CHECK_TIMEOUT = 1000; - public static final int DOCKER_CMD_CHECK_TIMEOUT = 3000; private final List strategies; @@ -36,8 +34,7 @@ public IsDockerWorking() { } public IsDockerWorking(boolean silent) { - this.strategies = List.of(new TestContainersStrategy(silent), new DockerHostStrategy(), - new DockerBinaryStrategy(silent)); + this.strategies = List.of(new TestContainersStrategy(silent), new DockerHostStrategy(), new DockerBinaryStrategy()); } @Override @@ -170,41 +167,11 @@ public Result get() { private static class DockerBinaryStrategy implements Strategy { - private final boolean silent; - private final String binary; - - private DockerBinaryStrategy(boolean silent) { - this.silent = silent; - this.binary = ConfigProvider.getConfig().getOptionalValue("quarkus.native.container-runtime", String.class) - .orElse("docker"); - } - @Override public Result get() { - try { - if (!ExecUtil.execSilentWithTimeout(Duration.ofMillis(DOCKER_CMD_CHECK_TIMEOUT), binary, "-v")) { - LOGGER.warnf("'%s -v' returned an error code. Make sure your Docker binary is correct", binary); - return Result.UNKNOWN; - } - } catch (Exception e) { - LOGGER.warnf("No %s binary found or general error: %s", binary, e); - return Result.UNKNOWN; - } - - try { - OutputFilter filter = new OutputFilter(); - if (ExecUtil.execWithTimeout(new File("."), filter, Duration.ofMillis(DOCKER_CMD_CHECK_TIMEOUT), - binary, "version", "--format", "'{{.Server.Version}}'")) { - LOGGER.debugf("Docker daemon found. Version: %s", filter.getOutput()); - return Result.AVAILABLE; - } else { - if (!silent) { - LOGGER.warn("Could not determine version of Docker daemon"); - } - return Result.UNAVAILABLE; - } - } catch (Exception e) { - LOGGER.warn("Unexpected error occurred while determining Docker daemon version", e); + if (ContainerRuntimeUtil.detectContainerRuntime(false) != UNAVAILABLE) { + return Result.AVAILABLE; + } else { return Result.UNKNOWN; } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java index f916ea37d08e2..3b3d00eac18f7 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java @@ -1,6 +1,8 @@ package io.quarkus.deployment.pkg.steps; import static io.quarkus.deployment.pkg.steps.LinuxIDUtil.getLinuxID; +import static io.quarkus.runtime.util.ContainerRuntimeUtil.ContainerRuntime.DOCKER; +import static io.quarkus.runtime.util.ContainerRuntimeUtil.ContainerRuntime.PODMAN; import java.nio.file.Path; import java.util.ArrayList; @@ -13,7 +15,6 @@ import io.quarkus.deployment.pkg.NativeConfig; import io.quarkus.deployment.util.FileUtil; -import io.quarkus.runtime.util.ContainerRuntimeUtil; public class NativeImageBuildLocalContainerRunner extends NativeImageBuildContainerRunner { @@ -23,17 +24,15 @@ public class NativeImageBuildLocalContainerRunner extends NativeImageBuildContai public NativeImageBuildLocalContainerRunner(NativeConfig nativeConfig, Path outputDir) { super(nativeConfig, outputDir); if (SystemUtils.IS_OS_LINUX) { - ArrayList containerRuntimeArgs = new ArrayList<>(Arrays.asList(baseContainerRuntimeArgs)); - if (containerRuntime == ContainerRuntimeUtil.ContainerRuntime.DOCKER - && containerRuntime.isRootless()) { + final ArrayList containerRuntimeArgs = new ArrayList<>(Arrays.asList(baseContainerRuntimeArgs)); + if (containerRuntime == DOCKER && containerRuntime.isRootless()) { Collections.addAll(containerRuntimeArgs, "--user", String.valueOf(0)); } else { 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 == ContainerRuntimeUtil.ContainerRuntime.PODMAN - && containerRuntime.isRootless()) { + if (containerRuntime == PODMAN && containerRuntime.isRootless()) { // Needed to avoid AccessDeniedExceptions containerRuntimeArgs.add("--userns=keep-id"); } @@ -45,16 +44,19 @@ public NativeImageBuildLocalContainerRunner(NativeConfig nativeConfig, Path outp @Override protected List getContainerRuntimeBuildArgs() { - List containerRuntimeArgs = super.getContainerRuntimeBuildArgs(); - String volumeOutputPath = outputPath; + final List containerRuntimeArgs = super.getContainerRuntimeBuildArgs(); + final String volumeOutputPath; if (SystemUtils.IS_OS_WINDOWS) { - volumeOutputPath = FileUtil.translateToVolumePath(volumeOutputPath); + volumeOutputPath = FileUtil.translateToVolumePath(outputPath); + } else { + volumeOutputPath = outputPath; } - String selinuxBindOption = ":z"; - if (SystemUtils.IS_OS_MAC - && ContainerRuntimeUtil.detectContainerRuntime() == ContainerRuntimeUtil.ContainerRuntime.PODMAN) { + final String selinuxBindOption; + if (SystemUtils.IS_OS_MAC && containerRuntime == PODMAN) { selinuxBindOption = ""; + } else { + selinuxBindOption = ":z"; } Collections.addAll(containerRuntimeArgs, "-v", diff --git a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java index 707d54fa963e8..9ac79ff44719a 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java @@ -5,6 +5,9 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -18,6 +21,16 @@ public final class ContainerRuntimeUtil { private static final Logger log = Logger.getLogger(ContainerRuntimeUtil.class); private static final String DOCKER_EXECUTABLE = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class) .getOptionalValue("quarkus.native.container-runtime", String.class).orElse(null); + /* + * Caching the value in a file helps us only as much as one JVM execution is concerned. + * Test suite's pom.xml sets things like -Djava.io.tmpdir="${project.build.directory}", + * so the file could appear in /tmp/ or C:\Users\karm\AppData\Local\Temp\ or in fact in + * quarkus/integration-tests/something/target. + * There is no point in reaching it in `Path.of(Paths.get("").toAbsolutePath().toString(), "target", + * "quarkus_container_runtime.txt")` + * as the file is deleted between JVM executions anyway. + */ + static final Path CONTAINER_RUNTIME = Path.of(System.getProperty("java.io.tmpdir"), "quarkus_container_runtime.txt"); private ContainerRuntimeUtil() { } @@ -25,46 +38,101 @@ private ContainerRuntimeUtil() { /** * @return {@link ContainerRuntime#DOCKER} if it's available, or {@link ContainerRuntime#PODMAN} * if the podman - * executable exists in the environment or if the docker executable is an alias to podman + * executable exists in the environment or if the docker executable is an alias to podman, + * or {@link ContainerRuntime#UNAVAILABLE} if no container runtime is available and the required arg is false. * @throws IllegalStateException if no container runtime was found to build the image */ public static ContainerRuntime detectContainerRuntime() { - // Docker version 19.03.14, build 5eb3275d40 - String dockerVersionOutput = getVersionOutputFor(ContainerRuntime.DOCKER); - boolean dockerAvailable = dockerVersionOutput.contains("Docker version"); - // Check if Podman is installed - // podman version 2.1.1 - String podmanVersionOutput = getVersionOutputFor(ContainerRuntime.PODMAN); - boolean podmanAvailable = podmanVersionOutput.startsWith("podman version"); - if (DOCKER_EXECUTABLE != null) { - if (DOCKER_EXECUTABLE.trim().equalsIgnoreCase("docker") && dockerAvailable) { + return detectContainerRuntime(true); + } + + public static ContainerRuntime detectContainerRuntime(boolean required) { + final ContainerRuntime containerRuntime = loadConfig(); + if (containerRuntime != null) { + return containerRuntime; + } else { + // Docker version 19.03.14, build 5eb3275d40 + final String dockerVersionOutput = getVersionOutputFor(ContainerRuntime.DOCKER); + boolean dockerAvailable = dockerVersionOutput.contains("Docker version"); + // Check if Podman is installed + // podman version 2.1.1 + final String podmanVersionOutput = getVersionOutputFor(ContainerRuntime.PODMAN); + boolean podmanAvailable = podmanVersionOutput.startsWith("podman version"); + if (DOCKER_EXECUTABLE != null) { + if (DOCKER_EXECUTABLE.trim().equalsIgnoreCase("docker") && dockerAvailable) { + storeConfig(ContainerRuntime.DOCKER); + return ContainerRuntime.DOCKER; + } else if (DOCKER_EXECUTABLE.trim().equalsIgnoreCase("podman") && podmanAvailable) { + storeConfig(ContainerRuntime.PODMAN); + return ContainerRuntime.PODMAN; + } else { + log.warn("quarkus.native.container-runtime config property must be set to either podman or docker " + + "and the executable must be available. Ignoring it."); + } + } + if (dockerAvailable) { + // Check if "docker" is an alias to "podman" + if (dockerVersionOutput.equals(podmanVersionOutput)) { + storeConfig(ContainerRuntime.PODMAN); + return ContainerRuntime.PODMAN; + } + storeConfig(ContainerRuntime.DOCKER); return ContainerRuntime.DOCKER; - } else if (DOCKER_EXECUTABLE.trim().equalsIgnoreCase("podman") && podmanAvailable) { + } else if (podmanAvailable) { + storeConfig(ContainerRuntime.PODMAN); return ContainerRuntime.PODMAN; } else { - log.warn("quarkus.native.container-runtime config property must be set to either podman or docker " + - "and the executable must be available. Ignoring it."); + if (required) { + throw new IllegalStateException("No container runtime was found. " + + "Make sure you have either Docker or Podman installed in your environment."); + } else { + storeConfig(ContainerRuntime.UNAVAILABLE); + return ContainerRuntime.UNAVAILABLE; + } } } + } - if (dockerAvailable) { - // Check if "docker" is an alias to "podman" - if (dockerVersionOutput.equals(podmanVersionOutput)) { - return ContainerRuntime.PODMAN; + private static ContainerRuntime loadConfig() { + try { + if (Files.isReadable(CONTAINER_RUNTIME)) { + final String runtime = Files.readString(CONTAINER_RUNTIME, StandardCharsets.UTF_8); + if (ContainerRuntime.DOCKER.name().equalsIgnoreCase(runtime)) { + return ContainerRuntime.DOCKER; + } else if (ContainerRuntime.PODMAN.name().equalsIgnoreCase(runtime)) { + return ContainerRuntime.PODMAN; + } else if (ContainerRuntime.UNAVAILABLE.name().equalsIgnoreCase(runtime)) { + return ContainerRuntime.UNAVAILABLE; + } else { + log.warnf("The file %s contains an unknown value %s. Ignoring it.", + CONTAINER_RUNTIME.toAbsolutePath(), runtime); + return null; + } + } else { + return null; } - return ContainerRuntime.DOCKER; - } else if (podmanAvailable) { - return ContainerRuntime.PODMAN; - } else { - throw new IllegalStateException("No container runtime was found to. " - + "Make sure you have Docker or Podman installed in your environment."); + } catch (IOException e) { + log.warnf("Error reading file %s. Ignoring it. See: %s", + CONTAINER_RUNTIME.toAbsolutePath(), e); + return null; + } + } + + private static void storeConfig(ContainerRuntime containerRuntime) { + try { + Files.writeString(CONTAINER_RUNTIME, containerRuntime.name(), StandardCharsets.UTF_8, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + CONTAINER_RUNTIME.toFile().deleteOnExit(); + } catch (IOException e) { + log.warnf("Error writing to file %s. Ignoring it. See: %s", + CONTAINER_RUNTIME.toAbsolutePath(), e); } } private static String getVersionOutputFor(ContainerRuntime containerRuntime) { Process versionProcess = null; try { - ProcessBuilder pb = new ProcessBuilder(containerRuntime.getExecutableName(), "--version") + final ProcessBuilder pb = new ProcessBuilder(containerRuntime.getExecutableName(), "--version") .redirectErrorStream(true); versionProcess = pb.start(); versionProcess.waitFor(); @@ -100,7 +168,7 @@ private static boolean getRootlessStateFor(ContainerRuntime containerRuntime) { bufferedReader.lines().collect(Collectors.joining(System.lineSeparator()))); return false; } else { - Predicate stringPredicate; + final Predicate stringPredicate; // Docker includes just "rootless" under SecurityOptions, while podman includes "rootless: " if (containerRuntime == ContainerRuntime.DOCKER) { stringPredicate = line -> line.trim().equals("rootless"); @@ -126,19 +194,26 @@ private static boolean getRootlessStateFor(ContainerRuntime containerRuntime) { */ public enum ContainerRuntime { DOCKER, - PODMAN; + PODMAN, + UNAVAILABLE; - private final boolean rootless; - - ContainerRuntime() { - this.rootless = getRootlessStateFor(this); - } + private Boolean rootless; public String getExecutableName() { return this.name().toLowerCase(); } public boolean isRootless() { + if (rootless != null) { + return rootless; + } else { + if (this != ContainerRuntime.UNAVAILABLE) { + rootless = getRootlessStateFor(this); + } else { + throw new IllegalStateException("No container runtime was found. " + + "Make sure you have either Docker or Podman installed in your environment."); + } + } return rootless; } } From 204c573d954417909ddccdda96c061e81f28eae1 Mon Sep 17 00:00:00 2001 From: Michal Karm Babacek Date: Tue, 14 Mar 2023 00:56:56 +0100 Subject: [PATCH 06/27] Fixes quarkus:dev for env without any docker/podman (cherry picked from commit 6678ea544e22de16f9dd02d7e258101b24e1f4f0) --- .../deployment/pkg/steps/AppCDSBuildStep.java | 9 +++++++-- .../NativeImageBuildContainerRunnerTest.java | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java index 7d9efb860968c..8e7352ba8184f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java @@ -29,6 +29,7 @@ import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; import io.quarkus.deployment.steps.MainClassBuildStep; import io.quarkus.runtime.LaunchMode; +import io.quarkus.runtime.util.ContainerRuntimeUtil; import io.quarkus.utilities.JavaBinFinder; public class AppCDSBuildStep { @@ -38,7 +39,7 @@ public class AppCDSBuildStep { public static final String CLASSES_LIST_FILE_NAME = "classes.lst"; private static final String CONTAINER_IMAGE_BASE_BUILD_DIR = "/tmp/quarkus"; private static final String CONTAINER_IMAGE_APPCDS_DIR = CONTAINER_IMAGE_BASE_BUILD_DIR + "/appcds"; - public static final String DOCKER_EXECUTABLE = detectContainerRuntime().getExecutableName(); + public static final ContainerRuntimeUtil.ContainerRuntime CONTAINER_RUNTIME = detectContainerRuntime(false); @BuildStep(onlyIf = AppCDSRequired.class) public void requested(OutputTargetBuildItem outputTarget, BuildProducer producer) @@ -203,8 +204,12 @@ private Path createClassesList(JarBuildItem jarResult, // generate the classes file on the host private List dockerRunCommands(OutputTargetBuildItem outputTarget, String containerImage, String containerWorkingDir) { + if (CONTAINER_RUNTIME == ContainerRuntimeUtil.ContainerRuntime.UNAVAILABLE) { + throw new IllegalStateException("No container runtime was found. " + + "Make sure you have either Docker or Podman installed in your environment."); + } List command = new ArrayList<>(10); - command.add(DOCKER_EXECUTABLE); + command.add(CONTAINER_RUNTIME.getExecutableName()); command.add("run"); command.add("-v"); command.add(outputTarget.getOutputDirectory().toAbsolutePath().toString() + ":" + CONTAINER_IMAGE_BASE_BUILD_DIR diff --git a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java index 3d3bd861d108b..4fc2f9698b783 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java @@ -1,6 +1,6 @@ package io.quarkus.deployment.pkg.steps; -import static io.quarkus.deployment.pkg.steps.NativeImageBuildLocalContainerRunner.DOCKER_EXECUTABLE; +import static io.quarkus.deployment.pkg.steps.AppCDSBuildStep.CONTAINER_RUNTIME; import static org.assertj.core.api.Assertions.assertThat; import java.nio.file.Path; @@ -11,6 +11,7 @@ import org.junit.jupiter.api.condition.DisabledIfSystemProperty; import io.quarkus.deployment.pkg.NativeConfig; +import io.quarkus.runtime.util.ContainerRuntimeUtil; class NativeImageBuildContainerRunnerTest { @@ -18,6 +19,10 @@ class NativeImageBuildContainerRunnerTest { @DisabledIfSystemProperty(named = "avoid-containers", matches = "true") @Test void testBuilderImageBeingPickedUp() { + if (CONTAINER_RUNTIME == ContainerRuntimeUtil.ContainerRuntime.UNAVAILABLE) { + throw new IllegalStateException("No container runtime was found. " + + "Make sure you have either Docker or Podman installed in your environment."); + } NativeConfig nativeConfig = new NativeConfig(); nativeConfig.containerRuntime = Optional.empty(); boolean found; @@ -26,7 +31,8 @@ void testBuilderImageBeingPickedUp() { nativeConfig.builderImage = "graalvm"; localRunner = new NativeImageBuildLocalContainerRunner(nativeConfig, Path.of("/tmp")); - command = localRunner.buildCommand(DOCKER_EXECUTABLE, Collections.emptyList(), Collections.emptyList()); + command = localRunner.buildCommand(CONTAINER_RUNTIME.getExecutableName(), Collections.emptyList(), + Collections.emptyList()); found = false; for (String part : command) { if (part.contains("ubi-quarkus-graalvmce-builder-image")) { @@ -38,7 +44,8 @@ void testBuilderImageBeingPickedUp() { nativeConfig.builderImage = "mandrel"; localRunner = new NativeImageBuildLocalContainerRunner(nativeConfig, Path.of("/tmp")); - command = localRunner.buildCommand(DOCKER_EXECUTABLE, Collections.emptyList(), Collections.emptyList()); + command = localRunner.buildCommand(CONTAINER_RUNTIME.getExecutableName(), Collections.emptyList(), + Collections.emptyList()); found = false; for (String part : command) { if (part.contains("ubi-quarkus-mandrel-builder-image")) { @@ -50,7 +57,8 @@ void testBuilderImageBeingPickedUp() { nativeConfig.builderImage = "RandomString"; localRunner = new NativeImageBuildLocalContainerRunner(nativeConfig, Path.of("/tmp")); - command = localRunner.buildCommand(DOCKER_EXECUTABLE, Collections.emptyList(), Collections.emptyList()); + command = localRunner.buildCommand(CONTAINER_RUNTIME.getExecutableName(), Collections.emptyList(), + Collections.emptyList()); found = false; for (String part : command) { if (part.equals("RandomString")) { From d76fed8c1c81e58d8fb60be2378ac3c7ccca2d8c Mon Sep 17 00:00:00 2001 From: Michal Karm Babacek Date: Wed, 15 Mar 2023 11:06:49 +0100 Subject: [PATCH 07/27] Swaps container runtime caching from file to sys prop (cherry picked from commit 4c449e00d247defaa223c3d02109e12cc0228127) --- .../runtime/util/ContainerRuntimeUtil.java | 58 ++++++------------- 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java index 9ac79ff44719a..834bf99476005 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java @@ -5,9 +5,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -21,16 +18,12 @@ public final class ContainerRuntimeUtil { private static final Logger log = Logger.getLogger(ContainerRuntimeUtil.class); private static final String DOCKER_EXECUTABLE = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class) .getOptionalValue("quarkus.native.container-runtime", String.class).orElse(null); - /* - * Caching the value in a file helps us only as much as one JVM execution is concerned. - * Test suite's pom.xml sets things like -Djava.io.tmpdir="${project.build.directory}", - * so the file could appear in /tmp/ or C:\Users\karm\AppData\Local\Temp\ or in fact in - * quarkus/integration-tests/something/target. - * There is no point in reaching it in `Path.of(Paths.get("").toAbsolutePath().toString(), "target", - * "quarkus_container_runtime.txt")` - * as the file is deleted between JVM executions anyway. + + /** + * Static variable is not used because the class gets loaded by different classloaders at + * runtime and the container runtime would be detected again and again unnecessarily. */ - static final Path CONTAINER_RUNTIME = Path.of(System.getProperty("java.io.tmpdir"), "quarkus_container_runtime.txt"); + private static final String CONTAINER_RUNTIME_SYS_PROP = "quarkus-local-container-runtime"; private ContainerRuntimeUtil() { } @@ -94,39 +87,24 @@ public static ContainerRuntime detectContainerRuntime(boolean required) { } private static ContainerRuntime loadConfig() { - try { - if (Files.isReadable(CONTAINER_RUNTIME)) { - final String runtime = Files.readString(CONTAINER_RUNTIME, StandardCharsets.UTF_8); - if (ContainerRuntime.DOCKER.name().equalsIgnoreCase(runtime)) { - return ContainerRuntime.DOCKER; - } else if (ContainerRuntime.PODMAN.name().equalsIgnoreCase(runtime)) { - return ContainerRuntime.PODMAN; - } else if (ContainerRuntime.UNAVAILABLE.name().equalsIgnoreCase(runtime)) { - return ContainerRuntime.UNAVAILABLE; - } else { - log.warnf("The file %s contains an unknown value %s. Ignoring it.", - CONTAINER_RUNTIME.toAbsolutePath(), runtime); - return null; - } - } else { - return null; - } - } catch (IOException e) { - log.warnf("Error reading file %s. Ignoring it. See: %s", - CONTAINER_RUNTIME.toAbsolutePath(), e); + final String runtime = System.getProperty(CONTAINER_RUNTIME_SYS_PROP); + if (runtime == null) { + return null; + } else if (ContainerRuntime.DOCKER.name().equalsIgnoreCase(runtime)) { + return ContainerRuntime.DOCKER; + } else if (ContainerRuntime.PODMAN.name().equalsIgnoreCase(runtime)) { + return ContainerRuntime.PODMAN; + } else if (ContainerRuntime.UNAVAILABLE.name().equalsIgnoreCase(runtime)) { + return ContainerRuntime.UNAVAILABLE; + } else { + log.warnf("System property %s contains an unknown value %s. Ignoring it.", + CONTAINER_RUNTIME_SYS_PROP, runtime); return null; } } private static void storeConfig(ContainerRuntime containerRuntime) { - try { - Files.writeString(CONTAINER_RUNTIME, containerRuntime.name(), StandardCharsets.UTF_8, - StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - CONTAINER_RUNTIME.toFile().deleteOnExit(); - } catch (IOException e) { - log.warnf("Error writing to file %s. Ignoring it. See: %s", - CONTAINER_RUNTIME.toAbsolutePath(), e); - } + System.setProperty(CONTAINER_RUNTIME_SYS_PROP, containerRuntime.name()); } private static String getVersionOutputFor(ContainerRuntime containerRuntime) { From 9709bdcc48996aafbbb1c3b47e8b342c9142b34a Mon Sep 17 00:00:00 2001 From: Michal Karm Babacek Date: Wed, 15 Mar 2023 15:45:35 +0100 Subject: [PATCH 08/27] Remove superflous calls to podman/docker on podman only/docker only box (cherry picked from commit f8c61cf409cea59977793b373e63f17b1864cde1) --- .../runtime/util/ContainerRuntimeUtil.java | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java index 834bf99476005..1a44a0c6ff4d6 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java @@ -45,43 +45,55 @@ public static ContainerRuntime detectContainerRuntime(boolean required) { return containerRuntime; } else { // Docker version 19.03.14, build 5eb3275d40 - final String dockerVersionOutput = getVersionOutputFor(ContainerRuntime.DOCKER); - boolean dockerAvailable = dockerVersionOutput.contains("Docker version"); + String dockerVersionOutput; + boolean dockerAvailable; // Check if Podman is installed // podman version 2.1.1 - final String podmanVersionOutput = getVersionOutputFor(ContainerRuntime.PODMAN); - boolean podmanAvailable = podmanVersionOutput.startsWith("podman version"); + String podmanVersionOutput; + boolean podmanAvailable; if (DOCKER_EXECUTABLE != null) { - if (DOCKER_EXECUTABLE.trim().equalsIgnoreCase("docker") && dockerAvailable) { - storeConfig(ContainerRuntime.DOCKER); - return ContainerRuntime.DOCKER; - } else if (DOCKER_EXECUTABLE.trim().equalsIgnoreCase("podman") && podmanAvailable) { - storeConfig(ContainerRuntime.PODMAN); - return ContainerRuntime.PODMAN; - } else { - log.warn("quarkus.native.container-runtime config property must be set to either podman or docker " + - "and the executable must be available. Ignoring it."); + if (DOCKER_EXECUTABLE.trim().equalsIgnoreCase("docker")) { + dockerVersionOutput = getVersionOutputFor(ContainerRuntime.DOCKER); + dockerAvailable = dockerVersionOutput.contains("Docker version"); + if (dockerAvailable) { + storeConfig(ContainerRuntime.DOCKER); + return ContainerRuntime.DOCKER; + } + } + if (DOCKER_EXECUTABLE.trim().equalsIgnoreCase("podman")) { + podmanVersionOutput = getVersionOutputFor(ContainerRuntime.PODMAN); + podmanAvailable = podmanVersionOutput.startsWith("podman version"); + if (podmanAvailable) { + storeConfig(ContainerRuntime.PODMAN); + return ContainerRuntime.PODMAN; + } } + log.warn("quarkus.native.container-runtime config property must be set to either podman or docker " + + "and the executable must be available. Ignoring it."); } + dockerVersionOutput = getVersionOutputFor(ContainerRuntime.DOCKER); + dockerAvailable = dockerVersionOutput.contains("Docker version"); if (dockerAvailable) { // Check if "docker" is an alias to "podman" - if (dockerVersionOutput.equals(podmanVersionOutput)) { + if (dockerVersionOutput.startsWith("podman version")) { storeConfig(ContainerRuntime.PODMAN); return ContainerRuntime.PODMAN; } storeConfig(ContainerRuntime.DOCKER); return ContainerRuntime.DOCKER; - } else if (podmanAvailable) { + } + podmanVersionOutput = getVersionOutputFor(ContainerRuntime.PODMAN); + podmanAvailable = podmanVersionOutput.startsWith("podman version"); + if (podmanAvailable) { storeConfig(ContainerRuntime.PODMAN); return ContainerRuntime.PODMAN; + } + if (required) { + throw new IllegalStateException("No container runtime was found. " + + "Make sure you have either Docker or Podman installed in your environment."); } else { - if (required) { - throw new IllegalStateException("No container runtime was found. " - + "Make sure you have either Docker or Podman installed in your environment."); - } else { - storeConfig(ContainerRuntime.UNAVAILABLE); - return ContainerRuntime.UNAVAILABLE; - } + storeConfig(ContainerRuntime.UNAVAILABLE); + return ContainerRuntime.UNAVAILABLE; } } } From 7b82dca4875a1798456d676420c34cc7decb00b6 Mon Sep 17 00:00:00 2001 From: Michal Karm Babacek Date: Wed, 15 Mar 2023 15:55:58 +0100 Subject: [PATCH 09/27] Similar to AppCDSBuildStep, IntegrationTestUtil needs to handle a situation\nwhere there is no container runtime working and none is needed. (cherry picked from commit 781eb53aef05b5fc2138f5c8b660d8c447dde755) --- .../quarkus/test/junit/IntegrationTestUtil.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java index 3c74d966feb80..782574638f5f3 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java @@ -52,6 +52,7 @@ import io.quarkus.paths.PathList; import io.quarkus.runtime.configuration.ProfileManager; import io.quarkus.runtime.logging.LoggingSetupRecorder; +import io.quarkus.runtime.util.ContainerRuntimeUtil; import io.quarkus.test.common.ArtifactLauncher; import io.quarkus.test.common.LauncherUtil; import io.quarkus.test.common.PathTestHelper; @@ -64,7 +65,7 @@ public final class IntegrationTestUtil { public static final int DEFAULT_PORT = 8081; public static final int DEFAULT_HTTPS_PORT = 8444; public static final long DEFAULT_WAIT_TIME_SECONDS = 60; - private static final String DOCKER_BINARY = detectContainerRuntime().getExecutableName(); + public static final ContainerRuntimeUtil.ContainerRuntime CONTAINER_RUNTIME = detectContainerRuntime(false); private IntegrationTestUtil() { } @@ -336,9 +337,15 @@ public void accept(String s, String s2) { private static void createNetworkIfNecessary( final ArtifactLauncher.InitContext.DevServicesLaunchResult devServicesLaunchResult) { if (devServicesLaunchResult.manageNetwork() && (devServicesLaunchResult.networkId() != null)) { + if (CONTAINER_RUNTIME == ContainerRuntimeUtil.ContainerRuntime.UNAVAILABLE) { + throw new IllegalStateException("No container runtime was found. " + + "Make sure you have either Docker or Podman installed in your environment."); + } try { int networkCreateResult = new ProcessBuilder().redirectError(DISCARD).redirectOutput(DISCARD) - .command(DOCKER_BINARY, "network", "create", devServicesLaunchResult.networkId()).start().waitFor(); + .command(CONTAINER_RUNTIME.getExecutableName(), "network", "create", + devServicesLaunchResult.networkId()) + .start().waitFor(); if (networkCreateResult > 0) { throw new RuntimeException("Creating container network '" + devServicesLaunchResult.networkId() + "' completed unsuccessfully"); @@ -349,7 +356,9 @@ private static void createNetworkIfNecessary( public void run() { try { new ProcessBuilder().redirectError(DISCARD).redirectOutput(DISCARD) - .command(DOCKER_BINARY, "network", "rm", devServicesLaunchResult.networkId()).start() + .command(CONTAINER_RUNTIME.getExecutableName(), "network", "rm", + devServicesLaunchResult.networkId()) + .start() .waitFor(); } catch (InterruptedException | IOException ignored) { System.out.println( From 9c15faf0f531ad97f0152577e6fef6c35bcb5d2d Mon Sep 17 00:00:00 2001 From: Michal Karm Babacek Date: Wed, 15 Mar 2023 23:33:07 +0100 Subject: [PATCH 10/27] Container Image Docker: uses Core autodetection too (cherry picked from commit ea1728b15edc47f0ba9fabb2f4b7a9713e3aa5f7) --- .../runtime/util/ContainerRuntimeUtil.java | 19 ++++++--- .../image/docker/deployment/DockerConfig.java | 5 ++- .../docker/deployment/DockerProcessor.java | 42 +++++++++++-------- 3 files changed, 41 insertions(+), 25 deletions(-) diff --git a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java index 1a44a0c6ff4d6..1e6cff3a46c07 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java @@ -5,6 +5,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -16,7 +17,7 @@ public final class ContainerRuntimeUtil { private static final Logger log = Logger.getLogger(ContainerRuntimeUtil.class); - private static final String DOCKER_EXECUTABLE = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class) + private static final String CONTAINER_EXECUTABLE = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class) .getOptionalValue("quarkus.native.container-runtime", String.class).orElse(null); /** @@ -51,8 +52,8 @@ public static ContainerRuntime detectContainerRuntime(boolean required) { // podman version 2.1.1 String podmanVersionOutput; boolean podmanAvailable; - if (DOCKER_EXECUTABLE != null) { - if (DOCKER_EXECUTABLE.trim().equalsIgnoreCase("docker")) { + if (CONTAINER_EXECUTABLE != null) { + if (CONTAINER_EXECUTABLE.trim().equalsIgnoreCase("docker")) { dockerVersionOutput = getVersionOutputFor(ContainerRuntime.DOCKER); dockerAvailable = dockerVersionOutput.contains("Docker version"); if (dockerAvailable) { @@ -60,7 +61,7 @@ public static ContainerRuntime detectContainerRuntime(boolean required) { return ContainerRuntime.DOCKER; } } - if (DOCKER_EXECUTABLE.trim().equalsIgnoreCase("podman")) { + if (CONTAINER_EXECUTABLE.trim().equalsIgnoreCase("podman")) { podmanVersionOutput = getVersionOutputFor(ContainerRuntime.PODMAN); podmanAvailable = podmanVersionOutput.startsWith("podman version"); if (podmanAvailable) { @@ -125,8 +126,14 @@ private static String getVersionOutputFor(ContainerRuntime containerRuntime) { final ProcessBuilder pb = new ProcessBuilder(containerRuntime.getExecutableName(), "--version") .redirectErrorStream(true); versionProcess = pb.start(); - versionProcess.waitFor(); - return new String(versionProcess.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + final int timeoutS = 10; + if (versionProcess.waitFor(timeoutS, TimeUnit.SECONDS)) { + return new String(versionProcess.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + } else { + log.debugf("Failure. It took command %s more than %d seconds to execute.", containerRuntime.getExecutableName(), + timeoutS); + return ""; + } } 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()); diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerConfig.java b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerConfig.java index 99d5420a51d92..6e10784ef2a04 100644 --- a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerConfig.java +++ b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerConfig.java @@ -50,9 +50,10 @@ public class DockerConfig { /** * Name of binary used to execute the docker commands. + * This setting can override the global container runtime detection. */ - @ConfigItem(defaultValue = "docker") - public String executableName; + @ConfigItem + public Optional executableName; /** * Configuration for Docker Buildx options 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 f6a5d79ab181d..2ede3959d21f8 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 @@ -4,6 +4,7 @@ import static io.quarkus.container.image.deployment.util.EnablementUtil.buildContainerImageNeeded; import static io.quarkus.container.image.deployment.util.EnablementUtil.pushContainerImageNeeded; import static io.quarkus.container.util.PathsUtil.findMainSourcesRoot; +import static io.quarkus.runtime.util.ContainerRuntimeUtil.detectContainerRuntime; import java.io.BufferedReader; import java.io.IOException; @@ -87,9 +88,10 @@ 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()); + DockerfilePaths dockerfilePaths = getDockerfilePaths(dockerConfig, false, packageConfig, out); + DockerFileBaseInformationProvider dockerFileBaseInformationProvider = DockerFileBaseInformationProvider.impl(); + Optional dockerFileBaseInformation = dockerFileBaseInformationProvider + .determine(dockerfilePaths.getDockerfilePath()); if ((compiledJavaVersion.getJavaVersion().isJava17OrHigher() == CompiledJavaVersionBuildItem.JavaVersion.Status.TRUE) && dockerFileBaseInformation.isPresent() && (dockerFileBaseInformation.get().getJavaVersion() < 17)) { @@ -168,11 +170,11 @@ private String createContainerImage(ContainerImageConfig containerImageConfig, D boolean pushContainerImage, PackageConfig packageConfig) { - var useBuildx = dockerConfig.buildx.useBuildx(); + boolean useBuildx = dockerConfig.buildx.useBuildx(); - // useBuildx: Whether or not any of the buildx parameters are set + // useBuildx: Whether any of the buildx parameters are set // - // pushImages: Whether or not the user requested the built images to be pushed to a registry + // pushImages: Whether the user requested the built images to be pushed to a registry // Pushing images is different based on if you're using buildx or not. // If not using any of the buildx params (useBuildx == false), then the flow is as it was before: // @@ -196,9 +198,10 @@ private String createContainerImage(ContainerImageConfig containerImageConfig, D } if (buildContainerImage) { - log.infof("Executing the following command to build docker image: '%s %s'", dockerConfig.executableName, + final String executableName = dockerConfig.executableName.orElse(detectContainerRuntime(true).getExecutableName()); + log.infof("Executing the following command to build docker image: '%s %s'", executableName, String.join(" ", dockerArgs)); - boolean buildSuccessful = ExecUtil.exec(out.getOutputDirectory().toFile(), reader, dockerConfig.executableName, + boolean buildSuccessful = ExecUtil.exec(out.getOutputDirectory().toFile(), reader, executableName, dockerArgs); if (!buildSuccessful) { throw dockerException(dockerArgs); @@ -234,7 +237,7 @@ private String createContainerImage(ContainerImageConfig containerImageConfig, D private void loginToRegistryIfNeeded(ContainerImageConfig containerImageConfig, ContainerImageInfoBuildItem containerImageInfo, DockerConfig dockerConfig) { - var registry = containerImageInfo.getRegistry() + String registry = containerImageInfo.getRegistry() .orElseGet(() -> { log.info("No container image registry was set, so 'docker.io' will be used"); return "docker.io"; @@ -242,7 +245,8 @@ private void loginToRegistryIfNeeded(ContainerImageConfig containerImageConfig, // Check if we need to login first if (containerImageConfig.username.isPresent() && containerImageConfig.password.isPresent()) { - boolean loginSuccessful = ExecUtil.exec(dockerConfig.executableName, "login", registry, "-u", + final String executableName = dockerConfig.executableName.orElse(detectContainerRuntime(true).getExecutableName()); + boolean loginSuccessful = ExecUtil.exec(executableName, "login", registry, "-u", containerImageConfig.username.get(), "-p" + containerImageConfig.password.get()); if (!loginSuccessful) { @@ -254,15 +258,17 @@ private void loginToRegistryIfNeeded(ContainerImageConfig containerImageConfig, private String[] getDockerArgs(String image, DockerfilePaths dockerfilePaths, ContainerImageConfig containerImageConfig, DockerConfig dockerConfig, ContainerImageInfoBuildItem containerImageInfo, boolean pushImages) { List dockerArgs = new ArrayList<>(6 + dockerConfig.buildArgs.size()); - var useBuildx = dockerConfig.buildx.useBuildx(); + boolean useBuildx = dockerConfig.buildx.useBuildx(); if (useBuildx) { + final String executableName = dockerConfig.executableName.orElse(detectContainerRuntime(true).getExecutableName()); // Check the executable. If not 'docker', then fail the build - if (!DOCKER.equals(dockerConfig.executableName)) { + if (!DOCKER.equals(executableName)) { throw new IllegalArgumentException( String.format( - "The 'buildx' properties are specific to 'executable-name=docker' and can not be used with the '%s' executable name. Either remove the `buildx` properties or the `executable-name` property.", - dockerConfig.executableName)); + "The 'buildx' properties are specific to 'executable-name=docker' and can not be used with " + + "the '%s' executable name. Either remove the `buildx` properties or the `executable-name` property.", + executableName)); } dockerArgs.add("buildx"); @@ -315,9 +321,10 @@ private String[] getDockerArgs(String image, DockerfilePaths dockerfilePaths, Co } private void createAdditionalTags(String image, List additionalImageTags, DockerConfig dockerConfig) { + final String executableName = dockerConfig.executableName.orElse(detectContainerRuntime(true).getExecutableName()); for (String additionalTag : additionalImageTags) { String[] tagArgs = { "tag", image, additionalTag }; - boolean tagSuccessful = ExecUtil.exec(dockerConfig.executableName, tagArgs); + boolean tagSuccessful = ExecUtil.exec(executableName, tagArgs); if (!tagSuccessful) { throw dockerException(tagArgs); } @@ -325,8 +332,9 @@ private void createAdditionalTags(String image, List additionalImageTags } private void pushImage(String image, DockerConfig dockerConfig) { + final String executableName = dockerConfig.executableName.orElse(detectContainerRuntime(true).getExecutableName()); String[] pushArgs = { "push", image }; - boolean pushSuccessful = ExecUtil.exec(dockerConfig.executableName, pushArgs); + boolean pushSuccessful = ExecUtil.exec(executableName, pushArgs); if (!pushSuccessful) { throw dockerException(pushArgs); } @@ -464,7 +472,7 @@ public static ProvidedDockerfile get(Path dockerfilePath, Path outputDirectory) : mainSourcesRoot.getValue().resolve(dockerfilePath); if (!effectiveDockerfilePath.toFile().exists()) { throw new IllegalArgumentException( - "Specified Dockerfile path " + effectiveDockerfilePath.toAbsolutePath().toString() + " does not exist"); + "Specified Dockerfile path " + effectiveDockerfilePath.toAbsolutePath() + " does not exist"); } return new ProvidedDockerfile( effectiveDockerfilePath, From f97f5cc9b961494d21c0916e03d268e7fc2bb10b Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Thu, 16 Mar 2023 14:26:13 +0100 Subject: [PATCH 11/27] Various adjustments for the container detection (cherry picked from commit 38d3c0d228b7ef3f28a620b171c16b3e961df91a) --- .../deployment/pkg/steps/AppCDSBuildStep.java | 11 ++-- .../NativeImageBuildContainerRunnerTest.java | 13 ++--- .../runtime/util/ContainerRuntimeUtil.java | 54 ++++++++++++------- .../docker/deployment/DockerProcessor.java | 14 ++--- .../test/junit/IntegrationTestUtil.java | 11 ++-- 5 files changed, 56 insertions(+), 47 deletions(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java index 8e7352ba8184f..49d64e6e45251 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java @@ -29,7 +29,7 @@ import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; import io.quarkus.deployment.steps.MainClassBuildStep; import io.quarkus.runtime.LaunchMode; -import io.quarkus.runtime.util.ContainerRuntimeUtil; +import io.quarkus.runtime.util.ContainerRuntimeUtil.ContainerRuntime; import io.quarkus.utilities.JavaBinFinder; public class AppCDSBuildStep { @@ -39,7 +39,6 @@ public class AppCDSBuildStep { public static final String CLASSES_LIST_FILE_NAME = "classes.lst"; private static final String CONTAINER_IMAGE_BASE_BUILD_DIR = "/tmp/quarkus"; private static final String CONTAINER_IMAGE_APPCDS_DIR = CONTAINER_IMAGE_BASE_BUILD_DIR + "/appcds"; - public static final ContainerRuntimeUtil.ContainerRuntime CONTAINER_RUNTIME = detectContainerRuntime(false); @BuildStep(onlyIf = AppCDSRequired.class) public void requested(OutputTargetBuildItem outputTarget, BuildProducer producer) @@ -204,12 +203,10 @@ private Path createClassesList(JarBuildItem jarResult, // generate the classes file on the host private List dockerRunCommands(OutputTargetBuildItem outputTarget, String containerImage, String containerWorkingDir) { - if (CONTAINER_RUNTIME == ContainerRuntimeUtil.ContainerRuntime.UNAVAILABLE) { - throw new IllegalStateException("No container runtime was found. " - + "Make sure you have either Docker or Podman installed in your environment."); - } + ContainerRuntime containerRuntime = detectContainerRuntime(true); + List command = new ArrayList<>(10); - command.add(CONTAINER_RUNTIME.getExecutableName()); + command.add(containerRuntime.getExecutableName()); command.add("run"); command.add("-v"); command.add(outputTarget.getOutputDirectory().toAbsolutePath().toString() + ":" + CONTAINER_IMAGE_BASE_BUILD_DIR diff --git a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java index 4fc2f9698b783..498130d4b71ac 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunnerTest.java @@ -1,6 +1,5 @@ package io.quarkus.deployment.pkg.steps; -import static io.quarkus.deployment.pkg.steps.AppCDSBuildStep.CONTAINER_RUNTIME; import static org.assertj.core.api.Assertions.assertThat; import java.nio.file.Path; @@ -19,10 +18,8 @@ class NativeImageBuildContainerRunnerTest { @DisabledIfSystemProperty(named = "avoid-containers", matches = "true") @Test void testBuilderImageBeingPickedUp() { - if (CONTAINER_RUNTIME == ContainerRuntimeUtil.ContainerRuntime.UNAVAILABLE) { - throw new IllegalStateException("No container runtime was found. " - + "Make sure you have either Docker or Podman installed in your environment."); - } + ContainerRuntimeUtil.ContainerRuntime containerRuntime = ContainerRuntimeUtil.detectContainerRuntime(true); + NativeConfig nativeConfig = new NativeConfig(); nativeConfig.containerRuntime = Optional.empty(); boolean found; @@ -31,7 +28,7 @@ void testBuilderImageBeingPickedUp() { nativeConfig.builderImage = "graalvm"; localRunner = new NativeImageBuildLocalContainerRunner(nativeConfig, Path.of("/tmp")); - command = localRunner.buildCommand(CONTAINER_RUNTIME.getExecutableName(), Collections.emptyList(), + command = localRunner.buildCommand(containerRuntime.getExecutableName(), Collections.emptyList(), Collections.emptyList()); found = false; for (String part : command) { @@ -44,7 +41,7 @@ void testBuilderImageBeingPickedUp() { nativeConfig.builderImage = "mandrel"; localRunner = new NativeImageBuildLocalContainerRunner(nativeConfig, Path.of("/tmp")); - command = localRunner.buildCommand(CONTAINER_RUNTIME.getExecutableName(), Collections.emptyList(), + command = localRunner.buildCommand(containerRuntime.getExecutableName(), Collections.emptyList(), Collections.emptyList()); found = false; for (String part : command) { @@ -57,7 +54,7 @@ void testBuilderImageBeingPickedUp() { nativeConfig.builderImage = "RandomString"; localRunner = new NativeImageBuildLocalContainerRunner(nativeConfig, Path.of("/tmp")); - command = localRunner.buildCommand(CONTAINER_RUNTIME.getExecutableName(), Collections.emptyList(), + command = localRunner.buildCommand(containerRuntime.getExecutableName(), Collections.emptyList(), Collections.emptyList()); found = false; for (String part : command) { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java index 1e6cff3a46c07..2be3cbf76bdaa 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java @@ -41,7 +41,7 @@ public static ContainerRuntime detectContainerRuntime() { } public static ContainerRuntime detectContainerRuntime(boolean required) { - final ContainerRuntime containerRuntime = loadConfig(); + final ContainerRuntime containerRuntime = loadContainerRuntimeFromSystemProperty(); if (containerRuntime != null) { return containerRuntime; } else { @@ -57,7 +57,7 @@ public static ContainerRuntime detectContainerRuntime(boolean required) { dockerVersionOutput = getVersionOutputFor(ContainerRuntime.DOCKER); dockerAvailable = dockerVersionOutput.contains("Docker version"); if (dockerAvailable) { - storeConfig(ContainerRuntime.DOCKER); + storeContainerRuntimeInSystemProperty(ContainerRuntime.DOCKER); return ContainerRuntime.DOCKER; } } @@ -65,7 +65,7 @@ public static ContainerRuntime detectContainerRuntime(boolean required) { podmanVersionOutput = getVersionOutputFor(ContainerRuntime.PODMAN); podmanAvailable = podmanVersionOutput.startsWith("podman version"); if (podmanAvailable) { - storeConfig(ContainerRuntime.PODMAN); + storeContainerRuntimeInSystemProperty(ContainerRuntime.PODMAN); return ContainerRuntime.PODMAN; } } @@ -77,46 +77,48 @@ public static ContainerRuntime detectContainerRuntime(boolean required) { if (dockerAvailable) { // Check if "docker" is an alias to "podman" if (dockerVersionOutput.startsWith("podman version")) { - storeConfig(ContainerRuntime.PODMAN); + storeContainerRuntimeInSystemProperty(ContainerRuntime.PODMAN); return ContainerRuntime.PODMAN; } - storeConfig(ContainerRuntime.DOCKER); + storeContainerRuntimeInSystemProperty(ContainerRuntime.DOCKER); return ContainerRuntime.DOCKER; } podmanVersionOutput = getVersionOutputFor(ContainerRuntime.PODMAN); podmanAvailable = podmanVersionOutput.startsWith("podman version"); if (podmanAvailable) { - storeConfig(ContainerRuntime.PODMAN); + storeContainerRuntimeInSystemProperty(ContainerRuntime.PODMAN); return ContainerRuntime.PODMAN; } + + storeContainerRuntimeInSystemProperty(ContainerRuntime.UNAVAILABLE); + if (required) { throw new IllegalStateException("No container runtime was found. " + "Make sure you have either Docker or Podman installed in your environment."); - } else { - storeConfig(ContainerRuntime.UNAVAILABLE); - return ContainerRuntime.UNAVAILABLE; } + + return ContainerRuntime.UNAVAILABLE; } } - private static ContainerRuntime loadConfig() { + private static ContainerRuntime loadContainerRuntimeFromSystemProperty() { final String runtime = System.getProperty(CONTAINER_RUNTIME_SYS_PROP); + if (runtime == null) { return null; - } else if (ContainerRuntime.DOCKER.name().equalsIgnoreCase(runtime)) { - return ContainerRuntime.DOCKER; - } else if (ContainerRuntime.PODMAN.name().equalsIgnoreCase(runtime)) { - return ContainerRuntime.PODMAN; - } else if (ContainerRuntime.UNAVAILABLE.name().equalsIgnoreCase(runtime)) { - return ContainerRuntime.UNAVAILABLE; - } else { + } + + ContainerRuntime containerRuntime = ContainerRuntime.valueOf(runtime); + + if (containerRuntime == null) { log.warnf("System property %s contains an unknown value %s. Ignoring it.", CONTAINER_RUNTIME_SYS_PROP, runtime); - return null; } + + return containerRuntime; } - private static void storeConfig(ContainerRuntime containerRuntime) { + private static void storeContainerRuntimeInSystemProperty(ContainerRuntime containerRuntime) { System.setProperty(CONTAINER_RUNTIME_SYS_PROP, containerRuntime.name()); } @@ -197,6 +199,10 @@ public enum ContainerRuntime { private Boolean rootless; public String getExecutableName() { + if (this == UNAVAILABLE) { + throw new IllegalStateException("Cannot get an executable name when no container runtime is available"); + } + return this.name().toLowerCase(); } @@ -213,5 +219,15 @@ public boolean isRootless() { } return rootless; } + + public static ContainerRuntime of(String value) { + for (ContainerRuntime containerRuntime : values()) { + if (containerRuntime.name().equalsIgnoreCase(value)) { + return containerRuntime; + } + } + + return null; + } } } 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 2ede3959d21f8..a3933d6d3faa1 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 @@ -204,7 +204,7 @@ private String createContainerImage(ContainerImageConfig containerImageConfig, D boolean buildSuccessful = ExecUtil.exec(out.getOutputDirectory().toFile(), reader, executableName, dockerArgs); if (!buildSuccessful) { - throw dockerException(dockerArgs); + throw dockerException(executableName, dockerArgs); } dockerConfig.buildx.platform @@ -250,7 +250,8 @@ private void loginToRegistryIfNeeded(ContainerImageConfig containerImageConfig, containerImageConfig.username.get(), "-p" + containerImageConfig.password.get()); if (!loginSuccessful) { - throw dockerException(new String[] { "-u", containerImageConfig.username.get(), "-p", "********" }); + throw dockerException(executableName, + new String[] { "-u", containerImageConfig.username.get(), "-p", "********" }); } } } @@ -326,7 +327,7 @@ private void createAdditionalTags(String image, List additionalImageTags String[] tagArgs = { "tag", image, additionalTag }; boolean tagSuccessful = ExecUtil.exec(executableName, tagArgs); if (!tagSuccessful) { - throw dockerException(tagArgs); + throw dockerException(executableName, tagArgs); } } } @@ -336,14 +337,15 @@ private void pushImage(String image, DockerConfig dockerConfig) { String[] pushArgs = { "push", image }; boolean pushSuccessful = ExecUtil.exec(executableName, pushArgs); if (!pushSuccessful) { - throw dockerException(pushArgs); + throw dockerException(executableName, pushArgs); } log.info("Successfully pushed docker image " + image); } - private RuntimeException dockerException(String[] dockerArgs) { + private RuntimeException dockerException(String executableName, String[] dockerArgs) { return new RuntimeException( - "Execution of 'docker " + String.join(" ", dockerArgs) + "' failed. See docker output for more details"); + "Execution of '" + executableName + " " + String.join(" ", dockerArgs) + + "' failed. See docker output for more details"); } private DockerfilePaths getDockerfilePaths(DockerConfig dockerConfig, boolean forNative, diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java index 782574638f5f3..645abb2bd3ebb 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java @@ -65,7 +65,6 @@ public final class IntegrationTestUtil { public static final int DEFAULT_PORT = 8081; public static final int DEFAULT_HTTPS_PORT = 8444; public static final long DEFAULT_WAIT_TIME_SECONDS = 60; - public static final ContainerRuntimeUtil.ContainerRuntime CONTAINER_RUNTIME = detectContainerRuntime(false); private IntegrationTestUtil() { } @@ -337,13 +336,11 @@ public void accept(String s, String s2) { private static void createNetworkIfNecessary( final ArtifactLauncher.InitContext.DevServicesLaunchResult devServicesLaunchResult) { if (devServicesLaunchResult.manageNetwork() && (devServicesLaunchResult.networkId() != null)) { - if (CONTAINER_RUNTIME == ContainerRuntimeUtil.ContainerRuntime.UNAVAILABLE) { - throw new IllegalStateException("No container runtime was found. " - + "Make sure you have either Docker or Podman installed in your environment."); - } + ContainerRuntimeUtil.ContainerRuntime containerRuntime = detectContainerRuntime(true); + try { int networkCreateResult = new ProcessBuilder().redirectError(DISCARD).redirectOutput(DISCARD) - .command(CONTAINER_RUNTIME.getExecutableName(), "network", "create", + .command(containerRuntime.getExecutableName(), "network", "create", devServicesLaunchResult.networkId()) .start().waitFor(); if (networkCreateResult > 0) { @@ -356,7 +353,7 @@ private static void createNetworkIfNecessary( public void run() { try { new ProcessBuilder().redirectError(DISCARD).redirectOutput(DISCARD) - .command(CONTAINER_RUNTIME.getExecutableName(), "network", "rm", + .command(containerRuntime.getExecutableName(), "network", "rm", devServicesLaunchResult.networkId()) .start() .waitFor(); From bd0cb267d6b580a59385c855dee473e8c04e000b Mon Sep 17 00:00:00 2001 From: Foivos Zakkak Date: Mon, 20 Mar 2023 19:55:51 +0200 Subject: [PATCH 12/27] Only substitute OctetKeyPair* classes when on the classpath Under certain configurations (e.g. in https://github.com/quarkusio/quarkus/issues/31930) the substitution fails with: ``` Error: Substitution target for io.quarkus.smallrye.jwt.build.runtime.graalvm.Target_org_jose4j_jwk_OctetKeyPairJsonWebKey is not loaded. Use field `onlyWith` in the `TargetClass` annotation to make substitution only active when needed. com.oracle.svm.core.util.UserError$UserException: Substitution target for io.quarkus.smallrye.jwt.build.runtime.graalvm.Target_org_jose4j_jwk_OctetKeyPairJsonWebKey is not loaded. Use field `onlyWith` in the `TargetClass` annotation to make substitution only active when needed. ``` which indicates that the class being substituted cannot be located on the classpath. (cherry picked from commit 1b143e9660c9cac7e5d0a3f76e43f9e232c0e9f5) --- .../build/runtime/graalvm/Substitutions.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/extensions/smallrye-jwt-build/runtime/src/main/java/io/quarkus/smallrye/jwt/build/runtime/graalvm/Substitutions.java b/extensions/smallrye-jwt-build/runtime/src/main/java/io/quarkus/smallrye/jwt/build/runtime/graalvm/Substitutions.java index 4d09817c95dc1..c7e868d0b0a74 100644 --- a/extensions/smallrye-jwt-build/runtime/src/main/java/io/quarkus/smallrye/jwt/build/runtime/graalvm/Substitutions.java +++ b/extensions/smallrye-jwt-build/runtime/src/main/java/io/quarkus/smallrye/jwt/build/runtime/graalvm/Substitutions.java @@ -5,25 +5,34 @@ import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; -@TargetClass(className = "org.jose4j.jwk.OctetKeyPairJsonWebKey", onlyWith = JavaVersionLessThan17.class) +@TargetClass(className = "org.jose4j.jwk.OctetKeyPairJsonWebKey", onlyWith = JavaVersionLessThan17andOctetKeyPairOnClasspath.class) final class Target_org_jose4j_jwk_OctetKeyPairJsonWebKey { @Substitute public Target_org_jose4j_jwk_OctetKeyPairJsonWebKey(java.security.PublicKey publicKey) { + throw new UnsupportedOperationException( + "OctetKeyPairJsonWebKey depends on EdECPrivateKeySpec which is not available in Java < 15"); } @Substitute Target_org_jose4j_jwk_OctetKeyPairUtil subtypeKeyUtil() { - return null; + throw new UnsupportedOperationException( + "OctetKeyPairJsonWebKey depends on EdECPrivateKeySpec which is not available in Java < 15"); } } -@TargetClass(className = "org.jose4j.keys.OctetKeyPairUtil", onlyWith = JavaVersionLessThan17.class) +@TargetClass(className = "org.jose4j.keys.OctetKeyPairUtil", onlyWith = JavaVersionLessThan17andOctetKeyPairOnClasspath.class) final class Target_org_jose4j_jwk_OctetKeyPairUtil { } -class JavaVersionLessThan17 implements BooleanSupplier { +class JavaVersionLessThan17andOctetKeyPairOnClasspath implements BooleanSupplier { @Override public boolean getAsBoolean() { - return Runtime.version().version().get(0) < 17; + try { + Class.forName("org.jose4j.jwk.OctetKeyPairJsonWebKey"); + Class.forName("org.jose4j.jwk.OctetKeyPairUtil"); + return Runtime.version().version().get(0) < 17; + } catch (ClassNotFoundException e) { + return false; + } } } From ce12d2db6530171787deb9145f833346ed8b629a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Mon, 20 Mar 2023 13:41:11 +0100 Subject: [PATCH 13/27] Remove quarkus.hibernate-orm.database.generation=drop-and-create from Hibernate ORM codestart 1. It's not necessary: if you're using dev services, this will be set implicitly in dev mode and test mode. 2. It's incorrect and dangerous: this setting should be applied to the dev and test profile explicitly, otherwise it will lead to dropping the whole database content in production... (cherry picked from commit 94029e3f21581889b38972276e60e1806c398738) --- .../base/src/main/resources/application.yml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/hibernate-orm-codestart/base/src/main/resources/application.yml diff --git a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/hibernate-orm-codestart/base/src/main/resources/application.yml b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/hibernate-orm-codestart/base/src/main/resources/application.yml deleted file mode 100644 index 0b80cb026e6bd..0000000000000 --- a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/hibernate-orm-codestart/base/src/main/resources/application.yml +++ /dev/null @@ -1,4 +0,0 @@ -quarkus: - hibernate-orm: - database: - generation: drop-and-create \ No newline at end of file From 9fec37779b6c3d29a7994be35e9a38e3ba625fcf Mon Sep 17 00:00:00 2001 From: Ozan Gunalp Date: Tue, 21 Mar 2023 16:50:25 +0000 Subject: [PATCH 14/27] Support repeatable Incomings annotation for reactive messaging - Assign the bean as unremovable - Kafka serde discovery Fixes #32002 (cherry picked from commit ab629723a8c067bfd0e47fbac6627fdf9435dde6) --- .../DefaultSerdeDiscoveryState.java | 7 +++++ ...allRyeReactiveMessagingKafkaProcessor.java | 4 +-- .../deployment/DefaultSerdeConfigTest.java | 27 ++++++++++++++++-- .../SmallRyeReactiveMessagingProcessor.java | 3 ++ .../it/kafka/KafkaRepeatableReceivers.java | 28 +++++++++++++++++++ .../io/quarkus/it/kafka/PricesProducer.java | 21 ++++++++++++++ .../src/main/resources/application.properties | 6 ++++ .../quarkus/it/kafka/KafkaConnectorTest.java | 9 ++++++ 8 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 integration-tests/reactive-messaging-kafka/src/main/java/io/quarkus/it/kafka/KafkaRepeatableReceivers.java create mode 100644 integration-tests/reactive-messaging-kafka/src/main/java/io/quarkus/it/kafka/PricesProducer.java diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeDiscoveryState.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeDiscoveryState.java index 8950e35fc578d..fe71061e93bac 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeDiscoveryState.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeDiscoveryState.java @@ -210,6 +210,13 @@ List findAnnotationsOnMethods(DotName annotation) { .collect(Collectors.toList()); } + List findRepeatableAnnotationsOnMethods(DotName annotation) { + return index.getAnnotationsWithRepeatable(annotation, index) + .stream() + .filter(it -> it.target().kind() == AnnotationTarget.Kind.METHOD) + .collect(Collectors.toList()); + } + List findAnnotationsOnInjectionPoints(DotName annotation) { return index.getAnnotations(annotation) .stream() diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java index 898774bf6d92e..e76ce08b458bf 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java @@ -163,7 +163,7 @@ public void defaultChannelConfiguration( if (launchMode.getLaunchMode().isDevOrTest()) { if (!runtimeConfig.enableGracefulShutdownInDevAndTestMode) { - List incomings = discoveryState.findAnnotationsOnMethods(DotNames.INCOMING); + List incomings = discoveryState.findRepeatableAnnotationsOnMethods(DotNames.INCOMING); List channels = discoveryState.findAnnotationsOnInjectionPoints(DotNames.CHANNEL); List annotations = new ArrayList<>(); annotations.addAll(incomings); @@ -190,7 +190,7 @@ void discoverDefaultSerdeConfig(DefaultSerdeDiscoveryState discovery, BuildProducer reflection) { Map alreadyGeneratedSerializers = new HashMap<>(); Map alreadyGeneratedDeserializers = new HashMap<>(); - for (AnnotationInstance annotation : discovery.findAnnotationsOnMethods(DotNames.INCOMING)) { + for (AnnotationInstance annotation : discovery.findRepeatableAnnotationsOnMethods(DotNames.INCOMING)) { String channelName = annotation.value().asString(); if (!discovery.isKafkaConnector(channelsManagedByConnectors, true, channelName)) { continue; diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java index ce568884642af..eda145afc68be 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -63,7 +64,9 @@ private static void doTest(Tuple[] expectations, Class... classesToIndex) { private static void doTest(Config customConfig, Tuple[] expectations, Class... classesToIndex) { List configs = new ArrayList<>(); - DefaultSerdeDiscoveryState discovery = new DefaultSerdeDiscoveryState(index(classesToIndex)) { + List> classes = new ArrayList<>(Arrays.asList(classesToIndex)); + classes.add(Incoming.class); + DefaultSerdeDiscoveryState discovery = new DefaultSerdeDiscoveryState(index(classes)) { @Override Config getConfig() { return customConfig != null ? customConfig : super.getConfig(); @@ -89,7 +92,7 @@ boolean isKafkaConnector(List list, boolean in } } - private static IndexView index(Class... classes) { + private static IndexView index(List> classes) { Indexer indexer = new Indexer(); for (Class clazz : classes) { try { @@ -2696,4 +2699,24 @@ private static class TransactionalProducer { } + @Test + void repeatableIncomings() { + Tuple[] expectations = { + tuple("mp.messaging.incoming.channel1.value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"), + tuple("mp.messaging.incoming.channel2.value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"), + }; + doTest(expectations, RepeatableIncomingsChannels.class); + } + + + private static class RepeatableIncomingsChannels { + + @Incoming("channel1") + @Incoming("channel2") + void method1(String msg) { + + } + } + + } diff --git a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/SmallRyeReactiveMessagingProcessor.java b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/SmallRyeReactiveMessagingProcessor.java index c329c7f8f7891..87b9d09139831 100644 --- a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/SmallRyeReactiveMessagingProcessor.java +++ b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/SmallRyeReactiveMessagingProcessor.java @@ -142,6 +142,9 @@ public List removalExclusions() { new UnremovableBeanBuildItem( new BeanClassAnnotationExclusion( ReactiveMessagingDotNames.INCOMING)), + new UnremovableBeanBuildItem( + new BeanClassAnnotationExclusion( + ReactiveMessagingDotNames.INCOMINGS)), new UnremovableBeanBuildItem( new BeanClassAnnotationExclusion( ReactiveMessagingDotNames.OUTGOING))); diff --git a/integration-tests/reactive-messaging-kafka/src/main/java/io/quarkus/it/kafka/KafkaRepeatableReceivers.java b/integration-tests/reactive-messaging-kafka/src/main/java/io/quarkus/it/kafka/KafkaRepeatableReceivers.java new file mode 100644 index 0000000000000..9ea81569e1b7b --- /dev/null +++ b/integration-tests/reactive-messaging-kafka/src/main/java/io/quarkus/it/kafka/KafkaRepeatableReceivers.java @@ -0,0 +1,28 @@ +package io.quarkus.it.kafka; + +import java.util.List; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; + +@ApplicationScoped +public class KafkaRepeatableReceivers { + + private final List prices = new CopyOnWriteArrayList<>(); + + @Incoming("prices-in") + @Incoming("prices-in2") + public CompletionStage consume(Message msg) { + prices.add(msg.getPayload()); + return msg.ack(); + } + + public List getPrices() { + return prices; + } + +} diff --git a/integration-tests/reactive-messaging-kafka/src/main/java/io/quarkus/it/kafka/PricesProducer.java b/integration-tests/reactive-messaging-kafka/src/main/java/io/quarkus/it/kafka/PricesProducer.java new file mode 100644 index 0000000000000..2b14aff36a449 --- /dev/null +++ b/integration-tests/reactive-messaging-kafka/src/main/java/io/quarkus/it/kafka/PricesProducer.java @@ -0,0 +1,21 @@ +package io.quarkus.it.kafka; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Outgoing; + +import io.smallrye.mutiny.Multi; + +@ApplicationScoped +public class PricesProducer { + + @Outgoing("prices-out") + public Multi generatePrices() { + return Multi.createFrom().items(1.2, 2.2, 3.4); + } + + @Outgoing("prices-out2") + public Multi generatePrices2() { + return Multi.createFrom().items(4.5, 5.6, 6.7); + } +} diff --git a/integration-tests/reactive-messaging-kafka/src/main/resources/application.properties b/integration-tests/reactive-messaging-kafka/src/main/resources/application.properties index 4a3e4b4dffe17..e1c86a9bd6319 100644 --- a/integration-tests/reactive-messaging-kafka/src/main/resources/application.properties +++ b/integration-tests/reactive-messaging-kafka/src/main/resources/application.properties @@ -25,3 +25,9 @@ mp.messaging.outgoing.pets-out.topic=pets mp.messaging.incoming.pets-in.topic=pets quarkus.redis.my-redis.hosts=${quarkus.redis.hosts} + +mp.messaging.outgoing.prices-out.topic=prices +mp.messaging.outgoing.prices-out2.topic=prices2 + +mp.messaging.incoming.prices-in.topic=prices +mp.messaging.incoming.prices-in2.topic=prices2 diff --git a/integration-tests/reactive-messaging-kafka/src/test/java/io/quarkus/it/kafka/KafkaConnectorTest.java b/integration-tests/reactive-messaging-kafka/src/test/java/io/quarkus/it/kafka/KafkaConnectorTest.java index 1acd4d3c45a77..b7e6e64b59dfa 100644 --- a/integration-tests/reactive-messaging-kafka/src/test/java/io/quarkus/it/kafka/KafkaConnectorTest.java +++ b/integration-tests/reactive-messaging-kafka/src/test/java/io/quarkus/it/kafka/KafkaConnectorTest.java @@ -8,7 +8,9 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import io.quarkus.arc.Arc; import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.DisabledOnIntegrationTest; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.kafka.KafkaCompanionResource; import io.restassured.common.mapper.TypeRef; @@ -48,4 +50,11 @@ public void testFruits() { await().untilAsserted(() -> Assertions.assertEquals(get("/kafka/fruits").as(TYPE_REF).size(), 4)); } + @Test + @DisabledOnIntegrationTest + public void testPrices() { + KafkaRepeatableReceivers repeatableReceivers = Arc.container().instance(KafkaRepeatableReceivers.class).get(); + await().untilAsserted(() -> Assertions.assertEquals(repeatableReceivers.getPrices().size(), 6)); + } + } From 38fac4454702cb4b2b4c322fd1a0bb3f5905c723 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Mon, 13 Mar 2023 12:16:42 +0000 Subject: [PATCH 15/27] Check the expiry date for inactive OIDC tokens (cherry picked from commit 402a9baa07d9bb9bfccb70d3018c15379e4eb32d) --- .../oidc/runtime/OidcIdentityProvider.java | 3 +- .../io/quarkus/oidc/runtime/OidcProvider.java | 21 +++++---- .../CodeFlowTokenIntrospectionResource.java | 21 +++++++++ .../CustomSecurityIdentityAugmentor.java | 1 + .../it/keycloak/CustomTenantResolver.java | 3 ++ .../src/main/resources/application.properties | 12 +++++ .../keycloak/CodeFlowAuthorizationTest.java | 46 +++++++++++++++++++ .../oidc/server/OidcWiremockTestResource.java | 40 ++++++++++++---- 8 files changed, 130 insertions(+), 17 deletions(-) create mode 100644 integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowTokenIntrospectionResource.java diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java index 98a8ed130b067..b6afb3fee8dc1 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java @@ -132,7 +132,8 @@ public Uni apply(UserInfo userInfo, Throwable t) { @Override public Uni apply(TokenVerificationResult codeAccessTokenResult, Throwable t) { if (t != null) { - return Uni.createFrom().failure(new AuthenticationFailedException(t)); + return Uni.createFrom().failure(t instanceof AuthenticationFailedException ? t + : new AuthenticationFailedException(t)); } if (codeAccessTokenResult != null) { if (tokenAutoRefreshPrepared(codeAccessTokenResult, vertxContext, diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java index da91862b37068..8c40298b1ff29 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java @@ -227,16 +227,10 @@ public TokenIntrospection apply(TokenIntrospection introspectionResult, Throwabl } if (!Boolean.TRUE.equals(introspectionResult.getBoolean(OidcConstants.INTROSPECTION_TOKEN_ACTIVE))) { LOG.debugf("Token issued to client %s is not active", oidcConfig.clientId.get()); + verifyTokenExpiry(introspectionResult.getLong(OidcConstants.INTROSPECTION_TOKEN_EXP)); throw new AuthenticationFailedException(); } - if (isTokenExpired(introspectionResult.getLong(OidcConstants.INTROSPECTION_TOKEN_EXP))) { - String error = String.format("Token issued to client %s has expired", - oidcConfig.clientId.get()); - LOG.debugf(error); - throw new AuthenticationFailedException( - new InvalidJwtException(error, - List.of(new ErrorCodeValidator.Error(ErrorCodes.EXPIRED, error)), null)); - } + verifyTokenExpiry(introspectionResult.getLong(OidcConstants.INTROSPECTION_TOKEN_EXP)); try { verifyTokenAge(introspectionResult.getLong(OidcConstants.INTROSPECTION_TOKEN_IAT)); } catch (InvalidJwtException ex) { @@ -246,6 +240,17 @@ public TokenIntrospection apply(TokenIntrospection introspectionResult, Throwabl return introspectionResult; } + private void verifyTokenExpiry(Long exp) { + if (isTokenExpired(exp)) { + String error = String.format("Token issued to client %s has expired", + oidcConfig.clientId.get()); + LOG.debugf(error); + throw new AuthenticationFailedException( + new InvalidJwtException(error, + List.of(new ErrorCodeValidator.Error(ErrorCodes.EXPIRED, error)), null)); + } + } + }); } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowTokenIntrospectionResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowTokenIntrospectionResource.java new file mode 100644 index 0000000000000..6b0df91d534d6 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowTokenIntrospectionResource.java @@ -0,0 +1,21 @@ +package io.quarkus.it.keycloak; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; + +@Path("/code-flow-token-introspection") +@Authenticated +public class CodeFlowTokenIntrospectionResource { + + @Inject + SecurityIdentity identity; + + @GET + public String access() { + return identity.getPrincipal().getName(); + } +} diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java index 84eae694557c0..76adb9e2db103 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java @@ -23,6 +23,7 @@ public Uni augment(SecurityIdentity identity, AuthenticationRe || routingContext.normalizedPath().endsWith("code-flow-user-info-github") || routingContext.normalizedPath().endsWith("bearer-user-info-github-service") || routingContext.normalizedPath().endsWith("code-flow-user-info-dynamic-github") + || routingContext.normalizedPath().endsWith("code-flow-token-introspection") || routingContext.normalizedPath().endsWith("code-flow-user-info-github-cached-in-idtoken"))) { QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity); UserInfo userInfo = identity.getAttribute("userinfo"); diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java index 61ee4ff44d326..2e75c5f4e28a8 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java @@ -38,6 +38,9 @@ public String resolve(RoutingContext context) { if (path.endsWith("code-flow-user-info-github-cached-in-idtoken")) { return "code-flow-user-info-github-cached-in-idtoken"; } + if (path.endsWith("code-flow-token-introspection")) { + return "code-flow-token-introspection"; + } if (path.endsWith("bearer")) { return "bearer"; } diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index 99d39eec302a1..cd675540342b3 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -97,6 +97,18 @@ quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.client-id=quarkus-web- quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.credentials.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow +quarkus.oidc.code-flow-token-introspection.provider=github +quarkus.oidc.code-flow-token-introspection.auth-server-url=${keycloak.url}/realms/quarkus/ +quarkus.oidc.code-flow-token-introspection.authorization-path=/ +quarkus.oidc.code-flow-token-introspection.user-info-path=protocol/openid-connect/userinfo +quarkus.oidc.code-flow-token-introspection.introspection-path=protocol/openid-connect/token/introspect +quarkus.oidc.code-flow-token-introspection.token.refresh-expired=true +quarkus.oidc.code-flow-token-introspection.token.refresh-token-time-skew=298 +quarkus.oidc.code-flow-token-introspection.authentication.verify-access-token=true +quarkus.oidc.code-flow-token-introspection.client-id=quarkus-web-app +quarkus.oidc.code-flow-token-introspection.credentials.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow +quarkus.oidc.code-flow-token-introspection.code-grant.headers.X-Custom=XTokenIntrospection + quarkus.oidc.token-cache.max-size=1 quarkus.oidc.bearer.auth-server-url=${keycloak.url}/realms/quarkus/ diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java index 7bbd13ba2e3cb..d3273facb0eb8 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java @@ -214,6 +214,30 @@ public void testCodeFlowUserInfo() throws Exception { doTestCodeFlowUserInfoCashedInIdToken(); } + @Test + public void testCodeFlowTokenIntrospection() throws Exception { + defineCodeFlowTokenIntrospectionStub(); + try (final WebClient webClient = createWebClient()) { + webClient.getOptions().setRedirectEnabled(true); + HtmlPage page = webClient.getPage("http://localhost:8081/code-flow-token-introspection"); + + HtmlForm form = page.getFormByName("form"); + form.getInputByName("username").type("alice"); + form.getInputByName("password").type("alice"); + + page = form.getInputByValue("login").click(); + + assertEquals("alice", page.getBody().asNormalizedText()); + + // refresh + Thread.sleep(3000); + page = webClient.getPage("http://localhost:8081/code-flow-token-introspection"); + assertEquals("admin", page.getBody().asNormalizedText()); + + webClient.getCookieManager().clearCookies(); + } + } + private void doTestCodeFlowUserInfo(String tenantId, long internalIdTokenLifetime) throws IOException { try (final WebClient webClient = createWebClient()) { webClient.getOptions().setRedirectEnabled(true); @@ -297,6 +321,28 @@ private void defineCodeFlowAuthorizationOauth2TokenStub() { } + private void defineCodeFlowTokenIntrospectionStub() { + wireMockServer + .stubFor(WireMock.post("/auth/realms/quarkus/access_token") + .withHeader("X-Custom", matching("XTokenIntrospection")) + .withRequestBody(containing("authorization_code")) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\n" + + " \"access_token\": \"alice\"," + + " \"refresh_token\": \"refresh5678\"" + + "}"))); + + wireMockServer + .stubFor(WireMock.post("/auth/realms/quarkus/access_token") + .withRequestBody(containing("refresh_token=refresh5678")) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\n" + + " \"access_token\": \"admin\"" + + "}"))); + } + private void defineCodeFlowLogoutStub() { wireMockServer.stubFor( get(urlPathMatching("/auth/realms/quarkus/protocol/openid-connect/end-session")) diff --git a/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java b/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java index 88bc2ec98a50a..8cf17de178afe 100644 --- a/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java +++ b/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java @@ -121,13 +121,9 @@ public Map start() { " ]\n" + "}"))); - server.stubFor( - get(urlEqualTo("/auth/realms/quarkus/protocol/openid-connect/userinfo")) - .willReturn(aResponse() - .withHeader("Content-Type", "application/json") - .withBody("{\n" + - " \"preferred_username\": \"alice\"" - + "}"))); + defineUserInfoStubForOpaqueToken("alice"); + defineUserInfoStubForOpaqueToken("admin"); + defineUserInfoStubForJwt(); // define the mock for the introspect endpoint @@ -221,7 +217,30 @@ public Map start() { return conf; } + private void defineUserInfoStubForOpaqueToken(String user) { + server.stubFor( + get(urlEqualTo("/auth/realms/quarkus/protocol/openid-connect/userinfo")) + .withHeader("Authorization", matching("Bearer " + user)) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\n" + + " \"preferred_username\": \"" + user + "\"" + + "}"))); + } + + private void defineUserInfoStubForJwt() { + server.stubFor( + get(urlEqualTo("/auth/realms/quarkus/protocol/openid-connect/userinfo")) + .withHeader("Authorization", containing("Bearer ey")) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\n" + + " \"preferred_username\": \"alice\"" + + "}"))); + } + private void defineValidIntrospectionMockTokenStubForUserWithRoles(String user, Set roles) { + long exp = now() + 300; server.stubFor(WireMock.post("/auth/realms/quarkus/protocol/openid-connect/token/introspect") .withRequestBody(matching("token=" + user + "&token_type_hint=access_token")) .willReturn(WireMock @@ -230,7 +249,12 @@ private void defineValidIntrospectionMockTokenStubForUserWithRoles(String user, .withBody( "{\"active\":true,\"scope\":\"" + roles.stream().collect(joining(" ")) + "\",\"username\":\"" + user - + "\",\"iat\":1,\"exp\":999999999999,\"expires_in\":999999999999,\"client_id\":\"my_client_id\"}"))); + + "\",\"iat\":1,\"exp\":" + exp + ",\"expires_in\":" + exp + + ",\"client_id\":\"my_client_id\"}"))); + } + + private static final long now() { + return System.currentTimeMillis(); } private void defineInvalidIntrospectionMockTokenStubForUserWithRoles(String user, Set roles) { From 4f5340673b2510f2c072cfca8fb640e99c1a3b76 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 23 Mar 2023 10:53:10 +0200 Subject: [PATCH 16/27] Allow use of null in REST Client request body (cherry picked from commit 67376559a58f50ca1f38f4c02b2abed5692c7180) --- .../rest/client/reactive/NullBodyTest.java | 45 +++++++++++++++++++ .../client/impl/RestClientRequestContext.java | 4 ++ 2 files changed, 49 insertions(+) create mode 100644 extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/NullBodyTest.java diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/NullBodyTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/NullBodyTest.java new file mode 100644 index 0000000000000..70a6c13b96c70 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/NullBodyTest.java @@ -0,0 +1,45 @@ +package io.quarkus.rest.client.reactive; + +import java.net.URI; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; + +import org.assertj.core.api.Assertions; +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; + +public class NullBodyTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addClasses(Client.class)); + + @TestHTTPResource + URI baseUri; + + @Test + void withBody() { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); + + Assertions.assertThatThrownBy(() -> client.call("test")).hasMessageContaining("404"); + } + + @Test + void withoutBody() { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); + + Assertions.assertThatThrownBy(() -> client.call(null)).hasMessageContaining("404"); + } + + public interface Client { + + @Path("/") + @POST + void call(String body); + } +} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java index 47dcf87d950ff..a4c05fa22760c 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java @@ -240,6 +240,10 @@ public ClientRequestContextImpl getOrCreateClientRequestContext() { public Buffer writeEntity(Entity entity, MultivaluedMap headerMap, WriterInterceptor[] interceptors) throws IOException { Object entityObject = entity.getEntity(); + if (entityObject == null) { + return AsyncInvokerImpl.EMPTY_BUFFER; + } + Class entityClass; Type entityType; if (entityObject instanceof GenericEntity) { From 6edf4022f08d585d5623bb281a04fe2f9c820260 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Thu, 23 Mar 2023 20:00:04 +0000 Subject: [PATCH 17/27] Remove the session cookie if ID token verification failed (cherry picked from commit 298b0080634d15368d9f3a174994bf993d03e106) --- .../runtime/CodeAuthenticationMechanism.java | 9 +++++++-- .../io/quarkus/it/keycloak/CodeFlowTest.java | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index fdf6a4fb4df6a..b3da2483290c9 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -287,8 +287,13 @@ public Uni apply(Throwable t) { if (!expired) { LOG.errorf("ID token verification failure: %s", t.getCause()); - return Uni.createFrom() - .failure(new AuthenticationCompletionException(t.getCause())); + return removeSessionCookie(context, configContext.oidcConfig) + .replaceWith(Uni.createFrom() + .failure(t + .getCause() instanceof AuthenticationCompletionException + ? t.getCause() + : new AuthenticationCompletionException( + t.getCause()))); } // Token has expired, try to refresh if (session.getRefreshToken() == null) { diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java index bddaa98eeeb00..4b5e574726d38 100644 --- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java @@ -564,6 +564,22 @@ public void testIdTokenInjection() throws IOException { page = webClient.getPage("http://localhost:8081/web-app"); assertEquals("alice", page.getBody().asNormalizedText()); + + Cookie sessionCookie = getSessionCookie(webClient, null); + assertNotNull(sessionCookie); + webClient.getCookieManager().clearCookies(); + webClient.getCookieManager().addCookie(new Cookie(sessionCookie.getDomain(), sessionCookie.getName(), + "1|2|3")); + sessionCookie = getSessionCookie(webClient, null); + assertEquals("1|2|3", sessionCookie.getValue()); + + try { + webClient.getPage("http://localhost:8081/web-app"); + fail("401 status error is expected"); + } catch (FailingHttpStatusCodeException ex) { + assertEquals(401, ex.getStatusCode()); + assertNull(getSessionCookie(webClient, null)); + } webClient.getCookieManager().clearCookies(); } } From 7cf63fccff511e72db1b9374288ec1c66d8d3ad4 Mon Sep 17 00:00:00 2001 From: Gerhard Flothow Date: Thu, 23 Mar 2023 10:18:10 -0600 Subject: [PATCH 18/27] fix missing quote in HQL expression (cherry picked from commit 6d46c2eae62094431bddacab803b6e790e33a99e) --- docs/src/main/asciidoc/hibernate-reactive-panache.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/hibernate-reactive-panache.adoc b/docs/src/main/asciidoc/hibernate-reactive-panache.adoc index 849ad0b8d768c..bfc9308f9d39a 100644 --- a/docs/src/main/asciidoc/hibernate-reactive-panache.adoc +++ b/docs/src/main/asciidoc/hibernate-reactive-panache.adoc @@ -710,7 +710,7 @@ It is not possible to have a HQL `select new` query and `.project(Class)` at the For example, this will fail: [source,java] ---- -PanacheQuery query = Person.find("select new MyView(d.race, AVG(d.weight)) from Dog d group by d.race).project(AnotherView.class); +PanacheQuery query = Person.find("select new MyView(d.race, AVG(d.weight)) from Dog d group by d.race").project(AnotherView.class); ---- ==== From 3f901dd404eae0cf261a7952955c5056b03450d6 Mon Sep 17 00:00:00 2001 From: Jonathan Kolberg Date: Thu, 23 Mar 2023 21:26:27 +0100 Subject: [PATCH 19/27] K8s moved its registry old registry k8s.grc.io will be phased out use registry.k8s.io instead fixes #32041 (cherry picked from commit 07d26397ca352dc4c57c6e739b4b032508b57f55) --- .../client/deployment/DevServicesKubernetesProcessor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/DevServicesKubernetesProcessor.java b/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/DevServicesKubernetesProcessor.java index 99e382137d249..9ba246d4a00e2 100644 --- a/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/DevServicesKubernetesProcessor.java +++ b/extensions/kubernetes-client/deployment/src/main/java/io/quarkus/kubernetes/client/deployment/DevServicesKubernetesProcessor.java @@ -343,7 +343,8 @@ public Map getKubeconfig() { return getKubernetesClientConfigFromKubeConfig( KubeConfigUtils.parseKubeConfig(KubeConfigUtils.replaceServerInKubeconfig(containerAddress.getUrl(), getFileContentFromContainer(KIND_KUBECONFIG)))); - } else if (image.contains("k8s.gcr.io/kube-apiserver")) { + } else if (image.contains("k8s.gcr.io/kube-apiserver") || + image.contains("registry.k8s.io/kube-apiserver")) { return getKubernetesClientConfigFromKubeConfig(getKubeconfigFromApiContainer(containerAddress.getUrl())); } From b3b3ec3a12d13637625c9206019e10d6cc718173 Mon Sep 17 00:00:00 2001 From: Alexey Loubyansky Date: Thu, 23 Mar 2023 22:50:31 +0100 Subject: [PATCH 20/27] Re-use current ApplicationModel for JaCoCo reports when testing Gradle projects (cherry picked from commit be495a101191bc58b7ffbe0ba5be67d3a0bc9716) --- .../quarkus/jacoco/deployment/JacocoProcessor.java | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/test-framework/jacoco/deployment/src/main/java/io/quarkus/jacoco/deployment/JacocoProcessor.java b/test-framework/jacoco/deployment/src/main/java/io/quarkus/jacoco/deployment/JacocoProcessor.java index be25c4ca0541d..c7273df4b8f4f 100644 --- a/test-framework/jacoco/deployment/src/main/java/io/quarkus/jacoco/deployment/JacocoProcessor.java +++ b/test-framework/jacoco/deployment/src/main/java/io/quarkus/jacoco/deployment/JacocoProcessor.java @@ -16,7 +16,6 @@ import org.jboss.jandex.ClassInfo; import io.quarkus.bootstrap.model.ApplicationModel; -import io.quarkus.bootstrap.utils.BuildToolHelper; import io.quarkus.bootstrap.workspace.SourceDir; import io.quarkus.deployment.ApplicationArchive; import io.quarkus.deployment.IsTest; @@ -105,17 +104,7 @@ public byte[] apply(String className, byte[] bytes) { info.classFiles = classes; Set sources = new HashSet<>(); - ApplicationModel model; - if (BuildToolHelper.isMavenProject(targetdir.toPath())) { - model = curateOutcomeBuildItem.getApplicationModel(); - } else if (BuildToolHelper.isGradleProject(targetdir.toPath())) { - //this seems counter productive, but we want the dev mode model and not the test model - //as the test model will include the test classes that we don't want in the report - model = BuildToolHelper.enableGradleAppModelForDevMode(targetdir.toPath()); - } else { - throw new RuntimeException("Cannot determine project type generating Jacoco report"); - } - + final ApplicationModel model = curateOutcomeBuildItem.getApplicationModel(); if (model.getApplicationModule() != null) { addProjectModule(model.getAppArtifact(), config, info, includes, excludes, classes, sources); } From 54e6bddd8b593c255440bf4ead13a3c19f832774 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 24 Mar 2023 11:03:52 +0100 Subject: [PATCH 21/27] Fix Podman detection on Windows Fixes #32106 (cherry picked from commit 82e62c76b90ca1ddef93eeaa056d4a93d6a44186) --- .../java/io/quarkus/runtime/util/ContainerRuntimeUtil.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java index 2be3cbf76bdaa..09ad0d37e7e80 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/util/ContainerRuntimeUtil.java @@ -76,7 +76,8 @@ public static ContainerRuntime detectContainerRuntime(boolean required) { dockerAvailable = dockerVersionOutput.contains("Docker version"); if (dockerAvailable) { // Check if "docker" is an alias to "podman" - if (dockerVersionOutput.startsWith("podman version")) { + if (dockerVersionOutput.startsWith("podman version") || + dockerVersionOutput.startsWith("podman.exe version")) { storeContainerRuntimeInSystemProperty(ContainerRuntime.PODMAN); return ContainerRuntime.PODMAN; } @@ -84,7 +85,8 @@ public static ContainerRuntime detectContainerRuntime(boolean required) { return ContainerRuntime.DOCKER; } podmanVersionOutput = getVersionOutputFor(ContainerRuntime.PODMAN); - podmanAvailable = podmanVersionOutput.startsWith("podman version"); + podmanAvailable = podmanVersionOutput.startsWith("podman version") || + podmanVersionOutput.startsWith("podman.exe version"); if (podmanAvailable) { storeContainerRuntimeInSystemProperty(ContainerRuntime.PODMAN); return ContainerRuntime.PODMAN; From 81eba640b9f2a59a50e70e52786aa7d5ccd93500 Mon Sep 17 00:00:00 2001 From: Clemens Classen Date: Fri, 24 Mar 2023 11:40:43 +0100 Subject: [PATCH 22/27] Prevent splitting of cookie header values when using AWS Lambda (cherry picked from commit 736ba35acdebace0bdc6395e9147c42e3bdebbf1) --- .../quarkus/amazon/lambda/runtime/MockHttpEventServer.java | 5 +++++ .../io/quarkus/amazon/lambda/http/LambdaHttpHandler.java | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/extensions/amazon-lambda-http/http-event-server/src/main/java/io/quarkus/amazon/lambda/runtime/MockHttpEventServer.java b/extensions/amazon-lambda-http/http-event-server/src/main/java/io/quarkus/amazon/lambda/runtime/MockHttpEventServer.java index e7b438422ea0c..69fe6e67955fe 100644 --- a/extensions/amazon-lambda-http/http-event-server/src/main/java/io/quarkus/amazon/lambda/runtime/MockHttpEventServer.java +++ b/extensions/amazon-lambda-http/http-event-server/src/main/java/io/quarkus/amazon/lambda/runtime/MockHttpEventServer.java @@ -128,6 +128,11 @@ public void processResponse(RoutingContext ctx, RoutingContext pending, Buffer b } } } + if (res.getCookies() != null) { + for (String cookie : res.getCookies()) { + response.headers().add("set-cookie", cookie); + } + } response.setStatusCode(res.getStatusCode()); String body = res.getBody(); if (body != null) { diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java index c7c9b7f10920b..8b5c2654937cd 100644 --- a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java @@ -108,6 +108,11 @@ public void handleMessage(Object msg) { if (allForName == null || allForName.isEmpty()) { continue; } + // Handle cookies separately to preserve commas in the header values + if ("set-cookie".equals(name)) { + responseBuilder.setCookies(allForName); + continue; + } final StringBuilder sb = new StringBuilder(); for (Iterator valueIterator = allForName.iterator(); valueIterator.hasNext();) { sb.append(valueIterator.next()); From f6e8403c437f1905368272e01163724cd9adf178 Mon Sep 17 00:00:00 2001 From: Alexey Loubyansky Date: Fri, 13 Jan 2023 23:45:32 +0100 Subject: [PATCH 23/27] Create a new base classloader including parent-first test scoped dependencies when bootstrapping for CT (cherry picked from commit 18e4136140e450ca5e1ee7014f824d1815e0a32a) --- .../dev/testing/JunitTestRunner.java | 7 - .../dev/testing/ModuleTestRunner.java | 5 +- .../deployment/dev/testing/TestSupport.java | 143 +++++++++++++----- .../bootstrap/BootstrapAppModelFactory.java | 28 +++- .../bootstrap/app/QuarkusBootstrap.java | 61 ++++---- .../java/io/quarkus/test/QuarkusUnitTest.java | 10 +- .../test/junit/QuarkusTestExtension.java | 6 +- 7 files changed, 159 insertions(+), 101 deletions(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java index cf5be62f97e40..24fe9708f9be3 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java @@ -754,7 +754,6 @@ static class Builder { private TestType testType = TestType.ALL; private TestState testState; private long runId = -1; - private DevModeContext devModeContext; private CuratedApplication testApplication; private ClassScanResult classScanResult; private TestClassUsages testClassUsages; @@ -783,11 +782,6 @@ public Builder setTestType(TestType testType) { return this; } - public Builder setDevModeContext(DevModeContext devModeContext) { - this.devModeContext = devModeContext; - return this; - } - public Builder setTestApplication(CuratedApplication testApplication) { this.testApplication = testApplication; return this; @@ -849,7 +843,6 @@ public Builder setExcludeEngines(List excludeEngines) { } public JunitTestRunner build() { - Objects.requireNonNull(devModeContext, "devModeContext"); Objects.requireNonNull(testClassUsages, "testClassUsages"); Objects.requireNonNull(testApplication, "testApplication"); Objects.requireNonNull(testState, "testState"); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/ModuleTestRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/ModuleTestRunner.java index 20d5f23873897..be0ca21445bf0 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/ModuleTestRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/ModuleTestRunner.java @@ -17,17 +17,15 @@ public class ModuleTestRunner { final TestState testState = new TestState(); private final TestSupport testSupport; - private final DevModeContext devModeContext; private final CuratedApplication testApplication; private final DevModeContext.ModuleInfo moduleInfo; private final TestClassUsages testClassUsages = new TestClassUsages(); private JunitTestRunner runner; - public ModuleTestRunner(TestSupport testSupport, DevModeContext devModeContext, CuratedApplication testApplication, + public ModuleTestRunner(TestSupport testSupport, CuratedApplication testApplication, DevModeContext.ModuleInfo moduleInfo) { this.testSupport = testSupport; - this.devModeContext = devModeContext; this.testApplication = testApplication; this.moduleInfo = moduleInfo; } @@ -50,7 +48,6 @@ Runnable prepare(ClassScanResult classScanResult, boolean reRunFailures, long ru } JunitTestRunner.Builder builder = new JunitTestRunner.Builder() .setClassScanResult(classScanResult) - .setDevModeContext(devModeContext) .setRunId(runId) .setTestState(testState) .setTestClassUsages(testClassUsages) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java index 3773e5467423e..b72f5753c298e 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java @@ -2,14 +2,13 @@ import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -18,6 +17,7 @@ import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -26,14 +26,19 @@ import io.quarkus.bootstrap.app.CuratedApplication; import io.quarkus.bootstrap.app.QuarkusBootstrap; +import io.quarkus.bootstrap.app.QuarkusBootstrap.Mode; +import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; +import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.deployment.dev.ClassScanResult; import io.quarkus.deployment.dev.CompilationProvider; import io.quarkus.deployment.dev.DevModeContext; +import io.quarkus.deployment.dev.DevModeContext.ModuleInfo; import io.quarkus.deployment.dev.QuarkusCompiler; import io.quarkus.deployment.dev.RuntimeUpdatesProcessor; import io.quarkus.dev.spi.DevModeType; import io.quarkus.dev.testing.TestWatchedFiles; +import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.paths.PathList; import io.quarkus.runtime.configuration.HyphenateEnumConverter; @@ -143,46 +148,36 @@ public void start() { } } + private static Pattern getCompiledPatternOrNull(Optional patternStr) { + return patternStr.isPresent() ? Pattern.compile(patternStr.get()) : null; + } + public void init() { if (moduleRunners.isEmpty()) { TestWatchedFiles.setWatchedFilesListener((s) -> RuntimeUpdatesProcessor.INSTANCE.setWatchedFilePaths(s, true)); + final Pattern includeModulePattern = getCompiledPatternOrNull(config.includeModulePattern); + final Pattern excludeModulePattern = getCompiledPatternOrNull(config.excludeModulePattern); for (var module : context.getAllModules()) { - boolean mainModule = module == context.getApplicationRoot(); + final boolean mainModule = module == context.getApplicationRoot(); if (config.onlyTestApplicationModule && !mainModule) { continue; - } else if (config.includeModulePattern.isPresent()) { - Pattern p = Pattern.compile(config.includeModulePattern.get()); - if (!p.matcher(module.getArtifactKey().getGroupId() + ":" + module.getArtifactKey().getArtifactId()) + } else if (includeModulePattern != null) { + if (!includeModulePattern + .matcher(module.getArtifactKey().getGroupId() + ":" + module.getArtifactKey().getArtifactId()) .matches()) { continue; } - } else if (config.excludeModulePattern.isPresent()) { - Pattern p = Pattern.compile(config.excludeModulePattern.get()); - if (p.matcher(module.getArtifactKey().getGroupId() + ":" + module.getArtifactKey().getArtifactId()) + } else if (excludeModulePattern != null) { + if (excludeModulePattern + .matcher(module.getArtifactKey().getGroupId() + ":" + module.getArtifactKey().getArtifactId()) .matches()) { continue; } } try { - Set paths = new LinkedHashSet<>(); - module.getTest().ifPresent(test -> { - paths.add(Paths.get(test.getClassesPath())); - if (test.getResourcesOutputPath() != null) { - paths.add(Paths.get(test.getResourcesOutputPath())); - } - }); - if (mainModule) { - curatedApplication.getQuarkusBootstrap().getApplicationRoot().forEach(paths::add); - } else { - paths.add(Paths.get(module.getMain().getClassesPath())); - } - for (var i : paths) { - if (!Files.exists(i)) { - Files.createDirectories(i); - } - } - QuarkusBootstrap.Builder builder = curatedApplication.getQuarkusBootstrap().clonedBuilder() + final Path projectDir = Path.of(module.getProjectDirectory()); + final QuarkusBootstrap.Builder bootstrapConfig = curatedApplication.getQuarkusBootstrap().clonedBuilder() .setMode(QuarkusBootstrap.Mode.TEST) .setAssertionsEnabled(true) .setDisableClasspathCache(false) @@ -192,20 +187,62 @@ public void init() { .setTest(true) .setAuxiliaryApplication(true) .setHostApplicationIsTestOnly(devModeType == DevModeType.TEST_ONLY) - .setProjectRoot(Paths.get(module.getProjectDirectory())) - .setApplicationRoot(PathList.from(paths)) + .setProjectRoot(projectDir) + .setApplicationRoot(getRootPaths(module, mainModule)) .clearLocalArtifacts(); + + final QuarkusClassLoader ctParentFirstCl; + final Mode currentMode = curatedApplication.getQuarkusBootstrap().getMode(); + // in case of quarkus:test the application model will already include test dependencies + if (Mode.CONTINUOUS_TEST != currentMode && Mode.TEST != currentMode) { + // In this case the current application model does not include test dependencies. + // 1) we resolve an application model for test mode; + // 2) we create a new CT base classloader that includes parent-first test scoped dependencies + // so that they are not loaded by augment and base runtime classloaders. + var appModelFactory = curatedApplication.getQuarkusBootstrap().newAppModelFactory(); + appModelFactory.setBootstrapAppModelResolver(null); + appModelFactory.setTest(true); + appModelFactory.setLocalArtifacts(Set.of()); + if (!mainModule) { + appModelFactory.setAppArtifact(null); + appModelFactory.setProjectRoot(projectDir); + } + final ApplicationModel testModel = appModelFactory.resolveAppModel().getApplicationModel(); + bootstrapConfig.setExistingModel(testModel); + + QuarkusClassLoader.Builder clBuilder = null; + var currentParentFirst = curatedApplication.getApplicationModel().getParentFirst(); + for (ResolvedDependency d : testModel.getDependencies()) { + if (d.isClassLoaderParentFirst() && !currentParentFirst.contains(d.getKey())) { + if (clBuilder == null) { + clBuilder = QuarkusClassLoader.builder("Continuous Testing Parent-First", + getClass().getClassLoader().getParent(), false); + } + clBuilder.addElement(ClassPathElement.fromDependency(d)); + } + } + + ctParentFirstCl = clBuilder == null ? null : clBuilder.build(); + if (ctParentFirstCl != null) { + bootstrapConfig.setBaseClassLoader(ctParentFirstCl); + } + } else { + ctParentFirstCl = null; + if (mainModule) { + // the model and the app classloader already include test scoped dependencies + bootstrapConfig.setExistingModel(curatedApplication.getApplicationModel()); + } + } + //we always want to propagate parent first //so it is consistent. Some modules may not have quarkus dependencies //so they won't load junit parent first without this for (var i : curatedApplication.getApplicationModel().getDependencies()) { if (i.isClassLoaderParentFirst()) { - builder.addParentFirstArtifact(i.getKey()); + bootstrapConfig.addParentFirstArtifact(i.getKey()); } } - var testCuratedApplication = builder // we want to re-discover the local dependencies with test scope - .build() - .bootstrap(); + var testCuratedApplication = bootstrapConfig.build().bootstrap(); if (mainModule) { //horrible hack //we really need a compiler per module but we are not setup for this yet @@ -215,7 +252,7 @@ public void init() { //has complained much compiler = new QuarkusCompiler(testCuratedApplication, compilationProviders, context); } - var testRunner = new ModuleTestRunner(this, context, testCuratedApplication, module); + var testRunner = new ModuleTestRunner(this, testCuratedApplication, module); QuarkusClassLoader cl = (QuarkusClassLoader) getClass().getClassLoader(); cl.addCloseTask(new Runnable() { @Override @@ -224,6 +261,9 @@ public void run() { close(); } finally { testCuratedApplication.close(); + if (ctParentFirstCl != null) { + ctParentFirstCl.close(); + } } } }); @@ -235,6 +275,37 @@ public void run() { } } + private PathList getRootPaths(ModuleInfo module, final boolean mainModule) { + final PathList.Builder pathBuilder = PathList.builder(); + final Consumer paths = new Consumer<>() { + @Override + public void accept(Path t) { + if (!pathBuilder.contains(t)) { + if (!Files.exists(t)) { + try { + Files.createDirectories(t); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + pathBuilder.add(t); + } + } + }; + module.getTest().ifPresent(test -> { + paths.accept(Path.of(test.getClassesPath())); + if (test.getResourcesOutputPath() != null) { + paths.accept(Path.of(test.getResourcesOutputPath())); + } + }); + if (mainModule) { + curatedApplication.getQuarkusBootstrap().getApplicationRoot().forEach(paths::accept); + } else { + paths.accept(Path.of(module.getMain().getClassesPath())); + } + return pathBuilder.build(); + } + public synchronized void close() { closed = true; stop(); @@ -522,10 +593,6 @@ public boolean isStarted() { return started; } - public CuratedApplication getCuratedApplication() { - return curatedApplication; - } - public QuarkusCompiler getCompiler() { return compiler; } diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/BootstrapAppModelFactory.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/BootstrapAppModelFactory.java index 410640b31a9e7..6c068b880292c 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/BootstrapAppModelFactory.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/BootstrapAppModelFactory.java @@ -11,12 +11,10 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; -import org.apache.maven.model.Dependency; import org.jboss.logging.Logger; import io.quarkus.bootstrap.app.CurationResult; @@ -74,9 +72,9 @@ public static BootstrapAppModelFactory newInstance() { private MavenArtifactResolver mavenArtifactResolver; private BootstrapMavenContext mvnContext; - Set reloadableModules = Collections.emptySet(); + Set reloadableModules = Set.of(); - private Collection forcedDependencies = Collections.emptyList(); + private Collection forcedDependencies = List.of(); private BootstrapAppModelFactory() { } @@ -306,9 +304,23 @@ private boolean isWorkspaceDiscoveryEnabled() { } private LocalProject loadWorkspace() throws AppModelResolverException { - return projectRoot == null || !Files.isDirectory(projectRoot) - ? null - : createBootstrapMavenContext().getCurrentProject(); + if (projectRoot == null || !Files.isDirectory(projectRoot)) { + return null; + } + LocalProject project = createBootstrapMavenContext().getCurrentProject(); + if (project == null) { + return null; + } + if (project.getDir().equals(projectRoot)) { + return project; + } + for (LocalProject p : project.getWorkspace().getProjects().values()) { + if (p.getDir().equals(projectRoot)) { + return p; + } + } + log.warnf("Expected project directory %s does not match current project directory %s", projectRoot, project.getDir()); + return project; } private CurationResult createAppModelForJar(Path appArtifactPath) { @@ -321,7 +333,7 @@ private CurationResult createAppModelForJar(Path appArtifactPath) { } modelResolver.relink(appArtifact, appArtifactPath); //we need some way to figure out dependencies here - appModel = modelResolver.resolveManagedModel(appArtifact, Collections.emptyList(), managingProject, + appModel = modelResolver.resolveManagedModel(appArtifact, List.of(), managingProject, reloadableModules); } catch (AppModelResolverException | IOException e) { throw new RuntimeException("Failed to resolve initial application dependencies", e); diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java index 3757ed11b36e3..ea8d53f0c959a 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java @@ -128,19 +128,33 @@ private QuarkusBootstrap(Builder builder) { public CuratedApplication bootstrap() throws BootstrapException { //all we want to do is resolve all our dependencies //once we have this it is up to augment to set up the class loader to actually use them + final CurationResult curationResult = existingModel != null + ? new CurationResult(existingModel) + : newAppModelFactory().resolveAppModel(); - if (existingModel != null) { - final ConfiguredClassLoading classLoadingConfig = ConfiguredClassLoading.builder() - .setApplicationRoot(applicationRoot) - .setDefaultFlatTestClassPath(defaultFlatTestClassPath) - .setMode(mode) - .addParentFirstArtifacts(parentFirstArtifacts) - .setApplicationModel(existingModel) - .build(); - return new CuratedApplication(this, new CurationResult(existingModel), classLoadingConfig); + if (curationResult.getApplicationModel().getAppArtifact() != null) { + if (curationResult.getApplicationModel().getAppArtifact().getArtifactId() != null) { + buildSystemProperties.putIfAbsent("quarkus.application.name", + curationResult.getApplicationModel().getAppArtifact().getArtifactId()); + } + if (curationResult.getApplicationModel().getAppArtifact().getVersion() != null) { + buildSystemProperties.putIfAbsent("quarkus.application.version", + curationResult.getApplicationModel().getAppArtifact().getVersion()); + } } - BootstrapAppModelFactory appModelFactory = BootstrapAppModelFactory.newInstance() + final ConfiguredClassLoading classLoadingConfig = ConfiguredClassLoading.builder() + .setApplicationRoot(applicationRoot) + .setDefaultFlatTestClassPath(defaultFlatTestClassPath) + .setMode(mode) + .addParentFirstArtifacts(parentFirstArtifacts) + .setApplicationModel(curationResult.getApplicationModel()) + .build(); + return new CuratedApplication(this, curationResult, classLoadingConfig); + } + + public BootstrapAppModelFactory newAppModelFactory() { + final BootstrapAppModelFactory appModelFactory = BootstrapAppModelFactory.newInstance() .setOffline(offline) .setMavenArtifactResolver(mavenArtifactResolver) .setBootstrapAppModelResolver(appModelResolver) @@ -149,7 +163,7 @@ public CuratedApplication bootstrap() throws BootstrapException { .setManagingProject(managingProject) .setForcedDependencies(forcedDependencies) .setLocalArtifacts(localArtifacts) - .setProjectRoot(getProjectRoot()); + .setProjectRoot(projectRoot); if (mode == Mode.TEST || test) { appModelFactory.setTest(true); if (!disableClasspathCache) { @@ -162,30 +176,7 @@ public CuratedApplication bootstrap() throws BootstrapException { appModelFactory.setEnableClasspathCache(true); } } - CurationResult curationResult = appModelFactory.resolveAppModel(); - if (curationResult.getApplicationModel().getAppArtifact() != null) { - if (curationResult.getApplicationModel().getAppArtifact().getArtifactId() != null) { - buildSystemProperties.putIfAbsent("quarkus.application.name", - curationResult.getApplicationModel().getAppArtifact().getArtifactId()); - } - if (curationResult.getApplicationModel().getAppArtifact().getVersion() != null) { - buildSystemProperties.putIfAbsent("quarkus.application.version", - curationResult.getApplicationModel().getAppArtifact().getVersion()); - } - } - - final ConfiguredClassLoading classLoadingConfig = ConfiguredClassLoading.builder() - .setApplicationRoot(applicationRoot) - .setDefaultFlatTestClassPath(defaultFlatTestClassPath) - .setMode(mode) - .addParentFirstArtifacts(parentFirstArtifacts) - .setApplicationModel(curationResult.getApplicationModel()) - .build(); - return new CuratedApplication(this, curationResult, classLoadingConfig); - } - - public AppModelResolver getAppModelResolver() { - return appModelResolver; + return appModelFactory; } public PathCollection getApplicationRoot() { diff --git a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java index 9ab4fd658eb7e..c700acb94b16a 100644 --- a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java +++ b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java @@ -15,7 +15,6 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -599,16 +598,15 @@ public boolean test(String s) { + " other beans may also be removed and injection may not work as expected"); } - final Path testLocation = PathTestHelper.getTestClassesLocation(testClass); - try { + final Path testLocation = PathTestHelper.getTestClassesLocation(testClass); + final Path projectDir = Path.of("").normalize().toAbsolutePath(); QuarkusBootstrap.Builder builder = QuarkusBootstrap.builder() .setApplicationRoot(deploymentDir.resolve(APP_ROOT)) .setMode(QuarkusBootstrap.Mode.TEST) .addExcludedPath(testLocation) - .setProjectRoot(testLocation) - .setTargetDirectory( - PathTestHelper.getProjectBuildDir(Paths.get("").normalize().toAbsolutePath(), testLocation)) + .setProjectRoot(projectDir) + .setTargetDirectory(PathTestHelper.getProjectBuildDir(projectDir, testLocation)) .setFlatClassPath(flatClassPath) .setForcedDependencies(forcedDependencies); for (JavaArchive dependency : additionalDependencies) { diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java index e64a8ace36a76..19506ebfd9ca0 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java @@ -955,7 +955,6 @@ private Object runExtensionMethod(ReflectiveInvocationContext invocation while (theclass.isArray()) { theclass = theclass.getComponentType(); } - String className = theclass.getName(); if (theclass.isPrimitive()) { cloneRequired = false; } else if (TestInfo.class.isAssignableFrom(theclass)) { @@ -963,9 +962,10 @@ private Object runExtensionMethod(ReflectiveInvocationContext invocation Method newTestMethod = info.getTestMethod().isPresent() ? determineTCCLExtensionMethod(info.getTestMethod().get(), testClassFromTCCL) : null; - replacement = new TestInfoImpl(info.getDisplayName(), info.getTags(), Optional.of(testClassFromTCCL), + replacement = new TestInfoImpl(info.getDisplayName(), info.getTags(), + Optional.of(testClassFromTCCL), Optional.ofNullable(newTestMethod)); - } else if (clonePattern.matcher(className).matches()) { + } else if (clonePattern.matcher(theclass.getName()).matches()) { cloneRequired = true; } else { try { From be56f0c06baa0e3d72bff7ee229d8f58fd432145 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Wed, 1 Mar 2023 19:04:08 +0000 Subject: [PATCH 24/27] Bump Keycloak version to 21.0.1 (cherry picked from commit 551fd86e41e3adfd354ded24ec8a9f60daf6b0ce) --- bom/application/pom.xml | 2 +- build-parent/pom.xml | 2 +- .../src/main/asciidoc/security-openid-connect-dev-services.adoc | 2 +- .../oidc/deployment/devservices/keycloak/DevServicesConfig.java | 2 +- .../java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 8f7d15170c49f..6d568faf0d9f2 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -182,7 +182,7 @@ 5.8.0 4.10.1 1.1.4.Final - 20.0.3 + 21.0.1 1.15.0 3.29.0 2.17.0 diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 13d92d192c982..888e2d263aba0 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -101,7 +101,7 @@ - 20.0.3 + 21.0.1 19.0.3 quay.io/keycloak/keycloak:${keycloak.version} quay.io/keycloak/keycloak:${keycloak.wildfly.version}-legacy diff --git a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc index 8a979891f5296..8f7ae741a6813 100644 --- a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc @@ -217,7 +217,7 @@ Please see xref:security-openid-connect.adoc#integration-testing-keycloak-devser [[keycloak-initialization]] === Keycloak Initialization -The `quay.io/keycloak/keycloak:20.0.3` image which contains a Keycloak distribution powered by Quarkus is used to start a container by default. +The `quay.io/keycloak/keycloak:21.0.1` image which contains a Keycloak distribution powered by Quarkus is used to start a container by default. `quarkus.keycloak.devservices.image-name` can be used to change the Keycloak image name. For example, set it to `quay.io/keycloak/keycloak:19.0.3-legacy` to use a Keycloak distribution powered by WildFly. Note that only a Quarkus based Keycloak distribution is available starting from Keycloak `20.0.0`. diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java index 8e642fc0335c5..2550cc7cea229 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java @@ -35,7 +35,7 @@ public class DevServicesConfig { * string. * Set 'quarkus.keycloak.devservices.keycloak-x-image' to override this check. */ - @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:20.0.3") + @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:21.0.1") public String imageName; /** diff --git a/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java b/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java index bcd717025d989..29a9327e6d87b 100644 --- a/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java +++ b/integration-tests/oidc-token-propagation/src/test/java/io/quarkus/it/keycloak/OidcTokenPropagationTest.java @@ -53,7 +53,7 @@ public void testGetUserNameWithAccessTokenPropagation() { //.statusCode(200) //.body(equalTo("alice")); .statusCode(500) - .body(containsString("Client not allowed to exchange")); + .body(containsString("Feature not enabled")); } @Test From 1b1024f80e12c068dcb3a842f9e50e0e3e4da1a4 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Wed, 8 Mar 2023 12:42:43 -0300 Subject: [PATCH 25/27] custom keycloak image to install JS engine (cherry picked from commit a2914f71d9085bef21b61ebb710fb1e6d452a0e3) --- .../keycloak-authorization/pom.xml | 35 +++++++++++++++++++ .../it/keycloak/KeycloakLifecycleManager.java | 19 ++++++---- .../src/test/resources/Dockerfile | 8 +++++ 3 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 integration-tests/keycloak-authorization/src/test/resources/Dockerfile diff --git a/integration-tests/keycloak-authorization/pom.xml b/integration-tests/keycloak-authorization/pom.xml index 05e1cd9d2e417..7463c62cdfd0b 100644 --- a/integration-tests/keycloak-authorization/pom.xml +++ b/integration-tests/keycloak-authorization/pom.xml @@ -15,6 +15,7 @@ http://localhost:8180/auth + 15.3 @@ -110,20 +111,37 @@ htmlunit test + + org.openjdk.nashorn + nashorn-core + ${nashorn-core.version} + + + + src/test/resources + true + + maven-surefire-plugin true + + ${keycloak.image.version} + maven-failsafe-plugin true + + ${keycloak.image.version} + @@ -137,6 +155,23 @@ + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-dependencies-quarkus + process-test-resources + + copy-dependencies + + + ${project.build.testOutputDirectory} + nashorn-core,asm,asm-util,asm-commons + + + + diff --git a/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/KeycloakLifecycleManager.java b/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/KeycloakLifecycleManager.java index 676a24b81bb82..a1690b8467287 100644 --- a/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/KeycloakLifecycleManager.java +++ b/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/KeycloakLifecycleManager.java @@ -31,6 +31,7 @@ import org.keycloak.util.JsonSerialization; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.ImageFromDockerfile; import org.testcontainers.images.builder.Transferable; import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; @@ -44,16 +45,22 @@ public class KeycloakLifecycleManager implements QuarkusTestResourceLifecycleMan protected static String KEYCLOAK_SERVER_URL; private static final String KEYCLOAK_REALM = "quarkus"; private static final String KEYCLOAK_SERVICE_CLIENT = "quarkus-service-app"; - private static final String KEYCLOAK_VERSION = System.getProperty("keycloak.version"); + private static final String KEYCLOAK_IMAGE = System.getProperty("keycloak.docker.image"); @SuppressWarnings("resource") @Override public Map start() { - keycloak = new GenericContainer<>("quay.io/keycloak/keycloak:" + KEYCLOAK_VERSION) - .withExposedPorts(8080) - .withEnv("KEYCLOAK_ADMIN", "admin") - .withEnv("KEYCLOAK_ADMIN_PASSWORD", "admin") - .waitingFor(Wait.forLogMessage(".*Keycloak.*started.*", 1)); + try { + keycloak = new GenericContainer<>( + new ImageFromDockerfile().withDockerfile(Paths.get(getClass().getResource("/Dockerfile").toURI())) + .withBuildArg("KEYCLOAK_IMAGE", KEYCLOAK_IMAGE)) + .withExposedPorts(8080) + .withEnv("KEYCLOAK_ADMIN", "admin") + .withEnv("KEYCLOAK_ADMIN_PASSWORD", "admin") + .waitingFor(Wait.forLogMessage(".*Keycloak.*started.*", 1)); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } keycloak = keycloak .withCopyToContainer(Transferable.of(createPoliciesJar().toByteArray()), "/opt/keycloak/providers/policies.jar") diff --git a/integration-tests/keycloak-authorization/src/test/resources/Dockerfile b/integration-tests/keycloak-authorization/src/test/resources/Dockerfile new file mode 100644 index 0000000000000..7e12d7f27db4f --- /dev/null +++ b/integration-tests/keycloak-authorization/src/test/resources/Dockerfile @@ -0,0 +1,8 @@ +FROM ${keycloak.docker.image} as builder + +COPY ./*.jar /opt/keycloak/providers/ + +FROM ${keycloak.docker.image} +COPY --from=builder /opt/keycloak/ /opt/keycloak/ + +ENTRYPOINT ["/opt/keycloak/bin/kc.sh"] From 3dcb2b5684c670f773852c682cf2ce6a0b9350ee Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Fri, 10 Mar 2023 10:14:18 -0300 Subject: [PATCH 26/27] adding default crypto provider SPI to native images (cherry picked from commit f964a49393f183bd1a404e3777592b67d6a97e52) --- .../keycloak/pep/deployment/KeycloakReflectionBuildStep.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakReflectionBuildStep.java b/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakReflectionBuildStep.java index eb93628c912bf..2e30a81c9ec2f 100644 --- a/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakReflectionBuildStep.java +++ b/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakReflectionBuildStep.java @@ -9,6 +9,8 @@ import org.keycloak.adapters.authorization.cip.HttpClaimInformationPointProviderFactory; import org.keycloak.authorization.client.representation.ServerConfiguration; import org.keycloak.authorization.client.representation.TokenIntrospectionResponse; +import org.keycloak.common.crypto.CryptoProvider; +import org.keycloak.crypto.def.DefaultCryptoProvider; import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jws.JWSHeader; @@ -79,7 +81,8 @@ public void registerServiceProviders(BuildProducer ser serviceProvider.produce(new ServiceProviderBuildItem(ClaimInformationPointProviderFactory.class.getName(), HttpClaimInformationPointProviderFactory.class.getName(), ClaimsInformationPointProviderFactory.class.getName())); - + serviceProvider.produce(new ServiceProviderBuildItem(CryptoProvider.class.getName(), + DefaultCryptoProvider.class.getName())); } @BuildStep From 9dbfe9c499c4dfbd07be205a39ab81ebda3c3f7b Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Fri, 31 Mar 2023 14:47:46 +0200 Subject: [PATCH 27/27] Qute - fix validation of expressions with the "cdi" namespace - if a bean that has iterable in its set of bean types is used as an iterable in a loop section then the hints are not processed correctly (cherry picked from commit ed7e3c51bd6026f3ecba0ef04edb668f2e1d308e) --- .../io/quarkus/qute/deployment/QuteProcessor.java | 13 +++++++++++-- .../inject/NamedBeanIterableReturnTypeTest.java | 10 +++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index 721d56e9f8350..dcff5f2d06533 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -1023,6 +1023,8 @@ static Match validateNestedExpressions(QuteConfig config, TemplateAnalysis templ BeanInfo bean = findBean(expression, index, incorrectExpressions, namedBeans); if (bean != null) { rootClazz = bean.getImplClazz(); + // Skip the first part - the name of the bean, e.g. for {inject:foo.name} we start validation with "name" + match.setValues(rootClazz, bean.getProviderType()); } else { // Bean not found return putResult(match, results, expression); @@ -1171,8 +1173,15 @@ static Match validateNestedExpressions(QuteConfig config, TemplateAnalysis templ } } else { if (INJECT_NAMESPACE.equals(namespace) || CDI_NAMESPACE.equals(namespace)) { - // Skip the first part - the name of the bean, e.g. for {inject:foo.name} we start validation with "name" - match.setValues(rootClazz, Type.create(rootClazz.name(), org.jboss.jandex.Type.Kind.CLASS)); + if (root.hasHints()) { + // Root is not a type info but a property with hint + // E.g. 'it' and 'STATUS' + if (processHints(templateAnalysis, root.asHintInfo().hints, match, index, expression, + generatedIdsToMatches, incorrectExpressions)) { + // In some cases it's necessary to reset the iterator + iterator = parts.iterator(); + } + } } else if (templateData != null) { // Set the root type and reset the iterator match.setValues(rootClazz, Type.create(rootClazz.name(), org.jboss.jandex.Type.Kind.CLASS)); diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/NamedBeanIterableReturnTypeTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/NamedBeanIterableReturnTypeTest.java index 1314c6e301d19..76515ed78d452 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/NamedBeanIterableReturnTypeTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/NamedBeanIterableReturnTypeTest.java @@ -5,6 +5,7 @@ import java.util.List; import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; import javax.inject.Inject; import javax.inject.Named; @@ -26,6 +27,7 @@ public class NamedBeanIterableReturnTypeTest { "{@java.lang.String field}" + "{#if cdi:validation.hasViolations(field)}" + "{#each cdi:validation.getViolations(field)}{it}{/each}" + + "{#each cdi:violations}:{it.toUpperCase}{/each}" + "{/if}"), "templates/validate.html")); @@ -34,7 +36,7 @@ public class NamedBeanIterableReturnTypeTest { @Test public void testResult() { - assertEquals("Foo!", validate.data("field", "foo").render()); + assertEquals("Foo!:BAR:BAZ", validate.data("field", "foo").render()); } @ApplicationScoped @@ -48,6 +50,12 @@ public boolean hasViolations(String field) { public List getViolations(String field) { return List.of("Foo!"); } + + @Named("violations") + @Produces + public List getViolations() { + return List.of("bar", "baz"); + } } }