Skip to content

Commit

Permalink
add scylladb module
Browse files Browse the repository at this point in the history
  • Loading branch information
mkorolyov committed Jan 2, 2024
1 parent 36c8727 commit ff3b42c
Show file tree
Hide file tree
Showing 16 changed files with 636 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ body:
- QuestDB
- RabbitMQ
- Redpanda
- ScyllaDB
- Selenium
- Solace
- Solr
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 @@ -47,6 +47,7 @@ body:
- QuestDB
- RabbitMQ
- Redpanda
- ScyllaDB
- Selenium
- Solace
- Solr
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 @@ -47,6 +47,7 @@ body:
- Pulsar
- RabbitMQ
- Redpanda
- ScyllaDB
- Selenium
- Solace
- Solr
Expand Down
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
28 changes: 28 additions & 0 deletions docs/modules/databases/scylladb.md
Original file line number Diff line number Diff line change
@@ -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.

<!--codeinclude-->
[Building CqlSession](../../../modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBDriver4Test.java) inside_block:scylladb
<!--/codeinclude-->

## 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
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>scylladb</artifactId>
<version>{{latest_version}}</version>
<scope>test</scope>
</dependency>
```
16 changes: 16 additions & 0 deletions modules/scylladb/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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 'com.datastax.oss:java-driver-core:4.17.0'
testImplementation 'org.assertj:assertj-core:3.24.2'
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* Supported image: {@code scylladb}
* <p>
* Exposed ports: 9042
*/
public class ScyllaDBContainer<SELF extends ScyllaDBContainer<SELF>> extends GenericContainer<SELF> {

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("CASSANDRA_ENDPOINT_SNITCH", "GossipingPropertyFileSnitch");
withEnv("CASSANDRA_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
* <p>
* 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
* <p>
* 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<CqlSession> {

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);
}
}
}
Loading

0 comments on commit ff3b42c

Please sign in to comment.