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

janhoy 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 bde37d6c633 SOLR-18514: Port HealthcheckTool to picocli (#5041)
bde37d6c633 is described below

commit bde37d6c6335035b933c9600ff7bbd12e5590532
Author: Serhiy Bzhezytskyy <[email protected]>
AuthorDate: Thu Oct 8 18:56:06 2026 +0300

    SOLR-18514: Port HealthcheckTool to picocli (#5041)
    
    Co-authored-by: Jan Høydahl <[email protected]>
---
 .../SOLR-17697-picocli-experimental-cli.yml        |  2 +
 .../java/org/apache/solr/cli/HealthcheckTool.java  | 96 +++++++++++++++++++++-
 .../core/src/java/org/apache/solr/cli/SolrCLI.java |  3 +-
 .../solr/cli/HealthcheckToolPicocliTest.java       | 48 +++++++++++
 .../org/apache/solr/cli/HealthcheckToolTest.java   | 15 ++--
 solr/packaging/build.gradle                        |  1 +
 .../modules/deployment-guide/deployment-nav.adoc   |  1 +
 .../modules/deployment-guide/pages/cli/index.adoc  |  1 +
 .../pages/cli/solr-healthcheck.adoc                | 81 ++++++++++++++++++
 9 files changed, 241 insertions(+), 7 deletions(-)

diff --git a/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml 
b/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml
index 3510707f7e0..84e285fbbb2 100644
--- a/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml
+++ b/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml
@@ -21,3 +21,5 @@ links:
     url: https://issues.apache.org/jira/browse/SOLR-18512
   - name: SOLR-18513
     url: https://issues.apache.org/jira/browse/SOLR-18513
+  - name: SOLR-18514
+    url: https://issues.apache.org/jira/browse/SOLR-18514
diff --git a/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java 
b/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java
index 7311877cc78..005be05cec7 100644
--- a/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java
@@ -20,16 +20,19 @@ package org.apache.solr.cli;
 import static org.apache.solr.common.params.CommonParams.DISTRIB;
 import static org.apache.solr.common.params.CommonParams.NAME;
 
+import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.Option;
 import org.apache.commons.cli.Options;
+import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
 import org.apache.solr.client.solrj.request.SolrQuery;
@@ -40,15 +43,30 @@ import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.Slice;
+import org.apache.solr.common.util.EnvUtils;
 import org.noggit.CharArr;
 import org.noggit.JSONWriter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /** Supports healthcheck command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
[email protected](
+    name = "healthcheck",
+    description =
+        "Verifies that a collection is functioning: queries every replica 
directly, compares"
+            + " document counts and checks that each shard has a leader and 
every replica is"
+            + " ACTIVE. Requires SolrCloud.",
+    footerHeading = "%nExamples:%n",
+    footer = {"  # Check the health of a collection", "  bin/solr healthcheck 
-c gettingstarted"})
 public class HealthcheckTool extends ToolBase {
   private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
+  /**
+   * @deprecated Only used by the commons-cli parser; the picocli path 
declares this as an annotated
+   *     field.
+   */
+  @Deprecated
   private static final Option COLLECTION_NAME_OPTION =
       Option.builder("c")
           .longOpt("name")
@@ -76,6 +94,24 @@ public class HealthcheckTool extends ToolBase {
   /** Parameters for the healthcheck command, independent of the command line 
parser. */
   record HealthcheckParams(String collection, String credentials) {}
 
+  // --- picocli fields ---
+
+  @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+  private ConnectionOptions connectionOptions;
+
+  @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;
+
+  @picocli.CommandLine.Option(
+      names = {"-c", "--name"},
+      required = true,
+      paramLabel = "COLLECTION",
+      description = "Name of the collection to check.")
+  private String nameOpt;
+
+  public HealthcheckTool() {
+    this(new DefaultToolRuntime());
+  }
+
   /** Requests health information about a specific collection in SolrCloud. */
   public HealthcheckTool(ToolRuntime runtime) {
     super(runtime);
@@ -213,7 +249,65 @@ public class HealthcheckTool extends ToolBase {
 
   @Override
   public int callTool() throws Exception {
-    throw new UnsupportedOperationException("This tool does not yet support 
PicoCli");
+    var solrConnection = resolveSolrConnection(credentialsOptions.credentials);
+    if (solrConnection == null) {
+      CLIO.err("Healthcheck tool only works in Solr Cloud mode.");
+      return 1;
+    }
+    HealthcheckParams params = new HealthcheckParams(nameOpt, 
credentialsOptions.credentials);
+    var builder =
+        new 
HttpJettySolrClient.Builder().withOptionalBasicAuthCredentials(params.credentials());
+    try (var cloudSolrClient = CLIUtils.getCloudSolrClient(solrConnection, 
builder)) {
+      echoIfVerbose("Connecting to Solr at " + solrConnection.toString());
+      runCloudTool(cloudSolrClient, params);
+    }
+    return 0;
+  }
+
+  /**
+   * Mirrors {@link CLIUtils#getSolrConnection(CommandLine)}: an explicit 
{@code --solr-connection}
+   * or {@code --zk-host} (or the matching property) wins, otherwise a running 
Solr is asked for its
+   * ZooKeeper, and null means it is not in SolrCloud mode.
+   */
+  private CloudSolrClient.CloudSolrClientConnection 
resolveSolrConnection(String credentials)
+      throws Exception {
+    String solrConnection =
+        (connectionOptions != null && connectionOptions.solrConnection != null)
+            ? connectionOptions.solrConnection
+            : EnvUtils.getProperty("solr.connection");
+    if (solrConnection != null && !solrConnection.isBlank()) {
+      return CloudSolrClient.CloudSolrClientConnection.parse(solrConnection);
+    }
+    String zkHost =
+        (connectionOptions != null && connectionOptions.zkHost != null)
+            ? connectionOptions.zkHost
+            : EnvUtils.getProperty("zkHost");
+    if (zkHost != null && !zkHost.isBlank()) {
+      var zkConnection = 
CloudSolrClient.CloudSolrClientConnection.parse(zkHost);
+      if (!zkConnection.isZookeeper()) {
+        throw new IOException(
+            String.format(
+                Locale.ROOT, "Expected ZooKeeper connection string, but got: 
'%s'.", zkHost));
+      }
+      return zkConnection;
+    }
+    String solrUrl =
+        (connectionOptions != null && connectionOptions.solrUrl != null)
+            ? CLIUtils.normalizeSolrUrl(connectionOptions.solrUrl)
+            : CLIUtils.getDefaultSolrUrl();
+    try (SolrClient solrClient = CLIUtils.getSolrClient(solrUrl, credentials)) 
{
+      Map<String, Object> status = StatusTool.reportStatus(solrClient);
+      @SuppressWarnings("unchecked")
+      Map<String, Object> cloud = (Map<String, Object>) status.get("cloud");
+      if (cloud == null) {
+        return null;
+      }
+      String zookeeper = (String) cloud.get("ZooKeeper");
+      if (zookeeper.endsWith("(embedded)")) {
+        zookeeper = zookeeper.substring(0, zookeeper.length() - 
"(embedded)".length());
+      }
+      return CloudSolrClient.CloudSolrClientConnection.parse(zookeeper);
+    }
   }
 }
 
diff --git a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java 
b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
index 2dd48b27cad..57b22e79d3e 100755
--- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
+++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
@@ -85,7 +85,8 @@ import org.slf4j.LoggerFactory;
       ApiTool.class,
       ClusterTool.class,
       ConfigTool.class,
-      ExportTool.class
+      ExportTool.class,
+      HealthcheckTool.class
     })
 public class SolrCLI implements CLIO, java.util.concurrent.Callable<Integer> {
 
diff --git 
a/solr/core/src/test/org/apache/solr/cli/HealthcheckToolPicocliTest.java 
b/solr/core/src/test/org/apache/solr/cli/HealthcheckToolPicocliTest.java
new file mode 100644
index 00000000000..bcef8b7e705
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/cli/HealthcheckToolPicocliTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.cli;
+
+import java.util.Arrays;
+import org.junit.Test;
+import picocli.CommandLine;
+
+/**
+ * Runs all {@link HealthcheckToolTest} tests through the picocli invocation 
path.
+ *
+ * <p>All {@code @Test} methods are inherited; only the invocation strategy is 
overridden.
+ */
+public class HealthcheckToolPicocliTest extends HealthcheckToolTest {
+
+  @Override
+  protected int runTool(String[] args, Class<? extends ToolBase> clazz) throws 
Exception {
+    // args[0] is the tool name used by commons-cli dispatch; strip it for 
picocli.
+    String[] toolArgs = Arrays.copyOfRange(args, 1, args.length);
+    ToolRuntime runtime = new CLITestHelper.TestingRuntime(false);
+    ToolBase tool = 
clazz.getDeclaredConstructor(ToolRuntime.class).newInstance(runtime);
+    return new CommandLine(tool)
+        .setDefaultValueProvider(new CliDefaultValueProvider())
+        .execute(toolArgs);
+  }
+
+  @Test
+  public void testHealthcheckWithSolrConnectionProperty() throws Exception {
+    // SOLR_CONNECTION reaches the JVM as the solr.connection property
+    System.setProperty("solr.connection", getHttpSolrConnection().toString());
+    String[] args = new String[] {"healthcheck", "-c", "bob"};
+    assertEquals(0, runTool(args, HealthcheckTool.class));
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/cli/HealthcheckToolTest.java 
b/solr/core/src/test/org/apache/solr/cli/HealthcheckToolTest.java
index 68d7fba3c5d..0cd3d8b583c 100644
--- a/solr/core/src/test/org/apache/solr/cli/HealthcheckToolTest.java
+++ b/solr/core/src/test/org/apache/solr/cli/HealthcheckToolTest.java
@@ -27,6 +27,11 @@ import org.junit.Test;
 
 public class HealthcheckToolTest extends SolrCloudTestCase {
 
+  /** Runs the tool. Overridden by the picocli variant of this test. */
+  protected int runTool(String[] args, Class<? extends ToolBase> clazz) throws 
Exception {
+    return CLITestHelper.runTool(args, clazz);
+  }
+
   @BeforeClass
   public static void setupCluster() throws Exception {
     configureCluster(1)
@@ -44,7 +49,7 @@ public class HealthcheckToolTest extends SolrCloudTestCase {
 
     String[] args =
         new String[] {"healthcheck", "-c", "bob", "-z", 
cluster.getZkClient().getZkServerAddress()};
-    assertEquals(0, CLITestHelper.runTool(args, HealthcheckTool.class));
+    assertEquals(0, runTool(args, HealthcheckTool.class));
   }
 
   @Test
@@ -56,7 +61,7 @@ public class HealthcheckToolTest extends SolrCloudTestCase {
         
ZkStateReader.from(cluster.getSolrClient()).getBaseUrlForNodeName(firstLiveNode);
 
     String[] args = new String[] {"healthcheck", "-c", "bob", "--solr-url", 
solrUrl};
-    assertEquals(0, CLITestHelper.runTool(args, HealthcheckTool.class));
+    assertEquals(0, runTool(args, HealthcheckTool.class));
   }
 
   @Test
@@ -70,7 +75,7 @@ public class HealthcheckToolTest extends SolrCloudTestCase {
     solrUrl = solrUrl.substring(0, solrUrl.indexOf("/solr"));
 
     String[] args = new String[] {"healthcheck", "-c", "bob", "--solr-url", 
solrUrl};
-    assertEquals(0, CLITestHelper.runTool(args, HealthcheckTool.class));
+    assertEquals(0, runTool(args, HealthcheckTool.class));
   }
 
   @Test
@@ -81,7 +86,7 @@ public class HealthcheckToolTest extends SolrCloudTestCase {
     for (String connectionString : connStrings) {
       String[] args =
           new String[] {"healthcheck", "-c", "bob", "--solr-connection", 
connectionString};
-      assertEquals(0, CLITestHelper.runTool(args, HealthcheckTool.class));
+      assertEquals(0, runTool(args, HealthcheckTool.class));
     }
   }
 
@@ -89,6 +94,6 @@ public class HealthcheckToolTest extends SolrCloudTestCase {
   public void testHealthcheckWithZookeeperParameter() throws Exception {
     String zkHost = cluster.getZkServer().getZkAddress();
     String[] args = new String[] {"healthcheck", "-c", "bob", "--zk-host", 
zkHost};
-    assertEquals(0, CLITestHelper.runTool(args, HealthcheckTool.class));
+    assertEquals(0, runTool(args, HealthcheckTool.class));
   }
 }
diff --git a/solr/packaging/build.gradle b/solr/packaging/build.gradle
index 19c9de38673..50efa69ddbf 100644
--- a/solr/packaging/build.gradle
+++ b/solr/packaging/build.gradle
@@ -366,6 +366,7 @@ task integrationTestsPicocli(type: BatsTask) {
       'test/test_auth.bats',
       'test/test_modules.bats',
       'test/test_security_manager.bats',
+      'test/test_healthcheck.bats',
   ]
 }
 
diff --git a/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc 
b/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc
index 712f9fee593..8336adf0a11 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc
@@ -28,6 +28,7 @@
 *** xref:cli/solr-create.adoc[create]
 *** xref:cli/solr-delete.adoc[delete]
 *** xref:cli/solr-export.adoc[export]
+*** xref:cli/solr-healthcheck.adoc[healthcheck]
 *** xref:cli/solr-start.adoc[start]
 *** xref:cli/solr-status.adoc[status]
 *** xref:cli/solr-stop.adoc[stop]
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc
index 051f9f6f1b7..4bf311d3315 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/index.adoc
@@ -67,6 +67,7 @@ Any command not listed here is unavailable when the picocli 
implementation is en
 |xref:cli/solr-create.adoc[bin/solr create]
 |xref:cli/solr-delete.adoc[bin/solr delete]
 |xref:cli/solr-export.adoc[bin/solr export]
+|xref:cli/solr-healthcheck.adoc[bin/solr healthcheck]
 |xref:cli/solr-start.adoc[bin/solr start]
 |xref:cli/solr-status.adoc[bin/solr status]
 |xref:cli/solr-stop.adoc[bin/solr stop]
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-healthcheck.adoc 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-healthcheck.adoc
new file mode 100644
index 00000000000..15365fde2fc
--- /dev/null
+++ 
b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-healthcheck.adoc
@@ -0,0 +1,81 @@
+// 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.
+//
+// DO NOT EDIT -- this page is auto-generated from picocli annotations.
+// To update: modify the @Command/@Option annotations in the Java source, then 
run:
+//   ./gradlew :solr:solr-ref-guide:generateCliDocs
+
+= bin/solr healthcheck
+:page-toclevels: 2
+
+[WARNING]
+====
+This page documents the *experimental* picocli implementation of `bin/solr`, 
which is not enabled by default.
+For what `bin/solr` does by default, see 
xref:solr-control-script-reference.adoc[].
+See xref:cli/index.adoc[] to enable this implementation and for its known 
limitations.
+====
+
+// tag::picocli-generated-man-section-synopsis[]
+== Synopsis
+
+....
+bin/solr healthcheck [-hv] -c=COLLECTION [-u=<credentials>] 
[-s=<solrConnection> |
+                     --solr-url=<solrUrl> | -z=<zkHost>]
+....
+
+// end::picocli-generated-man-section-synopsis[]
+
+// tag::picocli-generated-man-section-description[]
+== Description
+
+Verifies that a collection is functioning: queries every replica directly, 
compares document counts and checks that each shard has a leader and every 
replica is ACTIVE. Requires SolrCloud.
+
+// end::picocli-generated-man-section-description[]
+
+// tag::picocli-generated-man-section-options[]
+== Options
+
+*-c*, *--name*=_COLLECTION_::
+  *(required)* Name of the collection to check.
+
+*-h*, *--help*::
+  Print this help message and exit.
+
+*-s*, *--solr-connection*=_<solrConnection>_::
+  Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is 
defined in solr.in.sh; otherwise, defaults to localhost:9983.
+
+*--solr-url*=_<solrUrl>_::
+  Base Solr URL, which can be used to determine the zk-host if that's not 
known.
+
+*-u*, *--credentials*=_<credentials>_::
+  Credentials in the format username:password. Example: --credentials 
solr:SolrRocks
+
+*-v*, *--verbose*::
+  Enable verbose mode.
+
+*-z*, *--zk-host*=_<zkHost>_::
+  Zookeeper connection string; unnecessary if ZK_HOST is defined in 
solr.in.sh; otherwise, defaults to localhost:9983.
+
+// end::picocli-generated-man-section-options[]
+
+// tag::picocli-generated-man-section-footer[]
+== Examples
+[%hardbreaks]
+  # Check the health of a collection
+  bin/solr healthcheck -c gettingstarted
+
+// end::picocli-generated-man-section-footer[]

Reply via email to