Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add oceanbase-ce module #7502

Merged
merged 13 commits into from
Feb 28, 2024
Merged
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ body:
- MySQL
- Neo4j
- NGINX
- OceanBase
- OpenFGA
- Oracle Free
- Oracle XE
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 @@ -41,6 +41,7 @@ body:
- MySQL
- Neo4j
- NGINX
- OceanBase
- OpenFGA
- Oracle Free
- Oracle XE
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 @@ -41,6 +41,7 @@ body:
- MySQL
- Neo4j
- NGINX
- OceanBase
- OpenFGA
- Oracle Free
- Oracle XE
Expand Down
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@ updates:
schedule:
interval: "weekly"
open-pull-requests-limit: 10
- package-ecosystem: "gradle"
directory: "/modules/oceanbase"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
- package-ecosystem: "gradle"
directory: "/modules/openfga"
schedule:
Expand Down
4 changes: 4 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@
- changed-files:
- any-glob-to-any-file:
- modules/nginx/**/*
"modules/oceanbase":
- changed-files:
- any-glob-to-any-file:
- modules/oceanbase/**/*
"modules/openfga":
- changed-files:
- any-glob-to-any-file:
Expand Down
4 changes: 4 additions & 0 deletions docs/modules/databases/jdbc.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ Insert `tc:` after `jdbc:` as follows. Note that the hostname, port and database

`jdbc:tc:sqlserver:2017-CU12:///databasename`

#### Using OceanBase

`jdbc:tc:oceanbasece:4.2.2:///databasename`

#### Using Oracle

`jdbc:tc:oracle:21-slim-faststart:///databasename`
Expand Down
25 changes: 25 additions & 0 deletions docs/modules/databases/oceanbase.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# OceanBase Module

See [Database containers](./index.md) for documentation and usage that is common to all relational database container types.

## Adding this module to your project dependencies

Add the following dependency to your `pom.xml`/`build.gradle` file:

=== "Gradle"
```groovy
testImplementation "org.testcontainers:oceanbase:{{latest_version}}"
```

