diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index c31dd05e048..33a909afd22 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -27,6 +27,7 @@ body:
- DB2
- Dynalite
- Elasticsearch
+ - GaussDB
- GCloud
- Grafana
- HiveMQ
diff --git a/.github/ISSUE_TEMPLATE/enhancement.yaml b/.github/ISSUE_TEMPLATE/enhancement.yaml
index 9b9a06ecf6a..5ca2d16c037 100644
--- a/.github/ISSUE_TEMPLATE/enhancement.yaml
+++ b/.github/ISSUE_TEMPLATE/enhancement.yaml
@@ -27,6 +27,7 @@ body:
- DB2
- Dynalite
- Elasticsearch
+ - GaussDB
- GCloud
- Grafana
- HiveMQ
diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml
index b655b4ac505..61ebd39621e 100644
--- a/.github/ISSUE_TEMPLATE/feature.yaml
+++ b/.github/ISSUE_TEMPLATE/feature.yaml
@@ -27,6 +27,7 @@ body:
- DB2
- Dynalite
- Elasticsearch
+ - GaussDB
- GCloud
- Grafana
- HiveMQ
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index fe7a57a8603..a8b4670ae52 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -105,6 +105,11 @@ updates:
schedule:
interval: "monthly"
open-pull-requests-limit: 10
+ - package-ecosystem: "gradle"
+ directory: "/modules/gaussdb"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
- package-ecosystem: "gradle"
directory: "/modules/gcloud"
schedule:
diff --git a/.github/labeler.yml b/.github/labeler.yml
index f4649bd7f99..a47c4aa36d3 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -71,6 +71,10 @@
- changed-files:
- any-glob-to-any-file:
- modules/elasticsearch/**/*
+"modules/gaussdb":
+ - changed-files:
+ - any-glob-to-any-file:
+ - modules/gaussdb/**/*
"modules/gcloud":
- changed-files:
- any-glob-to-any-file:
diff --git a/docs/modules/databases/gaussdb.md b/docs/modules/databases/gaussdb.md
new file mode 100644
index 00000000000..5b066963157
--- /dev/null
+++ b/docs/modules/databases/gaussdb.md
@@ -0,0 +1,33 @@
+# GaussDB Module
+
+See [Database containers](./index.md) for documentation and usage that is common to all relational database container types.
+
+## Usage example
+
+You can use 'GaussDBContainer' like any other JDBC container:
+
+[Container creation](../../../modules/gaussdb/src/test/java/org/testcontainers/junit/gaussdb/SimpleGaussDBTest.java) inside_block:constructor
+
+
+## Adding this module to your project dependencies
+
+Add the following dependency to your `pom.xml`/`build.gradle` file:
+
+=== "Gradle"
+ ```groovy
+ testImplementation "org.testcontainers:gaussdb:{{latest_version}}"
+ ```
+=== "Maven"
+ ```xml
+
+ org.testcontainers
+ gaussdb
+ {{latest_version}}
+ test
+
+ ```
+
+!!! hint
+ Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency.
+
+
diff --git a/modules/gaussdb/build.gradle b/modules/gaussdb/build.gradle
new file mode 100644
index 00000000000..5c72a4dd20a
--- /dev/null
+++ b/modules/gaussdb/build.gradle
@@ -0,0 +1,10 @@
+description = "Testcontainers :: JDBC :: GaussDB"
+
+dependencies {
+ api project(':jdbc')
+
+ testImplementation project(':jdbc-test')
+ testRuntimeOnly 'com.huaweicloud.gaussdb:gaussdbjdbc:506.0.0.b058'
+
+ compileOnly 'org.jetbrains:annotations:24.1.0'
+}
diff --git a/modules/gaussdb/src/main/java/org/testcontainers/containers/GaussDBContainer.java b/modules/gaussdb/src/main/java/org/testcontainers/containers/GaussDBContainer.java
new file mode 100644
index 00000000000..78a2e88cd7e
--- /dev/null
+++ b/modules/gaussdb/src/main/java/org/testcontainers/containers/GaussDBContainer.java
@@ -0,0 +1,169 @@
+package org.testcontainers.containers;
+
+import org.jetbrains.annotations.NotNull;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.containers.wait.strategy.WaitStrategy;
+import org.testcontainers.containers.wait.strategy.WaitStrategyTarget;
+import org.testcontainers.utility.DockerImageName;
+
+import java.time.Duration;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+/**
+ * Testcontainers implementation for GaussDB.
+ *
+ * Supported images: {@code opengauss/opengauss}
+ *
+ * Exposed ports: 8000
+ */
+public class GaussDBContainer> extends JdbcDatabaseContainer {
+
+ public static final String NAME = "gaussdb";
+
+ public static final String IMAGE = "opengauss/opengauss";
+
+ public static final String DEFAULT_TAG = "7.0.0-RC1.B023";
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(IMAGE);
+
+ public static final Integer GaussDB_PORT = 8000;
+
+ public static final String DEFAULT_USER_NAME = "test";
+
+ // At least one uppercase, lowercase, numeric, special character, and password length(8).
+ public static final String DEFAULT_PASSWORD = "Test@123";
+
+ private String databaseName = "gaussdb";
+
+ private String username = DEFAULT_USER_NAME;
+
+ private String password = DEFAULT_PASSWORD;
+
+ private static final String PASSWORD_REGEX = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=!*(),.?\":{}|<>]).{8,}$";
+
+ private static final Pattern PASSWORD_PATTERN = Pattern.compile(PASSWORD_REGEX);
+
+ /**
+ * @deprecated use {@link #GaussDBContainer(DockerImageName)} or {@link #GaussDBContainer(String)} instead
+ */
+ @Deprecated
+ public GaussDBContainer() {
+ this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG));
+ }
+
+ public GaussDBContainer(final String dockerImageName) {
+ this(DockerImageName.parse(dockerImageName));
+ }
+
+ public GaussDBContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+ setWaitStrategy(new WaitStrategy() {
+ @Override
+ public void waitUntilReady(WaitStrategyTarget waitStrategyTarget) {
+ Wait.forListeningPort().waitUntilReady(waitStrategyTarget);
+ try {
+ // Open Gauss will set up users and password when ports are ready.
+ Wait.forLogMessage(".*gs_ctl stopped.*", 1).waitUntilReady(waitStrategyTarget);
+ // Not enough and no idea
+ TimeUnit.SECONDS.sleep(3);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public WaitStrategy withStartupTimeout(Duration duration) {
+ return GenericContainer.DEFAULT_WAIT_STRATEGY.withStartupTimeout(duration);
+ }
+ });
+ }
+
+ /**
+ * @return the ports on which to check if the container is ready
+ * @deprecated use {@link #getLivenessCheckPortNumbers()} instead
+ */
+ @NotNull
+ @Override
+ @Deprecated
+ protected Set getLivenessCheckPorts() {
+ return super.getLivenessCheckPorts();
+ }
+
+ @Override
+ protected void configure() {
+ // Disable GaussDB driver use of java.util.logging to reduce noise at startup time
+ withUrlParam("loggerLevel", "OFF");
+ addExposedPorts(GaussDB_PORT);
+ addEnv("GS_DB", databaseName);
+ addEnv("GS_PORT", String.valueOf(GaussDB_PORT));
+ addEnv("GS_USERNAME", username);
+ addEnv("GS_PASSWORD", password);
+ }
+
+ @Override
+ public String getDriverClassName() {
+ return "com.huawei.gaussdb.jdbc.Driver";
+ }
+
+ @Override
+ public String getJdbcUrl() {
+ String additionalUrlParams = constructUrlParameters("?", "&");
+ return (
+ "jdbc:gaussdb://" +
+ getHost() +
+ ":" +
+ getMappedPort(GaussDB_PORT) +
+ "/" +
+ databaseName +
+ additionalUrlParams
+ );
+ }
+
+ @Override
+ public String getDatabaseName() {
+ return databaseName;
+ }
+
+ @Override
+ public String getUsername() {
+ return username;
+ }
+
+ @Override
+ public String getPassword() {
+ return password;
+ }
+
+ @Override
+ public String getTestQueryString() {
+ return "SELECT 1";
+ }
+
+ @Override
+ public SELF withDatabaseName(final String databaseName) {
+ this.databaseName = databaseName;
+ return self();
+ }
+
+ @Override
+ public SELF withUsername(final String username) {
+ this.username = username;
+ return self();
+ }
+
+ @Override
+ public SELF withPassword(final String password) {
+ if (!PASSWORD_PATTERN.matcher(password).matches()){
+ throw new ContainerLaunchException("The password should contain at least one uppercase, lowercase, numeric, special character, and password length(8).");
+ }
+ this.password = password;
+ return self();
+ }
+
+ @Override
+ protected void waitUntilContainerStarted() {
+ getWaitStrategy().waitUntilReady(this);
+ }
+}
diff --git a/modules/gaussdb/src/main/java/org/testcontainers/containers/GaussDBContainerProvider.java b/modules/gaussdb/src/main/java/org/testcontainers/containers/GaussDBContainerProvider.java
new file mode 100644
index 00000000000..e21aebb85ec
--- /dev/null
+++ b/modules/gaussdb/src/main/java/org/testcontainers/containers/GaussDBContainerProvider.java
@@ -0,0 +1,34 @@
+package org.testcontainers.containers;
+
+import org.testcontainers.jdbc.ConnectionUrl;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * Factory for GaussDB containers.
+ */
+public class GaussDBContainerProvider extends JdbcDatabaseContainerProvider {
+
+ public static final String USER_PARAM = "user";
+
+ public static final String PASSWORD_PARAM = "password";
+
+ @Override
+ public boolean supports(String databaseType) {
+ return databaseType.equals(GaussDBContainer.NAME);
+ }
+
+ @Override
+ public JdbcDatabaseContainer newInstance() {
+ return newInstance(GaussDBContainer.DEFAULT_TAG);
+ }
+
+ @Override
+ public JdbcDatabaseContainer newInstance(String tag) {
+ return new GaussDBContainer(DockerImageName.parse(GaussDBContainer.IMAGE).withTag(tag));
+ }
+
+ @Override
+ public JdbcDatabaseContainer newInstance(ConnectionUrl connectionUrl) {
+ return newInstanceFromConnectionUrl(connectionUrl, USER_PARAM, PASSWORD_PARAM);
+ }
+}
diff --git a/modules/gaussdb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider b/modules/gaussdb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider
new file mode 100644
index 00000000000..9707299c2ba
--- /dev/null
+++ b/modules/gaussdb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider
@@ -0,0 +1 @@
+org.testcontainers.containers.GaussDBContainerProvider
diff --git a/modules/gaussdb/src/test/java/org/testcontainers/GaussDBTestImages.java b/modules/gaussdb/src/test/java/org/testcontainers/GaussDBTestImages.java
new file mode 100644
index 00000000000..25d0975fd60
--- /dev/null
+++ b/modules/gaussdb/src/test/java/org/testcontainers/GaussDBTestImages.java
@@ -0,0 +1,7 @@
+package org.testcontainers;
+
+import org.testcontainers.utility.DockerImageName;
+
+public interface GaussDBTestImages {
+ DockerImageName GAUSSDB_TEST_IMAGE = DockerImageName.parse("opengauss/opengauss:7.0.0-RC1.B023");
+}
diff --git a/modules/gaussdb/src/test/java/org/testcontainers/containers/GaussDBConnectionURLTest.java b/modules/gaussdb/src/test/java/org/testcontainers/containers/GaussDBConnectionURLTest.java
new file mode 100644
index 00000000000..31a6f93f189
--- /dev/null
+++ b/modules/gaussdb/src/test/java/org/testcontainers/containers/GaussDBConnectionURLTest.java
@@ -0,0 +1,84 @@
+package org.testcontainers.containers;
+
+import org.junit.Test;
+import org.testcontainers.GaussDBTestImages;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+
+public class GaussDBConnectionURLTest {
+
+ @Test
+ public void shouldCorrectlyAppendQueryString() {
+ GaussDBContainer> gaussDB = new FixedJdbcUrlGaussDBContainer();
+ String connectionUrl = gaussDB.constructUrlForConnection("?stringtype=unspecified&stringtype=unspecified");
+ String queryString = connectionUrl.substring(connectionUrl.indexOf('?'));
+
+ assertThat(queryString)
+ .as("Query String contains expected params")
+ .contains("?stringtype=unspecified&stringtype=unspecified");
+ assertThat(queryString.indexOf('?')).as("Query String starts with '?'").isZero();
+ assertThat(queryString.substring(1)).as("Query String does not contain extra '?'").doesNotContain("?");
+ }
+
+ @Test
+ public void shouldCorrectlyAppendQueryStringWhenNoBaseParams() {
+ GaussDBContainer> gaussDB = new NoParamsUrlGaussDBContainer();
+ String connectionUrl = gaussDB.constructUrlForConnection("?stringtype=unspecified&stringtype=unspecified");
+ String queryString = connectionUrl.substring(connectionUrl.indexOf('?'));
+
+ assertThat(queryString)
+ .as("Query String contains expected params")
+ .contains("?stringtype=unspecified&stringtype=unspecified");
+ assertThat(queryString.indexOf('?')).as("Query String starts with '?'").isZero();
+ assertThat(queryString.substring(1)).as("Query String does not contain extra '?'").doesNotContain("?");
+ }
+
+ @Test
+ public void shouldReturnOriginalURLWhenEmptyQueryString() {
+ GaussDBContainer> gaussDB = new FixedJdbcUrlGaussDBContainer();
+ String connectionUrl = gaussDB.constructUrlForConnection("");
+
+ assertThat(gaussDB.getJdbcUrl()).as("Query String remains unchanged").isEqualTo(connectionUrl);
+ }
+
+ @Test
+ public void shouldRejectInvalidQueryString() {
+ assertThat(
+ catchThrowable(() -> {
+ new NoParamsUrlGaussDBContainer().constructUrlForConnection("stringtype=unspecified");
+ })
+ )
+ .as("Fails when invalid query string provided")
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ static class FixedJdbcUrlGaussDBContainer extends GaussDBContainer {
+
+ public FixedJdbcUrlGaussDBContainer() {
+ super(GaussDBTestImages.GAUSSDB_TEST_IMAGE);
+ }
+
+ @Override
+ public String getHost() {
+ return "localhost";
+ }
+
+ @Override
+ public Integer getMappedPort(int originalPort) {
+ return 34532;
+ }
+ }
+
+ static class NoParamsUrlGaussDBContainer extends GaussDBContainer {
+
+ public NoParamsUrlGaussDBContainer() {
+ super(GaussDBTestImages.GAUSSDB_TEST_IMAGE);
+ }
+
+ @Override
+ public String getJdbcUrl() {
+ return "jdbc:gaussdb://host:port/database";
+ }
+ }
+}
diff --git a/modules/gaussdb/src/test/java/org/testcontainers/jdbc/DatabaseDriverShutdownTest.java b/modules/gaussdb/src/test/java/org/testcontainers/jdbc/DatabaseDriverShutdownTest.java
new file mode 100644
index 00000000000..ec3efa85603
--- /dev/null
+++ b/modules/gaussdb/src/test/java/org/testcontainers/jdbc/DatabaseDriverShutdownTest.java
@@ -0,0 +1,52 @@
+package org.testcontainers.jdbc;
+
+import org.junit.AfterClass;
+import org.junit.Test;
+import org.testcontainers.containers.JdbcDatabaseContainer;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * This test belongs in the jdbc module, as it is focused on testing the behaviour of {@link org.testcontainers.containers.JdbcDatabaseContainer}.
+ * However, the need to use the {@link org.testcontainers.containers.GaussDBContainerProvider} (due to the jdbc:tc:gaussdb) URL forces it to live here in
+ * the mysql module, to avoid circular dependencies.
+ * TODO: Move to the jdbc module and either (a) implement a barebones {@link org.testcontainers.containers.JdbcDatabaseContainerProvider} for testing, or (b) refactor into a unit test.
+ */
+public class DatabaseDriverShutdownTest {
+
+ @AfterClass
+ public static void testCleanup() {
+ ContainerDatabaseDriver.killContainers();
+ }
+
+ @Test
+ public void shouldStopContainerWhenAllConnectionsClosed() throws SQLException {
+ final String jdbcUrl = "jdbc:tc:gaussdb://hostname/databasename";
+
+ getConnectionAndClose(jdbcUrl);
+
+ JdbcDatabaseContainer> container = ContainerDatabaseDriver.getContainer(jdbcUrl);
+ assertThat(container).as("Database container instance is null as expected").isNull();
+ }
+
+ @Test
+ public void shouldNotStopDaemonContainerWhenAllConnectionsClosed() throws SQLException {
+ final String jdbcUrl = "jdbc:tc:gaussdb://hostname/databasename?TC_DAEMON=true";
+
+ getConnectionAndClose(jdbcUrl);
+
+ JdbcDatabaseContainer> container = ContainerDatabaseDriver.getContainer(jdbcUrl);
+ assertThat(container).as("Database container instance is not null as expected").isNotNull();
+ assertThat(container.isRunning()).as("Database container is running as expected").isTrue();
+ }
+
+ private void getConnectionAndClose(String jdbcUrl) throws SQLException {
+ try (Connection connection = DriverManager.getConnection(jdbcUrl)) {
+ assertThat(connection).as("Obtained connection as expected").isNotNull();
+ }
+ }
+}
diff --git a/modules/gaussdb/src/test/java/org/testcontainers/jdbc/DatabaseDriverTmpfsTest.java b/modules/gaussdb/src/test/java/org/testcontainers/jdbc/DatabaseDriverTmpfsTest.java
new file mode 100644
index 00000000000..00c40a4456c
--- /dev/null
+++ b/modules/gaussdb/src/test/java/org/testcontainers/jdbc/DatabaseDriverTmpfsTest.java
@@ -0,0 +1,37 @@
+package org.testcontainers.jdbc;
+
+import org.junit.Test;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.JdbcDatabaseContainer;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * This test belongs in the jdbc module, as it is focused on testing the behaviour of {@link org.testcontainers.containers.JdbcDatabaseContainer}.
+ * However, the need to use the {@link org.testcontainers.containers.GaussDBContainerProvider} (due to the jdbc:tc:gaussdb) URL forces it to live here in
+ * the mysql module, to avoid circular dependencies.
+ * TODO: Move to the jdbc module and either (a) implement a barebones {@link org.testcontainers.containers.JdbcDatabaseContainerProvider} for testing, or (b) refactor into a unit test.
+ */
+public class DatabaseDriverTmpfsTest {
+
+ @Test
+ public void testDatabaseHasTmpFsViaConnectionString() throws Exception {
+ final String jdbcUrl = "jdbc:tc:gaussdb://hostname/databasename?TC_TMPFS=/testtmpfs:rw";
+ try (Connection ignored = DriverManager.getConnection(jdbcUrl)) {
+ JdbcDatabaseContainer> container = ContainerDatabaseDriver.getContainer(jdbcUrl);
+ // check file doesn't exist
+ String path = "/testtmpfs/test.file";
+ Container.ExecResult execResult = container.execInContainer("ls", path);
+ assertThat(execResult.getExitCode())
+ .as("tmpfs inside container doesn't have file that doesn't exist")
+ .isNotZero();
+ // touch && check file does exist
+ container.execInContainer("touch", path);
+ execResult = container.execInContainer("ls", path);
+ assertThat(execResult.getExitCode()).as("tmpfs inside container has file that does exist").isZero();
+ }
+ }
+}
diff --git a/modules/gaussdb/src/test/java/org/testcontainers/jdbc/gaussdb/GaussDBJDBCDriverTest.java b/modules/gaussdb/src/test/java/org/testcontainers/jdbc/gaussdb/GaussDBJDBCDriverTest.java
new file mode 100644
index 00000000000..4932fdcc4c8
--- /dev/null
+++ b/modules/gaussdb/src/test/java/org/testcontainers/jdbc/gaussdb/GaussDBJDBCDriverTest.java
@@ -0,0 +1,24 @@
+package org.testcontainers.jdbc.gaussdb;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.testcontainers.jdbc.AbstractJDBCDriverTest;
+
+import java.util.Arrays;
+import java.util.EnumSet;
+
+@RunWith(Parameterized.class)
+public class GaussDBJDBCDriverTest extends AbstractJDBCDriverTest {
+
+ @Parameterized.Parameters(name = "{index} - {0}")
+ public static Iterable