Skip to content

Commit e26ca4a

Browse files
author
Eduard Tudenhöfner
authored
Merge pull request #27 from datastax/create-keyspace-api
Add API endpoint that allows creating a Keyspace
2 parents fdf07f2 + 68c5e96 commit e26ca4a

File tree

13 files changed

+264
-45
lines changed

13 files changed

+264
-45
lines changed

management-api-agent/pom.xml

+10
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@
6363
</exclusions>
6464
<scope>provided</scope>
6565
</dependency>
66+
<dependency>
67+
<groupId>com.datastax.oss</groupId>
68+
<artifactId>java-driver-query-builder</artifactId>
69+
<version>${driver.version}</version>
70+
</dependency>
6671
<dependency>
6772
<groupId>junit</groupId>
6873
<artifactId>junit</artifactId>
@@ -119,6 +124,11 @@
119124
</exclusions>
120125
<scope>provided</scope>
121126
</dependency>
127+
<dependency>
128+
<groupId>com.datastax.oss</groupId>
129+
<artifactId>java-driver-query-builder</artifactId>
130+
<version>${driver.version}</version>
131+
</dependency>
122132
<dependency>
123133
<groupId>junit</groupId>
124134
<artifactId>junit</artifactId>

management-api-agent/src/main/java/com/datastax/mgmtapi/NodeOpsProvider.java

+20
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
import com.datastax.mgmtapi.rpc.Rpc;
2626
import com.datastax.mgmtapi.rpc.RpcParam;
2727
import com.datastax.mgmtapi.rpc.RpcRegistry;
28+
import com.datastax.oss.driver.api.querybuilder.SchemaBuilder;
2829
import org.apache.cassandra.auth.AuthenticatedUser;
2930
import org.apache.cassandra.auth.IRoleManager;
3031
import org.apache.cassandra.auth.RoleOptions;
3132
import org.apache.cassandra.auth.RoleResource;
33+
import org.apache.cassandra.db.ConsistencyLevel;
3234

3335
/**
3436
* Replace JMX calls with CQL 'CALL' methods via the the Rpc framework
@@ -318,4 +320,22 @@ public List<Map<String, List<Map<String, String>>>> getStreamInfo()
318320
{
319321
return ShimLoader.instance.get().getStreamInfo();
320322
}
323+
324+
@Rpc(name = "createKeyspace")
325+
public void createKeyspace(@RpcParam(name="keyspaceName") String keyspaceName, @RpcParam(name="replicationSettings") Map<String, Integer> replicationSettings) throws IOException
326+
{
327+
logger.debug("Creating keyspace {} with replication settings {}", keyspaceName, replicationSettings);
328+
329+
ShimLoader.instance.get().processQuery(SchemaBuilder.createKeyspace(keyspaceName)
330+
.ifNotExists()
331+
.withNetworkTopologyStrategy(replicationSettings)
332+
.asCql(),
333+
ConsistencyLevel.ONE);
334+
}
335+
336+
@Rpc(name = "getLocalDataCenter")
337+
public String getLocalDataCenter()
338+
{
339+
return ShimLoader.instance.get().getLocalDataCenter();
340+
}
321341
}

management-api-common/src/main/java/com/datastax/mgmtapi/shims/CassandraAPI.java

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import io.netty.channel.Channel;
1616
import io.netty.channel.ChannelInitializer;
1717
import org.apache.cassandra.auth.IRoleManager;
18+
import org.apache.cassandra.config.DatabaseDescriptor;
1819
import org.apache.cassandra.cql3.QueryProcessor;
1920
import org.apache.cassandra.cql3.UntypedResultSet;
2021
import org.apache.cassandra.db.ConsistencyLevel;
@@ -63,4 +64,6 @@ default Object handleRpcResult(Callable<Object> rpcResult) throws Exception
6364
{
6465
return rpcResult.call();
6566
}
67+
68+
String getLocalDataCenter();
6669
}

management-api-server/pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@
120120
<version>3.0.0</version>
121121
<scope>test</scope>
122122
</dependency>
123+
<dependency>
124+
<groupId>org.assertj</groupId>
125+
<artifactId>assertj-core</artifactId>
126+
<version>3.16.1</version>
127+
<scope>test</scope>
128+
</dependency>
123129
</dependencies>
124130

125131
<build>

management-api-server/src/main/java/com/datastax/mgmtapi/resources/KeyspaceOpsResources.java

+41-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*/
66
package com.datastax.mgmtapi.resources;
77