=== "Maven"
```xml
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>oceanbase</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.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ nav:
- modules/databases/mssqlserver.md
- modules/databases/mysql.md
- modules/databases/neo4j.md
- modules/databases/oceanbase.md
- modules/databases/oraclefree.md
- modules/databases/oraclexe.md
- modules/databases/orientdb.md
Expand Down
8 changes: 8 additions & 0 deletions modules/oceanbase/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
description = "Testcontainers :: JDBC :: OceanBase"

dependencies {
api project(':jdbc')

testImplementation project(':jdbc-test')
testRuntimeOnly 'mysql:mysql-connector-java:8.0.33'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package org.testcontainers.oceanbase;

import org.apache.commons.lang3.StringUtils;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.utility.DockerImageName;

/**
* Testcontainers implementation for OceanBase Community Edition.
* <p>
* Supported image: {@code oceanbase/oceanbase-ce}
* <p>
* Exposed ports:
* <ul>
* <li>SQL: 2881</li>
* <li>RPC: 2882</li>
* </ul>
*/
public class OceanBaseCEContainer extends JdbcDatabaseContainer<OceanBaseCEContainer> {

static final String NAME = "oceanbasece";

static final String DOCKER_IMAGE_NAME = "oceanbase/oceanbase-ce";

private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(DOCKER_IMAGE_NAME);

private static final Integer SQL_PORT = 2881;

private static final Integer RPC_PORT = 2882;

private static final String SYSTEM_TENANT_NAME = "sys";

private static final String DEFAULT_TEST_TENANT_NAME = "test";

private static final String DEFAULT_USERNAME = "root";

private static final String DEFAULT_PASSWORD = "";

private static final String DEFAULT_DATABASE_NAME = "test";

private String tenantName = DEFAULT_TEST_TENANT_NAME;

private String driverClassName = "com.mysql.cj.jdbc.Driver";

public OceanBaseCEContainer(String dockerImageName) {
this(DockerImageName.parse(dockerImageName));
}

public OceanBaseCEContainer(DockerImageName dockerImageName) {
super(dockerImageName);
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);

addExposedPorts(SQL_PORT, RPC_PORT);
eddumelendez marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
public String getDriverClassName() {
return driverClassName;
}

@Override
public String getJdbcUrl() {
return getJdbcUrl(DEFAULT_DATABASE_NAME);
}

public String getJdbcUrl(String databaseName) {
whhe marked this conversation as resolved.
Show resolved Hide resolved
String additionalUrlParams = constructUrlParameters("?", "&");
String prefix = driverClassName.contains("mysql") ? "jdbc:mysql://" : "jdbc:oceanbase://";
return prefix + getHost() + ":" + getMappedPort(SQL_PORT) + "/" + databaseName + additionalUrlParams;
}

@Override
public String getDatabaseName() {
return DEFAULT_DATABASE_NAME;
}

@Override
public String getUsername() {
return DEFAULT_USERNAME + "@" + tenantName;
}

@Override
public String getPassword() {
return DEFAULT_PASSWORD;
}

@Override
protected String getTestQueryString() {
return "SELECT 1";
}

/**
* Set the non-system tenant to be created for testing.
*
* @param tenantName the name of tenant to be created
* @return this
*/
public OceanBaseCEContainer withTenant(String tenantName) {
if (StringUtils.isEmpty(tenantName)) {
throw new IllegalArgumentException("Tenant name cannot be null or empty");
}
if (SYSTEM_TENANT_NAME.equals(tenantName)) {
throw new IllegalArgumentException("Tenant name cannot be " + SYSTEM_TENANT_NAME);
}
this.tenantName = tenantName;
return self();
}

/**
* Set the driver class name.
*
* @param driverClassName the driver class name
* @return this
*/
public OceanBaseCEContainer withDriverClassName(String driverClassName) {
if (StringUtils.isEmpty(driverClassName)) {
throw new IllegalArgumentException("Driver class name cannot be null or empty");
}
if (!driverClassName.contains("mysql") && !driverClassName.contains("oceanbase")) {
throw new IllegalArgumentException("Driver class name should contains 'mysql' or 'oceanbase'");
}
this.driverClassName = driverClassName;
return self();
}
eddumelendez marked this conversation as resolved.
Show resolved Hide resolved

@Override
protected void configure() {
if (!DEFAULT_TEST_TENANT_NAME.equals(tenantName)) {
withEnv("OB_TENANT_NAME", tenantName);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

according to the docs, the default value for OB_TENAN_NAME is test. So we can just have

Suggested change
@Override
protected void configure() {
if (!DEFAULT_TEST_TENANT_NAME.equals(tenantName)) {
withEnv("OB_TENANT_NAME", tenantName);
}
}
@Override
protected void configure() {
withEnv("OB_TENANT_NAME", tenantName);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User can also use 'withEnv' method to set it manually, in this case if we do not check the value, there may be a mistake.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't fully understand because the default is test and the tenantName = DEFAULT_TEST_TENANT_NAME; where DEFAULT_TEST_TENANT_NAME = "test". Unless, the right one should use SYSTEM_TENANT_NAME instead

if (!SYSTEM_TENANT_NAME.equals(tenantName)) {
    withEnv("OB_TENANT_NAME", tenantName);
}

Once we resolve this comment I can proceed and merge it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm considering this situation: someone use the container class but not set the tenant name with given method withTenant, like new OceanBaseCEContainer("xxx").withEnv("OB_TENANT_NAME", "tc");. In this case, as the tenantName field is still 'test', a 'test' tenant will be created, not user defined value 'tc'.

I'm not sure if we should deal with it. WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that given there is a default value in the image itself. The withTenant method and the withEnv in configure method can be omitted. If for some reason there is a need to modify it then new OceanBaseCEContainer(...).withEnv("OB_TENANT_NAME", "something"); that would a similar experience like running docker run command. Otherwise, it would be a little bit confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tenantName field here is required to construct the username used in jdbc connection, so I did not remove the withTenant method in previous cleanup. I reconsidered about it and maybe use the default value with necessary description is better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed we were missing a test for this use case and also think a little bit more about it. In the configure method we can add getEnvMap().computeIfAbsent("OB_TENAN_NAME", k -> tenantName != null ? tenantName : DEFAULT_TENANT_NAME) or just getEnvMap().computeIfAbsent("OB_TENAN_NAME", k -> tenantName) and we can keep withTenantName. LMK if this can help to the use case described.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes you are right, we can resolve it by using the envMap, but I think it's not necessary for now.

At present no other withXXX method is provided in this container class and all env options are set by withEnv, I think it's good and clean. Lets keep it like this.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.testcontainers.oceanbase;

import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.JdbcDatabaseContainerProvider;
import org.testcontainers.utility.DockerImageName;

/**
* Factory for OceanBase Community Edition containers.
*/
public class OceanBaseCEContainerProvider extends JdbcDatabaseContainerProvider {

private static final String DEFAULT_TAG = "4.2.2";

@Override
public boolean supports(String databaseType) {
return databaseType.equals(OceanBaseCEContainer.NAME);
}

@Override
public JdbcDatabaseContainer newInstance() {
return newInstance(DEFAULT_TAG);
}

@Override
public JdbcDatabaseContainer newInstance(String tag) {
if (tag != null) {
return new OceanBaseCEContainer(DockerImageName.parse(OceanBaseCEContainer.DOCKER_IMAGE_NAME).withTag(tag));
} else {
return newInstance();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.testcontainers.oceanbase.OceanBaseCEContainerProvider
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.testcontainers.jdbc.oceanbase;
whhe marked this conversation as resolved.
Show resolved Hide resolved

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 OceanBaseJdbcDriverTest extends AbstractJDBCDriverTest {

@Parameterized.Parameters(name = "{index} - {0}")
public static Iterable<Object[]> data() {
return Arrays.asList(
new Object[][] { { "jdbc:tc:oceanbasece://hostname/databasename", EnumSet.noneOf(Options.class) } }
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.testcontainers.junit.oceanbase;
whhe marked this conversation as resolved.
Show resolved Hide resolved

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.db.AbstractContainerDatabaseTest;
import org.testcontainers.oceanbase.OceanBaseCEContainer;
import org.testcontainers.oceanbase.OceanBaseCEContainerProvider;

import java.sql.ResultSet;
import java.sql.SQLException;

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

public class SimpleOceanBaseCETest extends AbstractContainerDatabaseTest {

private static final Logger logger = LoggerFactory.getLogger(SimpleOceanBaseCETest.class);

private final OceanBaseCEContainerProvider containerProvider = new OceanBaseCEContainerProvider();

@SuppressWarnings("resource")
private OceanBaseCEContainer testContainer() {
return ((OceanBaseCEContainer) containerProvider.newInstance()).withEnv("MODE", "slim")
.withEnv("FASTBOOT", "true")
.withLogConsumer(new Slf4jLogConsumer(logger));
}

@Test
public void testSimple() throws SQLException {
try (OceanBaseCEContainer container = testContainer()) {
container.start();

ResultSet resultSet = performQuery(container, "SELECT 1");
int resultSetInt = resultSet.getInt(1);
assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1);
assertHasCorrectExposedAndLivenessCheckPorts(container);
}
}

@Test
public void testExplicitInitScript() throws SQLException {
try (OceanBaseCEContainer container = testContainer().withInitScript("init.sql")) {
container.start();

ResultSet resultSet = performQuery(container, "SELECT foo FROM bar");
String firstColumnValue = resultSet.getString(1);
assertThat(firstColumnValue).as("Value from init script should equal real value").isEqualTo("hello world");
}
}

@Test
public void testWithAdditionalUrlParamInJdbcUrl() {
try (OceanBaseCEContainer container = testContainer().withUrlParam("useSSL", "false")) {
container.start();

String jdbcUrl = container.getJdbcUrl();
assertThat(jdbcUrl).contains("?");
assertThat(jdbcUrl).contains("useSSL=false");
}
}

private void assertHasCorrectExposedAndLivenessCheckPorts(OceanBaseCEContainer container) {
int sqlPort = 2881;
int rpcPort = 2882;

assertThat(container.getExposedPorts()).containsExactlyInAnyOrder(sqlPort, rpcPort);
assertThat(container.getLivenessCheckPortNumbers())
.containsExactlyInAnyOrder(container.getMappedPort(sqlPort), container.getMappedPort(rpcPort));
}
}