Skip to content

Add support for initialization scripts#11331

Open
taole33 wants to merge 8 commits intotestcontainers:mainfrom
taole33:feature/issue-3066-mongodb-init-script
Open

Add support for initialization scripts#11331
taole33 wants to merge 8 commits intotestcontainers:mainfrom
taole33:feature/issue-3066-mongodb-init-script

Conversation

@taole33
Copy link

@taole33 taole33 commented Dec 6, 2025

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 caused WaitStrategy timeouts because the container would stop and start again.

Changes

  • Added withInitScript(String scriptPath) method to MongoDBContainer.
  • The method copies the script to /docker-entrypoint-initdb.d/init.js.
  • It automatically configures the WaitStrategy to expect two "waiting for connections" log messages, ensuring the container is fully ready after the restart.

Verification

  • Added MongoDBInitScriptTest to verify that the container starts successfully with an init script.

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
@taole33 taole33 requested a review from a team as a code owner December 6, 2025 01:22
@eddumelendez eddumelendez changed the title feat(module/mongodb): Add support for initialization scripts Add support for initialization scripts Dec 9, 2025
* @param scriptPath the path to the init script file on the classpath
* @return this container instance
*/
public MongoDBContainer withInitScript(String scriptPath) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

org.testcontainers.containers.MongoDBContainer is deprecated. can you moved to org.testcontainers.mongodb.MongoDBContainer?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@taole33 taole33 force-pushed the feature/issue-3066-mongodb-init-script branch 2 times, most recently from 6665ad7 to 67088da Compare December 9, 2025 23:28
@taole33 taole33 force-pushed the feature/issue-3066-mongodb-init-script branch from 67088da to e81a2ac Compare December 9, 2025 23:32
@eddumelendez
Copy link
Member

does this work with ReplicaSet and Sharded mode? New MongoDBContainer implementation has withReplicaSet() and withSharding() methods. It would be nice we can make sure this work with all implementations

@taole33
Copy link
Author

taole33 commented Dec 11, 2025

I updated withInitScript to support ReplicaSet and Sharding modes.

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.

Reason

In Cluster modes (ReplicaSet/Sharding), relying on the default /docker-entrypoint-initdb.d/ mechanism causes issues:

  • ReplicaSet: The default entrypoint triggers a restart, causing a race condition with rs.initiate().
  • Sharding: The entrypoint is overridden, so the script is ignored.

Changes

  • Added logic to detect Cluster mode.
  • If Cluster mode is enabled, the script is mounted to a temporary location (isolating it from the default entrypoint) and executed manually after the cluster is fully initialized.
  • This ensures data consistency and avoids startup conflicts.

Fixes #3066

Comment on lines +61 to +63
.withEnv("LANG", "C.UTF-8")
.withEnv("LC_ALL", "C.UTF-8")
.withStartupTimeout(Duration.ofSeconds(30))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this required?

try (
MongoDBContainer mongoDB = new MongoDBContainer("mongo:4.0.10")
.withInitScript("init.js")
.withStartupTimeout(Duration.ofSeconds(30))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this required?

.isEqualTo("{ } [ ] : ,");

assertThat(doc.getString("description"))
.as("Japanese text should be preserved correctly")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove as


assertThat(doc.getString("description"))
.as("Japanese text should be preserved correctly")
.isEqualTo("特殊記号を含むコレクションへの挿入テスト");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use plain english, please?

Comment on lines +126 to +132
@Test
void shouldExecuteInitScriptWithShardingConfiguredFirst() {
try (MongoDBContainer mongo = new MongoDBContainer("mongo:7.0.0").withSharding().withInitScript("init.js")) {
mongo.start();
assertInitScriptExecuted(mongo);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remove it. There is no difference with the previous test

Comment on lines +110 to +116
@Test
void shouldExecuteInitScriptWithReplicaSetConfiguredFirst() {
try (MongoDBContainer mongo = new MongoDBContainer("mongo:7.0.0").withReplicaSet().withInitScript("init.js")) {
mongo.start();
assertInitScriptExecuted(mongo);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remove it. There is no difference with the previous test

@taole33
Copy link
Author

taole33 commented Dec 16, 2025

@eddumelendez
Thank you for the review. I have updated the implementation to address your feedback.

In this update, I also addressed an edge case regarding filenames containing special characters (e.g., # or spaces) which wasn't handled in the previous version.
Since standard resource loading can sometimes misinterpret these characters as URL fragments, I implemented the file transfer logic to explicitly handle raw bytes. This ensures robustness for any filename.

I believe this covers all cases. Let me know if you have any further questions.

@taole33 taole33 requested a review from eddumelendez December 16, 2025 02:29
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +110 to +113
Path scriptPath = Paths.get(this.initScriptPath);
String fileName = scriptPath.getFileName().toString();
Path parentDir = scriptPath.getParent();
String resourceDir = (parentDir == null) ? "" : parentDir.toString();
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +115 to +129
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;
}
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
}

String destination = isClusterMode ? SCRIPT_DESTINATION_MANUAL : SCRIPT_DESTINATION_DEFAULT;
withCopyToContainer(Transferable.of(fileContent, 0777), destination);
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
withCopyToContainer(Transferable.of(fileContent, 0777), destination);
withCopyToContainer(Transferable.of(fileContent, 0644), destination);

Copilot uses AI. Check for mistakes.
}
}

if (this.initScriptPath != null && !isClusterMode) {
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if (this.initScriptPath != null && !isClusterMode) {
if (this.initScriptPath != null
&& !isClusterMode
&& (this.waitStrategy == null || this.waitStrategy == GenericContainer.DEFAULT_WAIT_STRATEGY)) {

Copilot uses AI. Check for mistakes.

boolean isClusterMode = this.shardingEnabled || this.rsEnabled;

if (isClusterMode && this.initScriptPath != null) {
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if (isClusterMode && this.initScriptPath != null) {
if (isClusterMode && !reused && this.initScriptPath != null) {

Copilot uses AI. Check for mistakes.
@Test
void shouldExecuteInitScriptWithEdgeCases() {
try (
MongoDBContainer mongoDB = new MongoDBContainer("mongo:4.0.10").withInitScript("initEdgeCase!@#%^& *'().js")
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
MongoDBContainer mongoDB = new MongoDBContainer("mongo:4.0.10").withInitScript("initEdgeCase!@#%^& *'().js")
MongoDBContainer mongoDB = new MongoDBContainer("mongo:4.0.10").withInitScript("initEdgeCase!@#%^& '().js")

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support init scripts for MongoDBContainer without manually customizing the WaitStrategy

3 participants