Implement CompatibilityMetadataProvider for Cache CLI args

Closes #41138

Signed-off-by: Ryan Emerson <remerson@redhat.com>
This commit is contained in:
Ryan Emerson
2025-07-16 18:52:51 +01:00
committed by GitHub
parent d62d5030fe
commit 4bb02305c3
12 changed files with 313 additions and 85 deletions

View File

@@ -56,6 +56,12 @@
<artifactId>keycloak-server-spi-private</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-api</artifactId>

View File

@@ -0,0 +1,48 @@
package org.keycloak.compatibility;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
import org.keycloak.Config;
public abstract class AbstractCompatibilityMetadataProvider implements CompatibilityMetadataProvider {
final String spi;
final Config.Scope config;
public AbstractCompatibilityMetadataProvider(String spi, String providerId) {
this.spi = spi;
this.config = Config.scope(spi, providerId);
}
abstract protected boolean isEnabled(Config.Scope scope);
@Override
public Map<String, String> metadata() {
if (!isEnabled(config))
return Map.of();
Map<String, String> metadata = new HashMap<>(customMeta());
configKeys().forEach(key -> {
String value = config.get(key);
if (value != null)
metadata.put(key, value);
});
return metadata;
}
@Override
public String getId() {
return spi;
}
protected Map<String, String> customMeta() {
return Map.of();
}
protected Stream<String> configKeys() {
return Stream.of();
}
}

View File

@@ -1,45 +0,0 @@
package org.keycloak.infinispan.compatibility;
import java.util.Map;
import org.infinispan.commons.util.Version;
import org.keycloak.common.Profile;
import org.keycloak.common.util.MultiSiteUtils;
import org.keycloak.compatibility.CompatibilityMetadataProvider;
import org.keycloak.infinispan.util.InfinispanUtils;
/**
* A {@link CompatibilityMetadataProvider} to provide metadata for the CLI options under the Caching category and
* anything related to Infinispan.
*/
public class CachingCompatibilityMetadataProvider implements CompatibilityMetadataProvider {
public static final String ID = "caching";
@Override
public Map<String, String> metadata() {
return InfinispanUtils.isRemoteInfinispan() ?
remoteInfinispanMetadata() :
embeddedInfinispanMetadata();
}
@Override
public String getId() {
return ID;
}
private static Map<String, String> remoteInfinispanMetadata() {
return Map.of(
"mode", "remote",
"persistence", Boolean.toString(MultiSiteUtils.isPersistentSessionsEnabled())
);
}
private static Map<String, String> embeddedInfinispanMetadata() {
return Map.of(
"mode", "embedded",
"persistence", Boolean.toString(Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)),
"version", Version.getVersion(),
"jgroupsVersion", org.jgroups.Version.printVersion()
);
}
}

View File

@@ -0,0 +1,36 @@
package org.keycloak.infinispan.compatibility;
import java.util.Map;
import java.util.stream.Stream;
import org.infinispan.commons.util.Version;
import org.keycloak.Config;
import org.keycloak.compatibility.AbstractCompatibilityMetadataProvider;
import org.keycloak.infinispan.util.InfinispanUtils;
import org.keycloak.spi.infinispan.CacheEmbeddedConfigProviderSpi;
import org.keycloak.spi.infinispan.impl.embedded.DefaultCacheEmbeddedConfigProviderFactory;
public class CachingEmbeddedMetadataProvider extends AbstractCompatibilityMetadataProvider {
public CachingEmbeddedMetadataProvider() {
super(CacheEmbeddedConfigProviderSpi.SPI_NAME, DefaultCacheEmbeddedConfigProviderFactory.PROVIDER_ID);
}
@Override
protected boolean isEnabled(Config.Scope scope) {
return InfinispanUtils.isEmbeddedInfinispan();
}
@Override
public Map<String, String> customMeta() {
return Map.of(
"version", Version.getVersion(),
"jgroupsVersion", org.jgroups.Version.printVersion()
);
}
@Override
public Stream<String> configKeys() {
return Stream.of(DefaultCacheEmbeddedConfigProviderFactory.CONFIG, DefaultCacheEmbeddedConfigProviderFactory.STACK);
}
}

View File

