From 26b2e1d27b9f34977d6ebc0b3c5b403e02f4d1b1 Mon Sep 17 00:00:00 2001 From: prithvi Date: Wed, 2 Sep 2026 20:12:46 +0530 Subject: [PATCH] SOLR-16390: Migrate list cluster nodes API to JAX-RS Signed-off-by: prithvi --- .../SOLR-16390-list-cluster-nodes.yml | 8 +++ dev-docs/v2-api-conventions.adoc | 1 + .../api/model/ListClusterNodesResponse.java | 6 +-- .../org/apache/solr/handler/ClusterAPI.java | 5 -- .../handler/admin/CollectionsHandler.java | 2 + .../handler/admin/api/ListClusterNodes.java | 53 +++++++++++++++++++ .../api/ListClusterNodesStandaloneTest.java | 45 ++++++++++++++++ .../admin/api/ListClusterNodesTest.java | 51 ++++++++++++++++++ .../configuration-guide/pages/v2-api.adoc | 2 +- .../pages/cluster-node-management.adoc | 35 ++++++++++++ 10 files changed, 197 insertions(+), 11 deletions(-) create mode 100644 changelog/unreleased/SOLR-16390-list-cluster-nodes.yml create mode 100644 solr/core/src/java/org/apache/solr/handler/admin/api/ListClusterNodes.java create mode 100644 solr/core/src/test/org/apache/solr/handler/admin/api/ListClusterNodesStandaloneTest.java create mode 100644 solr/core/src/test/org/apache/solr/handler/admin/api/ListClusterNodesTest.java 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 000000000000..937d5cd42b26 --- /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 d40d7dd4a485..64484f8a2950 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/properties` 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 bd7a0195ae1d..9b67e0306e83 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 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 36ee024f7f23..6e1e8ced4c86 100644 --- a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java +++ b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java @@ -239,11 +239,6 @@ public void getCommandStatus(SolrQueryRequest req, SolrQueryResponse rsp) throws collectionsHandler.handleRequestBody(wrapParams(req, v1Params), rsp); } - @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 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 270afc24906f..04ae8b7a51fe 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 @@ -196,6 +196,7 @@ 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; @@ -1363,6 +1364,7 @@ public Collection> getJerseyResources() { 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 000000000000..7b8fa6023c09 --- /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. + * + *

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 000000000000..7d3050b98927 --- /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 000000000000..9d47da1be11f --- /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 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 844297cdce09..fb849d4f3788 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 46517de459d0..d1c848242355 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