Merge pull request #2487 from mposolda/master

databaseSchema option - proper support for "validate" for both JPA and Mongo
This commit is contained in:
Marek Posolda 2016-04-05 08:48:33 +02:00
commit 1714422b10
8 changed files with 132 additions and 46 deletions

View file

@ -186,7 +186,9 @@ bin/add-user-keycloak.[sh|bat] -r master -u <username> -p <password>
<term>databaseSchema</term> <term>databaseSchema</term>
<listitem> <listitem>
<para> <para>
Specify if schema should be updated or validated. Valid values are "update" and "validate" ("update is default). Specify if schema should be updated or validated. Valid values are <literal>update</literal> and <literal>validate</literal>.
Value <literal>update</literal> is default and means that DB schema will be updated to latest version.
Value <literal>validate</literal> won't touch database schema, but it will fail to start if schema is not updated to latest version.
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
@ -303,7 +305,11 @@ bin/add-user-keycloak.[sh|bat] -r master -u <username> -p <password>
}, },
"user": { "user": {
"provider": "${keycloak.user.provider:jpa}" "provider": "jpa"
},
"userSessionPersister": {
"provider": "jpa"
}, },
]]></programlisting> ]]></programlisting>
@ -324,6 +330,10 @@ bin/add-user-keycloak.[sh|bat] -r master -u <username> -p <password>
"user": { "user": {
"provider": "mongo" "provider": "mongo"
}, },
"userSessionPersister": {
"provider": "mongo"
},
]]></programlisting> ]]></programlisting>
And at the end of the file add the snippet like this where you can configure details about your Mongo database: And at the end of the file add the snippet like this where you can configure details about your Mongo database:
@ -333,7 +343,8 @@ bin/add-user-keycloak.[sh|bat] -r master -u <username> -p <password>
"host": "127.0.0.1", "host": "127.0.0.1",
"port": "27017", "port": "27017",
"db": "keycloak", "db": "keycloak",
"connectionsPerHost": 100 "connectionsPerHost": 100,
"databaseSchema": "update"
} }
} }
]]></programlisting> ]]></programlisting>
@ -343,6 +354,14 @@ bin/add-user-keycloak.[sh|bat] -r master -u <username> -p <password>
if you want authenticate against your MongoDB. If user and password are not specified, Keycloak will connect if you want authenticate against your MongoDB. If user and password are not specified, Keycloak will connect
unauthenticated to your MongoDB. unauthenticated to your MongoDB.
</para> </para>
<para>
Option <literal>databaseSchema</literal> specify if mongo database "schema" should be updated or validated. Valid values are <literal>update</literal> and <literal>validate</literal>.
Value <literal>update</literal> means that DB schema will be updated to latest version.
Value <literal>validate</literal> won't touch database schema, but it will fail to start if schema is not updated to latest version.
</para>
<note>
Mongo doesn't have real database schema, but 'schema' in this case means to which Keycloak version the data in the Mongo database corresponds.
</note>
<para>Finally there is set of optional configuration options, which can be used to specify connection-pooling capabilities of Mongo client. Supported int options are: <para>Finally there is set of optional configuration options, which can be used to specify connection-pooling capabilities of Mongo client. Supported int options are:
<literal>connectionsPerHost</literal>, <literal>threadsAllowedToBlockForConnectionMultiplier</literal>, <literal>maxWaitTime</literal>, <literal>connectTimeout</literal> <literal>connectionsPerHost</literal>, <literal>threadsAllowedToBlockForConnectionMultiplier</literal>, <literal>maxWaitTime</literal>, <literal>connectTimeout</literal>
<literal>socketTimeout</literal>. Supported boolean options are: <literal>socketKeepAlive</literal>, <literal>autoConnectRetry</literal>. <literal>socketTimeout</literal>. Supported boolean options are: <literal>socketKeepAlive</literal>, <literal>autoConnectRetry</literal>.
@ -359,7 +378,8 @@ bin/add-user-keycloak.[sh|bat] -r master -u <username> -p <password>
"connectionsMongo": { "connectionsMongo": {
"default": { "default": {
"uri": "mongodb://user:password@127.0.0.1/authentication", "uri": "mongodb://user:password@127.0.0.1/authentication",
"db": "keycloak" "db": "keycloak",
"databaseSchema": "update"
} }
} }
]]></programlisting> ]]></programlisting>

View file