8+
import java.util.ArrayList;
9+
import java.util.List;
810
import javax.ws.rs.Consumes;
911
import javax.ws.rs.POST;
1012
import javax.ws.rs.Path;
@@ -13,21 +15,18 @@
1315
import javax.ws.rs.core.MediaType;
1416
import javax.ws.rs.core.Response;
1517

16-
import java.util.ArrayList;
17-
import java.util.List;
18-
1918
import org.apache.commons.collections.CollectionUtils;
2019
import org.apache.commons.lang3.StringUtils;
21-
22-
import com.datastax.mgmtapi.resources.models.KeyspaceRequest;
23-
import org.apache.http.ConnectionClosedException;
24-
import org.apache.http.HttpStatus;
2520
import org.slf4j.Logger;
2621
import org.slf4j.LoggerFactory;
2722

2823
import com.datastax.mgmtapi.CqlService;
2924
import com.datastax.mgmtapi.ManagementApplication;
25+
import com.datastax.mgmtapi.resources.models.CreateKeyspaceRequest;
26+
import com.datastax.mgmtapi.resources.models.KeyspaceRequest;
3027
import io.swagger.v3.oas.annotations.Operation;
28+
import org.apache.http.ConnectionClosedException;
29+
import org.apache.http.HttpStatus;
3130

3231
@Path("/api/v0/ops/keyspace")
3332
public class KeyspaceOpsResources
@@ -106,4 +105,39 @@ public Response refresh(@QueryParam(value="keyspaceName")String keyspaceName, @Q
106105
return Response.status(HttpStatus.SC_INTERNAL_SERVER_ERROR).entity(t.getLocalizedMessage()).build();
107106
}
108107
}
108+
109+
@POST
110+
@Path("/create")
111+
@Produces(MediaType.TEXT_PLAIN)
112+
@Consumes(MediaType.APPLICATION_JSON)
113+
@Operation(summary = "Create a new keyspace with the given name and replication settings")
114+
public Response create(CreateKeyspaceRequest createKeyspaceRequest)
115+
{
116+
try
117+
{
118+
if (StringUtils.isBlank(createKeyspaceRequest.keyspaceName))
119+
{
120+
return Response.status(HttpStatus.SC_BAD_REQUEST).entity("Keyspace creation failed. Non-empty 'keyspace_name' must be provided").build();
121+
}
122+
123+
if (null == createKeyspaceRequest.replicationSettings || createKeyspaceRequest.replicationSettings.isEmpty())
124+
{
125+
return Response.status(HttpStatus.SC_BAD_REQUEST).entity("Keyspace creation failed. 'replication_settings' must be provided").build();
126+
}
127+
128+
cqlService.executePreparedStatement(app.dbUnixSocketFile, "CALL NodeOps.createKeyspace(?, ?)",
129+
createKeyspaceRequest.keyspaceName, createKeyspaceRequest.replicationSettingsAsMap());
130+
131+
return Response.ok("OK").build();
132+
}
133+
catch (ConnectionClosedException e)
134+
{
135+
return Response.status(HttpStatus.SC_INTERNAL_SERVER_ERROR).entity("Internal connection to Cassandra closed").build();
136+
}
137+
catch (Throwable t)
138+
{
139+
logger.error("Error when executing request", t);
140+
return Response.status(HttpStatus.SC_INTERNAL_SERVER_ERROR).entity(t.getLocalizedMessage()).build();
141+
}
142+
}
109143
}

