Add tests for lb-check endpoint

Added documentation why the check retries and updated outdated docs

Closes #25113

Signed-off-by: Michal Hajas <mhajas@redhat.com>
Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
Co-authored-by: Alexander Schwartz <aschwart@redhat.com>
This commit is contained in:
Michal Hajas 2023-12-04 08:53:37 +01:00 committed by GitHub
parent 10bcd896a9
commit d387f13525
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 499 additions and 40 deletions

View file

@ -185,7 +185,7 @@ jobs:
PARAMS["sanity-check-zip"]="-Dtest=StartCommandDistTest,StartDevCommandDistTest,BuildAndStartDistTest,ImportAtStartupDistTest"
PARAMS["zip"]=""
PARAMS["container"]="-Dkc.quarkus.tests.dist=docker"
PARAMS["storage"]="-Ptest-database -Dtest=PostgreSQLDistTest,MariaDBDistTest#testSuccessful,MySQLDistTest#testSuccessful,DatabaseOptionsDistTest,JPAStoreDistTest,HotRodStoreDistTest,MixedStoreDistTest,TransactionConfigurationDistTest"
PARAMS["storage"]="-Ptest-database -Dtest=PostgreSQLDistTest,MariaDBDistTest#testSuccessful,MySQLDistTest#testSuccessful,DatabaseOptionsDistTest,JPAStoreDistTest,HotRodStoreDistTest,MixedStoreDistTest,TransactionConfigurationDistTest,ExternalInfinispanTest"
./mvnw install -pl quarkus/tests/integration -am -DskipTests
./mvnw test -pl quarkus/tests/integration ${PARAMS["${{ matrix.server }}"]} | misc/log/trimmer.sh

View file

@ -62,13 +62,11 @@ Monitoring is necessary to detect degraded setups.
| {jdgserver_name} cluster failure
| If the {jdgserver_name} cluster fails in the active site, {project_name} will not be able to communicate with the external {jdgserver_name}, and the {project_name} service will be unavailable.
Manual switchover to the secondary site is recommended.
Future versions will detect this situation and do an automatic failover.
The loadbalancer will detect the situation as `/lb-check` returns an error, and will fail over to the other site.
When the {jdgserver_name} cluster is restored, its data will be out-of-sync with {project_name}.
Manual operations are required to get {jdgserver_name} in the primary site in sync with the secondary site.
| Loss of service
| Human intervention required
The setup is degraded until the {jdgserver_name} cluster is restored and the session data is re-synchronized to the primary.
| No data loss^3^
| Seconds to minutes (depending on load balancer setup)
| Connectivity {jdgserver_name}
| If the connectivity between the two sites is lost, session information cannot be sent to the other site.

View file

