Skip to content

Commit d18c8ea

Browse files
committed
Add JDBC URL option TC_COPY_FILES, copying files to container
1 parent dc0f819 commit d18c8ea

File tree

7 files changed

+125
-5
lines changed

7 files changed

+125
-5
lines changed

docs/modules/databases/jdbc.md

+12
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,18 @@ public class JDBCDriverTest {
8484
...
8585
```
8686

87+
### Copying files to the container
88+
89+
You may copy files into the container by specifying `TC_COPY_FILES`.
90+
This is useful for supplying custom configuration files or utilizing the entrypoint of a Docker image to run scripts.
91+
92+
The syntax is: `?TC_COPY_FILES=host-path:container-path[:file-mode]` (file mode is optional).
93+
If the host path starts with `/` it will be considered an absolute path, otherwise it's mapped from a classpath resource.
94+
95+
* Single file: `jdbc:tc:mysql:5.7.34:///databasename?TC_COPY_FILES=some-path/init_mysql.sql:/docker-entrypoint-initdb.d/init_mysql.sql`
96+
* Single file with file mode: `jdbc:tc:mysql:5.7.34:///databasename?TC_COPY_FILES=some-path/init_mysql.sql:/docker-entrypoint-initdb.d/init_mysql.sql:755`
97+
* Multiple files: `jdbc:tc:mysql:5.7.34:///databasename?TC_COPY_FILES=some-path/init_mysql.sql:/docker-entrypoint-initdb.d/init_mysql.sql:755,/absolute-path/another-file:/another/container-path/another-file`
98+
8799
### Running container in daemon mode
88100
89101
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:

modules/jdbc-test/src/main/java/org/testcontainers/jdbc/AbstractJDBCDriverTest.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public class AbstractJDBCDriverTest {
2222
protected enum Options {
2323
ScriptedSchema,
2424
CharacterSet,
25+
CopyFiles,
2526
CustomIniFile,
2627
JDBCParams,
2728
PmdKnownBroken,
@@ -63,7 +64,7 @@ public void test() throws SQLException {
6364
performTestForCharacterEncodingForInitialScriptConnection(dataSource);
6465
}
6566

66-
if (options.contains(Options.CustomIniFile)) {
67+
if (options.contains(Options.CopyFiles) || options.contains(Options.CustomIniFile)) {
6768
performTestForCustomIniFile(dataSource);
6869
}
6970
}
@@ -204,13 +205,13 @@ private HikariDataSource verifyCharacterSet(String jdbcUrl) throws SQLException
204205
private void performTestForCustomIniFile(HikariDataSource dataSource) throws SQLException {
205206
assumeFalse(SystemUtils.IS_OS_WINDOWS);
206207
Statement statement = dataSource.getConnection().createStatement();
207-
statement.execute("SELECT @@GLOBAL.innodb_file_format");
208+
statement.execute("SELECT @@GLOBAL.key_buffer_size");
208209
ResultSet resultSet = statement.getResultSet();
209210

210211
assertThat(resultSet.next()).as("The query returns a result").isTrue();
211212
String result = resultSet.getString(1);
212213

213-
assertThat(result).as("The InnoDB file format has been set by the ini file content").isEqualTo("Barracuda");
214+
assertThat(result).as("The InnoDB file format has been set by the ini file content").isEqualTo("32768");
214215
}
215216

216217
private HikariDataSource getDataSource(String jdbcUrl, int poolSize) {

modules/jdbc/src/main/java/org/testcontainers/jdbc/ConnectionUrl.java

+37
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
import lombok.EqualsAndHashCode;
55
import lombok.Getter;
66
import org.testcontainers.UnstableAPI;
7+
import org.testcontainers.utility.MountableFile;
78

9+
import java.util.ArrayList;
10+
import java.util.Arrays;
811
import java.util.Collections;
912
import java.util.HashMap;
13+
import java.util.List;
1014
import java.util.Map;
1115
import java.util.Objects;
1216
import java.util.Optional;
@@ -61,6 +65,8 @@ public class ConnectionUrl {
6165

6266
private Map<String, String> tmpfsOptions = new HashMap<>();
6367

68+
private Map<String, List<MountableFile>> copyFilesToContainerOptions = Collections.emptyMap();
69+
6470
public static ConnectionUrl newInstance(final String url) {
6571
ConnectionUrl connectionUrl = new ConnectionUrl(url);
6672
connectionUrl.parseUrl();
@@ -135,6 +141,8 @@ private void parseUrl() {
135141

136142
tmpfsOptions = parseTmpfsOptions(containerParameters);
137143

144+
copyFilesToContainerOptions = parseCopyFilesToContainerOptions(containerParameters);
145+
138146
initScriptPath = Optional.ofNullable(containerParameters.get("TC_INITSCRIPT"));
139147

140148
reusable = Boolean.parseBoolean(containerParameters.get("TC_REUSABLE"));
@@ -160,6 +168,31 @@ private Map<String, String> parseTmpfsOptions(Map<String, String> containerParam
160168
.collect(Collectors.toMap(string -> string.split(":")[0], string -> string.split(":")[1]));
161169
}
162170

171+
private Map<String, List<MountableFile>> parseCopyFilesToContainerOptions(Map<String, String> containerParameters) {
172+
if (!containerParameters.containsKey("TC_COPY_FILES")) {
173+
return Collections.emptyMap();
174+
}
175+
176+
String copyFilesToContainerOptions = containerParameters.get("TC_COPY_FILES");
177+
178+
Map<String, List<MountableFile>> mounts = new HashMap<>();
179+
Arrays
180+
.stream(copyFilesToContainerOptions.split(","))
181+
.map(string -> string.split(":"))
182+
.forEach(split -> {
183+
String hostPath = split[0];
184+
String containerPath = split[1];
185+
Integer mode = split.length > 2 ? Integer.parseInt(split[2], 8) : null;
186+
MountableFile mountableFile = (hostPath.startsWith("/"))
187+
? MountableFile.forHostPath(hostPath, mode)
188+
: MountableFile.forClasspathResource(hostPath, mode);
189+
mounts.putIfAbsent(containerPath, new ArrayList<>());
190+
mounts.get(containerPath).add(mountableFile);
191+
});
192+
193+
return mounts;
194+
}
195+
163196
/**
164197
* Get the TestContainers Parameters such as Init Function, Init Script path etc.
165198
*
@@ -201,6 +234,10 @@ public Map<String, String> getTmpfsOptions() {
201234
return Collections.unmodifiableMap(tmpfsOptions);
202235
}
203236

237+
public Map<String, List<MountableFile>> getCopyFilesToContainerOptions() {
238+
return Collections.unmodifiableMap(copyFilesToContainerOptions);
239+
}
240+
204241
/**
205242
* This interface defines the Regex Patterns used by {@link ConnectionUrl}.
206243
*

modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerDatabaseDriver.java

+9
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ public synchronized Connection connect(String url, final Properties info) throws
108108
if (candidateContainerType.supports(connectionUrl.getDatabaseType())) {
109109
container = candidateContainerType.newInstance(connectionUrl);
110110
container.withTmpFs(connectionUrl.getTmpfsOptions());
111+
112+
JdbcDatabaseContainer finalContainer = container;
113+
connectionUrl
114+
.getCopyFilesToContainerOptions()
115+
.forEach((containerPath, mountableFiles) -> {
116+
mountableFiles.forEach(mountableFile -> {
117+
finalContainer.withCopyFileToContainer(mountableFile, containerPath);
118+
});
119+
});
111120
delegate = container.getJdbcDriverInstance();
112121
}
113122
}

modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlTest.java

+57
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.junit.Rule;
44
import org.junit.Test;
55
import org.junit.rules.ExpectedException;
6+
import org.testcontainers.utility.MountableFile;
67

78
import static org.assertj.core.api.Assertions.assertThat;
89

@@ -68,6 +69,62 @@ public void testTmpfsOption() {
6869
assertThat(url.getTmpfsOptions()).as("tmpfs option key1 has correct value").containsEntry("key1", "value1");
6970
}
7071

72+
@Test
73+
public void testCopyFilesOption() {
74+
String urlString =
75+
"jdbc:tc:mysql://somehostname/databasename?TC_COPY_FILES=/key:path1,logback-test.xml:path2:755";
76+
ConnectionUrl url = ConnectionUrl.newInstance(urlString);
77+
78+
assertThat(url.getQueryParameters()).as("Connection Parameters set is empty").isEmpty();
79+
assertThat(url.getContainerParameters()).as("Container Parameters set is not empty").isNotEmpty();
80+
assertThat(url.getContainerParameters())
81+
.as("Container Parameter TC_COPY_FILES is true")
82+
.containsEntry("TC_COPY_FILES", "/key:path1,logback-test.xml:path2:755");
83+
assertThat(url.getCopyFilesToContainerOptions())
84+
.as("copyFiles option has correct values")
85+
.containsOnlyKeys("path1", "path2");
86+
87+
assertThat(url.getCopyFilesToContainerOptions())
88+
.as("copyFiles option path1 has correct value")
89+
.extractingByKey("path1")
90+
.asList()
91+
.hasSize(1);
92+
assertThat(url.getCopyFilesToContainerOptions())
93+
.as("copyFiles option path1 has correct value")
94+
.extractingByKey("path1")
95+
.asList()
96+
.element(0)
97+
.extracting("filesystemPath")
98+
.isEqualTo("/key");
99+
assertThat(url.getCopyFilesToContainerOptions())
100+
.as("copyFiles option path1 has correct value")
101+
.extractingByKey("path1")
102+
.asList()
103+
.element(0)
104+
.extracting("fileMode")
105+
.isEqualTo(0100000 | 0644);
106+
107+
assertThat(url.getCopyFilesToContainerOptions())
108+
.as("copyFiles option path1 has correct value")
109+
.extractingByKey("path2")
110+
.asList()
111+
.hasSize(1);
112+
assertThat(url.getCopyFilesToContainerOptions())
113+
.as("copyFiles option path1 has correct value")
114+
.extractingByKey("path2")
115+
.asList()
116+
.element(0)
117+
.extracting("filesystemPath")
118+
.isEqualTo(MountableFile.forClasspathResource("logback-test.xml").getFilesystemPath());
119+
assertThat(url.getCopyFilesToContainerOptions())
120+
.as("copyFiles option path1 has correct value")
121+
.extractingByKey("path2")
122+
.asList()
123+
.element(0)
124+
.extracting("fileMode")
125+
.isEqualTo(0100000 | 0755);
126+
}
127+
71128
@Test
72129
public void testInitScriptPathCapture() {
73130
String urlString =

modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/MySQLJDBCDriverTest.java

+4
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ public static Iterable<Object[]> data() {
4545
"jdbc:tc:mysql:5.6.51://hostname/databasename?TC_MY_CNF=somepath/mysql_conf_override",
4646
EnumSet.of(Options.CustomIniFile),
4747
},
48+
{
49+
"jdbc:tc:mysql://hostname/databasename?TC_COPY_FILES=somepath/mysql_conf_override/my.cnf:/etc/mysql/conf.d/mysqld.cnf",
50+
EnumSet.of(Options.CopyFiles),
51+
},
4852
}
4953
);
5054
}

modules/mysql/src/test/resources/somepath/mysql_conf_override/my.cnf

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ user = mysql
33
port = 3306
44
#socket = /tmp/mysql.sock
55
skip-external-locking
6-
key_buffer_size = 16K
6+
key_buffer_size = 32K
77
max_allowed_packet = 1M
88
table_open_cache = 4
99
sort_buffer_size = 64K
@@ -51,4 +51,4 @@ innodb_buffer_pool_size = 16M
5151
innodb_log_file_size = 5M
5252
innodb_log_buffer_size = 8M
5353
innodb_flush_log_at_trx_commit = 1
54-
innodb_lock_wait_timeout = 50
54+
innodb_lock_wait_timeout = 50

0 commit comments

Comments
 (0)