From 86ed2cbf0ee39405ce2e428f52371fea67a4475b Mon Sep 17 00:00:00 2001 From: Maxim Korolyov Date: Tue, 2 Jan 2024 08:23:25 +0100 Subject: [PATCH 1/5] add scylladb module --- .github/ISSUE_TEMPLATE/bug_report.yaml | 1 + .github/ISSUE_TEMPLATE/enhancement.yaml | 1 + .github/ISSUE_TEMPLATE/feature.yaml | 1 + .github/dependabot.yml | 5 + .github/labeler.yml | 4 + docs/modules/databases/scylladb.md | 28 +++ modules/scylladb/build.gradle | 15 ++ .../containers/ScyllaDBContainer.java | 194 ++++++++++++++++++ .../delegate/ScyllaDBDatabaseDelegate.java | 69 +++++++ .../wait/ScyllaDBQueryWaitStrategy.java | 43 ++++ .../containers/ScyllaDBContainerTest.java | 117 +++++++++++ .../containers/ScyllaDBDriver4Test.java | 37 ++++ .../containers/ScyllaDBServer5Test.java | 35 ++++ .../scylladb/src/test/resources/initial.cql | 7 + .../src/test/resources/logback-test.xml | 16 ++ .../scylla.yaml | 62 ++++++ 16 files changed, 635 insertions(+) create mode 100644 docs/modules/databases/scylladb.md create mode 100644 modules/scylladb/build.gradle create mode 100644 modules/scylladb/src/main/java/org/testcontainers/containers/ScyllaDBContainer.java create mode 100644 modules/scylladb/src/main/java/org/testcontainers/containers/delegate/ScyllaDBDatabaseDelegate.java create mode 100644 modules/scylladb/src/main/java/org/testcontainers/containers/wait/ScyllaDBQueryWaitStrategy.java create mode 100644 modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBContainerTest.java create mode 100644 modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBDriver4Test.java create mode 100644 modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBServer5Test.java create mode 100644 modules/scylladb/src/test/resources/initial.cql create mode 100644 modules/scylladb/src/test/resources/logback-test.xml create mode 100644 modules/scylladb/src/test/resources/scylla-test-configuration-example/scylla.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 135c8323866..5992ccdc46c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -47,6 +47,7 @@ body: - QuestDB - RabbitMQ - Redpanda + - ScyllaDB - Selenium - Solace - Solr diff --git a/.github/ISSUE_TEMPLATE/enhancement.yaml b/.github/ISSUE_TEMPLATE/enhancement.yaml index d77b4ce07a2..48e5813cc89 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.yaml +++ b/.github/ISSUE_TEMPLATE/enhancement.yaml @@ -47,6 +47,7 @@ body: - QuestDB - RabbitMQ - Redpanda + - ScyllaDB - Selenium - Solace - Solr diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml index 65a27e5f99a..5a2097bb62f 100644 --- a/.github/ISSUE_TEMPLATE/feature.yaml +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -47,6 +47,7 @@ body: - Pulsar - RabbitMQ - Redpanda + - ScyllaDB - Selenium - Solace - Solr diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3cb3e0fd222..3569b8b5cc2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -246,6 +246,11 @@ updates: schedule: interval: "weekly" open-pull-requests-limit: 10 + - package-ecosystem: "gradle" + directory: "/modules/scylladb" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/selenium" schedule: diff --git a/.github/labeler.yml b/.github/labeler.yml index eec54b67829..73df03d65f5 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -156,6 +156,10 @@ - changed-files: - any-glob-to-any-file: - modules/redpanda/**/* +"modules/scylladb": + - changed-files: + - any-glob-to-any-file: + - modules/scylladb/**/* "modules/selenium": - changed-files: - any-glob-to-any-file: diff --git a/docs/modules/databases/scylladb.md b/docs/modules/databases/scylladb.md new file mode 100644 index 00000000000..f2f6d2b6a52 --- /dev/null +++ b/docs/modules/databases/scylladb.md @@ -0,0 +1,28 @@ +# ScyllaDB Module + +## Usage example + +This example connects to the ScyllaDB Cluster, creates a keyspaces and asserts that is has been created. + + +[Building CqlSession](../../../modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBDriver4Test.java) inside_block:scylladb + + +## Adding this module to your project dependencies + +Add the following dependency to your `pom.xml`/`build.gradle` file: + +=== "Gradle" + ```groovy + testImplementation "org.testcontainers:scylladb:{{latest_version}}" + ``` + +=== "Maven" + ```xml + + org.testcontainers + scylladb + {{latest_version}} + test + + ``` diff --git a/modules/scylladb/build.gradle b/modules/scylladb/build.gradle new file mode 100644 index 00000000000..acb45635166 --- /dev/null +++ b/modules/scylladb/build.gradle @@ -0,0 +1,15 @@ +description = "Testcontainers :: ScyllaDB" + +configurations.all { + resolutionStrategy { + force 'io.dropwizard.metrics:metrics-core:3.2.6' + } +} + +dependencies { + api project(":database-commons") + api "com.scylladb:java-driver-core:4.15.0.0" + api "com.datastax.cassandra:cassandra-driver-core:3.10.0" + + testImplementation 'org.assertj:assertj-core:3.24.2' +} diff --git a/modules/scylladb/src/main/java/org/testcontainers/containers/ScyllaDBContainer.java b/modules/scylladb/src/main/java/org/testcontainers/containers/ScyllaDBContainer.java new file mode 100644 index 00000000000..e85eaacbdad --- /dev/null +++ b/modules/scylladb/src/main/java/org/testcontainers/containers/ScyllaDBContainer.java @@ -0,0 +1,194 @@ +package org.testcontainers.containers; + +import com.github.dockerjava.api.command.InspectContainerResponse; +import org.apache.commons.io.IOUtils; +import org.testcontainers.containers.delegate.ScyllaDBDatabaseDelegate; +import org.testcontainers.delegate.DatabaseDelegate; +import org.testcontainers.ext.ScriptUtils; +import org.testcontainers.ext.ScriptUtils.ScriptLoadException; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +import javax.script.ScriptException; + +/** + * Testcontainers implementation for ScyllaDB. + *

+ * Supported image: {@code scylladb} + *

