This is an automated email from the ASF dual-hosted git repository.
frankgh pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-sidecar.git
The following commit(s) were added to refs/heads/trunk by this push:
new c1b572e3 CASSSIDECAR-253: add start/stop binary transport&gossip (#225)
c1b572e3 is described below
commit c1b572e3271aeb022bdefe588ecc0648546eab63
Author: Yuntong Qu <[email protected]>
AuthorDate: Sun Jun 22 18:42:53 2025 -0400
CASSSIDECAR-253: add start/stop binary transport&gossip (#225)
Patch by Yuntong Qu; reviewed by Francisco Guerrero; Saranya Krishnakumar;
Yifan Cai; Arjun Ashok for CASSSIDECAR-253
---
.../adapters/base/CassandraStorageOperations.java | 40 +++++
.../jmx/GossipDependentStorageJmxOperations.java | 24 +++
.../adapters/base/jmx/StorageJmxOperations.java | 20 +++
.../cassandra/sidecar/common/ApiEndpointsV1.java | 7 +-
.../sidecar/common/request/GossipInfoRequest.java | 2 +-
...ipInfoRequest.java => GossipUpdateRequest.java} | 29 ++--
...ipInfoRequest.java => NativeUpdateRequest.java} | 31 ++--
.../request/data/NodeCommandRequestPayload.java | 103 ++++++++++++
.../cassandra/sidecar/client/RequestContext.java | 28 ++++
.../cassandra/sidecar/client/SidecarClient.java | 42 +++++
.../sidecar/client/SidecarClientTest.java | 39 ++++-
.../request/GossipInfoRequestTestParameters.java | 2 +-
.../NodeGossipAndNativeModifyIntegrationTest.java | 128 +++++++++++++++
.../sidecar/common/server/StorageOperations.java | 20 +++
.../acl/authorization/BasicPermissions.java | 3 +
.../sidecar/handlers/CassandraHealthHandler.java | 4 +-
.../sidecar/handlers/GossipHealthHandler.java | 4 +-
.../sidecar/handlers/GossipUpdateHandler.java | 87 +++++++++++
.../sidecar/handlers/NativeUpdateHandler.java | 88 +++++++++++
.../sidecar/handlers/NodeCommandHandler.java | 76 +++++++++
.../sidecar/handlers/ReportSchemaHandler.java | 2 +-
.../handlers/cdc/DeleteServiceConfigHandler.java | 2 +-
.../cassandra/sidecar/modules/ApiModule.java | 9 ++
.../sidecar/modules/CassandraOperationsModule.java | 24 +++
.../sidecar/modules/HealthCheckModule.java | 8 +-
.../modules/multibindings/VertxRouteMapKeys.java | 13 +-
.../sidecar/handlers/GossipUpdateHandlerTest.java | 173 +++++++++++++++++++++
.../sidecar/handlers/NativeUpdateHandlerTest.java | 169 ++++++++++++++++++++
.../sidecar/handlers/RouteBuilderTest.java | 2 +-
29 files changed, 1137 insertions(+), 42 deletions(-)
diff --git
a/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/CassandraStorageOperations.java
b/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/CassandraStorageOperations.java
index 88635d80..bcf06677 100644
---
a/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/CassandraStorageOperations.java
+++
b/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/CassandraStorageOperations.java
@@ -254,4 +254,44 @@ public class CassandraStorageOperations implements
StorageOperations
return jmxClient.proxy(StorageJmxOperations.class,
STORAGE_SERVICE_OBJ_NAME)
.getClusterName();
}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void stopNativeTransport()
+ {
+ jmxClient.proxy(StorageJmxOperations.class, STORAGE_SERVICE_OBJ_NAME)
+ .stopNativeTransport();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void startNativeTransport()
+ {
+ jmxClient.proxy(StorageJmxOperations.class, STORAGE_SERVICE_OBJ_NAME)
+ .startNativeTransport();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void stopGossiping()
+ {
+ jmxClient.proxy(StorageJmxOperations.class, STORAGE_SERVICE_OBJ_NAME)
+ .stopGossiping();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void startGossiping()
+ {
+ jmxClient.proxy(StorageJmxOperations.class, STORAGE_SERVICE_OBJ_NAME)
+ .startGossiping();
+ }
}
diff --git
a/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/jmx/GossipDependentStorageJmxOperations.java
b/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/jmx/GossipDependentStorageJmxOperations.java
index 526e8a03..7d1ecc73 100644
---
a/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/jmx/GossipDependentStorageJmxOperations.java
+++
b/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/jmx/GossipDependentStorageJmxOperations.java
@@ -177,4 +177,28 @@ public class GossipDependentStorageJmxOperations
implements StorageJmxOperations
{
return delegate.getClusterName();
}
+
+ @Override
+ public void stopNativeTransport()
+ {
+ delegate.stopNativeTransport();
+ }
+
+ @Override
+ public void startNativeTransport()
+ {
+ delegate.startNativeTransport();
+ }
+
+ @Override
+ public void stopGossiping()
+ {
+ delegate.stopGossiping();
+ }
+
+ @Override
+ public void startGossiping()
+ {
+ delegate.startGossiping();
+ }
}
diff --git
a/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/jmx/StorageJmxOperations.java
b/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/jmx/StorageJmxOperations.java
index 37cff021..a3517d7a 100644
---
a/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/jmx/StorageJmxOperations.java
+++
b/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/jmx/StorageJmxOperations.java
@@ -178,4 +178,24 @@ public interface StorageJmxOperations
* @return the name of the cluster
*/
String getClusterName();
+
+ /**
+ * Triggers stop native transport
+ */
+ void stopNativeTransport();
+
+ /**
+ * Triggers start native transport
+ */
+ void startNativeTransport();
+
+ /**
+ * Triggers stop gossip
+ */
+ void stopGossiping();
+
+ /**
+ * Triggers start gossip
+ */
+ void startGossiping();
}
diff --git
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
index cd1c62ab..cd47c812 100644
---
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
+++
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
@@ -59,7 +59,8 @@ public final class ApiEndpointsV1
*/
@Deprecated
public static final String CASSANDRA_HEALTH_ROUTE = API_V1 + CASSANDRA +
HEALTH;
- public static final String CASSANDRA_NATIVE_HEALTH_ROUTE = API_V1 +
CASSANDRA + NATIVE + HEALTH;
+ public static final String CASSANDRA_NATIVE_ROUTE = API_V1 + CASSANDRA +
NATIVE;
+ public static final String CASSANDRA_NATIVE_HEALTH_ROUTE =
CASSANDRA_NATIVE_ROUTE + HEALTH;
public static final String CASSANDRA_JMX_HEALTH_ROUTE = API_V1 + CASSANDRA
+ JMX + HEALTH;
@Deprecated // NOTE: Uses singular forms of "keyspace" and "table"
@@ -96,8 +97,8 @@ public final class ApiEndpointsV1
public static final String SSTABLE_IMPORT_ROUTE = API_V1 + PER_UPLOAD +
PER_KEYSPACE + PER_TABLE + "/import";
public static final String SSTABLE_CLEANUP_ROUTE = API_V1 + PER_UPLOAD;
- public static final String GOSSIP_INFO_ROUTE = API_V1 + CASSANDRA +
"/gossip";
- public static final String GOSSIP_HEALTH_ROUTE = GOSSIP_INFO_ROUTE +
HEALTH;
+ public static final String GOSSIP_ROUTE = API_V1 + CASSANDRA + "/gossip";
+ public static final String GOSSIP_HEALTH_ROUTE = GOSSIP_ROUTE + HEALTH;
public static final String TIME_SKEW_ROUTE = API_V1 + "/time-skew";
public static final String KEYSPACE_TOKEN_MAPPING_ROUTE = API_V1 +
PER_KEYSPACE + "/token-range-replicas";
diff --git
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
index 4947a8f0..2f9a2920 100644
---
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
+++
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
@@ -32,7 +32,7 @@ public class GossipInfoRequest extends
JsonRequest<GossipInfoResponse>
*/
public GossipInfoRequest()
{
- super(ApiEndpointsV1.GOSSIP_INFO_ROUTE);
+ super(ApiEndpointsV1.GOSSIP_ROUTE);
}
/**
diff --git
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipUpdateRequest.java
similarity index 59%
copy from
client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
copy to
client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipUpdateRequest.java
index 4947a8f0..0482fb1f 100644
---
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
+++
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipUpdateRequest.java
@@ -20,27 +20,36 @@ package org.apache.cassandra.sidecar.common.request;
import io.netty.handler.codec.http.HttpMethod;
import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
-import org.apache.cassandra.sidecar.common.response.GossipInfoResponse;
+import
org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
+import org.apache.cassandra.sidecar.common.response.HealthResponse;
/**
- * Represents a request to retrieve the Cassandra gossip information
+ * Gossip update request
*/
-public class GossipInfoRequest extends JsonRequest<GossipInfoResponse>
+public class GossipUpdateRequest extends JsonRequest<HealthResponse>
{
+ private final NodeCommandRequestPayload requestPayload;
+
/**
- * Constructs a request to retrieve the Cassandra gossip information
+ * Constructs a gossip update request with the provided parameters
+ *
+ * @param state START or STOP
*/
- public GossipInfoRequest()
+ public GossipUpdateRequest(NodeCommandRequestPayload.State state)
{
- super(ApiEndpointsV1.GOSSIP_INFO_ROUTE);
+ super(ApiEndpointsV1.GOSSIP_ROUTE);
+ this.requestPayload = new NodeCommandRequestPayload(state.toValue());
}
- /**
- * {@inheritDoc}
- */
@Override
public HttpMethod method()
{
- return HttpMethod.GET;
+ return HttpMethod.PUT;
+ }
+
+ @Override
+ public Object requestBody()
+ {
+ return requestPayload;
}
}
diff --git
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/NativeUpdateRequest.java
similarity index 58%
copy from
client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
copy to
client-common/src/main/java/org/apache/cassandra/sidecar/common/request/NativeUpdateRequest.java
index 4947a8f0..cdbd7464 100644
---
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
+++
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/NativeUpdateRequest.java
@@ -18,29 +18,40 @@
package org.apache.cassandra.sidecar.common.request;
+
import io.netty.handler.codec.http.HttpMethod;
import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
-import org.apache.cassandra.sidecar.common.response.GossipInfoResponse;
+import
org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
+import org.apache.cassandra.sidecar.common.response.HealthResponse;
/**
- * Represents a request to retrieve the Cassandra gossip information
+ * native transport update request
*/
-public class GossipInfoRequest extends JsonRequest<GossipInfoResponse>
+public class NativeUpdateRequest extends JsonRequest<HealthResponse>
{
+ private final NodeCommandRequestPayload requestPayload;
+
/**
- * Constructs a request to retrieve the Cassandra gossip information
+ * Constructs a gossip update request with the provided parameters
+ *
+ * @param state START or STOP
*/
- public GossipInfoRequest()
+ public NativeUpdateRequest(NodeCommandRequestPayload.State state)
{
- super(ApiEndpointsV1.GOSSIP_INFO_ROUTE);
+ super(ApiEndpointsV1.CASSANDRA_NATIVE_ROUTE);
+ this.requestPayload = new NodeCommandRequestPayload(state.toValue());
}
- /**
- * {@inheritDoc}
- */
@Override
public HttpMethod method()
{
- return HttpMethod.GET;
+ return HttpMethod.PUT;
+ }
+
+ @Override
+ public Object requestBody()
+ {
+ return requestPayload;
}
}
+
diff --git
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/data/NodeCommandRequestPayload.java
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/data/NodeCommandRequestPayload.java
new file mode 100644
index 00000000..9a75f4e3
--- /dev/null
+++
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/data/NodeCommandRequestPayload.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.common.request.data;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.cassandra.sidecar.common.utils.Preconditions;
+import org.apache.cassandra.sidecar.common.utils.StringUtils;
+
+/**
+ * Request payload for start/stop operations (gossip, native transport, etc.).
+ *
+ * <p>Valid JSON:</p>
+ * <pre>
+ * { "state": "start" }
+ * { "state": "stop" }
+ * </pre>
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class NodeCommandRequestPayload
+{
+ /**
+ * Node Command State
+ */
+ public enum State
+ {
+ @JsonProperty("start")
+ START,
+
+ @JsonProperty("stop")
+ STOP;
+
+ @JsonCreator
+ public static State fromString(String s)
+ {
+ if (s == null)
+ throw new IllegalArgumentException("Null state");
+
+ switch (s.trim().toLowerCase())
+ {
+ case "start":
+ return START;
+ case "stop":
+ return STOP;
+ default:
+ throw new IllegalArgumentException("Unknown state: " + s);
+ }
+ }
+
+ @JsonProperty
+ public String toValue()
+ {
+ return name().toLowerCase();
+ }
+ }
+
+ private final State state;
+
+ /**
+ * @param state the desired operation, must be "start" or "stop"
+ */
+ @JsonCreator
+ public NodeCommandRequestPayload(
+ @JsonProperty(value = "state", required = true) String state
+ )
+ {
+ Preconditions.checkArgument(StringUtils.isNotEmpty(state),
+ "state must be provided and non-empty");
+ this.state = State.fromString(state);
+ }
+
+ /**
+ * @return the parsed enum (START or STOP)
+ */
+ @JsonProperty("state")
+ public State state()
+ {
+ return state;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "NodeCommandRequestPayload{state=" + state + '}';
+ }
+}
diff --git
a/client/src/main/java/org/apache/cassandra/sidecar/client/RequestContext.java
b/client/src/main/java/org/apache/cassandra/sidecar/client/RequestContext.java
index 21ee1822..3909b74e 100644
---
a/client/src/main/java/org/apache/cassandra/sidecar/client/RequestContext.java
+++
b/client/src/main/java/org/apache/cassandra/sidecar/client/RequestContext.java
@@ -36,9 +36,11 @@ import
org.apache.cassandra.sidecar.common.request.ConnectedClientStatsRequest;
import org.apache.cassandra.sidecar.common.request.CreateSnapshotRequest;
import org.apache.cassandra.sidecar.common.request.GossipHealthRequest;
import org.apache.cassandra.sidecar.common.request.GossipInfoRequest;
+import org.apache.cassandra.sidecar.common.request.GossipUpdateRequest;
import org.apache.cassandra.sidecar.common.request.ImportSSTableRequest;
import org.apache.cassandra.sidecar.common.request.ListOperationalJobsRequest;
import org.apache.cassandra.sidecar.common.request.ListSnapshotFilesRequest;
+import org.apache.cassandra.sidecar.common.request.NativeUpdateRequest;
import org.apache.cassandra.sidecar.common.request.NodeDecommissionRequest;
import org.apache.cassandra.sidecar.common.request.NodeSettingsRequest;
import org.apache.cassandra.sidecar.common.request.OperationalJobRequest;
@@ -54,8 +56,10 @@ import
org.apache.cassandra.sidecar.common.request.TimeSkewRequest;
import org.apache.cassandra.sidecar.common.request.TokenRangeReplicasRequest;
import org.apache.cassandra.sidecar.common.request.UploadSSTableRequest;
import org.apache.cassandra.sidecar.common.request.data.Digest;
+import
org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
import org.apache.cassandra.sidecar.common.response.ListSnapshotFilesResponse;
import org.apache.cassandra.sidecar.common.utils.HttpRange;
+import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
@@ -563,6 +567,30 @@ public class RequestContext
return request(NODE_DECOMMISSION_REQUEST);
}
+ /**
+ * Sets the {@code request} to be a {@link GossipUpdateRequest} for the
+ * given {@link NodeCommandRequestPayload.State state}, and returns a
reference to this Builder enabling method chaining.
+ *
+ * @param state the desired state for gossip
+ * @return a reference to this Builder
+ */
+ public Builder nodeGossipUpdateRequest(@NotNull
NodeCommandRequestPayload.State state)
+ {
+ return request(new GossipUpdateRequest(state));
+ }
+
+ /**
+ * Sets the {@code request} to be a {@link NativeUpdateRequest} for the
+ * given {@link NodeCommandRequestPayload.State state}, and returns a
reference to this Builder enabling method chaining.
+ *
+ * @param state the desired state for native transport
+ * @return a reference to this Builder
+ */
+ public Builder nodeNativeUpdateRequest(@NotNull
NodeCommandRequestPayload.State state)
+ {
+ return request(new NativeUpdateRequest(state));
+ }
+
/**
* Sets the {@code request} to be a {@link StreamStatsRequest} and
returns a reference to this Builder
* enabling method chaining.
diff --git
a/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
b/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
index 56b3b31c..b022ef89 100644
---
a/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
+++
b/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
@@ -54,6 +54,7 @@ import
org.apache.cassandra.sidecar.common.request.data.AllServicesConfigPayload
import
org.apache.cassandra.sidecar.common.request.data.CreateRestoreJobRequestPayload;
import
org.apache.cassandra.sidecar.common.request.data.CreateSliceRequestPayload;
import org.apache.cassandra.sidecar.common.request.data.Digest;
+import
org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
import
org.apache.cassandra.sidecar.common.request.data.RestoreJobProgressRequestParams;
import
org.apache.cassandra.sidecar.common.request.data.UpdateCdcServiceConfigPayload;
import
org.apache.cassandra.sidecar.common.request.data.UpdateRestoreJobRequestPayload;
@@ -802,6 +803,47 @@ public class SidecarClient implements AutoCloseable,
SidecarClientBlobRestoreExt
.build());
}
+ /**
+ * Sends a request to start or stop Cassandra gossiping on the provided
instance.
+ * <p>
+ * This operation asynchronously triggers start or stop of gossip via
Cassandra's JMX interface.
+ * The request body must contain a JSON payload with a "state" field,
which can be either "start" or "stop".
+ * On success, the server responds with HTTP 200 OK and payload {@code
{"status":"OK"}}.
+ * </p>
+ *
+ * @param instance the instance where the request will be executed
+ * @param state the desired gossip state: {@link
NodeCommandRequestPayload.State#START} or {@link
NodeCommandRequestPayload.State#STOP}
+ * @return a CompletableFuture representing the completion of the operation
+ */
+ public CompletableFuture<HealthResponse> nodeUpdateGossip(SidecarInstance
instance, NodeCommandRequestPayload.State state)
+ {
+ return executor.executeRequestAsync(requestBuilder()
+
.singleInstanceSelectionPolicy(instance)
+ .nodeGossipUpdateRequest(state)
+ .build());
+ }
+
+ /**
+ * Sends a request to start or stop Cassandra native transport on the
provided instance.
+ * <p>
+ * This operation asynchronously triggers start or stop of native
transport via Cassandra's JMX interface.
+ * The request body must contain a JSON payload with a "state" field,
which can be either "start" or "stop".
+ * On success, the server responds with HTTP 200 OK and payload {@code
{"status":"OK"}}.
+ * </p>
+ *
+ * @param instance the instance where the request will be executed
+ * @param state the desired native transport state: {@link
NodeCommandRequestPayload.State#START} or {@link
NodeCommandRequestPayload.State#STOP}
+ * @return a CompletableFuture representing the completion of the operation
+ */
+ public CompletableFuture<HealthResponse> nodeUpdateNative(SidecarInstance
instance, NodeCommandRequestPayload.State state)
+ {
+ return executor.executeRequestAsync(requestBuilder()
+
.singleInstanceSelectionPolicy(instance)
+ .nodeNativeUpdateRequest(state)
+ .build());
+ }
+
+
/**
* Returns a copy of the request builder with the default parameters
configured for the client.
*
diff --git
a/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
b/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
index ad709b91..ad888878 100644
---
a/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
+++
b/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
@@ -72,6 +72,7 @@ import org.apache.cassandra.sidecar.common.request.Service;
import
org.apache.cassandra.sidecar.common.request.data.AllServicesConfigPayload;
import
org.apache.cassandra.sidecar.common.request.data.CreateRestoreJobRequestPayload;
import org.apache.cassandra.sidecar.common.request.data.MD5Digest;
+import
org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
import
org.apache.cassandra.sidecar.common.request.data.UpdateCdcServiceConfigPayload;
import org.apache.cassandra.sidecar.common.request.data.XXHash32Digest;
import
org.apache.cassandra.sidecar.common.response.ConnectedClientStatsResponse;
@@ -397,7 +398,7 @@ abstract class SidecarClientTest
assertThat(gossipInfo.releaseVersion()).isEqualTo("4.0.7");
assertThat(gossipInfo.sstableVersions()).isEqualTo(Collections.singletonList("big-nb"));
- validateResponseServed(ApiEndpointsV1.GOSSIP_INFO_ROUTE);
+ validateResponseServed(ApiEndpointsV1.GOSSIP_ROUTE);
}
@Test
@@ -1774,6 +1775,42 @@ abstract class SidecarClientTest
}
}
+ @Test
+ void testNodeUpdateGossip() throws Exception
+ {
+ MockResponse response = new
MockResponse().setResponseCode(200).setBody("{\"status\":\"OK\"}");
+ enqueue(response);
+
+ SidecarInstanceImpl sidecarInstance = instances.get(0);
+ HealthResponse result = client.nodeUpdateGossip(sidecarInstance,
NodeCommandRequestPayload.State.STOP).get(30, TimeUnit.SECONDS);
+ assertThat(result).isNotNull();
+ assertThat(result.status()).isEqualToIgnoringCase("OK");
+ assertThat(result.isOk()).isTrue();
+
+ validateResponseServed(ApiEndpointsV1.GOSSIP_ROUTE, request -> {
+ String requestBody = request.getBody().readUtf8();
+ assertThat(requestBody).isEqualTo("{\"state\":\"stop\"}");
+ });
+ }
+
+ @Test
+ void testNodeUpdateNative() throws Exception
+ {
+ MockResponse response = new
MockResponse().setResponseCode(200).setBody("{\"status\":\"OK\"}");
+ enqueue(response);
+
+ SidecarInstanceImpl sidecarInstance = instances.get(0);
+ HealthResponse result = client.nodeUpdateNative(sidecarInstance,
NodeCommandRequestPayload.State.STOP).get(30, TimeUnit.SECONDS);
+ assertThat(result).isNotNull();
+ assertThat(result.status()).isEqualToIgnoringCase("OK");
+ assertThat(result.isOk()).isTrue();
+
+ validateResponseServed(ApiEndpointsV1.CASSANDRA_NATIVE_ROUTE, request
-> {
+ String requestBody = request.getBody().readUtf8();
+ assertThat(requestBody).isEqualTo("{\"state\":\"stop\"}");
+ });
+ }
+
private void enqueue(MockResponse response)
{
for (MockWebServer server : servers)
diff --git
a/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/request/GossipInfoRequestTestParameters.java
b/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/request/GossipInfoRequestTestParameters.java
index 9b4b89f4..604a6692 100644
---
a/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/request/GossipInfoRequestTestParameters.java
+++
b/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/request/GossipInfoRequestTestParameters.java
@@ -61,7 +61,7 @@ public class GossipInfoRequestTestParameters implements
RequestTestParameters<Go
@Override
public String expectedEndpointPath()
{
- return ApiEndpointsV1.GOSSIP_INFO_ROUTE;
+ return ApiEndpointsV1.GOSSIP_ROUTE;
}
@Override
diff --git
a/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/routes/NodeGossipAndNativeModifyIntegrationTest.java
b/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/routes/NodeGossipAndNativeModifyIntegrationTest.java
new file mode 100644
index 00000000..622c188f
--- /dev/null
+++
b/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/routes/NodeGossipAndNativeModifyIntegrationTest.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.routes;
+
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.util.concurrent.Uninterruptibles;
+import org.junit.jupiter.api.Test;
+
+import io.vertx.core.buffer.Buffer;
+import io.vertx.ext.web.client.HttpResponse;
+import
org.apache.cassandra.sidecar.testing.SharedClusterSidecarIntegrationTestBase;
+
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static
io.netty.handler.codec.http.HttpResponseStatus.SERVICE_UNAVAILABLE;
+import static org.apache.cassandra.testing.utils.AssertionUtils.getBlocking;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Combines gossip‐toggle and native‐transport‐toggle route tests into a
single suite
+ * so that Cassandra+Sidecar come up exactly once per JVM.
+ */
+public class NodeGossipAndNativeModifyIntegrationTest extends
SharedClusterSidecarIntegrationTestBase
+{
+
+ @Test
+ void testGossipToggle()
+ {
+ // 1) STOP gossip
+ HttpResponse<Buffer> gossipStopResponse = getBlocking(
+ trustedClient()
+ .put(serverWrapper.serverPort, "localhost", "/api/v1/cassandra/gossip")
+ .sendBuffer(Buffer.buffer("{\"state\":\"stop\"}")));
+ assertThat(gossipStopResponse.statusCode()).isEqualTo(OK.code());
+
assertThat(gossipStopResponse.bodyAsJsonObject().getString("status")).isEqualTo("OK");
+
+ Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
+
+ // 2) gossip should now be NOT_OK
+ HttpResponse<Buffer> gossipHealthDown = getBlocking(
+ trustedClient()
+ .get(serverWrapper.serverPort, "localhost",
"/api/v1/cassandra/gossip/__health")
+ .send());
+ assertThat(gossipHealthDown.statusCode()).isEqualTo(OK.code());
+
assertThat(gossipHealthDown.bodyAsJsonObject().getString("status")).isEqualTo("NOT_OK");
+
+ // 3) START gossip
+ HttpResponse<Buffer> startGossipResponse = getBlocking(
+ trustedClient()
+ .put(serverWrapper.serverPort, "localhost", "/api/v1/cassandra/gossip")
+ .sendBuffer(Buffer.buffer("{\"state\":\"start\"}")));
+ assertThat(startGossipResponse.statusCode()).isEqualTo(OK.code());
+
assertThat(startGossipResponse.bodyAsJsonObject().getString("status")).isEqualTo("OK");
+
+ // wait for native-transport to actually stop
+ Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
+
+ // 4) gossip should now be OK
+ HttpResponse<Buffer> gossipHealthUp = getBlocking(
+ trustedClient()
+ .get(serverWrapper.serverPort, "localhost",
"/api/v1/cassandra/gossip/__health")
+ .send());
+ assertThat(gossipHealthUp.statusCode()).isEqualTo(OK.code());
+
assertThat(gossipHealthUp.bodyAsJsonObject().getString("status")).isEqualTo("OK");
+ }
+
+ @Test
+ void testNativeTransportToggle()
+ {
+ // 1) STOP native
+ HttpResponse<Buffer> nativeStopResponse = getBlocking(
+ trustedClient()
+ .put(serverWrapper.serverPort, "localhost", "/api/v1/cassandra/native")
+ .sendBuffer(Buffer.buffer("{\"state\":\"stop\"}")));
+ assertThat(nativeStopResponse.statusCode()).isEqualTo(OK.code());
+
assertThat(nativeStopResponse.bodyAsJsonObject().getString("status")).isEqualTo("OK");
+
+ Uninterruptibles.sleepUninterruptibly(3, TimeUnit.SECONDS);
+
+ // 2) native should now be NOT_OK
+ HttpResponse<Buffer> nativeHealthDown = getBlocking(
+ trustedClient()
+ .get(serverWrapper.serverPort, "localhost",
"/api/v1/cassandra/native/__health")
+ .send());
+
assertThat(nativeHealthDown.statusCode()).isEqualTo(SERVICE_UNAVAILABLE.code());
+
assertThat(nativeHealthDown.bodyAsJsonObject().getString("status")).isEqualTo("NOT_OK");
+
+ // 3) START native
+ HttpResponse<Buffer> startNativeResponse = getBlocking(
+ trustedClient()
+ .put(serverWrapper.serverPort, "localhost", "/api/v1/cassandra/native")
+ .sendBuffer(Buffer.buffer("{\"state\":\"start\"}")));
+ assertThat(startNativeResponse.statusCode()).isEqualTo(OK.code());
+
assertThat(startNativeResponse.bodyAsJsonObject().getString("status")).isEqualTo("OK");
+
+ Uninterruptibles.sleepUninterruptibly(3, TimeUnit.SECONDS);
+
+ // 4) native should now be OK
+ HttpResponse<Buffer> nativeHealthUp = getBlocking(
+ trustedClient()
+ .get(serverWrapper.serverPort, "localhost",
"/api/v1/cassandra/native/__health")
+ .send());
+ assertThat(nativeHealthUp.statusCode()).isEqualTo(OK.code());
+
assertThat(nativeHealthUp.bodyAsJsonObject().getString("status")).isEqualTo("OK");
+ }
+
+ @Override
+ protected void initializeSchemaForTest()
+ {
+ // Do nothing
+ }
+}
diff --git
a/server-common/src/main/java/org/apache/cassandra/sidecar/common/server/StorageOperations.java
b/server-common/src/main/java/org/apache/cassandra/sidecar/common/server/StorageOperations.java
index 58ed83ec..184c6818 100644
---
a/server-common/src/main/java/org/apache/cassandra/sidecar/common/server/StorageOperations.java
+++
b/server-common/src/main/java/org/apache/cassandra/sidecar/common/server/StorageOperations.java
@@ -129,4 +129,24 @@ public interface StorageOperations
* @return the name of the cluster
*/
String clusterName();
+
+ /**
+ * Triggers stop native transport of the Cassandra node
+ */
+ void stopNativeTransport();
+
+ /**
+ * Triggers start native transport of the Cassandra node
+ */
+ void startNativeTransport();
+
+ /**
+ * Triggers stop gossip of the Cassandra node
+ */
+ void stopGossiping();
+
+ /**
+ * Triggers start gossip of the Cassandra node
+ */
+ void startGossiping();
}
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/BasicPermissions.java
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/BasicPermissions.java
index a76affc9..a9ec98bf 100644
---
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/BasicPermissions.java
+++
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/BasicPermissions.java
@@ -70,9 +70,12 @@ public class BasicPermissions
public static final Permission READ_SCHEMA = new
DomainAwarePermission("SCHEMA:READ", CLUSTER_SCOPE);
public static final Permission READ_SCHEMA_KEYSPACE_SCOPED = new
DomainAwarePermission("SCHEMA:READ", KEYSPACE_SCOPE);
public static final Permission READ_GOSSIP = new
DomainAwarePermission("GOSSIP:READ", CLUSTER_SCOPE);
+ public static final Permission MODIFY_GOSSIP = new
DomainAwarePermission("GOSSIP:MODIFY", CLUSTER_SCOPE);
public static final Permission READ_RING = new
DomainAwarePermission("RING:READ", CLUSTER_SCOPE);
public static final Permission READ_RING_KEYSPACE_SCOPED = new
DomainAwarePermission("RING:READ", KEYSPACE_SCOPE);
public static final Permission READ_TOPOLOGY = new
DomainAwarePermission("TOPOLOGY:READ", KEYSPACE_SCOPE);
+ public static final Permission MODIFY_NATIVE = new
DomainAwarePermission("NATIVE:MODIFY", CLUSTER_SCOPE);
+
// cassandra stats permissions
public static final Permission STATS_CLUSTER_SCOPED = new
StandardPermission("STATS", CLUSTER_SCOPE);
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/CassandraHealthHandler.java
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/CassandraHealthHandler.java
index 39584b61..7beea488 100644
---
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/CassandraHealthHandler.java
+++
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/CassandraHealthHandler.java
@@ -33,8 +33,8 @@ import
org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
import org.jetbrains.annotations.NotNull;
import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.JMX;
-import static
org.apache.cassandra.sidecar.modules.HealthCheckModule.NOT_OK_STATUS;
-import static org.apache.cassandra.sidecar.modules.HealthCheckModule.OK_STATUS;
+import static org.apache.cassandra.sidecar.modules.ApiModule.NOT_OK_STATUS;
+import static org.apache.cassandra.sidecar.modules.ApiModule.OK_STATUS;
/**
* Provides a simple REST endpoint to determine if a Cassandra node is
available
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/GossipHealthHandler.java
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/GossipHealthHandler.java
index f18d292f..726817f9 100644
---
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/GossipHealthHandler.java
+++
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/GossipHealthHandler.java
@@ -27,8 +27,8 @@ import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
import org.jetbrains.annotations.NotNull;
-import static
org.apache.cassandra.sidecar.modules.HealthCheckModule.NOT_OK_STATUS;
-import static org.apache.cassandra.sidecar.modules.HealthCheckModule.OK_STATUS;
+import static org.apache.cassandra.sidecar.modules.ApiModule.NOT_OK_STATUS;
+import static org.apache.cassandra.sidecar.modules.ApiModule.OK_STATUS;
/**
* Handler to retrieve gossip health
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/GossipUpdateHandler.java
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/GossipUpdateHandler.java
new file mode 100644
index 00000000..cebd0ab0
--- /dev/null
+++
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/GossipUpdateHandler.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.handlers;
+
+import java.util.Collections;
+import java.util.Set;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.core.net.SocketAddress;
+import io.vertx.ext.auth.authorization.Authorization;
+import io.vertx.ext.web.RoutingContext;
+import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions;
+import
org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
+import org.apache.cassandra.sidecar.common.server.StorageOperations;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
+import org.jetbrains.annotations.NotNull;
+
+import static org.apache.cassandra.sidecar.modules.ApiModule.OK_STATUS;
+
+/**
+ * Handles {@code PUT /api/v1/cassandra/gossip} requests to start or stop
Cassandra gossip.
+ *
+ * <p>Expects a JSON payload:
+ * { "state": "start" } or { "state": "stop" }
+ * and will asynchronously invoke the corresponding JMX operation.</p>
+ */
+@Singleton
+public class GossipUpdateHandler extends NodeCommandHandler implements
AccessProtected
+{
+ @Inject
+ public GossipUpdateHandler(InstanceMetadataFetcher metadataFetcher,
ExecutorPools executorPools)
+ {
+ super(metadataFetcher, executorPools, null);
+ }
+
+ @Override
+ public Set<Authorization> requiredAuthorizations()
+ {
+ return
Collections.singleton(BasicPermissions.MODIFY_GOSSIP.toAuthorization());
+ }
+
+ @Override
+ protected void handleInternal(RoutingContext context,
+ HttpServerRequest httpRequest,
+ @NotNull String host,
+ SocketAddress remoteAddress,
+ NodeCommandRequestPayload request)
+ {
+ StorageOperations storageOps =
metadataFetcher.delegate(host).storageOperations();
+
+ executorPools.service().runBlocking(() -> {
+ switch (request.state())
+ {
+ case START:
+ storageOps.startGossiping();
+ break;
+ case STOP:
+ storageOps.stopGossiping();
+ break;
+ default:
+ throw new IllegalStateException("Unknown state: " +
request.state());
+ }
+ })
+ .onSuccess(ignored -> context.json(OK_STATUS))
+ .onFailure(cause -> processFailure(cause, context, host,
remoteAddress, request));
+ }
+}
+
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/NativeUpdateHandler.java
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/NativeUpdateHandler.java
new file mode 100644
index 00000000..bf43b3d3
--- /dev/null
+++
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/NativeUpdateHandler.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.handlers;
+
+import java.util.Collections;
+import java.util.Set;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.core.net.SocketAddress;
+import io.vertx.ext.auth.authorization.Authorization;
+import io.vertx.ext.web.RoutingContext;
+import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions;
+import
org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
+import org.apache.cassandra.sidecar.common.server.StorageOperations;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
+import org.jetbrains.annotations.NotNull;
+
+import static org.apache.cassandra.sidecar.modules.ApiModule.OK_STATUS;
+
+
+/**
+ * Handler for starting or stopping native transport on a Cassandra node.
+ * <p>
+ * Expects a JSON body:
+ * { "state": "start" } or { "state": "stop" }
+ * </p>
+ */
+@Singleton
+public class NativeUpdateHandler extends NodeCommandHandler implements
AccessProtected
+{
+ @Inject
+ public NativeUpdateHandler(InstanceMetadataFetcher metadataFetcher,
ExecutorPools executorPools)
+ {
+ super(metadataFetcher, executorPools, null);
+ }
+
+ @Override
+ public Set<Authorization> requiredAuthorizations()
+ {
+ return
Collections.singleton(BasicPermissions.MODIFY_NATIVE.toAuthorization());
+ }
+
+ @Override
+ protected void handleInternal(RoutingContext context,
+ HttpServerRequest httpRequest,
+ @NotNull String host,
+ SocketAddress remoteAddress,
+ NodeCommandRequestPayload request)
+ {
+ StorageOperations storageOps =
metadataFetcher.delegate(host).storageOperations();
+
+ executorPools.service().runBlocking(() -> {
+ switch (request.state())
+ {
+ case START:
+ storageOps.startNativeTransport();
+ break;
+ case STOP:
+ storageOps.stopNativeTransport();
+ break;
+ default:
+ throw new IllegalStateException("Unknown state: " +
request.state());
+ }
+ })
+ .onSuccess(ignored -> context.json(OK_STATUS))
+ .onFailure(cause -> processFailure(cause, context, host,
remoteAddress, request));
+ }
+}
+
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/NodeCommandHandler.java
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/NodeCommandHandler.java
new file mode 100644
index 00000000..c1bce4ef
--- /dev/null
+++
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/NodeCommandHandler.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.handlers;
+
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.vertx.core.json.DecodeException;
+import io.vertx.core.json.Json;
+import io.vertx.ext.web.RoutingContext;
+import
org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.utils.CassandraInputValidator;
+import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
+
+import static
org.apache.cassandra.sidecar.utils.HttpExceptions.wrapHttpException;
+
+/**
+ * Base handler for "node command" endpoints (e.g. start/stop gossip or native
transport).
+ * Extracts a {@link NodeCommandRequestPayload} from the request body,
validates it,
+ * and defers to subclasses in {@link #handleInternal} to perform the actual
operation.
+ */
+public abstract class NodeCommandHandler extends
AbstractHandler<NodeCommandRequestPayload>
+{
+ /**
+ * Constructs a handler with the provided {@code metadataFetcher}
+ *
+ * @param metadataFetcher the interface to retrieve instance metadata
+ * @param executorPools the executor pools for blocking executions
+ * @param validator a validator instance to validate
Cassandra-specific input
+ */
+ protected NodeCommandHandler(InstanceMetadataFetcher metadataFetcher,
ExecutorPools executorPools, CassandraInputValidator validator)
+ {
+ super(metadataFetcher, executorPools, validator);
+ }
+
+ /**
+ * extract node command from RoutingContext
+ *
+ * @param ctx the request context
+ * @return parsed NodeCommandRequestPayload
+ */
+ @Override
+ protected NodeCommandRequestPayload extractParamsOrThrow(RoutingContext
ctx)
+ {
+ String body = ctx.body().asString();
+ if (body == null || body.equalsIgnoreCase("null"))
+ {
+ logger.warn("Bad request. Received null payload.");
+ throw wrapHttpException(HttpResponseStatus.BAD_REQUEST, "Request
body must be JSON with a non-null \"state\" field");
+ }
+ try
+ {
+ return Json.decodeValue(body, NodeCommandRequestPayload.class);
+ }
+ catch (DecodeException e)
+ {
+ logger.warn("Bad request. Received invalid JSON payload.");
+ throw wrapHttpException(HttpResponseStatus.BAD_REQUEST, "Invalid
state value: " + e.getMessage());
+ }
+ }
+}
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/ReportSchemaHandler.java
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/ReportSchemaHandler.java
index 7a009b38..614e7353 100644
---
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/ReportSchemaHandler.java
+++
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/ReportSchemaHandler.java
@@ -35,7 +35,7 @@ import
org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import static org.apache.cassandra.sidecar.modules.HealthCheckModule.OK_STATUS;
+import static org.apache.cassandra.sidecar.modules.ApiModule.OK_STATUS;
/**
* An implementation of {@link AbstractHandler} used to trigger an immediate,
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/cdc/DeleteServiceConfigHandler.java
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/cdc/DeleteServiceConfigHandler.java
index 5180c4f5..deb1a117 100644
---
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/cdc/DeleteServiceConfigHandler.java
+++
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/cdc/DeleteServiceConfigHandler.java
@@ -33,7 +33,7 @@ import org.apache.cassandra.sidecar.db.ConfigAccessor;
import org.apache.cassandra.sidecar.db.ConfigAccessorFactory;
import org.apache.cassandra.sidecar.handlers.AccessProtected;
-import static org.apache.cassandra.sidecar.modules.HealthCheckModule.OK_STATUS;
+import static org.apache.cassandra.sidecar.modules.ApiModule.OK_STATUS;
import static
org.apache.cassandra.sidecar.utils.HttpExceptions.wrapHttpException;
/**
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/modules/ApiModule.java
b/server/src/main/java/org/apache/cassandra/sidecar/modules/ApiModule.java
index 291d75f2..4472718d 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/modules/ApiModule.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/modules/ApiModule.java
@@ -18,6 +18,9 @@
package org.apache.cassandra.sidecar.modules;
+import java.util.Collections;
+import java.util.Map;
+
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
@@ -59,6 +62,12 @@ import static
org.apache.cassandra.sidecar.common.ApiEndpointsV1.API_V1_ALL_ROUT
*/
public class ApiModule extends AbstractModule
{
+ /**
+ * Basic response body
+ */
+ public static final Map<String, String> OK_STATUS =
Collections.singletonMap("status", "OK");
+ public static final Map<String, String> NOT_OK_STATUS =
Collections.singletonMap("status", "NOT_OK");
+
@Provides
@Singleton
Router vertxRouter(Vertx vertx, MultiBindingTypeResolver<VertxRoute>
resolver)
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/modules/CassandraOperationsModule.java
b/server/src/main/java/org/apache/cassandra/sidecar/modules/CassandraOperationsModule.java
index 4c48238e..29f69214 100644
---
a/server/src/main/java/org/apache/cassandra/sidecar/modules/CassandraOperationsModule.java
+++
b/server/src/main/java/org/apache/cassandra/sidecar/modules/CassandraOperationsModule.java
@@ -22,9 +22,11 @@ import com.google.inject.AbstractModule;
import com.google.inject.multibindings.ProvidesIntoMap;
import org.apache.cassandra.sidecar.handlers.ConnectedClientStatsHandler;
import org.apache.cassandra.sidecar.handlers.GossipInfoHandler;
+import org.apache.cassandra.sidecar.handlers.GossipUpdateHandler;
import org.apache.cassandra.sidecar.handlers.KeyspaceRingHandler;
import org.apache.cassandra.sidecar.handlers.KeyspaceSchemaHandler;
import org.apache.cassandra.sidecar.handlers.ListOperationalJobsHandler;
+import org.apache.cassandra.sidecar.handlers.NativeUpdateHandler;
import org.apache.cassandra.sidecar.handlers.NodeDecommissionHandler;
import org.apache.cassandra.sidecar.handlers.OperationalJobHandler;
import org.apache.cassandra.sidecar.handlers.RingHandler;
@@ -173,4 +175,26 @@ public class CassandraOperationsModule extends
AbstractModule
.handler(validateTableExistenceHandler)
.handler(tableStatsHandler).build();
}
+
+ @ProvidesIntoMap
+ @KeyClassMapKey(VertxRouteMapKeys.UpdateNodeGossipStateRouteKey.class)
+ VertxRoute cassandraChangeGossipStateRoute(RouteBuilder.Factory factory,
+ GossipUpdateHandler nodeGossipHandler)
+ {
+ return factory.builderForRoute()
+ .setBodyHandler(true)
+ .handler(nodeGossipHandler)
+ .build();
+ }
+
+ @ProvidesIntoMap
+ @KeyClassMapKey(VertxRouteMapKeys.UpdateNodeNativeStateRouteKey.class)
+ VertxRoute cassandraChangeNativeStateRoute(RouteBuilder.Factory factory,
+ NativeUpdateHandler nodeNativeHandler)
+ {
+ return factory.builderForRoute()
+ .setBodyHandler(true)
+ .handler(nodeNativeHandler)
+ .build();
+ }
}
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/modules/HealthCheckModule.java
b/server/src/main/java/org/apache/cassandra/sidecar/modules/HealthCheckModule.java
index 91a74c17..8e8e64fc 100644
---
a/server/src/main/java/org/apache/cassandra/sidecar/modules/HealthCheckModule.java
+++
b/server/src/main/java/org/apache/cassandra/sidecar/modules/HealthCheckModule.java
@@ -18,9 +18,6 @@
package org.apache.cassandra.sidecar.modules;
-import java.util.Collections;
-import java.util.Map;
-
import com.google.inject.AbstractModule;
import com.google.inject.multibindings.ProvidesIntoMap;
import org.apache.cassandra.sidecar.cluster.InstancesMetadata;
@@ -42,9 +39,6 @@ import org.apache.cassandra.sidecar.tasks.PeriodicTask;
*/
public class HealthCheckModule extends AbstractModule
{
- public static final Map<String, String> OK_STATUS =
Collections.singletonMap("status", "OK");
- public static final Map<String, String> NOT_OK_STATUS =
Collections.singletonMap("status", "NOT_OK");
-
@ProvidesIntoMap
@KeyClassMapKey(PeriodicTaskMapKeys.HealthCheckPeriodicTaskKey.class)
PeriodicTask healthCheckPeriodicTask(SidecarConfiguration configuration,
@@ -60,7 +54,7 @@ public class HealthCheckModule extends AbstractModule
VertxRoute sidecarHealthRoute(RouteBuilder.Factory factory)
{
return factory.builderForUnauthorizedRoute()
- .handler(context -> context.json(OK_STATUS))
+ .handler(context -> context.json(ApiModule.OK_STATUS))
.build();
}
diff --git
a/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/VertxRouteMapKeys.java
b/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/VertxRouteMapKeys.java
index 3a4e7431..1706cc92 100644
---
a/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/VertxRouteMapKeys.java
+++
b/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/VertxRouteMapKeys.java
@@ -15,7 +15,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package org.apache.cassandra.sidecar.modules.multibindings;
import io.vertx.core.http.HttpMethod;
@@ -56,7 +55,7 @@ public interface VertxRouteMapKeys
interface CassandraGossipInfoRouteKey extends RouteClassKey
{
HttpMethod HTTP_METHOD = HttpMethod.GET;
- String ROUTE_URI = ApiEndpointsV1.GOSSIP_INFO_ROUTE;
+ String ROUTE_URI = ApiEndpointsV1.GOSSIP_ROUTE;
}
interface CassandraHealthRouteKey extends RouteClassKey
{
@@ -243,6 +242,16 @@ public interface VertxRouteMapKeys
HttpMethod HTTP_METHOD = HttpMethod.GET;
String ROUTE_URI = ApiEndpointsV1.TIME_SKEW_ROUTE;
}
+ interface UpdateNodeGossipStateRouteKey extends RouteClassKey
+ {
+ HttpMethod HTTP_METHOD = HttpMethod.PUT;
+ String ROUTE_URI = ApiEndpointsV1.GOSSIP_ROUTE;
+ }
+ interface UpdateNodeNativeStateRouteKey extends RouteClassKey
+ {
+ HttpMethod HTTP_METHOD = HttpMethod.PUT;
+ String ROUTE_URI = ApiEndpointsV1.CASSANDRA_NATIVE_ROUTE;
+ }
interface UpdateRestoreJobRouteKey extends RouteClassKey
{
HttpMethod HTTP_METHOD = HttpMethod.PATCH;
diff --git
a/server/src/test/java/org/apache/cassandra/sidecar/handlers/GossipUpdateHandlerTest.java
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/GossipUpdateHandlerTest.java
new file mode 100644
index 00000000..da8abd47
--- /dev/null
+++
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/GossipUpdateHandlerTest.java
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.handlers;
+
+
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.util.Modules;
+import io.vertx.core.Vertx;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+import org.apache.cassandra.sidecar.TestModule;
+import org.apache.cassandra.sidecar.TestResourceReaper;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.InstancesMetadata;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.server.StorageOperations;
+import org.apache.cassandra.sidecar.modules.SidecarModules;
+import org.apache.cassandra.sidecar.server.Server;
+
+import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static io.vertx.core.buffer.Buffer.buffer;
+import static org.apache.cassandra.testing.utils.AssertionUtils.getBlocking;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test for {@link GossipUpdateHandler}
+ */
+@ExtendWith(VertxExtension.class)
+public class GossipUpdateHandlerTest
+{
+ Vertx vertx;
+ Server server;
+ StorageOperations mockStorageOperations = mock(StorageOperations.class);
+
+ @BeforeEach
+ void before() throws InterruptedException
+ {
+ Injector injector;
+ Module testOverride = Modules.override(new TestModule()).with(new
GossipUpdateHandlerTest.GossipUpdateHandlerTestModule());
+ injector =
Guice.createInjector(Modules.override(SidecarModules.all()).with(testOverride));
+ vertx = injector.getInstance(Vertx.class);
+ server = injector.getInstance(Server.class);
+ VertxTestContext context = new VertxTestContext();
+ server.start().onSuccess(s ->
context.completeNow()).onFailure(context::failNow);
+ context.awaitCompletion(5, TimeUnit.SECONDS);
+ }
+
+ @AfterEach
+ void after() throws InterruptedException
+ {
+ getBlocking(TestResourceReaper.create().with(server).close(), 60,
TimeUnit.SECONDS, "Closing server");
+ }
+
+ @Test
+ void testStartGossip(VertxTestContext ctx)
+ {
+ WebClient client = WebClient.create(vertx);
+ String payload = "{\"state\":\"start\"}";
+ client.put(server.actualPort(), "127.0.0.1",
"/api/v1/cassandra/gossip")
+ .sendBuffer(buffer(payload), ctx.succeeding(resp -> {
+ ctx.verify(() -> {
+ verify(mockStorageOperations, times(1)).startGossiping();
+
+ assertThat(resp.statusCode()).isEqualTo(OK.code());
+ JsonObject json = resp.bodyAsJsonObject();
+ assertThat(json.getMap().get("status")).isEqualTo("OK");
+ });
+ ctx.completeNow();
+ }));
+ }
+
+ @Test
+ void testStopGossip(VertxTestContext ctx)
+ {
+ WebClient client = WebClient.create(vertx);
+ String payload = "{\"state\":\"STOP\"}";
+ client.put(server.actualPort(), "127.0.0.1",
"/api/v1/cassandra/gossip")
+ .sendBuffer(buffer(payload), ctx.succeeding(resp -> {
+ ctx.verify(() -> {
+ verify(mockStorageOperations, times(1)).stopGossiping();
+
+ assertThat(resp.statusCode()).isEqualTo(OK.code());
+ JsonObject json = resp.bodyAsJsonObject();
+ assertThat(json.getMap().get("status")).isEqualTo("OK");
+ });
+ ctx.completeNow();
+ }));
+ }
+
+ @Test
+ void testInvalidState(VertxTestContext ctx)
+ {
+ WebClient client = WebClient.create(vertx);
+ String payload = "{\"state\":\"foo\"}";
+ client.put(server.actualPort(), "127.0.0.1",
"/api/v1/cassandra/gossip")
+ .sendBuffer(buffer(payload), ctx.succeeding(resp -> {
+ ctx.verify(() -> {
+
assertThat(resp.statusCode()).isEqualTo(BAD_REQUEST.code());
+ verify(mockStorageOperations, times(0)).startGossiping();
+ verify(mockStorageOperations, times(0)).stopGossiping();
+ });
+ ctx.completeNow();
+ }));
+ }
+
+
+ /**
+ * Test guice module for {@link GossipUpdateHandler} tests
+ */
+ class GossipUpdateHandlerTestModule extends AbstractModule
+ {
+ @Provides
+ @Singleton
+ public InstancesMetadata instanceMetadata()
+ {
+ final int instanceId = 100;
+ final String host = "127.0.0.1";
+ final InstanceMetadata instanceMetadata =
mock(InstanceMetadata.class);
+ when(instanceMetadata.host()).thenReturn(host);
+ when(instanceMetadata.port()).thenReturn(9042);
+ when(instanceMetadata.id()).thenReturn(instanceId);
+ when(instanceMetadata.stagingDir()).thenReturn("");
+
+ CassandraAdapterDelegate delegate =
mock(CassandraAdapterDelegate.class);
+
+
when(delegate.storageOperations()).thenReturn(mockStorageOperations);
+ when(instanceMetadata.delegate()).thenReturn(delegate);
+
+ InstancesMetadata mockInstancesMetadata =
mock(InstancesMetadata.class);
+
when(mockInstancesMetadata.instances()).thenReturn(Collections.singletonList(instanceMetadata));
+
when(mockInstancesMetadata.instanceFromId(instanceId)).thenReturn(instanceMetadata);
+
when(mockInstancesMetadata.instanceFromHost(host)).thenReturn(instanceMetadata);
+
+ return mockInstancesMetadata;
+ }
+ }
+}
diff --git
a/server/src/test/java/org/apache/cassandra/sidecar/handlers/NativeUpdateHandlerTest.java
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/NativeUpdateHandlerTest.java
new file mode 100644
index 00000000..b84573bb
--- /dev/null
+++
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/NativeUpdateHandlerTest.java
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.handlers;
+
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.util.Modules;
+import io.vertx.core.Vertx;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+import org.apache.cassandra.sidecar.TestModule;
+import org.apache.cassandra.sidecar.TestResourceReaper;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.InstancesMetadata;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.server.StorageOperations;
+import org.apache.cassandra.sidecar.modules.SidecarModules;
+import org.apache.cassandra.sidecar.server.Server;
+
+import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static io.vertx.core.buffer.Buffer.buffer;
+import static org.apache.cassandra.testing.utils.AssertionUtils.getBlocking;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test for {@link NativeUpdateHandler}
+ */
+@ExtendWith(VertxExtension.class)
+public class NativeUpdateHandlerTest
+{
+ Vertx vertx;
+ Server server;
+ StorageOperations mockStorageOperations = mock(StorageOperations.class);
+
+ @BeforeEach
+ void before() throws InterruptedException
+ {
+ Injector injector;
+ Module testOverride = Modules.override(new TestModule()).with(new
NativeUpdateHandlerTest.NativeUpdateHandlerTestModule());
+ injector =
Guice.createInjector(Modules.override(SidecarModules.all()).with(testOverride));
+ vertx = injector.getInstance(Vertx.class);
+ server = injector.getInstance(Server.class);
+ VertxTestContext context = new VertxTestContext();
+ server.start().onSuccess(s ->
context.completeNow()).onFailure(context::failNow);
+ context.awaitCompletion(5, TimeUnit.SECONDS);
+ }
+
+ @AfterEach
+ void after() throws InterruptedException
+ {
+ getBlocking(TestResourceReaper.create().with(server).close(), 60,
TimeUnit.SECONDS, "Closing server");
+ }
+
+ @Test
+ void testStartNative(VertxTestContext ctx)
+ {
+ WebClient client = WebClient.create(vertx);
+ String payload = "{\"state\":\"start\"}";
+ client.put(server.actualPort(), "127.0.0.1",
"/api/v1/cassandra/native").sendBuffer(buffer(payload), ctx.succeeding(resp -> {
+ ctx.verify(() -> {
+ verify(mockStorageOperations, times(1)).startNativeTransport();
+
+ assertThat(resp.statusCode()).isEqualTo(OK.code());
+ JsonObject json = resp.bodyAsJsonObject();
+ assertThat(json.getMap().get("status")).isEqualTo("OK");
+ });
+ ctx.completeNow();
+ }));
+ }
+
+ @Test
+ void testStopNative(VertxTestContext ctx)
+ {
+ WebClient client = WebClient.create(vertx);
+ String payload = "{\"state\":\"stop\"}";
+ client.put(server.actualPort(), "127.0.0.1",
"/api/v1/cassandra/native").sendBuffer(buffer(payload), ctx.succeeding(resp -> {
+ ctx.verify(() -> {
+ verify(mockStorageOperations, times(1)).stopNativeTransport();
+
+ assertThat(resp.statusCode()).isEqualTo(OK.code());
+ JsonObject json = resp.bodyAsJsonObject();
+ assertThat(json.getMap().get("status")).isEqualTo("OK");
+ });
+ ctx.completeNow();
+ }));
+ }
+
+ @Test
+ void testInvalidState(VertxTestContext ctx)
+ {
+ WebClient client = WebClient.create(vertx);
+ String payload = "{\"state\":\"foo\"}";
+ client.put(server.actualPort(), "127.0.0.1",
"/api/v1/cassandra/native").sendBuffer(buffer(payload), ctx.succeeding(resp -> {
+ ctx.verify(() -> {
+ assertThat(resp.statusCode()).isEqualTo(BAD_REQUEST.code());
+ verify(mockStorageOperations, times(0)).startNativeTransport();
+ verify(mockStorageOperations, times(0)).stopNativeTransport();
+ });
+ ctx.completeNow();
+ }));
+ }
+
+
+ /**
+ * Test guice module for {@link NativeUpdateHandler}
+ */
+ class NativeUpdateHandlerTestModule extends AbstractModule
+ {
+ @Provides
+ @Singleton
+ public InstancesMetadata instanceMetadata()
+ {
+ int instanceId = 100;
+ String host = "127.0.0.1";
+ InstanceMetadata instanceMetadata = mock(InstanceMetadata.class);
+ when(instanceMetadata.host()).thenReturn(host);
+ when(instanceMetadata.port()).thenReturn(9042);
+ when(instanceMetadata.id()).thenReturn(instanceId);
+ when(instanceMetadata.stagingDir()).thenReturn("");
+
+ CassandraAdapterDelegate delegate =
mock(CassandraAdapterDelegate.class);
+
+
when(delegate.storageOperations()).thenReturn(mockStorageOperations);
+ when(instanceMetadata.delegate()).thenReturn(delegate);
+
+ InstancesMetadata mockInstancesMetadata =
mock(InstancesMetadata.class);
+
when(mockInstancesMetadata.instances()).thenReturn(Collections.singletonList(instanceMetadata));
+
when(mockInstancesMetadata.instanceFromId(instanceId)).thenReturn(instanceMetadata);
+
when(mockInstancesMetadata.instanceFromHost(host)).thenReturn(instanceMetadata);
+
+ return mockInstancesMetadata;
+ }
+ }
+}
diff --git
a/server/src/test/java/org/apache/cassandra/sidecar/handlers/RouteBuilderTest.java
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/RouteBuilderTest.java
index f3f081a5..f8aeb3a0 100644
---
a/server/src/test/java/org/apache/cassandra/sidecar/handlers/RouteBuilderTest.java
+++
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/RouteBuilderTest.java
@@ -96,7 +96,7 @@ class RouteBuilderTest
Factory::builderForUnauthorizedRoute,
route -> {
route.setHttpMethod(HttpMethod.GET);
- route.setRouteURI(ApiEndpointsV1.GOSSIP_INFO_ROUTE);
+ route.setRouteURI(ApiEndpointsV1.GOSSIP_ROUTE);
assertThatThrownBy(() -> route.mountTo(mockRouter))
.isInstanceOf(ConfigurationException.class)
.hasMessage("Unauthorized route must not have required
authorizations declared");
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]