diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a1c600ac149..bf0cf659a81 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -412,6 +412,12 @@ jobs:
- name: Prepare the local distribution archives
run: mvn clean install -DskipTests -Pdistribution
+ - name: Run Quarkus Tests in Docker
+ run: |
+ mvn clean install -nsu -B -f quarkus/tests/pom.xml -Dkc.quarkus.tests.dist=docker | misc/log/trimmer.sh
+ TEST_RESULT=${PIPESTATUS[0]}
+ exit $TEST_RESULT
+
- name: Run Quarkus Integration Tests
run: |
mvn clean install -nsu -B -f quarkus/tests/pom.xml | misc/log/trimmer.sh
diff --git a/quarkus/tests/integration/pom.xml b/quarkus/tests/integration/pom.xml
index 0fb7984c2ae..bf3626fcbc0 100644
--- a/quarkus/tests/integration/pom.xml
+++ b/quarkus/tests/integration/pom.xml
@@ -59,7 +59,28 @@
io.rest-assured
rest-assured
- test
+
+
+ org.testcontainers
+ junit-jupiter
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+
+ kc.quarkus.tests.dist
+ ${kc.quarkus.tests.dist}
+
+
+
+
+
+
+
diff --git a/quarkus/tests/integration/src/main/java/org/junit/rules/TestRule.java b/quarkus/tests/integration/src/main/java/org/junit/rules/TestRule.java
new file mode 100644
index 00000000000..cd0b3843711
--- /dev/null
+++ b/quarkus/tests/integration/src/main/java/org/junit/rules/TestRule.java
@@ -0,0 +1,6 @@
+package org.junit.rules;
+
+// WORKAROUND: https://github.com/testcontainers/testcontainers-java/issues/970#issuecomment-625044008
+@SuppressWarnings("unused")
+public interface TestRule {
+}
diff --git a/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java
index 6f34b560d40..420c48b9e73 100644
--- a/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java
+++ b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java
@@ -27,6 +27,7 @@ import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.keycloak.it.utils.KeycloakDistribution;
+import org.keycloak.it.utils.DockerKeycloakDistribution;
import org.keycloak.it.utils.RawKeycloakDistribution;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.cli.command.Start;
@@ -82,11 +83,24 @@ public class CLITestExtension extends QuarkusMainTestExtension {
}
private KeycloakDistribution createDistribution(DistributionTest config) {
- KeycloakDistribution distribution = new RawKeycloakDistribution(
- config.debug(),
- config.keepAlive(),
- !DistributionTest.ReInstall.NEVER.equals(config.reInstall())
- );
+ KeycloakDistribution distribution = null;
+
+ switch (System.getProperty("kc.quarkus.tests.dist", "raw")) {
+ case "docker":
+ distribution = new DockerKeycloakDistribution(
+ config.debug(),
+ config.keepAlive(),
+ !DistributionTest.ReInstall.NEVER.equals(config.reInstall())
+ );
+ break;
+ case "raw":
+ default:
+ distribution = new RawKeycloakDistribution(
+ config.debug(),
+ config.keepAlive(),
+ !DistributionTest.ReInstall.NEVER.equals(config.reInstall())
+ );
+ }
return distribution;
}
diff --git a/quarkus/tests/integration/src/main/java/org/keycloak/it/utils/DockerKeycloakDistribution.java b/quarkus/tests/integration/src/main/java/org/keycloak/it/utils/DockerKeycloakDistribution.java
new file mode 100644
index 00000000000..33e60fe0f19
--- /dev/null
+++ b/quarkus/tests/integration/src/main/java/org/keycloak/it/utils/DockerKeycloakDistribution.java
@@ -0,0 +1,143 @@
+package org.keycloak.it.utils;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.jboss.logging.Logger;
+import org.keycloak.common.Version;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.output.OutputFrame;
+import org.testcontainers.containers.output.ToStringConsumer;
+import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.containers.wait.strategy.WaitStrategy;
+import org.testcontainers.images.builder.ImageFromDockerfile;
+
+import java.io.File;
+import java.net.URL;
+import java.time.Duration;
+import java.util.List;
+
+public final class DockerKeycloakDistribution implements KeycloakDistribution {
+
+ private static final Logger LOGGER = Logger.getLogger(DockerKeycloakDistribution.class);
+
+ private boolean debug;
+ private boolean manualStop;
+ private int exitCode = -1;
+
+ private List stdout = List.of();
+ private List stderr = List.of();
+ private ToStringConsumer backupConsumer = new ToStringConsumer();
+
+ private File distributionFile = new File("../../../distribution/server-x-dist/target/keycloak.x-" + Version.VERSION_KEYCLOAK + ".tar.gz");
+
+ private GenericContainer keycloakContainer = null;
+
+ private GenericContainer runKeycloakContainer() {
+ if (!distributionFile.exists()) {
+ throw new RuntimeException("Distribution archive " + distributionFile.getAbsolutePath() +" doesn't exists");
+ }
+ File dockerFile = null;
+ try {
+ dockerFile = File.createTempFile("keycloakx", "Dockerfile");
+ FileUtils.copyURLToFile(new URL("https://raw.githubusercontent.com/keycloak/keycloak-containers/main/server-x/Dockerfile"), dockerFile);
+ } catch (Exception cause) {
+ throw new RuntimeException("Cannot download upstream Dockerfile", cause);
+ }
+ return new GenericContainer(
+ new ImageFromDockerfile()
+ .withFileFromFile("keycloakx.tar.gz", distributionFile)
+ .withFileFromFile("Dockerfile", dockerFile)
+ .withBuildArg("KEYCLOAK_DIST", "keycloakx.tar.gz")
+ )
+ .withExposedPorts(8080)
+ .withStartupTimeout(Duration.ofSeconds(40))
+ .withStartupAttempts(1)
+ .waitingFor(Wait.forHttp("/").forStatusCode(200).withReadTimeout(Duration.ofSeconds(2)));
+ }
+
+ public DockerKeycloakDistribution(boolean debug, boolean manualStop, boolean reCreate) {
+ this.debug = debug;
+ this.manualStop = manualStop;
+ }
+
+ @Override
+ public void start(List arguments) {
+ try {
+ this.exitCode = -1;
+ this.stdout = List.of();
+ this.stderr = List.of();
+ this.backupConsumer = new ToStringConsumer();
+
+ keycloakContainer = runKeycloakContainer();
+
+ keycloakContainer
+ .withLogConsumer(backupConsumer)
+ .withCommand(arguments.toArray(new String[0]))
+ .start();
+
+ // TODO: this is based on a lot of assumptions
+ io.restassured.RestAssured.port = keycloakContainer.getMappedPort(8080);
+ } catch (Exception cause) {
+ this.exitCode = -1;
+ this.stdout = List.of(backupConsumer.toUtf8String());
+ this.stderr = List.of(backupConsumer.toUtf8String());
+ keycloakContainer = null;
+ LOGGER.warn("Failed to start Keycloak container", cause);
+ }
+ }
+
+ @Override
+ public void stop() {
+ try {
+ if (keycloakContainer != null) {
+ this.stdout = getOutputStream();
+ this.stderr = getErrorStream();
+
+ keycloakContainer.stop();
+ keycloakContainer = null;
+ this.exitCode = 0;
+ }
+ } catch (Exception cause) {
+ this.exitCode = -1;
+ throw new RuntimeException("Failed to stop the server", cause);
+ }
+ }
+
+ @Override
+ public List getOutputStream() {
+ if (keycloakContainer != null && keycloakContainer.isRunning()) {
+ return List.of(keycloakContainer.getLogs(OutputFrame.OutputType.STDOUT));
+ } else if (this.stdout.isEmpty()) {
+ return List.of(backupConsumer.toUtf8String());
+ } else {
+ return this.stdout;
+ }
+ }
+
+ @Override
+ public List getErrorStream() {
+ if (keycloakContainer != null && keycloakContainer.isRunning()) {
+ return List.of(keycloakContainer.getLogs(OutputFrame.OutputType.STDERR));
+ } else if (this.stderr.isEmpty()) {
+ return List.of(backupConsumer.toUtf8String());
+ } else {
+ return this.stderr;
+ }
+ }
+
+ @Override
+ public int getExitCode() {
+ return this.exitCode;
+ }
+
+ @Override
+ public boolean isDebug() {
+ return this.debug;
+ }
+
+ @Override
+ public boolean isManualStop() {
+ return this.manualStop;
+ }
+}