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

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


The following commit(s) were added to refs/heads/branch_9x by this push:
     new 762ab9bb311 SOLR-11029 Create a v2 API equivalent for DELETENODE API
762ab9bb311 is described below

commit 762ab9bb3111d82fc60e15906a0555dbad22d5f9
Author: bszabo97 <[email protected]>
AuthorDate: Wed Feb 15 19:13:52 2023 +0200

    SOLR-11029 Create a v2 API equivalent for DELETENODE API
    
    The "DELETENODE" functionality is now exposed in the v2 API at:
    
    `POST /api/cluster/nodes/nodeName/clear {...}`
    
    Co-authored-by: Jason Gerlowski <[email protected]>
---
 solr/CHANGES.txt                                   |   3 +
 .../solr/handler/admin/CollectionsHandler.java     |  16 ++-
 .../solr/handler/admin/api/DeleteNodeAPI.java      | 130 +++++++++++++++++++++
 .../solr/handler/admin/api/DeleteNodeAPITest.java  |  73 ++++++++++++
 .../pages/cluster-node-management.adoc             |   9 +-
 5 files changed, 228 insertions(+), 3 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index cbd52a1adb7..8caea375f31 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -119,6 +119,9 @@ Improvements
 
 * SOLR-16665: The base docker image has been upgraded from Ubuntu 20 (Focal) 
to Ubuntu 22 (Jammy). (Houston Putman)
 
+* SOLR-11029: A v2 equivalent of the `/admin/collections?action=DELETENODE` 
command is now available at
+  `POST /api/cluster/nodes/nodeName/clear`. (Bence Szabo via 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 a4a7679daac..75d94fe63d9 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
@@ -215,6 +215,7 @@ import 
org.apache.solr.handler.admin.api.BalanceShardUniqueAPI;
 import org.apache.solr.handler.admin.api.CollectionStatusAPI;
 import org.apache.solr.handler.admin.api.CreateShardAPI;
 import org.apache.solr.handler.admin.api.DeleteCollectionAPI;
+import org.apache.solr.handler.admin.api.DeleteNodeAPI;
 import org.apache.solr.handler.admin.api.DeleteReplicaAPI;
 import org.apache.solr.handler.admin.api.DeleteReplicaPropertyAPI;
 import org.apache.solr.handler.admin.api.DeleteShardAPI;
@@ -1898,7 +1899,15 @@ public class CollectionsHandler extends 
RequestHandlerBase implements Permission
               "shard",
               FOLLOW_ALIASES);
         }),