management-api-server/src/main/java/com/datastax/mgmtapi/resources/MetadataResources.java

+9
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ public Response getEndpointStates()
5454
return executeWithJSONResponse("CALL NodeOps.getEndpointStates()");
5555
}
5656

57+
@GET
58+
@Path("/localdc")
59+
@Operation(summary = "Returns the DataCenter the local node belongs to")
60+
@Produces(MediaType.TEXT_PLAIN)
61+
public Response getLocalDataCenter()
62+
{
63+
return executeWithStringResponse("CALL NodeOps.getLocalDataCenter()");
64+
}
65+
5766
/**
5867
* Executes a CQL query with the expectation that there will be a single row returned with type String
5968
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.datastax.mgmtapi.resources.models;
2+
3+
import java.io.Serializable;
4+
import java.util.HashMap;
5+
import java.util.List;
6+
import java.util.Map;
7+
8+
import com.fasterxml.jackson.annotation.JsonCreator;
9+
import com.fasterxml.jackson.annotation.JsonProperty;
10+
11+
public class CreateKeyspaceRequest implements Serializable
12+
{
13+
@JsonProperty(value = "keyspace_name", required = true)
14+
public final String keyspaceName;
15+
16+
@JsonProperty(value = "replication_settings", required = true)
17+
public final List<ReplicationSetting> replicationSettings;
18+
19+
@JsonCreator
20+
public CreateKeyspaceRequest(@JsonProperty("keyspace_name") String keyspaceName, @JsonProperty("replication_settings") List<ReplicationSetting> replicationSettings)
21+
{
22+
this.keyspaceName = keyspaceName;
23+
this.replicationSettings = replicationSettings;
24+
}
25+
26+
public Map<String, Integer> replicationSettingsAsMap()
27+
{
28+
Map<String, Integer> result = new HashMap<>(replicationSettings.size());
29+
replicationSettings.forEach(r -> result.put(r.dcName, r.replicationFactor));
30+
return result;
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.datastax.mgmtapi.resources.models;
2+
3+
import java.io.Serializable;
4+
5+
import com.fasterxml.jackson.annotation.JsonCreator;
6+
import com.fasterxml.jackson.annotation.JsonProperty;
7+
8+
public class ReplicationSetting implements Serializable
9+
{
10+
@JsonProperty(value = "dc_name", required = true)
11+
public final String dcName;
12+
13+
@JsonProperty(value = "replication_factor", required = true)
14+
public final int replicationFactor;
15+
16+
@JsonCreator
17+
public ReplicationSetting(@JsonProperty("dc_name") String dcName, @JsonProperty("replication_factor") int replicationFactor)
18+
{
19+
this.dcName = dcName;
20+
this.replicationFactor = replicationFactor;
21+
}
22+
}

management-api-server/src/test/java/com/datastax/mgmtapi/K8OperatorResourcesTest.java

+60
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
package com.datastax.mgmtapi;
77

88
import javax.ws.rs.core.MediaType;
9+
import java.io.IOException;
910
import java.net.URISyntaxException;
1011
import java.util.ArrayList;
1112
import java.util.Arrays;
13+
import java.util.Collections;
1214
import java.util.List;
1315
import java.util.Map;
1416

@@ -21,9 +23,13 @@
2123
import com.datastax.mgmtapi.resources.NodeOpsResources;
2224
import com.datastax.mgmtapi.resources.TableOpsResources;
2325
import com.datastax.mgmtapi.resources.models.CompactRequest;
26+
import com.datastax.mgmtapi.resources.models.CreateKeyspaceRequest;
2427
import com.datastax.mgmtapi.resources.models.KeyspaceRequest;
28+
import com.datastax.mgmtapi.resources.models.ReplicationSetting;
2529
import com.datastax.mgmtapi.resources.models.ScrubRequest;
30+
import org.apache.http.ConnectionClosedException;
2631
import org.apache.http.HttpStatus;
32+
import org.assertj.core.api.Assertions;
2733
import org.jboss.resteasy.core.messagebody.WriterUtility;
2834
import org.jboss.resteasy.mock.MockDispatcherFactory;
2935
import org.jboss.resteasy.mock.MockHttpRequest;
@@ -37,6 +43,7 @@
3743
import com.datastax.oss.driver.api.core.cql.Row;
3844

3945
import static org.apache.commons.lang3.StringUtils.EMPTY;
46+
import static org.assertj.core.api.Assertions.assertThat;
4047
import static org.mockito.ArgumentMatchers.any;
4148
import static org.mockito.ArgumentMatchers.anyString;
4249
import static org.mockito.ArgumentMatchers.eq;
@@ -990,6 +997,59 @@ public void testGetStreamInfo() throws Exception
990997
verify(context.cqlService).executeCql(any(), eq("CALL NodeOps.getStreamInfo()"));
991998
}
992999

1000+
@Test
1001+
public void testCreatingKeyspace() throws IOException, URISyntaxException
1002+
{
1003+
CreateKeyspaceRequest keyspaceRequest = new CreateKeyspaceRequest("myKeyspace", Arrays.asList(new ReplicationSetting("dc1", 3), new ReplicationSetting("dc2", 3)));
1004+
1005+
Context context = setup();
1006+
1007+
when(context.cqlService.executePreparedStatement(any(), anyString()))
1008+
.thenReturn(null);
1009+
1010+
String keyspaceRequestAsJSON = WriterUtility.asString(keyspaceRequest, MediaType.APPLICATION_JSON);
1011+
MockHttpResponse response = postWithBody("/ops/keyspace/create", keyspaceRequestAsJSON, context);
1012+
1013+
assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK);
1014+
assertThat(response.getContentAsString()).contains("OK");
1015+
1016+
verify(context.cqlService).executePreparedStatement(any(), eq("CALL NodeOps.createKeyspace(?, ?)"), any());
1017+
}
1018+
1019+
@Test
1020+
public void testCreatingEmptyKeyspaceShouldFail() throws IOException, URISyntaxException
1021+
{
1022+
CreateKeyspaceRequest keyspaceRequest = new CreateKeyspaceRequest("", Arrays.asList(new ReplicationSetting("dc1", 3), new ReplicationSetting("dc2", 3)));
1023+
1024+
Context context = setup();
1025+
1026+
when(context.cqlService.executePreparedStatement(any(), anyString()))
1027+
.thenReturn(null);
1028+
1029+
String keyspaceRequestAsJSON = WriterUtility.asString(keyspaceRequest, MediaType.APPLICATION_JSON);
1030+
MockHttpResponse response = postWithBody("/ops/keyspace/create", keyspaceRequestAsJSON, context);
1031+
1032+
assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
1033+
assertThat(response.getContentAsString()).contains("Keyspace creation failed. Non-empty 'keyspace_name' must be provided");
1034+
}
1035+
1036+
@Test
1037+
public void testCreatingEmptyReplicationSettingsShouldFail() throws IOException, URISyntaxException
1038+
{
1039+
CreateKeyspaceRequest keyspaceRequest = new CreateKeyspaceRequest("TestKeyspace", Collections.emptyList());
1040+
1041+
Context context = setup();
1042+
1043+
when(context.cqlService.executePreparedStatement(any(), anyString()))
1044+
.thenReturn(null);
1045+
1046+
String keyspaceRequestAsJSON = WriterUtility.asString(keyspaceRequest, MediaType.APPLICATION_JSON);
1047+
MockHttpResponse response = postWithBody("/ops/keyspace/create", keyspaceRequestAsJSON, context);
1048+
1049+
assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
1050+
assertThat(response.getContentAsString()).contains("Keyspace creation failed. 'replication_settings' must be provided");
1051+
}
1052+
9931053
private MockHttpResponse postWithBody(String path, String body, Context context) throws URISyntaxException {
9941054
MockHttpRequest request = MockHttpRequest
9951055
.post(ROOT_PATH + path)

0 commit comments

Comments
 (0)