From 46b1899178d6df36a14a38d3a7b25c839fbaf883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Vacek?= <86605314+vaceksimon@users.noreply.github.com> Date: Mon, 23 Feb 2026 08:01:03 +0100 Subject: [PATCH] Hot deploy custom providers from module to test server (#45556) * Hot deploy provider module Closes #34188 Signed-off-by: Simon Vacek * fix for external projects and add deployCurrentProject Signed-off-by: Simon Vacek * address review comments Signed-off-by: Simon Vacek * improve dependency compatibility check Signed-off-by: Simon Vacek --------- Signed-off-by: Simon Vacek --- .../java/org/keycloak/it/utils/Maven.java | 27 ++-- test-framework/core/pom.xml | 8 + .../AuthenticationFlowConfigBuilder.java | 1 + .../realm/ClientConfigBuilder.java | 1 + .../realm/GroupConfigBuilder.java | 1 + .../realm/RealmConfigBuilder.java | 1 + .../realm/RoleConfigBuilder.java | 1 + .../realm/UserConfigBuilder.java | 1 + .../server/DistributionKeycloakServer.java | 65 +------- .../server/EmbeddedKeycloakServer.java | 3 +- .../server/KeycloakDependency.java | 58 +++++++ .../testframework/server/KeycloakServer.java | 6 + .../server/KeycloakServerConfigBuilder.java | 27 +++- .../server/ProviderDeployer.java | 141 ++++++++++++++++++ .../server/RemoteKeycloakServer.java | 8 +- .../{realm => util}/Collections.java | 22 +-- .../testframework/util/MavenProjectUtil.java | 80 ++++++++++ test-framework/examples/providers/pom.xml | 6 + .../MyCustomRealmResourceProvider.java | 2 + .../MyCustomProviderWithinSameModuleTest.java | 51 +++++++ .../test/examples/MyCustomProviderTest.java | 30 ++-- test-framework/pom.xml | 11 ++ 22 files changed, 438 insertions(+), 113 deletions(-) create mode 100644 test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakDependency.java create mode 100644 test-framework/core/src/main/java/org/keycloak/testframework/server/ProviderDeployer.java rename test-framework/core/src/main/java/org/keycloak/testframework/{realm => util}/Collections.java (63%) create mode 100644 test-framework/core/src/main/java/org/keycloak/testframework/util/MavenProjectUtil.java create mode 100644 test-framework/examples/providers/src/test/java/org/keycloak/providers/example/MyCustomProviderWithinSameModuleTest.java diff --git a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/Maven.java b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/Maven.java index 2f707a58632..ee902e57461 100644 --- a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/Maven.java +++ b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/Maven.java @@ -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; } } diff --git a/test-framework/core/pom.xml b/test-framework/core/pom.xml index b0def975613..bf6a7adb907 100755 --- a/test-framework/core/pom.xml +++ b/test-framework/core/pom.xml @@ -81,6 +81,14 @@ io.quarkus quarkus-bootstrap-maven-resolver + + org.jboss.shrinkwrap + shrinkwrap-api + + + org.jboss.shrinkwrap + shrinkwrap-impl-base + diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/realm/AuthenticationFlowConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/testframework/realm/AuthenticationFlowConfigBuilder.java index 9a148e7d74a..d76b1577c00 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/realm/AuthenticationFlowConfigBuilder.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/realm/AuthenticationFlowConfigBuilder.java @@ -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 { diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/realm/ClientConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/testframework/realm/ClientConfigBuilder.java index 72408cd2d8d..7bae14f6bf5 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/realm/ClientConfigBuilder.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/realm/ClientConfigBuilder.java @@ -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 { diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/realm/GroupConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/testframework/realm/GroupConfigBuilder.java index 3277f80a1a5..faaaa3a70e7 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/realm/GroupConfigBuilder.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/realm/GroupConfigBuilder.java @@ -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; diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/realm/RealmConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/testframework/realm/RealmConfigBuilder.java index 3a9a25c8784..11b5f0fac35 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/realm/RealmConfigBuilder.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/realm/RealmConfigBuilder.java @@ -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 { diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/realm/RoleConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/testframework/realm/RoleConfigBuilder.java index 348bad94e9c..52c78781336 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/realm/RoleConfigBuilder.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/realm/RoleConfigBuilder.java @@ -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 { diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/realm/UserConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/testframework/realm/UserConfigBuilder.java index 29774502079..2c1bfe733b0 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/realm/UserConfigBuilder.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/realm/UserConfigBuilder.java @@ -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 { 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 ba3bbf1e8cb..17ff0249a11 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 @@ -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 args = keycloakServerConfigBuilder.toArgs(); - Set dependencies = keycloakServerConfigBuilder.toDependencies(); try { boolean installationCreated = createInstallation(); @@ -77,8 +70,7 @@ public class DistributionKeycloakServer implements KeycloakServer { killPreviousProcess(); } - File providersDir = new File(keycloakHomeDir, "providers"); - List 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 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)) { + 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 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() { if (!reuse) { @@ -370,20 +329,6 @@ public class DistributionKeycloakServer implements KeycloakServer { 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]*)([ ]*)(.*)"); 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 62998ad7c28..030e06e8ed6 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 @@ -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(""); diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakDependency.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakDependency.java new file mode 100644 index 00000000000..9eed2fddba2 --- /dev/null +++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakDependency.java @@ -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); + } + + } +} diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServer.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServer.java index c68d9270a43..d38c30e7186 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServer.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServer.java @@ -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)); + } + } 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 d22ad46964d..d0c2322d8bb 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 @@ -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 features = new HashSet<>(); private final Set featuresDisabled = new HashSet<>(); private final LogBuilder log = new LogBuilder(); - private final Set dependencies = new HashSet<>(); + private final Set 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 toDependencies() { + Set toDependencies() { return dependencies; } diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/ProviderDeployer.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/ProviderDeployer.java new file mode 100644 index 00000000000..7957174bf85 --- /dev/null +++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/ProviderDeployer.java @@ -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 requestedDependencies; + + ProviderDeployer(Logger log, File keycloakHomeDir, Set 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 requestedJarNames = requestedDependencies.stream() + .map(this::getDependencyJarName) + .collect(Collectors.toSet()); + + List 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 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 stream = Files.walk(path)) { + return stream + .filter(Files::isRegularFile) + .mapToLong(p -> p.toFile().lastModified()) + .max() + .orElse(0); + } + } + +} 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 882833630ec..3643beef6d6 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 @@ -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 dependencies = config.toDependencies(); + Set 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 dependencies = config.toDependencies(); + Set 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 + " \\"); diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/realm/Collections.java b/test-framework/core/src/main/java/org/keycloak/testframework/util/Collections.java similarity index 63% rename from test-framework/core/src/main/java/org/keycloak/testframework/realm/Collections.java rename to test-framework/core/src/main/java/org/keycloak/testframework/util/Collections.java index 0d9e04a062b..726d03bc44d 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/realm/Collections.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/util/Collections.java @@ -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 List combine(List l1, List l2) { + public static List combine(List l1, List l2) { if (l1 == null) { return new LinkedList<>(l2); } else { @@ -25,16 +25,16 @@ class Collections { } @SafeVarargs - static List combine(List l1, T... items) { + public static List combine(List l1, T... items) { return combine(l1, Arrays.asList(items)); } - static List combine(List l1, Stream items) { + public static List combine(List l1, Stream items) { return combine(l1, items.toList()); } - static Set combine(Set s1, Set s2) { + public static Set combine(Set s1, Set s2) { if (s1 == null) { return new HashSet<>(s2); } else { @@ -44,16 +44,16 @@ class Collections { } @SafeVarargs - static Set combine(Set s1, T... items) { + public static Set combine(Set s1, T... items) { return combine(s1, Set.of(items)); } - static Set combine(Set s1, Stream items) { + public static Set combine(Set s1, Stream items) { return combine(s1, items.collect(Collectors.toSet())); } - static Map> combine(Map> m1, Map> m2) { + public static Map> combine(Map> m1, Map> m2) { if (m1 == null) { m1 = new HashMap<>(); } @@ -66,11 +66,11 @@ class Collections { } @SafeVarargs - static Map> combine(Map> m1, K key, V... values) { + public static Map> combine(Map> m1, K key, V... values) { return combine(m1, Map.of(key, List.of(values))); } - static Map> combine(Map> m1, K key, Stream values) { + public static Map> combine(Map> m1, K key, Stream values) { return combine(m1, Map.of(key, values.toList())); } diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/util/MavenProjectUtil.java b/test-framework/core/src/main/java/org/keycloak/testframework/util/MavenProjectUtil.java new file mode 100644 index 00000000000..577e4dcbd1e --- /dev/null +++ b/test-framework/core/src/main/java/org/keycloak/testframework/util/MavenProjectUtil.java @@ -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 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); + } +} diff --git a/test-framework/examples/providers/pom.xml b/test-framework/examples/providers/pom.xml index fb8bd77ab74..d2766f53695 100644 --- a/test-framework/examples/providers/pom.xml +++ b/test-framework/examples/providers/pom.xml @@ -36,6 +36,12 @@ jakarta.ws.rs jakarta.ws.rs-api + + org.keycloak.testframework + keycloak-test-framework-core + ${project.version} + test + diff --git a/test-framework/examples/providers/src/main/java/org/keycloak/providers/example/MyCustomRealmResourceProvider.java b/test-framework/examples/providers/src/main/java/org/keycloak/providers/example/MyCustomRealmResourceProvider.java index 6dd9431d31e..64aeb738e53 100644 --- a/test-framework/examples/providers/src/main/java/org/keycloak/providers/example/MyCustomRealmResourceProvider.java +++ b/test-framework/examples/providers/src/main/java/org/keycloak/providers/example/MyCustomRealmResourceProvider.java @@ -11,6 +11,8 @@ import org.keycloak.services.resource.RealmResourceProvider; /** * + * @see org.keycloak.providers.example.MyCustomProviderWithinSameModuleTest + * @see org.keycloak.test.examples.MyCustomProviderTest * @author Simon Vacek */ public class MyCustomRealmResourceProvider implements RealmResourceProvider { diff --git a/test-framework/examples/providers/src/test/java/org/keycloak/providers/example/MyCustomProviderWithinSameModuleTest.java b/test-framework/examples/providers/src/test/java/org/keycloak/providers/example/MyCustomProviderWithinSameModuleTest.java new file mode 100644 index 00000000000..55eaddd1b18 --- /dev/null +++ b/test-framework/examples/providers/src/test/java/org/keycloak/providers/example/MyCustomProviderWithinSameModuleTest.java @@ -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 Simon Vacek + */ +@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(); + } + + } +} diff --git a/test-framework/examples/tests/src/test/java/org/keycloak/test/examples/MyCustomProviderTest.java b/test-framework/examples/tests/src/test/java/org/keycloak/test/examples/MyCustomProviderTest.java index 81ab619e1ef..94c9a26ed95 100644 --- a/test-framework/examples/tests/src/test/java/org/keycloak/test/examples/MyCustomProviderTest.java +++ b/test-framework/examples/tests/src/test/java/org/keycloak/test/examples/MyCustomProviderTest.java @@ -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 Simon Vacek */ @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); } } diff --git a/test-framework/pom.xml b/test-framework/pom.xml index fdceea37806..f98017a42c0 100755 --- a/test-framework/pom.xml +++ b/test-framework/pom.xml @@ -34,6 +34,7 @@ 2.0.3 + 1.2.6 @@ -79,6 +80,16 @@ testcontainers-postgresql ${version.testcontainers} + + org.jboss.shrinkwrap + shrinkwrap-api + ${version.shrinkwrap} + + + org.jboss.shrinkwrap + shrinkwrap-impl-base + ${version.shrinkwrap} +