Skip to content

Commit ff3b42c

Browse files
committed
add scylladb module
1 parent 36c8727 commit ff3b42c

File tree

16 files changed

+636
-0
lines changed

16 files changed

+636
-0
lines changed

.github/ISSUE_TEMPLATE/bug_report.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ body:
4747
- QuestDB
4848
- RabbitMQ
4949
- Redpanda
50+
- ScyllaDB
5051
- Selenium
5152
- Solace
5253
- Solr

.github/ISSUE_TEMPLATE/enhancement.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ body:
4747
- QuestDB
4848
- RabbitMQ
4949
- Redpanda
50+
- ScyllaDB
5051
- Selenium
5152
- Solace
5253
- Solr

.github/ISSUE_TEMPLATE/feature.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ body:
4747
- Pulsar
4848
- RabbitMQ
4949
- Redpanda
50+
- ScyllaDB
5051
- Selenium
5152
- Solace
5253
- Solr

.github/dependabot.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,11 @@ updates:
246246
schedule:
247247
interval: "weekly"
248248
open-pull-requests-limit: 10
249+
- package-ecosystem: "gradle"
250+
directory: "/modules/scylladb"
251+
schedule:
252+
interval: "weekly"
253+
open-pull-requests-limit: 10
249254
- package-ecosystem: "gradle"
250255
directory: "/modules/selenium"
251256
schedule:

.github/labeler.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@
156156
- changed-files:
157157
- any-glob-to-any-file:
158158
- modules/redpanda/**/*
159+
"modules/scylladb":
160+
- changed-files:
161+
- any-glob-to-any-file:
162+
- modules/scylladb/**/*
159163
"modules/selenium":
160164
- changed-files:
161165
- any-glob-to-any-file:

docs/modules/databases/scylladb.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# ScyllaDB Module
2+
3+
## Usage example
4+
5+
This example connects to the ScyllaDB Cluster, creates a keyspaces and asserts that is has been created.
6+
7+
<!--codeinclude-->
8+
[Building CqlSession](../../../modules/scylladb/src/test/java/org/testcontainers/containers/ScyllaDBDriver4Test.java) inside_block:scylladb
9+
<!--/codeinclude-->
10+
11+
## Adding this module to your project dependencies
12+
13+
Add the following dependency to your `pom.xml`/`build.gradle` file:
14+
15+
=== "Gradle"
16+
```groovy
17+
testImplementation "org.testcontainers:scylladb:{{latest_version}}"
18+
```
19+
20+
=== "Maven"
21+
```xml
22+
<dependency>
23+
<groupId>org.testcontainers</groupId>
24+
<artifactId>scylladb</artifactId>
25+
<version>{{latest_version}}</version>
26+
<scope>test</scope>
27+
</dependency>
28+
```

