keycloak/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/DockerKeycloakDistribution....

248 lines
8.5 KiB
Java

package org.keycloak.it.utils;
import org.jboss.logging.Logger;
import org.keycloak.common.Version;
import org.keycloak.it.junit5.extension.CLIResult;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.ToStringConsumer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.images.RemoteDockerImage;
import org.testcontainers.images.builder.ImageFromDockerfile;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.LazyFuture;
import org.testcontainers.utility.ResourceReaper;
import java.io.File;
import java.lang.reflect.Field;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
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 String stdout = "";
private String stderr = "";
private ToStringConsumer backupConsumer = new ToStringConsumer();
private File dockerScriptFile = new File("../../container/ubi-null.sh");
private GenericContainer<?> keycloakContainer = null;
private String containerId = null;
private Executor parallelReaperExecutor = Executors.newSingleThreadExecutor();
public DockerKeycloakDistribution(boolean debug, boolean manualStop, boolean reCreate) {
this.debug = debug;
this.manualStop = manualStop;
}
private GenericContainer getKeycloakContainer() {
File distributionFile = new File("../../dist/" + File.separator + "target" + File.separator + "keycloak-" + Version.VERSION + ".tar.gz");
if (!distributionFile.exists()) {
distributionFile = Maven.resolveArtifact("org.keycloak", "keycloak-quarkus-dist").toFile();
}
if (!distributionFile.exists()) {
throw new RuntimeException("Distribution archive " + distributionFile.getAbsolutePath() +" doesn't exist");
}
File dockerFile = new File("../../container/Dockerfile");
LazyFuture<String> image;
if (dockerFile.exists()) {
image = new ImageFromDockerfile("keycloak-under-test", false)
.withFileFromFile("keycloak.tar.gz", distributionFile)
.withFileFromFile("ubi-null.sh", dockerScriptFile)
.withFileFromFile("Dockerfile", dockerFile)
.withBuildArg("KEYCLOAK_DIST", "keycloak.tar.gz");
toString();
} else {
image = new RemoteDockerImage(DockerImageName.parse("quay.io/keycloak/keycloak"));
}
return new GenericContainer(image)
.withExposedPorts(8080)
.withStartupAttempts(1)
.withStartupTimeout(Duration.ofSeconds(120))
.waitingFor(Wait.forListeningPort());
}
@Override
public CLIResult run(List<String> arguments) {
stop();
try {
this.exitCode = -1;
this.stdout = "";
this.stderr = "";
this.containerId = null;
this.backupConsumer = new ToStringConsumer();
keycloakContainer = getKeycloakContainer();
keycloakContainer
.withLogConsumer(backupConsumer)
.withCommand(arguments.toArray(new String[0]))
.start();
containerId = keycloakContainer.getContainerId();
waitForStableOutput();
} catch (Exception cause) {
this.exitCode = -1;
this.stdout = backupConsumer.toUtf8String();
this.stderr = backupConsumer.toUtf8String();
cleanupContainer();
keycloakContainer = null;
LOGGER.warn("Failed to start Keycloak container", cause);
}
trySetRestAssuredPort();
return CLIResult.create(getOutputStream(), getErrorStream(), getExitCode());
}
private void trySetRestAssuredPort() {
try {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Class<?> restAssured = classLoader.loadClass("io.restassured.RestAssured");
Field port = restAssured.getDeclaredField("port");
port.set(null, keycloakContainer.getMappedPort(8080));
} catch (Exception ignore) {
// keeping the workaround to set the container port to restassured
// TODO: better way to expose the port to tests
}
}
// After the web server is responding we are still producing some logs that got checked in the tests
private void waitForStableOutput() {
int retry = 10;
String lastLine = "";
boolean stableOutput = false;
while (!stableOutput) {
if (keycloakContainer.isRunning()) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
String[] splitted = keycloakContainer.getLogs().split(System.lineSeparator());
String newLastLine = splitted[splitted.length - 1];
retry -= 1;
stableOutput = lastLine.equals(newLastLine) | (retry <= 0);
lastLine = newLastLine;
} else {
stableOutput = true;
}
}
}
@Override
public void stop() {
try {
if (keycloakContainer != null) {
containerId = keycloakContainer.getContainerId();
this.stdout = fetchOutputStream();
this.stderr = fetchErrorStream();
keycloakContainer.stop();
this.exitCode = 0;
}
} catch (Exception cause) {
this.exitCode = -1;
throw new RuntimeException("Failed to stop the server", cause);
} finally {
cleanupContainer();
keycloakContainer = null;
}
}
private void cleanupContainer() {
if (containerId != null) {
try {
final String finalContainerId = containerId;
Runnable reaper = new Runnable() {
@Override
public void run() {
try {
ResourceReaper
.instance()
.stopAndRemoveContainer(finalContainerId);
} catch (Exception cause) {
throw new RuntimeException("Failed to stop and remove container", cause);
}
}
};
parallelReaperExecutor.execute(reaper);
} catch (Exception cause) {
throw new RuntimeException("Failed to schecdule the removal of the container", cause);
}
}
}
private String fetchOutputStream() {
if (keycloakContainer != null && keycloakContainer.isRunning()) {
return keycloakContainer.getLogs(OutputFrame.OutputType.STDOUT);
} else if (this.stdout.isEmpty()) {
return backupConsumer.toUtf8String();
} else {
return this.stdout;
}
}
@Override
public List<String> getOutputStream() {
return List.of(fetchOutputStream().split("\n"));
}
public String fetchErrorStream() {
if (keycloakContainer != null && keycloakContainer.isRunning()) {
return keycloakContainer.getLogs(OutputFrame.OutputType.STDERR);
} else if (this.stderr.isEmpty()) {
return backupConsumer.toUtf8String();
} else {
return this.stderr;
}
}
@Override
public List<String> getErrorStream() {
return List.of(fetchErrorStream().split("\n"));
}
@Override
public int getExitCode() {
return this.exitCode;
}
@Override
public boolean isDebug() {
return this.debug;
}
@Override
public boolean isManualStop() {
return this.manualStop;
}
@Override
public <D extends KeycloakDistribution> D unwrap(Class<D> type) {
if (!KeycloakDistribution.class.isAssignableFrom(type)) {
throw new IllegalArgumentException("Not a " + KeycloakDistribution.class + " type");
}
if (type.isInstance(this)) {
return (D) this;
}
throw new IllegalArgumentException("Not a " + type + " type");
}
}