-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathNeo4jContainer.java
More file actions
382 lines (330 loc) · 14.2 KB
/
Neo4jContainer.java
File metadata and controls
382 lines (330 loc) · 14.2 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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
package org.testcontainers.containers;
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
import org.testcontainers.containers.wait.strategy.WaitStrategy;
import org.testcontainers.utility.ComparableVersion;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.LicenseAcceptance;
import org.testcontainers.utility.MountableFile;
import java.net.HttpURLConnection;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Testcontainers implementation for Neo4j.
* <p>
* Supported image: {@code neo4j}
* <p>
* Exposed ports:
* <ul>
* <li>Bolt: 7687</li>
* <li>HTTP: 7474</li>
* <li>HTTPS: 7473</li>
* </ul>
*/
public class Neo4jContainer<S extends Neo4jContainer<S>> extends GenericContainer<S> {
/**
* The image defaults to the official Neo4j image: <a href="https://hub.docker.com/_/neo4j/">Neo4j</a>.
*/
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("neo4j");
/**
* The default tag (version) to use.
*/
private static final String DEFAULT_TAG = "4.4";
private static final String ENTERPRISE_TAG = DEFAULT_TAG + "-enterprise";
/**
* Default port for the binary Bolt protocol.
*/
private static final int DEFAULT_BOLT_PORT = 7687;
/**
* The port of the transactional HTTPS endpoint: <a href="https://neo4j.com/docs/rest-docs/current/">Neo4j REST API</a>.
*/
private static final int DEFAULT_HTTPS_PORT = 7473;
/**
* The port of the transactional HTTP endpoint: <a href="https://neo4j.com/docs/rest-docs/current/">Neo4j REST API</a>.
*/
private static final int DEFAULT_HTTP_PORT = 7474;
/**
* The official image requires a change of password by default from "neo4j" to something else. This defaults to "password".
*/
private static final String DEFAULT_ADMIN_PASSWORD = "password";
private static final String AUTH_FORMAT = "neo4j/%s";
private final boolean standardImage;
private String adminPassword = DEFAULT_ADMIN_PASSWORD;
private final Set<String> labsPlugins = new HashSet<>();
/**
* Default wait strategies
*/
public static final WaitStrategy WAIT_FOR_BOLT = new LogMessageWaitStrategy()
.withRegEx(String.format(".*Bolt enabled on .*:%d\\.\n", DEFAULT_BOLT_PORT));
private static final WaitStrategy WAIT_FOR_HTTP = new HttpWaitStrategy()
.forPort(DEFAULT_HTTP_PORT)
.forStatusCodeMatching(response -> response == HttpURLConnection.HTTP_OK);
/**
* Creates a Neo4jContainer using a specific docker image.
*
* @param dockerImageName The docker image to use.
*/
public Neo4jContainer(String dockerImageName) {
this(DockerImageName.parse(dockerImageName));
}
/**
* Creates a Neo4jContainer using a specific docker image.
*
* @param dockerImageName The docker image to use.
*/
public Neo4jContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
this.standardImage = dockerImageName.getUnversionedPart().equals(DEFAULT_IMAGE_NAME.getUnversionedPart());
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
this.waitStrategy =
new WaitAllStrategy()
.withStrategy(WAIT_FOR_BOLT)
.withStrategy(WAIT_FOR_HTTP)
.withStartupTimeout(Duration.ofMinutes(2));
addExposedPorts(DEFAULT_BOLT_PORT, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT);
}
@Override
public Set<Integer> getLivenessCheckPortNumbers() {
return Stream
.of(DEFAULT_BOLT_PORT, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT)
.map(this::getMappedPort)
.collect(Collectors.toSet());
}
@Override
protected void configure() {
configureAuth();
configureLabsPlugins();
configureWaitStrategy();
}
/**
* Configured via {@link Neo4jContainer#withAdminPassword(String)} or {@link Neo4jContainer#withoutAuthentication()}
* It is only possible to set the correct auth in the configuration call.
* Also, the custom methods overrule the set env parameter.
*/
private void configureAuth() {
String neo4jAuthEnvKey = "NEO4J_AUTH";
if (!getEnvMap().containsKey(neo4jAuthEnvKey) || !DEFAULT_ADMIN_PASSWORD.equals(this.adminPassword)) {
boolean emptyAdminPassword = this.adminPassword == null || this.adminPassword.isEmpty();
String neo4jAuth = emptyAdminPassword ? "none" : String.format(AUTH_FORMAT, this.adminPassword);
addEnv(neo4jAuthEnvKey, neo4jAuth);
}
}
/**
* Configured via {@link Neo4jContainer#withLabsPlugins}.
* Configuration can only happen in the configuration call because there is no default.
*/
private void configureLabsPlugins() {
String neo4jLabsPluginsEnvKey = "NEO4JLABS_PLUGINS";
if (!getEnv().contains(neo4jLabsPluginsEnvKey) && !this.labsPlugins.isEmpty()) {
String enabledPlugins =
this.labsPlugins.stream().map(pluginName -> "\"" + pluginName + "\"").collect(Collectors.joining(","));
addEnv(neo4jLabsPluginsEnvKey, "[" + enabledPlugins + "]");
}
}
/**
* Update the default Neo4jContainer wait strategy based on the exposed ports.
* Still possible to override the startup timeout before starting the container via {@link WaitStrategy#withStartupTimeout(Duration)}.
*/
private void configureWaitStrategy() {
List<Integer> exposedPorts = getExposedPorts();
boolean boltExposed = exposedPorts.contains(DEFAULT_BOLT_PORT);
boolean httpExposed = exposedPorts.contains(DEFAULT_HTTP_PORT);
boolean onlyBoltExposed = boltExposed && !httpExposed;
boolean onlyHttpExposed = !boltExposed && httpExposed;
if (onlyBoltExposed) {
this.waitStrategy =
new WaitAllStrategy().withStrategy(WAIT_FOR_BOLT).withStartupTimeout(Duration.ofMinutes(2));
} else if (onlyHttpExposed) {
this.waitStrategy =
new WaitAllStrategy().withStrategy(WAIT_FOR_HTTP).withStartupTimeout(Duration.ofMinutes(2));
}
}
/**
* @return Bolt URL for use with Neo4j's Java-Driver.
*/
public String getBoltUrl() {
return String.format("bolt://" + getHost() + ":" + getMappedPort(DEFAULT_BOLT_PORT));
}
/**
* @return URL of the transactional HTTP endpoint.
*/
public String getHttpUrl() {
return String.format("http://" + getHost() + ":" + getMappedPort(DEFAULT_HTTP_PORT));
}
/**
* @return URL of the transactional HTTPS endpoint.
*/
public String getHttpsUrl() {
return String.format("https://" + getHost() + ":" + getMappedPort(DEFAULT_HTTPS_PORT));
}
/**
* Configures the container to use the enterprise edition of the default docker image.
* <br><br>
* Please have a look at the <a href="https://neo4j.com/licensing/">Neo4j Licensing page</a>. While the Neo4j
* Community Edition can be used for free in your projects under the GPL v3 license, Neo4j Enterprise edition
* needs either a commercial, education or evaluation license.
*
* @return This container.
*/
public S withEnterpriseEdition() {
if (!standardImage) {
throw new IllegalStateException(
String.format("Cannot use enterprise version with alternative image %s.", getDockerImageName())
);
}
setDockerImageName(DEFAULT_IMAGE_NAME.withTag(ENTERPRISE_TAG).asCanonicalNameString());
LicenseAcceptance.assertLicenseAccepted(getDockerImageName());
addEnv("NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes");
return self();
}
/**
* Sets the admin password for the default account (which is <pre>neo4j</pre>). A null value or an empty string
* disables authentication.
*
* @param adminPassword The admin password for the default database account.
* @return This container.
*/
public S withAdminPassword(final String adminPassword) {
if (adminPassword != null && adminPassword.length() < 8) {
logger().warn("Your provided admin password is too short and will not work with Neo4j 5.3+.");
}
this.adminPassword = adminPassword;
return self();
}
/**
* Disables authentication.
*
* @return This container.
*/
public S withoutAuthentication() {
return withAdminPassword(null);
}
/**
* Copies an existing {@code graph.db} folder into the container. This can either be a classpath resource or a
* host resource. Please have a look at the factory methods in {@link MountableFile}.
* <br>
* If you want to map your database into the container instead of copying them, please use {@code #withClasspathResourceMapping},
* but this will only work when your test does not run in a container itself.
* <br>
* Note: This method only works with Neo4j 3.5.
* <br>
* Mapping would work like this:
* <pre>
* @Container
* private static final Neo4jContainer databaseServer = new Neo4jContainer<>()
* .withClasspathResourceMapping("/test-graph.db", "/data/databases/graph.db", BindMode.READ_WRITE);
* </pre>
*
* @param graphDb The graph.db folder to copy into the container
* @throws IllegalArgumentException If the database version is not 3.5.
* @return This container.
*/
public S withDatabase(MountableFile graphDb) {
if (!isNeo4jDatabaseVersionSupportingDbCopy()) {
throw new IllegalArgumentException(
"Copying database folder is not supported for Neo4j instances with version 4.0 or higher."
);
}
return withCopyFileToContainer(graphDb, "/data/databases/graph.db");
}
/**
* Adds plugins to the given directory to the container. If {@code plugins} denotes a directory, than all of that
* directory is mapped to Neo4j's plugins. Otherwise, single resources are copied over.
* <br>
* If you want to map your plugins into the container instead of copying them, please use {@code #withClasspathResourceMapping},
* but this will only work when your test does not run in a container itself.
*
* @param plugins
* @return This container.
*/
public S withPlugins(MountableFile plugins) {
return withCopyFileToContainer(plugins, "/var/lib/neo4j/plugins/");
}
/**
* Adds Neo4j configuration properties to the container. The properties can be added as in the official Neo4j
* configuration, the method automatically translate them into the format required by the Neo4j container.
*
* @param key The key to configure, i.e. {@code dbms.security.procedures.unrestricted}
* @param value The value to set
* @return This container.
*/
public S withNeo4jConfig(String key, String value) {
addEnv(formatConfigurationKey(key), value);
return self();
}
/**
* @return The admin password for the <code>neo4j</code> account or literal <code>null</code> if auth is disabled.
*/
public String getAdminPassword() {
return adminPassword;
}
/**
* Registers one or more {@link Neo4jLabsPlugin} for download and server startup.
*
* @param neo4jLabsPlugins The Neo4j plugins that should get started with the server.
* @return This container.
* @deprecated {@link Neo4jLabsPlugin} were deprecated due to naming changes that cannot be solved by this enumeration.
* Please use the {@link Neo4jContainer#withPlugins(String...)} method.
*/
public S withLabsPlugins(Neo4jLabsPlugin... neo4jLabsPlugins) {
List<String> pluginNames = Arrays
.stream(neo4jLabsPlugins)
.map(plugin -> plugin.pluginName)
.collect(Collectors.toList());
this.labsPlugins.addAll(pluginNames);
return self();
}
/**
* @deprecated Please use {@link Neo4jContainer#withPlugins(String...)} for named plugins.
*/
public S withLabsPlugins(String... neo4jLabsPlugins) {
return this.withPlugins(neo4jLabsPlugins);
}
/**
* Registers one or more Neo4j plugins for server startup.
* The plugins are listed here
* <ul>
* <li><a href="https://neo4j.com/docs/operations-manual/5/configuration/plugins/">Neo4j 5</a></li>
* <li><a href="https://neo4j.com/docs/operations-manual/4.4/docker/operations/#docker-neo4jlabs-plugins">Neo4j 4.4</a></li>
* </ul>
*
* @param plugins The Neo4j plugins that should get started with the server.
* @return This container.
*/
public S withPlugins(String... plugins) {
this.labsPlugins.addAll(Arrays.asList(plugins));
return self();
}
private static String formatConfigurationKey(String plainConfigKey) {
final String prefix = "NEO4J_";
return String.format("%s%s", prefix, plainConfigKey.replaceAll("_", "__").replaceAll("\\.", "_"));
}
private boolean isNeo4jDatabaseVersionSupportingDbCopy() {
String usedImageVersion = DockerImageName.parse(getDockerImageName()).getVersionPart();
ComparableVersion usedComparableVersion = new ComparableVersion(usedImageVersion);
boolean versionSupportingDbCopy =
usedComparableVersion.isLessThan("4.0") && usedComparableVersion.isGreaterThanOrEqualTo("2");
if (versionSupportingDbCopy) {
return true;
}
if (!usedComparableVersion.isSemanticVersion()) {
logger()
.warn(
"Version {} is not a semantic version. The function \"withDatabase\" will fail.",
usedImageVersion
);
logger().warn("Copying databases is only supported for Neo4j versions 3.5.x");
}
return false;
}
public S withRandomPassword() {
return withAdminPassword(UUID.randomUUID().toString());
}
}