diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java index 644734de4a..bc65e2554c 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java @@ -20,7 +20,7 @@ public class DatabaseOptions { .category(OptionCategory.DATABASE) .description("The database vendor.") .defaultValue("dev-file") - .expectedValues(Database::getAliases) + .expectedValues(Database::getLegacyStoreAliases) .buildTime(true) .build(); diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/StorageOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/StorageOptions.java index 10ce8080d9..0887346e17 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/StorageOptions.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/StorageOptions.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.keycloak.config.database.Database; public class StorageOptions { @@ -333,6 +334,14 @@ public class StorageOptions { .description("Root directory for file map store.") .build(); + public static final Option STORAGE_JPA_DB = new OptionBuilder<>("storage-jpa-db", String.class) + .category(OptionCategory.STORAGE) + .defaultValue(Database.Vendor.POSTGRES.name().toLowerCase()) + .expectedValues(Database::getAvailableMapStoreAliases) + .description("The database vendor for jpa map storage.") + .buildTime(true) + .build(); + private static String descriptionForStorageAreas(String areaAsText) { return "Sets a storage mechanism for " + areaAsText + "."; } @@ -344,4 +353,11 @@ public class StorageOptions { private static List getExpectedCacheNames() { return Stream.concat(Stream.of("all"), AutogeneratedHotRodDescriptors.ENTITY_DESCRIPTOR_MAP.values().stream().map(HotRodEntityDescriptor::getCacheName).distinct()).collect(Collectors.toList()); } + + public static Optional getDatabaseVendor(String databaseKind) { + return Stream.of(Database.Vendor.values()) + .filter(Database.Vendor::isEnabledOnNewStore) + .filter(v -> v.isOfKind(databaseKind)) + .findFirst(); + } } diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/database/Database.java b/quarkus/config-api/src/main/java/org/keycloak/config/database/Database.java index fccc178df7..c66a175e16 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/database/Database.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/database/Database.java @@ -22,9 +22,11 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import static java.util.Arrays.asList; @@ -94,12 +96,31 @@ public final class Database { return Optional.of(vendor.dialect.apply(alias)); } - public static List getAliases() { - return DATABASES.keySet().stream().sorted().collect(Collectors.toList()); + /** + * @return List of aliases of databases enabled on legacy store. + */ + public static List getLegacyStoreAliases() { + return DATABASES.entrySet().stream() + .filter(e -> e.getValue().isEnabledOnLegacyStore()) + .map(Entry::getKey) + .sorted() + .collect(Collectors.toList()); + } + + /** + * @return List of aliases of databases enabled on jpa map store. + */ + public static List getAvailableMapStoreAliases() { + return Stream.of(Database.Vendor.values()) + .filter(Database.Vendor::isEnabledOnNewStore) + .map(v -> v.aliases)// may be replaced by Database.Vendor::getAliases if we add the getter into Database.Vendor + .flatMap(Stream::of) + .collect(Collectors.toList()); } public enum Vendor { H2("h2", + Enabled.LEGACY_ONLY, "org.h2.jdbcx.JdbcDataSource", "org.h2.Driver", "io.quarkus.hibernate.orm.runtime.dialect.QuarkusH2Dialect", @@ -146,6 +167,7 @@ public final class Database { "dev-mem", "dev-file" ), MYSQL("mysql", + Enabled.LEGACY_ONLY, "com.mysql.cj.jdbc.MysqlXADataSource", "com.mysql.cj.jdbc.Driver", "org.hibernate.dialect.MySQL8Dialect", @@ -153,6 +175,7 @@ public final class Database { asList("org.keycloak.connections.jpa.updater.liquibase.UpdatedMySqlDatabase") ), MARIADB("mariadb", + Enabled.LEGACY_ONLY, "org.mariadb.jdbc.MariaDbDataSource", "org.mariadb.jdbc.Driver", "org.hibernate.dialect.MariaDBDialect", @@ -160,15 +183,25 @@ public final class Database { asList("org.keycloak.connections.jpa.updater.liquibase.UpdatedMariaDBDatabase") ), POSTGRES("postgresql", + Enabled.ENABLED, "org.postgresql.xa.PGXADataSource", "org.postgresql.Driver", "io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL10Dialect", "jdbc:postgresql://${kc.db-url-host:localhost}:${kc.db-url-port:5432}/${kc.db-url-database:keycloak}${kc.db-url-properties:}", - asList("liquibase.database.core.PostgresDatabase", "liquibase.database.core.CockroachDatabase", - "org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase"), + asList("liquibase.database.core.PostgresDatabase", "org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase"), "postgres" ), + COCKROACH(POSTGRES.databaseKind, //needs to be aligned with https://quarkus.io/guides/datasource#default-datasource + Enabled.MAP_STORE_ONLY, + POSTGRES.xaDriver, + POSTGRES.nonXaDriver, + "org.hibernate.dialect.CockroachDB201Dialect", + "jdbc:postgresql://${kc.db-url-host:localhost}:${kc.db-url-port:26257}/${kc.db-url-database:keycloak}${kc.db-url-properties:}", + List.of("liquibase.database.core.CockroachDatabase"), + "cockroach" + ), MSSQL("mssql", + Enabled.LEGACY_ONLY, "com.microsoft.sqlserver.jdbc.SQLServerXADataSource", "com.microsoft.sqlserver.jdbc.SQLServerDriver", "org.hibernate.dialect.SQLServer2016Dialect", @@ -177,6 +210,7 @@ public final class Database { "mssql" ), ORACLE("oracle", + Enabled.LEGACY_ONLY, "oracle.jdbc.xa.client.OracleXADataSource", "oracle.jdbc.driver.OracleDriver", "org.hibernate.dialect.Oracle12cDialect", @@ -185,6 +219,7 @@ public final class Database { ); final String databaseKind; + final Enabled enabled; final String xaDriver; final String nonXaDriver; final Function dialect; @@ -192,20 +227,21 @@ public final class Database { final List liquibaseTypes; final String[] aliases; - Vendor(String databaseKind, String xaDriver, String nonXaDriver, String dialect, String defaultUrl, List liquibaseTypes, + Vendor(String databaseKind, Enabled enabled, String xaDriver, String nonXaDriver, String dialect, String defaultUrl, List liquibaseTypes, String... aliases) { - this(databaseKind, xaDriver, nonXaDriver, alias -> dialect, alias -> defaultUrl, liquibaseTypes, aliases); + this(databaseKind, enabled, xaDriver, nonXaDriver, alias -> dialect, alias -> defaultUrl, liquibaseTypes, aliases); } - Vendor(String databaseKind, String xaDriver, String nonXaDriver, String dialect, Function defaultUrl, + Vendor(String databaseKind, Enabled enabled, String xaDriver, String nonXaDriver, String dialect, Function defaultUrl, List liquibaseTypes, String... aliases) { - this(databaseKind, xaDriver, nonXaDriver, alias -> dialect, defaultUrl, liquibaseTypes, aliases); + this(databaseKind, enabled, xaDriver, nonXaDriver, alias -> dialect, defaultUrl, liquibaseTypes, aliases); } - Vendor(String databaseKind, String xaDriver, String nonXaDriver, Function dialect, Function defaultUrl, + Vendor(String databaseKind, Enabled enabled, String xaDriver, String nonXaDriver, Function dialect, Function defaultUrl, List liquibaseTypes, String... aliases) { this.databaseKind = databaseKind; + this.enabled = enabled; this.xaDriver = xaDriver; this.nonXaDriver = nonXaDriver; this.dialect = dialect; @@ -214,6 +250,14 @@ public final class Database { this.aliases = aliases.length == 0 ? new String[] { databaseKind } : aliases; } + public boolean isEnabledOnLegacyStore() { + return enabled.legacyStore; + } + + public boolean isEnabledOnNewStore() { + return enabled.jpaMapStore; + } + public boolean isOfKind(String dbKind) { return databaseKind.equals(dbKind); } @@ -223,4 +267,18 @@ public final class Database { return databaseKind.toLowerCase(Locale.ROOT); } } + + private static class Enabled { + final static Enabled LEGACY_ONLY = new Enabled(true, false); + final static Enabled MAP_STORE_ONLY = new Enabled(false, true); + final static Enabled ENABLED = new Enabled(true, true); + + final boolean legacyStore; + final boolean jpaMapStore; + + private Enabled(boolean legacyStore, boolean jpaMapStore) { + this.legacyStore = legacyStore; + this.jpaMapStore = jpaMapStore; + } + } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java index ee95cda1b4..9b58561db9 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java @@ -12,6 +12,7 @@ import java.util.Optional; import static java.util.Optional.of; import static org.keycloak.config.StorageOptions.STORAGE; +import static org.keycloak.config.StorageOptions.STORAGE_JPA_DB; import static org.keycloak.quarkus.runtime.Messages.invalidDatabaseVendor; import static org.keycloak.quarkus.runtime.configuration.Configuration.getRawValue; import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX; @@ -99,7 +100,7 @@ final class DatabasePropertyMappers { if (url.isPresent()) { if (isJpaStore()) { - return Database.getDefaultUrl(Database.Vendor.POSTGRES.name().toLowerCase()); + return Database.getDefaultUrl(getJpaStoreDbVendor().name().toLowerCase()); } return url; } @@ -109,7 +110,8 @@ final class DatabasePropertyMappers { private static Optional getXaOrNonXaDriver(Optional value, ConfigSourceInterceptorContext context) { if (isJpaStore()) { - return Database.getDriver(Database.Vendor.POSTGRES.name().toLowerCase(), false); + // always use XA driver with jpa map store + return Database.getDriver(getJpaStoreDbVendor().name().toLowerCase(), true); } ConfigValue xaEnabledConfigValue = context.proceed("kc.transaction-xa-enabled"); @@ -133,7 +135,7 @@ final class DatabasePropertyMappers { private static Optional toDatabaseKind(Optional db, ConfigSourceInterceptorContext context) { if (isJpaStore()) { - return Database.getDatabaseKind(Database.Vendor.POSTGRES.name().toLowerCase()); + return Database.getDatabaseKind(getJpaStoreDbVendor().name().toLowerCase()); } Optional databaseKind = Database.getDatabaseKind(db.get()); @@ -142,14 +144,14 @@ final class DatabasePropertyMappers { return databaseKind; } - addInitializationException(invalidDatabaseVendor(db.get(), Database.getAliases())); + addInitializationException(invalidDatabaseVendor(db.get(), Database.getLegacyStoreAliases())); return of("h2"); } private static Optional resolveDatabaseVendor(Optional db, ConfigSourceInterceptorContext context) { if (isJpaStore()) { - return of(Database.Vendor.POSTGRES.name().toLowerCase()); + return Optional.of(getJpaStoreDbVendor().name().toLowerCase()); } if (db.isEmpty()) { @@ -204,16 +206,13 @@ final class DatabasePropertyMappers { return Database.getDialect("dev-file"); } - private static String getDefaultVendor() { - if (isJpaStore()) { - return Database.Vendor.POSTGRES.name().toLowerCase(); - } - - return "dev-file"; - } - private static boolean isJpaStore() { String storage = getRawValue(NS_KEYCLOAK_PREFIX.concat(STORAGE.getKey())); return storage != null && StorageOptions.StorageType.jpa.name().equals(storage); } + + private static Database.Vendor getJpaStoreDbVendor() { + String storageJpaDb = getRawValue(NS_KEYCLOAK_PREFIX.concat(STORAGE_JPA_DB.getKey())); + return StorageOptions.getDatabaseVendor(storageJpaDb).orElse(Database.Vendor.POSTGRES); + } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/StoragePropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/StoragePropertyMappers.java index bd1dc25c3f..b6059576df 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/StoragePropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/StoragePropertyMappers.java @@ -303,6 +303,11 @@ final class StoragePropertyMappers { .to("kc.spi-map-storage-file-dir") .mapFrom("storage") .paramLabel("dir") + .build(), + fromOption(StorageOptions.STORAGE_JPA_DB) + .to("kc.spi-map-storage-jpa-db") + .mapFrom("storage") + .paramLabel("type") .build() }; } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/TransactionPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/TransactionPropertyMappers.java index 30448e76ad..9d44dec784 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/TransactionPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/TransactionPropertyMappers.java @@ -40,7 +40,7 @@ public class TransactionPropertyMappers { if (storage != null && StorageOptions.StorageType.jpa.name().equals(storage.getValue())) { isJtaEnabled = true; - isXaEnabled = false; + isXaEnabled = true; } if (!isJtaEnabled) { diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.unix.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.unix.approved.txt index 2e1f413d7b..e48c8f1e9e 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.unix.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.unix.approved.txt @@ -87,6 +87,9 @@ Storage (Experimental): Experimental: Sets the port of the Infinispan server. --storage-hotrod-username Experimental: Sets the username of the Infinispan user. +--storage-jpa-db + Experimental: The database vendor for jpa map storage. Possible values are: + postgres, cockroach. Default: postgres. Database: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.windows.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.windows.approved.txt index 1e5934893a..927218d631 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.windows.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.windows.approved.txt @@ -87,6 +87,9 @@ Storage (Experimental): Experimental: Sets the port of the Infinispan server. --storage-hotrod-username Experimental: Sets the username of the Infinispan user. +--storage-jpa-db + Experimental: The database vendor for jpa map storage. Possible values are: + postgres, cockroach. Default: postgres. Database: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.unix.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.unix.approved.txt index 2056e203f1..dce36011b9 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.unix.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.unix.approved.txt @@ -93,6 +93,9 @@ Storage (Experimental): Experimental: Sets the port of the Infinispan server. --storage-hotrod-username Experimental: Sets the username of the Infinispan user. +--storage-jpa-db + Experimental: The database vendor for jpa map storage. Possible values are: + postgres, cockroach. Default: postgres. Database: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.windows.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.windows.approved.txt index bf83f14191..d6a74b7a30 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.windows.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.windows.approved.txt @@ -93,6 +93,9 @@ Storage (Experimental): Experimental: Sets the port of the Infinispan server. --storage-hotrod-username Experimental: Sets the username of the Infinispan user. +--storage-jpa-db + Experimental: The database vendor for jpa map storage. Possible values are: + postgres, cockroach. Default: postgres. Database: diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml index 9a28784816..91f553de67 100644 --- a/testsuite/integration-arquillian/tests/base/pom.xml +++ b/testsuite/integration-arquillian/tests/base/pom.xml @@ -894,6 +894,8 @@ ${keycloak.map.storage.connectionsJpa.user} ${keycloak.map.storage.connectionsJpa.password} + postgres + jpa jpa jpa @@ -1074,6 +1076,8 @@ ${keycloak.map.storage.connectionsJpa.user} ${keycloak.map.storage.connectionsJpa.password} + cockroach + jpa jpa jpa diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/model/StoreProvider.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/model/StoreProvider.java index 5289289d62..ff615ed574 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/model/StoreProvider.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/model/StoreProvider.java @@ -37,7 +37,7 @@ public enum StoreProvider { @Override public void addStoreOptions(List commands) { commands.add("--storage=" + getAlias()); - getDbVendor().ifPresent(vendor -> commands.add("--db=" + vendor)); + getDbVendor().ifPresent(vendor -> commands.add("--storage-jpa-db=" + vendor)); commands.add("--db-url=" + System.getProperty("keycloak.map.storage.connectionsJpa.url")); commands.add("--db-username=" + System.getProperty("keycloak.map.storage.connectionsJpa.user")); commands.add("--db-password=" + System.getProperty("keycloak.map.storage.connectionsJpa.password"));