This is an automated email from the ASF dual-hosted git repository. sigram pushed a commit to branch branch_10_1 in repository https://gitbox.apache.org/repos/asf/solr.git
commit aec4a3fbf2064a208cff36031b1ac13983d29c9c Author: Andrzej BiaĆecki <[email protected]> AuthorDate: Tue Oct 20 12:18:55 2026 +0200 Revert " SOLR-18450: Relocate GET /api/cluster under /api/collections and return only the collections tree (#4952)" This reverts commit 2db17cf25860c6ed5ae20e18fa9eaa3ab9311f08. --- .../SOLR-18450-v2-collections-detailed.yml | 14 -- .../client/api/endpoint/ListCollectionsApi.java | 36 +--- .../client/api/model/ListCollectionsResponse.java | 100 --------- .../java/org/apache/solr/handler/ClusterAPI.java | 9 + .../apache/solr/handler/admin/ClusterStatus.java | 127 ++++------- .../solr/handler/admin/CollectionsHandler.java | 3 +- .../solr/handler/admin/api/ListCollections.java | 48 +---- .../solr/handler/V2ClusterAPIMappingTest.java | 8 + .../api/ListCollectionsDetailedStandaloneTest.java | 45 ---- .../admin/api/ListCollectionsDetailedTest.java | 236 --------------------- .../modules/configuration-guide/pages/v2-api.adoc | 10 +- .../pages/cluster-node-management.adoc | 25 ++- .../pages/collection-management.adoc | 200 ----------------- solr/webapp/web/js/angular/app.js | 2 +- .../web/js/angular/controllers/alias-overview.js | 2 +- solr/webapp/web/js/angular/controllers/cloud.js | 124 ++++++----- .../js/angular/controllers/collection-overview.js | 13 +- .../web/js/angular/controllers/collections.js | 11 +- solr/webapp/web/js/angular/services.js | 14 +- 19 files changed, 164 insertions(+), 863 deletions(-) diff --git a/changelog/unreleased/SOLR-18450-v2-collections-detailed.yml b/changelog/unreleased/SOLR-18450-v2-collections-detailed.yml deleted file mode 100644 index 3406f380b72..00000000000 --- a/changelog/unreleased/SOLR-18450-v2-collections-detailed.yml +++ /dev/null @@ -1,14 +0,0 @@ -title: - V2 GET /api/collections gains a `detailed=true` param returning the collections, shards, and - replicas tree (in `collectionsDetail`), without live nodes, aliases, or cluster properties. This - tree previously lived at GET /api/cluster in an earlier draft of this change; it was relocated to - /api/collections since it's collection data, not cluster-wide data -- `/api/cluster/nodes`, - `/api/aliases`, and `/api/cluster/properties` already cover the cluster-wide pieces. -type: changed -authors: - - name: Prithvi S - nick: iprithv - - name: Eric Pugh -links: - - name: SOLR-18450 - url: https://issues.apache.org/jira/browse/SOLR-18450 diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/ListCollectionsApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/ListCollectionsApi.java index 468b79d52bf..2f20f4b4366 100644 --- a/solr/api/src/java/org/apache/solr/client/api/endpoint/ListCollectionsApi.java +++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/ListCollectionsApi.java @@ -17,10 +17,8 @@ package org.apache.solr.client.api.endpoint; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; -import jakarta.ws.rs.QueryParam; import org.apache.solr.client.api.model.ListCollectionsResponse; @Path("/collections") @@ -29,37 +27,5 @@ public interface ListCollectionsApi { @Operation( summary = "List all collections in this Solr cluster", tags = {"collections"}) - ListCollectionsResponse listCollections( - @Parameter( - description = - "When true, return the collections, shards, and replicas tree (in" - + " 'collectionsDetail') instead of the plain collection name list.") - @QueryParam("detailed") - Boolean detailed, - @Parameter( - description = - "Only used when 'detailed' is true. Collection or alias to return. Omit to" - + " return every collection. An alias returns the collections it points" - + " at.") - @QueryParam("collection") - String collection, - @Parameter( - description = - "Only used when 'detailed' is true. Shard or comma-separated shards to return." - + " Applied to each selected collection.") - @QueryParam("shard") - String shard, - @Parameter( - description = - "Only used when 'detailed' is true. Route key of a document. Limits the tree to" - + " the shard that would hold that document.") - @QueryParam("_route_") - String routeKey, - @Parameter( - description = - "Only used when 'detailed' is true. Include per-replica state when the" - + " collection uses it.") - @QueryParam("prs") - Boolean prs) - throws Exception; + ListCollectionsResponse listCollections(); } diff --git a/solr/api/src/java/org/apache/solr/client/api/model/ListCollectionsResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/ListCollectionsResponse.java index 996b6e70729..17bf44c44ec 100644 --- a/solr/api/src/java/org/apache/solr/client/api/model/ListCollectionsResponse.java +++ b/solr/api/src/java/org/apache/solr/client/api/model/ListCollectionsResponse.java @@ -16,110 +16,10 @@ */ package org.apache.solr.client.api.model; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.v3.oas.annotations.media.Schema; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; public class ListCollectionsResponse extends SolrJerseyResponse { @JsonProperty("collections") public List<String> collections; - - @Schema(description = "Collections, shards, and replicas, present only when detailed=true.") - @JsonProperty("collectionsDetail") - public Map<String, CollectionState> collectionsDetail; - - /** State of one collection, including its shards and replicas. */ - public static class CollectionState { - @JsonProperty public Map<String, ShardState> shards; - - @Schema(description = "Worst shard health in this collection: GREEN, YELLOW, ORANGE, or RED.") - @JsonProperty - public String health; - - @JsonProperty public String configName; - @JsonProperty public Integer znodeVersion; - @JsonProperty public Long creationTimeMillis; - - @Schema(description = "Aliases that point at this collection.") - @JsonProperty - public List<String> aliases; - - @Schema(description = "Document router for this collection, e.g. name=compositeId.") - @JsonProperty - public Map<String, String> router; - - @JsonProperty public Integer replicationFactor; - - private final Map<String, Object> additionalProperties = new LinkedHashMap<>(); - - @JsonAnyGetter - public Map<String, Object> unknownProperties() { - return additionalProperties; - } - - @JsonAnySetter - public void setUnknownProperty(String field, Object value) { - additionalProperties.put(field, value); - } - } - - /** State of one shard. */ - public static class ShardState { - @JsonProperty public String state; - @JsonProperty public String range; - - @Schema(description = "Shard health: GREEN, YELLOW, ORANGE, or RED.") - @JsonProperty - public String health; - - @JsonProperty public Map<String, ReplicaState> replicas; - - private final Map<String, Object> additionalProperties = new LinkedHashMap<>(); - - @JsonAnyGetter - public Map<String, Object> unknownProperties() { - return additionalProperties; - } - - @JsonAnySetter - public void setUnknownProperty(String field, Object value) { - additionalProperties.put(field, value); - } - } - - /** State of one replica. */ - public static class ReplicaState { - @JsonProperty public String state; - @JsonProperty public String core; - - @JsonProperty("node_name") - public String nodeName; - - @JsonProperty("base_url") - public String baseUrl; - - /** - * {@code "true"} when this replica is the leader. Absent otherwise. state.json stores this as a - * string. - */ - @JsonProperty public String leader; - - @JsonProperty public String type; - - private final Map<String, Object> additionalProperties = new LinkedHashMap<>(); - - @JsonAnyGetter - public Map<String, Object> unknownProperties() { - return additionalProperties; - } - - @JsonAnySetter - public void setUnknownProperty(String field, Object value) { - additionalProperties.put(field, value); - } - } } 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 c73f10f2ec6..41dd007d955 100644 --- a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java +++ b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java @@ -41,6 +41,8 @@ import org.apache.solr.common.SolrException; import org.apache.solr.common.annotation.JsonProperty; import org.apache.solr.common.cloud.ClusterProperties; import org.apache.solr.common.cloud.ZkStateReader; +import org.apache.solr.common.params.CollectionParams.CollectionAction; +import org.apache.solr.common.params.CommonParams; import org.apache.solr.common.params.DefaultSolrParams; import org.apache.solr.common.params.ModifiableSolrParams; import org.apache.solr.common.util.ReflectMapWriter; @@ -213,6 +215,13 @@ public class ClusterAPI { return req; } + @EndPoint(method = GET, path = "/cluster", permission = COLL_READ_PERM) + public void getClusterStatus(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception { + final Map<String, Object> v1Params = + Map.of(CommonParams.ACTION, CollectionAction.CLUSTERSTATUS.toLower()); + collectionsHandler.handleRequestBody(wrapParams(req, v1Params), rsp); + } + private CoreContainer getCoreContainer() { return collectionsHandler.getCoreContainer(); } diff --git a/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java b/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java index aaa1d6f5383..c0852ad94de 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java @@ -20,12 +20,10 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.BiConsumer; import java.util.stream.Stream; import org.apache.solr.client.api.util.SolrVersion; import org.apache.solr.common.MapWriter; @@ -122,10 +120,9 @@ public class ClusterStatus { boolean withAliases = solrParams.getBool(ALIASES_PROP, includeAll); List<String> liveNodes = null; - // Replica state is cross-checked against live nodes whenever the collection tree is built, - // including when the caller asked for that tree without the live-node list itself. - if (withLiveNodes || withCollection) { + if (withLiveNodes || collection != null) { liveNodes = zkStateReader.getZkClient().getChildren(ZkStateReader.LIVE_NODES_ZKNODE, null); + // add live_nodes if (withLiveNodes) clusterStatus.add("live_nodes", liveNodes); } @@ -136,7 +133,7 @@ public class ClusterStatus { if (withCollection) { assert liveNodes != null; - addCollectionStatuses(clusterStatus, liveNodes, aliases, solrVersion); + fetchClusterStatusForCollOrAlias(clusterStatus, liveNodes, aliases, solrVersion); } if (withAliases) { @@ -166,49 +163,13 @@ public class ClusterStatus { results.add("cluster", clusterStatus); } - /** - * Collections, shards, and replicas selected by this request's {@code collection}, {@code shard}, - * and {@code _route_} parameters. - * - * <p>Live nodes are read so replica state can be cross-checked, and aliases are read so a - * collection alias in {@code collection} can be resolved. Each collection still lists the aliases - * that point at it. The live-node list, the alias map, and cluster properties are not returned. - */ - public Map<String, Object> getCollectionStatuses() throws KeeperException, InterruptedException { - List<String> liveNodes = - zkStateReader.getZkClient().getChildren(ZkStateReader.LIVE_NODES_ZKNODE, null); - Aliases aliases = zkStateReader.getAliases(); - Map<String, Object> collections = new LinkedHashMap<>(); - forEachCollectionStatus( - liveNodes, aliases, (name, status) -> collections.put(name, copyForJackson(status))); - return collections; - } - - private void addCollectionStatuses( + private void fetchClusterStatusForCollOrAlias( NamedList<Object> clusterStatus, List<String> liveNodes, Aliases aliases, SolrVersion solrVersion) { - // Resolve the collection list now so a missing name fails the request, rather than during - // response writing. The per-collection JSON is still built while the response is written. - PreparedCollections prepared = prepareCollections(aliases); - if (solrVersion == null || solrVersion.greaterThanOrEqualTo(SolrVersion.valueOf("9.9.0"))) { - MapWriter collectionPropsWriter = - ew -> prepared.forEach(liveNodes, (name, status) -> ew.putNoEx(name, status)); - clusterStatus.add("collections", collectionPropsWriter); - } else { - NamedList<Object> collectionProps = new SimpleOrderedMap<>(); - prepared.forEach(liveNodes, collectionProps::add); - clusterStatus.add("collections", collectionProps); - } - } - - private void forEachCollectionStatus( - List<String> liveNodes, Aliases aliases, BiConsumer<String, Map<String, Object>> consumer) { - prepareCollections(aliases).forEach(liveNodes, consumer); - } - private PreparedCollections prepareCollections(Aliases aliases) { + // read aliases Map<String, List<String>> collectionVsAliases = new HashMap<>(); Map<String, List<String>> aliasVsCollections = aliases.getCollectionAliasListMap(); for (Map.Entry<String, List<String>> entry : aliasVsCollections.entrySet()) { @@ -223,6 +184,12 @@ public class ClusterStatus { } ClusterState clusterState = zkStateReader.getClusterState(); + + String routeKey = solrParams.get(ShardParams._ROUTE_); + String shard = solrParams.get(ZkStateReader.SHARD_ID_PROP); + + Set<String> requestedShards = (shard != null) ? Set.of(shard.split(",")) : null; + Stream<DocCollection> collectionStream; if (collection == null) { collectionStream = clusterState.collectionStream(); @@ -248,53 +215,33 @@ public class ClusterStatus { } } - String routeKey = solrParams.get(ShardParams._ROUTE_); - String shard = solrParams.get(ZkStateReader.SHARD_ID_PROP); - Set<String> requestedShards = (shard != null) ? Set.of(shard.split(",")) : null; - return new PreparedCollections( - collectionStream.toList(), collectionVsAliases, routeKey, requestedShards); - } - - private final class PreparedCollections { - private final List<DocCollection> collections; - private final Map<String, List<String>> collectionVsAliases; - private final String routeKey; - private final Set<String> requestedShards; - - private PreparedCollections( - List<DocCollection> collections, - Map<String, List<String>> collectionVsAliases, - String routeKey, - Set<String> requestedShards) { - this.collections = collections; - this.collectionVsAliases = collectionVsAliases; - this.routeKey = routeKey; - this.requestedShards = requestedShards; - } - - private void forEach(List<String> liveNodes, BiConsumer<String, Map<String, Object>> consumer) { - for (DocCollection collectionState : collections) { - consumer.accept( - collectionState.getName(), - buildResponseForCollection( - collectionState, collectionVsAliases, routeKey, liveNodes, requestedShards)); - } - } - } - - /** - * Jersey serializes the v2 response with Jackson, which does not write Solr {@link MapWriter}s. - * Per-replica state is the one value still attached as a writer; turn it into plain JSON objects. - * Other fields are already maps and scalars. - */ - private static Map<String, Object> copyForJackson(Map<String, Object> status) { - Object prs = status.get("PRS"); - if (!(prs instanceof MapWriter)) { - return status; + if (solrVersion == null || solrVersion.greaterThanOrEqualTo(SolrVersion.valueOf("9.9.0"))) { + MapWriter collectionPropsWriter = + ew -> { + collectionStream.forEach( + (collectionState) -> { + ew.putNoEx( + collectionState.getName(), + buildResponseForCollection( + collectionState, + collectionVsAliases, + routeKey, + liveNodes, + requestedShards)); + }); + }; + clusterStatus.add("collections", collectionPropsWriter); + } else { + NamedList<Object> collectionProps = new SimpleOrderedMap<>(); + collectionStream.forEach( + collectionState -> { + collectionProps.add( + collectionState.getName(), + buildResponseForCollection( + collectionState, collectionVsAliases, routeKey, liveNodes, requestedShards)); + }); + clusterStatus.add("collections", collectionProps); } - Map<String, Object> copy = new LinkedHashMap<>(status); - copy.put("PRS", Utils.fromJSON(Utils.toJSON(prs))); - return copy; } private void addAliasMap(Aliases aliases, NamedList<Object> clusterStatus) { 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 12055863ea6..6fac52efbf5 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 @@ -832,8 +832,7 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission LIST, (req, rsp, h) -> { final ListCollections listCollectionsAPI = new ListCollections(h.coreContainer, req, rsp); - final SolrJerseyResponse listCollectionsResponse = - listCollectionsAPI.listCollections(null, null, null, null, null); + final SolrJerseyResponse listCollectionsResponse = listCollectionsAPI.listCollections(); V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, listCollectionsResponse); return null; }), diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ListCollections.java b/solr/core/src/java/org/apache/solr/handler/admin/api/ListCollections.java index 831e5929b25..81c5fac32c5 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/api/ListCollections.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ListCollections.java @@ -21,18 +21,11 @@ import static org.apache.solr.security.PermissionNameProvider.Name.COLL_READ_PER import jakarta.inject.Inject; import java.util.List; -import java.util.Map; import org.apache.solr.client.api.endpoint.ListCollectionsApi; import org.apache.solr.client.api.model.ListCollectionsResponse; -import org.apache.solr.client.api.model.ListCollectionsResponse.CollectionState; import org.apache.solr.common.cloud.DocCollection; -import org.apache.solr.common.params.ModifiableSolrParams; -import org.apache.solr.common.params.ShardParams; -import org.apache.solr.common.util.CollectionUtil; import org.apache.solr.core.CoreContainer; -import org.apache.solr.handler.admin.ClusterStatus; import org.apache.solr.jersey.PermissionName; -import org.apache.solr.jersey.SolrJacksonMapper; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.SolrQueryResponse; @@ -40,12 +33,6 @@ import org.apache.solr.response.SolrQueryResponse; * V2 API for listing collections. * * <p>This API (GET /v2/collections) is equivalent to the v1 /admin/collections?action=LIST command - * - * <p>With {@code detailed=true}, it instead returns the collections, shards, and replicas tree (in - * {@code collectionsDetail}) -- the same tree v1 {@code CLUSTERSTATUS} returns under {@code - * cluster.collections}, without the live-node list, alias map, or cluster properties that accompany - * it there. Live nodes, aliases, and cluster properties have their own v2 endpoints: {@code GET - * /api/cluster/nodes}, {@code GET /api/aliases}, and {@code GET /api/cluster/properties}. */ public class ListCollections extends AdminAPIBase implements ListCollectionsApi { @@ -56,31 +43,11 @@ public class ListCollections extends AdminAPIBase implements ListCollectionsApi @Override @PermissionName(COLL_READ_PERM) - public ListCollectionsResponse listCollections( - Boolean detailed, String collection, String shard, String routeKey, Boolean prs) - throws Exception { + public ListCollectionsResponse listCollections() { final ListCollectionsResponse response = instantiateJerseyResponse(ListCollectionsResponse.class); validateZooKeeperAwareCoreContainer(coreContainer); - if (Boolean.TRUE.equals(detailed)) { - if (collection != null) { - recordCollectionForLogAndTracing(collection, solrQueryRequest); - } - // Bind only the documented query params; anything else on the request is ignored. - final ModifiableSolrParams params = new ModifiableSolrParams(); - params.setNonNull("collection", collection); - params.setNonNull("shard", shard); - params.setNonNull(ShardParams._ROUTE_, routeKey); - if (prs != null) { - params.set("prs", prs); - } - final ClusterStatus clusterStatus = - new ClusterStatus(coreContainer.getZkController().getZkStateReader(), params); - response.collectionsDetail = typedCollections(clusterStatus.getCollectionStatuses()); - return response; - } - // resolve each name to ensure it exists. // TODO https://issues.apache.org/jira/browse/SOLR-16909 to go direct to ZK? List<String> collectionList = @@ -96,17 +63,4 @@ public class ListCollections extends AdminAPIBase implements ListCollectionsApi return response; } - - /** - * Bind the state maps onto the response types. Fields the model knows about are typed; every - * other entry stays on the object through its catch-all. - */ - private static Map<String, CollectionState> typedCollections(Map<String, Object> raw) { - var mapper = SolrJacksonMapper.getObjectMapper(); - Map<String, CollectionState> collections = CollectionUtil.newLinkedHashMap(raw.size()); - for (Map.Entry<String, Object> entry : raw.entrySet()) { - collections.put(entry.getKey(), mapper.convertValue(entry.getValue(), CollectionState.class)); - } - return collections; - } } diff --git a/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java index 3974894386d..4fc458bc589 100644 --- a/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java +++ b/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java @@ -29,6 +29,7 @@ import org.apache.solr.SolrTestCaseJ4; import org.apache.solr.api.Api; import org.apache.solr.api.ApiBag; import org.apache.solr.common.params.CollectionParams; +import org.apache.solr.common.params.CollectionParams.CollectionAction; import org.apache.solr.common.params.ModifiableSolrParams; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.CommandOperation; @@ -74,6 +75,13 @@ public class V2ClusterAPIMappingTest extends SolrTestCaseJ4 { assertEquals(CollectionParams.CollectionAction.OVERSEERSTATUS.lowerName, v1Params.get(ACTION)); } + @Test + public void testClusterStatusAllParams() throws Exception { + final SolrParams v1Params = captureConvertedV1Params("/cluster", "GET", null); + + assertEquals(CollectionAction.CLUSTERSTATUS.lowerName, v1Params.get(ACTION)); + } + @Test public void testAddRoleAllParams() throws Exception { final SolrParams v1Params = diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedStandaloneTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedStandaloneTest.java deleted file mode 100644 index 48a08b7a9e3..00000000000 --- a/solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedStandaloneTest.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.CollectionsApi; -import org.apache.solr.util.SolrJettyTestRule; -import org.junit.BeforeClass; -import org.junit.ClassRule; -import org.junit.Test; - -/** Standalone coverage for {@code GET /api/collections?detailed=true}. */ -public class ListCollectionsDetailedStandaloneTest extends SolrTestCase { - - @ClassRule public static final SolrJettyTestRule solrTestRule = new SolrJettyTestRule(); - - @BeforeClass - public static void setupSolr() throws Exception { - solrTestRule.startSolr(createTempDir()); - } - - @Test - public void testRequiresSolrCloud() { - var req = new CollectionsApi.ListCollections(); - req.setDetailed(true); - final RemoteSolrException ex = - expectThrows(RemoteSolrException.class, () -> req.process(solrTestRule.getAdminClient())); - assertEquals(400, ex.code()); - } -} diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedTest.java deleted file mode 100644 index 93808b8a27a..00000000000 --- a/solr/core/src/test/org/apache/solr/handler/admin/api/ListCollectionsDetailedTest.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * 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.SolrRequest.METHOD.GET; - -import java.util.List; -import java.util.Map; -import java.util.Set; -import org.apache.solr.client.api.model.ListCollectionsResponse; -import org.apache.solr.client.api.model.ListCollectionsResponse.CollectionState; -import org.apache.solr.client.api.model.ListCollectionsResponse.ReplicaState; -import org.apache.solr.client.solrj.SolrRequest.SolrRequestType; -import org.apache.solr.client.solrj.request.CollectionAdminRequest; -import org.apache.solr.client.solrj.request.CollectionsApi; -import org.apache.solr.client.solrj.request.GenericSolrRequest; -import org.apache.solr.cloud.SolrCloudTestCase; -import org.apache.solr.common.params.ModifiableSolrParams; -import org.apache.solr.common.util.NamedList; -import org.apache.solr.common.util.Utils; -import org.eclipse.jetty.client.ContentResponse; -import org.eclipse.jetty.client.HttpClient; -import org.junit.BeforeClass; -import org.junit.Test; - -/** - * HTTP tests for {@code GET /api/collections?detailed=true}. - * - * <p>The response is the collections, shards, and replicas tree. Live nodes, the alias map, and - * cluster properties stay on their own endpoints (not this one). v1 {@code CLUSTERSTATUS} still - * returns them. - */ -public class ListCollectionsDetailedTest extends SolrCloudTestCase { - - private static final String COLLECTION = "clusterstatuscoll"; - private static final String ALIAS = "clusterstatusalias"; - - @BeforeClass - public static void setupCluster() throws Exception { - configureCluster(1).addConfig("conf", configset("cloud-minimal")).configure(); - CollectionAdminRequest.createCollection(COLLECTION, "conf", 1, 1) - .process(cluster.getSolrClient()); - CollectionAdminRequest.createAlias(ALIAS, COLLECTION).process(cluster.getSolrClient()); - } - - @Test - public void testReturnsCollectionTreeWithoutClusterLevelExtras() throws Exception { - Map<String, Object> body = getCollections("?detailed=true"); - assertNull(body.get("cluster")); - assertNull(body.get("live_nodes")); - assertNull(body.get("aliases")); - assertNull(body.get("properties")); - - Map<String, Object> collection = collection(body, COLLECTION); - assertNotNull(collection.get("health")); - assertEquals("conf", collection.get("configName")); - assertNotNull(collection.get("router")); - assertEquals(Set.of("shard1"), shards(collection).keySet()); - assertNotNull(replica(collection).get("node_name")); - assertNotNull(replica(collection).get("state")); - assertTrue(aliasesOf(collection).contains(ALIAS)); - } - - @Test - @SuppressWarnings("unchecked") - public void testGeneratedClientReadsTheTree() throws Exception { - var req = new CollectionsApi.ListCollections(); - req.setDetailed(true); - ListCollectionsResponse response = req.process(cluster.getSolrClient()); - assertNull(response.error); - assertNull(response.collections); - CollectionState collection = response.collectionsDetail.get(COLLECTION); - assertNotNull(collection); - assertEquals("conf", collection.configName); - assertNotNull(collection.health); - assertTrue(collection.aliases.contains(ALIAS)); - assertNotNull(collection.router); - assertEquals("compositeId", collection.router.get("name")); - assertEquals(Integer.valueOf(1), collection.replicationFactor); - - ReplicaState replica = collection.shards.get("shard1").replicas.values().iterator().next(); - assertNotNull(replica.nodeName); - assertNotNull(replica.state); - assertEquals("true", replica.leader); - } - - @Test - public void testWithoutDetailedReturnsPlainNameList() throws Exception { - var req = new CollectionsApi.ListCollections(); - ListCollectionsResponse response = req.process(cluster.getSolrClient()); - assertNull(response.error); - assertNull(response.collectionsDetail); - assertTrue(response.collections.contains(COLLECTION)); - } - - @Test - @SuppressWarnings("unchecked") - public void testPerReplicaState() throws Exception { - final String prsCollection = "prsclusterstatus"; - CollectionAdminRequest.createCollection(prsCollection, "conf", 1, 1) - .setPerReplicaState(Boolean.TRUE) - .process(cluster.getSolrClient()); - - Map<String, Object> prs = - (Map<String, Object>) - collection( - getCollections("?detailed=true&collection=" + prsCollection + "&prs=true"), - prsCollection) - .get("PRS"); - assertNotNull(prs); - assertNotNull(prs.get("states")); - - var request = new CollectionsApi.ListCollections(); - request.setDetailed(true); - request.setCollection(prsCollection); - request.setPrs(true); - CollectionState typed = - request.process(cluster.getSolrClient()).collectionsDetail.get(prsCollection); - assertNotNull(typed.unknownProperties().get("PRS")); - } - - @Test - public void testCollectionShardAndAliasFilters() throws Exception { - Map<String, Object> byCollection = - collections(getCollections("?detailed=true&collection=" + COLLECTION)); - assertEquals(Set.of(COLLECTION), byCollection.keySet()); - - Map<String, Object> oneShard = - shards( - collection( - getCollections("?detailed=true&collection=" + COLLECTION + "&shard=shard1"), - COLLECTION)); - assertEquals(Set.of("shard1"), oneShard.keySet()); - - ContentResponse missingShard = - httpGet("?detailed=true&collection=" + COLLECTION + "&shard=nosuchshard"); - assertEquals(400, missingShard.getStatus()); - - assertNotNull( - collections(getCollections("?detailed=true&collection=" + ALIAS)).get(COLLECTION)); - } - - @Test - public void testUnknownCollectionIsRejected() throws Exception { - ContentResponse response = httpGet("?detailed=true&collection=not-a-collection"); - assertEquals(400, response.getStatus()); - assertTrue(response.getContentAsString().contains("not found")); - } - - @Test - @SuppressWarnings("unchecked") - public void testV1ClusterStatusStillReturnsLiveNodes() throws Exception { - NamedList<Object> response = - CollectionAdminRequest.getClusterStatus().process(cluster.getSolrClient()).getResponse(); - Map<String, Object> clusterState = (Map<String, Object>) response.get("cluster"); - assertNotNull(clusterState.get("live_nodes")); - assertNotNull(clusterState.get("collections")); - - ModifiableSolrParams params = new ModifiableSolrParams(); - params.set("action", "CLUSTERSTATUS"); - params.set("liveNodes", false); - params.set("aliases", false); - params.set("clusterProperties", false); - NamedList<Object> collectionsOnly = - cluster - .getSolrClient() - .request( - new GenericSolrRequest(GET, "/admin/collections", SolrRequestType.ADMIN, params)); - Map<String, Object> filtered = (Map<String, Object>) collectionsOnly.get("cluster"); - assertNull(filtered.get("live_nodes")); - assertNull(filtered.get("aliases")); - assertNull(filtered.get("properties")); - assertNotNull(((Map<String, Object>) filtered.get("collections")).get(COLLECTION)); - } - - private static Map<String, Object> getCollections(String query) throws Exception { - ContentResponse response = httpGet(query); - assertEquals(response.getContentAsString(), 200, response.getStatus()); - return parsed(response); - } - - private static ContentResponse httpGet(String query) throws Exception { - HttpClient httpClient = cluster.getJettySolrRunner(0).getSolrClient().getHttpClient(); - String url = cluster.getJettySolrRunner(0).getBaseURLV2().toString() + "/collections" + query; - return httpClient.GET(url); - } - - @SuppressWarnings("unchecked") - private static Map<String, Object> parsed(ContentResponse response) { - return (Map<String, Object>) Utils.fromJSONString(response.getContentAsString()); - } - - @SuppressWarnings("unchecked") - private static Map<String, Object> collections(Map<String, Object> body) { - return (Map<String, Object>) body.get("collectionsDetail"); - } - - @SuppressWarnings("unchecked") - private static Map<String, Object> collection(Map<String, Object> body, String name) { - Map<String, Object> collection = (Map<String, Object>) collections(body).get(name); - assertNotNull(collection); - return collection; - } - - @SuppressWarnings("unchecked") - private static Map<String, Object> shards(Map<String, Object> collection) { - return (Map<String, Object>) collection.get("shards"); - } - - @SuppressWarnings("unchecked") - private static List<String> aliasesOf(Map<String, Object> collection) { - return (List<String>) collection.get("aliases"); - } - - @SuppressWarnings("unchecked") - private static Map<String, Object> replica(Map<String, Object> collection) { - Map<String, Object> shard = (Map<String, Object>) shards(collection).get("shard1"); - Map<String, Object> replicas = (Map<String, Object>) shard.get("replicas"); - assertFalse(replicas.isEmpty()); - return (Map<String, Object>) replicas.values().iterator().next(); - } -} 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 6d0e6c0e60d..26390f38c30 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 @@ -38,7 +38,7 @@ Following are some v2 API URL paths and path prefixes, along with some of the op [width="100%",options="header",] |=== |Path prefix |Some Supported Operations -|`/api/collections` |Create, alias, backup, and restore a collection. Fetch the collections, shards, and replicas tree (`?detailed=true`). +|`/api/collections` |Create, alias, backup, and restore a collection. |`/api/c/\{collectionName}/update` |Update requests. |`/api/c/\{collectionName}/config` |Configuration requests. |`/api/c/\{collectionName}/schema` |Schema requests. @@ -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/\{coreName}` |Reload, rename, delete, and unload a core. |`/api/node` |Perform overseer operation, rejoin leader election. -|`/api/cluster` |List live nodes, manage node roles, set cluster property. `POST` sets the cluster rate limiter. +|`/api/cluster` |List live nodes, add role, remove role, set cluster property. |=== == Introspect @@ -149,13 +149,13 @@ curl http://localhost:8983/api/c/gettingstarted -H 'Content-type:application/jso {"responseHeader":{"status":0,"QTime":842}} ---- -See the collections, shards, and replicas in the cluster: +See the state of the cluster: [source,bash] ---- -curl http://localhost:8983/api/collections?detailed=true +curl http://localhost:8983/api/cluster -{"responseHeader":{"status":0,"QTime":1},"collectionsDetail":{"gettingstarted":{"health":"GREEN","shards":{"shard1":{"state":"active"}}}}} +{"responseHeader":{"status":0,"QTime":0},"collections":["gettingstarted"]} ---- Set a cluster property: 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 8028a4d7d5e..22ee652954a 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 @@ -27,10 +27,6 @@ These API commands work with a SolrCloud cluster at the entire cluster level, or Fetch the cluster status including collections, shards, replicas, configuration name as well as collection aliases and cluster properties. -NOTE: This is a v1-only command. -The v2 API splits this information up: see xref:collection-management.adoc#collectionsdetail[Detailed Collections Tree] for the collections, shards, and replicas tree (`detailed=true`), and `GET /api/cluster/nodes`, `GET /api/aliases`, and `GET /api/cluster/properties` for live nodes, the alias map, and cluster properties, respectively. -The two collection-tree responses overlap in content -- same `health` states, same shard/replica fields -- because they cover the same underlying data; they're documented separately because the parameters and response shape otherwise differ between v1 and v2 here. - Additionally, this command reports a `health` status of each collection and shard, in order to make it easier to monitor the operational state of the collections. The @@ -48,10 +44,28 @@ No active replicas *OR* there's no shard leader. The collection health state is reported as the worst state of any shard, e.g., for a collection with all shards GREEN except for one YELLOW the collection health will be reported as YELLOW. +[tabs#clusterstatus-request] +====== +V1 API:: ++ +==== [source,bash] ---- http://localhost:8983/solr/admin/collections?action=CLUSTERSTATUS + ---- +==== + +V2 API:: ++ +==== +[source,bash] +---- +curl http://localhost:8983/api/cluster + +---- +==== +====== === CLUSTERSTATUS Parameters @@ -92,7 +106,6 @@ This can be used if you need the details of the shard where a particular documen |Optional |Default: will default to the default value of `includeAll` parameter specified below |=== + -If set to true, returns the cluster alias map. `liveNodes`:: + @@ -101,7 +114,7 @@ If set to true, returns the cluster alias map. |Optional |Default: will default to the default value of `includeAll` parameter specified below |=== + -If set to true, returns the live nodes in the cluster. +If set to true, returns the status of live nodes in the cluster. `clusterProperties`:: + diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc index 8dbc01b365a..8023f72bcb1 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc @@ -510,206 +510,6 @@ curl http://localhost:8983/api/collections "example2"]} ---- -[[collectionsdetail]] -=== Detailed Collections Tree (V2 Only) - -With `detailed=true`, the V2 API returns the collections, shards, and replicas tree, in `collectionsDetail`, including each collection's configuration name and health, instead of the plain collection name list `LIST` returns by default. - -NOTE: This is a v2-only variant of `LIST`. -See xref:cluster-node-management.adoc#clusterstatus[CLUSTERSTATUS] for the v1 command that returns similar information (plus live nodes, the alias map, and cluster properties). -The two overlap in content -- same `health` states, same shard/replica fields -- because they cover the same underlying data; they're documented separately because the parameters and response shape otherwise differ between v1 and v2 here. - -This command reports a `health` status of each collection and shard, in order to make -it easier to monitor the operational state of the collections. -The following health -state values are defined, ordered from the best to worst, based on the percentage of -active replicas (`active`): - -`GREEN`:: -`active == 100%`, all replicas are active and there's a shard leader. -`YELLOW`:: -`100% > active > 50%`, AND there's a shard leader. -`ORANGE`:: -`50% >= active > 0%`, AND there's a shard leader. -`RED`:: -No active replicas *OR* there's no shard leader. - -The collection health state is reported as the worst state of any shard, e.g., for a collection with all shards GREEN except for one YELLOW the collection health will be reported as YELLOW. - -[source,bash] ----- -curl -X GET "http://localhost:8983/api/collections?detailed=true" ----- - -==== Detailed Collections Tree Parameters - -`detailed`:: -+ -[%autowidth,frame=none] -|=== -|Optional |Default: `false` -|=== -+ -Return the collections, shards, and replicas tree in `collectionsDetail` instead of the plain collection name list in `collections`. - -`collection`:: -+ -[%autowidth,frame=none] -|=== -|Optional |Default: none -|=== -+ -Only used when `detailed=true`. -The collection or alias name for which information is requested. -If omitted, information on all collections in the cluster will be returned. -If an alias is supplied, information on the collections in the alias will be returned. - -`shard`:: -+ -[%autowidth,frame=none] -|=== -|Optional |Default: none -|=== -+ -Only used when `detailed=true`. -The shard(s) for which information is requested. -Multiple shard names can be specified as a comma-separated list. - -`\_route_`:: -+ -[%autowidth,frame=none] -|=== -|Optional |Default: none -|=== -+ -Only used when `detailed=true`. -This can be used if you need the details of the shard where a particular document belongs to and you don't know which shard it falls under. - -`prs`:: -+ -[%autowidth,frame=none] -|=== -|Optional |Default: `false` -|=== -+ -Only used when `detailed=true`. -Include per-replica state when the collection uses it. - -==== Detailed Collections Tree Response - -The response will include the status of the request and `collectionsDetail`, a map of collection name to collection state. - -==== Example - -*Input* - -[source,text] ----- -http://localhost:8983/api/collections?detailed=true ----- - -*Output* - -[source,json] ----- -{ - "responseHeader":{ - "status":0, - "QTime":333}, - "collectionsDetail":{ - "collection1":{ - "shards":{ - "shard1":{ - "range":"80000000-ffffffff", - "state":"active", - "health": "GREEN", - "replicas":{ - "core_node1":{ - "state":"active", - "core":"collection1", - "node_name":"127.0.1.1:8983_solr", - "base_url":"http://127.0.1.1:8983/solr", - "leader":"true"}, - "core_node3":{ - "state":"active", - "core":"collection1", - "node_name":"127.0.1.1:8900_solr", - "base_url":"http://127.0.1.1:8900/solr"}}}, - "shard2":{ - "range":"0-7fffffff", - "state":"active", - "health": "GREEN", - "replicas":{ - "core_node2":{ - "state":"active", - "core":"collection1", - "node_name":"127.0.1.1:7574_solr", - "base_url":"http://127.0.1.1:7574/solr", - "leader":"true"}, - "core_node4":{ - "state":"active", - "core":"collection1", - "node_name":"127.0.1.1:7500_solr", - "base_url":"http://127.0.1.1:7500/solr"}}}}, - "router":{"name":"compositeId"}, - "replicationFactor":1, - "znodeVersion": 11, - "configName" : "my_config", - "health": "GREEN", - "aliases":["both_collections"] - }, - "collection2":{"key": "value"} - } -} ----- - -Adding `collection` and `shard` narrows the tree to just that shard of that collection: - -*Input* - -[source,text] ----- -http://localhost:8983/api/collections?detailed=true&collection=collection1&shard=shard1 ----- - -*Output* - -[source,json] ----- -{ - "responseHeader":{ - "status":0, - "QTime":12}, - "collectionsDetail":{ - "collection1":{ - "shards":{ - "shard1":{ - "range":"80000000-ffffffff", - "state":"active", - "health": "GREEN", - "replicas":{ - "core_node1":{ - "state":"active", - "core":"collection1", - "node_name":"127.0.1.1:8983_solr", - "base_url":"http://127.0.1.1:8983/solr", - "leader":"true"}, - "core_node3":{ - "state":"active", - "core":"collection1", - "node_name":"127.0.1.1:8900_solr", - "base_url":"http://127.0.1.1:8900/solr"}}}}, - "router":{"name":"compositeId"}, - "replicationFactor":1, - "znodeVersion": 11, - "configName" : "my_config", - "health": "GREEN", - "aliases":["both_collections"] - } - } -} ----- - [[rename]] == RENAME: Rename a Collection diff --git a/solr/webapp/web/js/angular/app.js b/solr/webapp/web/js/angular/app.js index aba91049975..93b57bde5fb 100644 --- a/solr/webapp/web/js/angular/app.js +++ b/solr/webapp/web/js/angular/app.js @@ -579,7 +579,7 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $ var currentCollectionName = $route.current.params.core; delete $scope.currentCollection; if ($scope.isCloudEnabled) { - CollectionsV2.listCollections({}, function (error, cdata, response) { + CollectionsV2.listCollections(function (error, cdata, response) { $timeout(function() { if (error) { ApiErrorHandler.handle(response); return; } AliasesV2.getAliases(function (error, adata, response) { diff --git a/solr/webapp/web/js/angular/controllers/alias-overview.js b/solr/webapp/web/js/angular/controllers/alias-overview.js index 212493e8444..6fbca386fe2 100644 --- a/solr/webapp/web/js/angular/controllers/alias-overview.js +++ b/solr/webapp/web/js/angular/controllers/alias-overview.js @@ -16,7 +16,7 @@ */ solrAdminApp.controller('AliasOverviewController', -function($scope, $routeParams, Constants) { +function($scope, $routeParams, Collections, Constants) { $scope.resetMenu("collection-overview", Constants.IS_COLLECTION_PAGE); $scope.refresh = function() { diff --git a/solr/webapp/web/js/angular/controllers/cloud.js b/solr/webapp/web/js/angular/controllers/cloud.js index 87ae9eecf31..2ce9a392d1c 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, $q, Zookeeper, ZookeeperReadV2, Constants, ClusterV2, CollectionsV2, SystemV2, Metrics, MetricsExtractor, ZookeeperStatus, ApiErrorHandler) { + function($scope, $location, $timeout, $q, Zookeeper, ZookeeperReadV2, Constants, Collections, ClusterV2, SystemV2, Metrics, MetricsExtractor, ZookeeperStatus, ApiErrorHandler) { $scope.showDebug = false; @@ -37,7 +37,7 @@ solrAdminApp.controller('CloudController', graphSubController($scope, $timeout, Zookeeper, ClusterV2, ApiErrorHandler); } else if (view === "nodes") { $scope.resetMenu("cloud-nodes", Constants.IS_ROOT_PAGE); - nodesSubController($scope, $timeout, ClusterV2, CollectionsV2, SystemV2, Metrics, MetricsExtractor, ApiErrorHandler); + nodesSubController($scope, $timeout, Collections, ClusterV2, SystemV2, Metrics, MetricsExtractor, ApiErrorHandler); } else if (view === "zkstatus") { $scope.resetMenu("cloud-zkstatus", Constants.IS_ROOT_PAGE); zkStatusSubController($scope, ZookeeperStatus, false); @@ -107,7 +107,7 @@ function isNumeric(n) { return !isNaN(parseFloat(n)) && isFinite(n); } -var nodesSubController = function($scope, $timeout, ClusterV2, CollectionsV2, SystemV2, Metrics, MetricsExtractor, ApiErrorHandler) { +var nodesSubController = function($scope, $timeout, Collections, ClusterV2, SystemV2, Metrics, MetricsExtractor, ApiErrorHandler) { $scope.pageSize = 10; $scope.showNodes = true; $scope.showTree = false; @@ -178,74 +178,70 @@ var nodesSubController = function($scope, $timeout, ClusterV2, CollectionsV2, Sy var live_nodes = []; // We build a node-centric view of the cluster state which we can easily consume to render the table - CollectionsV2.listCollections({detailed: true}, function (error, data, response) { - $timeout(function() { - if (error) { ApiErrorHandler.handle(response); return; } - - // Fetch cluster state from CollectionsApi and invert to a nodes structure - for (var name in data.collectionsDetail) { - var collection = data.collectionsDetail[name]; - collection.name = name; - var shards = collection.shards; - collection.shards = []; - for (var shardName in shards) { - var shard = shards[shardName]; - shard.name = shardName; - shard.collection = collection.name; - var replicas = shard.replicas; - shard.replicas = []; - for (var replicaName in replicas) { - var core = replicas[replicaName]; - core.name = replicaName; - core.replica = core['core'].replace(/.*_(replica_.*)$/, '\$1'); - core.collection = collection.name; - core.shard = shard.name; - core.shard_state = shard.state; - core.label = core['collection'] + "_" - + (core['shard'] + "_").replace(/shard(\d+)_/, 's\$1') - + core['replica'].replace(/replica_?[ntp]?(\d+)/, 'r\$1'); - - var node_name = core['node_name']; - var node = getOrCreateObj(node_name, nodes); - var cores = getOrCreateList("cores", node); - cores.push(core); - node['base_url'] = core.base_url; - node['id'] = core.base_url.replace(/[^\w\d]/g, ''); - node['host'] = node_name.split(":")[0]; - var collections = getOrCreateList("collections", node); - ensureInList(core.collection, collections); - ensureNodeInHosts(node_name, hosts); - } + Collections.status(function (data) { + // Fetch cluster state from collections API and invert to a nodes structure + for (var name in data.cluster.collections) { + var collection = data.cluster.collections[name]; + collection.name = name; + var shards = collection.shards; + collection.shards = []; + for (var shardName in shards) { + var shard = shards[shardName]; + shard.name = shardName; + shard.collection = collection.name; + var replicas = shard.replicas; + shard.replicas = []; + for (var replicaName in replicas) { + var core = replicas[replicaName]; + core.name = replicaName; + core.replica = core['core'].replace(/.*_(replica_.*)$/, '\$1'); + core.collection = collection.name; + core.shard = shard.name; + core.shard_state = shard.state; + core.label = core['collection'] + "_" + + (core['shard'] + "_").replace(/shard(\d+)_/, 's\$1') + + core['replica'].replace(/replica_?[ntp]?(\d+)/, 'r\$1'); + + var node_name = core['node_name']; + var node = getOrCreateObj(node_name, nodes); + var cores = getOrCreateList("cores", node); + cores.push(core); + node['base_url'] = core.base_url; + node['id'] = core.base_url.replace(/[^\w\d]/g, ''); + node['host'] = node_name.split(":")[0]; + var collections = getOrCreateList("collections", node); + ensureInList(core.collection, collections); + ensureNodeInHosts(node_name, hosts); } } + } - ClusterV2.listClusterNodes(function (error, nodesData, response) { - $timeout(function() { - if (error) { ApiErrorHandler.handle(response); return; } - - live_nodes = nodesData.nodes; - for (n in live_nodes) { - node = live_nodes[n]; - if (!(node in nodes)) { - var hostName = node.split(":")[0]; - nodes[node] = {}; - nodes[node]['host'] = hostName; - } - ensureNodeInHosts(node, hosts); + ClusterV2.listClusterNodes(function (error, nodesData, response) { + $timeout(function() { + if (error) { ApiErrorHandler.handle(response); return; } + + live_nodes = nodesData.nodes; + for (n in live_nodes) { + node = live_nodes[n]; + if (!(node in nodes)) { + var hostName = node.split(":")[0]; + nodes[node] = {}; + nodes[node]['host'] = hostName; } + ensureNodeInHosts(node, hosts); + } - // Make sure nodes are sorted alphabetically to align with rowspan in table - for (var host in hosts) { - hosts[host].nodes.sort(); - } + // Make sure nodes are sorted alphabetically to align with rowspan in table + for (var host in hosts) { + hosts[host].nodes.sort(); + } - $scope.nodes = nodes; - $scope.hosts = hosts; - $scope.live_nodes = live_nodes; + $scope.nodes = nodes; + $scope.hosts = hosts; + $scope.live_nodes = live_nodes; - $scope.Math = window.Math; - $scope.reload(); - }); + $scope.Math = window.Math; + $scope.reload(); }); }); }); diff --git a/solr/webapp/web/js/angular/controllers/collection-overview.js b/solr/webapp/web/js/angular/controllers/collection-overview.js index 9292da4f7c6..d1834b2bd1e 100644 --- a/solr/webapp/web/js/angular/controllers/collection-overview.js +++ b/solr/webapp/web/js/angular/controllers/collection-overview.js @@ -16,17 +16,14 @@ */ solrAdminApp.controller('CollectionOverviewController', -function($scope, $routeParams, $timeout, CollectionsV2, Constants, ApiErrorHandler) { +function($scope, $routeParams, Collections, Constants) { $scope.resetMenu("collection-overview", Constants.IS_COLLECTION_PAGE); $scope.refresh = function() { - CollectionsV2.listCollections({detailed: true, collection: $routeParams.core}, function(error, data, response) { - $timeout(function() { - if (error) { ApiErrorHandler.handle(response); return; } - $scope.selectedCollection = data.collectionsDetail[$routeParams.core]; - $scope.selectedCollection.name = $routeParams.core; - $scope.rootUrl = Constants.ROOT_URL; - }); + Collections.status({}, function(data) { + $scope.selectedCollection = data.cluster.collections[$routeParams.core]; + $scope.selectedCollection.name = $routeParams.core; + $scope.rootUrl = Constants.ROOT_URL; }); }; diff --git a/solr/webapp/web/js/angular/controllers/collections.js b/solr/webapp/web/js/angular/controllers/collections.js index 17707f34243..1c93b088b5e 100644 --- a/solr/webapp/web/js/angular/controllers/collections.js +++ b/solr/webapp/web/js/angular/controllers/collections.js @@ -16,7 +16,7 @@ */ solrAdminApp.controller('CollectionsController', - function($scope, $routeParams, $location, $timeout, CollectionsV2, AliasesV2, ShardsV2, ReplicasV2, ConfigSetsV2, ClusterV2, Constants, ApiErrorHandler){ + function($scope, $routeParams, $location, $timeout, Collections, CollectionsV2, AliasesV2, ShardsV2, ReplicasV2, ConfigSetsV2, ClusterV2, Constants, ApiErrorHandler){ $scope.resetMenu("collections", Constants.IS_ROOT_PAGE); $scope.refresh = function() { @@ -30,15 +30,13 @@ solrAdminApp.controller('CollectionsController', }); }); - CollectionsV2.listCollections({detailed: true}, function (error, data, response) { - $timeout(function() { - if (error) { ApiErrorHandler.handle(response); return; } + Collections.status(function (data) { $scope.collections = []; - for (var name in data.collectionsDetail) { + for (var name in data.cluster.collections) { if (name.startsWith("._designer_")) { continue; } - var collection = data.collectionsDetail[name]; + var collection = data.cluster.collections[name]; collection.name = name; collection.type = 'collection'; var shards = collection.shards; @@ -87,7 +85,6 @@ solrAdminApp.controller('CollectionsController', } }); }); - }); }); 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 462d684121e..c0b525b1698 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -166,6 +166,16 @@ solrAdminServices.factory('Metrics', delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; return new solrApi.ConfigApi(); }) +.factory('Collections', + ['$resource', function ($resource) { + // v2 ClusterAPI (/api/cluster) delegates straight through to the same v1 CollectionsHandler + // that v1's CLUSTERSTATUS action used, so the response shape is byte-identical -- no + // generated solrApi client class exists for it (old-style @EndPoint API, predates the + // OpenAPI-based v2 framework), so this stays a plain $resource, like ParamSet. + return $resource('/api/cluster', {'wt':'json', '_':Date.now()}, { + "status": {} + }); + }]) .factory('ConfigSetFiles', ['$http', function ($http) { // Fetches a single file from a configset via V2 /api/configsets/{name}/files/{path}. @@ -266,7 +276,7 @@ solrAdminServices.factory('Metrics', // v2 GetConfigAPI/ModifyParamSetAPI (/api/(cores|collections)/:core/config/params) still // delegate straight through to the same v1 SolrConfigHandler, so the response shape is // byte-identical -- no generated solrApi client class exists for it (old-style @EndPoint API, - // predates the OpenAPI-based v2 framework), so this stays a plain $resource. + // predates the OpenAPI-based v2 framework), so this stays a plain $resource, like Collections. // NB: unlike v1's flexible routing, the v2 API requires knowing up front whether ":core" is a // collection name (SolrCloud) or an actual core name (standalone/user-managed) -- // /api/collections/... 500s in standalone mode (it tries to resolve aliases, which needs ZK), @@ -385,7 +395,7 @@ solrAdminServices.factory('Metrics', // body (the server deliberately reads the raw content stream, dispatched by Content-Type, // rather than a formal parameter) and query() takes no query params at all (the server // forwards arbitrary SolrParams straight through). Both stay on this plain $resource, like - // ParamSet. Every other Schema Designer endpoint uses SchemaDesignerV2. + // Collections/ParamSet. Every other Schema Designer endpoint uses SchemaDesignerV2. return $resource('/api/schema-designer/:configSet/:path', {wt: 'json', path: '@path', configSet: '@configSet', filePath: '@filePath', _:Date.now()}, { get: {method: "GET"}, post: {method: "POST", timeout: 90000},
