diff --git a/.github/scripts/run-tests.sh b/.github/scripts/run-tests.sh
index e04261bcd..fb78cebcc 100755
--- a/.github/scripts/run-tests.sh
+++ b/.github/scripts/run-tests.sh
@@ -217,8 +217,8 @@ case "${TEST_TYPE}" in
sudo apt-get update
sudo apt-get install jq -y
mvn -B package -DskipTests
- docker-compose -f ./src/packaging/docker-build/docker-compose.yml build
- docker-compose -f ./src/packaging/docker-build/docker-compose.yml run build
+ docker compose -f ./src/packaging/docker-build/docker-compose.yml build
+ docker compose -f ./src/packaging/docker-build/docker-compose.yml run build
VERSION=$(printf 'VER\t${project.version}' | mvn help:evaluate | grep '^VER' | cut -f2)
docker build --build-arg SHADED_JAR=src/server/target/cassandra-reaper-${VERSION}.jar -f src/server/src/main/docker/Dockerfile -t cassandra-reaper:latest .
docker images
@@ -226,9 +226,9 @@ case "${TEST_TYPE}" in
# Clear out Cassandra data before starting a new cluster
sudo rm -vfr ./src/packaging/data/
- docker-compose -f ./src/packaging/docker-compose.yml up -d cassandra
- sleep 30 && docker-compose -f ./src/packaging/docker-compose.yml run cqlsh-initialize-reaper_db
- sleep 10 && docker-compose -f ./src/packaging/docker-compose.yml up -d reaper
+ docker compose -f ./src/packaging/docker-compose.yml up -d cassandra
+ sleep 30 && docker compose -f ./src/packaging/docker-compose.yml run cqlsh-initialize-reaper_db
+ sleep 10 && docker compose -f ./src/packaging/docker-compose.yml up -d reaper
docker ps -a
# requests python package is needed to use spreaper
@@ -236,14 +236,14 @@ case "${TEST_TYPE}" in
mkdir -p ~/.reaper
echo "admin" > ~/.reaper/credentials
sleep 30 && src/packaging/bin/spreaper login admin
- src/packaging/bin/spreaper add-cluster $(docker-compose -f ./src/packaging/docker-compose.yml run nodetool status | grep UN | tr -s ' ' | cut -d' ' -f2) 7199 > cluster.json
+ src/packaging/bin/spreaper add-cluster $(docker compose -f ./src/packaging/docker-compose.yml run nodetool status | grep UN | tr -s ' ' | cut -d' ' -f2) 7199 > cluster.json
cat cluster.json
cluster_name=$(cat cluster.json|grep -v "#" | jq -r '.name')
if [[ "$cluster_name" != "reaper-cluster" ]]; then
echo "Failed registering cluster in Reaper running in Docker"
exit 1
fi
- sleep 5 && docker-compose -f ./src/packaging/docker-compose.yml down
+ sleep 5 && docker compose -f ./src/packaging/docker-compose.yml down
;;
*)
echo "Skipping, no actions for TEST_TYPE=${TEST_TYPE}."
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19b234da3..95f3e9401 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
## Change Log
+### 3.7.0 (2024/11/15 12:02 +00:00)
+- [#1527](https://github.com/thelastpickle/cassandra-reaper/pull/1527) Upgrade the base docker image from Corretto 11.0.20 to 11.0.25 (#1527) (@adejanovski)
+- [#1510](https://github.com/thelastpickle/cassandra-reaper/pull/1510) update doc for persistenceStoragePath (#1510) (@SarthakSahu)
+- [#1457](https://github.com/thelastpickle/cassandra-reaper/pull/1457) Bump io.netty:netty-handler in /src/server (#1457) (@dependabot[bot])
+- [#1461](https://github.com/thelastpickle/cassandra-reaper/pull/1461) Bump org.apache.shiro:shiro-core from 1.12.0 to 1.13.0 in /src/server (#1461) (@dependabot[bot])
+- [#1524](https://github.com/thelastpickle/cassandra-reaper/pull/1524) Allow setting up per-cluster TLS connections (#1524) (@rzvoncek)
+- [#1523](https://github.com/thelastpickle/cassandra-reaper/pull/1523) Update java driver 3.11.0 to 3.11.5 (#1523) (@bschoening)
+- [#1516](https://github.com/thelastpickle/cassandra-reaper/pull/1516) Switch CI to docker-compose v2 (#1516) (@adejanovski)
+- [#1509](https://github.com/thelastpickle/cassandra-reaper/pull/1509) Subrange incremental repair (#1509) (@adejanovski)
+
### 3.6.1 (2024/06/11 07:14 +00:00)
- [#1507](https://github.com/thelastpickle/cassandra-reaper/pull/1507) Allow mounting a volume to host the config and enable read only root FS (#1507) (@adejanovski)
diff --git a/src/docs/content/docs/backends/memory.md b/src/docs/content/docs/backends/memory.md
index 1a4ea1edb..38dd4bf24 100644
--- a/src/docs/content/docs/backends/memory.md
+++ b/src/docs/content/docs/backends/memory.md
@@ -11,6 +11,9 @@ To use in memory storage as the storage type for Reaper, the `storageType` setti
```yaml
storageType: memory
+persistenceStoragePath: /var/lib/cassandra-reaper/storage
```
In-memory storage is volatile and as such all registered cluster, column families and repair information will be lost upon service restart. This storage setting is intended for testing purposes only.
+
+Starting from 3.6.0, persistenceStoragePath is required for memory storage type. This enable lightweight deployments of Reaper, without requiring the use of a Cassandra database. It will store the data locally and reload them consistently upon startup.
diff --git a/src/server/pom.xml b/src/server/pom.xml
index 58aa96bca..83e92c928 100644
--- a/src/server/pom.xml
+++ b/src/server/pom.xml
@@ -33,7 +33,7 @@
2.35
1.3.14
3.4.5
- 1.12.0
+ 1.13.0
0.12.0
src/main/docker
${maven.build.timestamp}
@@ -169,12 +169,12 @@
io.netty
netty-handler
- 4.1.70.Final
+ 4.1.94.Final
com.datastax.cassandra
cassandra-driver-core
- 3.11.0
+ 3.11.5
com.fasterxml.jackson.core
diff --git a/src/server/src/main/docker/Dockerfile b/src/server/src/main/docker/Dockerfile
index 11df35ef3..3bc4be82f 100644
--- a/src/server/src/main/docker/Dockerfile
+++ b/src/server/src/main/docker/Dockerfile
@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-FROM amazoncorretto:11.0.22-alpine
+FROM amazoncorretto:11.0.25-alpine
ARG SHADED_JAR
@@ -84,6 +84,7 @@ ENV REAPER_SEGMENT_COUNT_PER_NODE=64 \
REAPER_HTTP_MANAGEMENT_ENABLE="false" \
REAPER_HTTP_MANAGEMENT_KEYSTORE_PATH="" \
REAPER_HTTP_MANAGEMENT_TRUSTSTORE_PATH="" \
+ REAPER_HTTP_MANAGEMENT_TRUSTSTORES_DIR="" \
REAPER_TMP_DIRECTORY="/var/tmp/cassandra-reaper" \
REAPER_MEMORY_STORAGE_DIRECTORY="/var/lib/cassandra-reaper/storage"
diff --git a/src/server/src/main/docker/cassandra-reaper.yml b/src/server/src/main/docker/cassandra-reaper.yml
index 5836e445c..1fde85785 100644
--- a/src/server/src/main/docker/cassandra-reaper.yml
+++ b/src/server/src/main/docker/cassandra-reaper.yml
@@ -86,4 +86,5 @@ httpManagement:
mgmtApiMetricsPort: ${REAPER_MGMT_API_METRICS_PORT}
keystore: ${REAPER_HTTP_MANAGEMENT_KEYSTORE_PATH}
truststore: ${REAPER_HTTP_MANAGEMENT_TRUSTSTORE_PATH}
+ truststoresDir: ${REAPER_HTTP_MANAGEMENT_TRUSTSTORES_DIR}
diff --git a/src/server/src/main/java/io/cassandrareaper/ReaperApplication.java b/src/server/src/main/java/io/cassandrareaper/ReaperApplication.java
index 110bc844f..ed69e14e7 100644
--- a/src/server/src/main/java/io/cassandrareaper/ReaperApplication.java
+++ b/src/server/src/main/java/io/cassandrareaper/ReaperApplication.java
@@ -50,6 +50,8 @@
import io.cassandrareaper.storage.IDistributedStorage;
import io.cassandrareaper.storage.InitializeStorage;
+import java.nio.file.Files;
+import java.nio.file.Paths;
import java.util.EnumSet;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
@@ -341,8 +343,9 @@ private void maybeInitializeSidecarMode(ClusterResource addClusterResource) thro
private boolean selfRegisterClusterForSidecar(ClusterResource addClusterResource, String seedHost)
throws ReaperException {
- final Optional cluster = addClusterResource.findClusterWithSeedHost(seedHost, Optional.empty(),
- Optional.empty());
+ final Optional cluster = addClusterResource.findClusterWithSeedHost(
+ seedHost, Optional.empty(),Optional.empty()
+ );
if (!cluster.isPresent()) {
return false;
}
@@ -427,6 +430,19 @@ private void checkConfiguration(ReaperApplicationConfiguration config) {
LOG.debug("repairParallelism: {}", config.getRepairParallelism());
LOG.debug("hangingRepairTimeoutMins: {}", config.getHangingRepairTimeoutMins());
LOG.debug("jmxPorts: {}", config.getJmxPorts());
+
+ if (config.getHttpManagement() != null) {
+ if (config.getHttpManagement().isEnabled()) {
+ if (config.getHttpManagement().getTruststoresDir() != null) {
+ if (!Files.exists(Paths.get(config.getHttpManagement().getTruststoresDir()))) {
+ throw new RuntimeException(String.format(
+ "HttpManagement truststores directory is configured as %s but it does not exist",
+ config.getHttpManagement().getTruststoresDir()
+ ));
+ }
+ }
+ }
+ }
}
private void tryInitializeStorage(ReaperApplicationConfiguration config, Environment environment)
diff --git a/src/server/src/main/java/io/cassandrareaper/ReaperApplicationConfiguration.java b/src/server/src/main/java/io/cassandrareaper/ReaperApplicationConfiguration.java
index 45a004906..705b78f9c 100644
--- a/src/server/src/main/java/io/cassandrareaper/ReaperApplicationConfiguration.java
+++ b/src/server/src/main/java/io/cassandrareaper/ReaperApplicationConfiguration.java
@@ -753,6 +753,9 @@ public static final class HttpManagement {
@JsonProperty
private String truststore;
+ @JsonProperty
+ private String truststoresDir;
+
@JsonProperty
private Integer mgmtApiMetricsPort;
@@ -772,6 +775,10 @@ public String getTruststore() {
return truststore;
}
+ public String getTruststoresDir() {
+ return truststoresDir;
+ }
+
@VisibleForTesting
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
@@ -787,6 +794,11 @@ public void setTruststore(String truststore) {
this.truststore = truststore;
}
+ @VisibleForTesting
+ public void setTruststoresDir(String truststoresDir) {
+ this.truststoresDir = truststoresDir;
+ }
+
public int getMgmtApiMetricsPort() {
return mgmtApiMetricsPort == null ? DEFAULT_MGMT_API_METRICS_PORT : mgmtApiMetricsPort;
}
diff --git a/src/server/src/main/java/io/cassandrareaper/management/ClusterFacade.java b/src/server/src/main/java/io/cassandrareaper/management/ClusterFacade.java
index 2745008f7..71aac259b 100644
--- a/src/server/src/main/java/io/cassandrareaper/management/ClusterFacade.java
+++ b/src/server/src/main/java/io/cassandrareaper/management/ClusterFacade.java
@@ -893,12 +893,10 @@ public ICassandraManagementProxy connect(Node node, Collection endpoints
private ICassandraManagementProxy connectImpl(Cluster cluster, Collection endpoints)
throws ReaperException {
try {
- ICassandraManagementProxy proxy = context.managementConnectionFactory
- .connectAny(
- endpoints
- .stream()
- .map(host -> Node.builder().withCluster(cluster).withHostname(host).build())
- .collect(Collectors.toList()));
+ ICassandraManagementProxy proxy = context.managementConnectionFactory.connectAny(endpoints.stream()
+ .map(host -> Node.builder().withCluster(cluster).withHostname(host).build())
+ .collect(Collectors.toList())
+ );
Async.markClusterActive(cluster, context);
return proxy;
diff --git a/src/server/src/main/java/io/cassandrareaper/management/http/HttpManagementConnectionFactory.java b/src/server/src/main/java/io/cassandrareaper/management/http/HttpManagementConnectionFactory.java
index 0925a8788..47f76bbba 100644
--- a/src/server/src/main/java/io/cassandrareaper/management/http/HttpManagementConnectionFactory.java
+++ b/src/server/src/main/java/io/cassandrareaper/management/http/HttpManagementConnectionFactory.java
@@ -77,6 +77,11 @@
public class HttpManagementConnectionFactory implements IManagementConnectionFactory {
private static final char[] KEYSTORE_PASSWORD = "changeit".toCharArray();
+
+ private static final String KEYSTORE_COMPONENT_NAME = "keystore.jks";
+
+ private static final String TRUSTSTORE_COMPONENT_NAME = "truststore.jks";
+
private static final Logger LOG = LoggerFactory.getLogger(HttpManagementConnectionFactory.class);
private static final ConcurrentMap HTTP_CONNECTIONS = Maps.newConcurrentMap();
private final MetricRegistry metricRegistry;
@@ -95,13 +100,22 @@ public HttpManagementConnectionFactory(AppContext context, ScheduledExecutorServ
this.config = context.config;
registerConnectionsGauge();
this.jobStatusPollerExecutor = jobStatusPollerExecutor;
- if (context.config.getHttpManagement().getKeystore() != null && !context.config.getHttpManagement().getKeystore()
- .isEmpty()) {
- try {
- createSslWatcher();
- } catch (IOException e) {
- throw new RuntimeException(e);
+
+ String ts = context.config.getHttpManagement().getTruststore();
+ boolean watchTruststore = ts != null && !ts.isEmpty();
+ String ks = context.config.getHttpManagement().getKeystore();
+ boolean watchKeystore = ks != null && !ks.isEmpty();
+ String tsd = context.config.getHttpManagement().getTruststoresDir();
+ boolean watchTruststoreDir = tsd != null && !tsd.isEmpty() && Files.isDirectory(Paths.get(tsd));
+
+ try {
+ if (watchKeystore || watchTruststore || watchTruststoreDir) {
+ createSslWatcher(watchTruststore, watchKeystore, watchTruststoreDir);
+ } else {
+ LOG.debug("Not setting up any SSL watchers");
}
+ } catch (IOException e) {
+ throw new RuntimeException(e);
}
}
@@ -170,19 +184,26 @@ private HttpCassandraManagementProxy connectImpl(Node node)
@Override
public HttpCassandraManagementProxy apply(@Nullable String hostName) {
ReaperApplicationConfiguration.HttpManagement httpConfig = config.getHttpManagement();
- boolean useMtls = httpConfig.getKeystore() != null && !httpConfig.getKeystore().isEmpty();
+
+ boolean useMtls = (httpConfig.getKeystore() != null && !httpConfig.getKeystore().isEmpty())
+ || (httpConfig.getTruststoresDir() != null && !httpConfig.getTruststoresDir().isEmpty());
OkHttpClient.Builder clientBuilder = new OkHttpClient().newBuilder();
String protocol = "http";
+
if (useMtls) {
+
+ Path truststoreName = getTruststoreComponentPath(node, TRUSTSTORE_COMPONENT_NAME);
+ Path keystoreName = getTruststoreComponentPath(node, KEYSTORE_COMPONENT_NAME);
+
LOG.debug("Using TLS connection to " + node.getHostname());
// We have to split TrustManagers to its own function to please OkHttpClient
TrustManager[] trustManagers;
SSLContext sslContext;
try {
- trustManagers = getTrustManagers();
- sslContext = createSslContext(trustManagers);
+ trustManagers = getTrustManagers(truststoreName);
+ sslContext = createSslContext(trustManagers, keystoreName);
} catch (ReaperException e) {
LOG.error("Failed to create SSLContext: " + e.getLocalizedMessage(), e);
throw new RuntimeException(e);
@@ -218,8 +239,7 @@ public HttpCassandraManagementProxy apply(@Nullable String hostName) {
}
@VisibleForTesting
- SSLContext createSslContext(TrustManager[] tms) throws ReaperException {
- Path keyStorePath = Paths.get(config.getHttpManagement().getKeystore());
+ SSLContext createSslContext(TrustManager[] tms, Path keyStorePath) throws ReaperException {
try (InputStream ksIs = Files.newInputStream(keyStorePath, StandardOpenOption.READ)) {
@@ -238,8 +258,11 @@ SSLContext createSslContext(TrustManager[] tms) throws ReaperException {
}
}
- private TrustManager[] getTrustManagers() throws ReaperException {
- Path trustStorePath = Paths.get(config.getHttpManagement().getTruststore());
+ @VisibleForTesting
+ TrustManager[] getTrustManagers(Path trustStorePath) throws ReaperException {
+
+ LOG.trace(String.format("Calling getSingleTrustManager with %s", trustStorePath));
+
try (InputStream tsIs = Files.newInputStream(trustStorePath, StandardOpenOption.READ)) {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(tsIs, KEYSTORE_PASSWORD);
@@ -249,30 +272,67 @@ private TrustManager[] getTrustManagers() throws ReaperException {
return tmf.getTrustManagers();
} catch (IOException | NoSuchAlgorithmException | KeyStoreException | CertificateException e) {
- throw new ReaperException(e);
+ throw new ReaperException("Error loading trust managers");
+ }
+ }
+
+ @VisibleForTesting
+ Path getTruststoreComponentPath(Node node, String truststoreComponentName) {
+ Path trustStorePath;
+
+ String clusterName = node.getClusterName();
+ // the cluster name is not available, or we don't have the per-cluster truststores
+ // we fall back to the global trust stores
+ if (clusterName.equals("") || config.getHttpManagement().getTruststoresDir() == null) {
+
+ trustStorePath = truststoreComponentName.equals(TRUSTSTORE_COMPONENT_NAME)
+ ? Paths.get(config.getHttpManagement().getTruststore()).toAbsolutePath()
+ : Paths.get(config.getHttpManagement().getKeystore()).toAbsolutePath();
+ } else {
+ // load a cluster-specific trust store otherwise
+ Path storesRootPath = Paths.get(config.getHttpManagement().getTruststoresDir());
+ trustStorePath = storesRootPath
+ .resolve(String.format("%s-%s", clusterName, truststoreComponentName))
+ .toAbsolutePath();
}
+
+ return trustStorePath;
}
@VisibleForTesting
- void createSslWatcher() throws IOException {
+ void createSslWatcher(boolean watchTruststore, boolean watchKeystore, boolean watchTruststoreDir) throws IOException {
+
WatchService watchService = FileSystems.getDefault().newWatchService();
- Path trustStorePath = Paths.get(config.getHttpManagement().getTruststore());
- Path keyStorePath = Paths.get(config.getHttpManagement().getKeystore());
- Path keystoreParent = trustStorePath.getParent();
- Path trustStoreParent = keyStorePath.getParent();
-
- keystoreParent.register(
- watchService,
- StandardWatchEventKinds.ENTRY_CREATE,
- StandardWatchEventKinds.ENTRY_DELETE,
- StandardWatchEventKinds.ENTRY_MODIFY);
-
- if (!keystoreParent.equals(trustStoreParent)) {
- trustStoreParent.register(
+
+ Path trustStorePath = watchTruststore ? Paths.get(config.getHttpManagement().getTruststore()) : null;
+ Path keyStorePath = watchKeystore ? Paths.get(config.getHttpManagement().getKeystore()) : null ;
+ Path truststoreDirPath = watchTruststoreDir ? Paths.get(config.getHttpManagement().getTruststoresDir()) : null ;
+
+ if (watchKeystore) {
+ keyStorePath.getParent().register(
+ watchService,
+ StandardWatchEventKinds.ENTRY_CREATE,
+ StandardWatchEventKinds.ENTRY_DELETE,
+ StandardWatchEventKinds.ENTRY_MODIFY
+ );
+ }
+ if (watchTruststore && watchKeystore) {
+ if (!trustStorePath.getParent().equals(keyStorePath.getParent())) {
+ trustStorePath.getParent().register(
+ watchService,
+ StandardWatchEventKinds.ENTRY_CREATE,
+ StandardWatchEventKinds.ENTRY_DELETE,
+ StandardWatchEventKinds.ENTRY_MODIFY
+ );
+ }
+ }
+ if (watchTruststoreDir) {
+ truststoreDirPath.register(
watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
- StandardWatchEventKinds.ENTRY_MODIFY);
+ StandardWatchEventKinds.ENTRY_MODIFY
+ );
}
ExecutorService executorService = Executors.newSingleThreadExecutor();
@@ -290,11 +350,23 @@ void createSslWatcher() throws IOException {
WatchEvent ev = (WatchEvent) event;
Path eventFilename = ev.context();
- if (keystoreParent.resolve(eventFilename).equals(keyStorePath)
- || trustStoreParent.resolve(eventFilename).equals(trustStorePath)) {
- // Something in the TLS has been modified.. recreate HTTP connections
- reloadNeeded = true;
+ if (watchKeystore) {
+ if (keyStorePath.getParent().resolve(eventFilename).equals(keyStorePath)) {
+ reloadNeeded = true;
+ }
+ }
+ if (watchTruststore) {
+ if (trustStorePath.getParent().resolve(eventFilename).equals(trustStorePath)) {
+ // Something in the TLS has been modified.. recreate HTTP connections
+ reloadNeeded = true;
+ }
}
+ if (watchTruststoreDir) {
+ if (eventFilename.toString().endsWith(".jks")) {
+ reloadNeeded = true;
+ }
+ }
+
}
if (!key.reset()) {
// The watched directories have disappeared..
diff --git a/src/server/src/main/java/io/cassandrareaper/resources/ClusterResource.java b/src/server/src/main/java/io/cassandrareaper/resources/ClusterResource.java
index 552649329..f769e66ea 100644
--- a/src/server/src/main/java/io/cassandrareaper/resources/ClusterResource.java
+++ b/src/server/src/main/java/io/cassandrareaper/resources/ClusterResource.java
@@ -220,7 +220,9 @@ public Response addOrUpdateCluster(
@QueryParam("jmxPort") Optional jmxPort) {
LOG.info("POST addOrUpdateCluster called with seedHost: {}", seedHost.orElse(null));
- return addOrUpdateCluster(uriInfo, Optional.empty(), seedHost, jmxPort, Optional.empty(), Optional.empty());
+ return addOrUpdateCluster(
+ uriInfo, Optional.empty(), seedHost, jmxPort, Optional.empty(), Optional.empty()
+ );
}
@POST
@@ -248,7 +250,9 @@ public Response addOrUpdateCluster(
"PUT addOrUpdateCluster called with: cluster_name = {}, seedHost = {}",
clusterName, seedHost.orElse(null));
- return addOrUpdateCluster(uriInfo, Optional.of(clusterName), seedHost, jmxPort, Optional.empty(), Optional.empty());
+ return addOrUpdateCluster(
+ uriInfo, Optional.of(clusterName), seedHost, jmxPort, Optional.empty(), Optional.empty()
+ );
}
@PUT
@@ -297,8 +301,10 @@ private Response addOrUpdateCluster(
}
}
- final Optional cluster = findClusterWithSeedHost(seedHost.get(), jmxPort,
- Optional.ofNullable(jmxCredentials));
+ final Optional cluster = findClusterWithSeedHost(
+ seedHost.get(), jmxPort, Optional.ofNullable(jmxCredentials)
+ );
+
if (!cluster.isPresent()) {
return Response
.status(Response.Status.BAD_REQUEST)
@@ -361,9 +367,11 @@ private Response addOrUpdateCluster(
return Response.created(location).build();
}
- public Optional findClusterWithSeedHost(String seedHost,
- Optional jmxPort,
- Optional jmxCredentials) {
+ public Optional findClusterWithSeedHost(
+ String seedHost,
+ Optional jmxPort,
+ Optional jmxCredentials
+ ) {
Set seedHosts = parseSeedHosts(seedHost);
try {
Cluster.Builder clusterBuilder = Cluster.builder()
diff --git a/src/server/src/test/java/io/cassandrareaper/management/http/HttpCassandraManagementProxyTest.java b/src/server/src/test/java/io/cassandrareaper/management/http/HttpCassandraManagementProxyTest.java
index 19f1ea7a7..2a9d555e3 100644
--- a/src/server/src/test/java/io/cassandrareaper/management/http/HttpCassandraManagementProxyTest.java
+++ b/src/server/src/test/java/io/cassandrareaper/management/http/HttpCassandraManagementProxyTest.java
@@ -20,6 +20,7 @@
import io.cassandrareaper.AppContext;
import io.cassandrareaper.ReaperApplicationConfiguration;
import io.cassandrareaper.ReaperException;
+import io.cassandrareaper.core.Cluster;
import io.cassandrareaper.core.GenericMetric;
import io.cassandrareaper.core.Node;
import io.cassandrareaper.core.RepairType;
@@ -31,9 +32,9 @@
import java.math.BigInteger;
import java.net.InetSocketAddress;
-import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -59,6 +60,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.MoreExecutors;
import org.apache.cassandra.repair.RepairParallelism;
@@ -817,11 +819,11 @@ public void testSSLHotReload() throws Exception {
context.config = config;
Path tempDirectory = Files.createTempDirectory("reload-test");
- // handle running tests locally vs. via GitHub actions
- // Path ks = Paths.get("/home/runner/work/cassandra-reaper/cassandra-reaper/.github/files/keystore.jks");
- // Path ts = Paths.get("/home/runner/work/cassandra-reaper/cassandra-reaper/.github/files/truststore.jks");
- // use relative paths as we are likely running locally
- Path projectRoot = FileSystems.getDefault().getPath("..", "..", "..");
+
+ Path projectRoot = Paths.get(".").toAbsolutePath();
+ while (!projectRoot.endsWith("cassandra-reaper")) {
+ projectRoot = projectRoot.getParent();
+ }
Path ks = projectRoot.resolve(".github/files/keystore.jks");
Path ts = projectRoot.resolve(".github/files/truststore.jks");
@@ -833,17 +835,99 @@ public void testSSLHotReload() throws Exception {
config.getHttpManagement().setEnabled(true);
config.getHttpManagement().setKeystore(ksCopy.toAbsolutePath().toString());
config.getHttpManagement().setTruststore(tsCopy.toAbsolutePath().toString());
+
+ Path storesRoot = Files.createDirectory(tempDirectory.resolve("stores"));
+ Path clustersStore = Files.createDirectory(storesRoot.resolve("cluster1"));
+ Files.copy(ts, clustersStore.resolve(ts.getFileName()));
+ Files.copy(ks, clustersStore.resolve(ks.getFileName()));
+ config.getHttpManagement().setTruststoresDir(storesRoot.toAbsolutePath().toString());
+
HttpManagementConnectionFactory connectionFactory = new HttpManagementConnectionFactory(context, null);
HttpManagementConnectionFactory spy = spy(connectionFactory);
- spy.createSslWatcher();
+ spy.createSslWatcher(true, true, true);
verify(spy, Mockito.timeout(1000)).clearHttpConnections();
// Modify filepaths
Files.delete(ksCopy);
+ Files.delete(clustersStore.resolve(ts.getFileName()));
- // We need 3 invocations, because we can't spy the original constructor call to the clearHttpConnections() and
+ // We need 4 invocations, because we can't spy the original constructor call to the clearHttpConnections() and
// as such need to create more SslWatchers() for the same path
- verify(spy, Mockito.timeout(30000).atLeast(2)).clearHttpConnections();
+ verify(spy, Mockito.timeout(30000).atLeast(3)).clearHttpConnections();
+ }
+
+ @Test
+ public void testGetTruststore() throws Exception {
+
+ Path projectRoot = Paths.get(".").toAbsolutePath();
+ while (!projectRoot.endsWith("cassandra-reaper")) {
+ projectRoot = projectRoot.getParent();
+ }
+ Path ks = projectRoot.resolve(".github/files/keystore.jks");
+ Path ts = projectRoot.resolve(".github/files/truststore.jks");
+
+ Path tempDirectory = Files.createTempDirectory("get-truststore-test");
+ Files.copy(ks, tempDirectory.resolve("keystore.jks"));
+ Files.copy(ts, tempDirectory.resolve("truststore.jks"));
+
+ Path perClusterStores = tempDirectory.resolve(Paths.get("perClusterStores"));
+ Files.createDirectory(perClusterStores.toAbsolutePath());
+ Files.copy(ks, perClusterStores.resolve("testCluster-keystore.jks"));
+ Files.copy(ts, perClusterStores .resolve("testCluster-truststore.jks"));
+
+ ReaperApplicationConfiguration config = new ReaperApplicationConfiguration();
+ config.getHttpManagement().setTruststoresDir(perClusterStores.toAbsolutePath().toString());
+
+ String tempTrustStore = tempDirectory.resolve("truststore.jks").toAbsolutePath().toString();
+ String tempKeyStore = tempDirectory.resolve("keystore.jks").toAbsolutePath().toString();
+ config.getHttpManagement().setTruststore(tempTrustStore);
+ config.getHttpManagement().setKeystore(tempKeyStore);
+
+ AppContext context = mock(AppContext.class);
+ context.config = config;
+ HttpManagementConnectionFactory connectionFactory = new HttpManagementConnectionFactory(context, null);
+
+ // if we specify a cluster name together with the seed host, Reaper will look for that cluster's trust stores
+ String seedHost = "testHost@testCluster";
+
+ Cluster clusterWithName = Cluster.builder()
+ .withName("testCluster")
+ .withSeedHosts(ImmutableSet.of(seedHost))
+ .build();
+ Node node = Node.builder()
+ .withHostname(seedHost)
+ .withCluster(clusterWithName)
+ .build();
+
+ Path expected = tempDirectory
+ .resolve("perClusterStores")
+ // something somewhere is doing a .lower() on the cluster name
+ .resolve("testcluster-truststore.jks");
+ Path actual = connectionFactory.getTruststoreComponentPath(node, "truststore.jks");
+ assertEquals(expected, actual);
+
+ expected = tempDirectory.resolve("perClusterStores").resolve("testcluster-keystore.jks");
+ actual = connectionFactory.getTruststoreComponentPath(node, "keystore.jks");
+ assertEquals(expected, actual);
+
+ // but if we don't provide the cluster name, reaper will we use the general one
+ Cluster clusterWithoutTruststore = Cluster.builder()
+ .withName("")
+ .withSeedHosts(ImmutableSet.of("testHost"))
+ .build();
+ node = Node.builder()
+ .withHostname("testHost")
+ .withCluster(clusterWithoutTruststore)
+ .build();
+
+ expected = tempDirectory.resolve("truststore.jks");
+ actual = connectionFactory.getTruststoreComponentPath(node, "truststore.jks");
+ assertEquals(expected, actual);
+
+ expected = tempDirectory.resolve("keystore.jks");
+ actual = connectionFactory.getTruststoreComponentPath(node, "keystore.jks");
+ assertEquals(expected, actual);
}
+
}
diff --git a/src/server/src/test/java/io/cassandrareaper/resources/ClusterResourceTest.java b/src/server/src/test/java/io/cassandrareaper/resources/ClusterResourceTest.java
index df472e229..56e370c36 100644
--- a/src/server/src/test/java/io/cassandrareaper/resources/ClusterResourceTest.java
+++ b/src/server/src/test/java/io/cassandrareaper/resources/ClusterResourceTest.java
@@ -471,8 +471,15 @@ public void testModifyClusterSeeds() throws ReaperException {
ClusterResource clusterResource
= ClusterResource.create(mocks.context, mocks.cryptograph, mocks.context.storage.getEventsDao(),
mocks.context.storage.getRepairRunDao());
- clusterResource.addOrUpdateCluster(mocks.uriInfo, Optional.of(SEED_HOST), Optional.of(Cluster.DEFAULT_JMX_PORT),
- Optional.of(JMX_USERNAME), Optional.of(JMX_PASSWORD));
+
+ clusterResource.addOrUpdateCluster(
+ mocks.uriInfo,
+ Optional.of(SEED_HOST),
+ Optional.of(Cluster.DEFAULT_JMX_PORT),
+ Optional.of(JMX_USERNAME),
+ Optional.of(JMX_PASSWORD)
+ );
+
doReturn(Arrays.asList(SEED_HOST + 1, SEED_HOST)).when(mocks.cassandraManagementProxy).getLiveNodes();
Response response = clusterResource
@@ -495,7 +502,8 @@ public void testModifyClusterSeeds() throws ReaperException {
Optional.of(SEED_HOST + 1),
Optional.of(Cluster.DEFAULT_JMX_PORT),
Optional.of(JMX_USERNAME),
- Optional.of(JMX_PASSWORD));
+ Optional.of(JMX_PASSWORD)
+ );
assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus());
assertTrue(response.getLocation().toString().endsWith("/cluster/" + cluster.getName()));
@@ -510,8 +518,14 @@ public void testModifyClusterSeedsWithClusterName() throws ReaperException {
mocks.context.storage.getEventsDao(),
mocks.context.storage.getRepairRunDao());
;
- clusterResource.addOrUpdateCluster(mocks.uriInfo, Optional.of(SEED_HOST), Optional.of(Cluster.DEFAULT_JMX_PORT),
- Optional.of(JMX_USERNAME), Optional.of(JMX_PASSWORD));
+ clusterResource.addOrUpdateCluster(
+ mocks.uriInfo,
+ Optional.of(SEED_HOST),
+ Optional.of(Cluster.DEFAULT_JMX_PORT),
+ Optional.of(JMX_USERNAME),
+ Optional.of(JMX_PASSWORD)
+ );
+
doReturn(Arrays.asList(SEED_HOST + 1, SEED_HOST)).when(mocks.cassandraManagementProxy).getLiveNodes();
Response response = clusterResource.addOrUpdateCluster(
@@ -565,9 +579,13 @@ public void addingAClusterAutomaticallySetupSchedulingRepairsWhenEnabled() throw
mocks.context.storage.getRepairRunDao());
;
- Response response = clusterResource
- .addOrUpdateCluster(mocks.uriInfo, Optional.of(SEED_HOST), Optional.of(Cluster.DEFAULT_JMX_PORT),
- Optional.of(JMX_USERNAME), Optional.of(JMX_PASSWORD));
+ Response response = clusterResource.addOrUpdateCluster(
+ mocks.uriInfo,
+ Optional.of(SEED_HOST),
+ Optional.of(Cluster.DEFAULT_JMX_PORT),
+ Optional.of(JMX_USERNAME),
+ Optional.of(JMX_PASSWORD)
+ );
assertEquals(HttpStatus.CREATED_201, response.getStatus());
assertEquals(1, mocks.context.storage.getRepairScheduleDao().getAllRepairSchedules().size());
@@ -599,9 +617,13 @@ public void testClusterDeleting() throws Exception {
mocks.context.storage.getRepairRunDao());
;
- Response response = clusterResource
- .addOrUpdateCluster(mocks.uriInfo, Optional.of(SEED_HOST), Optional.of(Cluster.DEFAULT_JMX_PORT),
- Optional.of(JMX_USERNAME), Optional.of(JMX_PASSWORD));
+ Response response = clusterResource.addOrUpdateCluster(
+ mocks.uriInfo,
+ Optional.of(SEED_HOST),
+ Optional.of(Cluster.DEFAULT_JMX_PORT),
+ Optional.of(JMX_USERNAME),
+ Optional.of(JMX_PASSWORD)
+ );
assertEquals(HttpStatus.CREATED_201, response.getStatus());
assertEquals(1, mocks.context.storage.getRepairScheduleDao().getAllRepairSchedules().size());
diff --git a/src/server/src/test/resources/cassandra-reaper.yaml b/src/server/src/test/resources/cassandra-reaper.yaml
index 2fab62d98..59fe46015 100644
--- a/src/server/src/test/resources/cassandra-reaper.yaml
+++ b/src/server/src/test/resources/cassandra-reaper.yaml
@@ -114,4 +114,4 @@ cryptograph:
type: symmetric
systemPropertySecret: REAPER_ENCRYPTION_KEY
-persistenceStoragePath: /tmp/reaper/storage/
\ No newline at end of file
+persistenceStoragePath: /tmp/reaper/storage/