From 10c747cf85c484bb98fc5b6d052b767a07677396 Mon Sep 17 00:00:00 2001 From: Jose Date: Wed, 15 Mar 2023 11:13:17 +0100 Subject: [PATCH] Support mapping volumes in Dev Service container based databases 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 https://github.com/quarkusio/quarkus/issues/30595 --- .../DevServicesDatasourceContainerConfig.java | 9 +++- .../DevServicesDatasourceProcessor.java | 4 +- .../runtime/DevServicesBuildTimeConfig.java | 12 ++++- .../quarkus/devservices/common/Volumes.java | 36 +++++++++++++++ .../deployment/DB2DevServicesProcessor.java | 2 + .../MariaDBDevServicesProcessor.java | 3 +- .../deployment/MSSQLDevServicesProcessor.java | 2 + .../deployment/MySQLDevServicesProcessor.java | 2 + .../OracleDevServicesProcessor.java | 2 + .../PostgresqlDevServicesProcessor.java | 2 + ...ostgresqlDatasourceWithVolumeTestCase.java | 44 +++++++++++++++++++ 11 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 extensions/devservices/common/src/main/java/io/quarkus/devservices/common/Volumes.java create mode 100644 extensions/jdbc/jdbc-postgresql/deployment/src/test/java/io/quarkus/jdbc/postgresql/deployment/DevServicesPostgresqlDatasourceWithVolumeTestCase.java diff --git a/extensions/datasource/deployment-spi/src/main/java/io/quarkus/datasource/deployment/spi/DevServicesDatasourceContainerConfig.java b/extensions/datasource/deployment-spi/src/main/java/io/quarkus/datasource/deployment/spi/DevServicesDatasourceContainerConfig.java index abba376e6eaf0..e4b533af72e13 100644 --- a/extensions/datasource/deployment-spi/src/main/java/io/quarkus/datasource/deployment/spi/DevServicesDatasourceContainerConfig.java +++ b/extensions/datasource/deployment-spi/src/main/java/io/quarkus/datasource/deployment/spi/DevServicesDatasourceContainerConfig.java @@ -15,6 +15,7 @@ public class DevServicesDatasourceContainerConfig { private final Optional username; private final Optional password; private final Optional initScriptPath; + private final Map volumes; public DevServicesDatasourceContainerConfig(Optional imageName, Map containerProperties, @@ -24,7 +25,8 @@ public DevServicesDatasourceContainerConfig(Optional imageName, Optional dbName, Optional username, Optional password, - Optional initScriptPath) { + Optional initScriptPath, + Map volumes) { this.imageName = imageName; this.containerProperties = containerProperties; this.additionalJdbcUrlProperties = additionalJdbcUrlProperties; @@ -34,6 +36,7 @@ public DevServicesDatasourceContainerConfig(Optional imageName, this.username = username; this.password = password; this.initScriptPath = initScriptPath; + this.volumes = volumes; } public Optional getImageName() { @@ -71,4 +74,8 @@ public Optional getPassword() { public Optional getInitScriptPath() { return initScriptPath; } + + public Map getVolumes() { + return volumes; + } } diff --git a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java index 65bc997799ab9..83af2b468d637 100644 --- a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java +++ b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java @@ -266,6 +266,7 @@ private RunningDevService startDevDb(String dbName, + " (" + defaultDbKind.get() + ") starting:", consoleInstalledBuildItem, loggingSetupBuildItem); + try { DevServicesDatasourceContainerConfig containerConfig = new DevServicesDatasourceContainerConfig( dataSourceBuildTimeConfig.devservices.imageName, @@ -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( diff --git a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DevServicesBuildTimeConfig.java b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DevServicesBuildTimeConfig.java index 9a2e33823e824..7c23bcdc04b7a 100644 --- a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DevServicesBuildTimeConfig.java +++ b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DevServicesBuildTimeConfig.java @@ -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 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 volumes; } diff --git a/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/Volumes.java b/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/Volumes.java new file mode 100644 index 0000000000000..39219be41eed6 --- /dev/null +++ b/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/Volumes.java @@ -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 volumes) { + for (Map.Entry 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); + } + } +} diff --git a/extensions/devservices/db2/src/main/java/io/quarkus/devservices/db2/deployment/DB2DevServicesProcessor.java b/extensions/devservices/db2/src/main/java/io/quarkus/devservices/db2/deployment/DB2DevServicesProcessor.java index de239be3b9d4c..fc75323b23ced 100644 --- a/extensions/devservices/db2/src/main/java/io/quarkus/devservices/db2/deployment/DB2DevServicesProcessor.java +++ b/extensions/devservices/db2/src/main/java/io/quarkus/devservices/db2/deployment/DB2DevServicesProcessor.java @@ -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 { @@ -50,6 +51,7 @@ public RunningDevServicesDatasource startDatabase(Optional 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); diff --git a/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java b/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java index c308925a2d671..e3b07f6d3125c 100644 --- a/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java +++ b/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java @@ -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 { @@ -53,6 +54,7 @@ public RunningDevServicesDatasource startDatabase(Optional 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( @@ -62,7 +64,6 @@ public RunningDevServicesDatasource startDatabase(Optional 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."); diff --git a/extensions/devservices/mssql/src/main/java/io/quarkus/devservices/mssql/deployment/MSSQLDevServicesProcessor.java b/extensions/devservices/mssql/src/main/java/io/quarkus/devservices/mssql/deployment/MSSQLDevServicesProcessor.java index 5f133ae371ff6..3da12d579152f 100644 --- a/extensions/devservices/mssql/src/main/java/io/quarkus/devservices/mssql/deployment/MSSQLDevServicesProcessor.java +++ b/extensions/devservices/mssql/src/main/java/io/quarkus/devservices/mssql/deployment/MSSQLDevServicesProcessor.java @@ -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 { @@ -46,6 +47,7 @@ public RunningDevServicesDatasource startDatabase(Optional 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); diff --git a/extensions/devservices/mysql/src/main/java/io/quarkus/devservices/mysql/deployment/MySQLDevServicesProcessor.java b/extensions/devservices/mysql/src/main/java/io/quarkus/devservices/mysql/deployment/MySQLDevServicesProcessor.java index 41d62223c327b..f59ac3f9cb4a1 100644 --- a/extensions/devservices/mysql/src/main/java/io/quarkus/devservices/mysql/deployment/MySQLDevServicesProcessor.java +++ b/extensions/devservices/mysql/src/main/java/io/quarkus/devservices/mysql/deployment/MySQLDevServicesProcessor.java @@ -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 { @@ -52,6 +53,7 @@ public RunningDevServicesDatasource startDatabase(Optional 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( diff --git a/extensions/devservices/oracle/src/main/java/io/quarkus/devservices/oracle/deployment/OracleDevServicesProcessor.java b/extensions/devservices/oracle/src/main/java/io/quarkus/devservices/oracle/deployment/OracleDevServicesProcessor.java index fa4ff0f5766dc..8e88e884170a5 100644 --- a/extensions/devservices/oracle/src/main/java/io/quarkus/devservices/oracle/deployment/OracleDevServicesProcessor.java +++ b/extensions/devservices/oracle/src/main/java/io/quarkus/devservices/oracle/deployment/OracleDevServicesProcessor.java @@ -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 { @@ -53,6 +54,7 @@ public RunningDevServicesDatasource startDatabase(Optional 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. diff --git a/extensions/devservices/postgresql/src/main/java/io/quarkus/devservices/postgresql/deployment/PostgresqlDevServicesProcessor.java b/extensions/devservices/postgresql/src/main/java/io/quarkus/devservices/postgresql/deployment/PostgresqlDevServicesProcessor.java index a4c9c9bf1c224..0e6a1427cdf6c 100644 --- a/extensions/devservices/postgresql/src/main/java/io/quarkus/devservices/postgresql/deployment/PostgresqlDevServicesProcessor.java +++ b/extensions/devservices/postgresql/src/main/java/io/quarkus/devservices/postgresql/deployment/PostgresqlDevServicesProcessor.java @@ -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 { @@ -57,6 +58,7 @@ public RunningDevServicesDatasource startDatabase(Optional 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); diff --git a/extensions/jdbc/jdbc-postgresql/deployment/src/test/java/io/quarkus/jdbc/postgresql/deployment/DevServicesPostgresqlDatasourceWithVolumeTestCase.java b/extensions/jdbc/jdbc-postgresql/deployment/src/test/java/io/quarkus/jdbc/postgresql/deployment/DevServicesPostgresqlDatasourceWithVolumeTestCase.java new file mode 100644 index 0000000000000..e00dfd1761f03 --- /dev/null +++ b/extensions/jdbc/jdbc-postgresql/deployment/src/test/java/io/quarkus/jdbc/postgresql/deployment/DevServicesPostgresqlDatasourceWithVolumeTestCase.java @@ -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"); + } +}