forked from testcontainers/testcontainers-java
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMongoDBContainer.java
More file actions
213 lines (188 loc) · 8.08 KB
/
MongoDBContainer.java
File metadata and controls
213 lines (188 loc) · 8.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
package org.testcontainers.containers;
import com.github.dockerjava.api.command.InspectContainerResponse;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;
import java.io.IOException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Constructs a single node MongoDB replica set for testing transactions.
* <p>To construct a multi-node MongoDB cluster, consider the <a href="https://github.com/silaev/mongodb-replica-set/">mongodb-replica-set project on GitHub</a>
* <p>Tested on a MongoDB version 4.0.10+ (that is the default version if not specified).
*/
@Slf4j
public class MongoDBContainer extends GenericContainer<MongoDBContainer> {
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mongo");
private static final String DEFAULT_TAG = "4.0.10";
private static final int CONTAINER_EXIT_CODE_OK = 0;
private static final int MONGODB_INTERNAL_PORT = 27017;
private static final int AWAIT_INIT_REPLICA_SET_ATTEMPTS = 60;
private static final String MONGODB_DATABASE_NAME_DEFAULT = "test";
private static final String DOCKER_ENTRYPOINT_INIT_DIR = "docker-entrypoint-initdb.d";
/**
* @deprecated use {@link MongoDBContainer(DockerImageName)} instead
*/
@Deprecated
public MongoDBContainer() {
this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG));
}
public MongoDBContainer(@NonNull final String dockerImageName) {
this(DockerImageName.parse(dockerImageName));
}
public MongoDBContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
withExposedPorts(MONGODB_INTERNAL_PORT);
withCommand("--replSet", "docker-rs");
waitingFor(
Wait.forLogMessage("(?i).*waiting for connections.*", 1)
);
}
/**
* 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 <code>databaseName</code>.
*
* @param databaseName a database name.
* @return a replica set url.
*/
public String getReplicaSetUrl(final String databaseName) {
checkIfRunning();
return String.format(
"mongodb://%s:%d/%s",
getContainerIpAddress(),
getMappedPort(MONGODB_INTERNAL_PORT),
databaseName
);
}
private void checkIfRunning() {
if (!isRunning()) {
throw new IllegalStateException("MongoDBContainer should be started first");
}
}
/**
* Loads and executes JavaScript files directly.
* Note that all the files are supposed to be delivered to a container via proper commands before starting.
* Should be used as an alternative to docker-entrypoint-initdb.d. This is because at docker-entrypoint-initdb.d
* stage, a replica set is not yet initialized and thus cannot accept operations.
*
* @param paths directory or file names as an array of strings.
* @see GenericContainer#withClasspathResourceMapping(String, String, BindMode) etc.
*/
@SneakyThrows(value = {IOException.class, InterruptedException.class})
public void loadAndExecuteJsFiles(final String... paths) {
checkIfRunning();
final String loadCommand =
Stream.of(paths).map(it -> "load(\"" + it + "\")").collect(Collectors.joining(";"));
final ExecResult execResult = execInContainer(buildMongoEvalCommand(loadCommand));
if (execResult.getExitCode() != CONTAINER_EXIT_CODE_OK) {
final String errorMessage = String.format("An error occurred: %s", execResult.getStdout());
log.error(errorMessage);
throw new LoadAndExecuteJsFilesException(errorMessage);
}
}
@Override
protected void containerIsStarted(InspectContainerResponse containerInfo) {
initReplicaSet();
}
private String[] buildMongoEvalCommand(final String command) {
return new String[]{"mongo", "--eval", command};
}
private void checkMongoNodeExitCode(final Container.ExecResult execResult) {
if (execResult.getExitCode() != CONTAINER_EXIT_CODE_OK) {
final String errorMessage = String.format("An error occurred: %s", execResult.getStdout());
log.error(errorMessage);
throw new ReplicaSetInitializationException(errorMessage);
}
}
@SneakyThrows(value = {IOException.class, InterruptedException.class})
private void checkDockerEntrypointDirIsEmpty() {
final ExecResult execResult = execInContainer(
"/bin/bash",
"-c",
String.format(
"if [ -n \"$(find \"%s\" -maxdepth 0 -type d -empty 2>/dev/null)\" ]; then exit 0; else exit -1; fi",
DOCKER_ENTRYPOINT_INIT_DIR
)
);
if (execResult.getExitCode() != CONTAINER_EXIT_CODE_OK) {
throw new DockerEntrypointInitDirIsNotEmptyException(
String.format(
"%s is supposed to be empty while running with the --replSet command-line option. " +
"Consider using loadAndExecuteJsFiles(...). Error: %s",
DOCKER_ENTRYPOINT_INIT_DIR,
execResult.getStdout()
)
);
}
}
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(
final Container.ExecResult execResultWaitForMaster
) {
if (execResultWaitForMaster.getExitCode() != CONTAINER_EXIT_CODE_OK) {
final 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() {
checkDockerEntrypointDirIsEmpty();
log.debug("Initializing a single node node replica set...");
final 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
);
final ExecResult execResultWaitForMaster = execInContainer(
buildMongoEvalCommand(buildMongoWaitCommand())
);
log.debug(execResultWaitForMaster.getStdout());
checkMongoNodeExitCodeAfterWaiting(execResultWaitForMaster);
}
public static class ReplicaSetInitializationException extends RuntimeException {
ReplicaSetInitializationException(final String errorMessage) {
super(errorMessage);
}
}
public static class DockerEntrypointInitDirIsNotEmptyException extends RuntimeException {
DockerEntrypointInitDirIsNotEmptyException(final String errorMessage) {
super(errorMessage);
}
}
public static class LoadAndExecuteJsFilesException extends RuntimeException {
LoadAndExecuteJsFilesException(final String errorMessage) {
super(errorMessage);
}
}
}