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

Use the quarkus MongoClients in the liquibase-mongodb extension #46326

Merged
merged 1 commit into from
Feb 18, 2025
Merged
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
5 changes: 3 additions & 2 deletions docs/src/main/asciidoc/liquibase-mongodb.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ The following is an example for the `{config-file}` file:
[source,properties]
----
# configure MongoDB
quarkus.mongodb.connection-string = mongodb://localhost:27017
quarkus.mongodb.connection-string = mongodb://localhost:27017/mydatabase

# Liquibase MongoDB minimal config properties
quarkus.liquibase-mongodb.migrate-at-start=true
Expand All @@ -80,8 +80,9 @@ quarkus.liquibase-mongodb.migrate-at-start=true
# quarkus.liquibase-mongodb.default-catalog-name=DefaultCatalog
# quarkus.liquibase-mongodb.default-schema-name=DefaultSchema
----
NOTE: Liquibase needs a database either in the connection string or with the `quarkus.mongodb.database` property.

NOTE: Liquibase MongoDB is configured using a connection string, we do our best to craft a connection string that matches the MongoDB client configuration but if some configuration properties are not working you may consider adding them directly into the `quarkus.mongodb.connection-string` config property.
NOTE: By default, Liquibase MongoDB is configured to use the default MongoDB client Quarkus creates, but you can configure the extension to use a named client by setting `quarkus.liquibase-mongodb.mongo-client-name`.

Add a changeLog file to the default folder following the Liquibase naming conventions: `{change-log}`
YAML, JSON and XML formats are supported for the changeLog.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,45 @@
import java.io.FileNotFoundException;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

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 io.quarkus.mongodb.runtime.MongoClients;
import io.quarkus.mongodb.runtime.MongodbConfig;
import io.quarkus.runtime.util.StringUtil;
import liquibase.Contexts;
import liquibase.LabelExpression;
import liquibase.Liquibase;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.ext.mongodb.database.MongoConnection;
import liquibase.ext.mongodb.database.MongoLiquibaseDatabase;
import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.resource.CompositeResourceAccessor;
import liquibase.resource.DirectoryResourceAccessor;
import liquibase.resource.ResourceAccessor;

