-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Add support for initialization scripts #11331
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
53b3869
e2adfb1
e81a2ac
35b753b
488b5fa
b7ca531
d868b95
4c3c477
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,10 +6,16 @@ | |||||||||||||||||||
| import lombok.extern.slf4j.Slf4j; | ||||||||||||||||||||
| import org.testcontainers.containers.GenericContainer; | ||||||||||||||||||||
| import org.testcontainers.containers.wait.strategy.Wait; | ||||||||||||||||||||
| import org.testcontainers.images.builder.Transferable; | ||||||||||||||||||||
| import org.testcontainers.utility.DockerImageName; | ||||||||||||||||||||
| import org.testcontainers.utility.MountableFile; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import java.io.IOException; | ||||||||||||||||||||
| import java.net.URL; | ||||||||||||||||||||
| import java.nio.file.Files; | ||||||||||||||||||||
| import java.nio.file.Path; | ||||||||||||||||||||
| import java.nio.file.Paths; | ||||||||||||||||||||
| import java.util.Enumeration; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Testcontainers implementation for MongoDB. | ||||||||||||||||||||
|
|
@@ -41,10 +47,16 @@ public class MongoDBContainer extends GenericContainer<MongoDBContainer> { | |||||||||||||||||||
|
|
||||||||||||||||||||
| private static final String STARTER_SCRIPT = "/testcontainers_start.sh"; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private static final String SCRIPT_DESTINATION_DEFAULT = "/docker-entrypoint-initdb.d/init.js"; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private static final String SCRIPT_DESTINATION_MANUAL = "/tmp/init.js"; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private boolean shardingEnabled; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private boolean rsEnabled; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private String initScriptPath; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| public MongoDBContainer(@NonNull String dockerImageName) { | ||||||||||||||||||||
| this(DockerImageName.parse(dockerImageName)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
@@ -68,6 +80,69 @@ protected void containerIsStarted(InspectContainerResponse containerInfo, boolea | |||||||||||||||||||
| if (this.rsEnabled) { | ||||||||||||||||||||
| initReplicaSet(reused); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| boolean isClusterMode = this.shardingEnabled || this.rsEnabled; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (isClusterMode && this.initScriptPath != null) { | ||||||||||||||||||||
| executeInitScriptInContainer(); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Configures the container. | ||||||||||||||||||||
| * <p> | ||||||||||||||||||||
| * This method handles the transfer of the initialization script to the container. | ||||||||||||||||||||
| * Unlike standard file copying mechanisms, this implementation explicitly reads the script content as bytes | ||||||||||||||||||||
| * and uses {@link org.testcontainers.images.builder.Transferable} to copy it. | ||||||||||||||||||||
| * <p> | ||||||||||||||||||||
| * This approach is necessary to strictly support filenames containing special characters | ||||||||||||||||||||
| * (e.g., "#", spaces, etc.) on the classpath. Standard resource loading methods may misinterpret | ||||||||||||||||||||
| * these characters (e.g., treating "#" as a URL fragment), causing resolution failures. | ||||||||||||||||||||
| * By manually resolving the file path and transferring the raw bytes, we ensure the script | ||||||||||||||||||||
| * is correctly deployed regardless of its filename complexity. | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| @Override | ||||||||||||||||||||
| protected void configure() { | ||||||||||||||||||||
| super.configure(); | ||||||||||||||||||||
| boolean isClusterMode = this.shardingEnabled || this.rsEnabled; | ||||||||||||||||||||
| if (this.initScriptPath != null) { | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| Path scriptPath = Paths.get(this.initScriptPath); | ||||||||||||||||||||
| String fileName = scriptPath.getFileName().toString(); | ||||||||||||||||||||
| Path parentDir = scriptPath.getParent(); | ||||||||||||||||||||
| String resourceDir = (parentDir == null) ? "" : parentDir.toString(); | ||||||||||||||||||||
|
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(); | |
| // 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
AI
Mar 12, 2026
There was a problem hiding this comment.
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
AI
Mar 12, 2026
There was a problem hiding this comment.
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.
| withCopyToContainer(Transferable.of(fileContent, 0777), destination); | |
| withCopyToContainer(Transferable.of(fileContent, 0644), destination); |
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
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.
| if (this.initScriptPath != null && !isClusterMode) { | |
| if (this.initScriptPath != null | |
| && !isClusterMode | |
| && (this.waitStrategy == null || this.waitStrategy == GenericContainer.DEFAULT_WAIT_STRATEGY)) { |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -38,4 +38,72 @@ void shouldTestDatabaseName() { | |||||
| assertThat(mongoDBContainer.getReplicaSetUrl(databaseName)).endsWith(databaseName); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| @Test | ||||||
| void shouldExecuteInitScript() { | ||||||
| try (MongoDBContainer mongoDB = new MongoDBContainer("mongo:4.0.10").withInitScript("init.js")) { | ||||||
| mongoDB.start(); | ||||||
| assertInitScriptExecuted(mongoDB); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| @Test | ||||||
| void shouldExecuteInitScriptWithEdgeCases() { | ||||||
| try ( | ||||||
| MongoDBContainer mongoDB = new MongoDBContainer("mongo:4.0.10").withInitScript("initEdgeCase!@#%^& *'().js") | ||||||
|
||||||
| MongoDBContainer mongoDB = new MongoDBContainer("mongo:4.0.10").withInitScript("initEdgeCase!@#%^& *'().js") | |
| MongoDBContainer mongoDB = new MongoDBContainer("mongo:4.0.10").withInitScript("initEdgeCase!@#%^& '().js") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| db.createCollection("test_collection"); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| var complexCollectionName = 'test_col_"_with_specials_!@#%^&*()'; | ||
|
|
||
| db.createCollection(complexCollectionName); | ||
|
|
||
| var collectionWithSpecialChars = "col with spaces & symbols !@#"; | ||
|
|
||
| db.createCollection(collectionWithSpecialChars); | ||
|
|
||
| db.getCollection(complexCollectionName).insertOne({ | ||
| "_id": 1, | ||
| "key_with_quotes": "This is a \"double quoted\" string", | ||
| "key_with_json_chars": "{ } [ ] : ,", | ||
| "description": "Insertion test for collection with special symbols" | ||
| }); | ||
|
|
||
| print("Initialization completed: " + complexCollectionName); |
There was a problem hiding this comment.
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 whenreused == 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.