Skip to content

Commit

Permalink
Support mapping volumes in Dev Service container based databases
Browse files Browse the repository at this point in the history
This is a very useful feature that allows mapping volumes in the Dev Service containers. 

For example: to keep the Postgres data folder in the localsystem and hence have the data persistently. 

Fix #30595
  • Loading branch information
Sgitario committed Mar 15, 2023
1 parent d40800d commit 10c747c
Show file tree
Hide file tree
Showing 11 changed files with 114 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class DevServicesDatasourceContainerConfig {
private final Optional<String> username;
private final Optional<String> password;
private final Optional<String> initScriptPath;
private final Map<String, String> volumes;

public DevServicesDatasourceContainerConfig(Optional<String> imageName,
Map<String, String> containerProperties,
Expand All @@ -24,7 +25,8 @@ public DevServicesDatasourceContainerConfig(Optional<String> imageName,
Optional<String> dbName,
Optional<String> username,
Optional<String> password,
Optional<String> initScriptPath) {
Optional<String> initScriptPath,
Map<String, String> volumes) {
this.imageName = imageName;
this.containerProperties = containerProperties;
this.additionalJdbcUrlProperties = additionalJdbcUrlProperties;
Expand All @@ -34,6 +36,7 @@ public DevServicesDatasourceContainerConfig(Optional<String> imageName,
this.username = username;
this.password = password;
this.initScriptPath = initScriptPath;
this.volumes = volumes;
}

public Optional<String> getImageName() {
Expand Down Expand Up @@ -71,4 +74,8 @@ public Optional<String> getPassword() {
public Optional<String> getInitScriptPath() {
return initScriptPath;
}

public Map<String, String> getVolumes() {
return volumes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ private RunningDevService startDevDb(String dbName,
+ " (" + defaultDbKind.get() + ") starting:",
consoleInstalledBuildItem,
loggingSetupBuildItem);

try {
DevServicesDatasourceContainerConfig containerConfig = new DevServicesDatasourceContainerConfig(
dataSourceBuildTimeConfig.devservices.imageName,
Expand All @@ -276,7 +277,8 @@ private RunningDevService startDevDb(String dbName,
dataSourceBuildTimeConfig.devservices.dbName,
dataSourceBuildTimeConfig.devservices.username,
dataSourceBuildTimeConfig.devservices.password,
dataSourceBuildTimeConfig.devservices.initScriptPath);
dataSourceBuildTimeConfig.devservices.initScriptPath,
dataSourceBuildTimeConfig.devservices.volumes);

DevServicesDatasourceProvider.RunningDevServicesDatasource datasource = devDbProvider
.startDatabase(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,18 @@ public class DevServicesBuildTimeConfig {
/**
* Path to a SQL script that will be loaded from the classpath and applied to the Dev Service database
*
* If the provider is not container based (e.g. a H2 or Derby Database) then this has no effect.
* If the provider is not container based (e.g. an H2 or Derby Database) then this has no effect.
*/
@ConfigItem
public Optional<String> initScriptPath;

/**
* The volumes to be mapped to the container. The map key corresponds to the host location and the map value is the
* container location. If the host location starts with "classpath:", then the mapping will load the resource from the
* classpath.
*
* If the provider is not container based (e.g. an H2 or Derby Database) then this has no effect.
*/
@ConfigItem
public Map<String, String> volumes;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.quarkus.devservices.common;

import java.net.URL;
import java.util.Map;

import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.GenericContainer;

public final class Volumes {

private static final String CLASSPATH = "classpath:";
private static final String EMPTY = "";

private Volumes() {

}

public static void addVolumes(GenericContainer<?> container, Map<String, String> volumes) {
for (Map.Entry<String, String> volume : volumes.entrySet()) {
String hostLocation = volume.getKey();
BindMode bindMode = BindMode.READ_WRITE;
if (volume.getKey().startsWith(CLASSPATH)) {
URL url = Thread.currentThread().getContextClassLoader()
.getResource(hostLocation.replaceFirst(CLASSPATH, EMPTY));
if (url == null) {
throw new IllegalStateException("Classpath resource at '" + hostLocation + "' not found!");
}

hostLocation = url.getPath();
bindMode = BindMode.READ_ONLY;
}

container.withFileSystemBind(hostLocation, volume.getValue(), bindMode);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.quarkus.devservices.common.ConfigureUtil;
import io.quarkus.devservices.common.ContainerShutdownCloseable;
import io.quarkus.devservices.common.Labels;
import io.quarkus.devservices.common.Volumes;
import io.quarkus.runtime.LaunchMode;

public class DB2DevServicesProcessor {
Expand Down Expand Up @@ -50,6 +51,7 @@ public RunningDevServicesDatasource startDatabase(Optional<String> username, Opt
.withDatabaseName(effectiveDbName)
.withReuse(true);
Labels.addDataSourceLabel(container, datasourceName);
Volumes.addVolumes(container, containerConfig.getVolumes());

containerConfig.getAdditionalJdbcUrlProperties().forEach(container::withUrlParam);
containerConfig.getCommand().ifPresent(container::setCommand);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.quarkus.devservices.common.ConfigureUtil;
import io.quarkus.devservices.common.ContainerShutdownCloseable;
import io.quarkus.devservices.common.Labels;
import io.quarkus.devservices.common.Volumes;
import io.quarkus.runtime.LaunchMode;

public class MariaDBDevServicesProcessor {
Expand Down Expand Up @@ -53,6 +54,7 @@ public RunningDevServicesDatasource startDatabase(Optional<String> username, Opt
.withDatabaseName(effectiveDbName)
.withReuse(true);
Labels.addDataSourceLabel(container, datasourceName);
Volumes.addVolumes(container, containerConfig.getVolumes());

if (containerConfig.getContainerProperties().containsKey(MY_CNF_CONFIG_OVERRIDE_PARAM_NAME)) {
container.withConfigurationOverride(
Expand All @@ -62,7 +64,6 @@ public RunningDevServicesDatasource startDatabase(Optional<String> username, Opt
containerConfig.getAdditionalJdbcUrlProperties().forEach(container::withUrlParam);
containerConfig.getCommand().ifPresent(container::setCommand);
containerConfig.getInitScriptPath().ifPresent(container::withInitScript);

container.start();

LOG.info("Dev Services for MariaDB started.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.quarkus.devservices.common.ConfigureUtil;
import io.quarkus.devservices.common.ContainerShutdownCloseable;
import io.quarkus.devservices.common.Labels;
import io.quarkus.devservices.common.Volumes;
import io.quarkus.runtime.LaunchMode;

public class MSSQLDevServicesProcessor {
Expand All @@ -46,6 +47,7 @@ public RunningDevServicesDatasource startDatabase(Optional<String> username, Opt
container.withPassword(effectivePassword)
.withReuse(true);
Labels.addDataSourceLabel(container, datasourceName);
Volumes.addVolumes(container, containerConfig.getVolumes());

containerConfig.getAdditionalJdbcUrlProperties().forEach(container::withUrlParam);
containerConfig.getCommand().ifPresent(container::setCommand);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.quarkus.devservices.common.ConfigureUtil;
import io.quarkus.devservices.common.ContainerShutdownCloseable;
import io.quarkus.devservices.common.Labels;
import io.quarkus.devservices.common.Volumes;
import io.quarkus.runtime.LaunchMode;

public class MySQLDevServicesProcessor {
Expand Down Expand Up @@ -52,6 +53,7 @@ public RunningDevServicesDatasource startDatabase(Optional<String> username, Opt
.withDatabaseName(effectiveDbName)
.withReuse(true);
Labels.addDataSourceLabel(container, datasourceName);
Volumes.addVolumes(container, containerConfig.getVolumes());

if (containerConfig.getContainerProperties().containsKey(MY_CNF_CONFIG_OVERRIDE_PARAM_NAME)) {
container.withConfigurationOverride(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.quarkus.devservices.common.ConfigureUtil;
import io.quarkus.devservices.common.ContainerShutdownCloseable;
import io.quarkus.devservices.common.Labels;
import io.quarkus.devservices.common.Volumes;
import io.quarkus.runtime.LaunchMode;

public class OracleDevServicesProcessor {
Expand Down Expand Up @@ -53,6 +54,7 @@ public RunningDevServicesDatasource startDatabase(Optional<String> username, Opt
.withDatabaseName(effectiveDbName)
.withReuse(true);
Labels.addDataSourceLabel(container, datasourceName);
Volumes.addVolumes(container, containerConfig.getVolumes());

// We need to limit the maximum amount of CPUs being used by the container;
// otherwise the hardcoded memory configuration of the DB might not be enough to successfully boot it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import io.quarkus.devservices.common.ConfigureUtil;
import io.quarkus.devservices.common.ContainerShutdownCloseable;
import io.quarkus.devservices.common.Labels;
import io.quarkus.devservices.common.Volumes;
import io.quarkus.runtime.LaunchMode;

public class PostgresqlDevServicesProcessor {
Expand Down Expand Up @@ -57,6 +58,7 @@ public RunningDevServicesDatasource startDatabase(Optional<String> username, Opt
.withDatabaseName(effectiveDbName)
.withReuse(true);
Labels.addDataSourceLabel(container, datasourceName);
Volumes.addVolumes(container, containerConfig.getVolumes());

containerConfig.getAdditionalJdbcUrlProperties().forEach(container::withUrlParam);
containerConfig.getCommand().ifPresent(container::setCommand);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.quarkus.jdbc.postgresql.deployment;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ResultSet;

import javax.sql.DataSource;

import jakarta.inject.Inject;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

public class DevServicesPostgresqlDatasourceWithVolumeTestCase {

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest()
.overrideConfigKey("quarkus.datasource.db-kind", "postgresql")
// The official postgres image will execute all the scripts in the folder "docker-entrypoint-initdb.d"
.overrideConfigKey("quarkus.datasource.devservices.volumes.\"classpath:./init-db.sql\"",
"/docker-entrypoint-initdb.d/init-db.sql");

@Inject
DataSource ds;

@Test
@DisplayName("Test if volume is mounted successfully")
public void testDatasource() throws Exception {
int result = 0;
try (Connection con = ds.getConnection();
CallableStatement cs = con.prepareCall("SELECT my_func()");
ResultSet rs = cs.executeQuery()) {
if (rs.next()) {
result = rs.getInt(1);
}
}
assertEquals(100, result, "The init script should have been executed");
}
}

0 comments on commit 10c747c

Please sign in to comment.