public class LiquibaseMongodbFactory {

private final MongoClientConfig mongoClientConfig;
private final LiquibaseMongodbConfig liquibaseMongodbConfig;
private final LiquibaseMongodbBuildTimeConfig liquibaseMongodbBuildTimeConfig;

//connection-string format, see https://docs.mongodb.com/manual/reference/connection-string/
Pattern HAS_DB = Pattern
private static final Pattern HAS_DB = Pattern
.compile("(?<prefix>mongodb://|mongodb\\+srv://)(?<hosts>[^/]*)(?<slash>[/]?)(?<db>[^?]*)(?<options>\\??.*)");
private final LiquibaseMongodbConfig liquibaseMongodbConfig;
private final LiquibaseMongodbBuildTimeConfig liquibaseMongodbBuildTimeConfig;
private final MongodbConfig mongodbConfig;

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

private ResourceAccessor resolveResourceAccessor() throws FileNotFoundException {
Expand Down Expand Up @@ -83,54 +89,46 @@ private String parseChangeLog(String changeLog) {

public Liquibase createLiquibase() {
try (ResourceAccessor resourceAccessor = resolveResourceAccessor()) {
MongoClients mongoClients = Arc.container().instance(MongoClients.class).get();
String mongoClientName;
MongoClientConfig mongoClientConfig;
if (liquibaseMongodbConfig.mongoClientName().isPresent()) {
mongoClientName = liquibaseMongodbConfig.mongoClientName().get();
mongoClientConfig = mongodbConfig.mongoClientConfigs().get(mongoClientName);
if (mongoClientConfig == null) {
throw new IllegalArgumentException("Mongo client named '%s' not found".formatted(mongoClientName));
}
} else {
mongoClientConfig = mongodbConfig.defaultMongoClientConfig();
mongoClientName = MongoClientBeanUtil.DEFAULT_MONGOCLIENT_NAME;
}
String parsedChangeLog = parseChangeLog(liquibaseMongodbBuildTimeConfig.changeLog());
String connectionString = this.mongoClientConfig.connectionString().orElse("mongodb://localhost:27017");

// Every MongoDB client configuration must be added to the connection string, we didn't add all as it would be too much to support.
// For reference, all connections string options can be found here: https://www.mongodb.com/docs/manual/reference/connection-string/#connection-string-options.

String connectionString = 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}");
Optional<String> maybeDatabase = mongoClientConfig.database();
if (maybeDatabase.isEmpty()) {
if (matcher.matches() && !StringUtil.isNullOrEmpty(matcher.group("db"))) {
maybeDatabase = Optional.of(matcher.group("db"));
} else {
throw new IllegalArgumentException("Config property 'quarkus.mongodb.database' must " +
"be defined when no database exist in the connection string");
}
}
if (mongoClientConfig.credentials().authSource().isPresent()) {
boolean alreadyHasQueryParams = connectionString.contains("?");
connectionString += (alreadyHasQueryParams ? "&" : "?") + "authSource="
+ mongoClientConfig.credentials().authSource().get();
Database database = createDatabase(mongoClients, mongoClientName, maybeDatabase.get());
if (liquibaseMongodbConfig.liquibaseCatalogName().isPresent()) {
database.setLiquibaseCatalogName(liquibaseMongodbConfig.liquibaseCatalogName().get());
}
if (mongoClientConfig.credentials().authMechanism().isPresent()) {
boolean alreadyHasQueryParams = connectionString.contains("?");
connectionString += (alreadyHasQueryParams ? "&" : "?") + "authMechanism="
+ mongoClientConfig.credentials().authMechanism().get();
if (liquibaseMongodbConfig.liquibaseSchemaName().isPresent()) {
database.setLiquibaseSchemaName(liquibaseMongodbConfig.liquibaseSchemaName().get());
}
if (!mongoClientConfig.credentials().authMechanismProperties().isEmpty()) {
boolean alreadyHasQueryParams = connectionString.contains("?");
connectionString += (alreadyHasQueryParams ? "&" : "?") + "authMechanismProperties="
+ mongoClientConfig.credentials().authMechanismProperties().entrySet().stream()
.map(prop -> prop.getKey() + ":" + prop.getValue()).collect(Collectors.joining(","));
if (liquibaseMongodbConfig.liquibaseTablespaceName().isPresent()) {
database.setLiquibaseTablespaceName(liquibaseMongodbConfig.liquibaseTablespaceName().get());
}

Database database = DatabaseFactory.getInstance().openDatabase(connectionString,
this.mongoClientConfig.credentials().username().orElse(null),
this.mongoClientConfig.credentials().password().orElse(null),
null, resourceAccessor);

if (database != null) {
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());
}
if (liquibaseMongodbConfig.defaultCatalogName().isPresent()) {
database.setDefaultCatalogName(liquibaseMongodbConfig.defaultCatalogName().get());
}
if (liquibaseMongodbConfig.defaultSchemaName().isPresent()) {
database.setDefaultSchemaName(liquibaseMongodbConfig.defaultSchemaName().get());
}
Liquibase liquibase = new Liquibase(parsedChangeLog, resourceAccessor, database);

Expand All @@ -145,6 +143,16 @@ public Liquibase createLiquibase() {
}
}

private Database createDatabase(MongoClients clients, String clientName, String databaseName) {
MongoConnection databaseConnection = new MongoConnection();
MongoClient mongoClient = clients.createMongoClient(clientName);
databaseConnection.setMongoClient(mongoClient);
databaseConnection.setMongoDatabase(mongoClient.getDatabase(databaseName));
Database database = new MongoLiquibaseDatabase();
database.setConnection(databaseConnection);
return database;
}

public LiquibaseMongodbConfig getConfiguration() {
return liquibaseMongodbConfig;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ public interface LiquibaseMongodbConfig {
@WithDefault("true")
boolean enabled();

/**
* Mongodb client name to use to connect to database, defaults to the default mongodb client.
*/
Optional<String> mongoClientName();

/**
* The migrate at start flag
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public Supplier<LiquibaseMongodbFactory> liquibaseSupplier(LiquibaseMongodbConfi
return new Supplier<LiquibaseMongodbFactory>() {
@Override
public LiquibaseMongodbFactory get() {
return new LiquibaseMongodbFactory(config, buildTimeConfig, mongodbConfig.defaultMongoClientConfig());
return new LiquibaseMongodbFactory(config, buildTimeConfig, mongodbConfig);
}
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package io.quarkus.it.liquibase.mongodb;

import io.quarkus.mongodb.panache.PanacheMongoEntity;
import io.quarkus.mongodb.panache.common.MongoEntity;

@MongoEntity(clientName = "fruit-client")
public class Fruit extends PanacheMongoEntity {
public String name;
public String color;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
quarkus.mongodb.connection-string=mongodb://localhost:27017
quarkus.mongodb.database=fruits
# The tests use flapdoodle no need for devservices
quarkus.mongodb.devservices.enabled=false

quarkus.liquibase-mongodb.change-log=liquibase/changelog.xml
quarkus.liquibase-mongodb.migrate-at-start=true
quarkus.liquibase-mongodb.migrate-at-start=true
quarkus.liquibase-mongodb.mongo-client-name=fruit-client
quarkus.mongodb.fruit-client.database=fruits
quarkus.mongodb.fruit-client.hosts=localhost:27018
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.stream.StreamSupport;

import jakarta.inject.Inject;
import jakarta.inject.Named;

import org.bson.Document;
import org.junit.jupiter.api.Assertions;
Expand All @@ -19,22 +20,24 @@
import com.mongodb.client.MongoClient;

import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.common.ResourceArg;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.mongodb.MongoTestResource;
import io.restassured.common.mapper.TypeRef;

@QuarkusTest
@QuarkusTestResource(MongoTestResource.class)
@QuarkusTestResource(value = MongoTestResource.class, initArgs = @ResourceArg(name = "port", value = "27018"))
@DisabledOnOs(OS.WINDOWS)
class FruitResourceTest {

@Inject
@Named("fruit-client")
MongoClient mongoClient;

@Test
public void testTheEndpoint() {
// assert that a fruit exist as one has been created in the changelog
List<Fruit> list = get("/fruits").as(new TypeRef<List<Fruit>>() {
List<Fruit> list = get("/fruits").as(new TypeRef<>() {
});
Assertions.assertEquals(1, list.size());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
import org.junit.jupiter.api.condition.OS;

import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.common.ResourceArg;
import io.quarkus.test.junit.QuarkusIntegrationTest;
import io.quarkus.test.mongodb.MongoTestResource;
import io.restassured.common.mapper.TypeRef;

@QuarkusIntegrationTest
@QuarkusTestResource(MongoTestResource.class)
@QuarkusTestResource(value = MongoTestResource.class, initArgs = @ResourceArg(name = "port", value = "27018"))
@DisabledOnOs(OS.WINDOWS)
class NativeFruitResourceTestIT {
@Test
Expand Down
Loading