Skip to content

Commit f0238c9

Browse files
committed
feat(embedded-mongodb): add support to launch mongo as a replicaset
1 parent dd9be4f commit f0238c9

File tree

8 files changed

+176
-42
lines changed

8 files changed

+176
-42
lines changed

embedded-mongodb/README.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
* `embedded.mongodb.username`
2424
* `embedded.mongodb.password`
2525
* `embedded.mongodb.database` `(default is test)`
26+
* `embedded.mongodb.replica-set-name` Default is to run in standalone
2627
* `embedded.toxiproxy.proxies.mongodb.enabled` Enables both creation of the container with ToxiProxy TCP proxy and a proxy to the `embedded-mongodb` container.
2728

2829

@@ -33,6 +34,7 @@
3334
* `embedded.mongodb.username`
3435
* `embedded.mongodb.password`
3536
* `embedded.mongodb.database`
37+
* `embedded.mongodb.replica-set-name`
3638
* `embedded.mongodb.toxiproxy.host`
3739
* `embedded.mongodb.toxiproxy.port`
3840
* `embedded.mongodb.networkAlias`
@@ -48,3 +50,5 @@ To auto-configure `spring-data-mongodb` use these properties in your test `appli
4850
----
4951
spring.data.mongodb.uri=mongodb://${embedded.mongodb.host}:${embedded.mongodb.port}/${embedded.mongodb.database}
5052
----
53+
54+
For replicaset, add additional mongodb properties: `?rs=${embedded.mongodb.replica-set-name}&directConnection=true&authSource=admin`

embedded-mongodb/src/main/java/com/playtika/testcontainer/mongodb/EmbeddedMongodbBootstrapConfiguration.java

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,23 @@
1010
import org.springframework.beans.factory.annotation.Qualifier;
1111
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
1212
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
13-
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
1413
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
1514
import org.springframework.boot.context.properties.EnableConfigurationProperties;
1615
import org.springframework.context.annotation.Bean;
1716
import org.springframework.context.annotation.Configuration;
1817
import org.springframework.core.env.ConfigurableEnvironment;
1918
import org.springframework.core.env.MapPropertySource;
19+
import org.springframework.core.io.ClassPathResource;
20+
import org.testcontainers.containers.BindMode;
2021
import org.testcontainers.containers.GenericContainer;
2122
import org.testcontainers.containers.Network;
2223
import org.testcontainers.containers.ToxiproxyContainer;
2324
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
25+
import org.testcontainers.images.builder.Transferable;
26+
import org.testcontainers.shaded.org.apache.commons.lang3.StringUtils;
2427

28+
import java.io.IOException;
29+
import java.nio.charset.Charset;
2530
import java.util.LinkedHashMap;
2631
import java.util.Optional;
2732

@@ -44,10 +49,10 @@ public class EmbeddedMongodbBootstrapConfiguration {
4449
@Bean
4550
@ConditionalOnToxiProxyEnabled(module = "mongodb")
4651
ToxiproxyClientProxy mongodbContainerProxy(ToxiproxyClient toxiproxyClient,
47-
ToxiproxyContainer toxiproxyContainer,
48-
@Qualifier(BEAN_NAME_EMBEDDED_MONGODB) GenericContainer<?> mongodb,
49-
MongodbProperties properties,
50-
ConfigurableEnvironment environment) {
52+
ToxiproxyContainer toxiproxyContainer,
53+
@Qualifier(BEAN_NAME_EMBEDDED_MONGODB) GenericContainer<?> mongodb,
54+
MongodbProperties properties,
55+
ConfigurableEnvironment environment) {
5156
ToxiproxyClientProxy proxy = ToxiproxyHelper.createProxy(
5257
toxiproxyClient,
5358
toxiproxyContainer,
@@ -64,16 +69,36 @@ ToxiproxyClientProxy mongodbContainerProxy(ToxiproxyClient toxiproxyClient,
6469
@Bean(value = BEAN_NAME_EMBEDDED_MONGODB, destroyMethod = "stop")
6570
public GenericContainer<?> mongodb(ConfigurableEnvironment environment,
6671
MongodbProperties properties,
67-
MongodbStatusCheck mongodbStatusCheck,
68-
Optional<Network> network) {
69-
GenericContainer<?> mongodb =
70-
new GenericContainer<>(ContainerUtils.getDockerImageName(properties))
71-
.withEnv("MONGO_INITDB_ROOT_USERNAME", properties.getUsername())
72-
.withEnv("MONGO_INITDB_ROOT_PASSWORD", properties.getPassword())
73-
.withEnv("MONGO_INITDB_DATABASE", properties.getDatabase())
74-
.withExposedPorts(properties.getPort())
75-
.waitingFor(new LogMessageWaitStrategy().withRegEx(".*mongod startup complete.*"))
76-
.withNetworkAliases(MONGODB_NETWORK_ALIAS);
72+
Optional<Network> network) throws IOException, InterruptedException {
73+
74+
GenericContainer<?> mongodb;
75+
if (properties.getReplicaSetName() == null) {
76+
mongodb = new GenericContainer<>(ContainerUtils.getDockerImageName(properties))
77+
.withEnv("MONGO_INITDB_ROOT_USERNAME", properties.getUsername())
78+
.withEnv("MONGO_INITDB_ROOT_PASSWORD", properties.getPassword())
79+
.withEnv("MONGO_INITDB_DATABASE", properties.getDatabase())
80+
.withExposedPorts(properties.getPort())
81+
.waitingFor(new LogMessageWaitStrategy().withRegEx(".*mongod startup complete.*"))
82+
.withNetworkAliases(MONGODB_NETWORK_ALIAS);
83+
} else {
84+
mongodb = new GenericContainer<>(ContainerUtils.getDockerImageName(properties))
85+
.withCommand("-f", "/etc/mongod.conf")
86+
.withClasspathResourceMapping("/mongod/gen-keyfile.sh", "/docker-entrypoint-initdb.d/gen-keyfile.sh", BindMode.READ_ONLY)
87+
.withCopyToContainer(
88+
Transferable.of(
89+
new ClassPathResource("/mongod/mongod.conf")
90+
.getContentAsString(Charset.defaultCharset())
91+
.replace("${replica-set-name}", properties.getReplicaSetName())
92+
)
93+
, "/etc/mongod.conf")
94+
.withEnv("MONGO_INITDB_ROOT_USERNAME", properties.getUsername())
95+
.withEnv("MONGO_INITDB_ROOT_PASSWORD", properties.getPassword())
96+
.withEnv("MONGO_INITDB_DATABASE", properties.getDatabase())
97+
.withEnv("MONGO_INITDB_REPL_SET_HOST", properties.getHost())
98+
.withExposedPorts(properties.getPort())
99+
.waitingFor(new MongodbWaitStrategy(properties))
100+
.withNetworkAliases(MONGODB_NETWORK_ALIAS);
101+
}
77102

78103
network.ifPresent(mongodb::withNetwork);
79104

@@ -82,12 +107,6 @@ public GenericContainer<?> mongodb(ConfigurableEnvironment environment,
82107
return mongodb;
83108
}
84109

85-
@Bean
86-
@ConditionalOnMissingBean
87-
MongodbStatusCheck mongodbStartupCheckStrategy(MongodbProperties properties) {
88-
return new MongodbStatusCheck(properties);
89-
}
90-
91110
private void registerMongodbEnvironment(GenericContainer<?> mongodb, ConfigurableEnvironment environment, MongodbProperties properties) {
92111
Integer mappedPort = mongodb.getMappedPort(properties.getPort());
93112
String host = mongodb.getHost();
@@ -100,8 +119,13 @@ private void registerMongodbEnvironment(GenericContainer<?> mongodb, Configurabl
100119
map.put("embedded.mongodb.database", properties.getDatabase());
101120
map.put("embedded.mongodb.networkAlias", MONGODB_NETWORK_ALIAS);
102121
map.put("embedded.mongodb.internalPort", properties.getPort());
122+
if (StringUtils.isNotBlank(properties.getReplicaSetName())) {
123+
map.put("embedded.mongodb.replica-set-name", properties.getReplicaSetName());
124+
}
103125

104-
log.info("Started mongodb. Connection Details: {}, Connection URI: mongodb://{}:{}/{}", map, host, mappedPort, properties.getDatabase());
126+
log.info("Started mongodb. Connection Details: {}, Connection URI: mongodb://{}:{}/{}{}", map, host, mappedPort, properties.getDatabase(), Optional.ofNullable(
127+
properties.getReplicaSetName()).map("?directConnection=true&authSource=admin&rs="::concat).orElse("")
128+
);
105129

106130
MapPropertySource propertySource = new MapPropertySource("embeddedMongoInfo", map);
107131
environment.getPropertySources().addFirst(propertySource);

embedded-mongodb/src/main/java/com/playtika/testcontainer/mongodb/MongodbProperties.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class MongodbProperties extends CommonContainerProperties {
2525
private String username;
2626
private String password;
2727
private String database = "test";
28-
private String[] checkCommand = new String[]{"mongosh", "admin", "--eval", "\"db['system.version'].find()\""};
28+
private String replicaSetName;
2929

3030
public MongodbProperties() {
3131
this.setCapabilities(List.of(Capability.ALL));
@@ -36,6 +36,6 @@ public String getDefaultDockerImage() {
3636
// Please don`t remove this comment.
3737
// renovate: datasource=docker
3838
// https://hub.docker.com/_/mongo
39-
return "mongodb/mongodb-community-server:8.0.10-ubuntu2204";
39+
return "mongodb/mongodb-community-server:8.2.2-ubuntu2204";
4040
}
4141
}

