Add support for initialization scripts#11331
Add support for initialization scripts#11331taole33 wants to merge 8 commits intotestcontainers:mainfrom
Conversation
Running initialization scripts in MongoDB causes the container to restart, which previously required manual WaitStrategy adjustment. This commit adds 'withInitScript(String)' to automatically handle the script copy and adjust the WaitStrategy to expect two startup log messages. Fixes testcontainers#3066
| * @param scriptPath the path to the init script file on the classpath | ||
| * @return this container instance | ||
| */ | ||
| public MongoDBContainer withInitScript(String scriptPath) { |
There was a problem hiding this comment.
org.testcontainers.containers.MongoDBContainer is deprecated. can you moved to org.testcontainers.mongodb.MongoDBContainer?
There was a problem hiding this comment.
I moved to org.testcontainers.mongodb.MongoDBContainer.
Also, For the tests, instead of creating a new file named MongoDBInitScriptTest.java, I added the test cases directly to MongoDBContainerTest.java.
6665ad7 to
67088da
Compare
67088da to
e81a2ac
Compare
|
does this work with ReplicaSet and Sharded mode? New MongoDBContainer implementation has |
|
I updated In these modes, the default entrypoint mechanism conflicts with the cluster initialization scripts or is ignored. To fix this, I modified the logic to isolate the init script from the default directory when running in Cluster mode, and instead execute it manually after the container has started. ReasonIn Cluster modes (ReplicaSet/Sharding), relying on the default
Changes
Fixes #3066 |
| .withEnv("LANG", "C.UTF-8") | ||
| .withEnv("LC_ALL", "C.UTF-8") | ||
| .withStartupTimeout(Duration.ofSeconds(30)) |
| try ( | ||
| MongoDBContainer mongoDB = new MongoDBContainer("mongo:4.0.10") | ||
| .withInitScript("init.js") | ||
| .withStartupTimeout(Duration.ofSeconds(30)) |
| .isEqualTo("{ } [ ] : ,"); | ||
|
|
||
| assertThat(doc.getString("description")) | ||
| .as("Japanese text should be preserved correctly") |
|
|
||
| assertThat(doc.getString("description")) | ||
| .as("Japanese text should be preserved correctly") | ||
| .isEqualTo("特殊記号を含むコレクションへの挿入テスト"); |
There was a problem hiding this comment.
Can we use plain english, please?
| @Test | ||
| void shouldExecuteInitScriptWithShardingConfiguredFirst() { | ||
| try (MongoDBContainer mongo = new MongoDBContainer("mongo:7.0.0").withSharding().withInitScript("init.js")) { | ||
| mongo.start(); | ||
| assertInitScriptExecuted(mongo); | ||
| } | ||
| } |
There was a problem hiding this comment.
let's remove it. There is no difference with the previous test
| @Test | ||
| void shouldExecuteInitScriptWithReplicaSetConfiguredFirst() { | ||
| try (MongoDBContainer mongo = new MongoDBContainer("mongo:7.0.0").withReplicaSet().withInitScript("init.js")) { | ||
| mongo.start(); | ||
| assertInitScriptExecuted(mongo); | ||
| } | ||
| } |
There was a problem hiding this comment.
let's remove it. There is no difference with the previous test
|
@eddumelendez In this update, I also addressed an edge case regarding filenames containing special characters (e.g., I believe this covers all cases. Let me know if you have any further questions. |
There was a problem hiding this comment.
Pull request overview
Adds first-class init-script support to org.testcontainers.mongodb.MongoDBContainer, addressing MongoDB’s entrypoint behavior of restarting mongod after running /docker-entrypoint-initdb.d/* scripts (which previously could cause readiness timing issues).
Changes:
- Added
MongoDBContainer.withInitScript(String)to copy an init script into the container and adjust readiness behavior. - Implemented special-character-tolerant init script transfer using
Transferable-based byte copying. - Added tests and resources to verify init script execution across standalone, replica set, and sharding modes.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBContainer.java | Adds init-script plumbing, classpath resolution + copy, wait strategy adjustments, and manual execution for cluster modes. |
| modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java | Adds test coverage for init scripts in standalone, replica set, sharding, and edge-case filenames. |
| modules/mongodb/src/test/resources/init.js | Provides a simple init script used by the new tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| Path scriptPath = Paths.get(this.initScriptPath); | ||
| String fileName = scriptPath.getFileName().toString(); | ||
| Path parentDir = scriptPath.getParent(); | ||
| String resourceDir = (parentDir == null) ? "" : parentDir.toString(); |
There was a problem hiding this comment.
configure() derives resourceDir via Paths.get(initScriptPath) / parentDir.toString(), which uses platform-specific separators (e.g., \ on Windows). Classpath resource lookup expects / separators, so scripts located under nested directories can fail to resolve on Windows. Consider treating scriptPath as a classpath resource name (string with /) rather than a filesystem Path, or normalize resourceDir to / separators before calling getResources.
| Path scriptPath = Paths.get(this.initScriptPath); | |
| String fileName = scriptPath.getFileName().toString(); | |
| Path parentDir = scriptPath.getParent(); | |
| String resourceDir = (parentDir == null) ? "" : parentDir.toString(); | |
| // Treat initScriptPath as a classpath resource name, not a filesystem path | |
| String normalizedScriptPath = this.initScriptPath.replace('\\', '/'); | |
| int lastSlash = normalizedScriptPath.lastIndexOf('/'); | |
| String fileName = (lastSlash == -1) ? normalizedScriptPath : normalizedScriptPath.substring(lastSlash + 1); | |
| String resourceDir = (lastSlash == -1) ? "" : normalizedScriptPath.substring(0, lastSlash); |
| Enumeration<URL> resources = this.getClass().getClassLoader().getResources(resourceDir); | ||
| byte[] fileContent = null; | ||
|
|
||
| while (resources.hasMoreElements()) { | ||
| URL dirUrl = resources.nextElement(); | ||
|
|
||
| if ("file".equals(dirUrl.getProtocol())) { | ||
| Path dirPath = Paths.get(dirUrl.toURI()); | ||
| Path candidatePath = dirPath.resolve(fileName); | ||
|
|
||
| if (Files.exists(candidatePath) && !Files.isDirectory(candidatePath)) { | ||
| fileContent = Files.readAllBytes(candidatePath); | ||
| break; | ||
| } | ||
| } |
There was a problem hiding this comment.
The init script resolution only considers classpath resources with the file: protocol. If the init script is packaged in a JAR (common in some test setups/build tools), dirUrl.getProtocol() will be jar and the script will never be found, resulting in a runtime failure. Consider loading the resource bytes via ClassLoader.getResourceAsStream (or similar) so both file: and jar: classpath resources work.
| } | ||
|
|
||
| String destination = isClusterMode ? SCRIPT_DESTINATION_MANUAL : SCRIPT_DESTINATION_DEFAULT; | ||
| withCopyToContainer(Transferable.of(fileContent, 0777), destination); |
There was a problem hiding this comment.
The init script is copied into the container with mode 0777. For a .js init script, execute permissions are not required and 0777 is overly permissive; it’s safer to use a read-only mode like 0644 (or 0444) unless execution is needed.
| withCopyToContainer(Transferable.of(fileContent, 0777), destination); | |
| withCopyToContainer(Transferable.of(fileContent, 0644), destination); |
| } | ||
| } | ||
|
|
||
| if (this.initScriptPath != null && !isClusterMode) { |
There was a problem hiding this comment.
configure() unconditionally overwrites the container’s waitStrategy when initScriptPath is set in standalone mode. This prevents users from supplying a custom waitingFor(...) strategy (it will be silently replaced at startup). Consider only adjusting the wait strategy if it is still the default (GenericContainer.DEFAULT_WAIT_STRATEGY) or offering an opt-out.
| if (this.initScriptPath != null && !isClusterMode) { | |
| if (this.initScriptPath != null | |
| && !isClusterMode | |
| && (this.waitStrategy == null || this.waitStrategy == GenericContainer.DEFAULT_WAIT_STRATEGY)) { |
|
|
||
| boolean isClusterMode = this.shardingEnabled || this.rsEnabled; | ||
|
|
||
| if (isClusterMode && this.initScriptPath != null) { |
There was a problem hiding this comment.
In cluster mode (replica set/sharding), the init script is executed unconditionally in containerIsStarted(...), even when reused == true. This differs from the official entrypoint behavior (init scripts run only on first initialization) and can break non-idempotent scripts when container reuse is enabled. Consider skipping init-script execution on reuse, or documenting/controlling this behavior explicitly.
| if (isClusterMode && this.initScriptPath != null) { | |
| if (isClusterMode && !reused && this.initScriptPath != null) { |
| @Test | ||
| void shouldExecuteInitScriptWithEdgeCases() { | ||
| try ( | ||
| MongoDBContainer mongoDB = new MongoDBContainer("mongo:4.0.10").withInitScript("initEdgeCase!@#%^& *'().js") |
There was a problem hiding this comment.
The init script resource name used here contains * ("initEdgeCase!@#%^& *'().js"). * is not a valid filename character on Windows filesystems, which can make the test resources impossible to check out/build on Windows and cause CI failures. Consider using a cross-platform-safe filename (still including tricky URL characters like # and spaces if needed) to validate the edge case.
| MongoDBContainer mongoDB = new MongoDBContainer("mongo:4.0.10").withInitScript("initEdgeCase!@#%^& *'().js") | |
| MongoDBContainer mongoDB = new MongoDBContainer("mongo:4.0.10").withInitScript("initEdgeCase!@#%^& '().js") |
Fixes #3066
Description
This PR adds support for initialization scripts in
MongoDBContainer.When an initialization script is placed in
/docker-entrypoint-initdb.d/, the official MongoDB image restarts the container process after executing the script. This behavior previously causedWaitStrategytimeouts because the container would stop and start again.Changes
withInitScript(String scriptPath)method toMongoDBContainer./docker-entrypoint-initdb.d/init.js.WaitStrategyto expect two "waiting for connections" log messages, ensuring the container is fully ready after the restart.Verification
MongoDBInitScriptTestto verify that the container starts successfully with an init script.