Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ body:
- DB2
- Dynalite
- Elasticsearch
- GaussDB
- GCloud
- Grafana
- HiveMQ
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/enhancement.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ body:
- DB2
- Dynalite
- Elasticsearch
- GaussDB
- GCloud
- Grafana
- HiveMQ
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/feature.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ body:
- DB2
- Dynalite
- Elasticsearch
- GaussDB
- GCloud
- Grafana
- HiveMQ
Expand Down
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
33 changes: 33 additions & 0 deletions docs/modules/databases/gaussdb.md
Original file line number Diff line number Diff line change
@@ -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:
<!--codeinclude-->
[Container creation](../../../modules/gaussdb/src/test/java/org/testcontainers/junit/gaussdb/SimpleGaussDBTest.java) inside_block:constructor
<!--/codeinclude-->

## 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
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>gaussdb</artifactId>
<version>{{latest_version}}</version>
<scope>test</scope>
</dependency>
```

!!! 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.


10 changes: 10 additions & 0 deletions modules/gaussdb/build.gradle
Original file line number Diff line number Diff line change
@@ -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'
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* Supported images: {@code opengauss/opengauss}
* <p>
* Exposed ports: 8000
*/
public class GaussDBContainer<SELF extends GaussDBContainer<SELF>> extends JdbcDatabaseContainer<SELF> {

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<Integer> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.testcontainers.containers.GaussDBContainerProvider
Original file line number Diff line number Diff line change
@@ -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");
}
Original file line number Diff line number Diff line change
@@ -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<FixedJdbcUrlGaussDBContainer> {

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<FixedJdbcUrlGaussDBContainer> {

public NoParamsUrlGaussDBContainer() {
super(GaussDBTestImages.GAUSSDB_TEST_IMAGE);
}

@Override
public String getJdbcUrl() {
return "jdbc:gaussdb://host:port/database";
}
}
}
Loading