@@ -0,0 +1,26 @@
package org.keycloak.infinispan.compatibility;
import java.util.stream.Stream;
import org.keycloak.Config;
import org.keycloak.compatibility.AbstractCompatibilityMetadataProvider;
import org.keycloak.infinispan.util.InfinispanUtils;
import org.keycloak.spi.infinispan.CacheRemoteConfigProviderSpi;
import org.keycloak.spi.infinispan.impl.remote.DefaultCacheRemoteConfigProviderFactory;
public class CachingRemoteMetadataProvider extends AbstractCompatibilityMetadataProvider {
public CachingRemoteMetadataProvider() {
super(CacheRemoteConfigProviderSpi.SPI_NAME, DefaultCacheRemoteConfigProviderFactory.PROVIDER_ID);
}
@Override
protected boolean isEnabled(Config.Scope scope) {
return InfinispanUtils.isRemoteInfinispan();
}
@Override
protected Stream<String> configKeys() {
return Stream.of(DefaultCacheRemoteConfigProviderFactory.HOSTNAME, DefaultCacheRemoteConfigProviderFactory.PORT);
}
}

View File

@@ -44,8 +44,10 @@ import org.keycloak.storage.configuration.ServerConfigStorageProvider;
*/
public class DefaultJGroupsCertificateProviderFactory implements JGroupsCertificateProviderFactory {
public static final String PROVIDER_ID = "default";
// config
private static final String ENABLED = "enabled";
public static final String ENABLED = "enabled";
private static final String ROTATION = "rotation";
private static final String KEYSTORE_PATH = "keystoreFile";
private static final String KEYSTORE_PASSWORD = "keystorePassword";
@@ -84,7 +86,7 @@ public class DefaultJGroupsCertificateProviderFactory implements JGroupsCertific
@Override
public String getId() {
return "default";
return PROVIDER_ID;
}
@Override

View File

@@ -0,0 +1,25 @@
package org.keycloak.jgroups.certificates;
import java.util.stream.Stream;
import org.keycloak.Config;
import org.keycloak.compatibility.AbstractCompatibilityMetadataProvider;
import org.keycloak.infinispan.util.InfinispanUtils;
import org.keycloak.spi.infinispan.JGroupsCertificateProviderSpi;
public class JGroupsCertificatesMetadataProvider extends AbstractCompatibilityMetadataProvider {
public JGroupsCertificatesMetadataProvider() {
super(JGroupsCertificateProviderSpi.SPI_NAME, DefaultJGroupsCertificateProviderFactory.PROVIDER_ID);
}
@Override
protected boolean isEnabled(Config.Scope scope) {
return InfinispanUtils.isEmbeddedInfinispan();
}
@Override
public Stream<String> configKeys() {
return Stream.of(DefaultJGroupsCertificateProviderFactory.ENABLED);
}
}

View File

@@ -1 +1,3 @@
org.keycloak.infinispan.compatibility.CachingCompatibilityMetadataProvider
org.keycloak.infinispan.compatibility.CachingEmbeddedMetadataProvider
org.keycloak.infinispan.compatibility.CachingRemoteMetadataProvider
org.keycloak.jgroups.certificates.JGroupsCertificatesMetadataProvider

View File

@@ -202,7 +202,7 @@ final class CachingPropertyMappers {
return homeDir == null ?
value :
homeDir + File.separator + "conf" + File.separator + value;
homeDir + (homeDir.endsWith(File.separator) ? "" : File.separator) + "conf" + File.separator + value;
}
private static String getDefaultKeystorePathValue() {

View File

@@ -17,38 +17,47 @@
package org.keycloak.it.cli.dist;
import io.quarkus.test.junit.main.Launch;
import java.io.File;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.keycloak.it.cli.dist.Util.createTempFile;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.keycloak.common.Profile;
import org.keycloak.common.Version;
import org.keycloak.compatibility.CompatibilityResult;
import org.keycloak.compatibility.FeatureCompatibilityMetadataProvider;
import org.keycloak.compatibility.KeycloakCompatibilityMetadataProvider;
import org.keycloak.infinispan.compatibility.CachingCompatibilityMetadataProvider;
import org.keycloak.it.junit5.extension.CLIResult;
import org.keycloak.it.junit5.extension.DistributionTest;
import org.keycloak.it.junit5.extension.RawDistOnly;
import org.keycloak.it.utils.KeycloakDistribution;
import org.keycloak.it.utils.RawKeycloakDistribution;
import org.keycloak.jgroups.certificates.DefaultJGroupsCertificateProviderFactory;
import org.keycloak.quarkus.runtime.cli.command.UpdateCompatibility;
import org.keycloak.quarkus.runtime.cli.command.UpdateCompatibilityCheck;
import org.keycloak.quarkus.runtime.cli.command.UpdateCompatibilityMetadata;
import org.keycloak.spi.infinispan.CacheEmbeddedConfigProviderSpi;
import org.keycloak.spi.infinispan.CacheRemoteConfigProviderSpi;
import org.keycloak.spi.infinispan.JGroupsCertificateProviderSpi;
import org.keycloak.spi.infinispan.impl.embedded.DefaultCacheEmbeddedConfigProviderFactory;
import org.keycloak.spi.infinispan.impl.remote.DefaultCacheRemoteConfigProviderFactory;
import org.keycloak.util.JsonSerialization;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.keycloak.it.cli.dist.Util.createTempFile;
import io.quarkus.test.junit.main.Launch;
@DistributionTest
@RawDistOnly(reason = "Requires creating JSON file to be available between containers")
public class UpdateCommandDistTest {
private static final String DISABLE_FEATURE = "--features-disabled=rolling-updates";
@Test
@Launch({UpdateCompatibility.NAME, UpdateCompatibilityMetadata.NAME, DISABLE_FEATURE})
@Launch({UpdateCompatibility.NAME, UpdateCompatibilityMetadata.NAME, "--features-disabled=rolling-updates"})
public void testFeatureNotEnabled(CLIResult cliResult) {
cliResult.assertError("Unable to use this command. None of the versions of the feature 'rolling-updates' is enabled.");
}
@@ -74,14 +83,21 @@ public class UpdateCommandDistTest {
@Test
public void testCompatible(KeycloakDistribution distribution) throws IOException {
var jsonFile = createTempFile("compatible", ".json");
var result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityMetadata.NAME, UpdateCompatibilityMetadata.OUTPUT_OPTION_NAME, jsonFile.getAbsolutePath());
var result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityMetadata.NAME, UpdateCompatibilityMetadata.OUTPUT_OPTION_NAME, jsonFile.getAbsolutePath(), "--cache-embedded-mtls-enabled", "true");
result.assertMessage("Metadata:");
assertEquals(0, result.exitCode());
var info = JsonSerialization.mapper.readValue(jsonFile, UpdateCompatibilityCheck.METADATA_TYPE_REF);
assertEquals(Version.VERSION, info.get(KeycloakCompatibilityMetadataProvider.ID).get("version"));
assertEquals(org.infinispan.commons.util.Version.getVersion(), info.get(CachingCompatibilityMetadataProvider.ID).get("version"));
assertEquals(org.jgroups.Version.printVersion(), info.get(CachingCompatibilityMetadataProvider.ID).get("jgroupsVersion"));
assertEquals(org.infinispan.commons.util.Version.getVersion(), info.get(CacheEmbeddedConfigProviderSpi.SPI_NAME).get("version"));
assertEquals(org.jgroups.Version.printVersion(), info.get(CacheEmbeddedConfigProviderSpi.SPI_NAME).get("jgroupsVersion"));
var cacheMeta = info.get(CacheEmbeddedConfigProviderSpi.SPI_NAME);
assertTrue(cacheMeta.get(DefaultCacheEmbeddedConfigProviderFactory.CONFIG).endsWith("conf/cache-ispn.xml"));
assertNull(cacheMeta.get(DefaultCacheEmbeddedConfigProviderFactory.STACK));
var jgroupsMeta = info.get(JGroupsCertificateProviderSpi.SPI_NAME);
assertEquals("true", jgroupsMeta.get(DefaultJGroupsCertificateProviderFactory.ENABLED));
result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityCheck.NAME, UpdateCompatibilityCheck.INPUT_OPTION_NAME, jsonFile.getAbsolutePath());
result.assertExitCode(CompatibilityResult.ExitCode.ROLLING.value());
@@ -94,14 +110,9 @@ public class UpdateCommandDistTest {
var jsonFile = createTempFile("wrong-versions", ".json");
// incompatible keycloak version
var info = new HashMap<String, Map<String, String>>();
info.put(KeycloakCompatibilityMetadataProvider.ID, Map.of("version", "0.0.0.Final"));
info.put(CachingCompatibilityMetadataProvider.ID, Map.of(
"version", org.infinispan.commons.util.Version.getVersion(),
"persistence", "true",
"mode", "embedded",
"jgroupsVersion", org.jgroups.Version.printVersion()
));
var info = defaultMeta(distribution);
info.get(KeycloakCompatibilityMetadataProvider.ID).put("version", "0.0.0.Final");
Profile.configure();
info.put(FeatureCompatibilityMetadataProvider.ID, new FeatureCompatibilityMetadataProvider().metadata());
JsonSerialization.mapper.writeValue(jsonFile, info);
@@ -111,32 +122,119 @@ public class UpdateCommandDistTest {
result.assertError("[%s] Rolling Update is not available. '%s.version' is incompatible: 0.0.0.Final -> %s.".formatted(KeycloakCompatibilityMetadataProvider.ID, KeycloakCompatibilityMetadataProvider.ID, Version.VERSION));
// incompatible infinispan version
info.put(KeycloakCompatibilityMetadataProvider.ID, Map.of("version", Version.VERSION));
info.put(CachingCompatibilityMetadataProvider.ID, Map.of(
"version", "0.0.0.Final",
"persistence", "true",
"mode", "embedded",
"jgroupsVersion", org.jgroups.Version.printVersion()
));
info = defaultMeta(distribution);
info.get(CacheEmbeddedConfigProviderSpi.SPI_NAME).put("version", "0.0.0.Final");
JsonSerialization.mapper.writeValue(jsonFile, info);
// incompatible jgroups version
result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityCheck.NAME, UpdateCompatibilityCheck.INPUT_OPTION_NAME, jsonFile.getAbsolutePath());
result.assertExitCode(CompatibilityResult.ExitCode.RECREATE.value());
result.assertError("[%s] Rolling Update is not available. '%s.version' is incompatible: 0.0.0.Final -> %s.".formatted(CachingCompatibilityMetadataProvider.ID, CachingCompatibilityMetadataProvider.ID, org.infinispan.commons.util.Version.getVersion())); // incompatible infinispan version
result.assertError("[%s] Rolling Update is not available. '%s.version' is incompatible: 0.0.0.Final -> %s.".formatted(CacheEmbeddedConfigProviderSpi.SPI_NAME, CacheEmbeddedConfigProviderSpi.SPI_NAME, org.infinispan.commons.util.Version.getVersion())); // incompatible infinispan version
info.put(KeycloakCompatibilityMetadataProvider.ID, Map.of("version", Version.VERSION));
info.put(CachingCompatibilityMetadataProvider.ID, Map.of(
"version", org.infinispan.commons.util.Version.getVersion(),
"persistence", "true",
"mode", "embedded",
"jgroupsVersion", "0.0.0.Final"
));
info = defaultMeta(distribution);
info.get(CacheEmbeddedConfigProviderSpi.SPI_NAME).put("jgroupsVersion", "0.0.0.Final");
JsonSerialization.mapper.writeValue(jsonFile, info);
result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityCheck.NAME, UpdateCompatibilityCheck.INPUT_OPTION_NAME, jsonFile.getAbsolutePath());
result.assertExitCode(CompatibilityResult.ExitCode.RECREATE.value());
result.assertError("[%s] Rolling Update is not available. '%s.jgroupsVersion' is incompatible: 0.0.0.Final -> %s.".formatted(CachingCompatibilityMetadataProvider.ID, CachingCompatibilityMetadataProvider.ID, org.jgroups.Version.printVersion()));
result.assertError("[%s] Rolling Update is not available. '%s.jgroupsVersion' is incompatible: 0.0.0.Final -> %s.".formatted(CacheEmbeddedConfigProviderSpi.SPI_NAME, CacheEmbeddedConfigProviderSpi.SPI_NAME, org.jgroups.Version.printVersion()));
}
private String resolveConfigFile(KeycloakDistribution distribution, String... paths) {
Path dist = distribution.unwrap(RawKeycloakDistribution.class).getDistPath();
return Paths.get(dist.toString(), paths).toString();
}
@Test
public void testCacheLocalChange(KeycloakDistribution distribution) throws IOException {
var jsonFile = createTempFile("compatible", ".json");
var result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityMetadata.NAME, UpdateCompatibilityMetadata.OUTPUT_OPTION_NAME, jsonFile.getAbsolutePath(), "--cache", "local");
result.assertMessage("Metadata:");
assertEquals(0, result.exitCode());
var info = JsonSerialization.mapper.readValue(jsonFile, UpdateCompatibilityCheck.METADATA_TYPE_REF);
assertTrue(info.get(CacheEmbeddedConfigProviderSpi.SPI_NAME).get("configFile").endsWith("cache-local.xml"));
result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityCheck.NAME, UpdateCompatibilityCheck.INPUT_OPTION_NAME, jsonFile.getAbsolutePath(), "--cache", "ispn");
result.assertExitCode(CompatibilityResult.ExitCode.RECREATE.value());
result.assertError("[%s] Rolling Update is not available. '%s.configFile' is incompatible: cache-local.xml -> %s.".formatted(CacheEmbeddedConfigProviderSpi.SPI_NAME, CacheEmbeddedConfigProviderSpi.SPI_NAME, resolveConfigFile(distribution, "conf", "cache-ispn.xml")));
}
@Test
public void testChangeCacheRemoteToEmbedded(KeycloakDistribution distribution) throws IOException {
var jsonFile = createTempFile("compatible", ".json");
var result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityMetadata.NAME, UpdateCompatibilityMetadata.OUTPUT_OPTION_NAME, jsonFile.getAbsolutePath(), "--features", "clusterless", "--cache-remote-host", "127.0.0.1");
result.assertMessage("Metadata:");
assertEquals(0, result.exitCode());
var info = JsonSerialization.mapper.readValue(jsonFile, UpdateCompatibilityCheck.METADATA_TYPE_REF);
assertEquals(Version.VERSION, info.get(KeycloakCompatibilityMetadataProvider.ID).get("version"));
assertNull(info.get(CacheEmbeddedConfigProviderSpi.SPI_NAME));
assertNull(info.get(JGroupsCertificateProviderSpi.SPI_NAME));
var cacheMeta = info.get(CacheRemoteConfigProviderSpi.SPI_NAME);
assertEquals("127.0.0.1", cacheMeta.get(DefaultCacheRemoteConfigProviderFactory.HOSTNAME));
assertEquals("11222", cacheMeta.get(DefaultCacheRemoteConfigProviderFactory.PORT));
result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityCheck.NAME, UpdateCompatibilityCheck.INPUT_OPTION_NAME, jsonFile.getAbsolutePath());
result.assertExitCode(CompatibilityResult.ExitCode.RECREATE.value());
result.assertError("[%1$s] Rolling Update is not available. '%1$s.configFile' is incompatible: null".formatted(CacheEmbeddedConfigProviderSpi.SPI_NAME));
result.assertError("[%1$s] Rolling Update is not available. '%1$s.jgroupsVersion' is incompatible: null".formatted(CacheEmbeddedConfigProviderSpi.SPI_NAME));
result.assertError("[%1$s] Rolling Update is not available. '%1$s.version' is incompatible: null".formatted(CacheEmbeddedConfigProviderSpi.SPI_NAME));
}
@Test
public void testChangeCacheEmbeddedToRemote(KeycloakDistribution distribution) throws IOException {
var jsonFile = createTempFile("compatible", ".json");
var result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityMetadata.NAME, UpdateCompatibilityMetadata.OUTPUT_OPTION_NAME, jsonFile.getAbsolutePath());
result.assertMessage("Metadata:");
assertEquals(0, result.exitCode());
var info = JsonSerialization.mapper.readValue(jsonFile, UpdateCompatibilityCheck.METADATA_TYPE_REF);
info.remove(FeatureCompatibilityMetadataProvider.ID);
assertEquals(defaultMeta(distribution), info);
result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityCheck.NAME, UpdateCompatibilityCheck.INPUT_OPTION_NAME, jsonFile.getAbsolutePath(), "--features", "clusterless", "--cache-remote-host", "127.0.0.1");
result.assertExitCode(CompatibilityResult.ExitCode.RECREATE.value());
result.assertError("[%1$s] Rolling Update is not available. '%1$s.configFile' is incompatible:".formatted(CacheEmbeddedConfigProviderSpi.SPI_NAME));
result.assertError("[%1$s] Rolling Update is not available. '%1$s.jgroupsVersion' is incompatible:".formatted(CacheEmbeddedConfigProviderSpi.SPI_NAME));
result.assertError("[%1$s] Rolling Update is not available. '%1$s.version' is incompatible:".formatted(CacheEmbeddedConfigProviderSpi.SPI_NAME));
}
@Test
public void testChangeCacheStack(KeycloakDistribution distribution) throws IOException {
var jsonFile = createTempFile("compatible", ".json");
var result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityMetadata.NAME, UpdateCompatibilityMetadata.OUTPUT_OPTION_NAME, jsonFile.getAbsolutePath());
result.assertMessage("Metadata:");
assertEquals(0, result.exitCode());
var info = JsonSerialization.mapper.readValue(jsonFile, UpdateCompatibilityCheck.METADATA_TYPE_REF);
info.remove(FeatureCompatibilityMetadataProvider.ID);
assertEquals(defaultMeta(distribution), info);
result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityCheck.NAME, UpdateCompatibilityCheck.INPUT_OPTION_NAME, jsonFile.getAbsolutePath(), "--cache-stack", "jdbc-ping-udp");
result.assertExitCode(CompatibilityResult.ExitCode.RECREATE.value());
result.assertError("[%1$s] Rolling Update is not available. '%1$s.stack' is incompatible: null -> jdbc-ping-udp".formatted(CacheEmbeddedConfigProviderSpi.SPI_NAME));
}
private Map<String, Map<String, String>> defaultMeta(KeycloakDistribution distribution) {
Map<String, String> keycloak = new HashMap<>(1);
keycloak.put("version", Version.VERSION);
Map<String, Map<String, String>> m = new HashMap<>();
m.put(KeycloakCompatibilityMetadataProvider.ID, keycloak);
m.put(CacheEmbeddedConfigProviderSpi.SPI_NAME, embeddedCachingMeta(distribution));
m.put(JGroupsCertificateProviderSpi.SPI_NAME, Map.of(
"enabled", "true"
));
return m;
}
private Map<String, String> embeddedCachingMeta(KeycloakDistribution distribution) {
Map<String, String> m = new HashMap<>();
m.put("version", org.infinispan.commons.util.Version.getVersion());
m.put("jgroupsVersion", org.jgroups.Version.printVersion());
m.put("configFile", resolveConfigFile(distribution, "conf", "cache-ispn.xml"));
return m;
}
}

