mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-30 11:29:57 -06:00
Add clustering tests to new test framework
Closes #39962 Signed-off-by: Michal Hajas <mhajas@redhat.com> Co-authored-by: Pedro Ruivo <pruivo@redhat.com>
This commit is contained in:
@@ -105,6 +105,12 @@
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak.testframework</groupId>
|
||||
<artifactId>keycloak-test-framework-clustering</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
</project>
|
||||
|
||||
22
test-framework/clustering/pom.xml
Normal file
22
test-framework/clustering/pom.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.keycloak.testframework</groupId>
|
||||
<artifactId>keycloak-test-framework-parent</artifactId>
|
||||
<version>999.0.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-test-framework-clustering</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak.testframework</groupId>
|
||||
<artifactId>keycloak-test-framework-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.keycloak.testframework;
|
||||
|
||||
import org.keycloak.testframework.clustering.LoadBalancerSupplier;
|
||||
import org.keycloak.testframework.injection.Supplier;
|
||||
import org.keycloak.testframework.server.ClusteredKeycloakServerSupplier;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ClusteringTestFrameworkExtension implements TestFrameworkExtension {
|
||||
|
||||
@Override
|
||||
public List<Supplier<?, ?>> suppliers() {
|
||||
return List.of(new ClusteredKeycloakServerSupplier(), new LoadBalancerSupplier());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.keycloak.testframework.annotations;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface InjectLoadBalancer {
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.keycloak.testframework.clustering;
|
||||
|
||||
import org.keycloak.testframework.server.ClusteredKeycloakServer;
|
||||
import org.keycloak.testframework.server.KeycloakUrls;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class LoadBalancer {
|
||||
private final ClusteredKeycloakServer server;
|
||||
private final HashMap<Integer, KeycloakUrls> urls = new HashMap<>();
|
||||
|
||||
public LoadBalancer(ClusteredKeycloakServer server) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
public KeycloakUrls node(int nodeIndex) {
|
||||
if (nodeIndex >= server.clusterSize()) {
|
||||
throw new IllegalArgumentException("Node index out of bounds. Requested nodeIndex: %d, cluster size: %d".formatted(server.clusterSize(), nodeIndex));
|
||||
}
|
||||
return urls.computeIfAbsent(nodeIndex, i -> new KeycloakUrls(server.getBaseUrl(i), server.getManagementBaseUrl(i)));
|
||||
}
|
||||
|
||||
public int clusterSize() {
|
||||
return server.clusterSize();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.keycloak.testframework.clustering;
|
||||
|
||||
import org.keycloak.testframework.annotations.InjectLoadBalancer;
|
||||
import org.keycloak.testframework.injection.InstanceContext;
|
||||
import org.keycloak.testframework.injection.RequestedInstance;
|
||||
import org.keycloak.testframework.injection.Supplier;
|
||||
import org.keycloak.testframework.server.ClusteredKeycloakServer;
|
||||
import org.keycloak.testframework.server.KeycloakServer;
|
||||
|
||||
public class LoadBalancerSupplier implements Supplier<LoadBalancer, InjectLoadBalancer> {
|
||||
|
||||
@Override
|
||||
public LoadBalancer getValue(InstanceContext<LoadBalancer, InjectLoadBalancer> instanceContext) {
|
||||
KeycloakServer server = instanceContext.getDependency(KeycloakServer.class);
|
||||
|
||||
if (server instanceof ClusteredKeycloakServer clusteredKeycloakServer) {
|
||||
return new LoadBalancer(clusteredKeycloakServer);
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Load balancer can only be used with ClusteredKeycloakServer");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean compatible(InstanceContext<LoadBalancer, InjectLoadBalancer> a, RequestedInstance<LoadBalancer, InjectLoadBalancer> b) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.testframework.server;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.quarkus.bootstrap.utils.BuildToolHelper;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.it.utils.DockerKeycloakDistribution;
|
||||
import org.keycloak.testframework.database.JBossLogConsumer;
|
||||
import org.testcontainers.images.RemoteDockerImage;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
import org.testcontainers.utility.LazyFuture;
|
||||
|
||||
public class ClusteredKeycloakServer implements KeycloakServer {
|
||||
|
||||
private static final boolean MANUAL_STOP = true;
|
||||
private static final int REQUEST_PORT = 8080;
|
||||
private static final int MANAGEMENT_PORT = 9000;
|
||||
public static final String SNAPSHOT_IMAGE = "-";
|
||||
|
||||
private final DockerKeycloakDistribution[] containers;
|
||||
private final String images;
|
||||
|
||||
private static LazyFuture<String> defaultImage() {
|
||||
return DockerKeycloakDistribution.createImage(true);
|
||||
}
|
||||
|
||||
public ClusteredKeycloakServer(int mumServers, String images) {
|
||||
containers = new DockerKeycloakDistribution[mumServers];
|
||||
this.images = images;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(KeycloakServerConfigBuilder configBuilder) {
|
||||
String[] imagePeServer = null;
|
||||
if (images == null || images.isEmpty() || (imagePeServer = images.split(",")).length == 1) {
|
||||
startContainersWithSameImage(configBuilder, imagePeServer == null ? SNAPSHOT_IMAGE : imagePeServer[0]);
|
||||
} else {
|
||||
startContainersWithMixedImage(configBuilder, imagePeServer);
|
||||
}
|
||||
}
|
||||
|
||||
private void startContainersWithMixedImage(KeycloakServerConfigBuilder configBuilder, String[] imagePeServer) {
|
||||
assert imagePeServer != null;
|
||||
if (containers.length != imagePeServer.length) {
|
||||
throw new IllegalArgumentException("The number of containers and the number of images must match");
|
||||
}
|
||||
int[] exposedPorts = new int[]{REQUEST_PORT, MANAGEMENT_PORT};
|
||||
LazyFuture<String> snapshotImage = null;
|
||||
for (int i = 0; i < containers.length; ++i) {
|
||||
LazyFuture<String> resolvedImage;
|
||||
if (SNAPSHOT_IMAGE.equals(imagePeServer[i])) {
|
||||
if (snapshotImage == null) {
|
||||
snapshotImage = defaultImage();
|
||||
}
|
||||
resolvedImage = snapshotImage;
|
||||
} else {
|
||||
resolvedImage = new RemoteDockerImage(DockerImageName.parse(imagePeServer[i]));
|
||||
}
|
||||
var container = new DockerKeycloakDistribution(false, MANUAL_STOP, REQUEST_PORT, exposedPorts, resolvedImage);
|
||||
containers[i] = container;
|
||||
|
||||
copyProvidersAndConfigs(container, configBuilder);
|
||||
|
||||
container.setCustomLogConsumer(new JBossLogConsumer(Logger.getLogger("managed.keycloak." + i)));
|
||||
container.run(configBuilder.toArgs());
|
||||
}
|
||||
}
|
||||
|
||||
private void startContainersWithSameImage(KeycloakServerConfigBuilder configBuilder, String image) {
|
||||
int[] exposedPorts = new int[]{REQUEST_PORT, MANAGEMENT_PORT};
|
||||
LazyFuture<String> imageFuture = image == null || SNAPSHOT_IMAGE.equals(image) ?
|
||||
defaultImage() :
|
||||
new RemoteDockerImage(DockerImageName.parse(image));
|
||||
for (int i = 0; i < containers.length; ++i) {
|
||||
var container = new DockerKeycloakDistribution(false, MANUAL_STOP, REQUEST_PORT, exposedPorts, imageFuture);
|
||||
containers[i] = container;
|
||||
|
||||
copyProvidersAndConfigs(container, configBuilder);
|
||||
|
||||
container.setCustomLogConsumer(new JBossLogConsumer(Logger.getLogger("managed.keycloak." + i)));
|
||||
container.run(configBuilder.toArgs());
|
||||
}
|
||||
}
|
||||
|
||||
private void copyProvidersAndConfigs(DockerKeycloakDistribution container, KeycloakServerConfigBuilder configBuilder) {
|
||||
for (var dependency : configBuilder.toDependencies()) {
|
||||
container.copyProvider(dependency.getGroupId(), dependency.getArtifactId());
|
||||
}
|
||||
|
||||
for(var config : configBuilder.toConfigFiles()) {
|
||||
container.copyConfigFile(config);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
Arrays.stream(containers)
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(DockerKeycloakDistribution::stop);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseUrl() {
|
||||
return getBaseUrl(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManagementBaseUrl() {
|
||||
return getManagementBaseUrl(0);
|
||||
}
|
||||
|
||||
public String getBaseUrl(int index) {
|
||||
return "http://localhost:%d".formatted(containers[index].getMappedPort(REQUEST_PORT));
|
||||
}
|
||||
|
||||
public String getManagementBaseUrl(int index) {
|
||||
return "http://localhost:%d".formatted(containers[index].getMappedPort(MANAGEMENT_PORT));
|
||||
}
|
||||
|
||||
public int clusterSize() {
|
||||
return containers.length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.keycloak.testframework.server;
|
||||
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
public class ClusteredKeycloakServerSupplier extends AbstractKeycloakServerSupplier {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(ClusteredKeycloakServerSupplier.class);
|
||||
|
||||
@ConfigProperty(name = "numContainer", defaultValue = "2")
|
||||
int numContainers = 2;
|
||||
|
||||
@ConfigProperty(name = "images", defaultValue = ClusteredKeycloakServer.SNAPSHOT_IMAGE)
|
||||
String images = ClusteredKeycloakServer.SNAPSHOT_IMAGE;
|
||||
|
||||
@Override
|
||||
public KeycloakServer getServer() {
|
||||
return new ClusteredKeycloakServer(numContainers, images);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresDatabase() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAlias() {
|
||||
return "cluster";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Logger getLogger() {
|
||||
return LOGGER;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String cache() {
|
||||
return "ispn";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.keycloak.testframework.ClusteringTestFrameworkExtension
|
||||
@@ -53,9 +53,13 @@ public abstract class AbstractContainerTestDatabase implements TestDatabase {
|
||||
|
||||
@Override
|
||||
public Map<String, String> serverConfig() {
|
||||
return serverConfig(false);
|
||||
}
|
||||
|
||||
public Map<String, String> serverConfig(boolean internal) {
|
||||
return Map.of(
|
||||
"db", getDatabaseVendor(),
|
||||
"db-url", getJdbcUrl(),
|
||||
"db-url", getJdbcUrl(internal),
|
||||
"db-username", getUsername(),
|
||||
"db-password", getPassword()
|
||||
);
|
||||
@@ -79,8 +83,13 @@ public abstract class AbstractContainerTestDatabase implements TestDatabase {
|
||||
return "keycloak";
|
||||
}
|
||||
|
||||
public String getJdbcUrl() {
|
||||
return container.getJdbcUrl();
|
||||
public String getJdbcUrl(boolean internal) {
|
||||
var url = container.getJdbcUrl();
|
||||
if (internal) {
|
||||
var ip = container.getContainerInfo().getNetworkSettings().getNetworks().values().iterator().next().getIpAddress();
|
||||
return url.replace(container.getHost() + ":" + container.getFirstMappedPort(), ip + ":" + container.getExposedPorts().get(0));
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
public abstract String getDatabaseVendor();
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package org.keycloak.testframework.database;
|
||||
|
||||
import org.keycloak.testframework.annotations.InjectTestDatabase;
|
||||
import org.keycloak.testframework.config.Config;
|
||||
import org.keycloak.testframework.injection.InstanceContext;
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
import org.keycloak.testframework.injection.RequestedInstance;
|
||||
import org.keycloak.testframework.injection.Supplier;
|
||||
import org.keycloak.testframework.injection.SupplierOrder;
|
||||
import org.keycloak.testframework.server.KeycloakServer;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfigInterceptor;
|
||||
|
||||
@@ -37,7 +39,17 @@ public abstract class AbstractDatabaseSupplier implements Supplier<TestDatabase,
|
||||
|
||||
@Override
|
||||
public KeycloakServerConfigBuilder intercept(KeycloakServerConfigBuilder serverConfig, InstanceContext<TestDatabase, InjectTestDatabase> instanceContext) {
|
||||
return serverConfig.options(instanceContext.getValue().serverConfig());
|
||||
String kcServerType = Config.getSelectedSupplier(KeycloakServer.class);
|
||||
TestDatabase database = instanceContext.getValue();
|
||||
|
||||
// If both KeycloakServer and TestDatabase run in container, we need to configure Keycloak with internal
|
||||
// url that is accessible within docker network
|
||||
if ("cluster".equals(kcServerType) &&
|
||||
database instanceof AbstractContainerTestDatabase containerDatabase) {
|
||||
return serverConfig.options(containerDatabase.serverConfig(true));
|
||||
}
|
||||
|
||||
return serverConfig.options(database.serverConfig());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,7 +21,7 @@ public abstract class AbstractKeycloakServerSupplier implements Supplier<Keycloa
|
||||
KeycloakServerConfig serverConfig = SupplierHelpers.getInstance(annotation.config());
|
||||
|
||||
KeycloakServerConfigBuilder command = KeycloakServerConfigBuilder.startDev()
|
||||
.cache("local")
|
||||
.cache(cache())
|
||||
.bootstrapAdminClient(Config.getAdminClientId(), Config.getAdminClientSecret())
|
||||
.bootstrapAdminUser(Config.getAdminUsername(), Config.getAdminPassword());
|
||||
|
||||
@@ -74,6 +74,10 @@ public abstract class AbstractKeycloakServerSupplier implements Supplier<Keycloa
|
||||
|
||||
public abstract Logger getLogger();
|
||||
|
||||
protected String cache() {
|
||||
return "local";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int order() {
|
||||
return SupplierOrder.KEYCLOAK_SERVER;
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
<module>remote</module>
|
||||
<module>remote-providers</module>
|
||||
<module>ui</module>
|
||||
<module>clustering</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
|
||||
Reference in New Issue
Block a user