diff --git a/modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBContainer.java b/modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBContainer.java index c61be6675ba..d8b97ac3ba0 100644 --- a/modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBContainer.java +++ b/modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBContainer.java @@ -6,6 +6,8 @@ import lombok.extern.slf4j.Slf4j; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.wait.strategy.WaitStrategy; +import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; @@ -41,10 +43,16 @@ public class MongoDBContainer extends GenericContainer { private static final String STARTER_SCRIPT = "/testcontainers_start.sh"; + private static final String INIT_SCRIPT_DIR = "/docker-entrypoint-initdb.d"; + private boolean shardingEnabled; private boolean rsEnabled; + private boolean hasInitScript; + + private boolean customWaitStrategy; + public MongoDBContainer(@NonNull String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } @@ -54,12 +62,71 @@ public MongoDBContainer(DockerImageName dockerImageName) { dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, COMMUNITY_SERVER_IMAGE, ENTERPRISE_SERVER_IMAGE); withExposedPorts(MONGODB_INTERNAL_PORT); + super.waitingFor(Wait.forLogMessage("(?i).*waiting for connections.*", 1)); + } + + @Override + public MongoDBContainer waitingFor(@NonNull WaitStrategy waitStrategy) { + this.customWaitStrategy = true; + return super.waitingFor(waitStrategy); + } + + @Override + public void setWaitStrategy(WaitStrategy waitStrategy) { + this.customWaitStrategy = true; + super.setWaitStrategy(waitStrategy); + } + + @Override + public MongoDBContainer withCopyFileToContainer(MountableFile mountableFile, String containerPath) { + checkInitScript(containerPath); + return super.withCopyFileToContainer(mountableFile, containerPath); + } + + @Override + public MongoDBContainer withCopyToContainer(Transferable transferable, String containerPath) { + checkInitScript(containerPath); + return super.withCopyToContainer(transferable, containerPath); + } + + private static boolean isInitScriptPath(String path) { + if (path == null) { + return false; + } + if (path.equals(INIT_SCRIPT_DIR) || path.equals(INIT_SCRIPT_DIR + "/")) { + return true; + } + if (path.startsWith(INIT_SCRIPT_DIR + "/")) { + String subPath = path.substring(INIT_SCRIPT_DIR.length() + 1); + return ( + !subPath.contains("/") && + !subPath.contains("\\") && + !subPath.startsWith(".") && + (subPath.endsWith(".js") || subPath.endsWith(".sh")) + ); + } + return false; + } + + private void checkInitScript(String containerPath) { + if (isInitScriptPath(containerPath)) { + this.hasInitScript = true; + } + } + + private boolean hasInitScript() { + return ( + this.hasInitScript || + getBinds().stream().anyMatch(b -> b.getVolume() != null && isInitScriptPath(b.getVolume().getPath())) + ); } @Override protected void containerIsStarting(InspectContainerResponse containerInfo) { if (this.shardingEnabled) { copyFileToContainer(MountableFile.forClasspathResource("/sharding.sh", 0777), STARTER_SCRIPT); + } else if (!this.customWaitStrategy && hasInitScript()) { + super.setWaitStrategy(Wait.forLogMessage("(?i).*waiting for connections.*", 2)); } } @@ -157,7 +224,6 @@ private boolean isReplicationSetAlreadyInitialized() { public MongoDBContainer withReplicaSet() { this.rsEnabled = true; withCommand("--replSet", "docker-rs"); - waitingFor(Wait.forLogMessage("(?i).*waiting for connections.*", 1)); return this; } diff --git a/modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java b/modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java index 816243d769e..287bbd86fbb 100644 --- a/modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java +++ b/modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java @@ -1,6 +1,14 @@ package org.testcontainers.mongodb; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import org.bson.Document; import org.junit.jupiter.api.Test; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.wait.strategy.WaitStrategy; +import org.testcontainers.utility.MountableFile; + +import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; @@ -38,4 +46,176 @@ void shouldTestDatabaseName() { assertThat(mongoDBContainer.getReplicaSetUrl(databaseName)).endsWith(databaseName); } } + + @Test + void shouldRunInitScript() { + try ( + MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.10") + .withCopyFileToContainer( + MountableFile.forClasspathResource("mongo-init.js"), + "/docker-entrypoint-initdb.d/mongo-init.js" + ) + ) { + mongoDBContainer.start(); + + try (MongoClient mongoClient = MongoClients.create(mongoDBContainer.getConnectionString())) { + final Document document = mongoClient + .getDatabase("init-script-db") + .getCollection("messages") + .find() + .first(); + + assertThat(document).containsEntry("message", "init script ran"); + } + } + } + + @Test + @SuppressWarnings("OctalInteger") + void shouldRunShInitScript() { + try ( + MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.10") + .withCopyFileToContainer( + MountableFile.forClasspathResource("mongo-init.sh", 0777), + "/docker-entrypoint-initdb.d/mongo-init.sh" + ) + ) { + mongoDBContainer.start(); + + try (MongoClient mongoClient = MongoClients.create(mongoDBContainer.getConnectionString())) { + final Document document = mongoClient + .getDatabase("init-script-sh-db") + .getCollection("messages") + .find() + .first(); + + assertThat(document).containsEntry("message", "init sh script ran"); + } + } + } + + @Test + void shouldNotTriggerTwoOccurrenceWaitForUnrelatedFile() { + try (TestableMongoDBContainer mongoDBContainer = new TestableMongoDBContainer("mongo:4.0.10")) { + WaitStrategy defaultWaitStrategy = mongoDBContainer.waitStrategy(); + mongoDBContainer.withCopyFileToContainer( + MountableFile.forClasspathResource("mongo-init.js"), + "/docker-entrypoint-initdb.d/notes.txt" + ); + mongoDBContainer.triggerContainerIsStarting(); + assertThat(mongoDBContainer.waitStrategy()).isSameAs(defaultWaitStrategy); + } + } + + @Test + void shouldNotTriggerTwoOccurrenceWaitForNestedPath() { + try (TestableMongoDBContainer mongoDBContainer = new TestableMongoDBContainer("mongo:4.0.10")) { + WaitStrategy defaultWaitStrategy = mongoDBContainer.waitStrategy(); + mongoDBContainer.withCopyFileToContainer( + MountableFile.forClasspathResource("mongo-init.js"), + "/docker-entrypoint-initdb.d/nested/mongo-init.js" + ); + mongoDBContainer.triggerContainerIsStarting(); + assertThat(mongoDBContainer.waitStrategy()).isSameAs(defaultWaitStrategy); + } + } + + @Test + void shouldPreserveCustomWaitStrategyViaWaitingFor() { + WaitStrategy customWaitStrategy = Wait + .forLogMessage("(?i).*waiting for connections.*", 2) + .withStartupTimeout(Duration.ofMinutes(3)); + try (TestableMongoDBContainer mongoDBContainer = new TestableMongoDBContainer("mongo:4.0.10")) { + mongoDBContainer + .withCopyFileToContainer( + MountableFile.forClasspathResource("mongo-init.js"), + "/docker-entrypoint-initdb.d/mongo-init.js" + ) + .waitingFor(customWaitStrategy); + + mongoDBContainer.start(); + assertThat(mongoDBContainer.waitStrategy()).isSameAs(customWaitStrategy); + } + } + + @Test + void shouldPreserveCustomWaitStrategyViaSetWaitStrategy() { + WaitStrategy customWaitStrategy = Wait + .forLogMessage("(?i).*waiting for connections.*", 2) + .withStartupTimeout(Duration.ofMinutes(3)); + try (TestableMongoDBContainer mongoDBContainer = new TestableMongoDBContainer("mongo:4.0.10")) { + mongoDBContainer.withCopyFileToContainer( + MountableFile.forClasspathResource("mongo-init.js"), + "/docker-entrypoint-initdb.d/mongo-init.js" + ); + mongoDBContainer.setWaitStrategy(customWaitStrategy); + + mongoDBContainer.start(); + assertThat(mongoDBContainer.waitStrategy()).isSameAs(customWaitStrategy); + } + } + + @Test + void shouldTriggerTwoOccurrenceWaitForDirectoryMount() { + try (TestableMongoDBContainer mongoDBContainer = new TestableMongoDBContainer("mongo:4.0.10")) { + WaitStrategy defaultWaitStrategy = mongoDBContainer.waitStrategy(); + mongoDBContainer.withFileSystemBind("/dummy/host/dir", "/docker-entrypoint-initdb.d"); + mongoDBContainer.triggerContainerIsStarting(); + assertThat(mongoDBContainer.waitStrategy()).isNotSameAs(defaultWaitStrategy); + } + } + + @Test + void shouldTriggerTwoOccurrenceWaitOnlyForValidInitScriptPaths() { + assertThat(triggersTwoOccurrenceWaitForBind("/docker-entrypoint-initdb.d")).isTrue(); + assertThat(triggersTwoOccurrenceWaitForBind("/docker-entrypoint-initdb.d/")).isTrue(); + assertThat(triggersTwoOccurrenceWaitForBind("/docker-entrypoint-initdb.d/init.js")).isTrue(); + assertThat(triggersTwoOccurrenceWaitForBind("/docker-entrypoint-initdb.d/init.sh")).isTrue(); + assertThat(triggersTwoOccurrenceWaitForBind("/docker-entrypoint-initdb.d/nested/init.js")).isFalse(); + assertThat(triggersTwoOccurrenceWaitForBind("/docker-entrypoint-initdb.d/notes.txt")).isFalse(); + + assertThat(triggersTwoOccurrenceWaitForCopy("/docker-entrypoint-initdb.d")).isTrue(); + assertThat(triggersTwoOccurrenceWaitForCopy("/docker-entrypoint-initdb.d/")).isTrue(); + assertThat(triggersTwoOccurrenceWaitForCopy("/docker-entrypoint-initdb.d/mongo-init.js")).isTrue(); + assertThat(triggersTwoOccurrenceWaitForCopy("/docker-entrypoint-initdb.d/mongo-init.sh")).isTrue(); + assertThat(triggersTwoOccurrenceWaitForCopy("/docker-entrypoint-initdb.d/nested/mongo-init.js")).isFalse(); + assertThat(triggersTwoOccurrenceWaitForCopy("/docker-entrypoint-initdb.d/notes.txt")).isFalse(); + assertThat(triggersTwoOccurrenceWaitForCopy("/docker-entrypoint-initdb.d/data.json")).isFalse(); + assertThat(triggersTwoOccurrenceWaitForCopy("/docker-entrypoint-initdb.d/.gitkeep")).isFalse(); + assertThat(triggersTwoOccurrenceWaitForCopy("/docker-entrypoint-initdb.d/.js")).isFalse(); + assertThat(triggersTwoOccurrenceWaitForCopy("/other-dir/mongo-init.js")).isFalse(); + } + + private boolean triggersTwoOccurrenceWaitForBind(String containerPath) { + try (TestableMongoDBContainer container = new TestableMongoDBContainer("mongo:4.0.10")) { + WaitStrategy defaultWaitStrategy = container.waitStrategy(); + container.withFileSystemBind("/dummy", containerPath); + container.triggerContainerIsStarting(); + return container.waitStrategy() != defaultWaitStrategy; + } + } + + private boolean triggersTwoOccurrenceWaitForCopy(String containerPath) { + try (TestableMongoDBContainer container = new TestableMongoDBContainer("mongo:4.0.10")) { + WaitStrategy defaultWaitStrategy = container.waitStrategy(); + container.withCopyFileToContainer(MountableFile.forClasspathResource("mongo-init.js"), containerPath); + container.triggerContainerIsStarting(); + return container.waitStrategy() != defaultWaitStrategy; + } + } + + private static class TestableMongoDBContainer extends MongoDBContainer { + + TestableMongoDBContainer(String dockerImageName) { + super(dockerImageName); + } + + WaitStrategy waitStrategy() { + return getWaitStrategy(); + } + + void triggerContainerIsStarting() { + containerIsStarting(null); + } + } } diff --git a/modules/mongodb/src/test/resources/mongo-init.js b/modules/mongodb/src/test/resources/mongo-init.js new file mode 100644 index 00000000000..e4e6fbed2f2 --- /dev/null +++ b/modules/mongodb/src/test/resources/mongo-init.js @@ -0,0 +1,2 @@ +db = db.getSiblingDB("init-script-db"); +db.messages.insertOne({ message: "init script ran" }); diff --git a/modules/mongodb/src/test/resources/mongo-init.sh b/modules/mongodb/src/test/resources/mongo-init.sh new file mode 100644 index 00000000000..71b407b41ec --- /dev/null +++ b/modules/mongodb/src/test/resources/mongo-init.sh @@ -0,0 +1 @@ +mongo init-script-sh-db --eval 'db.messages.insertOne({ message: "init sh script ran" })'