-    DELETENODE_OP(DELETENODE, (req, rsp, h) -> 
copy(req.getParams().required(), null, "node")),
+    DELETENODE_OP(
+        DELETENODE,
+        (req, rsp, h) -> {
+          final DeleteNodeAPI deleteNodeAPI = new 
DeleteNodeAPI(h.coreContainer, req, rsp);
+          final SolrJerseyResponse deleteNodeResponse =
+              DeleteNodeAPI.invokeUsingV1Inputs(deleteNodeAPI, 
req.getParams());
+          V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, 
deleteNodeResponse);
+          return null;
+        }),
     MOCK_COLL_TASK_OP(
         MOCK_COLL_TASK,
         (req, rsp, h) -> {
@@ -2225,7 +2234,10 @@ public class CollectionsHandler extends 
RequestHandlerBase implements Permission
   @Override
   public Collection<Class<? extends JerseyResource>> getJerseyResources() {
     return List.of(
-        AddReplicaPropertyAPI.class, DeleteReplicaPropertyAPI.class, 
ReplaceNodeAPI.class);
+        AddReplicaPropertyAPI.class,
+        DeleteReplicaPropertyAPI.class,
+        ReplaceNodeAPI.class,
+        DeleteNodeAPI.class);
   }
 
   @Override
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteNodeAPI.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteNodeAPI.java
new file mode 100644
index 00000000000..868e2b93273
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteNodeAPI.java
@@ -0,0 +1,130 @@
+/*
+ * 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.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CoreAdminParams.NODE;
+import static 
org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
+import static 
org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import java.util.HashMap;
+import java.util.Map;
+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.SolrResponse;
+import org.apache.solr.common.cloud.ZkNodeProps;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.common.params.RequiredSolrParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.CollectionsHandler;
+import org.apache.solr.jersey.JacksonReflectMapWriter;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SolrJerseyResponse;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for deleting all replicas of all collections in one node. Please 
note that the node itself
+ * will remain as a live node after this operation.
+ *
+ * <p>This API is analogous to the V1 /admin/collections?action=DELETENODE
+ */
+@Path("cluster/nodes/{nodeName}/clear/")
+public class DeleteNodeAPI extends AdminAPIBase {
+
+  @Inject
+  public DeleteNodeAPI(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+  }
+
+  @POST
+  @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+  @PermissionName(COLL_EDIT_PERM)
+  public SolrJerseyResponse deleteNode(
+      @Parameter(
+              description =
+                  "The name of the node to be cleared.  Usually of the form 
'host:1234_solr'.",
+              required = true)
+          @PathParam("nodeName")
+          String nodeName,
+      @RequestBody(description = "Contains user provided parameters", required 
= true)
+          DeleteNodeRequestBody requestBody)
+      throws Exception {
+    final SolrJerseyResponse response = 
instantiateJerseyResponse(SolrJerseyResponse.class);
+    final CoreContainer coreContainer = 
fetchAndValidateZooKeeperAwareCoreContainer();
+    final ZkNodeProps remoteMessage = createRemoteMessage(nodeName, 
requestBody);
+    final SolrResponse remoteResponse =
+        CollectionsHandler.submitCollectionApiCommand(
+            coreContainer,
+            coreContainer.getDistributedCollectionCommandRunner(),
+            remoteMessage,
+            CollectionParams.CollectionAction.DELETENODE,
+            DEFAULT_COLLECTION_OP_TIMEOUT);
+    if (remoteResponse.getException() != null) {
+      throw remoteResponse.getException();
+    }
+    disableResponseCaching();
+    return response;
+  }
+
+  public static SolrJerseyResponse invokeUsingV1Inputs(DeleteNodeAPI 
apiInstance, SolrParams params)
+      throws Exception {
+    final RequiredSolrParams requiredParams = params.required();
+    final DeleteNodeRequestBody requestBody = new 
DeleteNodeRequestBody(params.get(ASYNC));
+    return apiInstance.deleteNode(requiredParams.get(NODE), requestBody);
+  }
+
+  public static ZkNodeProps createRemoteMessage(
+      String nodeName, DeleteNodeRequestBody requestBody) {
+    Map<String, Object> remoteMessage = new HashMap<>();
+    remoteMessage.put(NODE, nodeName);
+    if (requestBody != null) {
+      if (requestBody.async != null) {
+        remoteMessage.put(ASYNC, requestBody.async);
+      }
+    }
+    remoteMessage.put(QUEUE_OPERATION, 
CollectionParams.CollectionAction.DELETENODE.toLower());
+
+    return new ZkNodeProps(remoteMessage);
+  }
+
+  public static class DeleteNodeRequestBody implements JacksonReflectMapWriter 
{
+
+    public DeleteNodeRequestBody() {}
+
+    public DeleteNodeRequestBody(String async) {
+      this.async = async;
+    }
+
+    @Schema(description = "Request ID to track this action which will be 
processed asynchronously.")
+    @JsonProperty("async")
+    public String async;
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteNodeAPITest.java 
b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteNodeAPITest.java
new file mode 100644
index 00000000000..f306881bf41
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteNodeAPITest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.mockito.Mockito.mock;
+
+import java.util.Map;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.cloud.ZkNodeProps;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/** Unit tests for {@link DeleteNodeAPI} */
+public class DeleteNodeAPITest extends SolrTestCaseJ4 {
+
+  @BeforeClass
+  public static void ensureWorkingMockito() {
+    assumeWorkingMockito();
+  }
+
+  @Test
+  public void testV1InvocationThrowsErrorsIfRequiredParametersMissing() {
+    final var api = mock(DeleteNodeAPI.class);
+    final SolrException e =
+        expectThrows(
+            SolrException.class,
+            () -> {
+              DeleteNodeAPI.invokeUsingV1Inputs(api, new 
ModifiableSolrParams());
+            });
+    assertEquals("Missing required parameter: node", e.getMessage());
+  }
+
+  @Test
+  public void testValidOverseerMessageIsCreated() {
+    DeleteNodeAPI.DeleteNodeRequestBody requestBody =
+        new DeleteNodeAPI.DeleteNodeRequestBody("async");
+    final ZkNodeProps createdMessage =
+        DeleteNodeAPI.createRemoteMessage("nodeNameToDelete", requestBody);
+    final Map<String, Object> createdMessageProps = 
createdMessage.getProperties();
+    assertEquals(3, createdMessageProps.size());
+    assertEquals("nodeNameToDelete", createdMessageProps.get("node"));
+    assertEquals("async", createdMessageProps.get("async"));
+    assertEquals("deletenode", createdMessageProps.get("operation"));
+  }
+
+  @Test
+  public void testRequestBodyCanBeOmitted() throws Exception {
+    final ZkNodeProps createdMessage = 
DeleteNodeAPI.createRemoteMessage("nodeNameToDelete", null);
+    final Map<String, Object> createdMessageProps = 
createdMessage.getProperties();
+    assertEquals(2, createdMessageProps.size());
+    assertEquals("nodeNameToDelete", createdMessageProps.get("node"));
+    assertEquals("deletenode", createdMessageProps.get("operation"));
+    assertFalse(
+        "Expected message to not contain value for async: " + 
createdMessageProps.get("async"),
+        createdMessageProps.containsKey("async"));
+  }
+}
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/cluster-node-management.adoc
 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cluster-node-management.adoc
index c0abf5cffb7..6fb17351171 100644
--- 
a/solr/solr-ref-guide/modules/deployment-guide/pages/cluster-node-management.adoc
+++ 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cluster-node-management.adoc
@@ -590,7 +590,14 @@ 
http://localhost:8983/solr/admin/collections?action=DELETENODE&node=nodeName
 ====
 [.tab-label]*V2 API*
 
-We do not currently have a V2 equivalent.
+[source,bash]
+----
+curl -X POST 
"http://localhost:8983/api/cluster/nodes/localhost:7574_solr/clear/"; -H 
'Content-Type: application/json' -d '
+    {
+      "async": "someAsyncId"
+    }
+'
+----
 
 ====
 --

Reply via email to