+ * Exposed ports: 9042 + */ +public class ScyllaDBContainer> extends GenericContainer { + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("scylladb/scylla:5.2.9"); + + public static final Integer CQL_PORT = 9042; + + private static final String DEFAULT_LOCAL_DATACENTER = "datacenter1"; + + private static final String CONTAINER_CONFIG_LOCATION = "/etc/scylla"; + + private static final String USERNAME = "scylladb"; + + private static final String PASSWORD = "scylladb"; + + private String configLocation; + + private String initScriptPath; + + private boolean enableJmxReporting; + + public ScyllaDBContainer(String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + public ScyllaDBContainer(DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + + addExposedPort(CQL_PORT); + this.enableJmxReporting = false; + + withEnv("CASSANDRA_SNITCH", "GossipingPropertyFileSnitch"); + withEnv("JVM_OPTS", "-Dcassandra.skip_wait_for_gossip_to_settle=0 -Dcassandra.initial_token=0"); + withEnv("HEAP_NEWSIZE", "128M"); + withEnv("MAX_HEAP_SIZE", "1024M"); + withEnv("SCYLLADB_ENDPOINT_SNITCH", "GossipingPropertyFileSnitch"); + withEnv("SCYLLADB_DC", DEFAULT_LOCAL_DATACENTER); + } + + @Override + protected void configure() { + optionallyMapResourceParameterAsVolume(CONTAINER_CONFIG_LOCATION, configLocation); + } + + @Override + protected void containerIsStarted(InspectContainerResponse containerInfo) { + runInitScriptIfRequired(); + } + + /** + * Load init script content and apply it to the database if initScriptPath is set + */ + private void runInitScriptIfRequired() { + if (initScriptPath != null) { + try { + URL resource = Thread.currentThread().getContextClassLoader().getResource(initScriptPath); + if (resource == null) { + logger().warn("Could not load classpath init script: {}", initScriptPath); + throw new ScriptLoadException( + "Could not load classpath init script: " + initScriptPath + ". Resource not found." + ); + } + String cql = IOUtils.toString(resource, StandardCharsets.UTF_8); + DatabaseDelegate databaseDelegate = new ScyllaDBDatabaseDelegate(this); + ScriptUtils.executeDatabaseScript(databaseDelegate, initScriptPath, cql); + } catch (IOException e) { + logger().warn("Could not load classpath init script: {}", initScriptPath); + throw new ScriptLoadException("Could not load classpath init script: " + initScriptPath, e); + } catch (ScriptException e) { + logger().error("Error while executing init script: {}", initScriptPath, e); + throw new ScriptUtils.UncategorizedScriptException( + "Error while executing init script: " + initScriptPath, + e + ); + } + } + } + + /** + * Map (effectively replace) directory in Docker with the content of resourceLocation if resource location is not null + * + * Protected to allow for changing implementation by extending the class + * + * @param pathNameInContainer path in docker + * @param resourceLocation relative classpath to resource + */ + protected void optionallyMapResourceParameterAsVolume(String pathNameInContainer, String resourceLocation) { + Optional + .ofNullable(resourceLocation) + .map(MountableFile::forClasspathResource) + .ifPresent(mountableFile -> withCopyFileToContainer(mountableFile, pathNameInContainer)); + } + + /** + * Initialize ScyllaDB with the custom overridden ScyllaDB configuration + *

+ * Be aware, that Docker effectively replaces all /etc/sylladb content with the content of config location, so if + * scylladb.yaml in configLocation is absent or corrupted, then ScyllaDB just won't launch + * + * @param configLocation relative classpath with the directory that contains cassandra.yaml and other configuration files + */ + public SELF withConfigurationOverride(String configLocation) { + this.configLocation = configLocation; + return self(); + } + + /** + * Initialize ScyllaDB with init CQL script + *

+ * CQL script will be applied after container is started (see using WaitStrategy) + * + * @param initScriptPath relative classpath resource + */ + public SELF withInitScript(String initScriptPath) { + this.initScriptPath = initScriptPath; + return self(); + } + + /** + * Initialize ScyllaDB client with JMX reporting enabled or disabled + */ + public SELF withJmxReporting(boolean enableJmxReporting) { + this.enableJmxReporting = enableJmxReporting; + return self(); + } + + /** + * Get username + * + * By default ScyllaDB has authenticator: AllowAllAuthenticator in scylladb.yaml + * If username and password need to be used, then authenticator should be set as PasswordAuthenticator + * (through custom ScyllaDB configuration) and through CQL with default scylladb-scylladb credentials + * user management should be modified + */ + public String getUsername() { + return USERNAME; + } + + /** + * Get password + * + * By default ScyllaDB has authenticator: AllowAllAuthenticator in scylladb.yaml + * If username and password need to be used, then authenticator should be set as PasswordAuthenticator + * (through custom Cassandra configuration) and through CQL with default scylladb-scylladb credentials + * user management should be modified + */ + public String getPassword() { + return PASSWORD; + } + + /** + * Retrieve an {@link InetSocketAddress} for connecting to the ScyllaDB container via the driver. + * + * @return A InetSocketAddrss representation of this ScyllaDB container's host and port. + */ + public InetSocketAddress getContactPoint() { + return new InetSocketAddress(getHost(), getMappedPort(CQL_PORT)); + } + + /** + * Retrieve the Local Datacenter for connecting to the ScyllaDB container via the driver. + * + * @return The configured local Datacenter name. + */ + public String getLocalDatacenter() { + return getEnvMap().getOrDefault("SCYLLADB_DC", DEFAULT_LOCAL_DATACENTER); + } +} diff --git a/modules/scylladb/src/main/java/org/testcontainers/containers/delegate/ScyllaDBDatabaseDelegate.java b/modules/scylladb/src/main/java/org/testcontainers/containers/delegate/ScyllaDBDatabaseDelegate.java new file mode 100644 index 00000000000..255ec5d90c8 --- /dev/null +++ b/modules/scylladb/src/main/java/org/testcontainers/containers/delegate/ScyllaDBDatabaseDelegate.java @@ -0,0 +1,69 @@ +package org.testcontainers.containers.delegate; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import org.slf4j.Logger; +import org.testcontainers.containers.ContainerState; +import org.testcontainers.containers.ScyllaDBContainer; +import org.testcontainers.delegate.AbstractDatabaseDelegate; +import org.testcontainers.exception.ConnectionCreationException; +import org.testcontainers.ext.ScriptUtils.ScriptStatementFailedException; +import org.testcontainers.utility.DockerLoggerFactory; + +import java.net.InetSocketAddress; + +public class ScyllaDBDatabaseDelegate extends AbstractDatabaseDelegate { + + public ScyllaDBDatabaseDelegate(ContainerState container) { + this.container = container; + } + + protected Logger logger() { + return DockerLoggerFactory.getLogger(container.getCurrentContainerInfo().getName()); + } + + private final ContainerState container; + + @Override + protected CqlSession createNewConnection() { + try { + return CqlSession + .builder() + .addContactPoint( + new InetSocketAddress(container.getHost(), container.getMappedPort(ScyllaDBContainer.CQL_PORT)) + ) + .withLocalDatacenter("datacenter1") + .build(); + } catch (DriverException e) { + throw new ConnectionCreationException("Could not obtain cassandra connection", e); + } + } + + @Override + public void execute( + String statement, + String scriptPath, + int lineNumber, + boolean continueOnError, + boolean ignoreFailedDrops + ) { + try { + ResultSet result = getConnection().execute(statement); + if (!result.wasApplied()) { + throw new ScriptStatementFailedException(statement, lineNumber, scriptPath); + } + } catch (DriverException e) { + throw new ScriptStatementFailedException(statement, lineNumber, scriptPath, e); + } + } + + @Override + protected void closeConnectionQuietly(CqlSession session) { + try { + session.close(); + } catch (Exception e) { + logger().error("Could not close cassandra connection", e); + } + } +} diff --git a/modules/scylladb/src/main/java/org/testcontainers/containers/wait/ScyllaDBQueryWaitStrategy.java b/modules/scylladb/src/main/java/org/testcontainers/containers/wait/ScyllaDBQueryWaitStrategy.java new file mode 100644 index 00000000000..c72822932ff --- /dev/null +++ b/modules/scylladb/src/main/java/org/testcontainers/containers/wait/ScyllaDBQueryWaitStrategy.java @@ -0,0 +1,43 @@ +package org.testcontainers.containers.wait; + +import org.rnorth.ducttape.TimeoutException; +import org.testcontainers.containers.ContainerLaunchException; +import org.testcontainers.containers.delegate.ScyllaDBDatabaseDelegate; +import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; +import org.testcontainers.delegate.DatabaseDelegate; + +import java.util.concurrent.TimeUnit; + +import static org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess; + +/** + * Waits until ScyllaDB returns its version + */ +public class ScyllaDBQueryWaitStrategy extends AbstractWaitStrategy { + + private static final String SELECT_VERSION_QUERY = "SELECT release_version FROM system.local"; + + private static final String TIMEOUT_ERROR = "Timed out waiting for ScyllaDB to be accessible for query execution"; + + @Override + protected void waitUntilReady() { + // execute select version query until success or timeout + try { + retryUntilSuccess( + (int) startupTimeout.getSeconds(), + TimeUnit.SECONDS, + () -> { + getRateLimiter() + .doWhenReady(() -> { + try (DatabaseDelegate databaseDelegate = new ScyllaDBDatabaseDelegate(waitStrategyTarget)) { + databaseDelegate.execute(SELECT_VERSION_QUERY, "", 1, false, false); + } + }); + return true; + } + ); + } catch (TimeoutException e) { + throw new ContainerLaunchException(TIMEOUT_ERROR); + } + } +} diff --git a/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBContainerTest.java b/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBContainerTest.java new file mode 100644 index 00000000000..61aceaa3ea6 --- /dev/null +++ b/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBContainerTest.java @@ -0,0 +1,117 @@ +package org.testcontainers.containers; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.testcontainers.containers.wait.ScyllaDBQueryWaitStrategy; +import org.testcontainers.utility.DockerImageName; + +import static org.assertj.core.api.Assertions.assertThat; + +@Slf4j +public class ScyllaDBContainerTest { + + private static final DockerImageName SCYLLADB_IMAGE = DockerImageName.parse("scylladb/scylla:5.2.9"); + + private static final String TEST_CLUSTER_NAME_IN_CONF = "Test Cluster Integration Test"; + + private static final String BASIC_QUERY = "SELECT release_version FROM system.local"; + + @Test + public void testSimple() { + try (ScyllaDBContainer scylladbContainer = new ScyllaDBContainer<>(SCYLLADB_IMAGE)) { + scylladbContainer.start(); + ResultSet resultSet = performQuery(scylladbContainer, BASIC_QUERY); + assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); + assertThat(resultSet.one().getString(0)).as("Result set has release_version").isNotNull(); + } + } + + @Test + public void testSpecificVersion() { + String scyllaDBReportedVersion = "3.0.8"; + try ( + ScyllaDBContainer ScyllaDBContainer = new ScyllaDBContainer<>(SCYLLADB_IMAGE) + ) { + ScyllaDBContainer.start(); + ResultSet resultSet = performQuery(ScyllaDBContainer, BASIC_QUERY); + assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); + assertThat(resultSet.one().getString(0)).as("ScyllaDB has right version").isEqualTo(scyllaDBReportedVersion); + } + } + + @Test + public void testConfigurationOverride() { + try ( + ScyllaDBContainer ScyllaDBContainer = new ScyllaDBContainer<>(SCYLLADB_IMAGE) + .withConfigurationOverride("scylla-test-configuration-example") + ) { + ScyllaDBContainer.start(); + ResultSet resultSet = performQuery(ScyllaDBContainer, "SELECT cluster_name FROM system.local"); + assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); + assertThat(resultSet.one().getString(0)) + .as("ScyllaDB configuration is overridden") + .isEqualTo(TEST_CLUSTER_NAME_IN_CONF); + } + } + + @Test(expected = ContainerLaunchException.class) + public void testEmptyConfigurationOverride() { + try ( + ScyllaDBContainer ScyllaDBContainer = new ScyllaDBContainer<>(SCYLLADB_IMAGE) + .withConfigurationOverride("scylladb-empty-configuration") + ) { + ScyllaDBContainer.start(); + } + } + + @Test + public void testInitScript() { + try ( + ScyllaDBContainer ScyllaDBContainer = new ScyllaDBContainer<>(SCYLLADB_IMAGE) + .withInitScript("initial.cql") + ) { + ScyllaDBContainer.start(); + testInitScript(ScyllaDBContainer); + } + } + + @Test + public void testScyllaDBQueryWaitStrategy() { + try ( + ScyllaDBContainer scyllaDBContainer = new ScyllaDBContainer<>(SCYLLADB_IMAGE) + .waitingFor(new ScyllaDBQueryWaitStrategy()) + ) { + scyllaDBContainer.start(); + ResultSet resultSet = performQuery(scyllaDBContainer, BASIC_QUERY); + assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); + } + } + + private void testInitScript(ScyllaDBContainer ScyllaDBContainer) { + ResultSet resultSet = performQuery(ScyllaDBContainer, "SELECT * FROM keySpaceTest.catalog_category"); + assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); + Row row = resultSet.one(); + assertThat(row.getLong(0)).as("Inserted row is in expected state").isEqualTo(1); + assertThat(row.getString(1)).as("Inserted row is in expected state").isEqualTo("test_category"); + } + + private ResultSet performQuery(ScyllaDBContainer ScyllaDBContainer, String cql) { + Cluster explicitCluster = Cluster + .builder() + .addContactPoint(ScyllaDBContainer.getHost()) + .withPort(ScyllaDBContainer.getMappedPort(ScyllaDBContainer.CQL_PORT)) + .build(); + return performQuery(explicitCluster, cql); + } + + private ResultSet performQuery(Cluster cluster, String cql) { + try (Cluster closeableCluster = cluster) { + Session session = closeableCluster.newSession(); + return session.execute(cql); + } + } +} diff --git a/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBDriver4Test.java b/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBDriver4Test.java new file mode 100644 index 00000000000..3953a1e1118 --- /dev/null +++ b/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBDriver4Test.java @@ -0,0 +1,37 @@ +package org.testcontainers.containers; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; +import org.junit.Rule; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ScyllaDBDriver4Test { + + @Rule + public ScyllaDBContainer scyllaDB = new ScyllaDBContainer<>("scylladb/scylla:5.2.9"); + + @Test + public void testCassandraGetContactPoint() { + try ( + // cassandra { + CqlSession session = CqlSession + .builder() + .addContactPoint(this.scyllaDB.getContactPoint()) + .withLocalDatacenter(this.scyllaDB.getLocalDatacenter()) + .build() + // } + ) { + session.execute( + "CREATE KEYSPACE IF NOT EXISTS test WITH replication = \n" + + "{'class':'SimpleStrategy','replication_factor':'1'};" + ); + + KeyspaceMetadata keyspace = session.getMetadata().getKeyspaces().get(CqlIdentifier.fromCql("test")); + + assertThat(keyspace).as("keyspace created").isNotNull(); + } + } +} diff --git a/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBServer5Test.java b/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBServer5Test.java new file mode 100644 index 00000000000..f6959744db7 --- /dev/null +++ b/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBServer5Test.java @@ -0,0 +1,35 @@ +package org.testcontainers.containers; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; +import org.junit.Rule; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ScyllaDBServer5Test { + + @Rule + public ScyllaDBContainer scylladb = new ScyllaDBContainer<>("scylladb/scylla:5.2.9"); + + @Test + public void testScyllaDBGetContactPoint() { + try ( + CqlSession session = CqlSession + .builder() + .addContactPoint(this.scylladb.getContactPoint()) + .withLocalDatacenter(this.scylladb.getLocalDatacenter()) + .build() + ) { + session.execute( + "CREATE KEYSPACE IF NOT EXISTS test WITH replication = \n" + + "{'class':'SimpleStrategy','replication_factor':'1'};" + ); + + KeyspaceMetadata keyspace = session.getMetadata().getKeyspaces().get(CqlIdentifier.fromCql("test")); + + assertThat(keyspace).as("test keyspace created").isNotNull(); + } + } +} diff --git a/modules/scylladb/src/test/resources/initial.cql b/modules/scylladb/src/test/resources/initial.cql new file mode 100644 index 00000000000..2caad3746f8 --- /dev/null +++ b/modules/scylladb/src/test/resources/initial.cql @@ -0,0 +1,7 @@ +CREATE KEYSPACE keySpaceTest WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 1}; + +USE keySpaceTest; + +CREATE TABLE catalog_category (id bigint primary key, name text); + +INSERT INTO catalog_category (id, name) VALUES (1, 'test_category'); \ No newline at end of file diff --git a/modules/scylladb/src/test/resources/logback-test.xml b/modules/scylladb/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/scylladb/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/scylladb/src/test/resources/scylla-test-configuration-example/scylla.yaml b/modules/scylladb/src/test/resources/scylla-test-configuration-example/scylla.yaml new file mode 100644 index 00000000000..086ae07239f --- /dev/null +++ b/modules/scylladb/src/test/resources/scylla-test-configuration-example/scylla.yaml @@ -0,0 +1,62 @@ +# ScyllaDB storage config YAML + +# NOTE: +# See https://opensource.docs.scylladb.com/stable/operating-scylla/admin.html for +# full explanations of configuration directives +# /NOTE + +# The name of the cluster. This is mainly used to prevent machines in +# one logical cluster from joining another. +cluster_name: 'Test Cluster Integration Test' + +# This defines the number of tokens randomly assigned to this node on the ring +# The more tokens, relative to other nodes, the larger the proportion of data +# that this node will store. You probably want all nodes to have the same number +# of tokens assuming they have equal hardware capability. +num_tokens: 256 + +# Directory where Scylla should store data on disk. +data_file_directories: + - /var/lib/scylla/data + +# commit log. when running on magnetic HDD, this should be a +# separate spindle than the data directories. +commitlog_directory: /var/lib/scylla/commitlog + +# schema commit log. A special commitlog instance +# used for schema and system tables. +# When running on magnetic HDD, this should be a +# separate spindle than the data directories. +# schema_commitlog_directory: /var/lib/scylla/commitlog/schema + +# seed_provider class_name is saved for future use. +# A seed address is mandatory. +seed_provider: + # The addresses of hosts that will serve as contact points for the joining node. + # It allows the node to discover the cluster ring topology on startup (when + # joining the cluster). + # Once the node has joined the cluster, the seed list has no function. + - class_name: org.apache.cassandra.locator.SimpleSeedProvider + parameters: + # In a new cluster, provide the address of the first node. + # In an existing cluster, specify the address of at least one existing node. + # If you specify addresses of more than one node, use a comma to separate them. + # For example: ",," + - seeds: "127.0.0.1" + +# Address or interface to bind to and tell other Scylla nodes to connect to. +# You _must_ change this if you want multiple nodes to be able to communicate! +# +# Setting listen_address to 0.0.0.0 is always wrong. +listen_address: localhost + +# Address to broadcast to other Scylla nodes +# Leaving this blank will set it to the same value as listen_address +# broadcast_address: 1.2.3.4 + +# port for the CQL native transport to listen for clients on +# For security reasons, you should not expose this port to the internet. Firewall it if needed. +native_transport_port: 9042 + +# Uncomment to enable experimental features +# experimental: true From bc7eb8be3952d7b9526e902994255528a469ecdc Mon Sep 17 00:00:00 2001 From: Maxim Korolyov Date: Tue, 16 Jan 2024 09:25:03 +0100 Subject: [PATCH 2/5] Update docs/modules/databases/scylladb.md fix typos in docs Co-authored-by: Yaniv Kaul --- docs/modules/databases/scylladb.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/databases/scylladb.md b/docs/modules/databases/scylladb.md index f2f6d2b6a52..e3024eb451c 100644 --- a/docs/modules/databases/scylladb.md +++ b/docs/modules/databases/scylladb.md @@ -2,7 +2,7 @@ ## Usage example -This example connects to the ScyllaDB Cluster, creates a keyspaces and asserts that is has been created. +This example connects to a ScyllaDB Cluster, creates a keyspace and asserts that is has been created. [Building CqlSession](../../../modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBDriver4Test.java) inside_block:scylladb From 35ea1c2a91596bb924a20cef29537ad61af2734c Mon Sep 17 00:00:00 2001 From: Maxim Korolyov Date: Tue, 16 Jan 2024 10:29:20 +0100 Subject: [PATCH 3/5] remove com.datastax.cassandra:cassandra-driver-core from deps --- modules/scylladb/build.gradle | 1 - .../containers/ScyllaDBContainerTest.java | 32 +++++++++---------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/modules/scylladb/build.gradle b/modules/scylladb/build.gradle index acb45635166..b3708ddf458 100644 --- a/modules/scylladb/build.gradle +++ b/modules/scylladb/build.gradle @@ -9,7 +9,6 @@ configurations.all { dependencies { api project(":database-commons") api "com.scylladb:java-driver-core:4.15.0.0" - api "com.datastax.cassandra:cassandra-driver-core:3.10.0" testImplementation 'org.assertj:assertj-core:3.24.2' } diff --git a/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBContainerTest.java b/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBContainerTest.java index 61aceaa3ea6..fc3babd9427 100644 --- a/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBContainerTest.java +++ b/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBContainerTest.java @@ -1,15 +1,17 @@ package org.testcontainers.containers; -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.Row; -import com.datastax.driver.core.Session; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.Row; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.testcontainers.containers.wait.ScyllaDBQueryWaitStrategy; import org.testcontainers.utility.DockerImageName; +import java.net.InetSocketAddress; + import static org.assertj.core.api.Assertions.assertThat; +import static org.testcontainers.containers.ScyllaDBContainer.CQL_PORT; @Slf4j public class ScyllaDBContainerTest { @@ -100,18 +102,16 @@ private void testInitScript(ScyllaDBContainer ScyllaDBContainer) { } private ResultSet performQuery(ScyllaDBContainer ScyllaDBContainer, String cql) { - Cluster explicitCluster = Cluster - .builder() - .addContactPoint(ScyllaDBContainer.getHost()) - .withPort(ScyllaDBContainer.getMappedPort(ScyllaDBContainer.CQL_PORT)) - .build(); - return performQuery(explicitCluster, cql); - } + CqlSession session = CqlSession. + builder(). + addContactPoint( + new InetSocketAddress( + ScyllaDBContainer.getHost(), + ScyllaDBContainer.getMappedPort(CQL_PORT) + )). + withLocalDatacenter("datacenter1"). + build(); - private ResultSet performQuery(Cluster cluster, String cql) { - try (Cluster closeableCluster = cluster) { - Session session = closeableCluster.newSession(); - return session.execute(cql); - } + return session.execute(cql); } } From 1805c16854890eedace5038af1a31b9c79285fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Tue, 17 Dec 2024 17:26:20 -0600 Subject: [PATCH 4/5] Polish --- docs/modules/databases/scylladb.md | 30 ++- modules/scylladb/build.gradle | 7 +- .../containers/ScyllaDBContainer.java | 194 ------------------ .../delegate/ScyllaDBDatabaseDelegate.java | 69 ------- .../wait/ScyllaDBQueryWaitStrategy.java | 43 ---- .../scylladb/ScyllaDBContainer.java | 83 ++++++++ .../containers/ScyllaDBContainerTest.java | 117 ----------- .../containers/ScyllaDBDriver4Test.java | 37 ---- .../containers/ScyllaDBServer5Test.java | 35 ---- .../scylladb/ScyllaDBContainerTest.java | 107 ++++++++++ 10 files changed, 217 insertions(+), 505 deletions(-) delete mode 100644 modules/scylladb/src/main/java/org/testcontainers/containers/ScyllaDBContainer.java delete mode 100644 modules/scylladb/src/main/java/org/testcontainers/containers/delegate/ScyllaDBDatabaseDelegate.java delete mode 100644 modules/scylladb/src/main/java/org/testcontainers/containers/wait/ScyllaDBQueryWaitStrategy.java create mode 100644 modules/scylladb/src/main/java/org/testcontainers/scylladb/ScyllaDBContainer.java delete mode 100644 modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBContainerTest.java delete mode 100644 modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBDriver4Test.java delete mode 100644 modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBServer5Test.java create mode 100644 modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java diff --git a/docs/modules/databases/scylladb.md b/docs/modules/databases/scylladb.md index e3024eb451c..825f0d515db 100644 --- a/docs/modules/databases/scylladb.md +++ b/docs/modules/databases/scylladb.md @@ -1,11 +1,33 @@ -# ScyllaDB Module +# ScyllaDB -## Usage example +Testcontainers module for [ScyllaDB](https://hub.docker.com/r/scylladb/scylla) -This example connects to a ScyllaDB Cluster, creates a keyspace and asserts that is has been created. +## ScyllaDB's usage examples + +You can start a ScyllaDB container instance from any Java application by using: + + +[Create container](../../../modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java) inside_block:container + + +### Building CqlSession + + +[Using CQL port](../../../modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java) inside_block:session + + + +[Using Shard Awareness port](../../../modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java) inside_block:shardAwarenessSession + + +### Alternator + + +[Enabling Alternator](../../../modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java) inside_block:alternator + -[Building CqlSession](../../../modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBDriver4Test.java) inside_block:scylladb +[DynamoDbClient with Alternator](../../../modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java) inside_block:dynamodDbClient ## Adding this module to your project dependencies diff --git a/modules/scylladb/build.gradle b/modules/scylladb/build.gradle index b3708ddf458..069a79cf36a 100644 --- a/modules/scylladb/build.gradle +++ b/modules/scylladb/build.gradle @@ -1,14 +1,9 @@ description = "Testcontainers :: ScyllaDB" -configurations.all { - resolutionStrategy { - force 'io.dropwizard.metrics:metrics-core:3.2.6' - } -} - dependencies { api project(":database-commons") api "com.scylladb:java-driver-core:4.15.0.0" testImplementation 'org.assertj:assertj-core:3.24.2' + testImplementation 'software.amazon.awssdk:dynamodb:2.28.6' } diff --git a/modules/scylladb/src/main/java/org/testcontainers/containers/ScyllaDBContainer.java b/modules/scylladb/src/main/java/org/testcontainers/containers/ScyllaDBContainer.java deleted file mode 100644 index e85eaacbdad..00000000000 --- a/modules/scylladb/src/main/java/org/testcontainers/containers/ScyllaDBContainer.java +++ /dev/null @@ -1,194 +0,0 @@ -package org.testcontainers.containers; - -import com.github.dockerjava.api.command.InspectContainerResponse; -import org.apache.commons.io.IOUtils; -import org.testcontainers.containers.delegate.ScyllaDBDatabaseDelegate; -import org.testcontainers.delegate.DatabaseDelegate; -import org.testcontainers.ext.ScriptUtils; -import org.testcontainers.ext.ScriptUtils.ScriptLoadException; -import org.testcontainers.utility.DockerImageName; -import org.testcontainers.utility.MountableFile; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.Optional; - -import javax.script.ScriptException; - -/** - * Testcontainers implementation for ScyllaDB. - *

- * Supported image: {@code scylladb} - *

- * Exposed ports: 9042 - */ -public class ScyllaDBContainer> extends GenericContainer { - - private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("scylladb/scylla:5.2.9"); - - public static final Integer CQL_PORT = 9042; - - private static final String DEFAULT_LOCAL_DATACENTER = "datacenter1"; - - private static final String CONTAINER_CONFIG_LOCATION = "/etc/scylla"; - - private static final String USERNAME = "scylladb"; - - private static final String PASSWORD = "scylladb"; - - private String configLocation; - - private String initScriptPath; - - private boolean enableJmxReporting; - - public ScyllaDBContainer(String dockerImageName) { - this(DockerImageName.parse(dockerImageName)); - } - - public ScyllaDBContainer(DockerImageName dockerImageName) { - super(dockerImageName); - dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); - - addExposedPort(CQL_PORT); - this.enableJmxReporting = false; - - withEnv("CASSANDRA_SNITCH", "GossipingPropertyFileSnitch"); - withEnv("JVM_OPTS", "-Dcassandra.skip_wait_for_gossip_to_settle=0 -Dcassandra.initial_token=0"); - withEnv("HEAP_NEWSIZE", "128M"); - withEnv("MAX_HEAP_SIZE", "1024M"); - withEnv("SCYLLADB_ENDPOINT_SNITCH", "GossipingPropertyFileSnitch"); - withEnv("SCYLLADB_DC", DEFAULT_LOCAL_DATACENTER); - } - - @Override - protected void configure() { - optionallyMapResourceParameterAsVolume(CONTAINER_CONFIG_LOCATION, configLocation); - } - - @Override - protected void containerIsStarted(InspectContainerResponse containerInfo) { - runInitScriptIfRequired(); - } - - /** - * Load init script content and apply it to the database if initScriptPath is set - */ - private void runInitScriptIfRequired() { - if (initScriptPath != null) { - try { - URL resource = Thread.currentThread().getContextClassLoader().getResource(initScriptPath); - if (resource == null) { - logger().warn("Could not load classpath init script: {}", initScriptPath); - throw new ScriptLoadException( - "Could not load classpath init script: " + initScriptPath + ". Resource not found." - ); - } - String cql = IOUtils.toString(resource, StandardCharsets.UTF_8); - DatabaseDelegate databaseDelegate = new ScyllaDBDatabaseDelegate(this); - ScriptUtils.executeDatabaseScript(databaseDelegate, initScriptPath, cql); - } catch (IOException e) { - logger().warn("Could not load classpath init script: {}", initScriptPath); - throw new ScriptLoadException("Could not load classpath init script: " + initScriptPath, e); - } catch (ScriptException e) { - logger().error("Error while executing init script: {}", initScriptPath, e); - throw new ScriptUtils.UncategorizedScriptException( - "Error while executing init script: " + initScriptPath, - e - ); - } - } - } - - /** - * Map (effectively replace) directory in Docker with the content of resourceLocation if resource location is not null - * - * Protected to allow for changing implementation by extending the class - * - * @param pathNameInContainer path in docker - * @param resourceLocation relative classpath to resource - */ - protected void optionallyMapResourceParameterAsVolume(String pathNameInContainer, String resourceLocation) { - Optional - .ofNullable(resourceLocation) - .map(MountableFile::forClasspathResource) - .ifPresent(mountableFile -> withCopyFileToContainer(mountableFile, pathNameInContainer)); - } - - /** - * Initialize ScyllaDB with the custom overridden ScyllaDB configuration - *

- * Be aware, that Docker effectively replaces all /etc/sylladb content with the content of config location, so if - * scylladb.yaml in configLocation is absent or corrupted, then ScyllaDB just won't launch - * - * @param configLocation relative classpath with the directory that contains cassandra.yaml and other configuration files - */ - public SELF withConfigurationOverride(String configLocation) { - this.configLocation = configLocation; - return self(); - } - - /** - * Initialize ScyllaDB with init CQL script - *

- * CQL script will be applied after container is started (see using WaitStrategy) - * - * @param initScriptPath relative classpath resource - */ - public SELF withInitScript(String initScriptPath) { - this.initScriptPath = initScriptPath; - return self(); - } - - /** - * Initialize ScyllaDB client with JMX reporting enabled or disabled - */ - public SELF withJmxReporting(boolean enableJmxReporting) { - this.enableJmxReporting = enableJmxReporting; - return self(); - } - - /** - * Get username - * - * By default ScyllaDB has authenticator: AllowAllAuthenticator in scylladb.yaml - * If username and password need to be used, then authenticator should be set as PasswordAuthenticator - * (through custom ScyllaDB configuration) and through CQL with default scylladb-scylladb credentials - * user management should be modified - */ - public String getUsername() { - return USERNAME; - } - - /** - * Get password - * - * By default ScyllaDB has authenticator: AllowAllAuthenticator in scylladb.yaml - * If username and password need to be used, then authenticator should be set as PasswordAuthenticator - * (through custom Cassandra configuration) and through CQL with default scylladb-scylladb credentials - * user management should be modified - */ - public String getPassword() { - return PASSWORD; - } - - /** - * Retrieve an {@link InetSocketAddress} for connecting to the ScyllaDB container via the driver. - * - * @return A InetSocketAddrss representation of this ScyllaDB container's host and port. - */ - public InetSocketAddress getContactPoint() { - return new InetSocketAddress(getHost(), getMappedPort(CQL_PORT)); - } - - /** - * Retrieve the Local Datacenter for connecting to the ScyllaDB container via the driver. - * - * @return The configured local Datacenter name. - */ - public String getLocalDatacenter() { - return getEnvMap().getOrDefault("SCYLLADB_DC", DEFAULT_LOCAL_DATACENTER); - } -} diff --git a/modules/scylladb/src/main/java/org/testcontainers/containers/delegate/ScyllaDBDatabaseDelegate.java b/modules/scylladb/src/main/java/org/testcontainers/containers/delegate/ScyllaDBDatabaseDelegate.java deleted file mode 100644 index 255ec5d90c8..00000000000 --- a/modules/scylladb/src/main/java/org/testcontainers/containers/delegate/ScyllaDBDatabaseDelegate.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.testcontainers.containers.delegate; - -import com.datastax.oss.driver.api.core.CqlSession; -import com.datastax.oss.driver.api.core.DriverException; -import com.datastax.oss.driver.api.core.cql.ResultSet; -import org.slf4j.Logger; -import org.testcontainers.containers.ContainerState; -import org.testcontainers.containers.ScyllaDBContainer; -import org.testcontainers.delegate.AbstractDatabaseDelegate; -import org.testcontainers.exception.ConnectionCreationException; -import org.testcontainers.ext.ScriptUtils.ScriptStatementFailedException; -import org.testcontainers.utility.DockerLoggerFactory; - -import java.net.InetSocketAddress; - -public class ScyllaDBDatabaseDelegate extends AbstractDatabaseDelegate { - - public ScyllaDBDatabaseDelegate(ContainerState container) { - this.container = container; - } - - protected Logger logger() { - return DockerLoggerFactory.getLogger(container.getCurrentContainerInfo().getName()); - } - - private final ContainerState container; - - @Override - protected CqlSession createNewConnection() { - try { - return CqlSession - .builder() - .addContactPoint( - new InetSocketAddress(container.getHost(), container.getMappedPort(ScyllaDBContainer.CQL_PORT)) - ) - .withLocalDatacenter("datacenter1") - .build(); - } catch (DriverException e) { - throw new ConnectionCreationException("Could not obtain cassandra connection", e); - } - } - - @Override - public void execute( - String statement, - String scriptPath, - int lineNumber, - boolean continueOnError, - boolean ignoreFailedDrops - ) { - try { - ResultSet result = getConnection().execute(statement); - if (!result.wasApplied()) { - throw new ScriptStatementFailedException(statement, lineNumber, scriptPath); - } - } catch (DriverException e) { - throw new ScriptStatementFailedException(statement, lineNumber, scriptPath, e); - } - } - - @Override - protected void closeConnectionQuietly(CqlSession session) { - try { - session.close(); - } catch (Exception e) { - logger().error("Could not close cassandra connection", e); - } - } -} diff --git a/modules/scylladb/src/main/java/org/testcontainers/containers/wait/ScyllaDBQueryWaitStrategy.java b/modules/scylladb/src/main/java/org/testcontainers/containers/wait/ScyllaDBQueryWaitStrategy.java deleted file mode 100644 index c72822932ff..00000000000 --- a/modules/scylladb/src/main/java/org/testcontainers/containers/wait/ScyllaDBQueryWaitStrategy.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.testcontainers.containers.wait; - -import org.rnorth.ducttape.TimeoutException; -import org.testcontainers.containers.ContainerLaunchException; -import org.testcontainers.containers.delegate.ScyllaDBDatabaseDelegate; -import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; -import org.testcontainers.delegate.DatabaseDelegate; - -import java.util.concurrent.TimeUnit; - -import static org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess; - -/** - * Waits until ScyllaDB returns its version - */ -public class ScyllaDBQueryWaitStrategy extends AbstractWaitStrategy { - - private static final String SELECT_VERSION_QUERY = "SELECT release_version FROM system.local"; - - private static final String TIMEOUT_ERROR = "Timed out waiting for ScyllaDB to be accessible for query execution"; - - @Override - protected void waitUntilReady() { - // execute select version query until success or timeout - try { - retryUntilSuccess( - (int) startupTimeout.getSeconds(), - TimeUnit.SECONDS, - () -> { - getRateLimiter() - .doWhenReady(() -> { - try (DatabaseDelegate databaseDelegate = new ScyllaDBDatabaseDelegate(waitStrategyTarget)) { - databaseDelegate.execute(SELECT_VERSION_QUERY, "", 1, false, false); - } - }); - return true; - } - ); - } catch (TimeoutException e) { - throw new ContainerLaunchException(TIMEOUT_ERROR); - } - } -} diff --git a/modules/scylladb/src/main/java/org/testcontainers/scylladb/ScyllaDBContainer.java b/modules/scylladb/src/main/java/org/testcontainers/scylladb/ScyllaDBContainer.java new file mode 100644 index 00000000000..03e5a29237c --- /dev/null +++ b/modules/scylladb/src/main/java/org/testcontainers/scylladb/ScyllaDBContainer.java @@ -0,0 +1,83 @@ +package org.testcontainers.scylladb; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +import java.net.InetSocketAddress; + +/** + * Testcontainers implementation for ScyllaDB. + *

+ * Supported image: {@code scylladb/scylla} + *

+ * Exposed ports: + *

+ */ +public class ScyllaDBContainer extends GenericContainer { + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("scylladb/scylla"); + + private static final Integer CQL_PORT = 9042; + + private static final Integer SHARD_AWARE_PORT = 19042; + + private static final Integer ALTERNATOR_PORT = 8000; + + private static final String COMMAND = "--developer-mode=1 --overprovisioned=1"; + + private boolean alternatorEnabled = false; + + public ScyllaDBContainer(String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + public ScyllaDBContainer(DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + + withExposedPorts(CQL_PORT, SHARD_AWARE_PORT); + + withCommand(COMMAND); + waitingFor(Wait.forLogMessage(".*initialization completed..*", 1)); + } + + @Override + protected void configure() { + if (this.alternatorEnabled) { + addExposedPort(8000); + String newCommand = + COMMAND + " --alternator-port=" + ALTERNATOR_PORT + " --alternator-write-isolation=always"; + withCommand(newCommand); + } + } + + public ScyllaDBContainer withAlternator() { + this.alternatorEnabled = true; + return this; + } + + /** + * Retrieve an {@link InetSocketAddress} for connecting to the ScyllaDB container via the driver. + * + * @return A InetSocketAddress representation of this ScyllaDB container's host and port. + */ + public InetSocketAddress getContactPoint() { + return new InetSocketAddress(getHost(), getMappedPort(CQL_PORT)); + } + + public InetSocketAddress getShardAwareContactPoint() { + return new InetSocketAddress(getHost(), getMappedPort(SHARD_AWARE_PORT)); + } + + public String getAlternatorEndpoint() { + if (!this.alternatorEnabled) { + throw new IllegalStateException("Alternator is not enabled"); + } + return "http://" + getHost() + ":" + getMappedPort(ALTERNATOR_PORT); + } +} diff --git a/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBContainerTest.java b/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBContainerTest.java deleted file mode 100644 index fc3babd9427..00000000000 --- a/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBContainerTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package org.testcontainers.containers; - -import com.datastax.oss.driver.api.core.CqlSession; -import com.datastax.oss.driver.api.core.cql.ResultSet; -import com.datastax.oss.driver.api.core.cql.Row; -import lombok.extern.slf4j.Slf4j; -import org.junit.Test; -import org.testcontainers.containers.wait.ScyllaDBQueryWaitStrategy; -import org.testcontainers.utility.DockerImageName; - -import java.net.InetSocketAddress; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.testcontainers.containers.ScyllaDBContainer.CQL_PORT; - -@Slf4j -public class ScyllaDBContainerTest { - - private static final DockerImageName SCYLLADB_IMAGE = DockerImageName.parse("scylladb/scylla:5.2.9"); - - private static final String TEST_CLUSTER_NAME_IN_CONF = "Test Cluster Integration Test"; - - private static final String BASIC_QUERY = "SELECT release_version FROM system.local"; - - @Test - public void testSimple() { - try (ScyllaDBContainer scylladbContainer = new ScyllaDBContainer<>(SCYLLADB_IMAGE)) { - scylladbContainer.start(); - ResultSet resultSet = performQuery(scylladbContainer, BASIC_QUERY); - assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); - assertThat(resultSet.one().getString(0)).as("Result set has release_version").isNotNull(); - } - } - - @Test - public void testSpecificVersion() { - String scyllaDBReportedVersion = "3.0.8"; - try ( - ScyllaDBContainer ScyllaDBContainer = new ScyllaDBContainer<>(SCYLLADB_IMAGE) - ) { - ScyllaDBContainer.start(); - ResultSet resultSet = performQuery(ScyllaDBContainer, BASIC_QUERY); - assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); - assertThat(resultSet.one().getString(0)).as("ScyllaDB has right version").isEqualTo(scyllaDBReportedVersion); - } - } - - @Test - public void testConfigurationOverride() { - try ( - ScyllaDBContainer ScyllaDBContainer = new ScyllaDBContainer<>(SCYLLADB_IMAGE) - .withConfigurationOverride("scylla-test-configuration-example") - ) { - ScyllaDBContainer.start(); - ResultSet resultSet = performQuery(ScyllaDBContainer, "SELECT cluster_name FROM system.local"); - assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); - assertThat(resultSet.one().getString(0)) - .as("ScyllaDB configuration is overridden") - .isEqualTo(TEST_CLUSTER_NAME_IN_CONF); - } - } - - @Test(expected = ContainerLaunchException.class) - public void testEmptyConfigurationOverride() { - try ( - ScyllaDBContainer ScyllaDBContainer = new ScyllaDBContainer<>(SCYLLADB_IMAGE) - .withConfigurationOverride("scylladb-empty-configuration") - ) { - ScyllaDBContainer.start(); - } - } - - @Test - public void testInitScript() { - try ( - ScyllaDBContainer ScyllaDBContainer = new ScyllaDBContainer<>(SCYLLADB_IMAGE) - .withInitScript("initial.cql") - ) { - ScyllaDBContainer.start(); - testInitScript(ScyllaDBContainer); - } - } - - @Test - public void testScyllaDBQueryWaitStrategy() { - try ( - ScyllaDBContainer scyllaDBContainer = new ScyllaDBContainer<>(SCYLLADB_IMAGE) - .waitingFor(new ScyllaDBQueryWaitStrategy()) - ) { - scyllaDBContainer.start(); - ResultSet resultSet = performQuery(scyllaDBContainer, BASIC_QUERY); - assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); - } - } - - private void testInitScript(ScyllaDBContainer ScyllaDBContainer) { - ResultSet resultSet = performQuery(ScyllaDBContainer, "SELECT * FROM keySpaceTest.catalog_category"); - assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); - Row row = resultSet.one(); - assertThat(row.getLong(0)).as("Inserted row is in expected state").isEqualTo(1); - assertThat(row.getString(1)).as("Inserted row is in expected state").isEqualTo("test_category"); - } - - private ResultSet performQuery(ScyllaDBContainer ScyllaDBContainer, String cql) { - CqlSession session = CqlSession. - builder(). - addContactPoint( - new InetSocketAddress( - ScyllaDBContainer.getHost(), - ScyllaDBContainer.getMappedPort(CQL_PORT) - )). - withLocalDatacenter("datacenter1"). - build(); - - return session.execute(cql); - } -} diff --git a/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBDriver4Test.java b/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBDriver4Test.java deleted file mode 100644 index 3953a1e1118..00000000000 --- a/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBDriver4Test.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.testcontainers.containers; - -import com.datastax.oss.driver.api.core.CqlIdentifier; -import com.datastax.oss.driver.api.core.CqlSession; -import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; -import org.junit.Rule; -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public class ScyllaDBDriver4Test { - - @Rule - public ScyllaDBContainer scyllaDB = new ScyllaDBContainer<>("scylladb/scylla:5.2.9"); - - @Test - public void testCassandraGetContactPoint() { - try ( - // cassandra { - CqlSession session = CqlSession - .builder() - .addContactPoint(this.scyllaDB.getContactPoint()) - .withLocalDatacenter(this.scyllaDB.getLocalDatacenter()) - .build() - // } - ) { - session.execute( - "CREATE KEYSPACE IF NOT EXISTS test WITH replication = \n" + - "{'class':'SimpleStrategy','replication_factor':'1'};" - ); - - KeyspaceMetadata keyspace = session.getMetadata().getKeyspaces().get(CqlIdentifier.fromCql("test")); - - assertThat(keyspace).as("keyspace created").isNotNull(); - } - } -} diff --git a/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBServer5Test.java b/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBServer5Test.java deleted file mode 100644 index f6959744db7..00000000000 --- a/modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBServer5Test.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.testcontainers.containers; - -import com.datastax.oss.driver.api.core.CqlIdentifier; -import com.datastax.oss.driver.api.core.CqlSession; -import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; -import org.junit.Rule; -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public class ScyllaDBServer5Test { - - @Rule - public ScyllaDBContainer scylladb = new ScyllaDBContainer<>("scylladb/scylla:5.2.9"); - - @Test - public void testScyllaDBGetContactPoint() { - try ( - CqlSession session = CqlSession - .builder() - .addContactPoint(this.scylladb.getContactPoint()) - .withLocalDatacenter(this.scylladb.getLocalDatacenter()) - .build() - ) { - session.execute( - "CREATE KEYSPACE IF NOT EXISTS test WITH replication = \n" + - "{'class':'SimpleStrategy','replication_factor':'1'};" - ); - - KeyspaceMetadata keyspace = session.getMetadata().getKeyspaces().get(CqlIdentifier.fromCql("test")); - - assertThat(keyspace).as("test keyspace created").isNotNull(); - } - } -} diff --git a/modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java b/modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java new file mode 100644 index 00000000000..32c64480e86 --- /dev/null +++ b/modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java @@ -0,0 +1,107 @@ +package org.testcontainers.scylladb; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import org.junit.Test; +import org.testcontainers.utility.DockerImageName; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.BillingMode; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; + +import java.net.URI; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ScyllaDBContainerTest { + + private static final DockerImageName SCYLLADB_IMAGE = DockerImageName.parse("scylladb/scylla:5.2.9"); + + private static final String BASIC_QUERY = "SELECT release_version FROM system.local"; + + @Test + public void testSimple() { + try ( // container { + ScyllaDBContainer scylladb = new ScyllaDBContainer("scylladb/scylla:5.2.9") + // } + ) { + scylladb.start(); + // session { + CqlSession session = CqlSession + .builder() + .addContactPoint(scylladb.getContactPoint()) + .withLocalDatacenter("datacenter1") + .build(); + // } + ResultSet resultSet = session.execute(BASIC_QUERY); + assertThat(resultSet.wasApplied()).isTrue(); + assertThat(resultSet.one().getString(0)).isNotNull(); + assertThat(session.getMetadata().getNodes().values()).hasSize(1); + } + } + + @Test + public void testShardAwareness() { + try (ScyllaDBContainer scylladb = new ScyllaDBContainer(SCYLLADB_IMAGE)) { + scylladb.start(); + // shardAwarenessSession { + CqlSession session = CqlSession + .builder() + .addContactPoint(scylladb.getShardAwareContactPoint()) + .withLocalDatacenter("datacenter1") + .build(); + // } + ResultSet resultSet = session.execute("SELECT driver_name FROM system.clients"); + assertThat(resultSet.one().getString(0)).isNotNull(); + assertThat(session.getMetadata().getNodes().values()).hasSize(1); + } + } + + @Test + public void testAlternator() { + try ( // alternator { + ScyllaDBContainer scylladb = new ScyllaDBContainer(SCYLLADB_IMAGE).withAlternator() + // } + ) { + scylladb.start(); + + // dynamodDbClient { + DynamoDbClient client = DynamoDbClient + .builder() + .endpointOverride(URI.create(scylladb.getAlternatorEndpoint())) + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("test", "test"))) + .region(Region.US_EAST_1) + .build(); + // } + client.createTable( + CreateTableRequest + .builder() + .tableName("demo_table") + .keySchema(KeySchemaElement.builder().attributeName("id").keyType(KeyType.HASH).build()) + .attributeDefinitions( + AttributeDefinition.builder().attributeName("id").attributeType(ScalarAttributeType.S).build() + ) + .billingMode(BillingMode.PAY_PER_REQUEST) + .build() + ); + assertThat(client.listTables().tableNames()).containsExactly(("demo_table")); + } + } + + @Test + public void throwExceptionWhenAlternatorDisabled() { + try (ScyllaDBContainer scylladb = new ScyllaDBContainer(SCYLLADB_IMAGE)) { + scylladb.start(); + assertThatThrownBy(scylladb::getAlternatorEndpoint) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Alternator is not enabled"); + } + } +} From b5fa599cfbcf989b30e16c8130b1dbdb401f8a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Tue, 17 Dec 2024 17:29:11 -0600 Subject: [PATCH 5/5] Add docs --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index c308128969b..946b0053f58 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -72,6 +72,7 @@ nav: - modules/databases/postgres.md - modules/databases/presto.md - modules/databases/questdb.md + - modules/databases/scylladb.md - modules/databases/tidb.md - modules/databases/timeplus.md - modules/databases/trino.md