janhoy commented on code in PR #4739:
URL: https://github.com/apache/solr/pull/4739#discussion_r3835772433
##########
solr/core/src/java/org/apache/solr/cli/PackageTool.java:
##########
@@ -381,6 +492,64 @@ public Options getOptions() {
@Override
public int callTool() throws Exception {
- throw new UnsupportedOperationException("This tool does not yet support
PicoCli");
+ String credentials = credentialsOptions.credentials;
+ String solrUrl = resolveSolrUrl(credentials);
+ String zkHost = resolveZkHost(solrUrl, credentials);
+ String[] args = cmdArgs == null ? new String[0] : cmdArgs;
+ PackageFlags packageFlags =
+ new PackageFlags(collections, cluster, param, update, collection,
noPrompt);
+ executePackage(solrUrl, zkHost, credentials, cmd, args, packageFlags);
+ return 0;
Review Comment:
`callTool()` returns 0 unconditionally, so the exit codes advertised in
`@Command(exitCodeList = ...)` never actually occur: an `install` that prints
`printRed(pkg + " installation failed.")` still exits 0, as does a `deploy`
rejected for missing `--cluster`/`--collections`. Only a thrown exception
yields 1, via `ToolBase.call`.
That makes `bin/solr package install foo; echo $?` misleading in scripts.
Either return 1 on those failure paths (per subcommand, once they're split out)
or drop the exit-code-1 entry from the annotation.
##########
solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package.adoc:
##########
@@ -0,0 +1,130 @@
+// 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 package
+:page-toclevels: 2
+
+// tag::picocli-generated-man-section-name[]
+== Name
+
+bin/solr package - Install, deploy and manage Solr packages in SolrCloud.
+
+// end::picocli-generated-man-section-name[]
+
+// tag::picocli-generated-man-section-synopsis[]
+== Synopsis
+
+....
+bin/solr package [-vy] [--cluster] [--update] [-c=COLLECTION]
+ [--collections=COLLECTIONS] [-u=<credentials>]
[--param=PARAMS]...
+ [-s=<solrConnection> | --solr-url=<solrUrl> | -z=<zkHost>]
COMMAND
+ [ARGS...]
+....
+
+// end::picocli-generated-man-section-synopsis[]
+
+// tag::picocli-generated-man-section-description[]
+== Description
+
+Install, deploy and manage Solr packages in SolrCloud.
+
+// end::picocli-generated-man-section-description[]
+
+// tag::picocli-generated-man-section-options[]
+== Options
+
+*-c*, *--collection*=_COLLECTION_::
+ The collection to apply the package to.
+
+*--cluster*::
+ Specifies that this action should affect cluster-level plugins only.
+
+*--collections*=_COLLECTIONS_::
+ Specifies that this action should affect plugins for the given collections
only, excluding cluster level plugins.
+
+*--param*=_PARAMS_::
+ List of parameters to be used with the deploy command.
+
+*-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
+
+*--update*::
+ If a deployment is an update over a previous deployment.
+
+*-v*, *--verbose*::
+ Enable verbose mode.
+
+*-y*, *--no-prompt*::
+ Don't prompt for input; accept all default choices, defaults to false.
+
+*-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-arguments[]
+== Arguments
+
+_COMMAND_::
+ Package command: add-repo, add-key, list-installed, list-available,
list-deployed, install, deploy, undeploy, uninstall.
+
+[_ARGS_...]::
+ Command-specific arguments (package name[:version], repository name/URL, key
file, etc.).
+
+// end::picocli-generated-man-section-arguments[]
+
+// tag::picocli-generated-man-section-commands[]
+// end::picocli-generated-man-section-commands[]
Review Comment:
The `== Commands` section is generated empty, because `package` exposes its
nine commands as a free-text positional rather than as picocli subcommands (see
my comment on the `COMMAND` positional in `PackageTool.java`).
That means the ref-guide page documents the entire package CLI as one option
list plus a single prose sentence listing command names — no per-command
synopsis, no indication of which options apply to which command. Once the
commands become real subcommands, this section populates itself and each
command also gets its own generated page and nav entry, matching how `zk` is
documented today.
##########
solr/core/src/test/org/apache/solr/cli/PackageToolPicocliTest.java:
##########
@@ -0,0 +1,34 @@
+/*
+ * 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 picocli.CommandLine;
+
+/**
+ * Runs all {@link PackageToolTest} tests through the picocli invocation path.
+ */
Review Comment:
`./gradlew tidy` hasn't been run — `./gradlew :solr:core:spotlessJavaCheck`
currently fails on this file and on `PackageToolTest.java`, so `./gradlew
check` will fail too.
##########
solr/core/src/java/org/apache/solr/cli/PackageTool.java:
##########
@@ -113,185 +206,203 @@ public String getName() {
+ "don't print stack traces, hence special treatment is needed
here."
+ "Need to turn off logging, and SLF4J doesn't seem to provide
for a way.")
public void runImpl(CommandLine cli) throws Exception {
+ String solrUrl = CLIUtils.normalizeSolrUrl(cli);
+ String zkHost = CLIUtils.getZkHost(cli);
+ String credentials =
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION);
+ String command = cli.getArgs()[0];
+ String[] cmdArgs = Arrays.copyOfRange(cli.getArgs(), 1,
cli.getArgs().length);
+ PackageFlags packageFlags =
+ new PackageFlags(
+ cli.getOptionValue(COLLECTIONS_OPTION),
+ cli.hasOption(CLUSTER_OPTION),
+ cli.getOptionValues(PARAM_OPTION),
+ cli.hasOption(UPDATE_OPTION),
+ cli.getOptionValue(COLLECTION_OPTION),
+ cli.hasOption(NO_PROMPT_OPTION));
+
+ executePackage(solrUrl, zkHost, credentials, command, cmdArgs,
packageFlags);
+ }
+
+ private void executePackage(
+ String solrUrl,
+ String zkHost,
+ String credentials,
+ String command,
+ String[] cmdArgs,
+ PackageFlags packageFlags)
+ throws Exception {
// Need a logging free, clean output going through to the user.
Level oldLevel =
LoggerContext.getContext(false).getRootLogger().getLevel();
Configurator.setRootLevel(Level.OFF);
try {
- String solrUrl = CLIUtils.normalizeSolrUrl(cli);
- String zkHost = CLIUtils.getZkHost(cli);
if (zkHost == null) {
throw new SolrException(ErrorCode.INVALID_STATE, "Package manager runs
only in SolrCloud");
}
log.info("ZK: {}", zkHost);
- String cmd = cli.getArgs()[0];
-
- try (SolrClient solrClient = CLIUtils.getSolrClient(cli, true)) {
+ try (SolrClient solrClient = CLIUtils.getSolrClient(solrUrl,
credentials, true)) {
packageManager = new PackageManager(runtime, solrClient, solrUrl,
zkHost);
try {
repositoryManager = new RepositoryManager(solrClient,
packageManager);
-
- switch (cmd) {
- case "add-repo":
- String repoName = cli.getArgs()[1];
- String repoUrl = cli.getArgs()[2];
- repositoryManager.addRepository(repoName, repoUrl);
- printGreen("Added repository: " + repoName);
- break;
- case "add-key":
- String keyFilename = cli.getArgs()[1];
- Path path = Path.of(keyFilename);
- repositoryManager.addKey(Files.readAllBytes(path),
path.getFileName().toString());
- break;
- case "list-installed":
- printGreen("Installed packages:\n-----");
- for (SolrPackageInstance pkg :
packageManager.fetchInstalledPackageInstances()) {
- printGreen(pkg);
- }
- break;
- case "list-available":
- printGreen("Available packages:\n-----");
- for (SolrPackage pkg : repositoryManager.getPackages()) {
- printGreen(pkg.name + " \t\t" + pkg.description);
- for (SolrPackageRelease version : pkg.versions) {
- printGreen("\tVersion: " + version.version);
- }
- }
- break;
- case "list-deployed":
- if (cli.hasOption(COLLECTION_OPTION)) {
- String collection = cli.getOptionValue(COLLECTION_OPTION);
- Map<String, SolrPackageInstance> packages =
- packageManager.getPackagesDeployed(collection);
- printGreen("Packages deployed on " + collection + ":");
- for (String packageName : packages.keySet()) {
- printGreen("\t" + packages.get(packageName));
- }
- } else {
- // nuance that we use an arg here instead of requiring a
--package parameter with a
- // value
- // in this code path
- String packageName = cli.getArgs()[1];
- Map<String, String> deployedCollections =
- packageManager.getDeployedCollections(packageName);
- if (!deployedCollections.isEmpty()) {
- printGreen("Collections on which package " + packageName + "
was deployed:");
- for (String collection : deployedCollections.keySet()) {
- printGreen(
- "\t"
- + collection
- + "("
- + packageName
- + ":"
- + deployedCollections.get(collection)
- + ")");
- }
- } else {
- printGreen("Package " + packageName + " not deployed on any
collection.");
- }
- }
- break;
- case "install":
- {
- Pair<String, String> parsedVersion =
parsePackageVersion(cli.getArgList().get(1));
- String packageName = parsedVersion.first();
- String version = parsedVersion.second();
- boolean success = repositoryManager.install(packageName,
version);
- if (success) {
- printGreen(packageName + " installed.");
- } else {
- printRed(packageName + " installation failed.");
- }
- break;
- }
- case "deploy":
- {
- if (cli.hasOption(CLUSTER_OPTION) ||
cli.hasOption(COLLECTIONS_OPTION)) {
- Pair<String, String> parsedVersion =
parsePackageVersion(cli.getArgList().get(1));
- String packageName = parsedVersion.first();
- String version = parsedVersion.second();
- boolean noPrompt = cli.hasOption(NO_PROMPT_OPTION);
- boolean isUpdate = cli.hasOption(UPDATE_OPTION);
- String[] collections =
- cli.hasOption(COLLECTIONS_OPTION)
- ? PackageUtils.validateCollections(
-
cli.getOptionValue(COLLECTIONS_OPTION).split(","))
- : new String[] {};
- String[] parameters = cli.getOptionValues(PARAM_OPTION);
- packageManager.deploy(
- packageName,
- version,
- collections,
- cli.hasOption(CLUSTER_OPTION),
- parameters,
- isUpdate,
- noPrompt);
- } else {
- printRed(
- "Either specify --cluster to deploy cluster level
plugins or --collections <list-of-collections> to deploy collection level
plugins");
- }
- break;
- }
- case "undeploy":
- {
- if (cli.hasOption(CLUSTER_OPTION) ||
cli.hasOption(COLLECTIONS_OPTION)) {
- Pair<String, String> parsedVersion =
parsePackageVersion(cli.getArgList().get(1));
- if (parsedVersion.second() != null) {
- throw new SolrException(
- ErrorCode.BAD_REQUEST,
- "Only package name expected, without a version.
Actual: "
- + cli.getArgList().get(1));
- }
- String packageName = parsedVersion.first();
- String[] collections =
- cli.hasOption(COLLECTIONS_OPTION)
- ? PackageUtils.validateCollections(
-
cli.getOptionValue(COLLECTIONS_OPTION).split(","))
- : new String[] {};
- packageManager.undeploy(packageName, collections,
cli.hasOption(CLUSTER_OPTION));
- } else {
- printRed(
- "Either specify --cluster to undeploy cluster level
plugins or -collections <list-of-collections> to undeploy collection level
plugins");
- }
- break;
- }
- case "uninstall":
- {
- Pair<String, String> parsedVersion =
parsePackageVersion(cli.getArgList().get(1));
- if (parsedVersion.second() == null) {
- throw new SolrException(
- ErrorCode.BAD_REQUEST,
- "Package name and version are both required. Actual: "
- + cli.getArgList().get(1));
- }
- String packageName = parsedVersion.first();
- String version = parsedVersion.second();
- packageManager.uninstall(packageName, version);
- break;
- }
- default:
- throw new RuntimeException("Unrecognized command: " + cmd);
- }
+ handleCommand(command, cmdArgs, packageFlags);
} finally {
packageManager.close();
}
}
log.info("Finished: {}", cmd);
Review Comment:
This logs the `cmd` **field**, which is only populated on the picocli path,
rather than the `command` parameter that `executePackage` was given. On the
commons-cli path it prints `Finished: null`. Should be `command`.
##########
solr/core/src/java/org/apache/solr/cli/PackageTool.java:
##########
@@ -113,185 +206,203 @@ public String getName() {
+ "don't print stack traces, hence special treatment is needed
here."
+ "Need to turn off logging, and SLF4J doesn't seem to provide
for a way.")
public void runImpl(CommandLine cli) throws Exception {
+ String solrUrl = CLIUtils.normalizeSolrUrl(cli);
+ String zkHost = CLIUtils.getZkHost(cli);
+ String credentials =
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION);
+ String command = cli.getArgs()[0];
+ String[] cmdArgs = Arrays.copyOfRange(cli.getArgs(), 1,
cli.getArgs().length);
+ PackageFlags packageFlags =
+ new PackageFlags(
+ cli.getOptionValue(COLLECTIONS_OPTION),
+ cli.hasOption(CLUSTER_OPTION),
+ cli.getOptionValues(PARAM_OPTION),
+ cli.hasOption(UPDATE_OPTION),
+ cli.getOptionValue(COLLECTION_OPTION),
+ cli.hasOption(NO_PROMPT_OPTION));
+
+ executePackage(solrUrl, zkHost, credentials, command, cmdArgs,
packageFlags);
+ }
+
+ private void executePackage(
+ String solrUrl,
+ String zkHost,
+ String credentials,
+ String command,
+ String[] cmdArgs,
+ PackageFlags packageFlags)
+ throws Exception {
// Need a logging free, clean output going through to the user.
Level oldLevel =
LoggerContext.getContext(false).getRootLogger().getLevel();
Configurator.setRootLevel(Level.OFF);
try {
- String solrUrl = CLIUtils.normalizeSolrUrl(cli);
- String zkHost = CLIUtils.getZkHost(cli);
if (zkHost == null) {
throw new SolrException(ErrorCode.INVALID_STATE, "Package manager runs
only in SolrCloud");
}
log.info("ZK: {}", zkHost);
- String cmd = cli.getArgs()[0];
-
- try (SolrClient solrClient = CLIUtils.getSolrClient(cli, true)) {
+ try (SolrClient solrClient = CLIUtils.getSolrClient(solrUrl,
credentials, true)) {
packageManager = new PackageManager(runtime, solrClient, solrUrl,
zkHost);
try {
repositoryManager = new RepositoryManager(solrClient,
packageManager);
-
- switch (cmd) {
- case "add-repo":
- String repoName = cli.getArgs()[1];
- String repoUrl = cli.getArgs()[2];
- repositoryManager.addRepository(repoName, repoUrl);
- printGreen("Added repository: " + repoName);
- break;
- case "add-key":
- String keyFilename = cli.getArgs()[1];
- Path path = Path.of(keyFilename);
- repositoryManager.addKey(Files.readAllBytes(path),
path.getFileName().toString());
- break;
- case "list-installed":
- printGreen("Installed packages:\n-----");
- for (SolrPackageInstance pkg :
packageManager.fetchInstalledPackageInstances()) {
- printGreen(pkg);
- }
- break;
- case "list-available":
- printGreen("Available packages:\n-----");
- for (SolrPackage pkg : repositoryManager.getPackages()) {
- printGreen(pkg.name + " \t\t" + pkg.description);
- for (SolrPackageRelease version : pkg.versions) {
- printGreen("\tVersion: " + version.version);
- }
- }
- break;
- case "list-deployed":
- if (cli.hasOption(COLLECTION_OPTION)) {
- String collection = cli.getOptionValue(COLLECTION_OPTION);
- Map<String, SolrPackageInstance> packages =
- packageManager.getPackagesDeployed(collection);
- printGreen("Packages deployed on " + collection + ":");
- for (String packageName : packages.keySet()) {
- printGreen("\t" + packages.get(packageName));
- }
- } else {
- // nuance that we use an arg here instead of requiring a
--package parameter with a
- // value
- // in this code path
- String packageName = cli.getArgs()[1];
- Map<String, String> deployedCollections =
- packageManager.getDeployedCollections(packageName);
- if (!deployedCollections.isEmpty()) {
- printGreen("Collections on which package " + packageName + "
was deployed:");
- for (String collection : deployedCollections.keySet()) {
- printGreen(
- "\t"
- + collection
- + "("
- + packageName
- + ":"
- + deployedCollections.get(collection)
- + ")");
- }
- } else {
- printGreen("Package " + packageName + " not deployed on any
collection.");
- }
- }
- break;
- case "install":
- {
- Pair<String, String> parsedVersion =
parsePackageVersion(cli.getArgList().get(1));
- String packageName = parsedVersion.first();
- String version = parsedVersion.second();
- boolean success = repositoryManager.install(packageName,
version);
- if (success) {
- printGreen(packageName + " installed.");
- } else {
- printRed(packageName + " installation failed.");
- }
- break;
- }
- case "deploy":
- {
- if (cli.hasOption(CLUSTER_OPTION) ||
cli.hasOption(COLLECTIONS_OPTION)) {
- Pair<String, String> parsedVersion =
parsePackageVersion(cli.getArgList().get(1));
- String packageName = parsedVersion.first();
- String version = parsedVersion.second();
- boolean noPrompt = cli.hasOption(NO_PROMPT_OPTION);
- boolean isUpdate = cli.hasOption(UPDATE_OPTION);
- String[] collections =
- cli.hasOption(COLLECTIONS_OPTION)
- ? PackageUtils.validateCollections(
-
cli.getOptionValue(COLLECTIONS_OPTION).split(","))
- : new String[] {};
- String[] parameters = cli.getOptionValues(PARAM_OPTION);
- packageManager.deploy(
- packageName,
- version,
- collections,
- cli.hasOption(CLUSTER_OPTION),
- parameters,
- isUpdate,
- noPrompt);
- } else {
- printRed(
- "Either specify --cluster to deploy cluster level
plugins or --collections <list-of-collections> to deploy collection level
plugins");
- }
- break;
- }
- case "undeploy":
- {
- if (cli.hasOption(CLUSTER_OPTION) ||
cli.hasOption(COLLECTIONS_OPTION)) {
- Pair<String, String> parsedVersion =
parsePackageVersion(cli.getArgList().get(1));
- if (parsedVersion.second() != null) {
- throw new SolrException(
- ErrorCode.BAD_REQUEST,
- "Only package name expected, without a version.
Actual: "
- + cli.getArgList().get(1));
- }
- String packageName = parsedVersion.first();
- String[] collections =
- cli.hasOption(COLLECTIONS_OPTION)
- ? PackageUtils.validateCollections(
-
cli.getOptionValue(COLLECTIONS_OPTION).split(","))
- : new String[] {};
- packageManager.undeploy(packageName, collections,
cli.hasOption(CLUSTER_OPTION));
- } else {
- printRed(
- "Either specify --cluster to undeploy cluster level
plugins or -collections <list-of-collections> to undeploy collection level
plugins");
- }
- break;
- }
- case "uninstall":
- {
- Pair<String, String> parsedVersion =
parsePackageVersion(cli.getArgList().get(1));
- if (parsedVersion.second() == null) {
- throw new SolrException(
- ErrorCode.BAD_REQUEST,
- "Package name and version are both required. Actual: "
- + cli.getArgList().get(1));
- }
- String packageName = parsedVersion.first();
- String version = parsedVersion.second();
- packageManager.uninstall(packageName, version);
- break;
- }
- default:
- throw new RuntimeException("Unrecognized command: " + cmd);
- }
+ handleCommand(command, cmdArgs, packageFlags);
} finally {
packageManager.close();
}
}
log.info("Finished: {}", cmd);
- } catch (Exception ex) {
+ } catch (Exception exception) {
// We need to print this since SolrCLI drops the stack trace in favour
// of brevity. Package tool should surely print the full stacktrace!
- ex.printStackTrace();
- throw ex;
+ exception.printStackTrace();
+ throw exception;
} finally {
// Restore the old logging level
Configurator.setRootLevel(oldLevel);
}
}
+ private void handleCommand(String command, String[] cmdArgs, PackageFlags
packageFlags)
+ throws Exception {
+ switch (command) {
+ case "add-repo":
+ String repoName = cmdArgs[0];
+ String repoUrl = cmdArgs[1];
+ repositoryManager.addRepository(repoName, repoUrl);
+ printGreen("Added repository: " + repoName);
+ break;
+ case "add-key":
+ String keyFilename = cmdArgs[0];
+ Path path = Path.of(keyFilename);
+ repositoryManager.addKey(Files.readAllBytes(path),
path.getFileName().toString());
+ break;
+ case "list-installed":
+ printGreen("Installed packages:\n-----");
+ for (SolrPackageInstance pkg :
packageManager.fetchInstalledPackageInstances()) {
+ printGreen(pkg);
+ }
+ break;
+ case "list-available":
+ printGreen("Available packages:\n-----");
+ for (SolrPackage pkg : repositoryManager.getPackages()) {
+ printGreen(pkg.name + " \t\t" + pkg.description);
+ for (SolrPackageRelease version : pkg.versions) {
+ printGreen("\tVersion: " + version.version);
+ }
+ }
+ break;
+ case "list-deployed":
+ if (packageFlags.collection() != null) {
+ String collection = packageFlags.collection();
+ Map<String, SolrPackageInstance> packages =
+ packageManager.getPackagesDeployed(collection);
+ printGreen("Packages deployed on " + collection + ":");
+ for (String packageName : packages.keySet()) {
+ printGreen("\t" + packages.get(packageName));
+ }
+ } else {
+ // nuance that we use an arg here instead of requiring a --package
parameter with a
+ // value
+ // in this code path
+ String packageName = cmdArgs[0];
+ Map<String, String> deployedCollections =
+ packageManager.getDeployedCollections(packageName);
+ if (!deployedCollections.isEmpty()) {
+ printGreen("Collections on which package " + packageName + " was
deployed:");
+ for (String collection : deployedCollections.keySet()) {
+ printGreen(
+ "\t"
+ + collection
+ + "("
+ + packageName
+ + ":"
+ + deployedCollections.get(collection)
+ + ")");
+ }
+ } else {
+ printGreen("Package " + packageName + " not deployed on any
collection.");
+ }
+ }
+ break;
+ case "install":
+ {
+ Pair<String, String> parsedVersion = parsePackageVersion(cmdArgs[0]);
+ String packageName = parsedVersion.first();
+ String version = parsedVersion.second();
+ boolean success = repositoryManager.install(packageName, version);
+ if (success) {
+ printGreen(packageName + " installed.");
+ } else {
+ printRed(packageName + " installation failed.");
+ }
+ break;
+ }
+ case "deploy":
+ {
+ if (packageFlags.cluster() || packageFlags.collections() != null) {
+ Pair<String, String> parsedVersion =
parsePackageVersion(cmdArgs[0]);
+ String packageName = parsedVersion.first();
+ String version = parsedVersion.second();
+ String[] collections =
+ packageFlags.collections() != null
+ ?
PackageUtils.validateCollections(packageFlags.collections().split(","))
+ : new String[] {};
+ packageManager.deploy(
+ packageName,
+ version,
+ collections,
+ packageFlags.cluster(),
+ packageFlags.parameters(),
+ packageFlags.update(),
+ packageFlags.noPrompt());
+ } else {
+ printRed(
+ "Either specify --cluster to deploy cluster level plugins or
--collections <list-of-collections> to deploy collection level plugins");
+ }
+ break;
+ }
+ case "undeploy":
+ {
+ if (packageFlags.cluster() || packageFlags.collections() != null) {
+ Pair<String, String> parsedVersion =
parsePackageVersion(cmdArgs[0]);
+ if (parsedVersion.second() != null) {
+ throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Only package name expected, without a version. Actual: " +
cmdArgs[0]);
+ }
+ String packageName = parsedVersion.first();
+ String[] collections =
+ packageFlags.collections() != null
+ ?
PackageUtils.validateCollections(packageFlags.collections().split(","))
+ : new String[] {};
+ packageManager.undeploy(packageName, collections,
packageFlags.cluster());
+ } else {
+ printRed(
+ "Either specify --cluster to undeploy cluster level plugins or
-collections <list-of-collections> to undeploy collection level plugins");
Review Comment:
Pre-existing typo now being carried over: `-collections` should be
`--collections` (the `deploy` counterpart a few lines up already says
`--collections`). Cheap to fix while this line is being touched — and if this
check becomes an `@ArgGroup` on an `Undeploy` subcommand, the message goes away
entirely.
##########
solr/core/src/java/org/apache/solr/cli/PackageTool.java:
##########
@@ -93,6 +117,75 @@ public class PackageTool extends ToolBase {
.desc("Don't prompt for input; accept all default choices, defaults
to false.")
.get();
+ record PackageFlags(
+ String collections,
+ boolean cluster,
+ String[] parameters,
+ boolean update,
+ String collection,
+ boolean noPrompt) {}
+
+ // --- picocli fields ---
+
+ @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+ private ConnectionOptions connectionOptions;
+
+ @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;
+
+ @picocli.CommandLine.Parameters(
+ index = "0",
+ arity = "1",
+ paramLabel = "COMMAND",
+ description =
+ "Package command: add-repo, add-key, list-installed, list-available,
list-deployed, install, deploy, undeploy, uninstall.")
+ private String cmd;
Review Comment:
**Model the nine package commands as real picocli subcommands.**
Right now `cmd` is a plain `String` positional and everything after it lands
in the `ARGS` catch-all below, with dispatch done by a hand-written `switch`.
The branch already has the pattern we want in `ZkTool`, which declares
`subcommands = {ConfigSetDownloadTool.class, ZkCpTool.class, ...}` and gets
synopsis, arity checking and per-command help for free.
Concretely, what we lose by not doing that here:
* `bin/solr package install --help` can't produce install-specific help, and
the generated ref-guide page has an **empty** `== Commands` section (see my
comment on `solr-package.adoc`).
* No arity validation. `bin/solr package add-repo myrepo` throws
`ArrayIndexOutOfBoundsException` on `cmdArgs[1]` instead of picocli's "Missing
required parameter".
* Every option is advertised globally even though each belongs to one or two
commands: `--collections` / `--cluster` / `--param` / `--update` are
deploy/undeploy-only, and `-c/--collection` is `list-deployed`-only. Nothing
tells the user, and nothing rejects a wrong combination.
* The `printRed("Either specify --cluster ... or --collections ...")`
runtime checks in `deploy`/`undeploy` are exactly what `@ArgGroup(exclusive =
true, multiplicity = "1")` on a `deploy` subcommand expresses declaratively.
* An unknown command becomes `RuntimeException("Unrecognized command: ...")`
rather than picocli's standard unknown-subcommand usage error.
Suggestion: `@Command(name = "package", subcommands = {AddRepo.class,
AddKey.class, ListInstalled.class, ListAvailable.class, ListDeployed.class,
Install.class, Deploy.class, Undeploy.class, Uninstall.class})`, with each
nested command carrying only its own `@Parameters`/`@Option`. The rest of my
comments assume this lands.
**PS: We should probably extend the LLM prompt template to identify this
pattern of sub-commands and guide it to rework in native PicoCli fashion**
##########
solr/core/src/java/org/apache/solr/cli/PackageTool.java:
##########
@@ -381,6 +492,64 @@ public Options getOptions() {
@Override
public int callTool() throws Exception {
- throw new UnsupportedOperationException("This tool does not yet support
PicoCli");
+ String credentials = credentialsOptions.credentials;
+ String solrUrl = resolveSolrUrl(credentials);
+ String zkHost = resolveZkHost(solrUrl, credentials);
+ String[] args = cmdArgs == null ? new String[0] : cmdArgs;
+ PackageFlags packageFlags =
+ new PackageFlags(collections, cluster, param, update, collection,
noPrompt);
+ executePackage(solrUrl, zkHost, credentials, cmd, args, packageFlags);
+ return 0;
+ }
+
+ private String resolveSolrUrl(String credentials) throws Exception {
Review Comment:
**These two resolvers duplicate connection logic that already exists in
three places.**
`resolveZkHost` below re-implements `ZkConnectionOptions.resolveZkHost()`
almost verbatim — same `StatusTool.reportStatus` → `cloud` → `ZooKeeper`
lookup, same `(embedded)` suffix stripping — and `resolveSolrUrl` copies the
stderr string from `CLIUtils.normalizeSolrUrl(CommandLine)` literally.
`CreateTool.callTool()` and `DeleteTool.callTool()` already carry the same
copy/paste, so this would make it the third and fourth.
Suggestion: hoist `resolveSolrUrl(credentials)` and
`resolveZkHost(credentials)` onto `ConnectionOptions` (it already has
`effectiveSolrUrl()` / `effectiveZkHost()`) and have `PackageTool`,
`CreateTool` and `DeleteTool` all call them. With subcommands this becomes even
more valuable: resolution belongs on the parent `package` command once,
inherited by all nine.
Minor while you're in here: `cloud.get("ZooKeeper").toString()` NPEs if the
key is absent; `ZkConnectionOptions` casts and null-checks instead.
##########
solr/core/src/java/org/apache/solr/cli/PackageTool.java:
##########
@@ -381,6 +492,64 @@ public Options getOptions() {
@Override
public int callTool() throws Exception {
- throw new UnsupportedOperationException("This tool does not yet support
PicoCli");
+ String credentials = credentialsOptions.credentials;
+ String solrUrl = resolveSolrUrl(credentials);
+ String zkHost = resolveZkHost(solrUrl, credentials);
+ String[] args = cmdArgs == null ? new String[0] : cmdArgs;
+ PackageFlags packageFlags =
+ new PackageFlags(collections, cluster, param, update, collection,
noPrompt);
+ executePackage(solrUrl, zkHost, credentials, cmd, args, packageFlags);
+ return 0;
+ }
+
+ private String resolveSolrUrl(String credentials) throws Exception {
+ if (connectionOptions != null) {
+ String solrUrl = connectionOptions.effectiveSolrUrl();
+ if (solrUrl != null) {
+ return CLIUtils.normalizeSolrUrl(solrUrl);
+ }
+ String zkHost = connectionOptions.effectiveZkHost();
+ if (zkHost != null) {
+ return CLIUtils.solrUrlFromConnection(
+ CloudSolrClient.CloudSolrClientConnection.parse(zkHost),
credentials);
+ }
+ }
+ String zkHostProp = EnvUtils.getProperty("zkHost");
+ if (zkHostProp != null && !zkHostProp.isBlank()) {
+ return CLIUtils.solrUrlFromConnection(
+ CloudSolrClient.CloudSolrClientConnection.parse(zkHostProp),
credentials);
+ }
+ String defaultUrl = CLIUtils.getDefaultSolrUrl();
+ CLIO.err(
+ "Neither --solr-connection, --zk-host or --solr-url parameters, nor
SOLR_CONNECTION, ZK_HOST env var provided, so assuming solr url is "
Review Comment:
This message promises more than the code delivers: the only fallback
actually consulted is `EnvUtils.getProperty("zkHost")` (line above, and again
in `resolveZkHost`). `SOLR_CONNECTION` / the `solrConnection` property is never
read, so it works under commons-cli (via
`CLIUtils.resolveSolrConnectionFromCli`) but is silently ignored on the picocli
path — while this message claims it was checked.
Worth noting the underlying cause is a framework gap rather than something
to solve per-tool: picocli doesn't apply the `CliDefaultValueProvider` to
`@ArgGroup` members when the group is unmatched, which is exactly why
`connectionOptions` can be `null` here. That's the still-unchecked "Solve
value-fallback to ENV" milestone on #3254.
If you spin this into a separate PR, then add a TODO or NOCOMMIT comment
with a link so we know there is a bug here that depends on some other PR.
##########
solr/core/src/java/org/apache/solr/cli/PackageTool.java:
##########
@@ -46,6 +49,27 @@
import org.slf4j.LoggerFactory;
/** Supports package command in the bin/solr script. */
+@SuppressWarnings("UnnecessarilyFullyQualified")
[email protected](
+ name = "package",
+ description = "Install, deploy and manage Solr packages in SolrCloud.",
Review Comment:
**The single-line description drops all the guidance the commons-cli help
gives today.**
`getHeader()` further down is effectively a mini-manual: a synopsis and
one-liner for each of the nine commands, plus two operational notes that aren't
documented anywhere else in the CLI:
> (a) Please add '--solr-url http://host:port' parameter if needed (usually
on Windows).
> (b) Please make sure that all Solr nodes are started with
'-solr.packages.enabled=true' parameter.
None of that reaches the picocli path, so `bin/solr package` under
`SOLR_PICOCLI=true` is meaningfully less helpful than today — note (b) in
particular is a common first-run gotcha.
Once the commands are split into subcommands, most of the per-command text
becomes each subcommand's own `description`. The two notes should move here, as
a multi-line `description = { ... }` array in the style of `ZkCpTool`.
Multi-line *command* descriptions render in full —
`SolrCLI.installFirstLineOnlyHelpFactory` only truncates *option* descriptions
— so nothing is lost in interactive help.
##########
solr/core/src/java/org/apache/solr/cli/PackageTool.java:
##########
@@ -113,185 +206,203 @@ public String getName() {
+ "don't print stack traces, hence special treatment is needed
here."
+ "Need to turn off logging, and SLF4J doesn't seem to provide
for a way.")
public void runImpl(CommandLine cli) throws Exception {
+ String solrUrl = CLIUtils.normalizeSolrUrl(cli);
Review Comment:
Hoisting the connection resolution into `runImpl` moved it outside the
`Configurator.setRootLevel(Level.OFF)` window that starts in `executePackage`.
Both `normalizeSolrUrl` and `getZkHost` can hit ZooKeeper/Solr and log, so the
"logging free, clean output going through to the user" intent no longer holds
for that phase. `callTool()` has the same shape.
Either move the level switch so it also wraps resolution, or drop the
comment if the narrower window is deliberate.
##########
solr/core/src/java/org/apache/solr/cli/PackageTool.java:
##########
@@ -93,6 +117,75 @@ public class PackageTool extends ToolBase {
.desc("Don't prompt for input; accept all default choices, defaults
to false.")
.get();
+ record PackageFlags(
+ String collections,
+ boolean cluster,
+ String[] parameters,
+ boolean update,
+ String collection,
+ boolean noPrompt) {}
+
+ // --- picocli fields ---
+
+ @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+ private ConnectionOptions connectionOptions;
+
+ @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;
+
+ @picocli.CommandLine.Parameters(
+ index = "0",
+ arity = "1",
+ paramLabel = "COMMAND",
+ description =
+ "Package command: add-repo, add-key, list-installed, list-available,
list-deployed, install, deploy, undeploy, uninstall.")
+ private String cmd;
+
+ @picocli.CommandLine.Parameters(
+ index = "1..*",
+ arity = "0..*",
+ paramLabel = "ARGS",
+ description =
+ "Command-specific arguments (package name[:version], repository
name/URL, key file, etc.).")
+ private String[] cmdArgs;
+
+ @picocli.CommandLine.Option(
+ names = {"--collections"},
+ paramLabel = "COLLECTIONS",
+ description =
+ "Specifies that this action should affect plugins for the given
collections only, excluding cluster level plugins.")
+ private String collections;
+
+ @picocli.CommandLine.Option(
+ names = {"--cluster"},
+ description = "Specifies that this action should affect cluster-level
plugins only.")
+ private boolean cluster;
+
+ @picocli.CommandLine.Option(
+ names = {"--param"},
+ paramLabel = "PARAMS",
+ description = "List of parameters to be used with the deploy command.")
+ private String[] param;
+
+ @picocli.CommandLine.Option(
+ names = {"--update"},
+ description = "If a deployment is an update over a previous deployment.")
+ private boolean update;
+
+ @picocli.CommandLine.Option(
+ names = {"-c", "--collection"},
+ paramLabel = "COLLECTION",
+ description = "The collection to apply the package to.")
Review Comment:
Small consistency point: the picocli and commons-cli descriptions for the
same flags have drifted, and both parsers ship simultaneously until commons-cli
is removed, so users can see two different help texts for one option.
* `-c/--collection`: "The collection to apply the package to." here vs "The
collection to apply the package to, **not required**." on `COLLECTION_OPTION`.
* `--param`: "…used with **the** deploy command." here vs "…used with deploy
command." on `PARAM_OPTION`.
The picocli wording is better in both cases — rewording commons-cli is kind
of a back-break, so perhaps let this stay, but if we get issues in BATS tests
(have not checked) we'll need to tweak the asserts to match both.
##########
solr/core/src/java/org/apache/solr/cli/PackageTool.java:
##########
@@ -93,6 +117,75 @@ public class PackageTool extends ToolBase {
.desc("Don't prompt for input; accept all default choices, defaults
to false.")
.get();
+ record PackageFlags(
+ String collections,
+ boolean cluster,
+ String[] parameters,
+ boolean update,
+ String collection,
+ boolean noPrompt) {}
+
+ // --- picocli fields ---
+
+ @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
+ private ConnectionOptions connectionOptions;
+
+ @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;
+
+ @picocli.CommandLine.Parameters(
+ index = "0",
+ arity = "1",
+ paramLabel = "COMMAND",
+ description =
+ "Package command: add-repo, add-key, list-installed, list-available,
list-deployed, install, deploy, undeploy, uninstall.")
+ private String cmd;
+
+ @picocli.CommandLine.Parameters(
+ index = "1..*",
+ arity = "0..*",
+ paramLabel = "ARGS",
+ description =
+ "Command-specific arguments (package name[:version], repository
name/URL, key file, etc.).")
+ private String[] cmdArgs;
+
+ @picocli.CommandLine.Option(
+ names = {"--collections"},
+ paramLabel = "COLLECTIONS",
+ description =
+ "Specifies that this action should affect plugins for the given
collections only, excluding cluster level plugins.")
+ private String collections;
+
+ @picocli.CommandLine.Option(
+ names = {"--cluster"},
+ description = "Specifies that this action should affect cluster-level
plugins only.")
+ private boolean cluster;
+
+ @picocli.CommandLine.Option(
+ names = {"--param"},
+ paramLabel = "PARAMS",
+ description = "List of parameters to be used with the deploy command.")
+ private String[] param;
Review Comment:
This silently changes `--param` arity. The commons-cli option uses
`.hasArgs()` (unlimited), so `--param a=1 b=2` worked; picocli's default arity
for a multi-value option is 1 per occurrence, so `b=2` becomes a positional
instead. Today it is then swallowed by the `ARGS` catch-all and ignored — the
user gets a half-configured deploy with no diagnostic.
Splitting into subcommands removes the catch-all and turns this into a clean
parse error, which is most of the fix. Beyond that, either set `arity = "1..*"`
to preserve the old behaviour, or keep arity 1 and document the repeated form —
`package-manager.adoc` already shows `--param a=1 --param b=2`, so the repeated
form is the documented one and I'd lean that way.
##########
solr/core/src/java/org/apache/solr/cli/PackageTool.java:
##########
@@ -93,6 +117,75 @@ public class PackageTool extends ToolBase {
.desc("Don't prompt for input; accept all default choices, defaults
to false.")
.get();
+ record PackageFlags(
Review Comment:
Worth a short javadoc, matching `AuthTool.AuthParams` ("Parameters for the
auth sub-commands, independent of the command line parser") — that record also
documents its non-obvious field. Keeping the pattern self-documenting matters
as more tools copy it.
That said, once the nine commands are separate subcommands this record
likely shrinks a lot or splits per command, since most fields apply to only one
or two of them — so maybe settle its final shape first.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]