@ -95,8 +95,6 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
Connection connection = null; Connection connection = null;
String databaseSchema = config.get("databaseSchema");
Map<String, Object> properties = new HashMap<String, Object>(); Map<String, Object> properties = new HashMap<String, Object>();
String unitName = "keycloak-default"; String unitName = "keycloak-default";
@ -127,7 +125,12 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
properties.put(JpaUtils.HIBERNATE_DEFAULT_SCHEMA, schema); properties.put(JpaUtils.HIBERNATE_DEFAULT_SCHEMA, schema);
} }
if (databaseSchema != null) {
String databaseSchema = config.get("databaseSchema");
if (databaseSchema == null) {
throw new RuntimeException("Property 'databaseSchema' needs to be specified in the configuration");
}
if (databaseSchema.equals("development-update")) { if (databaseSchema.equals("development-update")) {
properties.put("hibernate.hbm2ddl.auto", "update"); properties.put("hibernate.hbm2ddl.auto", "update");
databaseSchema = null; databaseSchema = null;
@ -135,7 +138,6 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
properties.put("hibernate.hbm2ddl.auto", "validate"); properties.put("hibernate.hbm2ddl.auto", "validate");
databaseSchema = null; databaseSchema = null;
} }
}
properties.put("hibernate.show_sql", config.getBoolean("showSql", false)); properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
properties.put("hibernate.format_sql", config.getBoolean("formatSql", true)); properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));

View file

@ -21,6 +21,7 @@ import liquibase.Contexts;
import liquibase.Liquibase; import liquibase.Liquibase;
import liquibase.changelog.ChangeSet; import liquibase.changelog.ChangeSet;
import liquibase.changelog.RanChangeSet; import liquibase.changelog.RanChangeSet;
import liquibase.exception.LiquibaseException;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider; import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider; import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider;
@ -96,16 +97,27 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
@Override @Override
public void validate(Connection connection, String defaultSchema) { public void validate(Connection connection, String defaultSchema) {
logger.debug("Validating if database is updated");
try { try {
Liquibase liquibase = getLiquibase(connection, defaultSchema); Liquibase liquibase = getLiquibase(connection, defaultSchema);
liquibase.validate(); List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
} catch (Exception e) { if (!changeSets.isEmpty()) {
List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
String errorMessage = String.format("Failed to validate database schema. Schema needs updating database from %s to %s. Please change databaseSchema to 'update' or use other database",
ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId());
throw new RuntimeException(errorMessage);
} else {
logger.debug("Validation passed. Database is up-to-date");
}
} catch (LiquibaseException e) {
throw new RuntimeException("Failed to validate database", e); throw new RuntimeException("Failed to validate database", e);
} }
} }
private Liquibase getLiquibase(Connection connection, String defaultSchema) throws Exception { private Liquibase getLiquibase(Connection connection, String defaultSchema) throws LiquibaseException {
LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class); LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class);
return liquibaseProvider.getLiquibase(connection, defaultSchema); return liquibaseProvider.getLiquibase(connection, defaultSchema);
} }

View file

@ -156,13 +156,24 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
} }
private void update(KeycloakSession session) { private void update(KeycloakSession session) {
MongoUpdaterProvider mongoUpdater = session.getProvider(MongoUpdaterProvider.class); String databaseSchema = config.get("databaseSchema");
if (databaseSchema == null) {
throw new RuntimeException("Property 'databaseSchema' needs to be specified in the configuration of mongo connections");
} else {
MongoUpdaterProvider mongoUpdater = session.getProvider(MongoUpdaterProvider.class);
if (mongoUpdater == null) { if (mongoUpdater == null) {
throw new RuntimeException("Can't update database: Mongo updater provider not found"); throw new RuntimeException("Can't update database: Mongo updater provider not found");
} }
if (databaseSchema.equals("update")) {
mongoUpdater.update(session, db); mongoUpdater.update(session, db);
} else if (databaseSchema.equals("validate")) {
mongoUpdater.validate(session, db);
} else {
throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema);
}
}
} }

View file

@ -26,6 +26,8 @@ import org.keycloak.provider.Provider;
*/ */
public interface MongoUpdaterProvider extends Provider { public interface MongoUpdaterProvider extends Provider {
public void update(KeycloakSession session, DB db); void update(KeycloakSession session, DB db);
void validate(KeycloakSession session, DB db);
} }

View file

