diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index ab1de00f6f8..033fd19c49d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -20,6 +20,7 @@ body: - CockroachDB - Consul - Couchbase + - CrateDB - DB2 - Dynalite - Elasticsearch diff --git a/.github/ISSUE_TEMPLATE/enhancement.yaml b/.github/ISSUE_TEMPLATE/enhancement.yaml index b80fa9387da..b9d67bc7d52 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.yaml +++ b/.github/ISSUE_TEMPLATE/enhancement.yaml @@ -20,6 +20,7 @@ body: - CockroachDB - Consul - Couchbase + - CrateDB - DB2 - Dynalite - Elasticsearch diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml index 024d0414d2f..ffc1b19a6cb 100644 --- a/.github/ISSUE_TEMPLATE/feature.yaml +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -18,6 +18,7 @@ body: - Cassandra - Clickhouse - CockroachDB + - CrateDB - Consul - Couchbase - DB2 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ecf473b2d65..e78603d163f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -62,6 +62,11 @@ updates: schedule: interval: "monthly" open-pull-requests-limit: 10 + - package-ecosystem: "gradle" + directory: "/modules/cratedb" + schedule: + interval: "monthly" + open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/database-commons" schedule: diff --git a/.github/labeler.yml b/.github/labeler.yml index 55163e0d7c7..48238471563 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -19,6 +19,8 @@ - modules/consul/**/* "modules/couchbase": - modules/couchbase/**/* +"modules/cratedb": + - modules/cratedb/**/* "modules/db2": - modules/db2/**/* "modules/dynalite": diff --git a/.github/settings.yml b/.github/settings.yml index 7844182893b..7cc83a69524 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -124,6 +124,9 @@ labels: - name: modules/couchbase color: '#006b75' + - name: modules/cratedb + color: '#006b75' + - name: modules/db2 color: '#006b75' diff --git a/docs/modules/databases/cratedb.md b/docs/modules/databases/cratedb.md new file mode 100644 index 00000000000..74ec23dda00 --- /dev/null +++ b/docs/modules/databases/cratedb.md @@ -0,0 +1,25 @@ +# CrateDB 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:cratedb:{{latest_version}}" + ``` +=== "Maven" + ```xml + + org.testcontainers + cratedb + {{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/docs/modules/databases/jdbc.md b/docs/modules/databases/jdbc.md index 6cf4977ad96..020a96ff941 100644 --- a/docs/modules/databases/jdbc.md +++ b/docs/modules/databases/jdbc.md @@ -51,6 +51,10 @@ Insert `tc:` after `jdbc:` as follows. Note that the hostname, port and database `jdbc:tc:cockroach:v21.2.3:///databasename` +#### Using CrateDB + +`jdbc:tc:cratedb:5.2.3//localhost:5432/crate` + #### Using TiDB `jdbc:tc:tidb:v6.1.0:///databasename` diff --git a/mkdocs.yml b/mkdocs.yml index 8da6fa584ac..72c457b9d5f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -53,6 +53,7 @@ nav: - modules/databases/cockroachdb.md - modules/databases/couchbase.md - modules/databases/clickhouse.md + - modules/databases/cratedb.md - modules/databases/db2.md - modules/databases/dynalite.md - modules/databases/influxdb.md diff --git a/modules/cratedb/build.gradle b/modules/cratedb/build.gradle new file mode 100644 index 00000000000..d53031542f5 --- /dev/null +++ b/modules/cratedb/build.gradle @@ -0,0 +1,13 @@ +description = "Testcontainers :: JDBC :: CrateDB" + +dependencies { + annotationProcessor 'com.google.auto.service:auto-service:1.0.1' + compileOnly 'com.google.auto.service:auto-service:1.0.1' + + api project(':jdbc') + + testImplementation project(':jdbc-test') + testImplementation 'org.postgresql:postgresql:42.5.4' + + compileOnly 'org.jetbrains:annotations:24.0.0' +} diff --git a/modules/cratedb/src/main/java/org/testcontainers/cratedb/CrateDBContainer.java b/modules/cratedb/src/main/java/org/testcontainers/cratedb/CrateDBContainer.java new file mode 100644 index 00000000000..45cd03f460a --- /dev/null +++ b/modules/cratedb/src/main/java/org/testcontainers/cratedb/CrateDBContainer.java @@ -0,0 +1,116 @@ +package org.testcontainers.cratedb; + +import org.jetbrains.annotations.NotNull; +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +import java.util.Set; + +public class CrateDBContainer extends JdbcDatabaseContainer { + + static final String NAME = "cratedb"; + + static final String IMAGE = "crate"; + + static final String DEFAULT_TAG = "5.2.5"; + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("crate"); + + static final Integer CRATEDB_PG_PORT = 5432; + + static final Integer CRATEDB_HTTP_PORT = 4200; + + private String databaseName = "crate"; + + private String username = "crate"; + + private String password = "crate"; + + public CrateDBContainer(final String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + public CrateDBContainer(final DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + + this.waitStrategy = Wait.forHttp("/").forPort(CRATEDB_HTTP_PORT).forStatusCode(200); + + addExposedPort(CRATEDB_PG_PORT); + addExposedPort(CRATEDB_HTTP_PORT); + } + + /** + * @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 + public String getDriverClassName() { + return "org.postgresql.Driver"; + } + + @Override + public String getJdbcUrl() { + String additionalUrlParams = constructUrlParameters("?", "&"); + return ( + "jdbc:postgresql://" + + getHost() + + ":" + + getMappedPort(CRATEDB_PG_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 CrateDBContainer withDatabaseName(final String databaseName) { + this.databaseName = databaseName; + return self(); + } + + @Override + public CrateDBContainer withUsername(final String username) { + this.username = username; + return self(); + } + + @Override + public CrateDBContainer withPassword(final String password) { + this.password = password; + return self(); + } + + @Override + protected void waitUntilContainerStarted() { + getWaitStrategy().waitUntilReady(this); + } +} diff --git a/modules/cratedb/src/main/java/org/testcontainers/cratedb/CrateDBContainerProvider.java b/modules/cratedb/src/main/java/org/testcontainers/cratedb/CrateDBContainerProvider.java new file mode 100644 index 00000000000..0bf80f051ec --- /dev/null +++ b/modules/cratedb/src/main/java/org/testcontainers/cratedb/CrateDBContainerProvider.java @@ -0,0 +1,36 @@ +package org.testcontainers.cratedb; + +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.containers.JdbcDatabaseContainerProvider; +import org.testcontainers.jdbc.ConnectionUrl; +import org.testcontainers.utility.DockerImageName; + +/** + * Factory for CrateDB containers using PostgreSQL JDBC driver. + */ +public class CrateDBContainerProvider 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(CrateDBContainer.NAME); + } + + @Override + public JdbcDatabaseContainer newInstance() { + return newInstance(CrateDBContainer.DEFAULT_TAG); + } + + @Override + public JdbcDatabaseContainer newInstance(String tag) { + return new CrateDBContainer(DockerImageName.parse(CrateDBContainer.IMAGE).withTag(tag)); + } + + @Override + public JdbcDatabaseContainer newInstance(ConnectionUrl connectionUrl) { + return newInstanceFromConnectionUrl(connectionUrl, USER_PARAM, PASSWORD_PARAM); + } +} diff --git a/modules/cratedb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider b/modules/cratedb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider new file mode 100644 index 00000000000..ddd58ec8794 --- /dev/null +++ b/modules/cratedb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.cratedb.CrateDBContainerProvider diff --git a/modules/cratedb/src/test/java/org/testcontainers/CrateDBTestImages.java b/modules/cratedb/src/test/java/org/testcontainers/CrateDBTestImages.java new file mode 100644 index 00000000000..5025d409839 --- /dev/null +++ b/modules/cratedb/src/test/java/org/testcontainers/CrateDBTestImages.java @@ -0,0 +1,7 @@ +package org.testcontainers; + +import org.testcontainers.utility.DockerImageName; + +public interface CrateDBTestImages { + DockerImageName CRATEDB_TEST_IMAGE = DockerImageName.parse("crate:5.2.5"); +} diff --git a/modules/cratedb/src/test/java/org/testcontainers/jdbc/cratedb/CrateDBJDBCDriverTest.java b/modules/cratedb/src/test/java/org/testcontainers/jdbc/cratedb/CrateDBJDBCDriverTest.java new file mode 100644 index 00000000000..54ef9704ac5 --- /dev/null +++ b/modules/cratedb/src/test/java/org/testcontainers/jdbc/cratedb/CrateDBJDBCDriverTest.java @@ -0,0 +1,21 @@ +package org.testcontainers.jdbc.cratedb; + +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 CrateDBJDBCDriverTest extends AbstractJDBCDriverTest { + + @Parameterized.Parameters(name = "{index} - {0}") + public static Iterable data() { + return Arrays.asList( + new Object[][] { + { "jdbc:tc:cratedb:5.2.3://hostname/crate?user=crate&password=somepwd", EnumSet.noneOf(Options.class) }, + } + ); + } +} diff --git a/modules/cratedb/src/test/java/org/testcontainers/junit/cratedb/SimpleCrateDBTest.java b/modules/cratedb/src/test/java/org/testcontainers/junit/cratedb/SimpleCrateDBTest.java new file mode 100644 index 00000000000..ea7bb1a4f10 --- /dev/null +++ b/modules/cratedb/src/test/java/org/testcontainers/junit/cratedb/SimpleCrateDBTest.java @@ -0,0 +1,67 @@ +package org.testcontainers.junit.cratedb; + +import org.junit.Test; +import org.testcontainers.CrateDBTestImages; +import org.testcontainers.cratedb.CrateDBContainer; +import org.testcontainers.db.AbstractContainerDatabaseTest; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.logging.Level; +import java.util.logging.LogManager; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SimpleCrateDBTest extends AbstractContainerDatabaseTest { + static { + // Postgres JDBC driver uses JUL; disable it to avoid annoying, irrelevant, stderr logs during connection testing + LogManager.getLogManager().getLogger("").setLevel(Level.OFF); + } + + @Test + public void testSimple() throws SQLException { + try (CrateDBContainer cratedb = new CrateDBContainer(CrateDBTestImages.CRATEDB_TEST_IMAGE)) { + cratedb.start(); + + ResultSet resultSet = performQuery(cratedb, "SELECT 1"); + int resultSetInt = resultSet.getInt(1); + assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); + assertHasCorrectExposedAndLivenessCheckPorts(cratedb); + } + } + + @Test + public void testCommandOverride() throws SQLException { + try ( + CrateDBContainer cratedb = new CrateDBContainer(CrateDBTestImages.CRATEDB_TEST_IMAGE) + .withCommand("crate -C cluster.name=testcontainers") + ) { + cratedb.start(); + + ResultSet resultSet = performQuery(cratedb, "select name from sys.cluster"); + String result = resultSet.getString(1); + assertThat(result).as("cluster name should be overriden").isEqualTo("testcontainers"); + } + } + + @Test + public void testExplicitInitScript() throws SQLException { + try ( + CrateDBContainer cratedb = new CrateDBContainer(CrateDBTestImages.CRATEDB_TEST_IMAGE) + .withInitScript("somepath/init_cratedb.sql") + ) { + cratedb.start(); + + ResultSet resultSet = performQuery(cratedb, "SELECT foo FROM bar"); + + String firstColumnValue = resultSet.getString(1); + assertThat(firstColumnValue).as("Value from init script should equal real value").isEqualTo("hello world"); + } + } + + private void assertHasCorrectExposedAndLivenessCheckPorts(CrateDBContainer cratedb) { + assertThat(cratedb.getExposedPorts()).containsExactly(5432, 4200); + assertThat(cratedb.getLivenessCheckPortNumbers()) + .containsExactlyInAnyOrder(cratedb.getMappedPort(5432), cratedb.getMappedPort(4200)); + } +} diff --git a/modules/cratedb/src/test/resources/logback-test.xml b/modules/cratedb/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/cratedb/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/cratedb/src/test/resources/somepath/init_cratedb.sql b/modules/cratedb/src/test/resources/somepath/init_cratedb.sql new file mode 100644 index 00000000000..4faa1ede65c --- /dev/null +++ b/modules/cratedb/src/test/resources/somepath/init_cratedb.sql @@ -0,0 +1,6 @@ +CREATE TABLE bar ( + foo STRING +); + +INSERT INTO bar (foo) VALUES ('hello world'); +REFRESH TABLE bar;