Automatically connect to a writer instance of PostgreSQL (#40384)

Closes #40383

Signed-off-by: Alexander Schwartz <alexander.schwartz@gmx.net>
Co-authored-by: Václav Muzikář <vaclav@muzikari.cz>
Co-authored-by: Martin Bartoš <mabartos@redhat.com>
This commit is contained in:
Alexander Schwartz 2025-07-04 16:46:49 +02:00 committed by GitHub
parent 47ca339656
commit 05d0c34681
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 114 additions and 2 deletions

View File

@ -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.
=== <TODO>
// ------------------------ 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.
=== <TODO>
// ------------------------ Removed features ------------------------ //
== Removed features
The following features have been removed from this release.
=== <TODO>

View File

@ -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]).

View File

@ -106,6 +106,11 @@ public class DatabaseOptions {
.description("Deactivate specific named datasource <datasource>.")
.build();
public static final Option<String> 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-<datasource>` is created

View File

@ -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);
}

View File

@ -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

View File

@ -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());
}
});
}
}

View File

@ -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 {
}