You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ge...@apache.org on 2023/06/01 13:10:05 UTC

[solr] branch main updated: SOLR-16392: Tweak v2 CREATESHARD to be more REST-ful (#1671)

This is an automated email from the ASF dual-hosted git repository.

gerlowskija pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new 66d6a55895f SOLR-16392: Tweak v2 CREATESHARD to be more REST-ful (#1671)
66d6a55895f is described below

commit 66d6a55895fa51074e325d260bbdc14f8beb560a
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Thu Jun 1 09:09:58 2023 -0400

    SOLR-16392: Tweak v2 CREATESHARD to be more REST-ful (#1671)
    
    The "create" command-specifier has been removed, but the API otherwise
    remains unchanged.
    
    This commit also converts the API over to the JAX-RS framework.
---
 solr/CHANGES.txt                                   |   3 +
 .../solr/handler/admin/CollectionsHandler.java     |  41 +---
 .../solr/handler/admin/api/CreateShardAPI.java     | 220 +++++++++++++++++----
 .../solr/handler/admin/api/CreateShardAPITest.java | 172 ++++++++++++++++
 .../handler/admin/api/V2ShardsAPIMappingTest.java  |  41 ----
 .../deployment-guide/pages/shard-management.adoc   |   6 +-
 6 files changed, 366 insertions(+), 117 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 511854d30c0..df3f81eebbb 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -147,6 +147,9 @@ Improvements
 * SOLR-16816: Update node metrics while making affinityPlacement selections. Therefore selections can be made given the expected cluster
   information after the previous selections are implemented. (Houston Putman)
 
+* SOLR-16392: The v2 "create shard" API has been tweaked to be more intuitive, by removing the top-level "create"
+  command specifier.  The rest of the API remains unchanged. (Jason Gerlowski)
+
 Optimizations
 ---------------------
 
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
index e7bf5e92ec5..d00ed09b766 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
@@ -136,7 +136,6 @@ import org.apache.solr.client.solrj.impl.HttpSolrClient.Builder;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.request.CoreAdminRequest.RequestSyncShard;
 import org.apache.solr.client.solrj.response.RequestStatusState;
-import org.apache.solr.client.solrj.util.SolrIdentifierValidator;
 import org.apache.solr.cloud.OverseerSolrResponse;
 import org.apache.solr.cloud.OverseerSolrResponseSerializer;
 import org.apache.solr.cloud.OverseerTaskQueue;
@@ -152,8 +151,6 @@ import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.common.cloud.ClusterProperties;
 import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.DocCollection;
-import org.apache.solr.common.cloud.DocCollection.CollectionStateProps;
-import org.apache.solr.common.cloud.ImplicitDocRouter;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.Replica.State;
 import org.apache.solr.common.cloud.Slice;
@@ -763,40 +760,8 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
     CREATESHARD_OP(
         CREATESHARD,
         (req, rsp, h) -> {
-          Map<String, Object> map =
-              copy(req.getParams().required(), null, COLLECTION_PROP, SHARD_ID_PROP);
-          ClusterState clusterState = h.coreContainer.getZkController().getClusterState();
-          final String newShardName =
-              SolrIdentifierValidator.validateShardName(req.getParams().get(SHARD_ID_PROP));
-          boolean followAliases = req.getParams().getBool(FOLLOW_ALIASES, false);
-          String extCollectionName = req.getParams().get(COLLECTION_PROP);
-          String collectionName =
-              followAliases
-                  ? h.coreContainer
-                      .getZkController()
-                      .getZkStateReader()
-                      .getAliases()
-                      .resolveSimpleAlias(extCollectionName)
-                  : extCollectionName;
-          if (!ImplicitDocRouter.NAME.equals(
-              ((Map<?, ?>)
-                      clusterState
-                          .getCollection(collectionName)
-                          .get(CollectionStateProps.DOC_ROUTER))
-                  .get(NAME)))
-            throw new SolrException(
-                ErrorCode.BAD_REQUEST, "shards can be added only to 'implicit' collections");
-          copy(
-              req.getParams(),
-              map,
-              REPLICATION_FACTOR,
-              NRT_REPLICAS,
-              TLOG_REPLICAS,
-              PULL_REPLICAS,
-              CREATE_NODE_SET,
-              WAIT_FOR_FINAL_STATE,
-              FOLLOW_ALIASES);
-          return copyPropertiesWithPrefix(req.getParams(), map, PROPERTY_PREFIX);
+          CreateShardAPI.invokeFromV1Params(h.coreContainer, req, rsp);
+          return null;
         }),
     DELETEREPLICA_OP(
         DELETEREPLICA,
@@ -1553,6 +1518,7 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
         CreateAliasAPI.class,
         CreateCollectionAPI.class,
         CreateCollectionBackupAPI.class,
+        CreateShardAPI.class,
         DeleteAliasAPI.class,
         DeleteCollectionBackupAPI.class,
         DeleteCollectionAPI.class,
@@ -1577,7 +1543,6 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
   public Collection<Api> getApis() {
     final List<Api> apis = new ArrayList<>();
     apis.addAll(AnnotatedApi.getApis(new SplitShardAPI(this)));
-    apis.addAll(AnnotatedApi.getApis(new CreateShardAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new AddReplicaAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new SyncShardAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new ForceLeaderAPI(this)));
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateShardAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateShardAPI.java
index 080068c4d69..3f39404f45a 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateShardAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateShardAPI.java
@@ -17,65 +17,215 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.POST;
+import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.CREATE_NODE_SET;
+import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
+import static org.apache.solr.common.cloud.ZkStateReader.NRT_REPLICAS;
+import static org.apache.solr.common.cloud.ZkStateReader.PULL_REPLICAS;
+import static org.apache.solr.common.cloud.ZkStateReader.REPLICATION_FACTOR;
+import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
+import static org.apache.solr.common.cloud.ZkStateReader.TLOG_REPLICAS;
 import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION;
 import static org.apache.solr.common.params.CollectionAdminParams.CREATE_NODE_SET_PARAM;
+import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
 import static org.apache.solr.common.params.CollectionAdminParams.PROPERTY_PREFIX;
-import static org.apache.solr.common.params.CommonParams.ACTION;
-import static org.apache.solr.handler.ClusterAPI.wrapParams;
-import static org.apache.solr.handler.api.V2ApiUtils.flattenMapWithPrefix;
+import static org.apache.solr.common.params.CollectionAdminParams.SHARD;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
+import static org.apache.solr.common.params.CommonParams.NAME;
+import static org.apache.solr.handler.admin.api.CreateCollectionAPI.copyPrefixedPropertiesWithoutPrefix;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import org.apache.solr.api.Command;
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.api.PayloadObj;
-import org.apache.solr.client.solrj.request.beans.CreateShardPayload;
+import javax.inject.Inject;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import org.apache.solr.client.solrj.util.SolrIdentifierValidator;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.cloud.ClusterState;
+import org.apache.solr.common.cloud.DocCollection;
+import org.apache.solr.common.cloud.ImplicitDocRouter;
+import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.CollectionParams;
-import org.apache.solr.handler.admin.CollectionsHandler;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.api.V2ApiUtils;
+import org.apache.solr.jersey.JacksonReflectMapWriter;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SubResponseAccumulatingJerseyResponse;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
 
 /**
  * V2 API for creating a new shard in a collection.
  *
- * <p>This API (POST /v2/collections/collectionName/shards {'create': {...}}) is analogous to the v1
+ * <p>This API (POST /v2/collections/collectionName/shards {...}) is analogous to the v1
  * /admin/collections?action=CREATESHARD command.
- *
- * @see CreateShardAPI
  */
-@EndPoint(
-    path = {"/c/{collection}/shards", "/collections/{collection}/shards"},
-    method = POST,
-    permission = COLL_EDIT_PERM)
-public class CreateShardAPI {
-  private static final String V2_CREATE_CMD = "create";
+@Path("/collections/{collectionName}/shards")
+public class CreateShardAPI extends AdminAPIBase {
+
+  @Inject
+  public CreateShardAPI(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+  }
 
-  private final CollectionsHandler collectionsHandler;
+  @POST
+  @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+  @PermissionName(COLL_EDIT_PERM)
+  public SubResponseAccumulatingJerseyResponse createShard(
+      @PathParam("collectionName") String collectionName, CreateShardRequestBody requestBody)
+      throws Exception {
+    final var response = instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
+    if (requestBody == null) {
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST, "Required request-body is missing");
+    }
+    ensureRequiredParameterProvided(COLLECTION_PROP, collectionName);
+    ensureRequiredParameterProvided(SHARD_ID_PROP, requestBody.shardName);
+    SolrIdentifierValidator.validateShardName(requestBody.shardName);
+    final String resolvedCollectionName =
+        resolveAndValidateAliasIfEnabled(
+            collectionName, Boolean.TRUE.equals(requestBody.followAliases));
+    ensureCollectionUsesImplicitRouter(resolvedCollectionName);
 
-  public CreateShardAPI(CollectionsHandler collectionsHandler) {
-    this.collectionsHandler = collectionsHandler;
+    final ZkNodeProps remoteMessage = createRemoteMessage(resolvedCollectionName, requestBody);
+    submitRemoteMessageAndHandleResponse(
+        response,
+        CollectionParams.CollectionAction.CREATESHARD,
+        remoteMessage,
+        requestBody.asyncId);
+    return response;
   }
 
-  @Command(name = V2_CREATE_CMD)
-  public void createShard(PayloadObj<CreateShardPayload> obj) throws Exception {
-    final CreateShardPayload v2Body = obj.get();
-    final Map<String, Object> v1Params = v2Body.toMap(new HashMap<>());
-    v1Params.put(ACTION, CollectionParams.CollectionAction.CREATESHARD.toLower());
-    v1Params.put(COLLECTION, obj.getRequest().getPathTemplateValues().get(COLLECTION));
+  public static class CreateShardRequestBody implements JacksonReflectMapWriter {
+    @JsonProperty(NAME)
+    public String shardName;
+
+    @JsonProperty(REPLICATION_FACTOR)
+    public Integer replicationFactor;
+
+    @JsonProperty(NRT_REPLICAS)
+    public Integer nrtReplicas;
+
+    @JsonProperty(TLOG_REPLICAS)
+    public Integer tlogReplicas;
+
+    @JsonProperty(PULL_REPLICAS)
+    public Integer pullReplicas;
+
+    @JsonProperty("createReplicas")
+    public Boolean createReplicas;
+
+    @JsonProperty("nodeSet")
+    public List<String> nodeSet;
+
+    @JsonProperty(WAIT_FOR_FINAL_STATE)
+    public Boolean waitForFinalState;
 
-    if (v2Body.nodeSet != null) {
-      v1Params.put(CREATE_NODE_SET_PARAM, buildV1CreateNodeSetValue(v2Body.nodeSet));
+    @JsonProperty(FOLLOW_ALIASES)
+    public Boolean followAliases;
+
+    @JsonProperty(ASYNC)
+    public String asyncId;
+
+    @JsonProperty public Map<String, String> properties;
+
+    public static CreateShardRequestBody fromV1Params(SolrParams params) {
+      params.required().check(COLLECTION, SHARD);
+
+      final var requestBody = new CreateShardRequestBody();
+      requestBody.shardName = params.get(SHARD);
+      requestBody.replicationFactor = params.getInt(REPLICATION_FACTOR);
+      requestBody.nrtReplicas = params.getInt(NRT_REPLICAS);
+      requestBody.tlogReplicas = params.getInt(TLOG_REPLICAS);
+      requestBody.pullReplicas = params.getInt(PULL_REPLICAS);
+      if (params.get(CREATE_NODE_SET_PARAM) != null) {
+        final String nodeSetStr = params.get(CREATE_NODE_SET_PARAM);
+        if ("EMPTY".equals(nodeSetStr)) {
+          requestBody.createReplicas = false;
+        } else {
+          requestBody.nodeSet = Arrays.asList(nodeSetStr.split(","));
+        }
+      }
+      requestBody.waitForFinalState = params.getBool(WAIT_FOR_FINAL_STATE);
+      requestBody.followAliases = params.getBool(FOLLOW_ALIASES);
+      requestBody.asyncId = params.get(ASYNC);
+      requestBody.properties =
+          copyPrefixedPropertiesWithoutPrefix(params, new HashMap<>(), PROPERTY_PREFIX);
+
+      return requestBody;
     }
+  }
 
-    flattenMapWithPrefix(v2Body.coreProperties, v1Params, PROPERTY_PREFIX);
-    collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), v1Params), obj.getResponse());
+  public static void invokeFromV1Params(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse)
+      throws Exception {
+    final var requestBody = CreateShardRequestBody.fromV1Params(solrQueryRequest.getParams());
+    final var createShardApi =
+        new CreateShardAPI(coreContainer, solrQueryRequest, solrQueryResponse);
+    final var response =
+        createShardApi.createShard(solrQueryRequest.getParams().get(COLLECTION), requestBody);
+    V2ApiUtils.squashIntoSolrResponseWithoutHeader(solrQueryResponse, response);
   }
 
-  private String buildV1CreateNodeSetValue(List<String> nodeSet) {
-    if (nodeSet.size() > 0) {
-      return String.join(",", nodeSet);
+  public static ZkNodeProps createRemoteMessage(
+      String collectionName, CreateShardRequestBody requestBody) {
+    final Map<String, Object> remoteMessage = new HashMap<>();
+    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.CREATESHARD.toLower());
+    remoteMessage.put(COLLECTION_PROP, collectionName);
+    remoteMessage.put(SHARD_ID_PROP, requestBody.shardName);
+    if (requestBody.createReplicas == null || requestBody.createReplicas) {
+      // The remote message expects a single comma-delimited string, so nodeSet requires flattening
+      if (requestBody.nodeSet != null) {
+        remoteMessage.put(CREATE_NODE_SET_PARAM, String.join(",", requestBody.nodeSet));
+      }
+    } else {
+      remoteMessage.put(CREATE_NODE_SET, "EMPTY");
     }
-    return "EMPTY";
+    insertIfNotNull(remoteMessage, REPLICATION_FACTOR, requestBody.replicationFactor);
+    insertIfNotNull(remoteMessage, NRT_REPLICAS, requestBody.nrtReplicas);
+    insertIfNotNull(remoteMessage, TLOG_REPLICAS, requestBody.tlogReplicas);
+    insertIfNotNull(remoteMessage, PULL_REPLICAS, requestBody.pullReplicas);
+    insertIfNotNull(remoteMessage, WAIT_FOR_FINAL_STATE, requestBody.waitForFinalState);
+    insertIfNotNull(remoteMessage, FOLLOW_ALIASES, requestBody.followAliases);
+    insertIfNotNull(remoteMessage, ASYNC, requestBody.asyncId);
+
+    if (requestBody.properties != null) {
+      requestBody
+          .properties
+          .entrySet()
+          .forEach(
+              entry -> {
+                remoteMessage.put(PROPERTY_PREFIX + entry.getKey(), entry.getValue());
+              });
+    }
+
+    return new ZkNodeProps(remoteMessage);
+  }
+
+  private void ensureCollectionUsesImplicitRouter(String collectionName) {
+    final ClusterState clusterState = coreContainer.getZkController().getClusterState();
+    if (!ImplicitDocRouter.NAME.equals(
+        ((Map<?, ?>)
+                clusterState
+                    .getCollection(collectionName)
+                    .get(DocCollection.CollectionStateProps.DOC_ROUTER))
+            .get(NAME)))
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST,
+          "shards can be added only to 'implicit' collections");
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/CreateShardAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/CreateShardAPITest.java
new file mode 100644
index 00000000000..07b71d8a805
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/CreateShardAPITest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.solr.handler.admin.api;
+
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.common.cloud.ZkStateReader.NRT_REPLICAS;
+import static org.apache.solr.common.cloud.ZkStateReader.PULL_REPLICAS;
+import static org.apache.solr.common.cloud.ZkStateReader.REPLICATION_FACTOR;
+import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
+import static org.apache.solr.common.cloud.ZkStateReader.TLOG_REPLICAS;
+import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION;
+import static org.apache.solr.common.params.CollectionAdminParams.CREATE_NODE_SET_PARAM;
+import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STATE;
+import static org.hamcrest.Matchers.containsString;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.hamcrest.MatcherAssert;
+import org.junit.Test;
+
+/** Unit tests for {@link CreateShardAPI} */
+public class CreateShardAPITest extends SolrTestCaseJ4 {
+
+  @Test
+  public void testReportsErrorIfRequestBodyMissing() {
+    final SolrException thrown =
+        expectThrows(
+            SolrException.class,
+            () -> {
+              final var api = new CreateShardAPI(null, null, null);
+              api.createShard("someCollName", null);
+            });
+
+    assertEquals(400, thrown.code());
+    assertEquals("Required request-body is missing", thrown.getMessage());
+  }
+
+  @Test
+  public void testReportsErrorIfCollectionNameMissing() {
+    final var requestBody = new CreateShardAPI.CreateShardRequestBody();
+    requestBody.shardName = "someShardName";
+    final SolrException thrown =
+        expectThrows(
+            SolrException.class,
+            () -> {
+              final var api = new CreateShardAPI(null, null, null);
+              api.createShard(null, requestBody);
+            });
+
+    assertEquals(400, thrown.code());
+    assertEquals("Missing required parameter: collection", thrown.getMessage());
+  }
+
+  @Test
+  public void testReportsErrorIfShardNameMissing() {
+    final var requestBody = new CreateShardAPI.CreateShardRequestBody();
+    requestBody.shardName = null;
+    final SolrException thrown =
+        expectThrows(
+            SolrException.class,
+            () -> {
+              final var api = new CreateShardAPI(null, null, null);
+              api.createShard("someCollectionName", requestBody);
+            });
+
+    assertEquals(400, thrown.code());
+    assertEquals("Missing required parameter: shard", thrown.getMessage());
+  }
+
+  @Test
+  public void testReportsErrorIfShardNameIsInvalid() {
+    final var requestBody = new CreateShardAPI.CreateShardRequestBody();
+    requestBody.shardName = "invalid$shard@name";
+    final SolrException thrown =
+        expectThrows(
+            SolrException.class,
+            () -> {
+              final var api = new CreateShardAPI(null, null, null);
+              api.createShard("someCollectionName", requestBody);
+            });
+
+    assertEquals(400, thrown.code());
+    MatcherAssert.assertThat(
+        thrown.getMessage(), containsString("Invalid shard: [invalid$shard@name]"));
+  }
+
+  @Test
+  public void testCreateRemoteMessageAllProperties() {
+    final var requestBody = new CreateShardAPI.CreateShardRequestBody();
+    requestBody.shardName = "someShardName";
+    requestBody.replicationFactor = 123;
+    requestBody.nrtReplicas = 123;
+    requestBody.tlogReplicas = 456;
+    requestBody.pullReplicas = 789;
+    requestBody.createReplicas = true;
+    requestBody.nodeSet = List.of("node1", "node2");
+    requestBody.waitForFinalState = true;
+    requestBody.followAliases = true;
+    requestBody.asyncId = "someAsyncId";
+    requestBody.properties = Map.of("propName1", "propVal1", "propName2", "propVal2");
+
+    final var remoteMessage =
+        CreateShardAPI.createRemoteMessage("someCollectionName", requestBody).getProperties();
+
+    assertEquals(13, remoteMessage.size());
+    assertEquals("createshard", remoteMessage.get(QUEUE_OPERATION));
+    assertEquals("someCollectionName", remoteMessage.get(COLLECTION));
+    assertEquals("someShardName", remoteMessage.get(SHARD_ID_PROP));
+    assertEquals(123, remoteMessage.get(REPLICATION_FACTOR));
+    assertEquals(123, remoteMessage.get(NRT_REPLICAS));
+    assertEquals(456, remoteMessage.get(TLOG_REPLICAS));
+    assertEquals(789, remoteMessage.get(PULL_REPLICAS));
+    assertEquals("node1,node2", remoteMessage.get(CREATE_NODE_SET_PARAM));
+    assertEquals(true, remoteMessage.get(WAIT_FOR_FINAL_STATE));
+    assertEquals(true, remoteMessage.get(FOLLOW_ALIASES));
+    assertEquals("someAsyncId", remoteMessage.get(ASYNC));
+    assertEquals("propVal1", remoteMessage.get("property.propName1"));
+    assertEquals("propVal2", remoteMessage.get("property.propName2"));
+  }
+
+  @Test
+  public void testCanConvertV1ParamsToV2RequestBody() {
+    final var v1Params = new ModifiableSolrParams();
+    v1Params.add(COLLECTION, "someCollectionName");
+    v1Params.add(SHARD_ID_PROP, "someShardName");
+    v1Params.set(REPLICATION_FACTOR, 123);
+    v1Params.set(NRT_REPLICAS, 123);
+    v1Params.set(TLOG_REPLICAS, 456);
+    v1Params.set(PULL_REPLICAS, 789);
+    v1Params.add(CREATE_NODE_SET_PARAM, "node1,node2");
+    v1Params.set(WAIT_FOR_FINAL_STATE, true);
+    v1Params.set(FOLLOW_ALIASES, true);
+    v1Params.add(ASYNC, "someAsyncId");
+    v1Params.add("property.propName1", "propVal1");
+    v1Params.add("property.propName2", "propVal2");
+
+    final var requestBody = CreateShardAPI.CreateShardRequestBody.fromV1Params(v1Params);
+
+    assertEquals("someShardName", requestBody.shardName);
+    assertEquals(Integer.valueOf(123), requestBody.replicationFactor);
+    assertEquals(Integer.valueOf(123), requestBody.nrtReplicas);
+    assertEquals(Integer.valueOf(456), requestBody.tlogReplicas);
+    assertEquals(Integer.valueOf(789), requestBody.pullReplicas);
+    assertNull(requestBody.createReplicas);
+    assertEquals(List.of("node1", "node2"), requestBody.nodeSet);
+    assertEquals(Boolean.TRUE, requestBody.waitForFinalState);
+    assertEquals(Boolean.TRUE, requestBody.followAliases);
+    assertEquals("someAsyncId", requestBody.asyncId);
+    assertEquals("propVal1", requestBody.properties.get("propName1"));
+    assertEquals("propVal2", requestBody.properties.get("propName2"));
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java
index af4fda1cd43..3d8d1448329 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/V2ShardsAPIMappingTest.java
@@ -17,11 +17,7 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.common.cloud.ZkStateReader.NRT_REPLICAS;
-import static org.apache.solr.common.cloud.ZkStateReader.PULL_REPLICAS;
-import static org.apache.solr.common.cloud.ZkStateReader.REPLICATION_FACTOR;
 import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
-import static org.apache.solr.common.cloud.ZkStateReader.TLOG_REPLICAS;
 import static org.apache.solr.common.params.CollectionAdminParams.COLLECTION;
 import static org.apache.solr.common.params.CollectionAdminParams.CREATE_NODE_SET_PARAM;
 import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
@@ -59,7 +55,6 @@ public class V2ShardsAPIMappingTest extends V2ApiMappingTest<CollectionsHandler>
   public void populateApiBag() {
     final CollectionsHandler collectionsHandler = getRequestHandler();
     apiBag.registerObject(new SplitShardAPI(collectionsHandler));
-    apiBag.registerObject(new CreateShardAPI(collectionsHandler));
     apiBag.registerObject(new AddReplicaAPI(collectionsHandler));
     apiBag.registerObject(new SyncShardAPI(collectionsHandler));
     apiBag.registerObject(new ForceLeaderAPI(collectionsHandler));
@@ -137,42 +132,6 @@ public class V2ShardsAPIMappingTest extends V2ApiMappingTest<CollectionsHandler>
     assertEquals("bar1", v1Params.get("property.bar"));
   }
 
-  @Test
-  public void testCreateShardAllProperties() throws Exception {
-    final SolrParams v1Params =
-        captureConvertedV1Params(
-            "/collections/collName/shards",
-            "POST",
-            "{ 'create': {"
-                + "'shard': 'shard1', "
-                + "'nodeSet': ['foo', 'bar', 'baz'], "
-                + "'followAliases': true, "
-                + "'async': 'some_async_id', "
-                + "'waitForFinalState': true, "
-                + "'replicationFactor': 123, "
-                + "'nrtReplicas': 456, "
-                + "'tlogReplicas': 789, "
-                + "'pullReplicas': 101, "
-                + "'coreProperties': {"
-                + "    'foo': 'foo1', "
-                + "    'bar': 'bar1', "
-                + "}}}");
-
-    assertEquals(CollectionParams.CollectionAction.CREATESHARD.lowerName, v1Params.get(ACTION));
-    assertEquals("collName", v1Params.get(COLLECTION));
-    assertEquals("shard1", v1Params.get(SHARD_ID_PROP));
-    assertEquals("foo,bar,baz", v1Params.get(CREATE_NODE_SET_PARAM));
-    assertTrue(v1Params.getPrimitiveBool(FOLLOW_ALIASES));
-    assertEquals("some_async_id", v1Params.get(ASYNC));
-    assertTrue(v1Params.getPrimitiveBool(WAIT_FOR_FINAL_STATE));
-    assertEquals(123, v1Params.getPrimitiveInt(REPLICATION_FACTOR));
-    assertEquals(456, v1Params.getPrimitiveInt(NRT_REPLICAS));
-    assertEquals(789, v1Params.getPrimitiveInt(TLOG_REPLICAS));
-    assertEquals(101, v1Params.getPrimitiveInt(PULL_REPLICAS));
-    assertEquals("foo1", v1Params.get("property.foo"));
-    assertEquals("bar1", v1Params.get("property.bar"));
-  }
-
   @Test
   public void testAddReplicaAllProperties() throws Exception {
     final SolrParams v1Params =
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/shard-management.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/shard-management.adoc
index a691743c1e5..c8548b573b9 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/shard-management.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/shard-management.adoc
@@ -324,9 +324,7 @@ http://localhost:8983/solr/admin/collections?action=CREATESHARD&shard=newShardNa
 ----
 curl -X POST http://localhost:8983/api/collections/techproducts/shards -H 'Content-Type: application/json' -d '
   {
-    "create":{
-      "shard":"newShardName"
-    }
+    "shard":"newShardName"
   }
 '
 ----
@@ -358,6 +356,8 @@ s|Required |Default: none
 +
 
 The name of the collection that includes the shard to be split.
+Provided as a query parameter in v1 requests, and as a path parameter for v2 requests.
+
 
 `shard`::
 +