From f71a9328f9fa9958ca43dae6968aaa94fb478f3b Mon Sep 17 00:00:00 2001 From: jaykay12 Date: Sat, 15 Aug 2026 13:45:14 +0530 Subject: [PATCH 1/5] prompt done --- .../java/org/apache/solr/cli/PackageTool.java | 460 ++++++++++++------ .../src/java/org/apache/solr/cli/SolrCLI.java | 3 +- 2 files changed, 311 insertions(+), 152 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/cli/PackageTool.java b/solr/core/src/java/org/apache/solr/cli/PackageTool.java index 9dea8aaba9e9..a9aae6039216 100644 --- a/solr/core/src/java/org/apache/solr/cli/PackageTool.java +++ b/solr/core/src/java/org/apache/solr/cli/PackageTool.java @@ -24,6 +24,7 @@ import java.lang.invoke.MethodHandles; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; import java.util.Map; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; @@ -33,8 +34,10 @@ import org.apache.logging.log4j.core.config.Configurator; import org.apache.lucene.util.SuppressForbidden; import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.impl.CloudSolrClient; import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrException.ErrorCode; +import org.apache.solr.common.util.EnvUtils; import org.apache.solr.common.util.Pair; import org.apache.solr.packagemanager.PackageManager; import org.apache.solr.packagemanager.PackageUtils; @@ -46,6 +49,27 @@ import org.slf4j.LoggerFactory; /** Supports package command in the bin/solr script. */ +@SuppressWarnings("UnnecessarilyFullyQualified") +@picocli.CommandLine.Command( + name = "package", + description = "Install, deploy and manage Solr packages in SolrCloud.", + exitCodeListHeading = "%nExit Codes:%n", + exitCodeList = { + "0:Operation completed successfully.", + "1:Operation failed; check output for details." + }, + footerHeading = "%nExamples:%n", + footer = { + " # Add a package repository", + " bin/solr package add-repo myrepo https://my.repo.example/solr-packages", + "", + " # Install a package and deploy it to a collection", + " bin/solr package install mypkg-1.0.0", + " bin/solr package deploy mypkg-1.0.0 --collections myCollection -y", + "", + " # List packages deployed on a collection", + " bin/solr package list-deployed -c myCollection" + }) public class PackageTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -93,6 +117,72 @@ 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 collection 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.") + private String collection; + + @picocli.CommandLine.Option( + names = {"-y", "--no-prompt"}, + description = "Don't prompt for input; accept all default choices, defaults to false.") + private boolean noPrompt; + + public PackageTool() { + this(new DefaultToolRuntime()); + } + public PackageTool(ToolRuntime runtime) { super(runtime); } @@ -113,185 +203,198 @@ 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 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 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 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 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 to deploy collection level plugins"); - } - break; - } - case "undeploy": - { - if (cli.hasOption(CLUSTER_OPTION) || cli.hasOption(COLLECTIONS_OPTION)) { - Pair 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 to undeploy collection level plugins"); - } - break; - } - case "uninstall": - { - Pair 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 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 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 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 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 to deploy collection level plugins"); + } + break; + } + case "undeploy": + { + if (packageFlags.cluster() || packageFlags.collections() != null) { + Pair 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 to undeploy collection level plugins"); + } + break; + } + case "uninstall": + { + Pair parsedVersion = parsePackageVersion(cmdArgs[0]); + if (parsedVersion.second() == null) { + throw new SolrException( + ErrorCode.BAD_REQUEST, + "Package name and version are both required. Actual: " + cmdArgs[0]); + } + String packageName = parsedVersion.first(); + String version = parsedVersion.second(); + packageManager.uninstall(packageName, version); + break; + } + default: + throw new RuntimeException("Unrecognized command: " + command); + } + } + @Override public String getHeader() { StringBuilder sb = new StringBuilder(); @@ -381,6 +484,61 @@ 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 " + + defaultUrl + + "."); + return defaultUrl; + } + + private String resolveZkHost(String solrUrl, String credentials) throws Exception { + if (connectionOptions != null) { + String zkHost = connectionOptions.effectiveZkHost(); + if (zkHost != null) { + return zkHost; + } + } + String zkHostProp = EnvUtils.getProperty("zkHost"); + if (zkHostProp != null && !zkHostProp.isBlank()) { + return zkHostProp; + } + try (SolrClient solrClient = CLIUtils.getSolrClient(solrUrl, credentials)) { + Map status = StatusTool.reportStatus(solrClient); + @SuppressWarnings("unchecked") + Map cloud = (Map) status.get("cloud"); + if (cloud != null) { + String zookeeper = cloud.get("ZooKeeper").toString(); + if (zookeeper != null && zookeeper.endsWith("(embedded)")) { + zookeeper = zookeeper.substring(0, zookeeper.length() - "(embedded)".length()); + } + return zookeeper; + } + } + return null; } } 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 266bb08badb6..2eb29b2aae28 100755 --- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java +++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java @@ -82,7 +82,8 @@ ZkTool.class, AuthTool.class, CreateTool.class, - DeleteTool.class + DeleteTool.class, + PackageTool.class }) public class SolrCLI implements CLIO { From f61cdebd880505260844400297aa8624536c459c Mon Sep 17 00:00:00 2001 From: jaykay12 Date: Sat, 15 Aug 2026 13:48:37 +0530 Subject: [PATCH 2/5] minor --- solr/core/src/java/org/apache/solr/cli/PackageTool.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/cli/PackageTool.java b/solr/core/src/java/org/apache/solr/cli/PackageTool.java index a9aae6039216..3d088bbac776 100644 --- a/solr/core/src/java/org/apache/solr/cli/PackageTool.java +++ b/solr/core/src/java/org/apache/solr/cli/PackageTool.java @@ -64,8 +64,8 @@ " bin/solr package add-repo myrepo https://my.repo.example/solr-packages", "", " # Install a package and deploy it to a collection", - " bin/solr package install mypkg-1.0.0", - " bin/solr package deploy mypkg-1.0.0 --collections myCollection -y", + " bin/solr package install mypkg:1.0.0", + " bin/solr package deploy mypkg:1.0.0 --collections myCollection -y", "", " # List packages deployed on a collection", " bin/solr package list-deployed -c myCollection" @@ -149,12 +149,12 @@ record PackageFlags( @picocli.CommandLine.Option( names = {"--collections"}, paramLabel = "COLLECTIONS", - description = "Specifies that this action should affect plugins for the given collection only, excluding cluster level plugins.") + 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.") + description = "Specifies that this action should affect cluster-level plugins only.") private boolean cluster; @picocli.CommandLine.Option( From 86913e59b1707b80c5d4ceab38a869fd1f784cac Mon Sep 17 00:00:00 2001 From: jaykay12 Date: Sat, 15 Aug 2026 13:49:39 +0530 Subject: [PATCH 3/5] tidying done --- .../java/org/apache/solr/cli/PackageTool.java | 181 ++++++++++-------- .../src/java/org/apache/solr/cli/SolrCLI.java | 2 +- 2 files changed, 97 insertions(+), 86 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/cli/PackageTool.java b/solr/core/src/java/org/apache/solr/cli/PackageTool.java index 3d088bbac776..4975378ce435 100644 --- a/solr/core/src/java/org/apache/solr/cli/PackageTool.java +++ b/solr/core/src/java/org/apache/solr/cli/PackageTool.java @@ -55,20 +55,20 @@ description = "Install, deploy and manage Solr packages in SolrCloud.", exitCodeListHeading = "%nExit Codes:%n", exitCodeList = { - "0:Operation completed successfully.", - "1:Operation failed; check output for details." + "0:Operation completed successfully.", + "1:Operation failed; check output for details." }, footerHeading = "%nExamples:%n", footer = { - " # Add a package repository", - " bin/solr package add-repo myrepo https://my.repo.example/solr-packages", - "", - " # Install a package and deploy it to a collection", - " bin/solr package install mypkg:1.0.0", - " bin/solr package deploy mypkg:1.0.0 --collections myCollection -y", - "", - " # List packages deployed on a collection", - " bin/solr package list-deployed -c myCollection" + " # Add a package repository", + " bin/solr package add-repo myrepo https://my.repo.example/solr-packages", + "", + " # Install a package and deploy it to a collection", + " bin/solr package install mypkg:1.0.0", + " bin/solr package deploy mypkg:1.0.0 --collections myCollection -y", + "", + " # List packages deployed on a collection", + " bin/solr package list-deployed -c myCollection" }) public class PackageTool extends ToolBase { @@ -136,20 +136,23 @@ record PackageFlags( index = "0", arity = "1", paramLabel = "COMMAND", - description = "Package command: add-repo, add-key, list-installed, list-available, list-deployed, install, deploy, undeploy, uninstall.") + 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.).") + 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.") + description = + "Specifies that this action should affect plugins for the given collections only, excluding cluster level plugins.") private String collections; @picocli.CommandLine.Option( @@ -208,13 +211,14 @@ public void runImpl(CommandLine cli) throws Exception { 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)); + 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); } @@ -225,7 +229,8 @@ private void executePackage( String credentials, String command, String[] cmdArgs, - PackageFlags packageFlags) throws Exception { + PackageFlags packageFlags) + throws Exception { // Need a logging free, clean output going through to the user. Level oldLevel = LoggerContext.getContext(false).getRootLogger().getLevel(); @@ -260,7 +265,8 @@ private void executePackage( } } - private void handleCommand(String command, String[] cmdArgs, PackageFlags packageFlags) throws Exception { + private void handleCommand(String command, String[] cmdArgs, PackageFlags packageFlags) + throws Exception { switch (command) { case "add-repo": String repoName = cmdArgs[0]; @@ -322,74 +328,76 @@ private void handleCommand(String command, String[] cmdArgs, PackageFlags packag } break; case "install": - { - Pair 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 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 to deploy collection level plugins"); + 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 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 to deploy collection level plugins"); + } + break; } - break; - } case "undeploy": - { - if (packageFlags.cluster() || packageFlags.collections() != null) { + { + if (packageFlags.cluster() || packageFlags.collections() != null) { + Pair 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 to undeploy collection level plugins"); + } + break; + } + case "uninstall": + { Pair parsedVersion = parsePackageVersion(cmdArgs[0]); - if (parsedVersion.second() != null) { + if (parsedVersion.second() == null) { throw new SolrException( ErrorCode.BAD_REQUEST, - "Only package name expected, without a version. Actual: " + cmdArgs[0]); + "Package name and version are both required. 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 to undeploy collection level plugins"); - } - break; - } - case "uninstall": - { - Pair parsedVersion = parsePackageVersion(cmdArgs[0]); - if (parsedVersion.second() == null) { - throw new SolrException( - ErrorCode.BAD_REQUEST, - "Package name and version are both required. Actual: " + cmdArgs[0]); + String version = parsedVersion.second(); + packageManager.uninstall(packageName, version); + break; } - String packageName = parsedVersion.first(); - String version = parsedVersion.second(); - packageManager.uninstall(packageName, version); - break; - } default: throw new RuntimeException("Unrecognized command: " + command); } @@ -488,7 +496,8 @@ public int callTool() throws Exception { 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); + PackageFlags packageFlags = + new PackageFlags(collections, cluster, param, update, collection, noPrompt); executePackage(solrUrl, zkHost, credentials, cmd, args, packageFlags); return 0; } @@ -501,18 +510,20 @@ private String resolveSolrUrl(String credentials) throws Exception { } String zkHost = connectionOptions.effectiveZkHost(); if (zkHost != null) { - return CLIUtils.solrUrlFromConnection(CloudSolrClient.CloudSolrClientConnection.parse(zkHost), credentials); + 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); + 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 " - + defaultUrl - + "."); + + defaultUrl + + "."); return defaultUrl; } 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 2eb29b2aae28..263dc54dce30 100755 --- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java +++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java @@ -83,7 +83,7 @@ AuthTool.class, CreateTool.class, DeleteTool.class, - PackageTool.class + PackageTool.class }) public class SolrCLI implements CLIO { From 124256ca94c5f0d71c92c7f944babef19bb07775 Mon Sep 17 00:00:00 2001 From: jaykay12 Date: Sat, 15 Aug 2026 16:58:35 +0530 Subject: [PATCH 4/5] generated cli docs --- .../deployment-guide/deployment-nav.adoc | 1 + .../pages/cli/solr-package.adoc | 130 ++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package.adoc 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 5f83b0407404..fb90bd82b38b 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc @@ -22,6 +22,7 @@ ** xref:cli/solr-auth.adoc[auth] ** xref:cli/solr-create.adoc[create] ** xref:cli/solr-delete.adoc[delete] +** xref:cli/solr-package.adoc[package] ** 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/solr-package.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-package.adoc new file mode 100644 index 000000000000..6190d55eed44 --- /dev/null +++ b/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=] [--param=PARAMS]... + [-s= | --solr-url= | -z=] 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*=__:: + Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +*--solr-url*=__:: + Base Solr URL, which can be used to determine the zk-host if that's not known. + +*-u*, *--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*=__:: + 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[] + +// tag::picocli-generated-man-section-exit-status[] +== Exit Codes: + +*0*:: + Operation completed successfully. + +*1*:: + Operation failed; check output for details. + +// end::picocli-generated-man-section-exit-status[] + +// tag::picocli-generated-man-section-footer[] +== Examples: + +[%hardbreaks] + # Add a package repository + bin/solr package add-repo myrepo https://my.repo.example/bin/solr packages + +[%hardbreaks] + # Install a package and deploy it to a collection + bin/solr package install mypkg:1.0.0 + bin/solr package deploy mypkg:1.0.0 --collections myCollection -y + +[%hardbreaks] + # List packages deployed on a collection + bin/solr package list-deployed -c myCollection + +// end::picocli-generated-man-section-footer[] From 4e2e52822f5f23341866e25d30d11149365f9587 Mon Sep 17 00:00:00 2001 From: jaykay12 Date: Sat, 15 Aug 2026 18:30:54 +0530 Subject: [PATCH 5/5] tests --- .../solr/cli/PackageToolPicocliTest.java | 34 ++++++++ .../org/apache/solr/cli/PackageToolTest.java | 79 ++++++++----------- 2 files changed, 65 insertions(+), 48 deletions(-) create mode 100644 solr/core/src/test/org/apache/solr/cli/PackageToolPicocliTest.java diff --git a/solr/core/src/test/org/apache/solr/cli/PackageToolPicocliTest.java b/solr/core/src/test/org/apache/solr/cli/PackageToolPicocliTest.java new file mode 100644 index 000000000000..70d0ccc7ccb2 --- /dev/null +++ b/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. + */ +public class PackageToolPicocliTest extends PackageToolTest { + + @Override + protected int runTool(String[] args, ToolRuntime runtime) throws Exception { + PackageTool tool = new PackageTool(runtime); + return new CommandLine(tool) + .setDefaultValueProvider(new CliDefaultValueProvider()) + .execute(args); + } +} diff --git a/solr/core/src/test/org/apache/solr/cli/PackageToolTest.java b/solr/core/src/test/org/apache/solr/cli/PackageToolTest.java index 9282af37e520..abb8be0f7a52 100644 --- a/solr/core/src/test/org/apache/solr/cli/PackageToolTest.java +++ b/solr/core/src/test/org/apache/solr/cli/PackageToolTest.java @@ -89,21 +89,21 @@ private > T withBasicAuth(T req) { return req; } - @Test - public void testPackageTool() throws Exception { - ToolRuntime runtime = new CLITestHelper.TestingRuntime(false); + protected int runTool(String[] args, ToolRuntime runtime) throws Exception { PackageTool tool = new PackageTool(runtime); + return tool.runTool(SolrCLI.processCommandLineArgs(tool, args)); + } + @Test + public void testPackageTool() throws Exception { String solrUrl = cluster.getJettySolrRunner(0).getBaseUrl().toString(); run( - tool, new String[] { "--solr-url", solrUrl, "list-installed", "--credentials", SecurityJson.USER_PASS }); run( - tool, new String[] { "--solr-url", solrUrl, @@ -115,13 +115,11 @@ public void testPackageTool() throws Exception { }); run( - tool, new String[] { "--solr-url", solrUrl, "list-available", "--credentials", SecurityJson.USER_PASS }); run( - tool, new String[] { "--solr-url", solrUrl, @@ -132,7 +130,6 @@ public void testPackageTool() throws Exception { }); run( - tool, new String[] { "--solr-url", solrUrl, "list-installed", "--credentials", SecurityJson.USER_PASS }); @@ -145,7 +142,6 @@ public void testPackageTool() throws Exception { String rhPath = "/mypath2"; run( - tool, new String[] { "--solr-url", solrUrl, @@ -157,7 +153,6 @@ public void testPackageTool() throws Exception { // Leaving -p in for --param to test the deprecated value continues to work. run( - tool, new String[] { "--solr-url", solrUrl, @@ -175,7 +170,6 @@ public void testPackageTool() throws Exception { "abc", "question-answer", "1.0.0", rhPath, "1.0.0", SecurityJson.USER_PASS); run( - tool, new String[] { "--solr-url", solrUrl, @@ -186,7 +180,6 @@ public void testPackageTool() throws Exception { }); run( - tool, new String[] { "--solr-url", solrUrl, @@ -206,7 +199,6 @@ public void testPackageTool() throws Exception { // This command pegs the version to the latest available run( - tool, new String[] { "--solr-url", solrUrl, @@ -222,7 +214,6 @@ public void testPackageTool() throws Exception { "abc", "question-answer", "$LATEST", rhPath, "1.0.0", SecurityJson.USER_PASS); run( - tool, new String[] { "--solr-url", solrUrl, @@ -237,7 +228,6 @@ public void testPackageTool() throws Exception { log.info("Testing explicit deployment to a different/newer version"); run( - tool, new String[] { "--solr-url", solrUrl, @@ -252,7 +242,6 @@ public void testPackageTool() throws Exception { // even if parameters are not passed in, they should be picked up from previous deployment if (random().nextBoolean()) { run( - tool, new String[] { "--solr-url", solrUrl, @@ -269,7 +258,6 @@ public void testPackageTool() throws Exception { }); } else { run( - tool, new String[] { "--solr-url", solrUrl, @@ -289,7 +277,6 @@ public void testPackageTool() throws Exception { log.info("Running undeploy..."); run( - tool, new String[] { "--solr-url", solrUrl, @@ -302,7 +289,6 @@ public void testPackageTool() throws Exception { }); run( - tool, new String[] { "--solr-url", solrUrl, @@ -382,23 +368,21 @@ public void testDeployValidationMessages() throws Exception { .processAndWait(cluster.getSolrClient(), 10); CLITestHelper.TestingRuntime captureRuntime = new CLITestHelper.TestingRuntime(true); - PackageTool tool = new PackageTool(captureRuntime); // Collection exists but package does not — collection validation should pass, // package lookup should fail. - tool.runTool( - SolrCLI.processCommandLineArgs( - tool, - new String[] { - "--solr-url", - solrUrl, - "deploy", - "NONEXISTENT_PKG", - "--collections", - "validation-test", - "--credentials", - SecurityJson.USER_PASS - })); + runTool( + new String[] { + "--solr-url", + solrUrl, + "deploy", + "NONEXISTENT_PKG", + "--collections", + "validation-test", + "--credentials", + SecurityJson.USER_PASS + }, + captureRuntime); String deployOut = captureRuntime.getOutput(); assertFalse( "Should not complain about invalid collection", deployOut.contains("Invalid collection")); @@ -409,19 +393,18 @@ public void testDeployValidationMessages() throws Exception { captureRuntime.clearOutput(); // Undeploy of a package that was never deployed should give a clear message. - tool.runTool( - SolrCLI.processCommandLineArgs( - tool, - new String[] { - "--solr-url", - solrUrl, - "undeploy", - "NONEXISTENT_PKG", - "--collections", - "validation-test", - "--credentials", - SecurityJson.USER_PASS - })); + runTool( + new String[] { + "--solr-url", + solrUrl, + "undeploy", + "NONEXISTENT_PKG", + "--collections", + "validation-test", + "--credentials", + SecurityJson.USER_PASS + }, + captureRuntime); String undeployOut = captureRuntime.getOutput(); assertFalse( "Should not complain about invalid collection", undeployOut.contains("Invalid collection")); @@ -430,8 +413,8 @@ public void testDeployValidationMessages() throws Exception { undeployOut.contains("Package NONEXISTENT_PKG not deployed on collection validation-test")); } - private void run(PackageTool tool, String[] args) throws Exception { - int res = tool.runTool(SolrCLI.processCommandLineArgs(tool, args)); + private void run(String[] args) throws Exception { + int res = runTool(args, new CLITestHelper.TestingRuntime(false)); assertEquals("Non-zero status returned for: " + Arrays.toString(args), 0, res); }