This is an automated email from the ASF dual-hosted git repository.
epugh 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 c6d595b2218 SOLR-16390: Migrate list cluster nodes API to JAX-RS
(#4865)
c6d595b2218 is described below
commit c6d595b22182d53fc825ef6885aef2a13180740f
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]>
---
.../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 9c5def46a6f..d292cb4a05f 100644
--- a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
@@ -210,11 +210,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 0c13cd33670..9b8662176d6 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
@@ -184,6 +184,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;
@@ -1189,6 +1190,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 0c06f73b92b..c61716aa5e5 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
@@ -212,6 +212,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"}}
});