Skip to content

Commit 1540256

Browse files
authored
Add MongoDB container implementation under org.testcontainers.mongodb (#11095)
1 parent 71996af commit 1540256

File tree

6 files changed

+220
-10
lines changed

6 files changed

+220
-10
lines changed

docs/modules/databases/mongodb.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ The MongoDB module provides two Testcontainers for MongoDB unit testing:
1212
The following example shows how to create a MongoDBContainer:
1313

1414
<!--codeinclude-->
15-
[Creating a MongoDB container](../../../modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java) inside_block:creatingMongoDBContainer
15+
[Creating a MongoDB container](../../../modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java) inside_block:creatingMongoDBContainer
1616
<!--/codeinclude-->
1717

1818
And how to start it:
1919

2020
<!--codeinclude-->
21-
[Starting a MongoDB container](../../../modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java) inside_block:startingMongoDBContainer
21+
[Starting a MongoDB container](../../../modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java) inside_block:startingMongoDBContainer
2222
<!--/codeinclude-->
2323

2424
!!! note

modules/mongodb/src/main/java/org/testcontainers/containers/MongoDBContainer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
* Supported images: {@code mongo}, {@code mongodb/mongodb-community-server}, {@code mongodb/mongodb-enterprise-server}
1717
* <p>
1818
* Exposed ports: 27017
19+
*
20+
* @deprecated use {@link org.testcontainers.mongodb.MongoDBContainer} instead.
1921
*/
2022
@Slf4j
23+
@Deprecated
2124
public class MongoDBContainer extends GenericContainer<MongoDBContainer> {
2225

2326
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mongo");
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package org.testcontainers.mongodb;
2+
3+
import com.github.dockerjava.api.command.InspectContainerResponse;
4+
import lombok.NonNull;
5+
import lombok.SneakyThrows;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.testcontainers.containers.GenericContainer;
8+
import org.testcontainers.containers.wait.strategy.Wait;
9+
import org.testcontainers.utility.DockerImageName;
10+
import org.testcontainers.utility.MountableFile;
11+
12+
import java.io.IOException;
13+
14+
/**
15+
* Testcontainers implementation for MongoDB.
16+
* <p>
17+
* Supported images: {@code mongo}, {@code mongodb/mongodb-community-server}, {@code mongodb/mongodb-enterprise-server}
18+
* <p>
19+
* Exposed ports: 27017
20+
*/
21+
@Slf4j
22+
public class MongoDBContainer extends GenericContainer<MongoDBContainer> {
23+
24+
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mongo");
25+
26+
private static final DockerImageName COMMUNITY_SERVER_IMAGE = DockerImageName.parse(
27+
"mongodb/mongodb-community-server"
28+
);
29+
30+
private static final DockerImageName ENTERPRISE_SERVER_IMAGE = DockerImageName.parse(
31+
"mongodb/mongodb-enterprise-server"
32+
);
33+
34+
private static final int MONGODB_INTERNAL_PORT = 27017;
35+
36+
private static final int CONTAINER_EXIT_CODE_OK = 0;
37+
38+
private static final int AWAIT_INIT_REPLICA_SET_ATTEMPTS = 60;
39+
40+
private static final String MONGODB_DATABASE_NAME_DEFAULT = "test";
41+
42+
private static final String STARTER_SCRIPT = "/testcontainers_start.sh";
43+
44+
private boolean shardingEnabled;
45+
46+
private boolean rsEnabled;
47+
48+
public MongoDBContainer(@NonNull String dockerImageName) {
49+
this(DockerImageName.parse(dockerImageName));
50+
}
51+
52+
public MongoDBContainer(DockerImageName dockerImageName) {
53+
super(dockerImageName);
54+
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, COMMUNITY_SERVER_IMAGE, ENTERPRISE_SERVER_IMAGE);
55+
56+
withExposedPorts(MONGODB_INTERNAL_PORT);
57+
}
58+
59+
@Override
60+
protected void containerIsStarting(InspectContainerResponse containerInfo) {
61+
if (this.shardingEnabled) {
62+
copyFileToContainer(MountableFile.forClasspathResource("/sharding.sh", 0777), STARTER_SCRIPT);
63+
}
64+
}
65+
66+
@Override
67+
protected void containerIsStarted(InspectContainerResponse containerInfo, boolean reused) {
68+
if (this.rsEnabled) {
69+
initReplicaSet(reused);
70+
}
71+
}
72+
73+
private String[] buildMongoEvalCommand(String command) {
74+
return new String[] {
75+
"sh",
76+
"-c",
77+
"mongosh mongo --eval \"" + command + "\" || mongo --eval \"" + command + "\"",
78+
};
79+
}
80+
81+
private void checkMongoNodeExitCode(ExecResult execResult) {
82+
if (execResult.getExitCode() != CONTAINER_EXIT_CODE_OK) {
83+
String errorMessage = String.format("An error occurred: %s", execResult.getStdout());
84+
log.error(errorMessage);
85+
throw new ReplicaSetInitializationException(errorMessage);
86+
}
87+
}
88+
89+
private String buildMongoWaitCommand() {
90+
return String.format(
91+
"var attempt = 0; " +
92+
"while" +
93+
"(%s) " +
94+
"{ " +
95+
"if (attempt > %d) {quit(1);} " +
96+
"print('%s ' + attempt); sleep(100); attempt++; " +
97+
" }",
98+
"db.runCommand( { isMaster: 1 } ).ismaster==false",
99+
AWAIT_INIT_REPLICA_SET_ATTEMPTS,
100+
"An attempt to await for a single node replica set initialization:"
101+
);
102+
}
103+
104+
private void checkMongoNodeExitCodeAfterWaiting(ExecResult execResultWaitForMaster) {
105+
if (execResultWaitForMaster.getExitCode() != CONTAINER_EXIT_CODE_OK) {
106+
String errorMessage = String.format(
107+
"A single node replica set was not initialized in a set timeout: %d attempts",
108+
AWAIT_INIT_REPLICA_SET_ATTEMPTS
109+
);
110+
log.error(errorMessage);
111+
throw new ReplicaSetInitializationException(errorMessage);
112+
}
113+
}
114+
115+
@SneakyThrows(value = { IOException.class, InterruptedException.class })
116+
private void initReplicaSet(boolean reused) {
117+
if (reused && isReplicationSetAlreadyInitialized()) {
118+
log.debug("Replica set already initialized.");
119+
} else {
120+
log.debug("Initializing a single node node replica set...");
121+
ExecResult execResultInitRs = execInContainer(buildMongoEvalCommand("rs.initiate();"));
122+
log.debug(execResultInitRs.getStdout());
123+
checkMongoNodeExitCode(execResultInitRs);
124+
125+
log.debug(
126+
"Awaiting for a single node replica set initialization up to {} attempts",
127+
AWAIT_INIT_REPLICA_SET_ATTEMPTS
128+
);
129+
ExecResult execResultWaitForMaster = execInContainer(buildMongoEvalCommand(buildMongoWaitCommand()));
130+
log.debug(execResultWaitForMaster.getStdout());
131+
132+
checkMongoNodeExitCodeAfterWaiting(execResultWaitForMaster);
133+
}
134+
}
135+
136+
public static class ReplicaSetInitializationException extends RuntimeException {
137+
138+
ReplicaSetInitializationException(String errorMessage) {
139+
super(errorMessage);
140+
}
141+
}
142+
143+
@SneakyThrows
144+
private boolean isReplicationSetAlreadyInitialized() {
145+
// since we are creating a replica set with one node, this node must be primary (state = 1)
146+
ExecResult execCheckRsInit = execInContainer(
147+
buildMongoEvalCommand("if(db.adminCommand({replSetGetStatus: 1})['myState'] != 1) quit(900)")
148+
);
149+
return execCheckRsInit.getExitCode() == CONTAINER_EXIT_CODE_OK;
150+
}
151+
152+
/**
153+
* Enables replica set on the cluster
154+
*
155+
* @return this
156+
*/
157+
public MongoDBContainer withReplicaSet() {
158+
this.rsEnabled = true;
159+
withCommand("--replSet", "docker-rs");
160+
waitingFor(Wait.forLogMessage("(?i).*waiting for connections.*", 1));
161+
return this;
162+
}
163+
164+
/**
165+
* Enables sharding on the cluster
166+
*
167+
* @return this
168+
*/
169+
public MongoDBContainer withSharding() {
170+
this.shardingEnabled = true;
171+
withCommand("-c", "while [ ! -f " + STARTER_SCRIPT + " ]; do sleep 0.1; done; " + STARTER_SCRIPT);
172+
waitingFor(Wait.forLogMessage("(?i).*mongos ready.*", 1));
173+
withCreateContainerCmdModifier(cmd -> cmd.withEntrypoint("sh"));
174+
return this;
175+
}
176+
177+
/**
178+
* Gets a connection string url, unlike {@link #getReplicaSetUrl} this does not point to a
179+
* database
180+
* @return a connection url pointing to a mongodb instance
181+
*/
182+
public String getConnectionString() {
183+
return String.format("mongodb://%s:%d", getHost(), getMappedPort(MONGODB_INTERNAL_PORT));
184+
}
185+
186+
/**
187+
* Gets a replica set url for the default {@value #MONGODB_DATABASE_NAME_DEFAULT} database.
188+
*
189+
* @return a replica set url.
190+
*/
191+
public String getReplicaSetUrl() {
192+
return getReplicaSetUrl(MONGODB_DATABASE_NAME_DEFAULT);
193+
}
194+
195+
/**
196+
* Gets a replica set url for a provided <code>databaseName</code>.
197+
*
198+
* @param databaseName a database name.
199+
* @return a replica set url.
200+
*/
201+
public String getReplicaSetUrl(String databaseName) {
202+
if (!isRunning()) {
203+
throw new IllegalStateException("MongoDBContainer should be started first");
204+
}
205+
return getConnectionString() + "/" + databaseName;
206+
}
207+
}

modules/mongodb/src/test/java/org/testcontainers/containers/AbstractMongo.java renamed to modules/mongodb/src/test/java/org/testcontainers/mongodb/AbstractMongo.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.testcontainers.containers;
1+
package org.testcontainers.mongodb;
22

33
import com.mongodb.ReadConcern;
44
import com.mongodb.ReadPreference;

modules/mongodb/src/test/java/org/testcontainers/containers/CompatibleImageTest.java renamed to modules/mongodb/src/test/java/org/testcontainers/mongodb/CompatibleImageTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.testcontainers.containers;
1+
package org.testcontainers.mongodb;
22

33
import com.mongodb.client.MongoClient;
44
import com.mongodb.client.MongoClients;
@@ -23,7 +23,7 @@ static String[] image() {
2323
void shouldExecuteTransactions() {
2424
try (
2525
// creatingMongoDBContainer {
26-
final MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.10")
26+
MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.10").withReplicaSet()
2727
// }
2828
) {
2929
// startingMongoDBContainer {
@@ -36,7 +36,7 @@ void shouldExecuteTransactions() {
3636
@ParameterizedTest
3737
@MethodSource("image")
3838
void shouldSupportSharding(String image) {
39-
try (final MongoDBContainer mongoDBContainer = new MongoDBContainer(image).withSharding()) {
39+
try (MongoDBContainer mongoDBContainer = new MongoDBContainer(image).withSharding()) {
4040
mongoDBContainer.start();
4141
final MongoClient mongoClient = MongoClients.create(mongoDBContainer.getReplicaSetUrl());
4242

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.testcontainers.containers;
1+
package org.testcontainers.mongodb;
22

33
import org.junit.jupiter.api.Test;
44

@@ -13,7 +13,7 @@ class MongoDBContainerTest extends AbstractMongo {
1313
void shouldExecuteTransactions() {
1414
try (
1515
// creatingMongoDBContainer {
16-
final MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.10")
16+
MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.10").withReplicaSet()
1717
// }
1818
) {
1919
// startingMongoDBContainer {
@@ -25,14 +25,14 @@ void shouldExecuteTransactions() {
2525

2626
@Test
2727
void supportsMongoDB_7_0() {
28-
try (final MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:7.0")) {
28+
try (MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:7.0")) {
2929
mongoDBContainer.start();
3030
}
3131
}
3232

3333
@Test
3434
void shouldTestDatabaseName() {
35-
try (final MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.10")) {
35+
try (MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.10")) {
3636
mongoDBContainer.start();
3737
final String databaseName = "my-db";
3838
assertThat(mongoDBContainer.getReplicaSetUrl(databaseName)).endsWith(databaseName);

0 commit comments

Comments
 (0)