diff --git a/docs/guides/src/main/server/db.adoc b/docs/guides/src/main/server/db.adoc index 4805aac858..4c6c50d693 100644 --- a/docs/guides/src/main/server/db.adoc +++ b/docs/guides/src/main/server/db.adoc @@ -5,7 +5,7 @@ <@tmpl.guide title="Configuring the database" summary="An overview about how to configure relational databases" - includedOptions="db db-*"> + includedOptions="db db-* transaction-xa-enabled"> In this guide, you are going to understand how to configure the server to store data using different relational databases. You should also learn You will also learn what databases are supported by the server. @@ -119,4 +119,11 @@ By default, the maximum timeout for this lock is 900 seconds. If a node is waiti <@kc.start parameters="--spi-dblock-jpa-lock-wait-timeout 900"/> +== Using Database Vendors without XA transaction support +Keycloak uses XA transactions and the appropriate database drivers by default. There are vendors like Azure SQL and MariaDB Galera, that do not support or rely on the XA transaction mechanism. To use Keycloak without XA transaction support using the appropriate jdbc driver, invoke the following command: + +<@kc.build parameters="--db= --transaction-xa-enabled=false"/> + +Keycloak will automatically choose the appropriate jdbc driver for your vendor. + \ No newline at end of file diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ConfigCategory.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ConfigCategory.java index 9d8610c1c9..a5306176c1 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ConfigCategory.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ConfigCategory.java @@ -4,13 +4,14 @@ public enum ConfigCategory { // ordered by name asc CLUSTERING("Cluster", 10), DATABASE("Database", 20), - FEATURE("Feature", 30), - HOSTNAME("Hostname", 40), - HTTP("HTTP/TLS", 50), - METRICS("Metrics", 60), - PROXY("Proxy", 70), - VAULT("Vault", 80), - LOGGING("Logging", 90), + TRANSACTION("Transaction",30), + FEATURE("Feature", 40), + HOSTNAME("Hostname", 50), + HTTP("HTTP/TLS", 60), + METRICS("Metrics", 70), + PROXY("Proxy", 80), + VAULT("Vault", 90), + LOGGING("Logging", 100), GENERAL("General", 999); private final String heading; 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 3e9d44efbb..d1fa845648 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 @@ -2,8 +2,10 @@ package org.keycloak.quarkus.runtime.configuration.mappers; import io.quarkus.datasource.common.runtime.DatabaseKind; import io.smallrye.config.ConfigSourceInterceptorContext; +import io.smallrye.config.ConfigValue; import org.keycloak.quarkus.runtime.storage.database.Database; +import java.util.List; import java.util.Optional; import java.util.function.BiFunction; @@ -26,9 +28,9 @@ final class DatabasePropertyMappers { .build(), builder().from("db-driver") .mapFrom("db") - .defaultValue(Database.getDriver("dev-file").get()) + .defaultValue(Database.getDriver("dev-file", true).get()) .to("quarkus.datasource.jdbc.driver") - .transformer((db, context) -> Database.getDriver(db).orElse(db)) + .transformer(DatabasePropertyMappers::getXaOrNonXaDriver) .hidden(true) .build(), builder().from("db"). @@ -39,11 +41,6 @@ final class DatabasePropertyMappers { .paramLabel("vendor") .expectedValues(asList(Database.getAliases())) .build(), - builder().from("db-tx-type") - .defaultValue("xa") - .to("quarkus.datasource.jdbc.transactions") - .hidden(true) - .build(), builder().from("db-url") .to("quarkus.datasource.jdbc.url") .mapFrom("db") @@ -106,6 +103,14 @@ final class DatabasePropertyMappers { }; } + private static String getXaOrNonXaDriver(String db, ConfigSourceInterceptorContext context) { + ConfigValue xaEnabledConfigValue = context.proceed("kc.transaction-xa-enabled"); + + boolean isXaEnabled = xaEnabledConfigValue == null || Boolean.parseBoolean(xaEnabledConfigValue.getValue()); + + return Database.getDriver(db, isXaEnabled).orElse(db); + } + private static BiFunction toDatabaseKind() { return (db, context) -> { Optional databaseKind = Database.getDatabaseKind(db); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java index c17eb841a5..6528d1f3e1 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java @@ -31,6 +31,7 @@ public final class PropertyMappers { MAPPERS.addAll(VaultPropertyMappers.getVaultPropertyMappers()); MAPPERS.addAll(FeaturePropertyMappers.getMappers()); MAPPERS.addAll(LoggingPropertyMappers.getMappers()); + MAPPERS.addAll(TransactionPropertyMappers.getTransactionPropertyMappers()); } public static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) { 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 new file mode 100644 index 0000000000..f3715ecfbf --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/TransactionPropertyMappers.java @@ -0,0 +1,39 @@ +package org.keycloak.quarkus.runtime.configuration.mappers; + +import io.smallrye.config.ConfigSourceInterceptorContext; + +import java.util.Arrays; + +public class TransactionPropertyMappers { + + private TransactionPropertyMappers(){} + + public static PropertyMapper[] getTransactionPropertyMappers() { + return new PropertyMapper[] { + builder().from("transaction-xa-enabled") + .to("quarkus.datasource.jdbc.transactions") + .defaultValue(Boolean.TRUE.toString()) + .description("Manually override the transaction type. Transaction type XA and the appropriate driver is used by default.") + .paramLabel(Boolean.TRUE + "|" + Boolean.FALSE) + .expectedValues(Arrays.asList(Boolean.TRUE.toString(), Boolean.FALSE.toString())) + .isBuildTimeProperty(true) + .transformer(TransactionPropertyMappers::getQuarkusTransactionsValue) + .build(), + }; + } + + private static String getQuarkusTransactionsValue(String txValue, ConfigSourceInterceptorContext context) { + boolean isXaEnabled = Boolean.parseBoolean(txValue); + + if (isXaEnabled) { + return "xa"; + } + + return "enabled"; + } + + private static PropertyMapper.Builder builder() { + return PropertyMapper.builder(ConfigCategory.TRANSACTION); + } + +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/Database.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/Database.java index 7ffd6606df..90f8f08d5c 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/Database.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/Database.java @@ -68,14 +68,18 @@ public final class Database { return Optional.of(vendor.defaultUrl.apply(alias)); } - public static Optional getDriver(String alias) { + public static Optional getDriver(String alias, boolean isXaEnabled) { Vendor vendor = DATABASES.get(alias); if (vendor == null) { return Optional.empty(); } - return Optional.of(vendor.driver); + if (isXaEnabled) { + return Optional.of(vendor.xaDriver); + } + + return Optional.of(vendor.nonXaDriver); } public static Optional getDialect(String alias) { @@ -95,6 +99,7 @@ public final class Database { private enum Vendor { H2("h2", "org.h2.jdbcx.JdbcDataSource", + "org.h2.Driver", "io.quarkus.hibernate.orm.runtime.dialect.QuarkusH2Dialect", new Function() { @Override @@ -112,19 +117,21 @@ public final class Database { ), MYSQL("mysql", "com.mysql.cj.jdbc.MysqlXADataSource", + "com.mysql.cj.jdbc.Driver", "org.hibernate.dialect.MySQL8Dialect", - "jdbc:mysql://${kc.db-url-host:localhost}/${kc.db-url-database:keycloak}${kc.db-url-properties:}", asList("org.keycloak.connections.jpa.updater.liquibase.UpdatedMySqlDatabase") ), MARIADB("mariadb", "org.mariadb.jdbc.MySQLDataSource", + "org.mariadb.jdbc.Driver", "org.hibernate.dialect.MariaDBDialect", "jdbc:mariadb://${kc.db-url-host:localhost}/${kc.db-url-database:keycloak}${kc.db-url-properties:}", asList("org.keycloak.connections.jpa.updater.liquibase.UpdatedMariaDBDatabase") ), POSTGRES("postgresql", "org.postgresql.xa.PGXADataSource", + "org.postgresql.Driver", "io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL10Dialect", "jdbc:postgresql://${kc.db-url-host:localhost}/${kc.db-url-database:keycloak}${kc.db-url-properties:}", asList("liquibase.database.core.PostgresDatabase", @@ -133,6 +140,7 @@ public final class Database { ), MSSQL("mssql", "com.microsoft.sqlserver.jdbc.SQLServerXADataSource", + "com.microsoft.sqlserver.jdbc.SQLServerDriver", "org.hibernate.dialect.SQLServer2016Dialect", "jdbc:sqlserver://${kc.db-url-host:localhost}:1433;databaseName=${kc.db-url-database:keycloak}${kc.db-url-properties:}", asList("org.keycloak.quarkus.runtime.storage.database.liquibase.database.CustomMSSQLDatabase"), @@ -140,38 +148,36 @@ public final class Database { ), ORACLE("oracle", "oracle.jdbc.xa.client.OracleXADataSource", + "oracle.jdbc.driver.OracleDriver", "org.hibernate.dialect.Oracle12cDialect", "jdbc:oracle:thin:@//${kc.db-url-host:localhost}:1521/${kc.db-url-database:keycloak}", asList("liquibase.database.core.OracleDatabase") ); final String databaseKind; - final String driver; + final String xaDriver; + final String nonXaDriver; final Function dialect; final Function defaultUrl; final List liquibaseTypes; final String[] aliases; - Vendor(String databaseKind, String driver, String dialect, String defaultUrl, List liquibaseTypes, - String... aliases) { - this(databaseKind, driver, alias -> dialect, alias -> defaultUrl, liquibaseTypes, aliases); + Vendor(String databaseKind, String xaDriver, String nonXaDriver, String dialect, String defaultUrl, List liquibaseTypes, + String... aliases) { + this(databaseKind, xaDriver, nonXaDriver, alias -> dialect, alias -> defaultUrl, liquibaseTypes, aliases); } - Vendor(String databaseKind, String driver, String dialect, Function defaultUrl, - List liquibaseTypes, String... aliases) { - this(databaseKind, driver, alias -> dialect, defaultUrl, liquibaseTypes, aliases); + Vendor(String databaseKind, String xaDriver, String nonXaDriver, String dialect, Function defaultUrl, + List liquibaseTypes, String... aliases) { + this(databaseKind, xaDriver, nonXaDriver, alias -> dialect, defaultUrl, liquibaseTypes, aliases); } - Vendor(String databaseKind, String driver, Function dialect, String defaultUrl, - List liquibaseTypes, String... aliases) { - this(databaseKind, driver, dialect, alias -> defaultUrl, liquibaseTypes, aliases); - } - - Vendor(String databaseKind, String driver, Function dialect, Function defaultUrl, - List liquibaseTypes, - String... aliases) { + Vendor(String databaseKind, String xaDriver, String nonXaDriver, Function dialect, Function defaultUrl, + List liquibaseTypes, + String... aliases) { this.databaseKind = databaseKind; - this.driver = driver; + this.xaDriver = xaDriver; + this.nonXaDriver = nonXaDriver; this.dialect = dialect; this.defaultUrl = defaultUrl; this.liquibaseTypes = liquibaseTypes; diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java index 5a153b37b8..8cb3c1cb0b 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java @@ -380,7 +380,7 @@ public class ConfigurationTest { public void testDatabaseDriverSetExplicitly() { System.setProperty(CLI_ARGS, "--db=mssql" + ARG_SEPARATOR + "--db-url=jdbc:sqlserver://localhost/keycloak"); System.setProperty("kc.db-driver", "com.microsoft.sqlserver.jdbc.SQLServerDriver"); - System.setProperty("kc.db-tx-type", "enabled"); + System.setProperty("kc.transaction-xa-enabled", "false"); assertTrue(System.getProperty(CLI_ARGS, "").contains("mssql")); SmallRyeConfig config = createConfig(); assertEquals("jdbc:sqlserver://localhost/keycloak", config.getConfigValue("quarkus.datasource.jdbc.url").getValue()); @@ -389,6 +389,23 @@ public class ConfigurationTest { assertEquals("enabled", config.getConfigValue("quarkus.datasource.jdbc.transactions").getValue()); } + @Test + public void testTransactionTypeChangesDriver() { + System.setProperty(CLI_ARGS, "--db=mssql" + ARG_SEPARATOR + "--transaction-xa-enabled=false"); + assertTrue(System.getProperty(CLI_ARGS, "").contains("mssql")); + + SmallRyeConfig config = createConfig(); + assertEquals("com.microsoft.sqlserver.jdbc.SQLServerDriver", config.getConfigValue("quarkus.datasource.jdbc.driver").getValue()); + assertEquals("enabled", config.getConfigValue("quarkus.datasource.jdbc.transactions").getValue()); + + System.setProperty(CLI_ARGS, "--db=mssql" + ARG_SEPARATOR + "--transaction-xa-enabled=true"); + assertTrue(System.getProperty(CLI_ARGS, "").contains("mssql")); + SmallRyeConfig config2 = createConfig(); + + assertEquals("com.microsoft.sqlserver.jdbc.SQLServerXADataSource", config2.getConfigValue("quarkus.datasource.jdbc.driver").getValue()); + assertEquals("xa", config2.getConfigValue("quarkus.datasource.jdbc.transactions").getValue()); + } + @Test public void testResolveMetricsOption() { System.setProperty(CLI_ARGS, "--metrics-enabled=true"); diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/MSSQLStartDatabaseTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/MSSQLStartDatabaseTest.java index f9d50bb49f..4bb272b5fe 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/MSSQLStartDatabaseTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/MSSQLStartDatabaseTest.java @@ -36,7 +36,7 @@ public class MSSQLStartDatabaseTest extends AbstractStartDabataseTest { */ @Override @Test - @Launch({ "-Dkc.db-tx-type=enabled", "-Dkc.db-driver=com.microsoft.sqlserver.jdbc.SQLServerDriver", "start-dev" }) + @Launch({ "--transaction-xa-enabled=false", "start-dev" }) void testSuccessful(LaunchResult result) { CLIResult cliResult = (CLIResult) result; cliResult.assertStartedDevMode(); diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/dist/CustomTransactionDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/dist/CustomTransactionDistTest.java index 705ca056b5..f20626bc03 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/dist/CustomTransactionDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/dist/CustomTransactionDistTest.java @@ -28,21 +28,7 @@ import io.quarkus.test.junit.main.LaunchResult; public class CustomTransactionDistTest { @Test - @Launch({ "-Dkc.db-tx-type=enabled", "-Dkc.db-driver=org.postgresql.xa.PGXADataSource", "build", "--db=postgres" }) - void failNoXAUsingXADriver(LaunchResult result) { - CLIResult cliResult = (CLIResult) result; - cliResult.assertError("Driver org.postgresql.xa.PGXADataSource is an XA datasource, but XA transactions have not been enabled on the default datasource"); - } - - @Test - @Launch({ "-Dkc.db-driver=com.microsoft.sqlserver.jdbc.SQLServerDriver", "build", "--db=mssql" }) - void failXAUsingNonXADriver(LaunchResult result) { - CLIResult cliResult = (CLIResult) result; - cliResult.assertError("Driver is not an XA dataSource, while XA has been enabled in the configuration of the default datasource"); - } - - @Test - @Launch({ "-Dkc.db-tx-type=enabled", "-Dkc.db-driver=com.microsoft.sqlserver.jdbc.SQLServerDriver", "build", "--db=mssql" }) + @Launch({ "build", "--db=mssql", "--transaction-xa-enabled=false" }) void testNoXa(LaunchResult result) { CLIResult cliResult = (CLIResult) result; cliResult.assertBuild(); diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelp.approved.txt index 51f18bd0b0..89051b3f0e 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelp.approved.txt @@ -39,6 +39,12 @@ Database: --db The database vendor. Possible values are: dev-file, dev-mem, mariadb, mssql, mysql, oracle, postgres +Transaction: + +--transaction-xa-enabled + Manually override the transaction type. Transaction type XA and the + appropriate driver is used by default. Default: true. + Feature: --features Enables a set of one or more features. diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testStartDevHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testStartDevHelpAll.approved.txt index a91dfb6efc..8a1083e3e1 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testStartDevHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testStartDevHelpAll.approved.txt @@ -52,6 +52,12 @@ Database: --db-username The username of the database user. +Transaction: + +--transaction-xa-enabled + Manually override the transaction type. Transaction type XA and the + appropriate driver is used by default. Default: true. + Feature: --features Enables a set of one or more features.