View File

@@ -15,6 +15,8 @@ public class KeycloakCompatibilityMetadataProvider implements CompatibilityMetad
public static final String VERSION_KEY = "version";
private final String version;
// Constructor required for ServiceLoader
@SuppressWarnings("unused")
public KeycloakCompatibilityMetadataProvider() {
this(Version.VERSION);
}

View File

@@ -2,6 +2,7 @@ package org.keycloak.compatibility;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
@@ -9,6 +10,7 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import org.infinispan.commons.util.ReflectionUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
@@ -122,6 +124,32 @@ public class FeatureCompatibilityMetadataProviderTest extends AbstractCompatibil
assertCompatibility(CompatibilityResult.ExitCode.RECREATE, provider.isCompatible(v1Meta));
}
@ParameterizedTest
@MethodSource("addedFeatures")
public void testAddedFeature(CompatibilityResult.ExitCode exitCode, Profile.Feature featureToAdd) {
Profile.configure();
FeatureCompatibilityMetadataProvider provider = new FeatureCompatibilityMetadataProvider();
Map<String, String> other = provider.metadata();
// Remove an existing Feature from the profile to emulate a new Profile.Feature being added in a subsequent KC version
Profile instance = Profile.getInstance();
Map<Profile.Feature, Boolean> features = new HashMap<>(instance.getFeatures());
features.remove(featureToAdd);
Field featuresField = ReflectionUtil.getField("features", Profile.class);
featuresField.setAccessible(true);
ReflectionUtil.setField(instance, featuresField, features);
assertCompatibility(exitCode, provider.isCompatible(other));
}
private static Stream<Arguments> addedFeatures() {
return Stream.of(
Arguments.of(CompatibilityResult.ExitCode.ROLLING, Profile.Feature.IMPERSONATION),
Arguments.of(CompatibilityResult.ExitCode.RECREATE, Profile.Feature.PERSISTENT_USER_SESSIONS),
// Expect a RECREATE as the Feature has the ROLLING_NO_UPGRADE policy
Arguments.of(CompatibilityResult.ExitCode.RECREATE, Profile.Feature.LOGIN_V2)
);
}
@ParameterizedTest
@MethodSource("removedFeatures")
public void testRemovedFeature(CompatibilityResult.ExitCode exitCode, Profile.FeatureUpdatePolicy updatePolicy) {