mirror of
https://github.com/keycloak/keycloak.git
synced 2026-02-18 18:37:54 -05:00
Allow re-using server when running tests with the new framework
Closes #44101 Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
parent
4f91b5246e
commit
8aaf3e4606
12 changed files with 576 additions and 134 deletions
|
|
@ -101,10 +101,11 @@ Valid values:
|
|||
|
||||
Configuration:
|
||||
|
||||
| Value | Description |
|
||||
|---------------------------------------------------|----------------------------------------------------------------------------------------|
|
||||
| `kc.test.server.config` / `KC_TEST_SERVER_CONFIG` | The name of a KeycloakServerConfig class to use when running the tests |
|
||||
| `kc.test.server.kcw` / `KC_TEST_SERVER_KCW` | Set to a kcw command to use kcw with remote server (see `kcw help` for valid commands) |
|
||||
| Value | Description |
|
||||
|---------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
|
||||
| `kc.test.server.reuse` / `KC_TEST_SERVER_REUSE` | If enabled for the `distribution` server the started server will be left running and re-used for subsequent runs |
|
||||
| `kc.test.server.config` / `KC_TEST_SERVER_CONFIG` | The name of a KeycloakServerConfig class to use when running the tests |
|
||||
| `kc.test.server.kcw` / `KC_TEST_SERVER_KCW` | Set to a kcw command to use kcw with remote server (see `kcw help` for valid commands) |
|
||||
|
||||
|
||||
### Database
|
||||
|
|
|
|||
|
|
@ -131,10 +131,6 @@ public class ClusteredKeycloakServer implements KeycloakServer {
|
|||
for (var dependency : configBuilder.toDependencies()) {
|
||||
container.copyProvider(dependency.getGroupId(), dependency.getArtifactId());
|
||||
}
|
||||
|
||||
for(var config : configBuilder.toConfigFiles()) {
|
||||
container.copyConfigFile(config);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1,9 +1,22 @@
|
|||
package org.keycloak.testframework.database;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.testframework.util.TmpDir;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
public class DevFileDatabaseSupplier extends AbstractDatabaseSupplier {
|
||||
|
||||
private static final File DB_DIR = Path.of(TmpDir.resolveTmpDir().getAbsolutePath(), "kc-test-framework", "h2").toFile();
|
||||
|
||||
@ConfigProperty(name = "reuse", defaultValue = "false")
|
||||
boolean reuse;
|
||||
|
||||
@Override
|
||||
public String getAlias() {
|
||||
return "dev-file";
|
||||
|
|
@ -11,25 +24,46 @@ public class DevFileDatabaseSupplier extends AbstractDatabaseSupplier {
|
|||
|
||||
@Override
|
||||
TestDatabase getTestDatabase() {
|
||||
return new DevFileTestDatabase();
|
||||
return new DevFileTestDatabase(reuse);
|
||||
}
|
||||
|
||||
private static class DevFileTestDatabase implements TestDatabase {
|
||||
|
||||
private final boolean reuse;
|
||||
|
||||
public DevFileTestDatabase(boolean reuse) {
|
||||
this.reuse = reuse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(DatabaseConfiguration config) {
|
||||
if (config.getInitScript() != null)
|
||||
deleteDatabase();
|
||||
if (config.getInitScript() != null) {
|
||||
throw new IllegalArgumentException("init script not supported, configure h2 properties via --db-url-properties");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
// TODO Should we clean-up H2 database here?
|
||||
deleteDatabase();
|
||||
}
|
||||
|
||||
private void deleteDatabase() {
|
||||
if (!reuse && DB_DIR.exists()) {
|
||||
try {
|
||||
FileUtils.deleteDirectory(DB_DIR);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to delete directory: " + DB_DIR.getAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> serverConfig() {
|
||||
return Map.of("db", "dev-file");
|
||||
return Map.of(
|
||||
"db", "dev-file",
|
||||
"db-url", "jdbc:h2:file:" + DB_DIR + "/keycloak.db;DB_CLOSE_ON_EXIT=true;NON_KEYWORDS=VALUE;DB_CLOSE_DELAY=0"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,65 +1,240 @@
|
|||
package org.keycloak.testframework.server;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import org.keycloak.it.utils.OutputConsumer;
|
||||
import org.keycloak.it.utils.RawKeycloakDistribution;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.KeycloakBuilder;
|
||||
import org.keycloak.common.Version;
|
||||
import org.keycloak.it.utils.Maven;
|
||||
import org.keycloak.quarkus.runtime.Environment;
|
||||
import org.keycloak.representations.info.ServerInfoRepresentation;
|
||||
import org.keycloak.testframework.util.FileUtils;
|
||||
import org.keycloak.testframework.util.ProcessUtils;
|
||||
import org.keycloak.testframework.util.TmpDir;
|
||||
|
||||
import io.quarkus.fs.util.ZipUtils;
|
||||
import io.quarkus.maven.dependency.Dependency;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class DistributionKeycloakServer implements KeycloakServer {
|
||||
|
||||
private static final boolean MANUAL_STOP = true;
|
||||
private static final boolean RE_CREATE = false;
|
||||
private static final boolean REMOVE_BUILD_OPTIONS_AFTER_BUILD = false;
|
||||
private static final int REQUEST_PORT = 8080;
|
||||
private static final Logger log = Logger.getLogger(DistributionKeycloakServer.class);
|
||||
|
||||
private RawKeycloakDistribution keycloak;
|
||||
private static final File INSTALL_DIR = Path.of(TmpDir.resolveTmpDir().getAbsolutePath(), "kc-test-framework", "keycloak").toFile();
|
||||
private static final String CMD = "kc" + (Environment.isWindows() ? ".bat" : ".sh");
|
||||
|
||||
private File keycloakHomeDir;
|
||||
private Process keycloakProcess;
|
||||
|
||||
private final boolean debug;
|
||||
private final boolean reuse;
|
||||
private final long startTimeout;
|
||||
private boolean tlsEnabled = false;
|
||||
|
||||
public DistributionKeycloakServer(boolean debug, long startTimeout) {
|
||||
public DistributionKeycloakServer(boolean debug, boolean reuse, long startTimeout) {
|
||||
this.debug = debug;
|
||||
this.reuse = reuse;
|
||||
this.startTimeout = startTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(KeycloakServerConfigBuilder keycloakServerConfigBuilder, boolean tlsEnabled) {
|
||||
this.tlsEnabled = tlsEnabled;
|
||||
keycloak = new RawKeycloakDistribution(false, MANUAL_STOP, false, RE_CREATE, REMOVE_BUILD_OPTIONS_AFTER_BUILD, REQUEST_PORT, new LoggingOutputConsumer())
|
||||
.withThreadDump(false)
|
||||
.withThrowErrorIfFailedToStart(true);
|
||||
|
||||
if (startTimeout > 0) {
|
||||
keycloak.withStartTimeout(startTimeout);
|
||||
List<String> args = keycloakServerConfigBuilder.toArgs();
|
||||
Set<Dependency> dependencies = keycloakServerConfigBuilder.toDependencies();
|
||||
|
||||
if (!reuse) {
|
||||
killPreviousProcess();
|
||||
}
|
||||
|
||||
// RawKeycloakDistribution sets "DEBUG_SUSPEND", not "DEBUG" when debug is passed to constructor
|
||||
try {
|
||||
boolean installationCreated = createInstallation();
|
||||
|
||||
File providersDir = new File(keycloakHomeDir, "providers");
|
||||
List<File> existingProviders = listExistingProviders(providersDir);
|
||||
|
||||
if (!installationCreated && reuse && ping()) {
|
||||
checkRunning();
|
||||
|
||||
File startupArgsFile = getServerArgsFile();
|
||||
String startedWithArgs = startupArgsFile.isFile() ? FileUtils.readStringFromFile(startupArgsFile) : null;
|
||||
String requestedArgs = String.join(" ", args);
|
||||
|
||||
Set<String> requestedDependencies = dependencies.stream().map(d -> d.getGroupId() + "__" + d.getArtifactId() + ".jar").collect(Collectors.toSet());
|
||||
Set<String> startedWithDependencies = existingProviders.stream().map(File::getName).collect(Collectors.toSet());
|
||||
|
||||
if (requestedArgs.equals(startedWithArgs) && setEquals(requestedDependencies, startedWithDependencies)) {
|
||||
log.trace("Re-using already running Keycloak");
|
||||
return;
|
||||
} else {
|
||||
if (killPreviousProcess()) {
|
||||
log.trace("Killed existing Keycloak");
|
||||
} else {
|
||||
throw new RuntimeException("Running Keycloak not started with required arguments or providers, and could not kill the current process");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateProviders(existingProviders, dependencies, providersDir);
|
||||
|
||||
OutputHandler outputHandler = startKeycloak(args);
|
||||
|
||||
waitForStart(outputHandler);
|
||||
|
||||
FileUtils.writeToFile(getServerArgsFile(), String.join(" ", args));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkRunning() {
|
||||
if (!Environment.isWindows()) {
|
||||
ProcessBuilder pb = new ProcessBuilder("fuser", "-n", "tcp", tlsEnabled ? "8443" : "8080");
|
||||
try {
|
||||
Process process = pb.start();
|
||||
process.waitFor(1, TimeUnit.SECONDS);
|
||||
String pid = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8).trim();
|
||||
String expectedPid = FileUtils.readStringFromFile(getPidFile());
|
||||
if (!pid.equals(expectedPid)) {
|
||||
throw new RuntimeException("Process running on port is not a managed Keycloak server");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else {
|
||||
ServerInfoRepresentation serverInfo;
|
||||
try {
|
||||
serverInfo = getServerInfo();
|
||||
} catch (Throwable t) {
|
||||
throw new RuntimeException("Non-managed Keycloak server or other process running on " + getBaseUrl());
|
||||
}
|
||||
File userDir = new File(serverInfo.getSystemInfo().getUserDir()).getParentFile();
|
||||
if (!userDir.equals(keycloakHomeDir)) {
|
||||
throw new RuntimeException("Non-managed Keycloak server running from " + userDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull DistributionKeycloakServer.OutputHandler startKeycloak(List<String> args) {
|
||||
log.trace("Starting Keycloak");
|
||||
List<String> cmd = new LinkedList<>();
|
||||
if (Environment.isWindows()) {
|
||||
cmd.add(keycloakHomeDir.toPath().resolve("bin").resolve(CMD).toString());
|
||||
} else {
|
||||
cmd.add("./" + CMD);
|
||||
}
|
||||
cmd.addAll(args);
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(cmd);
|
||||
pb.directory(new File(keycloakHomeDir, "bin"));
|
||||
|
||||
if (debug) {
|
||||
keycloak.setEnvVar("DEBUG", "true");
|
||||
pb.environment().put("DEBUG", "true");
|
||||
}
|
||||
|
||||
for (Dependency dependency : keycloakServerConfigBuilder.toDependencies()) {
|
||||
keycloak.copyProvider(dependency.getGroupId(), dependency.getArtifactId());
|
||||
OutputHandler outputHandler;
|
||||
try {
|
||||
keycloakProcess = pb.start();
|
||||
outputHandler = new OutputHandler(keycloakProcess);
|
||||
new Thread(outputHandler).start();
|
||||
|
||||
ProcessHandle descendent = ProcessUtils.waitForDescendent(keycloakProcess);
|
||||
FileUtils.writeToFile(getPidFile(), descendent.pid());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
for (Path configFile : keycloakServerConfigBuilder.toConfigFiles()) {
|
||||
keycloak.copyConfigFile(configFile);
|
||||
}
|
||||
return outputHandler;
|
||||
}
|
||||
|
||||
keycloak.run(keycloakServerConfigBuilder.toArgs());
|
||||
private static void updateProviders(List<File> existingProviders, Set<Dependency> dependencies, File providersDir) throws IOException {
|
||||
existingProviders.stream()
|
||||
.filter(f -> f.getName().endsWith(".jar"))
|
||||
.filter(f -> {
|
||||
String fileName = f.getName();
|
||||
String groupId = fileName.substring(0, fileName.indexOf("__"));
|
||||
String artifactId = fileName.substring(fileName.indexOf("__") + 2, fileName.lastIndexOf(".jar"));
|
||||
return dependencies.stream().noneMatch(d -> d.getGroupId().equals(groupId) && d.getArtifactId().equals(artifactId));
|
||||
}).forEach(f -> {
|
||||
log.trace("Deleted non-requested provider: " + f.getAbsolutePath());
|
||||
FileUtils.delete(f);
|
||||
FileUtils.delete(new File(f.getAbsolutePath() + ".lastModified"));
|
||||
});
|
||||
|
||||
Path providersPath = providersDir.toPath();
|
||||
for (Dependency d : dependencies) {
|
||||
Path dependencyPath = Maven.resolveArtifact(d.getGroupId(), d.getArtifactId());
|
||||
File dependencyFile = dependencyPath.toFile();
|
||||
Path targetPath = providersPath.resolve(d.getGroupId() + "__" + d.getArtifactId() + ".jar");
|
||||
File targetFile = targetPath.toFile();
|
||||
File targetLastModified = new File(targetFile.getAbsolutePath() + ".lastModified");
|
||||
long lastModified = targetLastModified.isFile() ? FileUtils.readLongFromFile(targetLastModified) : -1;
|
||||
|
||||
if (lastModified != dependencyPath.toFile().lastModified() || !targetFile.isFile()) {
|
||||
log.trace("Adding or overriding existing provider: " + targetPath.toFile().getAbsolutePath());
|
||||
Files.copy(dependencyPath, targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
Files.writeString(targetLastModified.toPath(), Long.toString(dependencyFile.lastModified()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
keycloak.stop();
|
||||
if (!reuse) {
|
||||
ProcessUtils.killRunningProcess(keycloakProcess);
|
||||
|
||||
File pidFile = getPidFile();
|
||||
if (pidFile.exists()) {
|
||||
FileUtils.delete(pidFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean killPreviousProcess() {
|
||||
if (!Environment.isWindows()) {
|
||||
File pidFile = getPidFile();
|
||||
if (pidFile.exists()) {
|
||||
try {
|
||||
String previousPid = FileUtils.readStringFromFile(pidFile);
|
||||
if (ProcessUtils.killProcess(previousPid)) {
|
||||
log.trace("Killed running managed Keycloak: " + previousPid);
|
||||
FileUtils.delete(pidFile);
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -80,51 +255,204 @@ public class DistributionKeycloakServer implements KeycloakServer {
|
|||
}
|
||||
}
|
||||
|
||||
private static final class LoggingOutputConsumer implements OutputConsumer {
|
||||
private boolean createInstallation() throws IOException {
|
||||
File dist = resolveKeycloakDist();
|
||||
|
||||
if (INSTALL_DIR.isDirectory()) {
|
||||
File[] f = INSTALL_DIR.listFiles();
|
||||
if (f != null && f.length == 1) {
|
||||
long fromZipLastModified = FileUtils.readLongFromFile(getZipLastModifiedFile(f[0]));
|
||||
if (fromZipLastModified != dist.lastModified()) {
|
||||
log.trace("Deleting installation from a previous distribution");
|
||||
FileUtils.delete(INSTALL_DIR);
|
||||
} else {
|
||||
log.trace("Re-using previous installation");
|
||||
keycloakHomeDir = f[0];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (INSTALL_DIR.isDirectory()) {
|
||||
FileUtils.delete(INSTALL_DIR);
|
||||
}
|
||||
if (!INSTALL_DIR.mkdirs()) {
|
||||
throw new IOException("Failed to create directory " + INSTALL_DIR);
|
||||
}
|
||||
|
||||
ZipUtils.unzip(dist.toPath(), INSTALL_DIR.toPath());
|
||||
|
||||
File[] files = INSTALL_DIR.listFiles();
|
||||
if (files == null || files.length != 1) {
|
||||
throw new RuntimeException("Expected " + INSTALL_DIR.getAbsolutePath() + " to contain a single directory");
|
||||
}
|
||||
keycloakHomeDir = files[0];
|
||||
|
||||
if (!Path.of(keycloakHomeDir.getPath(), "bin", CMD).toFile().setExecutable(true)) {
|
||||
throw new RuntimeException("Failed to make startup script executable");
|
||||
}
|
||||
|
||||
FileUtils.writeToFile(getZipLastModifiedFile(keycloakHomeDir), dist.lastModified());
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean ping() {
|
||||
try {
|
||||
HttpURLConnection urlConnection = (HttpURLConnection) new URL(getBaseUrl()).openConnection();
|
||||
urlConnection.setConnectTimeout(1000);
|
||||
urlConnection.setReadTimeout(1000);
|
||||
if(urlConnection instanceof HttpsURLConnection httpsURLConnection) {
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, new TrustManager[] { new NullTrustManager() }, new SecureRandom());
|
||||
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
|
||||
httpsURLConnection.setSSLSocketFactory(socketFactory);
|
||||
}
|
||||
urlConnection.connect();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void waitForStart(OutputHandler outputHandler) {
|
||||
boolean started = outputHandler.waitForStarted();
|
||||
if (started && ping()) {
|
||||
return;
|
||||
}
|
||||
keycloakProcess.destroy();
|
||||
throw new RuntimeException("Keycloak did not start within timeout: " + getErrorOutput());
|
||||
}
|
||||
|
||||
private File getZipLastModifiedFile(File dir) {
|
||||
return new File(dir, "zip-last-modified");
|
||||
}
|
||||
|
||||
private File getPidFile() {
|
||||
return new File(keycloakHomeDir, "pid");
|
||||
}
|
||||
|
||||
private File getServerArgsFile() {
|
||||
return new File(keycloakHomeDir, "startup-args");
|
||||
}
|
||||
|
||||
private ServerInfoRepresentation getServerInfo() {
|
||||
KeycloakBuilder kcb = KeycloakBuilder.builder()
|
||||
.serverUrl(getBaseUrl())
|
||||
.realm("master")
|
||||
.clientId("temp-admin")
|
||||
.clientSecret("mysecret")
|
||||
.grantType("client_credentials");
|
||||
|
||||
Keycloak kc = kcb.build();
|
||||
ServerInfoRepresentation info = kc.serverInfo().getInfo();
|
||||
kc.close();
|
||||
return info;
|
||||
}
|
||||
|
||||
private String getErrorOutput() {
|
||||
try {
|
||||
return new String(keycloakProcess.getErrorStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static File resolveKeycloakDist() {
|
||||
Path p = Path.of(System.getProperty("user.dir"));
|
||||
String dist = "quarkus/dist/target/" + "keycloak-" + Version.VERSION + ".zip";
|
||||
while (p.resolve("pom.xml").toFile().isFile()) {
|
||||
File zip = p.resolve(dist).toFile();
|
||||
if (zip.isFile()) {
|
||||
return zip;
|
||||
}
|
||||
p = p.getParent();
|
||||
}
|
||||
|
||||
return Maven.resolveArtifact("org.keycloak", "keycloak-quarkus-dist").toFile();
|
||||
}
|
||||
|
||||
private boolean setEquals(Set<String> a, Set<String> b) {
|
||||
return a.size() == b.size() && a.containsAll(b);
|
||||
}
|
||||
|
||||
private List<File> listExistingProviders(File providersDir) {
|
||||
if (providersDir.isDirectory()) {
|
||||
File[] files = providersDir.listFiles(n -> n.getName().endsWith(".jar"));
|
||||
if (files != null) {
|
||||
return Arrays.stream(files).toList();
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private class OutputHandler implements Runnable {
|
||||
|
||||
private static final Pattern LOG_PATTERN = Pattern.compile("([^ ]*) ([^ ]*) ([A-Z]*)([ ]*)(.*)");
|
||||
private static final Logger LOGGER = Logger.getLogger("managed.keycloak");
|
||||
|
||||
@Override
|
||||
public void onStdOut(String line) {
|
||||
onLine(line, Logger.Level.INFO);
|
||||
private boolean startedInPrinted = false;
|
||||
private final Process process;
|
||||
|
||||
private CountDownLatch startupLatch = new CountDownLatch(1);
|
||||
|
||||
private OutputHandler(Process process) {
|
||||
this.process = process;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onErrOut(String line) {
|
||||
onLine(line, Logger.Level.ERROR);
|
||||
}
|
||||
public void run() {
|
||||
InputStream is = process.getInputStream();
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
|
||||
try {
|
||||
for (String line = br.readLine(); process.isAlive() && line != null; line = br.readLine()) {
|
||||
if (!startedInPrinted && line.matches(".*Keycloak.* started in.*")) {
|
||||
startupLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getStdOut() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getErrOut() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private void onLine(String line, Logger.Level defaultLevel) {
|
||||
Matcher matcher = LOG_PATTERN.matcher(line);
|
||||
if (matcher.matches()) {
|
||||
String levelString = matcher.group(3);
|
||||
String message = matcher.group(5);
|
||||
if (levelString != null && message != null) {
|
||||
for (Logger.Level l : Logger.Level.values()) {
|
||||
if (l.name().equals(levelString)) {
|
||||
LOGGER.log(l, message);
|
||||
return;
|
||||
Matcher matcher = LOG_PATTERN.matcher(line);
|
||||
if (matcher.matches()) {
|
||||
String levelString = matcher.group(3);
|
||||
String message = matcher.group(5);
|
||||
if (levelString != null && message != null) {
|
||||
for (Logger.Level l : Logger.Level.values()) {
|
||||
if (l.name().equals(levelString)) {
|
||||
LOGGER.log(l, message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LOGGER.info(line);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.log(defaultLevel, line);
|
||||
public boolean waitForStarted() {
|
||||
try {
|
||||
startupLatch.await(startTimeout, TimeUnit.SECONDS);
|
||||
return true;
|
||||
} catch (InterruptedException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class NullTrustManager implements X509TrustManager {
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,15 +7,18 @@ public class DistributionKeycloakServerSupplier extends AbstractKeycloakServerSu
|
|||
|
||||
private static final Logger LOGGER = Logger.getLogger(DistributionKeycloakServerSupplier.class);
|
||||
|
||||
@ConfigProperty(name = "start.timeout", defaultValue = "-1")
|
||||
@ConfigProperty(name = "start.timeout", defaultValue = "120")
|
||||
long startTimeout;
|
||||
|
||||
@ConfigProperty(name = "debug", defaultValue = "false")
|
||||
boolean debug;
|
||||
boolean debug = false;
|
||||
|
||||
@ConfigProperty(name = "reuse", defaultValue = "false")
|
||||
boolean reuse;
|
||||
|
||||
@Override
|
||||
public KeycloakServer getServer() {
|
||||
return new DistributionKeycloakServer(debug, startTimeout);
|
||||
return new DistributionKeycloakServer(debug, reuse, startTimeout);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1,21 +1,15 @@
|
|||
package org.keycloak.testframework.server;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.keycloak.Keycloak;
|
||||
import org.keycloak.common.Version;
|
||||
import org.keycloak.platform.Platform;
|
||||
|
||||
import io.quarkus.maven.dependency.Dependency;
|
||||
|
||||
public class EmbeddedKeycloakServer implements KeycloakServer {
|
||||
|
||||
private Keycloak keycloak;
|
||||
private Path homeDir;
|
||||
private boolean tlsEnabled = false;
|
||||
|
||||
@Override
|
||||
|
|
@ -27,29 +21,6 @@ public class EmbeddedKeycloakServer implements KeycloakServer {
|
|||
builder.addDependency(dependency.getGroupId(), dependency.getArtifactId(), "");
|
||||
}
|
||||
|
||||
Set<Path> configFiles = keycloakServerConfigBuilder.toConfigFiles();
|
||||
if (!configFiles.isEmpty()) {
|
||||
if (homeDir == null) {
|
||||
homeDir = Platform.getPlatform().getTmpDirectory().toPath();
|
||||
}
|
||||
|
||||
Path conf = homeDir.resolve("conf");
|
||||
|
||||
if (!conf.toFile().exists()) {
|
||||
conf.toFile().mkdirs();
|
||||
}
|
||||
|
||||
for (Path configFile : configFiles) {
|
||||
try {
|
||||
Files.copy(configFile, conf.resolve(configFile.getFileName()));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
builder.setHomeDir(homeDir);
|
||||
keycloak = builder.start(keycloakServerConfigBuilder.toArgs());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
package org.keycloak.testframework.server;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
|
@ -31,7 +27,6 @@ public class KeycloakServerConfigBuilder {
|
|||
private final Set<String> featuresDisabled = new HashSet<>();
|
||||
private final LogBuilder log = new LogBuilder();
|
||||
private final Set<Dependency> dependencies = new HashSet<>();
|
||||
private final Set<Path> configFiles = new HashSet<>();
|
||||
private CacheType cacheType = CacheType.LOCAL;
|
||||
private boolean externalInfinispan = false;
|
||||
|
||||
|
|
@ -106,18 +101,6 @@ public class KeycloakServerConfigBuilder {
|
|||
dependencies.add(new DependencyBuilder().setGroupId(groupId).setArtifactId(artifactId).build());
|
||||
return this;
|
||||
}
|
||||
|
||||
public KeycloakServerConfigBuilder cacheConfigFile(String resourcePath) {
|
||||
try {
|
||||
Path p = Paths.get(Objects.requireNonNull(getClass().getResource(resourcePath)).toURI());
|
||||
configFiles.add(p);
|
||||
option("cache-config-file", p.getFileName().toString());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public class LogBuilder {
|
||||
|
||||
|
|
@ -235,10 +218,6 @@ public class KeycloakServerConfigBuilder {
|
|||
return dependencies;
|
||||
}
|
||||
|
||||
Set<Path> toConfigFiles() {
|
||||
return configFiles;
|
||||
}
|
||||
|
||||
private Set<String> toFeatureStrings(Profile.Feature... features) {
|
||||
return Arrays.stream(features).map(f -> {
|
||||
if (f.getVersion() > 1 || Profile.getFeatureVersions(f.getKey()).size() > 1) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package org.keycloak.testframework.server;
|
|||
|
||||
import java.net.ConnectException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
|
@ -71,15 +70,6 @@ public class RemoteKeycloakServer implements KeycloakServer {
|
|||
}
|
||||
out.println();
|
||||
}
|
||||
|
||||
Set<Path> configFiles = config.toConfigFiles();
|
||||
if (!configFiles.isEmpty()) {
|
||||
out.println("Config files:");
|
||||
for (Path c : configFiles) {
|
||||
out.print("* " + c.toAbsolutePath());
|
||||
}
|
||||
out.println();
|
||||
}
|
||||
}
|
||||
|
||||
private void printStartupInstructionsKcw(KeycloakServerConfigBuilder config) {
|
||||
|
|
@ -92,12 +82,6 @@ public class RemoteKeycloakServer implements KeycloakServer {
|
|||
out.println("KCW_PROVIDERS=" + dependencyPaths + " \\");
|
||||
}
|
||||
|
||||
Set<Path> configFiles = config.toConfigFiles();
|
||||
if (!configFiles.isEmpty()) {
|
||||
String configPaths = configFiles.stream().map(p -> p.toAbsolutePath().toString()).collect(Collectors.joining(","));
|
||||
out.println("KCW_CONFIGS=" + configPaths + " \\");
|
||||
}
|
||||
|
||||
out.println("kcw " + kcwCommand + " " + String.join(" \\\n", config.toArgs()));
|
||||
out.println();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
package org.keycloak.testframework.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class FileUtils {
|
||||
|
||||
public static long readLongFromFile(File file) {
|
||||
return Long.parseLong(readStringFromFile(file));
|
||||
}
|
||||
|
||||
public static String readStringFromFile(File file) {
|
||||
try {
|
||||
return org.apache.commons.io.FileUtils.readFileToString(file, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeToFile(File file, long value) {
|
||||
writeToFile(file, Long.toString(value));
|
||||
}
|
||||
|
||||
public static void writeToFile(File file, String value) {
|
||||
try {
|
||||
org.apache.commons.io.FileUtils.writeStringToFile(file, value);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void delete(File file) {
|
||||
if (file.isFile()) {
|
||||
if (!file.delete()) {
|
||||
throw new RuntimeException("Failed to delete file: " + file.getAbsolutePath());
|
||||
}
|
||||
} else if (file.isDirectory()) {
|
||||
try {
|
||||
org.apache.commons.io.FileUtils.deleteDirectory(file);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to delete directory: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package org.keycloak.testframework.util;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.keycloak.quarkus.runtime.Environment;
|
||||
|
||||
public class ProcessUtils {
|
||||
|
||||
public static ProcessHandle waitForDescendent(Process process) {
|
||||
long timeout = System.currentTimeMillis() + 5000;
|
||||
while (System.currentTimeMillis() < timeout) {
|
||||
Optional<ProcessHandle> descendent = process.descendants().findFirst();
|
||||
if (descendent.isPresent()) {
|
||||
return descendent.get();
|
||||
}
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Descendent process not started within timeout");
|
||||
}
|
||||
|
||||
public static boolean killProcess(String pid) {
|
||||
try {
|
||||
if (!Environment.isWindows()) {
|
||||
ProcessBuilder pb = new ProcessBuilder("kill", "--timeout", "10000", "TERM", "--timeout", "10000", "KILL", pid);
|
||||
Process process = pb.start();
|
||||
process.waitFor(10, TimeUnit.SECONDS);
|
||||
return process.exitValue() == 0;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void killRunningProcess(Process process) {
|
||||
killRunningProcess(process, false);
|
||||
}
|
||||
|
||||
public static void killRunningProcess(Process process, boolean force) {
|
||||
try {
|
||||
if (Environment.isWindows()) {
|
||||
CompletableFuture<?> allProcesses = CompletableFuture.completedFuture(null);
|
||||
Iterator<ProcessHandle> itr = process.descendants().iterator();
|
||||
while (itr.hasNext()) {
|
||||
ProcessHandle ph = itr.next();
|
||||
if (force) {
|
||||
ph.destroyForcibly();
|
||||
} else {
|
||||
ph.destroy();
|
||||
}
|
||||
allProcesses = CompletableFuture.allOf(allProcesses, ph.onExit());
|
||||
}
|
||||
allProcesses.get(10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
if (force) {
|
||||
process.destroyForcibly();
|
||||
} else {
|
||||
process.destroy();
|
||||
}
|
||||
process.waitFor(10, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
if (!force) {
|
||||
killRunningProcess(process, true);
|
||||
} else {
|
||||
throw new RuntimeException("Failed to stop Keycloak process");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package org.keycloak.testframework.util;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class TmpDir {
|
||||
|
||||
// Maven overrides java.io.tmpdir, resolving to OS tmp directory
|
||||
public static File resolveTmpDir() {
|
||||
File tmpDir = new File("/tmp");
|
||||
if (tmpDir.isDirectory()) {
|
||||
return tmpDir;
|
||||
}
|
||||
tmpDir = new File(System.getenv("TEMP"));
|
||||
if (tmpDir.isDirectory()) {
|
||||
return tmpDir;
|
||||
}
|
||||
return new File(System.getProperty("java.io.tmpdir"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ public class InfinispanXMLBackwardCompatibilityTest {
|
|||
|
||||
@Override
|
||||
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
|
||||
return config.cacheConfigFile(CONFIG_FILE);
|
||||
return config.option("cache-config-file", getClass().getResource(CONFIG_FILE).getFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue