Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

quarkus-liquibase-mongodb enhancements #29265

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
package io.quarkus.liquibase.mongodb;

import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.enterprise.inject.Default;
import javax.enterprise.inject.literal.NamedLiteral;
import javax.enterprise.util.AnnotationLiteral;

import com.mongodb.client.MongoClient;

import io.quarkus.arc.Arc;
import io.quarkus.liquibase.mongodb.runtime.LiquibaseMongodbBuildTimeConfig;
import io.quarkus.liquibase.mongodb.runtime.LiquibaseMongodbConfig;
import io.quarkus.mongodb.runtime.MongoClientBeanUtil;
import io.quarkus.mongodb.runtime.MongoClientConfig;
import liquibase.Contexts;
import liquibase.LabelExpression;
import liquibase.Liquibase;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.exception.DatabaseException;
import liquibase.ext.mongodb.database.MongoConnection;
import liquibase.ext.mongodb.database.MongoLiquibaseDatabase;
import liquibase.resource.ClassLoaderResourceAccessor;

public class LiquibaseMongodbFactory {

private final String mongoClientName;
private final MongoClientConfig mongoClientConfig;
private final LiquibaseMongodbConfig liquibaseMongodbConfig;
private final LiquibaseMongodbBuildTimeConfig liquibaseMongodbBuildTimeConfig;
Expand All @@ -25,53 +37,80 @@ public class LiquibaseMongodbFactory {
.compile("(?<prefix>mongodb://|mongodb\\+srv://)(?<hosts>[^/]*)(?<slash>[/]?)(?<db>[^?]*)(?<options>\\??.*)");

public LiquibaseMongodbFactory(LiquibaseMongodbConfig config,
LiquibaseMongodbBuildTimeConfig liquibaseMongodbBuildTimeConfig, MongoClientConfig mongoClientConfig) {
LiquibaseMongodbBuildTimeConfig liquibaseMongodbBuildTimeConfig, String mongoClientName,
MongoClientConfig mongoClientConfig) {
this.liquibaseMongodbConfig = config;
this.liquibaseMongodbBuildTimeConfig = liquibaseMongodbBuildTimeConfig;
this.mongoClientName = mongoClientName;
this.mongoClientConfig = mongoClientConfig;
}

public Liquibase createLiquibase() {
try (ClassLoaderResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(
Thread.currentThread().getContextClassLoader())) {
String connectionString = this.mongoClientConfig.connectionString.orElse("mongodb://localhost:27017");

Matcher matcher = HAS_DB.matcher(connectionString);
if (!matcher.matches() || matcher.group("db") == null || matcher.group("db").isEmpty()) {
connectionString = matcher.replaceFirst(
"${prefix}${hosts}/"
+ this.mongoClientConfig.database
.orElseThrow(() -> new IllegalArgumentException("Config property " +
"'quarkus.mongodb.database' must be defined when no database exist in the connection string"))
+ "${options}");
}
if (mongoClientConfig.credentials.authSource.isPresent()) {
boolean alreadyHasQueryParams = connectionString.contains("?");
connectionString += (alreadyHasQueryParams ? "&" : "?") + "authSource="
+ mongoClientConfig.credentials.authSource.get();
}
String databaseName = this.mongoClientConfig.database.orElseGet(() -> getConnectionStringDatabase().orElseThrow(() -> {
String propertyName = MongoClientBeanUtil.isDefault(this.mongoClientName)
? "quarkus.mongodb.database"
: "quarkus.mongodb." + this.mongoClientName + ".database";
return new IllegalArgumentException(
"Config property '" + propertyName + "' must be defined when no database exist in the connection string");
}));
return createLiquibase(databaseName);
}

Database database = DatabaseFactory.getInstance().openDatabase(connectionString,
this.mongoClientConfig.credentials.username.orElse(null),
this.mongoClientConfig.credentials.password.orElse(null),
null, resourceAccessor);
public Liquibase createLiquibase(String databaseName) {
return createLiquibase(databaseName, liquibaseMongodbBuildTimeConfig.changeLog);
}

public Liquibase createLiquibase(String databaseName, String changeLog) {
return createLiquibase(databaseName, changeLog, liquibaseMongodbConfig.changeLogParameters);
}

if (database != null) {
liquibaseMongodbConfig.liquibaseCatalogName.ifPresent(database::setLiquibaseCatalogName);
liquibaseMongodbConfig.liquibaseSchemaName.ifPresent(database::setLiquibaseSchemaName);
liquibaseMongodbConfig.liquibaseTablespaceName.ifPresent(database::setLiquibaseTablespaceName);
public Liquibase createLiquibase(String databaseName, String changeLog, Map<String, String> changeLogParameters) {
if (databaseName == null) {
throw new IllegalArgumentException("'databaseName' cannot be null");
}

if (changeLog == null) {
throw new IllegalArgumentException("'changelog' file cannot be null");
}

if (liquibaseMongodbConfig.defaultCatalogName.isPresent()) {
database.setDefaultCatalogName(liquibaseMongodbConfig.defaultCatalogName.get());
MongoClient mongoClient = Arc.container()
.instance(MongoClient.class, getLiteral()).get();

try (ClassLoaderResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(
Thread.currentThread().getContextClassLoader())) {
MongoConnection connection = new MongoConnection() {
@Override
public void close() throws DatabaseException {
// Ignore
}
if (liquibaseMongodbConfig.defaultSchemaName.isPresent()) {
database.setDefaultSchemaName(liquibaseMongodbConfig.defaultSchemaName.get());
};
connection.setMongoClient(mongoClient);
connection.setMongoDatabase(mongoClient.getDatabase(databaseName));

Database database = new MongoLiquibaseDatabase() {
@Override
public void close() throws DatabaseException {
// Ignore
}
};
database.setConnection(connection);

liquibaseMongodbConfig.liquibaseCatalogName.ifPresent(database::setLiquibaseCatalogName);
liquibaseMongodbConfig.liquibaseSchemaName.ifPresent(database::setLiquibaseSchemaName);
liquibaseMongodbConfig.liquibaseTablespaceName.ifPresent(database::setLiquibaseTablespaceName);

if (liquibaseMongodbConfig.defaultCatalogName.isPresent()) {
database.setDefaultCatalogName(liquibaseMongodbConfig.defaultCatalogName.get());
}
if (liquibaseMongodbConfig.defaultSchemaName.isPresent()) {
database.setDefaultSchemaName(liquibaseMongodbConfig.defaultSchemaName.get());
}
Liquibase liquibase = new Liquibase(liquibaseMongodbBuildTimeConfig.changeLog, resourceAccessor, database);
Liquibase liquibase = new Liquibase(changeLog, resourceAccessor, database);

for (Map.Entry<String, String> entry : liquibaseMongodbConfig.changeLogParameters.entrySet()) {
liquibase.getChangeLogParameters().set(entry.getKey(), entry.getValue());
if (changeLogParameters != null) {
for (Map.Entry<String, String> entry : changeLogParameters.entrySet()) {
liquibase.getChangeLogParameters().set(entry.getKey(), entry.getValue());
}
}

return liquibase;
Expand All @@ -81,6 +120,19 @@ public Liquibase createLiquibase() {
}
}

private Optional<String> getConnectionStringDatabase() {
String connectionString = this.mongoClientConfig.connectionString.orElse("mongodb://localhost:27017");
Matcher matcher = HAS_DB.matcher(connectionString);
if (!matcher.matches() || matcher.group("db") == null || matcher.group("db").isEmpty()) {
return Optional.empty();
}
return Optional.of(matcher.group("db"));
}

private AnnotationLiteral getLiteral() {
return MongoClientBeanUtil.isDefault(mongoClientName) ? Default.Literal.INSTANCE : NamedLiteral.of(mongoClientName);
}

public LiquibaseMongodbConfig getConfiguration() {
return liquibaseMongodbConfig;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.Map;
import java.util.Optional;

import io.quarkus.mongodb.runtime.MongoClientBeanUtil;
import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
Expand All @@ -15,6 +16,12 @@
@ConfigRoot(name = "liquibase-mongodb", phase = ConfigPhase.RUN_TIME)
public class LiquibaseMongodbConfig {

/**
* The name of mongo client
*/
@ConfigItem(defaultValue = MongoClientBeanUtil.DEFAULT_MONGOCLIENT_NAME)
public String mongoClientName;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to be able to target a specific client or should we support migrations of potentially all the MongoDB clients?

Copy link
Contributor Author

@mazenkhalil mazenkhalil Dec 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically we should have the ability to decide which mongo client to use for running liquibase (The proposed solution here).

An extent to this would be implementing named liquibase configurations where you can create multiple liquibase configurations each operates on specific mongo client as needed (Same as you can create named mongo clients to connect against different databases). This would allow us to run multiple migration scripts against different mongo clients through configurations.

I was exploring the codebase of mongoclient to see how named client were implemented, but I don't feel confident enough to propose same concept for liquibase within this PR before fully understanding how build steps work in quarkus.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree we should provide liquibase migration for all MongoDB client as its JDBC counterpart support migration for all named datasources.


/**
* The migrate at start flag
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import io.quarkus.arc.InjectableInstance;
import io.quarkus.arc.InstanceHandle;
import io.quarkus.liquibase.mongodb.LiquibaseMongodbFactory;
import io.quarkus.mongodb.runtime.MongoClientBeanUtil;
import io.quarkus.mongodb.runtime.MongoClientConfig;
import io.quarkus.mongodb.runtime.MongodbConfig;
import io.quarkus.runtime.annotations.Recorder;
import liquibase.Liquibase;
Expand All @@ -21,7 +23,10 @@ public Supplier<LiquibaseMongodbFactory> liquibaseSupplier(LiquibaseMongodbConfi
return new Supplier<LiquibaseMongodbFactory>() {
@Override
public LiquibaseMongodbFactory get() {
return new LiquibaseMongodbFactory(config, buildTimeConfig, mongodbConfig.defaultMongoClientConfig);
MongoClientConfig mongoClientConfig = MongoClientBeanUtil.isDefault(config.mongoClientName)
? mongodbConfig.defaultMongoClientConfig
: mongodbConfig.mongoClientConfigs.get(config.mongoClientName);
return new LiquibaseMongodbFactory(config, buildTimeConfig, config.mongoClientName, mongoClientConfig);
}
};
}
Expand All @@ -37,18 +42,21 @@ public void doStartActions() {
for (InstanceHandle<LiquibaseMongodbFactory> liquibaseFactoryHandle : liquibaseFactoryInstance.handles()) {
try {
LiquibaseMongodbFactory liquibaseFactory = liquibaseFactoryHandle.get();
if (liquibaseFactory.getConfiguration().cleanAtStart) {
try (Liquibase liquibase = liquibaseFactory.createLiquibase()) {

if (!liquibaseFactory.getConfiguration().cleanAtStart
&& !liquibaseFactory.getConfiguration().migrateAtStart) {
// Don't initialize if no clean or migration required at start
return;
}

try (Liquibase liquibase = liquibaseFactory.createLiquibase()) {
if (liquibaseFactory.getConfiguration().cleanAtStart) {
liquibase.dropAll();
}
}
if (liquibaseFactory.getConfiguration().migrateAtStart) {
if (liquibaseFactory.getConfiguration().validateOnMigrate) {
try (Liquibase liquibase = liquibaseFactory.createLiquibase()) {
if (liquibaseFactory.getConfiguration().migrateAtStart) {
if (liquibaseFactory.getConfiguration().validateOnMigrate) {
liquibase.validate();
}
}
try (Liquibase liquibase = liquibaseFactory.createLiquibase()) {
liquibase.update(liquibaseFactory.createContexts(), liquibaseFactory.createLabels());
}
}
Expand Down