embedded-mongodb/src/main/java/com/playtika/testcontainer/mongodb/MongodbStatusCheck.java

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.playtika.testcontainer.mongodb;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.SneakyThrows;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy;
7+
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
8+
9+
@Slf4j
10+
@AllArgsConstructor
11+
public class MongodbWaitStrategy extends AbstractWaitStrategy {
12+
13+
private final MongodbProperties properties;
14+
15+
@Override
16+
@SneakyThrows
17+
protected void waitUntilReady() {
18+
log.info("Waiting for mongodb to start");
19+
new LogMessageWaitStrategy().withRegEx(".*Waiting for connections.*").waitUntilReady(waitStrategyTarget);
20+
if (properties.getReplicaSetName() != null) {
21+
// The docker container will restart mongod and initialize the replicaset, so we just have to wait for that to finish now.
22+
log.info("Waiting for mongodb to become primary.");
23+
24+
LogMessageWaitStrategy logMessageWaitStrategy = new LogMessageWaitStrategy().withRegEx(".*database writes are now permitted.*");
25+
logMessageWaitStrategy.waitUntilReady(waitStrategyTarget);
26+
}
27+
}
28+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env bash
2+
3+
# Mongo requires a keyfile for replicasets, but the docker image does not generate one.
4+
# The image does let you run this arbitrary script after db initialization.
5+
6+
echo "testcontainers" > /data/configdb/mongod.keyfile
7+
chmod 400 /data/configdb/mongod.keyfile
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
storage:
3+
dbPath: /data/db
4+
5+
processManagement:
6+
timeZoneInfo: /usr/share/zoneinfo
7+
8+
net:
9+
port: 27017
10+
bindIp: 0.0.0.0
11+
12+
security:
13+
keyFile: /data/configdb/mongod.keyfile
14+
authorization: enabled
15+
16+
replication:
17+
replSetName: ${replica-set-name}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.playtika.testcontainer.mongodb;
2+
3+
4+
import lombok.Value;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.junit.jupiter.api.Test;
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
9+
import org.springframework.boot.test.context.SpringBootTest;
10+
import org.springframework.context.annotation.Configuration;
11+
import org.springframework.core.env.ConfigurableEnvironment;
12+
import org.springframework.data.annotation.Id;
13+
import org.springframework.data.mongodb.core.MongoTemplate;
14+
15+
import java.time.Instant;
16+
import java.util.UUID;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
@Slf4j
21+
@SpringBootTest(
22+
properties = {
23+
"embedded.mongodb.username=root",
24+
"embedded.mongodb.password=letmein",
25+
"embedded.mongodb.replica-set-name=rs0",
26+
"spring.data.mongodb.uri=mongodb://${embedded.mongodb.username}:${embedded.mongodb.password}@${embedded.mongodb.host}:${embedded.mongodb.port}/${embedded.mongodb.database}?replicaSet=${embedded.mongodb.replica-set-name}&directConnection=true&authSource=admin"
27+
}
28+
, classes = EmbeddedMongodbBootstrapReplicaSetConfigurationTest.TestConfiguration.class
29+
)
30+
public class EmbeddedMongodbBootstrapReplicaSetConfigurationTest {
31+
32+
@Autowired
33+
MongoTemplate mongoTemplate;
34+
35+
@Autowired
36+
ConfigurableEnvironment environment;
37+
38+
39+
@Test
40+
public void shouldSaveAndGet() {
41+
System.out.println("mongo-uri=" + environment.getProperty("spring.data.mongodb.uri"));
42+
String someId = UUID.randomUUID().toString();
43+
Foo foo = new Foo(someId, "foo", Instant.parse("2019-09-26T07:57:12.801Z"), -42L);
44+
mongoTemplate.save(foo);
45+
46+
assertThat(mongoTemplate.findById(someId, Foo.class)).isEqualTo(foo);
47+
}
48+
49+
@Test
50+
public void propertiesAreAvailable() {
51+
assertThat(environment.getProperty("embedded.mongodb.port")).isNotEmpty();
52+
assertThat(environment.getProperty("embedded.mongodb.host")).isNotEmpty();
53+
assertThat(environment.getProperty("embedded.mongodb.username")).isNotEmpty();
54+
assertThat(environment.getProperty("embedded.mongodb.password")).isNotEmpty();
55+
assertThat(environment.getProperty("embedded.mongodb.database")).isNotEmpty();
56+
assertThat(environment.getProperty("embedded.mongodb.replica-set-name")).isNotEmpty();
57+
}
58+
59+
@Value
60+
static class Foo {
61+
@Id
62+
String someId;
63+
String someString;
64+
Instant someTimestamp;
65+
Long someNumber;
66+
}
67+
68+
@EnableAutoConfiguration
69+
@Configuration
70+
static class TestConfiguration {
71+
}
72+
}

0 commit comments

Comments
 (0)