diff --git a/docs/modules/databases/jdbc.md b/docs/modules/databases/jdbc.md index 9e976d872ba..24f975e7ce3 100644 --- a/docs/modules/databases/jdbc.md +++ b/docs/modules/databases/jdbc.md @@ -84,6 +84,18 @@ public class JDBCDriverTest { ... ``` +### Copying files to the container + +You may copy files into the container by specifying `TC_COPY_FILE`. +This is useful for supplying custom configuration files or utilizing the entrypoint of a Docker image to run scripts. + +The syntax is: `?TC_COPY_FILE=host-path:container-path[:file-mode]` (file mode is optional). +If the host path is prefixed with `file:` it will be loaded from a file (relative to the working directory, which will usually be the project root), otherwise it's mapped from a classpath resource. + +* Single file: `jdbc:tc:mysql:5.7.34:///databasename?TC_COPY_FILE=some-path/init_mysql.sql:/docker-entrypoint-initdb.d/init_mysql.sql` +* Single file with file mode: `jdbc:tc:mysql:5.7.34:///databasename?TC_COPY_FILE=some-path/init_mysql.sql:/docker-entrypoint-initdb.d/init_mysql.sql:755` +* Multiple files: `jdbc:tc:mysql:5.7.34:///databasename?TC_COPY_FILE=some-path/init_mysql.sql:/docker-entrypoint-initdb.d/init_mysql.sql:755&TC_COPY_FILE=file:/absolute-path/another-file:/another/container-path/another-file` + ### Running container in daemon mode By default database container is being stopped as soon as last connection is closed. There are cases when you might need to start container and keep it running till you stop it explicitly or JVM is shutdown. To do this, add `TC_DAEMON` parameter to the URL as follows: diff --git a/modules/jdbc-test/src/main/java/org/testcontainers/jdbc/AbstractJDBCDriverTest.java b/modules/jdbc-test/src/main/java/org/testcontainers/jdbc/AbstractJDBCDriverTest.java index a152a1ae550..99e3668ff1a 100644 --- a/modules/jdbc-test/src/main/java/org/testcontainers/jdbc/AbstractJDBCDriverTest.java +++ b/modules/jdbc-test/src/main/java/org/testcontainers/jdbc/AbstractJDBCDriverTest.java @@ -22,6 +22,7 @@ public class AbstractJDBCDriverTest { protected enum Options { ScriptedSchema, CharacterSet, + CopyFiles, CustomIniFile, JDBCParams, PmdKnownBroken, @@ -63,7 +64,7 @@ public void test() throws SQLException { performTestForCharacterEncodingForInitialScriptConnection(dataSource); } - if (options.contains(Options.CustomIniFile)) { + if (options.contains(Options.CopyFiles) || options.contains(Options.CustomIniFile)) { performTestForCustomIniFile(dataSource); } } @@ -204,13 +205,13 @@ private HikariDataSource verifyCharacterSet(String jdbcUrl) throws SQLException private void performTestForCustomIniFile(HikariDataSource dataSource) throws SQLException { assumeFalse(SystemUtils.IS_OS_WINDOWS); Statement statement = dataSource.getConnection().createStatement(); - statement.execute("SELECT @@GLOBAL.innodb_file_format"); + statement.execute("SELECT @@GLOBAL.key_buffer_size"); ResultSet resultSet = statement.getResultSet(); assertThat(resultSet.next()).as("The query returns a result").isTrue(); String result = resultSet.getString(1); - assertThat(result).as("The InnoDB file format has been set by the ini file content").isEqualTo("Barracuda"); + assertThat(result).as("The InnoDB file format has been set by the ini file content").isEqualTo("32768"); } private HikariDataSource getDataSource(String jdbcUrl, int poolSize) { diff --git a/modules/jdbc/src/main/java/org/testcontainers/jdbc/ConnectionUrl.java b/modules/jdbc/src/main/java/org/testcontainers/jdbc/ConnectionUrl.java index b4aef740da0..7f369c2b186 100644 --- a/modules/jdbc/src/main/java/org/testcontainers/jdbc/ConnectionUrl.java +++ b/modules/jdbc/src/main/java/org/testcontainers/jdbc/ConnectionUrl.java @@ -4,9 +4,12 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import org.testcontainers.UnstableAPI; +import org.testcontainers.utility.MountableFile; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -61,6 +64,8 @@ public class ConnectionUrl { private Map tmpfsOptions = new HashMap<>(); + private Map> copyFilesToContainerOptions = Collections.emptyMap(); + public static ConnectionUrl newInstance(final String url) { ConnectionUrl connectionUrl = new ConnectionUrl(url); connectionUrl.parseUrl(); @@ -135,6 +140,8 @@ private void parseUrl() { tmpfsOptions = parseTmpfsOptions(containerParameters); + copyFilesToContainerOptions = parseCopyFilesToContainerOptions(this.getUrl()); + initScriptPath = Optional.ofNullable(containerParameters.get("TC_INITSCRIPT")); reusable = Boolean.parseBoolean(containerParameters.get("TC_REUSABLE")); @@ -160,6 +167,26 @@ private Map parseTmpfsOptions(Map containerParam .collect(Collectors.toMap(string -> string.split(":")[0], string -> string.split(":")[1])); } + private Map> parseCopyFilesToContainerOptions(CharSequence url) { + Matcher matcher = Patterns.COPY_FILE_MATCHING_PATTERN.matcher(this.getUrl()); + + Map> mounts = new HashMap<>(); + while (matcher.find()) { + boolean isFile = matcher.group(1).startsWith("file:"); + String[] split = matcher.group(1).replaceFirst("file:", "").split(":"); + String hostPath = split[0]; + String containerPath = split[1]; + Integer mode = split.length > 2 ? Integer.parseInt(split[2], 8) : null; + MountableFile mountableFile = isFile + ? MountableFile.forHostPath(hostPath, mode) + : MountableFile.forClasspathResource(hostPath, mode); + mounts.putIfAbsent(containerPath, new ArrayList<>()); + mounts.get(containerPath).add(mountableFile); + } + + return mounts; + } + /** * Get the TestContainers Parameters such as Init Function, Init Script path etc. * @@ -201,6 +228,10 @@ public Map getTmpfsOptions() { return Collections.unmodifiableMap(tmpfsOptions); } + public Map> getCopyFilesToContainerOptions() { + return Collections.unmodifiableMap(copyFilesToContainerOptions); + } + /** * This interface defines the Regex Patterns used by {@link ConnectionUrl}. * @@ -257,6 +288,8 @@ public interface Patterns { ".*" ); + Pattern COPY_FILE_MATCHING_PATTERN = Pattern.compile("TC_COPY_FILE=([^?&]+)"); + String TC_PARAM_NAME_PATTERN = "(TC_[A-Z_]+)"; Pattern TC_PARAM_MATCHING_PATTERN = Pattern.compile(TC_PARAM_NAME_PATTERN + "=([^?&]+)"); diff --git a/modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerDatabaseDriver.java b/modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerDatabaseDriver.java index 8f5f5f5a425..76c4ee8b870 100644 --- a/modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerDatabaseDriver.java +++ b/modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerDatabaseDriver.java @@ -108,6 +108,15 @@ public synchronized Connection connect(String url, final Properties info) throws if (candidateContainerType.supports(connectionUrl.getDatabaseType())) { container = candidateContainerType.newInstance(connectionUrl); container.withTmpFs(connectionUrl.getTmpfsOptions()); + + JdbcDatabaseContainer finalContainer = container; + connectionUrl + .getCopyFilesToContainerOptions() + .forEach((containerPath, mountableFiles) -> { + mountableFiles.forEach(mountableFile -> { + finalContainer.withCopyFileToContainer(mountableFile, containerPath); + }); + }); delegate = container.getJdbcDriverInstance(); } } diff --git a/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlTest.java b/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlTest.java index fa50dbbe76e..27ac53e7cb3 100644 --- a/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlTest.java +++ b/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlTest.java @@ -3,6 +3,9 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.testcontainers.utility.MountableFile; + +import java.nio.file.Paths; import static org.assertj.core.api.Assertions.assertThat; @@ -68,6 +71,82 @@ public void testTmpfsOption() { assertThat(url.getTmpfsOptions()).as("tmpfs option key1 has correct value").containsEntry("key1", "value1"); } + @Test + public void testCopyFilesOption() { + String urlString = + "jdbc:tc:mysql://somehostname/databasename" + + "?TC_COPY_FILE=file:key1:path1" + + "?TC_COPY_FILE=file:/key2:path2" + + "&TC_COPY_FILE=logback-test.xml:path3:755"; + ConnectionUrl url = ConnectionUrl.newInstance(urlString); + + assertThat(url.getQueryParameters()).as("Connection Parameters set is empty").isEmpty(); + assertThat(url.getContainerParameters()).as("Container Parameters set is not empty").isNotEmpty(); + assertThat(url.getCopyFilesToContainerOptions()) + .as("copyFiles option has correct values") + .containsOnlyKeys("path1", "path2", "path3"); + + assertThat(url.getCopyFilesToContainerOptions()) + .as("copyFiles option path1 has correct value") + .extractingByKey("path1") + .asList() + .hasSize(1); + assertThat(url.getCopyFilesToContainerOptions()) + .as("copyFiles option path1 has correct value") + .extractingByKey("path1") + .asList() + .element(0) + .extracting("filesystemPath") + .isEqualTo(Paths.get(".").toAbsolutePath().normalize() + "/key1"); + assertThat(url.getCopyFilesToContainerOptions()) + .as("copyFiles option path1 has correct value") + .extractingByKey("path1") + .asList() + .element(0) + .extracting("fileMode") + .isEqualTo(0100000 | 0644); + + assertThat(url.getCopyFilesToContainerOptions()) + .as("copyFiles option path1 has correct value") + .extractingByKey("path2") + .asList() + .hasSize(1); + assertThat(url.getCopyFilesToContainerOptions()) + .as("copyFiles option path1 has correct value") + .extractingByKey("path2") + .asList() + .element(0) + .extracting("filesystemPath") + .isEqualTo("/key2"); + assertThat(url.getCopyFilesToContainerOptions()) + .as("copyFiles option path1 has correct value") + .extractingByKey("path2") + .asList() + .element(0) + .extracting("fileMode") + .isEqualTo(0100000 | 0644); + + assertThat(url.getCopyFilesToContainerOptions()) + .as("copyFiles option path1 has correct value") + .extractingByKey("path3") + .asList() + .hasSize(1); + assertThat(url.getCopyFilesToContainerOptions()) + .as("copyFiles option path1 has correct value") + .extractingByKey("path3") + .asList() + .element(0) + .extracting("filesystemPath") + .isEqualTo(MountableFile.forClasspathResource("logback-test.xml").getFilesystemPath()); + assertThat(url.getCopyFilesToContainerOptions()) + .as("copyFiles option path1 has correct value") + .extractingByKey("path3") + .asList() + .element(0) + .extracting("fileMode") + .isEqualTo(0100000 | 0755); + } + @Test public void testInitScriptPathCapture() { String urlString = diff --git a/modules/mariadb/src/test/resources/somepath/mariadb_conf_override/my.cnf b/modules/mariadb/src/test/resources/somepath/mariadb_conf_override/my.cnf index dcba17aff5c..d4841edd5ba 100644 --- a/modules/mariadb/src/test/resources/somepath/mariadb_conf_override/my.cnf +++ b/modules/mariadb/src/test/resources/somepath/mariadb_conf_override/my.cnf @@ -2,7 +2,7 @@ port = 3306 #socket = /tmp/mysql.sock skip-external-locking -key_buffer_size = 16K +key_buffer_size = 32K max_allowed_packet = 1M table_open_cache = 4 sort_buffer_size = 64K @@ -52,4 +52,4 @@ innodb_buffer_pool_size = 16M innodb_log_file_size = 5M innodb_log_buffer_size = 8M innodb_flush_log_at_trx_commit = 1 -innodb_lock_wait_timeout = 50 \ No newline at end of file +innodb_lock_wait_timeout = 50 diff --git a/modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/MySQLJDBCDriverTest.java b/modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/MySQLJDBCDriverTest.java index 752fbca8e07..90e7f118ec0 100644 --- a/modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/MySQLJDBCDriverTest.java +++ b/modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/MySQLJDBCDriverTest.java @@ -45,6 +45,10 @@ public static Iterable data() { "jdbc:tc:mysql:5.6.51://hostname/databasename?TC_MY_CNF=somepath/mysql_conf_override", EnumSet.of(Options.CustomIniFile), }, + { + "jdbc:tc:mysql://hostname/databasename?TC_COPY_FILE=somepath/mysql_conf_override/my.cnf:/etc/mysql/conf.d/mysqld.cnf", + EnumSet.of(Options.CopyFiles), + }, } ); } diff --git a/modules/mysql/src/test/resources/somepath/mysql_conf_override/my.cnf b/modules/mysql/src/test/resources/somepath/mysql_conf_override/my.cnf index 78012cc4bfe..b2e5fd77285 100644 --- a/modules/mysql/src/test/resources/somepath/mysql_conf_override/my.cnf +++ b/modules/mysql/src/test/resources/somepath/mysql_conf_override/my.cnf @@ -3,7 +3,7 @@ user = mysql port = 3306 #socket = /tmp/mysql.sock skip-external-locking -key_buffer_size = 16K +key_buffer_size = 32K max_allowed_packet = 1M table_open_cache = 4 sort_buffer_size = 64K @@ -51,4 +51,4 @@ innodb_buffer_pool_size = 16M innodb_log_file_size = 5M innodb_log_buffer_size = 8M innodb_flush_log_at_trx_commit = 1 -innodb_lock_wait_timeout = 50 \ No newline at end of file +innodb_lock_wait_timeout = 50