diff --git a/docs/documentation/upgrading/topics/changes/changes-26_4_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_4_0.adoc index 8b96fca9d6a..f8e50431dcf 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_4_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_4_0.adoc @@ -1,3 +1,4 @@ +// ------------------------ Breaking changes ------------------------ // == Breaking changes Breaking changes are identified as requiring changes from existing users to their configurations. @@ -5,6 +6,7 @@ In minor or patch releases we will only do breaking changes to fix bugs. === +// ------------------------ Notable changes ------------------------ // == Notable changes Notable changes where an internal behavior changed to prevent common misconfigurations, fix bugs or simplify running {project_name}. @@ -23,14 +25,25 @@ GET /admin/realms/{realm}/users?exact=false&q=myattribute: The {project_name} Admin Client is also updated with a new method to search users by attribute using the `exact` request parameter. +=== Automatic database connection properties for the PostgreSQL driver + +When running PostgreSQL reader and writer instances, {project_name} needs to always connect to the writer instance to do its work. + +Starting with this release, and when using the original PostgreSQL driver, {project_name} sets the `targetServerType` property of the PostgreSQL JDBC driver to `primary` to ensure that it always connects to a writable primary instance and never connects to a secondary reader instance in failover or switchover scenarios. + +You can override this behavior by setting your own value for `targetServerType` in the DB URL or additional properties. + +// ------------------------ Deprecated features ------------------------ // == Deprecated features The following sections provide details on deprecated features. === +// ------------------------ Removed features ------------------------ // == Removed features The following features have been removed from this release. === + diff --git a/docs/guides/server/db.adoc b/docs/guides/server/db.adoc index 85a93d0744b..98cd28d85a5 100644 --- a/docs/guides/server/db.adoc +++ b/docs/guides/server/db.adoc @@ -271,6 +271,18 @@ show server_encoding; create database keycloak with encoding 'UTF8'; ---- +== Preparing for PostgreSQL + +When running PostgreSQL reader and writer instances, {project_name} needs to always connect to the writer instance to do its work. +When using the original PostgreSQL driver, {project_name} sets the `targetServerType` property of the PostgreSQL JDBC driver to `primary` to ensure that it always connects to a writable primary instance and never connects to a secondary reader instance in failover or switchover scenarios. + +You can override this behavior by setting your own value for `targetServerType` in the DB URL or additional properties. + +[NOTE] +==== +The `targetServerType` is only applied automatically to the primary datasource, as requirements might be different for additional datasources. +==== + [[preparing-keycloak-for-amazon-aurora-postgresql]] == Preparing for Amazon Aurora PostgreSQL @@ -296,6 +308,8 @@ See the <@links.server id="containers" /> {section} for details on how to build `db-url`:: Insert `aws-wrapper` to the regular PostgreSQL JDBC URL resulting in a URL like `+jdbc:aws-wrapper:postgresql://...+`. `db-driver`:: Set to `software.amazon.jdbc.Driver` to use the AWS JDBC wrapper. +NOTE: When overriding the `wrapperPlugins` option of the AWS JDBC Driver, always include the `failover` or `failover2` plugin to ensure that {project_name} always connects to the writer instance even in failover or switchover scenarios. + == Preparing for MySQL server Beginning with MySQL 8.0.30, MySQL supports generated invisible primary keys for any InnoDB table that is created without an explicit primary key (more information https://dev.mysql.com/doc/refman/8.0/en/create-table-gipks.html[here]). 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 183fed3b98f..bd719e018b2 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 @@ -106,6 +106,11 @@ public class DatabaseOptions { .description("Deactivate specific named datasource .") .build(); + public static final Option DB_POSTGRESQL_TARGET_SERVER_TYPE = new OptionBuilder<>("db-postgres-target-server-type", String.class) + .category(OptionCategory.DATABASE) + .hidden() + .build(); + /** * Options that have their sibling for a named datasource * Example: for `db-dialect`, `db-dialect-` is created 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 8c12ffa3e44..986ffce47c4 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 @@ -15,10 +15,12 @@ import org.keycloak.utils.StringUtil; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Supplier; +import static org.keycloak.config.DatabaseOptions.DB; import static org.keycloak.config.DatabaseOptions.OPTIONS_DATASOURCES; import static org.keycloak.config.DatabaseOptions.getDatasourceOption; import static org.keycloak.config.DatabaseOptions.getKeyForDatasource; @@ -51,6 +53,11 @@ final class DatabasePropertyMappers { .mapFrom(DatabaseOptions.DB, DatabasePropertyMappers::getDatabaseUrl) .paramLabel("jdbc-url") .build(), + fromOption(DatabaseOptions.DB_POSTGRESQL_TARGET_SERVER_TYPE) + .to("quarkus.datasource.jdbc.additional-jdbc-properties.targetServerType") + .mapFrom(DatabaseOptions.DB, DatabasePropertyMappers::getPostgresqlTargetServerType) + .isEnabled(() -> getPostgresqlTargetServerType(Configuration.getConfigValue(DB).getValue(), null) != null) + .build(), fromOption(DatabaseOptions.DB_URL_HOST) .paramLabel("hostname") .build(), @@ -114,6 +121,33 @@ final class DatabasePropertyMappers { .description("Used for internal purposes of H2 database.") .build(); + private static String getPostgresqlTargetServerType(String db, ConfigSourceInterceptorContext context) { + Database.Vendor vendor = Database.getVendor(db).orElse(null); + if (vendor != Database.Vendor.POSTGRES) { + return null; + } + + String dbDriver = Configuration.getConfigValue(DatabaseOptions.DB_DRIVER).getValue(); + String dbUrl = Configuration.getConfigValue(DatabaseOptions.DB_URL).getValue(); + String dbUrlProperties = Configuration.getConfigValue(DatabaseOptions.DB_URL_PROPERTIES).getValue(); + + if (!Objects.equals(Database.getDriver(db, true).orElse(null), dbDriver) && + !Objects.equals(Database.getDriver(db, false).orElse(null), dbDriver)) { + // Custom JDBC-Driver, for example, AWS JDBC Wrapper. + return null; + } + if (dbUrlProperties != null && dbUrl != null && dbUrl.contains("${kc.db-url-properties:}") && dbUrlProperties.contains("targetServerType")) { + // targetServerType already set to same or different value in db-url-properties, ignore + return null; + } + if (dbUrl != null && dbUrl.contains("targetServerType")) { + // targetServerType already set to same or different value in db-url, ignore + return null; + } + log.debug("setting targetServerType for PostgreSQL to 'primary'"); + return "primary"; + } + private static String getDatabaseUrl(String name, String value, ConfigSourceInterceptorContext c) { return Database.getDefaultUrl(name, value).orElse(null); } diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/ConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/ConfigurationTest.java index 69367b86c76..f9c25c4fb63 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/ConfigurationTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/ConfigurationTest.java @@ -45,7 +45,6 @@ import org.junit.Assert; import org.junit.Test; import org.keycloak.Config; import org.keycloak.config.CachingOptions; -import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource; import org.keycloak.quarkus.runtime.configuration.mappers.HttpPropertyMappers; import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.vault.FilesKeystoreVaultProviderFactory; @@ -355,6 +354,24 @@ public class ConfigurationTest extends AbstractConfigurationTest { config = createConfig(); assertEquals("test-schema", config.getConfigValue("kc.db-schema").getValue()); assertEquals("test-schema", config.getConfigValue("kc.db-schema").getValue()); + + ConfigArgsConfigSource.setCliArgs("--db=postgres"); + config = createConfig(); + assertEquals("primary", config.getConfigValue("quarkus.datasource.jdbc.additional-jdbc-properties.targetServerType").getValue()); + + + ConfigArgsConfigSource.setCliArgs("--db=postgres", "--db-url-properties=?targetServerType=any"); + config = createConfig(); + assertNull(config.getConfigValue("quarkus.datasource.jdbc.additional-jdbc-properties.targetServerType").getValue()); + assertEquals("jdbc:postgresql://localhost:5432/keycloak?targetServerType=any", config.getConfigValue("quarkus.datasource.jdbc.url").getValue()); + + ConfigArgsConfigSource.setCliArgs("--db=postgres", "--db-driver=software.amazon.jdbc.Driver"); + config = createConfig(); + assertNull(config.getConfigValue("quarkus.datasource.jdbc.additional-jdbc-properties.targetServerType").getValue()); + + ConfigArgsConfigSource.setCliArgs("--db=postgres", "--db-url=jdbc:postgresql://localhost:5432/keycloak?targetServerType=any"); + config = createConfig(); + assertNull(config.getConfigValue("quarkus.datasource.jdbc.additional-jdbc-properties.targetServerType").getValue()); } // KEYCLOAK-15632 diff --git a/tests/base/src/test/java/org/keycloak/tests/db/DbTest.java b/tests/base/src/test/java/org/keycloak/tests/db/DbTest.java new file mode 100644 index 00000000000..0df0abffed0 --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/db/DbTest.java @@ -0,0 +1,29 @@ +package org.keycloak.tests.db; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.keycloak.config.DatabaseOptions; +import org.keycloak.quarkus.runtime.configuration.Configuration; +import org.keycloak.testframework.annotations.KeycloakIntegrationTest; +import org.keycloak.testframework.remote.runonserver.InjectRunOnServer; +import org.keycloak.testframework.remote.runonserver.RunOnServerClient; + +@KeycloakIntegrationTest +public class DbTest { + + @InjectRunOnServer + RunOnServerClient runOnServer; + + @Test + public void ensurePostgreSQLSettingsAreApplied() { + runOnServer.run(session -> { + if (Configuration.getConfigValue(DatabaseOptions.DB).getValue().equals("postgres") && + Configuration.getConfigValue(DatabaseOptions.DB_DRIVER).getValue().equals("org.postgresql.Driver")) { + Assertions.assertEquals("primary", Configuration.getConfigValue(DatabaseOptions.DB_POSTGRESQL_TARGET_SERVER_TYPE).getValue()); + } else { + Assertions.assertNull(Configuration.getConfigValue(DatabaseOptions.DB_POSTGRESQL_TARGET_SERVER_TYPE).getValue()); + } + }); + } + +} diff --git a/tests/base/src/test/java/org/keycloak/tests/suites/DatabaseTestSuite.java b/tests/base/src/test/java/org/keycloak/tests/suites/DatabaseTestSuite.java index 673fd5d4e74..6c001f3c038 100644 --- a/tests/base/src/test/java/org/keycloak/tests/suites/DatabaseTestSuite.java +++ b/tests/base/src/test/java/org/keycloak/tests/suites/DatabaseTestSuite.java @@ -4,6 +4,6 @@ import org.junit.platform.suite.api.SelectPackages; import org.junit.platform.suite.api.Suite; @Suite -@SelectPackages({"org.keycloak.tests.admin"}) +@SelectPackages({"org.keycloak.tests.admin", "org.keycloak.tests.db"}) public class DatabaseTestSuite { }