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

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

commit e0c344a59e249773376615dab3907284231a01df
Author: Prithvi S <[email protected]>
AuthorDate: Wed Sep 9 06:26:21 2026 +0530

    SOLR-16390: Migrate list cluster nodes API to JAX-RS (#4865)
    
    Signed-off-by: prithvi <[email protected]>
    Co-authored-by: Eric Pugh <[email protected]>
    (cherry picked from commit c6d595b22182d53fc825ef6885aef2a13180740f)
---
 .../unreleased/SOLR-16390-list-cluster-nodes.yml   |  8 ++++
 dev-docs/v2-api-conventions.adoc                   |  1 +
 .../client/api/model/ListClusterNodesResponse.java |  6 +--
 .../java/org/apache/solr/handler/ClusterAPI.java   |  5 --
 .../solr/handler/admin/CollectionsHandler.java     |  2 +
 .../solr/handler/admin/api/ListClusterNodes.java   | 53 ++++++++++++++++++++++
 .../admin/api/ListClusterNodesStandaloneTest.java  | 45 ++++++++++++++++++
 .../handler/admin/api/ListClusterNodesTest.java    | 51 +++++++++++++++++++++
 .../modules/configuration-guide/pages/v2-api.adoc  |  2 +-
 .../pages/cluster-node-management.adoc             | 35 ++++++++++++++
 solr/webapp/web/js/angular/controllers/cloud.js    | 13 +++---
 .../web/js/angular/controllers/collections.js      |  2 -
 solr/webapp/web/js/angular/services.js             |  1 -
 13 files changed, 204 insertions(+), 20 deletions(-)

diff --git a/changelog/unreleased/SOLR-16390-list-cluster-nodes.yml 
b/changelog/unreleased/SOLR-16390-list-cluster-nodes.yml
new file mode 100644
index 00000000000..937d5cd42b2
--- /dev/null
+++ b/changelog/unreleased/SOLR-16390-list-cluster-nodes.yml
@@ -0,0 +1,8 @@
+title: Migrated ListClusterNodes API from homegrown @EndPoint to JAX-RS
+type: other
+authors:
+  - name: Prithvi S
+    nick: iprithv
+links:
+  - name: SOLR-16390
+    url: https://issues.apache.org/jira/browse/SOLR-16390
diff --git a/dev-docs/v2-api-conventions.adoc b/dev-docs/v2-api-conventions.adoc
index 08a9d01fdba..0dc61247bea 100644
--- a/dev-docs/v2-api-conventions.adoc
+++ b/dev-docs/v2-api-conventions.adoc
@@ -21,6 +21,7 @@ Following these guidelines has given us the following 
(non-exhaustive) list of v
 * `/api/backups/specificBackupName`
 * `/api/backups/specificBackupName/versions`
 * `/api/backups/specificBackupName/versions/specificVersion`
+* `/api/cluster/nodes`
 * `/api/cluster/nodes/specificNodeName/roles`
 * `/api/cluster/nodes/specificNodeName/roles/specificRoleName`
 * `/api/cluster/commands`
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/model/ListClusterNodesResponse.java
 
b/solr/api/src/java/org/apache/solr/client/api/model/ListClusterNodesResponse.java
index bd7a0195ae1..9b67e0306e8 100644
--- 
a/solr/api/src/java/org/apache/solr/client/api/model/ListClusterNodesResponse.java
+++ 
b/solr/api/src/java/org/apache/solr/client/api/model/ListClusterNodesResponse.java
@@ -20,11 +20,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 import java.util.Set;
 
-/**
- * Response for the v2 "list cluster nodes" API. This is a bit unusual that 
it's wrapping a non
- * JAX-RS V2 API defined in org.apache.solr.handler.ClusterAPI.getNodes(). The 
calls are made using
- * just the defaults. TODO: Update this when we migrate ClusterAPI to JAX-RS.
- */
+/** Response body for {@code GET /api/cluster/nodes}. */
 public class ListClusterNodesResponse extends SolrJerseyResponse {
 
   @Schema(description = "The live nodes in the cluster.")
diff --git a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java 
b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
index 4e672348292..41dd007d955 100644
--- a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
@@ -215,11 +215,6 @@ public class ClusterAPI {
     return req;
   }
 
-  @EndPoint(method = GET, path = "/cluster/nodes", permission = COLL_READ_PERM)
-  public void getNodes(SolrQueryRequest req, SolrQueryResponse rsp) {
-    rsp.add("nodes", 
getCoreContainer().getZkController().getClusterState().getLiveNodes());
-  }
-
   @EndPoint(method = GET, path = "/cluster", permission = COLL_READ_PERM)
   public void getClusterStatus(SolrQueryRequest req, SolrQueryResponse rsp) 
throws Exception {
     final Map<String, Object> v1Params =
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 f901fffc6ab..95446f3fd4d 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
@@ -187,6 +187,7 @@ import org.apache.solr.handler.admin.api.DeleteShard;
 import org.apache.solr.handler.admin.api.ForceLeader;
 import org.apache.solr.handler.admin.api.InstallShardData;
 import org.apache.solr.handler.admin.api.ListAliases;
+import org.apache.solr.handler.admin.api.ListClusterNodes;
 import org.apache.solr.handler.admin.api.ListCollectionBackups;
 import org.apache.solr.handler.admin.api.ListCollectionSnapshots;
 import org.apache.solr.handler.admin.api.ListCollections;
@@ -1213,6 +1214,7 @@ public class CollectionsHandler extends 
RequestHandlerBase implements Permission
         DeleteShard.class,
         ForceLeader.class,
         InstallShardData.class,
+        ListClusterNodes.class,
         ListCollections.class,
         ListCollectionBackups.class,
         ReloadCollectionAPI.class,
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/ListClusterNodes.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/ListClusterNodes.java
new file mode 100644
index 00000000000..7b8fa6023c0
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ListClusterNodes.java
@@ -0,0 +1,53 @@
+/*
+ * 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.security.PermissionNameProvider.Name.COLL_READ_PERM;
+
+import jakarta.inject.Inject;
+import java.util.Set;
+import org.apache.solr.client.api.endpoint.ListClusterNodesApi;
+import org.apache.solr.client.api.model.ListClusterNodesResponse;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for listing live nodes in the SolrCloud cluster.
+ *
+ * <p>This API (GET /api/cluster/nodes) has no dedicated v1 equivalent; {@code
+ * /admin/collections?action=CLUSTERSTATUS} with {@code liveNodes=true} is the 
closest v1 form.
+ */
+public class ListClusterNodes extends AdminAPIBase implements 
ListClusterNodesApi {
+
+  @Inject
+  public ListClusterNodes(
+      CoreContainer coreContainer, SolrQueryRequest req, SolrQueryResponse 
rsp) {
+    super(coreContainer, req, rsp);
+  }
+
+  @Override
+  @PermissionName(COLL_READ_PERM)
+  public ListClusterNodesResponse listClusterNodes() {
+    final ListClusterNodesResponse response =
+        instantiateJerseyResponse(ListClusterNodesResponse.class);
+    validateZooKeeperAwareCoreContainer(coreContainer);
+    response.nodes = 
Set.copyOf(coreContainer.getZkController().getClusterState().getLiveNodes());
+    return response;
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/api/ListClusterNodesStandaloneTest.java
 
b/solr/core/src/test/org/apache/solr/handler/admin/api/ListClusterNodesStandaloneTest.java
new file mode 100644
index 00000000000..7d3050b9892
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/api/ListClusterNodesStandaloneTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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 org.apache.solr.SolrTestCase;
+import org.apache.solr.client.solrj.RemoteSolrException;
+import org.apache.solr.client.solrj.request.ClusterApi;
+import org.apache.solr.util.SolrJettyTestRule;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+/** Standalone coverage for {@code GET /api/cluster/nodes}. */
+public class ListClusterNodesStandaloneTest extends SolrTestCase {
+
+  @ClassRule public static final SolrJettyTestRule solrTestRule = new 
SolrJettyTestRule();
+
+  @BeforeClass
+  public static void setupSolr() throws Exception {
+    solrTestRule.startSolr(createTempDir());
+  }
+
+  @Test
+  public void testRequiresSolrCloud() {
+    final RemoteSolrException ex =
+        expectThrows(
+            RemoteSolrException.class,
+            () -> new 
ClusterApi.ListClusterNodes().process(solrTestRule.getAdminClient()));
+    assertEquals(400, ex.code());
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/api/ListClusterNodesTest.java
 
b/solr/core/src/test/org/apache/solr/handler/admin/api/ListClusterNodesTest.java
new file mode 100644
index 00000000000..9d47da1be11
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/api/ListClusterNodesTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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 java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.solr.client.api.model.ListClusterNodesResponse;
+import org.apache.solr.client.solrj.request.ClusterApi;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.embedded.JettySolrRunner;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/** HTTP tests for {@code GET /api/cluster/nodes} via the generated SolrJ 
ClusterApi client. */
+public class ListClusterNodesTest extends SolrCloudTestCase {
+
+  @BeforeClass
+  public static void setupCluster() throws Exception {
+    configureCluster(2).addConfig("conf", 
configset("cloud-minimal")).configure();
+  }
+
+  @Test
+  public void testListLiveNodes() throws Exception {
+    ListClusterNodesResponse rsp =
+        new ClusterApi.ListClusterNodes().process(cluster.getSolrClient());
+
+    assertNotNull(rsp);
+    assertNull(rsp.error);
+    assertNotNull(rsp.nodes);
+
+    Set<String> expected =
+        cluster.getJettySolrRunners().stream()
+            .map(JettySolrRunner::getNodeName)
+            .collect(Collectors.toSet());
+    assertEquals(expected, rsp.nodes);
+  }
+}
diff --git a/solr/solr-ref-guide/modules/configuration-guide/pages/v2-api.adoc 
b/solr/solr-ref-guide/modules/configuration-guide/pages/v2-api.adoc
index 844297cdce0..fb849d4f378 100644
--- a/solr/solr-ref-guide/modules/configuration-guide/pages/v2-api.adoc
+++ b/solr/solr-ref-guide/modules/configuration-guide/pages/v2-api.adoc
@@ -49,7 +49,7 @@ Following are some v2 API URL paths and path prefixes, along 
with some of the op
 |`/api/cores` |Create a core.
 |`/api/cores/_core-name_` |Reload, rename, delete, and unload a core.
 |`/api/node` |Perform overseer operation, rejoin leader election.
-|`/api/cluster` |Add role, remove role, set cluster property.
+|`/api/cluster` |List live nodes, add role, remove role, set cluster property.
 |===
 
 == Introspect
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 46517de459d..d1c84824235 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
@@ -226,6 +226,41 @@ 
http://localhost:8983/solr/admin/collections?action=CLUSTERSTATUS
 }
 ----
 
+[[list-cluster-nodes]]
+== List Cluster Nodes
+
+Fetch the live node names in the SolrCloud cluster.
+
+This is a v2-only API. The closest v1 equivalent is 
xref:#clusterstatus[CLUSTERSTATUS] with `liveNodes=true`.
+
+[tabs#list-cluster-nodes-request]
+======
+V2 API::
++
+====
+[source,bash]
+----
+curl -X GET http://localhost:8983/api/cluster/nodes
+----
+====
+======
+
+=== List Cluster Nodes Response
+
+The response includes a `nodes` array of live node names. SolrJ provides 
`ClusterApi.ListClusterNodes` for this request.
+
+[source,json]
+----
+{
+  "responseHeader":{
+    "status":0,
+    "QTime":3},
+  "nodes":[
+    "127.0.1.1:8983_solr",
+    "127.0.1.1:7574_solr"]
+}
+----
+
 [[clusterprop]]
 == CLUSTERPROP: Cluster Properties
 
diff --git a/solr/webapp/web/js/angular/controllers/cloud.js 
b/solr/webapp/web/js/angular/controllers/cloud.js
index 4dcc1324fbd..17d93d2c344 100644
--- a/solr/webapp/web/js/angular/controllers/cloud.js
+++ b/solr/webapp/web/js/angular/controllers/cloud.js
@@ -16,7 +16,7 @@
 */
 
 solrAdminApp.controller('CloudController',
-    function($scope, $location, $timeout, Zookeeper, Constants, Collections, 
SystemV2, Metrics, MetricsExtractor, ZookeeperStatus, ApiErrorHandler) {
+    function($scope, $location, $timeout, Zookeeper, Constants, Collections, 
ClusterV2, SystemV2, Metrics, MetricsExtractor, ZookeeperStatus, 
ApiErrorHandler) {
 
         $scope.showDebug = false;
 
@@ -34,7 +34,7 @@ solrAdminApp.controller('CloudController',
             treeSubController($scope, Zookeeper);
         } else if (view === "graph") {
             $scope.resetMenu("cloud-graph", Constants.IS_ROOT_PAGE);
-            graphSubController($scope, Zookeeper, false);
+            graphSubController($scope, Zookeeper, ClusterV2, ApiErrorHandler);
         } else if (view === "nodes") {
             $scope.resetMenu("cloud-nodes", Constants.IS_ROOT_PAGE);
             nodesSubController($scope, $timeout, Collections, SystemV2, 
Metrics, MetricsExtractor, ApiErrorHandler);
@@ -744,7 +744,7 @@ function secondsForHumans ( seconds ) {
     return returntext.trim() === '' ? '0m' : returntext.trim();
 }
 
-var graphSubController = function ($scope, Zookeeper) {
+var graphSubController = function ($scope, Zookeeper, ClusterV2, 
ApiErrorHandler) {
     $scope.showZkStatus = false;
     $scope.showTree = false;
     $scope.showGraph = true;
@@ -781,10 +781,11 @@ var graphSubController = function ($scope, Zookeeper) {
     };
 
     $scope.initGraph = function() {
-        Zookeeper.liveNodes(function (data) {
+        ClusterV2.listClusterNodes(function (error, data, response) {
+            if (error) { ApiErrorHandler.handle(response); return; }
             var live_nodes = {};
-            for (var c in data.tree[0].children) {
-                live_nodes[data.tree[0].children[c].text] = true;
+            for (var i = 0; i < data.nodes.length; i++) {
+                live_nodes[data.nodes[i]] = true;
             }
 
             var params = {view: "graph"};
diff --git a/solr/webapp/web/js/angular/controllers/collections.js 
b/solr/webapp/web/js/angular/controllers/collections.js
index 93fd5fb1b6d..d7a14b3dcf3 100644
--- a/solr/webapp/web/js/angular/controllers/collections.js
+++ b/solr/webapp/web/js/angular/controllers/collections.js
@@ -85,8 +85,6 @@ solrAdminApp.controller('CollectionsController',
                       }
                   });
               });
-
-              $scope.liveNodes = data.cluster.liveNodes;
           });
           ConfigSetsV2.listConfigSet(function(error, data, response) {
               $timeout(function() {
diff --git a/solr/webapp/web/js/angular/services.js 
b/solr/webapp/web/js/angular/services.js
index 1272fceff94..59c9add0635 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -194,7 +194,6 @@ solrAdminServices.factory('Metrics',
   ['$resource', function($resource) {
     return $resource('admin/zookeeper', {wt:'json', _:Date.now()}, {
       "simple": {},
-      "liveNodes": {params: {path: '/live_nodes'}},
       "clusterState": {params: {detail: "true", path: "/clusterstate.json"}},
       "detail": {params: {detail: "true", path: "@path"}}
     });

Reply via email to