mirror of
https://github.com/keycloak/keycloak.git
synced 2026-05-28 04:13:22 -04:00
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:
parent
f9373a247c
commit
46b1899178
22 changed files with 438 additions and 113 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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]*)([ ]*)(.*)");
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 + " \\");
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue