diff --git a/docs/modules/databases/mongodb.md b/docs/modules/databases/mongodb.md index 0d410d115d5..cbd82aabc33 100644 --- a/docs/modules/databases/mongodb.md +++ b/docs/modules/databases/mongodb.md @@ -12,13 +12,13 @@ The MongoDB module provides two Testcontainers for MongoDB unit testing: The following example shows how to create a MongoDBContainer: -[Creating a MongoDB container](../../../modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java) inside_block:creatingMongoDBContainer +[Creating a MongoDB container](../../../modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java) inside_block:creatingMongoDBContainer And how to start it: -[Starting a MongoDB container](../../../modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java) inside_block:startingMongoDBContainer +[Starting a MongoDB container](../../../modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java) inside_block:startingMongoDBContainer !!! note diff --git a/modules/mongodb/src/main/java/org/testcontainers/containers/MongoDBContainer.java b/modules/mongodb/src/main/java/org/testcontainers/containers/MongoDBContainer.java index e0bb04bb166..b986a720964 100644 --- a/modules/mongodb/src/main/java/org/testcontainers/containers/MongoDBContainer.java +++ b/modules/mongodb/src/main/java/org/testcontainers/containers/MongoDBContainer.java @@ -16,8 +16,11 @@ * Supported images: {@code mongo}, {@code mongodb/mongodb-community-server}, {@code mongodb/mongodb-enterprise-server} *

* Exposed ports: 27017 + * + * @deprecated use {@link org.testcontainers.mongodb.MongoDBContainer} instead. */ @Slf4j +@Deprecated public class MongoDBContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mongo"); diff --git a/modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBContainer.java b/modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBContainer.java new file mode 100644 index 00000000000..c61be6675ba --- /dev/null +++ b/modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBContainer.java @@ -0,0 +1,207 @@ +package org.testcontainers.mongodb; + +import com.github.dockerjava.api.command.InspectContainerResponse; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import java.io.IOException; + +/** + * Testcontainers implementation for MongoDB. + *

+ * Supported images: {@code mongo}, {@code mongodb/mongodb-community-server}, {@code mongodb/mongodb-enterprise-server} + *

+ * Exposed ports: 27017 + */ +@Slf4j +public class MongoDBContainer extends GenericContainer { + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mongo"); + + private static final DockerImageName COMMUNITY_SERVER_IMAGE = DockerImageName.parse( + "mongodb/mongodb-community-server" + ); + + private static final DockerImageName ENTERPRISE_SERVER_IMAGE = DockerImageName.parse( + "mongodb/mongodb-enterprise-server" + ); + + private static final int MONGODB_INTERNAL_PORT = 27017; + + private static final int CONTAINER_EXIT_CODE_OK = 0; + + private static final int AWAIT_INIT_REPLICA_SET_ATTEMPTS = 60; + + private static final String MONGODB_DATABASE_NAME_DEFAULT = "test"; + + private static final String STARTER_SCRIPT = "/testcontainers_start.sh"; + + private boolean shardingEnabled; + + private boolean rsEnabled; + + public MongoDBContainer(@NonNull String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + public MongoDBContainer(DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, COMMUNITY_SERVER_IMAGE, ENTERPRISE_SERVER_IMAGE); + + withExposedPorts(MONGODB_INTERNAL_PORT); + } + + @Override + protected void containerIsStarting(InspectContainerResponse containerInfo) { + if (this.shardingEnabled) { + copyFileToContainer(MountableFile.forClasspathResource("/sharding.sh", 0777), STARTER_SCRIPT); + } + } + + @Override + protected void containerIsStarted(InspectContainerResponse containerInfo, boolean reused) { + if (this.rsEnabled) { + initReplicaSet(reused); + } + } + + private String[] buildMongoEvalCommand(String command) { + return new String[] { + "sh", + "-c", + "mongosh mongo --eval \"" + command + "\" || mongo --eval \"" + command + "\"", + }; + } + + private void checkMongoNodeExitCode(ExecResult execResult) { + if (execResult.getExitCode() != CONTAINER_EXIT_CODE_OK) { + String errorMessage = String.format("An error occurred: %s", execResult.getStdout()); + log.error(errorMessage); + throw new ReplicaSetInitializationException(errorMessage); + } + } + + private String buildMongoWaitCommand() { + return String.format( + "var attempt = 0; " + + "while" + + "(%s) " + + "{ " + + "if (attempt > %d) {quit(1);} " + + "print('%s ' + attempt); sleep(100); attempt++; " + + " }", + "db.runCommand( { isMaster: 1 } ).ismaster==false", + AWAIT_INIT_REPLICA_SET_ATTEMPTS, + "An attempt to await for a single node replica set initialization:" + ); + } + + private void checkMongoNodeExitCodeAfterWaiting(ExecResult execResultWaitForMaster) { + if (execResultWaitForMaster.getExitCode() != CONTAINER_EXIT_CODE_OK) { + String errorMessage = String.format( + "A single node replica set was not initialized in a set timeout: %d attempts", + AWAIT_INIT_REPLICA_SET_ATTEMPTS + ); + log.error(errorMessage); + throw new ReplicaSetInitializationException(errorMessage); + } + } + + @SneakyThrows(value = { IOException.class, InterruptedException.class }) + private void initReplicaSet(boolean reused) { + if (reused && isReplicationSetAlreadyInitialized()) { + log.debug("Replica set already initialized."); + } else { + log.debug("Initializing a single node node replica set..."); + ExecResult execResultInitRs = execInContainer(buildMongoEvalCommand("rs.initiate();")); + log.debug(execResultInitRs.getStdout()); + checkMongoNodeExitCode(execResultInitRs); + + log.debug( + "Awaiting for a single node replica set initialization up to {} attempts", + AWAIT_INIT_REPLICA_SET_ATTEMPTS + ); + ExecResult execResultWaitForMaster = execInContainer(buildMongoEvalCommand(buildMongoWaitCommand())); + log.debug(execResultWaitForMaster.getStdout()); + + checkMongoNodeExitCodeAfterWaiting(execResultWaitForMaster); + } + } + + public static class ReplicaSetInitializationException extends RuntimeException { + + ReplicaSetInitializationException(String errorMessage) { + super(errorMessage); + } + } + + @SneakyThrows + private boolean isReplicationSetAlreadyInitialized() { + // since we are creating a replica set with one node, this node must be primary (state = 1) + ExecResult execCheckRsInit = execInContainer( + buildMongoEvalCommand("if(db.adminCommand({replSetGetStatus: 1})['myState'] != 1) quit(900)") + ); + return execCheckRsInit.getExitCode() == CONTAINER_EXIT_CODE_OK; + } + + /** + * Enables replica set on the cluster + * + * @return this + */ + public MongoDBContainer withReplicaSet() { + this.rsEnabled = true; + withCommand("--replSet", "docker-rs"); + waitingFor(Wait.forLogMessage("(?i).*waiting for connections.*", 1)); + return this; + } + + /** + * Enables sharding on the cluster + * + * @return this + */ + public MongoDBContainer withSharding() { + this.shardingEnabled = true; + withCommand("-c", "while [ ! -f " + STARTER_SCRIPT + " ]; do sleep 0.1; done; " + STARTER_SCRIPT); + waitingFor(Wait.forLogMessage("(?i).*mongos ready.*", 1)); + withCreateContainerCmdModifier(cmd -> cmd.withEntrypoint("sh")); + return this; + } + + /** + * Gets a connection string url, unlike {@link #getReplicaSetUrl} this does not point to a + * database + * @return a connection url pointing to a mongodb instance + */ + public String getConnectionString() { + return String.format("mongodb://%s:%d", getHost(), getMappedPort(MONGODB_INTERNAL_PORT)); + } + + /** + * Gets a replica set url for the default {@value #MONGODB_DATABASE_NAME_DEFAULT} database. + * + * @return a replica set url. + */ + public String getReplicaSetUrl() { + return getReplicaSetUrl(MONGODB_DATABASE_NAME_DEFAULT); + } + + /** + * Gets a replica set url for a provided databaseName. + * + * @param databaseName a database name. + * @return a replica set url. + */ + public String getReplicaSetUrl(String databaseName) { + if (!isRunning()) { + throw new IllegalStateException("MongoDBContainer should be started first"); + } + return getConnectionString() + "/" + databaseName; + } +} diff --git a/modules/mongodb/src/test/java/org/testcontainers/containers/AbstractMongo.java b/modules/mongodb/src/test/java/org/testcontainers/mongodb/AbstractMongo.java similarity index 98% rename from modules/mongodb/src/test/java/org/testcontainers/containers/AbstractMongo.java rename to modules/mongodb/src/test/java/org/testcontainers/mongodb/AbstractMongo.java index 4fe23fbb50c..8bd0f99dbd5 100644 --- a/modules/mongodb/src/test/java/org/testcontainers/containers/AbstractMongo.java +++ b/modules/mongodb/src/test/java/org/testcontainers/mongodb/AbstractMongo.java @@ -1,4 +1,4 @@ -package org.testcontainers.containers; +package org.testcontainers.mongodb; import com.mongodb.ReadConcern; import com.mongodb.ReadPreference; diff --git a/modules/mongodb/src/test/java/org/testcontainers/containers/CompatibleImageTest.java b/modules/mongodb/src/test/java/org/testcontainers/mongodb/CompatibleImageTest.java similarity index 88% rename from modules/mongodb/src/test/java/org/testcontainers/containers/CompatibleImageTest.java rename to modules/mongodb/src/test/java/org/testcontainers/mongodb/CompatibleImageTest.java index 7cd7296c14a..b877dca8954 100644 --- a/modules/mongodb/src/test/java/org/testcontainers/containers/CompatibleImageTest.java +++ b/modules/mongodb/src/test/java/org/testcontainers/mongodb/CompatibleImageTest.java @@ -1,4 +1,4 @@ -package org.testcontainers.containers; +package org.testcontainers.mongodb; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; @@ -23,7 +23,7 @@ static String[] image() { void shouldExecuteTransactions() { try ( // creatingMongoDBContainer { - final MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.10") + MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.10").withReplicaSet() // } ) { // startingMongoDBContainer { @@ -36,7 +36,7 @@ void shouldExecuteTransactions() { @ParameterizedTest @MethodSource("image") void shouldSupportSharding(String image) { - try (final MongoDBContainer mongoDBContainer = new MongoDBContainer(image).withSharding()) { + try (MongoDBContainer mongoDBContainer = new MongoDBContainer(image).withSharding()) { mongoDBContainer.start(); final MongoClient mongoClient = MongoClients.create(mongoDBContainer.getReplicaSetUrl()); diff --git a/modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java b/modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java similarity index 74% rename from modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java rename to modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java index 29d75f80640..816243d769e 100644 --- a/modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java +++ b/modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java @@ -1,4 +1,4 @@ -package org.testcontainers.containers; +package org.testcontainers.mongodb; import org.junit.jupiter.api.Test; @@ -13,7 +13,7 @@ class MongoDBContainerTest extends AbstractMongo { void shouldExecuteTransactions() { try ( // creatingMongoDBContainer { - final MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.10") + MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.10").withReplicaSet() // } ) { // startingMongoDBContainer { @@ -25,14 +25,14 @@ void shouldExecuteTransactions() { @Test void supportsMongoDB_7_0() { - try (final MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:7.0")) { + try (MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:7.0")) { mongoDBContainer.start(); } } @Test void shouldTestDatabaseName() { - try (final MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.10")) { + try (MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.10")) { mongoDBContainer.start(); final String databaseName = "my-db"; assertThat(mongoDBContainer.getReplicaSetUrl(databaseName)).endsWith(databaseName);