@ -65,38 +65,19 @@ public class DefaultMongoUpdaterProvider implements MongoUpdaterProvider {
log.debug("Starting database update"); log.debug("Starting database update");
try { try {
boolean changeLogExists = db.collectionExists(CHANGE_LOG_COLLECTION); boolean changeLogExists = db.collectionExists(CHANGE_LOG_COLLECTION);
boolean realmExists = db.collectionExists("realms");
DBCollection changeLog = db.getCollection(CHANGE_LOG_COLLECTION); DBCollection changeLog = db.getCollection(CHANGE_LOG_COLLECTION);
List<String> executed = new LinkedList<String>(); List<String> executed = getExecuted(db, changeLogExists, changeLog);
if (!changeLogExists && realmExists) { List<Update> updatesToRun = getUpdatesToRun(executed);
Update1_0_0_Final u = new Update1_0_0_Final();
executed.add(u.getId());
createLog(changeLog, u, 1);
} else if (changeLogExists) {
DBCursor cursor = changeLog.find().sort(new BasicDBObject("orderExecuted", 1));
while (cursor.hasNext()) {
executed.add((String) cursor.next().get("_id"));
}
}
List<Update> updatesToRun = new LinkedList<Update>();
for (Class<? extends Update> updateClass : updates) {
Update u = updateClass.newInstance();
if (!executed.contains(u.getId())) {
updatesToRun.add(u);
}
}
if (!updatesToRun.isEmpty()) { if (!updatesToRun.isEmpty()) {
if (executed.isEmpty()) { if (executed.isEmpty()) {
log.info("Initializing database schema"); log.info("Initializing database schema");
} else { } else {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.infov("Updating database from {0} to {1}", executed.get(executed.size() - 1), updatesToRun.get(updatesToRun.size() - 1).getId()); log.debugv("Updating database from {0} to {1}", executed.get(executed.size() - 1), updatesToRun.get(updatesToRun.size() - 1).getId());
} else { } else {
log.debugv("Updating database"); log.info("Updating database");
} }
} }
@ -121,10 +102,66 @@ public class DefaultMongoUpdaterProvider implements MongoUpdaterProvider {
} }
} }
@Override
public void validate(KeycloakSession session, DB db) {
log.debug("Validating database");
boolean changeLogExists = db.collectionExists(CHANGE_LOG_COLLECTION);
DBCollection changeLog = db.getCollection(CHANGE_LOG_COLLECTION);
List<String> executed = getExecuted(db, changeLogExists, changeLog);
List<Update> updatesToRun = getUpdatesToRun(executed);
if (!updatesToRun.isEmpty()) {
String errorMessage = String.format("Failed to validate Mongo database schema. Schema needs updating database from %s to %s. Please change databaseSchema to 'update'",
executed.get(executed.size() - 1), updatesToRun.get(updatesToRun.size() - 1).getId());
throw new RuntimeException(errorMessage);
} else {
log.debug("Validation passed. Database is up to date");
}
}
private List<String> getExecuted(DB db, boolean changeLogExists, DBCollection changeLog) {
boolean realmExists = db.collectionExists("realms");
List<String> executed = new LinkedList<>();
if (!changeLogExists && realmExists) {
Update1_0_0_Final u = new Update1_0_0_Final();
executed.add(u.getId());
createLog(changeLog, u, 1);
} else if (changeLogExists) {
DBCursor cursor = changeLog.find().sort(new BasicDBObject("orderExecuted", 1));
while (cursor.hasNext()) {
executed.add((String) cursor.next().get("_id"));
}
}
return executed;
}
private List<Update> getUpdatesToRun(List<String> executed) {
try {
List<Update> updatesToRun = new LinkedList<>();
for (Class<? extends Update> updateClass : updates) {
Update u = updateClass.newInstance();
if (!executed.contains(u.getId())) {
updatesToRun.add(u);
}
}
return updatesToRun;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void createLog(DBCollection changeLog, Update update, int orderExecuted) { private void createLog(DBCollection changeLog, Update update, int orderExecuted) {
changeLog.insert(new BasicDBObject("_id", update.getId()).append("dateExecuted", new Date()).append("orderExecuted", orderExecuted)); changeLog.insert(new BasicDBObject("_id", update.getId()).append("dateExecuted", new Date()).append("orderExecuted", orderExecuted));
} }
@Override @Override
public void close() { public void close() {
} }

View file

@ -94,6 +94,7 @@
"host": "${keycloak.connectionsMongo.host:127.0.0.1}", "host": "${keycloak.connectionsMongo.host:127.0.0.1}",
"port": "${keycloak.connectionsMongo.port:27017}", "port": "${keycloak.connectionsMongo.port:27017}",
"db": "${keycloak.connectionsMongo.db:keycloak}", "db": "${keycloak.connectionsMongo.db:keycloak}",
"databaseSchema": "${keycloak.connectionsMongo.databaseSchema:update}",
"connectionsPerHost": "${keycloak.connectionsMongo.connectionsPerHost:100}" "connectionsPerHost": "${keycloak.connectionsMongo.connectionsPerHost:100}"
} }
}, },

View file

@ -73,6 +73,7 @@
"host": "${keycloak.connectionsMongo.host:127.0.0.1}", "host": "${keycloak.connectionsMongo.host:127.0.0.1}",
"port": "${keycloak.connectionsMongo.port:27017}", "port": "${keycloak.connectionsMongo.port:27017}",
"db": "${keycloak.connectionsMongo.db:keycloak}", "db": "${keycloak.connectionsMongo.db:keycloak}",
"databaseSchema": "${keycloak.connectionsMongo.databaseSchema:update}",
"connectionsPerHost": "${keycloak.connectionsMongo.connectionsPerHost:100}" "connectionsPerHost": "${keycloak.connectionsMongo.connectionsPerHost:100}"
} }
}, },