@ -47,6 +47,8 @@ public class HealthDistTest {
.statusCode(404);
when().get("/q/health/ready").then()
.statusCode(404);
when().get("/lb-check").then()
.statusCode(404);
}
@Test
@ -61,6 +63,8 @@ public class HealthDistTest {
// Metrics should not be enabled
when().get("/metrics").then()
.statusCode(404);
when().get("/lb-check").then()
.statusCode(404);
}
@Test
@ -72,6 +76,8 @@ public class HealthDistTest {
.statusCode(200)
.body("checks[0].name", equalTo("Keycloak database connections async health check"))
.body("checks.size()", equalTo(1));
when().get("/lb-check").then()
.statusCode(404);
}
@Test
@ -83,6 +89,8 @@ public class HealthDistTest {
.statusCode(200)
.body("checks[0].name", equalTo("Keycloak database connections health check"))
.body("checks.size()", equalTo(1));
when().get("/lb-check").then()
.statusCode(404);
}
@Test
@ -125,4 +133,11 @@ public class HealthDistTest {
distribution.stop();
}
}
@Test
@Launch({ "start-dev", "--features=multi-site" })
void testLoadBalancerCheck() {
when().get("/lb-check").then()
.statusCode(200);
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright 2021 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.it.storage.database;
import io.quarkus.test.junit.main.Launch;
import org.junit.jupiter.api.Test;
import org.keycloak.common.util.Retry;
import org.keycloak.it.junit5.extension.DistributionTest;
import org.keycloak.it.junit5.extension.InfinispanContainer;
import org.keycloak.it.junit5.extension.WithExternalInfinispan;
import static io.restassured.RestAssured.when;
@DistributionTest(keepAlive = true)
@WithExternalInfinispan
public class ExternalInfinispanTest {
@Test
@Launch({ "start-dev", "--features=multi-site", "--cache=ispn", "--cache-config-file=../../../test-classes/ExternalInfinispan/kcb-infinispan-cache-remote-store-config.xml", "-Djboss.site.name=ISPN" })
void testLoadBalancerCheckFailure() {
when().get("/lb-check").then()
.statusCode(200);
InfinispanContainer.remoteCacheManager.administration().removeCache("sessions");
// The `lb-check` relies on the Infinispan's persistence check status. By default, Infinispan checks in the background every second that the remote store is available.
// So we'll wait on average about one second here for the check to switch its state.
Retry.execute(() -> {
when().get("/lb-check").then()
.statusCode(503);
}, 10, 200);
}
}

View file

@ -0,0 +1,255 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- end::keycloak-ispn-configmap[] -->
<!--
~ Copyright 2019 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.
-->
<!--tag::keycloak-ispn-configmap[] -->
<infinispan
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:infinispan:config:14.0 https://www.infinispan.org/schemas/infinispan-config-14.0.xsd
urn:infinispan:config:store:remote:14.0 https://www.infinispan.org/schemas/infinispan-cachestore-remote-config-14.0.xsd"
xmlns="urn:infinispan:config:14.0">
<!--end::keycloak-ispn-configmap[] -->
<!-- the statistics="true" attribute is not part of the original KC config and was added by Keycloak Benchmark -->
<cache-container name="keycloak" statistics="true">
<transport lock-timeout="60000"/>
<metrics names-as-tags="true" />
<local-cache name="realms" simple-cache="true" statistics="true">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory max-count="10000"/>
</local-cache>
<local-cache name="users" simple-cache="true" statistics="true">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory max-count="10000"/>
</local-cache>
<!--tag::keycloak-ispn-remotestore[] -->
<distributed-cache name="sessions" owners="2" statistics="true">
<expiration lifespan="-1"/>
<persistence passivation="false"> <!--1-->
<remote-store xmlns="urn:infinispan:config:store:remote:14.0"
cache="sessions"
raw-values="true"
shared="true"
segmented="false">
<remote-server host="localhost"
port="11222"/> <!--2-->
<connection-pool max-active="16"
exhausted-action="CREATE_NEW"/>
<security>
<authentication server-name="infinispan">
<digest username="keycloak"
password="Password1!"
realm="default"/> <!--3-->
</authentication>
</security>
</remote-store>
</persistence>
<state-transfer enabled="false"/> <!--5-->
</distributed-cache>
<!--end::keycloak-ispn-remotestore[] -->
<distributed-cache name="authenticationSessions" owners="2" statistics="true">
<expiration lifespan="-1"/>
<persistence passivation="false">
<remote-store xmlns="urn:infinispan:config:store:remote:14.0"
cache="authenticationSessions"
raw-values="true"
shared="true"
segmented="false">
<remote-server host="localhost"
port="11222"/>
<connection-pool max-active="16"
exhausted-action="CREATE_NEW"/>
<security>
<authentication server-name="infinispan">
<digest username="keycloak"
password="Password1!"
realm="default"/>
</authentication>
</security>
</remote-store>
</persistence>
<state-transfer enabled="false"/>
</distributed-cache>
<distributed-cache name="offlineSessions" owners="2" statistics="true">
<expiration lifespan="-1"/>
<persistence passivation="false">
<remote-store xmlns="urn:infinispan:config:store:remote:14.0"
cache="offlineSessions"
raw-values="true"
shared="true"
segmented="false">
<remote-server host="localhost"
port="11222"/>
<connection-pool max-active="16"
exhausted-action="CREATE_NEW"/>
<security>
<authentication server-name="infinispan">
<digest username="keycloak"
password="Password1!"
realm="default"/>
</authentication>
</security>
</remote-store>
</persistence>
<state-transfer enabled="false"/>
</distributed-cache>
<distributed-cache name="clientSessions" owners="2" statistics="true">
<expiration lifespan="-1"/>
<persistence passivation="false">
<remote-store xmlns="urn:infinispan:config:store:remote:14.0"
cache="clientSessions"
raw-values="true"
shared="true"
segmented="false">
<remote-server host="localhost"
port="11222"/>
<connection-pool max-active="16"
exhausted-action="CREATE_NEW"/>
<security>
<authentication server-name="infinispan">
<digest username="keycloak"
password="Password1!"
realm="default"/>
</authentication>
</security>
</remote-store>
</persistence>
<state-transfer enabled="false"/>
</distributed-cache>
<distributed-cache name="offlineClientSessions" owners="2" statistics="true">
<expiration lifespan="-1"/>
<persistence passivation="false">
<remote-store xmlns="urn:infinispan:config:store:remote:14.0"
cache="offlineClientSessions"
raw-values="true"
shared="true"
segmented="false">
<remote-server host="localhost"
port="11222"/>
<connection-pool max-active="16"
exhausted-action="CREATE_NEW"/>
<security>
<authentication server-name="infinispan">
<digest username="keycloak"
password="Password1!"
realm="default"/>
</authentication>
</security>
</remote-store>
</persistence>
<state-transfer enabled="false"/>
</distributed-cache>
<distributed-cache name="loginFailures" owners="2" statistics="true">
<expiration lifespan="-1"/>
<persistence passivation="false">
<remote-store xmlns="urn:infinispan:config:store:remote:14.0"
cache="loginFailures"
raw-values="true"
shared="true"
segmented="false">
<remote-server host="localhost"
port="11222"/>
<connection-pool max-active="16"
exhausted-action="CREATE_NEW"/>
<security>
<authentication server-name="infinispan">
<digest username="keycloak"
password="Password1!"
realm="default"/>
</authentication>
</security>
</remote-store>
</persistence>
<state-transfer enabled="false"/>
</distributed-cache>
<local-cache name="authorization" simple-cache="true" statistics="true">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory max-count="10000"/>
</local-cache>
<replicated-cache name="work" statistics="true">
<expiration lifespan="-1"/>
<persistence passivation="false">
<remote-store xmlns="urn:infinispan:config:store:remote:14.0"
cache="work"
raw-values="true"
shared="true"
segmented="false">
<remote-server host="localhost"
port="11222"/>
<connection-pool max-active="16"
exhausted-action="CREATE_NEW"/>
<security>
<authentication server-name="infinispan">
<digest username="keycloak"
password="Password1!"
realm="default"/>
</authentication>
</security>
</remote-store>
</persistence>
</replicated-cache>
<local-cache name="keys" simple-cache="true" statistics="true">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<expiration max-idle="3600000"/>
<memory max-count="1000"/>
</local-cache>
<distributed-cache name="actionTokens" owners="2" statistics="true">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<expiration max-idle="-1" lifespan="-1" interval="300000"/>
<memory max-count="-1"/>
<persistence passivation="false">
<remote-store xmlns="urn:infinispan:config:store:remote:14.0"
cache="actionTokens"
raw-values="true"
shared="true"
segmented="false">
<remote-server host="localhost"
port="11222"/>
<connection-pool max-active="16"
exhausted-action="CREATE_NEW"/>
<security>
<authentication server-name="infinispan">
<digest username="keycloak"
password="Password1!"
realm="default"/>
</authentication>
</security>
</remote-store>
</persistence>
<state-transfer enabled="false"/>
</distributed-cache>
</cache-container>
</infinispan>

View file

@ -35,8 +35,10 @@ import org.keycloak.quarkus.runtime.cli.command.StartDev;
import org.keycloak.quarkus.runtime.configuration.KeycloakPropertiesConfigSource;
import org.keycloak.quarkus.runtime.configuration.test.TestConfigArgsConfigSource;
import org.keycloak.quarkus.runtime.integration.QuarkusPlatform;
import org.testcontainers.containers.GenericContainer;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
@ -59,6 +61,7 @@ public class CLITestExtension extends QuarkusMainTestExtension {
private KeycloakDistribution dist;
private final Set<String> testSysProps = new HashSet<>();
private DatabaseContainer databaseContainer;
private InfinispanContainer infinispanContainer;
private CLIResult result;
@Override
@ -88,6 +91,7 @@ public class CLITestExtension extends QuarkusMainTestExtension {
}
configureDatabase(context);
infinispanContainer = configureExternalInfinispan(context);
if (distConfig != null) {
onKeepServerAlive(context.getRequiredTestMethod().getAnnotation(KeepServerAlive.class));
@ -192,6 +196,9 @@ public class CLITestExtension extends QuarkusMainTestExtension {
databaseContainer.stop();
databaseContainer = null;
}
if (infinispanContainer != null && infinispanContainer.isRunning()) {
infinispanContainer.stop();
}
result = null;
if (RAW.equals(DistributionType.getCurrent().orElse(RAW))) {
if (distConfig != null && !DistributionTest.ReInstall.NEVER.equals(distConfig.reInstall()) && dist != null) {
@ -324,8 +331,24 @@ public class CLITestExtension extends QuarkusMainTestExtension {
}
}
private static InfinispanContainer configureExternalInfinispan(ExtensionContext context) {
if (getAnnotationFromTestContext(context, WithExternalInfinispan.class) != null) {
InfinispanContainer infinispanContainer = new InfinispanContainer();
infinispanContainer.start();
return infinispanContainer;
}
return null;
}
private static WithDatabase getDatabaseConfig(ExtensionContext context) {
return context.getTestClass().orElse(Object.class).getDeclaredAnnotation(WithDatabase.class);
return getAnnotationFromTestContext(context, WithDatabase.class);
}
private static <T extends Annotation> T getAnnotationFromTestContext(ExtensionContext context, Class<T> annotationClass) {
return context.getTestClass().map(c -> c.getDeclaredAnnotation(annotationClass))
.or(() -> context.getTestMethod().map(m -> m.getAnnotation(annotationClass)))
.orElse(null);
}
private void configureDevServices() {

View file

@ -50,17 +50,10 @@ public class DatabaseContainer {
}
void configureDistribution(KeycloakDistribution dist) {
if (alias.equals("infinispan")) {
dist.setProperty("storage-hotrod-username", getUsername());
dist.setProperty("storage-hotrod-password", getPassword());
dist.setProperty("storage-hotrod-host", container.getHost());
dist.setProperty("storage-hotrod-port", String.valueOf(container.getMappedPort(11222)));
} else {
dist.setProperty("db-username", getUsername());
dist.setProperty("db-password", getPassword());
dist.setProperty("db-url", getJdbcUrl());
}
}
private String getJdbcUrl() {
return ((JdbcDatabaseContainer)container).getJdbcUrl();
@ -97,24 +90,10 @@ public class DatabaseContainer {
.withInitScript(resolveInitScript());
}
private GenericContainer<?> configureInfinispanUser(GenericContainer<?> infinispanContainer) {
infinispanContainer.addEnv("USER", getUsername());
infinispanContainer.addEnv("PASS", getPassword());
return infinispanContainer;
}
private GenericContainer<?> createContainer() {
String POSTGRES_IMAGE = System.getProperty("kc.db.postgresql.container.image");
String MARIADB_IMAGE = System.getProperty("kc.db.mariadb.container.image");
String MYSQL_IMAGE = System.getProperty("kc.db.mysql.container.image");
String INFINISPAN_IMAGE = System.getProperty("kc.infinispan.container.image");
if (INFINISPAN_IMAGE.matches("quay.io/infinispan/.*-SNAPSHOT")) {
// If the image name ends with SNAPSHOT, someone is trying to use a snapshot release of Infinispan.
// Then switch to the closest match of the Infinispan test container
INFINISPAN_IMAGE = INFINISPAN_IMAGE.replaceAll("quay.io/infinispan/", "quay.io/infinispan-test/");
INFINISPAN_IMAGE = INFINISPAN_IMAGE.replaceAll("[0-9]*-SNAPSHOT$", "x");
}
String MSSQL_IMAGE = System.getProperty("kc.db.mssql.container.image");
switch (alias) {
@ -130,14 +109,6 @@ public class DatabaseContainer {
case "mssql":
DockerImageName MSSQL = DockerImageName.parse(MSSQL_IMAGE).asCompatibleSubstituteFor("sqlserver");
return configureJdbcContainer(new MSSQLServerContainer<>(MSSQL));
case "infinispan":
GenericContainer<?> infinispanContainer = configureInfinispanUser(new GenericContainer<>(INFINISPAN_IMAGE))
.withExposedPorts(11222);
// the images in the 'infinispan-test' repository point to tags that are frequently refreshed, therefore, always pull them
if (infinispanContainer.getDockerImageName().startsWith("quay.io/infinispan-test")) {
infinispanContainer.withImagePullPolicy(PullPolicy.alwaysPull());
}
return infinispanContainer;
default:
throw new RuntimeException("Unsupported database: " + alias);
}

View file

@ -0,0 +1,114 @@
/*
* Copyright 2023 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.it.junit5.extension;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.configuration.ClientIntelligence;
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
import org.infinispan.commons.configuration.XMLStringConfiguration;
import org.jboss.logging.Logger;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.images.PullPolicy;
import java.time.Duration;
import java.util.stream.Stream;
public class InfinispanContainer extends GenericContainer<InfinispanContainer> {
private final Logger LOG = Logger.getLogger(getClass());
public static final String PORT = System.getProperty("keycloak.externalInfinispan.port", "11222");
public static final String USERNAME = System.getProperty("keycloak.externalInfinispan.username", "keycloak");
public static final String PASSWORD = System.getProperty("keycloak.externalInfinispan.password", DatabaseContainer.DEFAULT_PASSWORD);
public static RemoteCacheManager remoteCacheManager;
public InfinispanContainer() {
super(getImageName());
withEnv("USER", USERNAME);
withEnv("PASS", PASSWORD);
withNetworkMode("host");
// the images in the 'infinispan-test' repository point to tags that are frequently refreshed, therefore, always pull them
if (getImageName().startsWith("quay.io/infinispan-test")) {
withImagePullPolicy(PullPolicy.alwaysPull());
}
//order of waitingFor and withStartupTimeout matters as the latter sets the timeout for WaitStrategy set by waitingFor
waitingFor(Wait.forLogMessage(".*Infinispan Server.*started in.*", 1));
withStartupTimeout(Duration.ofMinutes(5));
}
private static String getImageName() {
String INFINISPAN_IMAGE = System.getProperty("kc.infinispan.container.image");
if (INFINISPAN_IMAGE.matches("quay.io/infinispan/.*-SNAPSHOT")) {
// If the image name ends with SNAPSHOT, someone is trying to use a snapshot release of Infinispan.
// Then switch to the closest match of the Infinispan test container
INFINISPAN_IMAGE = INFINISPAN_IMAGE.replaceAll("quay.io/infinispan/", "quay.io/infinispan-test/");
INFINISPAN_IMAGE = INFINISPAN_IMAGE.replaceAll("[0-9]*-SNAPSHOT$", "x");
}
return INFINISPAN_IMAGE;
}
private void establishHotRodConnection() {
ConfigurationBuilder configBuilder = new ConfigurationBuilder()
.addServers(getContainerIpAddress() + ":11222")
.security()
.authentication()
.username(getUsername())
.password(getPassword())
.clientIntelligence(ClientIntelligence.BASIC);
configBuilder.statistics().enable()
.statistics().jmxEnable();
remoteCacheManager = new RemoteCacheManager(configBuilder.build());
}
@Override
public void start() {
super.start();
establishHotRodConnection();
Stream.of("sessions", "actionTokens", "authenticationSessions", "clientSessions", "offlineSessions", "offlineClientSessions", "loginFailures", "work")
.forEach(cacheName -> {
LOG.infof("Creating cache '%s'", cacheName);
createCache(remoteCacheManager, cacheName);
});
}
public void createCache(RemoteCacheManager remoteCacheManager, String cacheName) {
String xml = String.format("<distributed-cache name=\"%s\" mode=\"SYNC\" owners=\"2\"></distributed-cache>" , cacheName);
remoteCacheManager.administration().getOrCreateCache(cacheName, new XMLStringConfiguration(xml));
}
public String getPort() {
return PORT;
}
public String getUsername() {
return USERNAME;
}
public String getPassword() {
return PASSWORD;
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2021 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.it.junit5.extension;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* {@link WithExternalInfinispan} is used to start an Infinispan container.
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@EnabledIfSystemProperty(named = "kc.test.storage.database", matches = "true", disabledReason = "Docker takes too much time and stability depends on the environment. We should try running these tests in CI but isolated.")
public @interface WithExternalInfinispan {
}