Hot deploy custom providers from module to test server (#45556)

* Hot deploy provider module

Closes #34188

Signed-off-by: Simon Vacek <simonvacky@email.cz>

* fix for external projects and add deployCurrentProject

Signed-off-by: Simon Vacek <simonvacky@email.cz>

* address review comments

Signed-off-by: Simon Vacek <simonvacky@email.cz>

* improve dependency compatibility check

Signed-off-by: Simon Vacek <simonvacky@email.cz>

---------

Signed-off-by: Simon Vacek <simonvacky@email.cz>
This commit is contained in:
Šimon Vacek 2026-02-23 08:01:03 +01:00 committed by GitHub
parent f9373a247c
commit 46b1899178
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 438 additions and 113 deletions

View file

@ -139,12 +139,7 @@ public final class Maven {
public static Path getKeycloakQuarkusModulePath() {
// Find keycloak-parent module first
BootstrapMavenContext ctx = null;
try {
ctx = bootstrapCurrentMavenContext();
} catch (BootstrapMavenException | URISyntaxException e) {
throw new RuntimeException("Failed bootstrap maven context", e);
}
BootstrapMavenContext ctx = bootstrapCurrentMavenContext();
for (LocalProject m = ctx.getCurrentProject(); m != null; m = m.getLocalParent()) {
if ("keycloak-parent".equals(m.getArtifactId())) {
// When found, advance to quarkus module
@ -155,14 +150,18 @@ public final class Maven {
throw new RuntimeException("Failed to find keycloak-parent module.");
}
public static BootstrapMavenContext bootstrapCurrentMavenContext() throws BootstrapMavenException, URISyntaxException {
if (context == null) {
Path classPathDir = Paths.get(Thread.currentThread().getContextClassLoader().getResource(".").toURI());
Path projectDir = BuildToolHelper.getProjectDir(classPathDir);
context = new BootstrapMavenContext(
BootstrapMavenContext.config().setPreferPomsFromWorkspace(true).setWorkspaceModuleParentHierarchy(true)
.setCurrentProject(projectDir.toString()));
public static BootstrapMavenContext bootstrapCurrentMavenContext() {
try {
if (context == null) {
Path classPathDir = Paths.get(Thread.currentThread().getContextClassLoader().getResource(".").toURI());
Path projectDir = BuildToolHelper.getProjectDir(classPathDir);
context = new BootstrapMavenContext(
BootstrapMavenContext.config().setPreferPomsFromWorkspace(true).setWorkspaceModuleParentHierarchy(true)
.setCurrentProject(projectDir.toString()));
}
return context;
} catch (BootstrapMavenException | URISyntaxException e) {
throw new RuntimeException("Failed bootstrap maven context", e);
}
return context;
}
}

View file

@ -81,6 +81,14 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-bootstrap-maven-resolver</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap</groupId>
<artifactId>shrinkwrap-api</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap</groupId>
<artifactId>shrinkwrap-impl-base</artifactId>
</dependency>
</dependencies>
<build>

View file

@ -2,6 +2,7 @@ package org.keycloak.testframework.realm;
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.testframework.util.Collections;
public class AuthenticationFlowConfigBuilder {

View file

@ -6,6 +6,7 @@ import java.util.List;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.testframework.util.Collections;
public class ClientConfigBuilder {

View file

@ -4,6 +4,7 @@ import java.util.List;
import java.util.Map;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.testframework.util.Collections;
public class GroupConfigBuilder {
private final GroupRepresentation rep;

View file

@ -18,6 +18,7 @@ import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.RolesRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testframework.util.Collections;
public class RealmConfigBuilder {

View file

@ -4,6 +4,7 @@ import java.util.List;
import java.util.Map;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.testframework.util.Collections;
public class RoleConfigBuilder {

View file

@ -10,6 +10,7 @@ import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testframework.util.Collections;
public class UserConfigBuilder {

View file

@ -8,20 +8,15 @@ 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.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;
@ -39,7 +34,6 @@ 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;
@ -69,7 +63,6 @@ public class DistributionKeycloakServer implements KeycloakServer {
this.tlsEnabled = tlsEnabled;
List<String> args = keycloakServerConfigBuilder.toArgs();
Set<Dependency> dependencies = keycloakServerConfigBuilder.toDependencies();
try {
boolean installationCreated = createInstallation();
@ -77,8 +70,7 @@ public class DistributionKeycloakServer implements KeycloakServer {
killPreviousProcess();
}
File providersDir = new File(keycloakHomeDir, "providers");
List<File> existingProviders = listExistingProviders(providersDir);
ProviderDeployer providerDeployer = new ProviderDeployer(log, keycloakHomeDir, keycloakServerConfigBuilder.toDependencies(), KeycloakServer.getDependencyHotDeployEnabled());
if (!installationCreated && reuse && ping()) {
checkRunning();
@ -87,10 +79,8 @@ public class DistributionKeycloakServer implements KeycloakServer {
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)) {
boolean dependenciesChanged = providerDeployer.updateDependencies();
if (requestedArgs.equals(startedWithArgs) && !dependenciesChanged) {
log.trace("Re-using already running Keycloak");
return;
} else {
@ -100,10 +90,10 @@ public class DistributionKeycloakServer implements KeycloakServer {
throw new RuntimeException("Running Keycloak not started with required arguments or providers, and could not kill the current process");
}
}
} else {
providerDeployer.updateDependencies();
}
updateProviders(existingProviders, dependencies, providersDir);
OutputHandler outputHandler = startKeycloak(args);
waitForStart(outputHandler);
@ -174,37 +164,6 @@ public class DistributionKeycloakServer implements KeycloakServer {
return outputHandler;
}
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() {
if (!reuse) {
@ -370,20 +329,6 @@ public class DistributionKeycloakServer implements KeycloakServer {
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]*)([ ]*)(.*)");

View file

@ -7,7 +7,6 @@ import org.keycloak.Keycloak;
import org.keycloak.common.Version;
import org.keycloak.it.utils.Maven;
import io.quarkus.maven.dependency.Dependency;
import org.eclipse.aether.artifact.Artifact;
public class EmbeddedKeycloakServer implements KeycloakServer {
@ -20,7 +19,7 @@ public class EmbeddedKeycloakServer implements KeycloakServer {
Keycloak.Builder builder = Keycloak.builder().setVersion(Version.VERSION);
this.tlsEnabled = tlsEnabled;
for(Dependency dependency : keycloakServerConfigBuilder.toDependencies()) {
for(KeycloakDependency dependency : keycloakServerConfigBuilder.toDependencies()) {
var version = Optional.ofNullable(Maven.getArtifact(dependency.getGroupId(), dependency.getArtifactId()))
.map(Artifact::getVersion)
.orElse("");

View file

@ -0,0 +1,58 @@
package org.keycloak.testframework.server;
import io.quarkus.maven.dependency.ArtifactDependency;
import io.quarkus.maven.dependency.DependencyBuilder;
public class KeycloakDependency extends ArtifactDependency {
private final boolean hotDeployable;
private final boolean dependencyCurrentProject;
private KeycloakDependency(Builder dependencyBuilder) {
super(dependencyBuilder);
this.hotDeployable = dependencyBuilder.hotDeployable;
this.dependencyCurrentProject = dependencyBuilder.dependencyCurrentProject;
}
public boolean isHotDeployable() {
return this.hotDeployable;
}
public boolean dependencyCurrentProject() {
return this.dependencyCurrentProject;
}
public static class Builder extends DependencyBuilder {
private boolean hotDeployable = false;
private boolean dependencyCurrentProject = false;
public Builder hotDeployable(boolean hotDeployable) {
this.hotDeployable = hotDeployable;
return this;
}
public Builder dependencyCurrentProject(boolean dependencyCurrentProject) {
this.dependencyCurrentProject = dependencyCurrentProject;
return this;
}
@Override
public Builder setGroupId(String groupId) {
super.setGroupId(groupId);
return this;
}
@Override
public Builder setArtifactId(String artifactId) {
super.setArtifactId(artifactId);
return this;
}
@Override
public KeycloakDependency build() {
return new KeycloakDependency(this);
}
}
}

View file

@ -1,5 +1,7 @@
package org.keycloak.testframework.server;
import org.keycloak.testframework.config.Config;
public interface KeycloakServer {
void start(KeycloakServerConfigBuilder keycloakServerConfigBuilder, boolean tlsEnabled);
@ -10,4 +12,8 @@ public interface KeycloakServer {
String getManagementBaseUrl();
static boolean getDependencyHotDeployEnabled() {
return Boolean.parseBoolean(Config.getValueTypeConfig(KeycloakServer.class, "hot.deploy", "false", String.class));
}
}

View file

@ -12,8 +12,6 @@ import java.util.stream.Collectors;
import org.keycloak.common.Profile;
import org.keycloak.testframework.infinispan.CacheType;
import io.quarkus.maven.dependency.Dependency;
import io.quarkus.maven.dependency.DependencyBuilder;
import io.smallrye.config.SmallRyeConfig;
import org.eclipse.microprofile.config.spi.ConfigSource;
@ -26,7 +24,7 @@ public class KeycloakServerConfigBuilder {
private final Set<String> features = new HashSet<>();
private final Set<String> featuresDisabled = new HashSet<>();
private final LogBuilder log = new LogBuilder();
private final Set<Dependency> dependencies = new HashSet<>();
private final Set<KeycloakDependency> dependencies = new HashSet<>();
private CacheType cacheType = CacheType.LOCAL;
private boolean externalInfinispan = false;
@ -172,10 +170,29 @@ public class KeycloakServerConfigBuilder {
* @return
*/
public KeycloakServerConfigBuilder dependency(String groupId, String artifactId) {
dependencies.add(new DependencyBuilder().setGroupId(groupId).setArtifactId(artifactId).build());
return dependency(groupId, artifactId, false, false);
}
public KeycloakServerConfigBuilder dependency(String groupId, String artifactId, boolean hotDeployable) {
return dependency(groupId, artifactId, hotDeployable, false);
}
private KeycloakServerConfigBuilder dependency(String groupId, String artifactId, boolean hotDeployable, boolean dependencyCurrentProject) {
dependencies.add(
new KeycloakDependency.Builder()
.setGroupId(groupId)
.setArtifactId(artifactId)
.hotDeployable(hotDeployable)
.dependencyCurrentProject(dependencyCurrentProject)
.build()
);
return this;
}
public KeycloakServerConfigBuilder dependencyCurrentProject() {
return dependency("", "", false, true);
}
public class LogBuilder {
private Boolean color;
@ -288,7 +305,7 @@ public class KeycloakServerConfigBuilder {
return args;
}
Set<Dependency> toDependencies() {
Set<KeycloakDependency> toDependencies() {
return dependencies;
}

View file

@ -0,0 +1,141 @@
package org.keycloak.testframework.server;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.keycloak.it.utils.Maven;
import org.keycloak.testframework.util.FileUtils;
import org.keycloak.testframework.util.MavenProjectUtil;
import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject;
import org.jboss.logging.Logger;
final class ProviderDeployer {
private final Logger log;
private final File providersDir;
private final boolean hotDeployEnabled;
private final Set<KeycloakDependency> requestedDependencies;
ProviderDeployer(Logger log, File keycloakHomeDir, Set<KeycloakDependency> requestedDependencies, boolean hotDeployEnabled) {
this.log = log;
this.providersDir = new File(keycloakHomeDir, "providers");
this.requestedDependencies = requestedDependencies;
this.hotDeployEnabled = hotDeployEnabled;
}
boolean updateDependencies() throws IOException {
boolean anyDependenciesModified = deleteNotRequestedDependencies();
for (KeycloakDependency d : requestedDependencies) {
boolean shouldPackageClasses = hotDeployEnabled && d.isHotDeployable();
String jarName = getDependencyJarName(d);
Path dependencyPath = getDependencyPath(d);
Path targetPath = providersDir.toPath().resolve(jarName);
File targetFile = targetPath.toFile();
long dependencyLastModified = getMostRecentModification(dependencyPath);
File targetLastModifiedFile = new File(targetFile.getAbsolutePath() + ".lastModified");
long targetLastModified = targetLastModifiedFile.isFile() ? FileUtils.readLongFromFile(targetLastModifiedFile) : -1;
if (dependencyLastModified != targetLastModified || !targetFile.isFile()) {
log.trace("Adding or overwriting existing provider: " + targetPath.toFile().getAbsolutePath());
if (shouldPackageClasses || d.dependencyCurrentProject()) {
MavenProjectUtil.buildJar(jarName, dependencyPath, targetPath);
} else {
Files.copy(dependencyPath, targetPath, StandardCopyOption.REPLACE_EXISTING);
}
Files.writeString(targetLastModifiedFile.toPath(), Long.toString(dependencyLastModified));
anyDependenciesModified = true;
}
}
return anyDependenciesModified;
}
private String getDependencyJarName(KeycloakDependency dependency) {
String groupId = dependency.getGroupId();
String artifactId = dependency.getArtifactId();
if (dependency.dependencyCurrentProject()) {
LocalProject project = MavenProjectUtil.getCurrentModule();
groupId = project.getGroupId();
artifactId = project.getArtifactId();
}
return groupId + "__" + artifactId + ".jar";
}
private boolean deleteNotRequestedDependencies() {
Set<String> requestedJarNames = requestedDependencies.stream()
.map(this::getDependencyJarName)
.collect(Collectors.toSet());
List<File> toDelete = listExistingDependencies().stream()
.filter(f -> !requestedJarNames.contains(f.getName()))
.toList();
for (File f : toDelete) {
String path = f.getAbsolutePath();
log.trace("Deleted non-requested provider: " + path);
FileUtils.delete(f);
FileUtils.delete(new File(path + ".lastModified"));
}
return !toDelete.isEmpty();
}
private List<File> listExistingDependencies() {
if (providersDir.isDirectory()) {
File[] files = providersDir.listFiles(n -> n.getName().endsWith(".jar"));
if (files != null) {
return Arrays.stream(files).toList();
}
}
return List.of();
}
private Path getDependencyPath(KeycloakDependency d) {
if (d.dependencyCurrentProject()) {
return MavenProjectUtil.getCurrentModule().getClassesDir();
}
if (d.isHotDeployable() && hotDeployEnabled) {
return MavenProjectUtil.findLocalModule(d.getGroupId(), d.getArtifactId()).getClassesDir();
}
return Maven.resolveArtifact(d.getGroupId(), d.getArtifactId());
}
private long getMostRecentModification(Path path) throws IOException {
File file = path.toFile();
if (!file.exists()) {
return 0;
}
if (file.isFile()) {
return file.lastModified();
}
try (Stream<Path> stream = Files.walk(path)) {
return stream
.filter(Files::isRegularFile)
.mapToLong(p -> p.toFile().lastModified())
.max()
.orElse(0);
}
}
}

View file

@ -10,8 +10,6 @@ import javax.net.ssl.SSLException;
import org.keycloak.it.utils.Maven;
import org.keycloak.testframework.config.Config;
import io.quarkus.maven.dependency.Dependency;
import static java.lang.System.out;
public class RemoteKeycloakServer implements KeycloakServer {
@ -62,10 +60,10 @@ public class RemoteKeycloakServer implements KeycloakServer {
out.println(String.join(" \\\n", config.toArgs()));
out.println();
Set<Dependency> dependencies = config.toDependencies();
Set<KeycloakDependency> dependencies = config.toDependencies();
if (!dependencies.isEmpty()) {
out.println("Requested providers:");
for (Dependency d : dependencies) {
for (KeycloakDependency d : dependencies) {
out.println("* " + d.getGroupId() + ":" + d.getArtifactId());
}
out.println();
@ -76,7 +74,7 @@ public class RemoteKeycloakServer implements KeycloakServer {
out.println("Remote Keycloak server is not running on " + getBaseUrl() + ", please start Keycloak with:");
out.println();
Set<Dependency> dependencies = config.toDependencies();
Set<KeycloakDependency> dependencies = config.toDependencies();
if (!dependencies.isEmpty()) {
String dependencyPaths = dependencies.stream().map(d -> Maven.resolveArtifact(d.getGroupId(), d.getArtifactId()).toString()).collect(Collectors.joining(","));
out.println("KCW_PROVIDERS=" + dependencyPaths + " \\");

View file

@ -1,4 +1,4 @@
package org.keycloak.testframework.realm;
package org.keycloak.testframework.util;
import java.util.Arrays;
import java.util.HashMap;
@ -10,12 +10,12 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
class Collections {
public class Collections {
private Collections() {
}
static <T> List<T> combine(List<T> l1, List<T> l2) {
public static <T> List<T> combine(List<T> l1, List<T> l2) {
if (l1 == null) {
return new LinkedList<>(l2);
} else {
@ -25,16 +25,16 @@ class Collections {
}
@SafeVarargs
static <T> List<T> combine(List<T> l1, T... items) {
public static <T> List<T> combine(List<T> l1, T... items) {
return combine(l1, Arrays.asList(items));
}
static <T> List<T> combine(List<T> l1, Stream<T> items) {
public static <T> List<T> combine(List<T> l1, Stream<T> items) {
return combine(l1, items.toList());
}
static <T> Set<T> combine(Set<T> s1, Set<T> s2) {
public static <T> Set<T> combine(Set<T> s1, Set<T> s2) {
if (s1 == null) {
return new HashSet<>(s2);
} else {
@ -44,16 +44,16 @@ class Collections {
}
@SafeVarargs
static <T> Set<T> combine(Set<T> s1, T... items) {
public static <T> Set<T> combine(Set<T> s1, T... items) {
return combine(s1, Set.of(items));
}
static <T> Set<T> combine(Set<T> s1, Stream<T> items) {
public static <T> Set<T> combine(Set<T> s1, Stream<T> items) {
return combine(s1, items.collect(Collectors.toSet()));
}
static <K, V> Map<K, List<V>> combine(Map<K, List<V>> m1, Map<K, List<V>> m2) {
public static <K, V> Map<K, List<V>> combine(Map<K, List<V>> m1, Map<K, List<V>> m2) {
if (m1 == null) {
m1 = new HashMap<>();
}
@ -66,11 +66,11 @@ class Collections {
}
@SafeVarargs
static <K, V> Map<K, List<V>> combine(Map<K, List<V>> m1, K key, V... values) {
public static <K, V> Map<K, List<V>> combine(Map<K, List<V>> m1, K key, V... values) {
return combine(m1, Map.of(key, List.of(values)));
}
static <K, V> Map<K, List<V>> combine(Map<K, List<V>> m1, K key, Stream<V> values) {
public static <K, V> Map<K, List<V>> combine(Map<K, List<V>> m1, K key, Stream<V> values) {
return combine(m1, Map.of(key, values.toList()));
}

View file

@ -0,0 +1,80 @@
package org.keycloak.testframework.util;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
import org.keycloak.it.utils.Maven;
import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext;
import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.exporter.ZipExporter;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
public final class MavenProjectUtil {
private static LocalProject rootModuleProject;
private static LocalProject getRootModule() {
if (rootModuleProject != null) {
return rootModuleProject;
}
BootstrapMavenContext ctx = Maven.bootstrapCurrentMavenContext();
LocalProject m = ctx.getCurrentProject();
while (m.getLocalParent() != null) {
m = m.getLocalParent();
}
rootModuleProject = m;
return rootModuleProject;
}
public static LocalProject findLocalModule(String groupId, String artifactId) {
LocalProject rootModule = getRootModule();
LocalProject dependencyModule = rootModule.getWorkspace().getProject(groupId, artifactId);
if (dependencyModule == null) {
throw new RuntimeException("Failed to resolve artifact in this project: [" + groupId + ":" + artifactId + "]");
}
return dependencyModule;
}
public static LocalProject getCurrentModule() {
BootstrapMavenContext ctx = Maven.bootstrapCurrentMavenContext();
return ctx.getCurrentProject();
}
/**
* Builds and exports a JAR from compiled classes and resources.
*
* @param jarName the JAR filename
* @param classesPath path to compiled output directory ({@code target/classes})
* @param targetPath path where to export the JAR
*/
public static void buildJar(String jarName, Path classesPath, Path targetPath) {
JavaArchive providerJar = ShrinkWrap.create(JavaArchive.class, jarName);
try (Stream<Path> sourcePathStream = Files.walk(classesPath)) {
sourcePathStream.filter(Files::isRegularFile)
.forEach(p -> {
String relativeFilePath = classesPath.relativize(p).toString();
if (relativeFilePath.endsWith(".class")) {
String fullyQualifiedClassName = relativeFilePath.replace(File.separatorChar, '.').substring(0, relativeFilePath.lastIndexOf('.'));
providerJar.addClass(fullyQualifiedClassName);
} else {
File resourceFile = p.toFile();
providerJar.addAsResource(resourceFile, relativeFilePath);
}
});
} catch (IOException e) {
throw new RuntimeException(e);
}
providerJar.as(ZipExporter.class).exportTo(targetPath.toFile(), true);
}
}

View file

@ -36,6 +36,12 @@
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak.testframework</groupId>
<artifactId>keycloak-test-framework-core</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View file

@ -11,6 +11,8 @@ import org.keycloak.services.resource.RealmResourceProvider;
/**
*
* @see org.keycloak.providers.example.MyCustomProviderWithinSameModuleTest
* @see org.keycloak.test.examples.MyCustomProviderTest
* @author <a href="mailto:svacek@redhat.com">Simon Vacek</a>
*/
public class MyCustomRealmResourceProvider implements RealmResourceProvider {

View file

@ -0,0 +1,51 @@
package org.keycloak.providers.example;
import java.io.IOException;
import java.net.URL;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.http.simple.SimpleHttp;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.InjectSimpleHttp;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.server.KeycloakServerConfig;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/**
*
* @see org.keycloak.providers.example.MyCustomRealmResourceProvider
* @see org.keycloak.test.examples.MyCustomProviderTest
* @author <a href="mailto:svacek@redhat.com">Simon Vacek</a>
*/
@KeycloakIntegrationTest(config = MyCustomProviderWithinSameModuleTest.ServerConfig.class)
public class MyCustomProviderWithinSameModuleTest {
@InjectRealm
ManagedRealm realm;
@InjectSimpleHttp
SimpleHttp simpleHttp;
@Test
public void httpGetTest() throws IOException {
URL url = KeycloakUriBuilder.fromUri(realm.getBaseUrl()).path("/custom-provider/hello").build().toURL();
String response = simpleHttp.doGet(url.toString()).header("Accept", "text/plain").asString();
Assertions.assertEquals("Hello World!", response);
}
public static class ServerConfig implements KeycloakServerConfig {
@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return config.dependencyCurrentProject();
}
}
}

View file

@ -1,25 +1,25 @@
package org.keycloak.test.examples;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.net.URL;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.http.simple.SimpleHttp;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.InjectSimpleHttp;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.server.KeycloakServerConfig;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.HttpClientBuilder;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/**
*
* @see org.keycloak.providers.example.MyCustomRealmResourceProvider
* @see org.keycloak.providers.example.MyCustomProviderWithinSameModuleTest
* @author <a href="mailto:svacek@redhat.com">Simon Vacek</a>
*/
@KeycloakIntegrationTest(config = MyCustomProviderTest.ServerConfig.class)
@ -28,25 +28,23 @@ public class MyCustomProviderTest {
@InjectRealm
ManagedRealm realm;
@InjectSimpleHttp
SimpleHttp simpleHttp;
@Test
public void httpGetTest() {
String url = realm.getBaseUrl();
public void httpGetTest() throws IOException {
URL url = KeycloakUriBuilder.fromUri(realm.getBaseUrl()).path("/custom-provider/hello").build().toURL();
HttpUriRequest request = new HttpGet(url + "/custom-provider/hello");
try {
HttpResponse response = HttpClientBuilder.create().build().execute(request);
Assertions.assertEquals(200, response.getStatusLine().getStatusCode());
String response = simpleHttp.doGet(url.toString()).header("Accept", "text/plain").asString();
String content = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
Assertions.assertEquals("Hello World!", content);
} catch (IOException ignored) {}
Assertions.assertEquals("Hello World!", response);
}
public static class ServerConfig implements KeycloakServerConfig {
@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return config.dependency("org.keycloak.testframework", "keycloak-test-framework-example-providers");
return config.dependency("org.keycloak.testframework", "keycloak-test-framework-example-providers", true);
}
}

View file

@ -34,6 +34,7 @@
<properties>
<version.testcontainers>2.0.3</version.testcontainers>
<version.shrinkwrap>1.2.6</version.shrinkwrap>
</properties>
<dependencyManagement>
@ -79,6 +80,16 @@
<artifactId>testcontainers-postgresql</artifactId>
<version>${version.testcontainers}</version>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap</groupId>
<artifactId>shrinkwrap-api</artifactId>
<version>${version.shrinkwrap}</version>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap</groupId>
<artifactId>shrinkwrap-impl-base</artifactId>
<version>${version.shrinkwrap}</version>
</dependency>
</dependencies>
</dependencyManagement>