modules/scylladb/build.gradle

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
description = "Testcontainers :: ScyllaDB"
2+
3+
configurations.all {
4+
resolutionStrategy {
5+
force 'io.dropwizard.metrics:metrics-core:3.2.6'
6+
}
7+
}
8+
9+
dependencies {
10+
api project(":database-commons")
11+
api "com.scylladb:java-driver-core:4.15.0.0"
12+
api "com.datastax.cassandra:cassandra-driver-core:3.10.0"
13+
14+
// testImplementation 'com.datastax.oss:java-driver-core:4.17.0'
15+
testImplementation 'org.assertj:assertj-core:3.24.2'
16+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package org.testcontainers.containers;
2+
3+
import com.github.dockerjava.api.command.InspectContainerResponse;
4+
import org.apache.commons.io.IOUtils;
5+
import org.testcontainers.containers.delegate.ScyllaDBDatabaseDelegate;
6+
import org.testcontainers.delegate.DatabaseDelegate;
7+
import org.testcontainers.ext.ScriptUtils;
8+
import org.testcontainers.ext.ScriptUtils.ScriptLoadException;
9+
import org.testcontainers.utility.DockerImageName;
10+
import org.testcontainers.utility.MountableFile;
11+
12+
import java.io.IOException;
13+
import java.net.InetSocketAddress;
14+
import java.net.URL;
15+
import java.nio.charset.StandardCharsets;
16+
import java.util.Optional;
17+
18+
import javax.script.ScriptException;
19+
20+
/**
21+
* Testcontainers implementation for ScyllaDB.
22+
* <p>
23+
* Supported image: {@code scylladb}
24+
* <p>
25+
* Exposed ports: 9042
26+
*/
27+
public class ScyllaDBContainer<SELF extends ScyllaDBContainer<SELF>> extends GenericContainer<SELF> {
28+
29+
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("scylladb/scylla:5.2.9");
30+
31+
public static final Integer CQL_PORT = 9042;
32+
33+
private static final String DEFAULT_LOCAL_DATACENTER = "datacenter1";
34+
35+
private static final String CONTAINER_CONFIG_LOCATION = "/etc/scylla";
36+
37+
private static final String USERNAME = "scylladb";
38+
39+
private static final String PASSWORD = "scylladb";
40+
41+
private String configLocation;
42+
43+
private String initScriptPath;
44+
45+
private boolean enableJmxReporting;
46+
47+
public ScyllaDBContainer(String dockerImageName) {
48+
this(DockerImageName.parse(dockerImageName));
49+
}
50+
51+
public ScyllaDBContainer(DockerImageName dockerImageName) {
52+
super(dockerImageName);
53+
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
54+
55+
addExposedPort(CQL_PORT);
56+
this.enableJmxReporting = false;
57+
58+
withEnv("CASSANDRA_SNITCH", "GossipingPropertyFileSnitch");
59+
withEnv("JVM_OPTS", "-Dcassandra.skip_wait_for_gossip_to_settle=0 -Dcassandra.initial_token=0");
60+
withEnv("HEAP_NEWSIZE", "128M");
61+
withEnv("MAX_HEAP_SIZE", "1024M");
62+
withEnv("CASSANDRA_ENDPOINT_SNITCH", "GossipingPropertyFileSnitch");
63+
withEnv("CASSANDRA_DC", DEFAULT_LOCAL_DATACENTER);
64+
}
65+
66+
@Override
67+
protected void configure() {
68+
optionallyMapResourceParameterAsVolume(CONTAINER_CONFIG_LOCATION, configLocation);
69+
}
70+
71+
@Override
72+
protected void containerIsStarted(InspectContainerResponse containerInfo) {
73+
runInitScriptIfRequired();
74+
}
75+
76+
/**
77+
* Load init script content and apply it to the database if initScriptPath is set
78+
*/
79+
private void runInitScriptIfRequired() {
80+
if (initScriptPath != null) {
81+
try {
82+
URL resource = Thread.currentThread().getContextClassLoader().getResource(initScriptPath);
83+
if (resource == null) {
84+
logger().warn("Could not load classpath init script: {}", initScriptPath);
85+
throw new ScriptLoadException(
86+
"Could not load classpath init script: " + initScriptPath + ". Resource not found."
87+
);
88+
}
89+
String cql = IOUtils.toString(resource, StandardCharsets.UTF_8);
90+
DatabaseDelegate databaseDelegate = new ScyllaDBDatabaseDelegate(this);
91+
ScriptUtils.executeDatabaseScript(databaseDelegate, initScriptPath, cql);
92+
} catch (IOException e) {
93+
logger().warn("Could not load classpath init script: {}", initScriptPath);
94+
throw new ScriptLoadException("Could not load classpath init script: " + initScriptPath, e);
95+
} catch (ScriptException e) {
96+
logger().error("Error while executing init script: {}", initScriptPath, e);
97+
throw new ScriptUtils.UncategorizedScriptException(
98+
"Error while executing init script: " + initScriptPath,
99+
e
100+
);
101+
}
102+
}
103+
}
104+
105+
/**
106+
* Map (effectively replace) directory in Docker with the content of resourceLocation if resource location is not null
107+
*
108+
* Protected to allow for changing implementation by extending the class
109+
*
110+
* @param pathNameInContainer path in docker
111+
* @param resourceLocation relative classpath to resource
112+
*/
113+
protected void optionallyMapResourceParameterAsVolume(String pathNameInContainer, String resourceLocation) {
114+
Optional
115+
.ofNullable(resourceLocation)
116+
.map(MountableFile::forClasspathResource)
117+
.ifPresent(mountableFile -> withCopyFileToContainer(mountableFile, pathNameInContainer));
118+
}
119+
120+
/**
121+
* Initialize ScyllaDB with the custom overridden ScyllaDB configuration
122+
* <p>
123+
* Be aware, that Docker effectively replaces all /etc/sylladb content with the content of config location, so if
124+
* scylladb.yaml in configLocation is absent or corrupted, then ScyllaDB just won't launch
125+
*
126+
* @param configLocation relative classpath with the directory that contains cassandra.yaml and other configuration files
127+
*/
128+
public SELF withConfigurationOverride(String configLocation) {
129+
this.configLocation = configLocation;
130+
return self();
131+
}
132+
133+
/**
134+
* Initialize ScyllaDB with init CQL script
135+
* <p>
136+
* CQL script will be applied after container is started (see using WaitStrategy)
137+
*
138+
* @param initScriptPath relative classpath resource
139+
*/
140+
public SELF withInitScript(String initScriptPath) {
141+
this.initScriptPath = initScriptPath;
142+
return self();
143+
}
144+
145+
/**
146+
* Initialize ScyllaDB client with JMX reporting enabled or disabled
147+
*/
148+
public SELF withJmxReporting(boolean enableJmxReporting) {
149+
this.enableJmxReporting = enableJmxReporting;
150+
return self();
151+
}
152+
153+
/**
154+
* Get username
155+
*
156+
* By default ScyllaDB has authenticator: AllowAllAuthenticator in scylladb.yaml
157+
* If username and password need to be used, then authenticator should be set as PasswordAuthenticator
158+
* (through custom ScyllaDB configuration) and through CQL with default scylladb-scylladb credentials
159+
* user management should be modified
160+
*/
161+
public String getUsername() {
162+
return USERNAME;
163+
}
164+
165+
/**
166+
* Get password
167+
*
168+
* By default ScyllaDB has authenticator: AllowAllAuthenticator in scylladb.yaml
169+
* If username and password need to be used, then authenticator should be set as PasswordAuthenticator
170+
* (through custom Cassandra configuration) and through CQL with default scylladb-scylladb credentials
171+
* user management should be modified
172+
*/
173+
public String getPassword() {
174+
return PASSWORD;
175+
}
176+
177+
/**
178+
* Retrieve an {@link InetSocketAddress} for connecting to the ScyllaDB container via the driver.
179+
*
180+
* @return A InetSocketAddrss representation of this ScyllaDB container's host and port.
181+
*/
182+
public InetSocketAddress getContactPoint() {
183+
return new InetSocketAddress(getHost(), getMappedPort(CQL_PORT));
184+
}
185+
186+
/**
187+
* Retrieve the Local Datacenter for connecting to the ScyllaDB container via the driver.
188+
*
189+
* @return The configured local Datacenter name.
190+
*/
191+
public String getLocalDatacenter() {
192+
return getEnvMap().getOrDefault("SCYLLADB_DC", DEFAULT_LOCAL_DATACENTER);
193+
}
194+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.testcontainers.containers.delegate;
2+
3+
import com.datastax.oss.driver.api.core.CqlSession;
4+
import com.datastax.oss.driver.api.core.DriverException;
5+
import com.datastax.oss.driver.api.core.cql.ResultSet;
6+
import org.slf4j.Logger;
7+
import org.testcontainers.containers.ContainerState;
8+
import org.testcontainers.containers.ScyllaDBContainer;
9+
import org.testcontainers.delegate.AbstractDatabaseDelegate;
10+
import org.testcontainers.exception.ConnectionCreationException;
11+
import org.testcontainers.ext.ScriptUtils.ScriptStatementFailedException;
12+
import org.testcontainers.utility.DockerLoggerFactory;
13+
14+
import java.net.InetSocketAddress;
15+
16+
public class ScyllaDBDatabaseDelegate extends AbstractDatabaseDelegate<CqlSession> {
17+
18+
public ScyllaDBDatabaseDelegate(ContainerState container) {
19+
this.container = container;
20+
}
21+
22+
protected Logger logger() {
23+
return DockerLoggerFactory.getLogger(container.getCurrentContainerInfo().getName());
24+
}
25+
26+
private final ContainerState container;
27+
28+
@Override
29+
protected CqlSession createNewConnection() {
30+
try {
31+
return CqlSession
32+
.builder()
33+
.addContactPoint(
34+
new InetSocketAddress(container.getHost(), container.getMappedPort(ScyllaDBContainer.CQL_PORT))
35+
)
36+
.withLocalDatacenter("datacenter1")
37+
.build();
38+
} catch (DriverException e) {
39+
throw new ConnectionCreationException("Could not obtain cassandra connection", e);
40+
}
41+
}
42+
43+
@Override
44+
public void execute(
45+
String statement,
46+
String scriptPath,
47+
int lineNumber,
48+
boolean continueOnError,
49+
boolean ignoreFailedDrops
50+
) {
51+
try {
52+
ResultSet result = getConnection().execute(statement);
53+
if (!result.wasApplied()) {
54+
throw new ScriptStatementFailedException(statement, lineNumber, scriptPath);
55+
}
56+
} catch (DriverException e) {
57+
throw new ScriptStatementFailedException(statement, lineNumber, scriptPath, e);
58+
}
59+
}
60+
61+
@Override
62+
protected void closeConnectionQuietly(CqlSession session) {
63+
try {
64+
session.close();
65+
} catch (Exception e) {
66+
logger().error("Could not close cassandra connection", e);
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)