diff --git a/test-framework/HOW_TO_RUN.md b/test-framework/HOW_TO_RUN.md index f386becdcaa..1833c71f468 100644 --- a/test-framework/HOW_TO_RUN.md +++ b/test-framework/HOW_TO_RUN.md @@ -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 diff --git a/test-framework/clustering/src/main/java/org/keycloak/testframework/server/ClusteredKeycloakServer.java b/test-framework/clustering/src/main/java/org/keycloak/testframework/server/ClusteredKeycloakServer.java index 54e10c36eea..66f79a1d3cc 100644 --- a/test-framework/clustering/src/main/java/org/keycloak/testframework/server/ClusteredKeycloakServer.java +++ b/test-framework/clustering/src/main/java/org/keycloak/testframework/server/ClusteredKeycloakServer.java @@ -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 diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/database/DevFileDatabaseSupplier.java b/test-framework/core/src/main/java/org/keycloak/testframework/database/DevFileDatabaseSupplier.java index e6a1b4286e4..a8827e9b630 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/database/DevFileDatabaseSupplier.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/database/DevFileDatabaseSupplier.java @@ -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 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" + ); } } diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/DistributionKeycloakServer.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/DistributionKeycloakServer.java index b19c993a03d..a2b1e289229 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/server/DistributionKeycloakServer.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/DistributionKeycloakServer.java @@ -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 args = keycloakServerConfigBuilder.toArgs(); + Set 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 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 requestedDependencies = dependencies.stream().map(d -> d.getGroupId() + "__" + d.getArtifactId() + ".jar").collect(Collectors.toSet()); + Set 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 args) { + log.trace("Starting Keycloak"); + List 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 existingProviders, Set 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 a, Set b) { + return a.size() == b.size() && a.containsAll(b); + } + + private List 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 getStdOut() { - return Collections.emptyList(); - } - - @Override - public List 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]; } } diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/DistributionKeycloakServerSupplier.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/DistributionKeycloakServerSupplier.java index 3ad8b180710..46ef8949006 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/server/DistributionKeycloakServerSupplier.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/DistributionKeycloakServerSupplier.java @@ -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 diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/EmbeddedKeycloakServer.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/EmbeddedKeycloakServer.java index 96dc06cef2b..5cbc52388e0 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/server/EmbeddedKeycloakServer.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/EmbeddedKeycloakServer.java @@ -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 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()); } diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServerConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServerConfigBuilder.java index 1f386898403..53a085c92ef 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServerConfigBuilder.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServerConfigBuilder.java @@ -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 featuresDisabled = new HashSet<>(); private final LogBuilder log = new LogBuilder(); private final Set dependencies = new HashSet<>(); - private final Set 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 toConfigFiles() { - return configFiles; - } - private Set toFeatureStrings(Profile.Feature... features) { return Arrays.stream(features).map(f -> { if (f.getVersion() > 1 || Profile.getFeatureVersions(f.getKey()).size() > 1) { diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/RemoteKeycloakServer.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/RemoteKeycloakServer.java index 59acbd9bfbd..882833630ec 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/server/RemoteKeycloakServer.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/RemoteKeycloakServer.java @@ -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 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 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(); } diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/util/FileUtils.java b/test-framework/core/src/main/java/org/keycloak/testframework/util/FileUtils.java new file mode 100644 index 00000000000..a345ca3563f --- /dev/null +++ b/test-framework/core/src/main/java/org/keycloak/testframework/util/FileUtils.java @@ -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); + } + } + + } + +} diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/util/ProcessUtils.java b/test-framework/core/src/main/java/org/keycloak/testframework/util/ProcessUtils.java new file mode 100644 index 00000000000..1e996e86250 --- /dev/null +++ b/test-framework/core/src/main/java/org/keycloak/testframework/util/ProcessUtils.java @@ -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 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 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"); + } + } + } + +} diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/util/TmpDir.java b/test-framework/core/src/main/java/org/keycloak/testframework/util/TmpDir.java new file mode 100644 index 00000000000..6efecfd9ea4 --- /dev/null +++ b/test-framework/core/src/main/java/org/keycloak/testframework/util/TmpDir.java @@ -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")); + } + +} diff --git a/tests/base/src/test/java/org/keycloak/tests/infinispan/InfinispanXMLBackwardCompatibilityTest.java b/tests/base/src/test/java/org/keycloak/tests/infinispan/InfinispanXMLBackwardCompatibilityTest.java index f7d0af18aa5..399bc1a9bc4 100644 --- a/tests/base/src/test/java/org/keycloak/tests/infinispan/InfinispanXMLBackwardCompatibilityTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/infinispan/InfinispanXMLBackwardCompatibilityTest.java @@ -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()); } } }