diff --git a/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java b/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java new file mode 100644 index 000000000000..b34d81f4c6a2 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java @@ -0,0 +1,39 @@ +/* + * 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; + +/** + * Picocli ArgGroup for mutually-exclusive Solr URL / ZooKeeper connection options. + * + *

Use as the type of an {@code @ArgGroup(exclusive = true, multiplicity = "0..1")} field to + * ensure the user provides at most one of {@code --solr-url} or {@code --zk-host}. + */ +class ConnectionOptions { + @picocli.CommandLine.Option( + names = {"-s", "--solr-url"}, + description = + "Base Solr URL, which can be used to determine the zk-host if that's not known.") + String solrUrl; + + @picocli.CommandLine.Option( + names = {"-z", "--zk-host"}, + description = + "Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to " + + CommonCLIOptions.DefaultValues.ZK_HOST + + ".") + String zkHost; +} diff --git a/solr/core/src/java/org/apache/solr/cli/CreateTool.java b/solr/core/src/java/org/apache/solr/cli/CreateTool.java index 1f81609f0b78..ed1995d94345 100644 --- a/solr/core/src/java/org/apache/solr/cli/CreateTool.java +++ b/solr/core/src/java/org/apache/solr/cli/CreateTool.java @@ -21,6 +21,7 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import org.apache.commons.cli.CommandLine; @@ -38,9 +39,15 @@ import org.apache.solr.client.solrj.response.SystemInfoResponse; import org.apache.solr.cloud.ZkConfigSetService; import org.apache.solr.common.cloud.ZkStateReader; +import org.apache.solr.common.util.EnvUtils; import org.apache.solr.core.ConfigSetService; /** Supports create command in the bin/solr script. */ +@picocli.CommandLine.Command( + name = "create", + mixinStandardHelpOptions = true, + description = + "Creates a core or collection depending on whether Solr is running in standalone (core) or SolrCloud mode (collection).") public class CreateTool extends ToolBase { private static final Option COLLECTION_NAME_OPTION = @@ -90,6 +97,60 @@ public class CreateTool extends ToolBase { .desc("Configuration name; default is the collection name.") .get(); + /** Options bean shared between commons-cli and picocli paths. */ + record CreateParams( + String name, + String confDir, + String confName, + String solrUrl, + String credentials, + int shards, + int replicationFactor) {} + + // --- 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, + description = "Name of collection or core to create.") + private String name; + + @picocli.CommandLine.Option( + names = {"-sh", "--shards"}, + description = "Number of shards; default is 1.", + defaultValue = "1") + private int shards; + + @picocli.CommandLine.Option( + names = {"-rf", "--replication-factor"}, + description = + "Number of copies of each document across the collection (replicas per shard); default is 1.", + defaultValue = "1") + private int replicationFactor; + + @picocli.CommandLine.Option( + names = {"-d", "--conf-dir"}, + description = + "Configuration directory to copy when creating the new collection; default is " + + DefaultValues.DEFAULT_CONFIG_SET + + ".", + defaultValue = DefaultValues.DEFAULT_CONFIG_SET) + private String confDir; + + @picocli.CommandLine.Option( + names = {"-n", "--conf-name"}, + description = "Configuration name; default is the collection name.") + private String confName; + + public CreateTool() { + this(new DefaultToolRuntime()); + } + public CreateTool(ToolRuntime runtime) { super(runtime); } @@ -123,45 +184,45 @@ public Options getOptions() { @Override public void runImpl(CommandLine cli) throws Exception { try (var solrClient = CLIUtils.getSolrClient(cli)) { + CreateParams params = + new CreateParams( + cli.getOptionValue(COLLECTION_NAME_OPTION), + cli.getOptionValue(CONF_DIR_OPTION, DefaultValues.DEFAULT_CONFIG_SET), + cli.getOptionValue(CONF_NAME_OPTION), + cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION, CLIUtils.getDefaultSolrUrl()), + cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION), + cli.getParsedOptionValue(SHARDS_OPTION, 1), + cli.getParsedOptionValue(REPLICATION_FACTOR_OPTION, 1)); if (CLIUtils.isCloudMode(solrClient)) { - createCollection(cli); + createCollection(CLIUtils.getZkHost(cli), params); } else { - createCore(cli, solrClient); + createCore(params, solrClient); } } } - protected void createCore(CommandLine cli, SolrClient solrClient) throws Exception { - String coreName = cli.getOptionValue(COLLECTION_NAME_OPTION); - String solrUrl = - cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION, CLIUtils.getDefaultSolrUrl()); - - final String solrInstallDir = System.getProperty("solr.install.dir"); - final String confDirName = - cli.getOptionValue(CONF_DIR_OPTION, DefaultValues.DEFAULT_CONFIG_SET); - + private void createCore(CreateParams params, SolrClient solrClient) throws Exception { // we allow them to pass a directory instead of a configset name - Path configsetDir = Path.of(confDirName); - Path solrInstallDirPath = Path.of(solrInstallDir); + Path configsetDir = Path.of(params.confDir); + Path solrInstallDirPath = Path.of(System.getProperty("solr.install.dir")); if (!Files.isDirectory(configsetDir)) { ensureConfDirExists(solrInstallDirPath, configsetDir); } - printDefaultConfigsetWarningIfNecessary(cli); + printDefaultConfigsetWarning(params); SystemInfoResponse sysResponse = (new SystemInfoRequest()).process(solrClient); // usually same as solr home, but not always String coreRootDirectory = sysResponse.getCoreRoot(); - if (CLIUtils.safeCheckCoreExists( - solrUrl, coreName, cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) { + if (CLIUtils.safeCheckCoreExists(params.solrUrl, params.name, params.credentials)) { throw new IllegalArgumentException( "\nCore '" - + coreName + + params.name + "' already exists!\nChecked core existence using Core API command"); } - Path coreInstanceDir = Path.of(coreRootDirectory, coreName); + Path coreInstanceDir = Path.of(coreRootDirectory, params.name); Path confDir = getFullConfDir(solrInstallDirPath, configsetDir).resolve("conf"); if (!Files.isDirectory(coreInstanceDir)) { Files.createDirectories(coreInstanceDir); @@ -177,14 +238,14 @@ protected void createCore(CommandLine cli, SolrClient solrClient) throws Excepti + coreInstanceDir.toAbsolutePath()); } - echoIfVerbose("\nCreating new core '" + coreName + "' using V2 Cores API"); + echoIfVerbose("\nCreating new core '" + params.name + "' using V2 Cores API"); try { var req = new CoresApi.CreateCore(); - req.setName(coreName); - req.setInstanceDir(coreName); + req.setName(params.name); + req.setInstanceDir(params.name); req.process(solrClient); - echo(String.format(Locale.ROOT, "\nCreated new core '%s'", coreName)); + echo(String.format(Locale.ROOT, "\nCreated new core '%s'", params.name)); } catch (Exception e) { /* create-core failed, cleanup the copied configset before propagating the error. */ @@ -193,32 +254,26 @@ protected void createCore(CommandLine cli, SolrClient solrClient) throws Excepti } } - protected void createCollection(CommandLine cli) throws Exception { + private void createCollection(String zkHost, CreateParams params) throws Exception { var builder = new HttpJettySolrClient.Builder() .withIdleTimeout(30, TimeUnit.SECONDS) .withConnectionTimeout(15, TimeUnit.SECONDS) .withKeyStoreReloadInterval(-1, TimeUnit.SECONDS) - .withOptionalBasicAuthCredentials( - cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION)); - String zkHost = CLIUtils.getZkHost(cli); + .withOptionalBasicAuthCredentials(params.credentials); echoIfVerbose("Connecting to ZooKeeper at " + zkHost); try (CloudSolrClient cloudSolrClient = CLIUtils.getCloudSolrClient(zkHost, builder)) { - createCollection(cloudSolrClient, cli); + createCollection(cloudSolrClient, params); } } - protected void createCollection(CloudSolrClient cloudSolrClient, CommandLine cli) + private void createCollection(CloudSolrClient cloudSolrClient, CreateParams params) throws Exception { - String collectionName = cli.getOptionValue(COLLECTION_NAME_OPTION); - final String solrInstallDir = System.getProperty("solr.install.dir"); - String confName = cli.getOptionValue(CONF_NAME_OPTION); - String confDir = cli.getOptionValue(CONF_DIR_OPTION, DefaultValues.DEFAULT_CONFIG_SET); - Path solrInstallDirPath = Path.of(solrInstallDir); - Path confDirPath = Path.of(confDir); + Path solrInstallDirPath = Path.of(System.getProperty("solr.install.dir")); + Path confDirPath = Path.of(params.confDir); ensureConfDirExists(solrInstallDirPath, confDirPath); - printDefaultConfigsetWarningIfNecessary(cli); + printDefaultConfigsetWarning(params); Set liveNodes = cloudSolrClient.getClusterState().getLiveNodes(); if (liveNodes.isEmpty()) @@ -226,15 +281,16 @@ protected void createCollection(CloudSolrClient cloudSolrClient, CommandLine cli "No live nodes found! Cannot create a collection until " + "there is at least 1 live node in the cluster."); - String solrUrl = cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION); + String solrUrl = params.solrUrl; if (solrUrl == null) { String firstLiveNode = liveNodes.iterator().next(); solrUrl = ZkStateReader.from(cloudSolrClient).getBaseUrlForNodeName(firstLiveNode); } // build a URL to create the collection - int numShards = cli.getParsedOptionValue(SHARDS_OPTION, 1); - int replicationFactor = cli.getParsedOptionValue(REPLICATION_FACTOR_OPTION, 1); + int numShards = params.shards; + int replicationFactor = params.replicationFactor; + String confName = params.confName; boolean configExistsInZk = confName != null @@ -245,14 +301,15 @@ protected void createCollection(CloudSolrClient cloudSolrClient, CommandLine cli echo("Re-using existing configuration directory " + confName); } else { // if (confdir != null && !confdir.trim().isEmpty()) { if (confName == null || confName.trim().isEmpty()) { - confName = collectionName; + confName = params.name; } // TODO: This should be done using the configSet API final Path configsetsDirPath = CLIUtils.getConfigSetsDir(solrInstallDirPath); ConfigSetService configSetService = new ZkConfigSetService(ZkStateReader.from(cloudSolrClient).getZkClient()); - Path confPath = ConfigSetService.getConfigsetPath(confDir, configsetsDirPath.toString()); + Path confPath = + ConfigSetService.getConfigsetPath(params.confDir, configsetsDirPath.toString()); echoIfVerbose( "Uploading " @@ -266,20 +323,19 @@ protected void createCollection(CloudSolrClient cloudSolrClient, CommandLine cli } // since creating a collection is a heavy-weight operation, check for existence first - if (CLIUtils.safeCheckCollectionExists( - solrUrl, collectionName, cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) { + if (CLIUtils.safeCheckCollectionExists(solrUrl, params.name, params.credentials)) { throw new IllegalStateException( "\nCollection '" - + collectionName + + params.name + "' already exists!\nChecked collection existence using V2 Collections API"); } // doesn't seem to exist ... try to create - echoIfVerbose("\nCreating new collection '" + collectionName + "' using V2 Collections API"); + echoIfVerbose("\nCreating new collection '" + params.name + "' using V2 Collections API"); try { var req = new CollectionsApi.CreateCollection(); - req.setName(collectionName); + req.setName(params.name); req.setConfig(confName); req.setNumShards(numShards); req.setReplicationFactor(replicationFactor); @@ -287,14 +343,14 @@ protected void createCollection(CloudSolrClient cloudSolrClient, CommandLine cli echoIfVerbose(response); } catch (SolrServerException sse) { throw new Exception( - "Failed to create collection '" + collectionName + "' due to: " + sse.getMessage()); + "Failed to create collection '" + params.name + "' due to: " + sse.getMessage()); } String endMessage = String.format( Locale.ROOT, "Created collection '%s' with %d shard(s), %d replica(s)", - collectionName, + params.name, numShards, replicationFactor); if (confName != null && !confName.trim().isEmpty()) { @@ -319,16 +375,18 @@ private void ensureConfDirExists(Path solrInstallDir, Path confDirName) { } } - private void printDefaultConfigsetWarningIfNecessary(CommandLine cli) { - final String confDirectoryName = - cli.getOptionValue(CONF_DIR_OPTION, DefaultValues.DEFAULT_CONFIG_SET); - final String confName = cli.getOptionValue(CONF_NAME_OPTION, ""); + private void printDefaultConfigsetWarning(CreateParams params) { + printDefaultConfigsetWarning( + params.confDir, + params.confName != null ? params.confName : "", + params.name, + params.solrUrl != null ? params.solrUrl : CLIUtils.getDefaultSolrUrl()); + } - if (confDirectoryName.equals("_default") - && (confName.isEmpty() || confName.equals("_default"))) { - final String collectionName = cli.getOptionValue(COLLECTION_NAME_OPTION); - final String solrUrl = - cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION, CLIUtils.getDefaultSolrUrl()); + private void printDefaultConfigsetWarning( + String confDirName, String confNameArg, String collectionName, String solrUrl) { + if (confDirName.equals("_default") + && (confNameArg.isEmpty() || confNameArg.equals("_default"))) { final String curlCommand = String.format( Locale.ROOT, @@ -355,6 +413,57 @@ private void printDefaultConfigsetWarningIfNecessary(CommandLine cli) { @Override public int callTool() throws Exception { - throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + String zkHostArg = + (connectionOptions != null) ? connectionOptions.zkHost : EnvUtils.getProperty("zkHost"); + String solrUrlArg = (connectionOptions != null) ? connectionOptions.solrUrl : null; + + if (zkHostArg != null) { + CreateParams params = + new CreateParams( + name, + confDir, + confName, + null, + credentialsOptions.credentials, + shards, + replicationFactor); + createCollection(zkHostArg, params); + } else { + String resolvedSolrUrl; + if (solrUrlArg != null) { + resolvedSolrUrl = CLIUtils.normalizeSolrUrl(solrUrlArg); + } else { + resolvedSolrUrl = CLIUtils.getDefaultSolrUrl(); + CLIO.err( + "Neither --zk-host or --solr-url parameters, nor ZK_HOST env var provided, so assuming solr url is " + + resolvedSolrUrl + + "."); + } + CreateParams params = + new CreateParams( + name, + confDir, + confName, + resolvedSolrUrl, + credentialsOptions.credentials, + shards, + replicationFactor); + try (var solrClient = + CLIUtils.getSolrClient(resolvedSolrUrl, credentialsOptions.credentials)) { + Map status = StatusTool.reportStatus(solrClient); + @SuppressWarnings("unchecked") + Map cloud = (Map) status.get("cloud"); + if (cloud != null) { + String zookeeper = (String) cloud.get("ZooKeeper"); + if (zookeeper != null && zookeeper.endsWith("(embedded)")) { + zookeeper = zookeeper.substring(0, zookeeper.length() - "(embedded)".length()); + } + createCollection(zookeeper, params); + } else { + createCore(params, solrClient); + } + } + } + return 0; } } diff --git a/solr/core/src/java/org/apache/solr/cli/CredentialsOptions.java b/solr/core/src/java/org/apache/solr/cli/CredentialsOptions.java new file mode 100644 index 000000000000..53bc24ca8b34 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/cli/CredentialsOptions.java @@ -0,0 +1,30 @@ +/* + * 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; + +/** + * Picocli mixin providing the common {@code --credentials} option. + * + *

Use as {@code @CommandLine.Mixin CredentialsOptions credentialsOptions} in a command class. + */ +public class CredentialsOptions { + @picocli.CommandLine.Option( + names = {"-u", "--credentials"}, + description = + "Credentials in the format username:password. Example: --credentials solr:SolrRocks") + public String credentials; +} diff --git a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java index 07f92894f311..25a49e4e82b3 100644 --- a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java +++ b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java @@ -19,6 +19,7 @@ import java.lang.invoke.MethodHandles; import java.util.Collection; import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -32,10 +33,16 @@ import org.apache.solr.client.solrj.request.CollectionsApi; import org.apache.solr.client.solrj.request.CoresApi; import org.apache.solr.common.cloud.ZkStateReader; +import org.apache.solr.common.util.EnvUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** Supports delete command in the bin/solr script. */ +@picocli.CommandLine.Command( + name = "delete", + mixinStandardHelpOptions = true, + description = + "Deletes a collection or core depending on whether Solr is running in SolrCloud or standalone mode.") public class DeleteTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -52,7 +59,7 @@ public class DeleteTool extends ToolBase { Option.builder() .longOpt("delete-config") .desc( - "Flag to indicate if the underlying configuration directory for a collection should also be deleted; default is true.") + "Flag to indicate if the underlying configuration directory for a collection should also be deleted; default is false.") .get(); private static final Option FORCE_OPTION = @@ -62,6 +69,38 @@ public class DeleteTool extends ToolBase { "Skip safety checks when deleting the configuration directory used by a collection.") .get(); + /** Options bean shared between commons-cli and picocli paths. */ + record DeleteParams(String name, String credentials, boolean deleteConfig, boolean force) {} + + // --- 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, + description = "Name of the core / collection to delete.") + private String name; + + @picocli.CommandLine.Option( + names = {"--delete-config"}, + description = + "Flag to indicate if the underlying configuration directory for a collection should also be deleted; default is false.") + private boolean deleteConfig; + + @picocli.CommandLine.Option( + names = {"-f", "--force"}, + description = + "Skip safety checks when deleting the configuration directory used by a collection.") + private boolean force; + + public DeleteTool() { + this(new DefaultToolRuntime()); + } + public DeleteTool(ToolRuntime runtime) { super(runtime); } @@ -93,31 +132,34 @@ public Options getOptions() { @Override public void runImpl(CommandLine cli) throws Exception { try (var solrClient = CLIUtils.getSolrClient(cli)) { + DeleteParams params = + new DeleteParams( + cli.getOptionValue(COLLECTION_NAME_OPTION), + cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION), + cli.hasOption(DELETE_CONFIG_OPTION), + cli.hasOption(FORCE_OPTION)); if (CLIUtils.isCloudMode(solrClient)) { - deleteCollection(cli); + deleteCollection(CLIUtils.getZkHost(cli), params); } else { - deleteCore(cli, solrClient); + deleteCore(params, solrClient); } } } - protected void deleteCollection(CommandLine cli) throws Exception { + private void deleteCollection(String zkHost, DeleteParams params) throws Exception { var builder = new HttpJettySolrClient.Builder() .withIdleTimeout(30, TimeUnit.SECONDS) .withConnectionTimeout(15, TimeUnit.SECONDS) .withKeyStoreReloadInterval(-1, TimeUnit.SECONDS) - .withOptionalBasicAuthCredentials( - cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION)); - - String zkHost = CLIUtils.getZkHost(cli); + .withOptionalBasicAuthCredentials(params.credentials); + echoIfVerbose("Connecting to ZooKeeper at " + zkHost); try (CloudSolrClient cloudSolrClient = CLIUtils.getCloudSolrClient(zkHost, builder)) { - echoIfVerbose("Connecting to ZooKeeper at " + zkHost); - deleteCollection(cloudSolrClient, cli); + deleteCollection(cloudSolrClient, params); } } - protected void deleteCollection(CloudSolrClient cloudSolrClient, CommandLine cli) + private void deleteCollection(CloudSolrClient cloudSolrClient, DeleteParams params) throws Exception { Set liveNodes = cloudSolrClient.getClusterState().getLiveNodes(); if (liveNodes.isEmpty()) @@ -126,17 +168,15 @@ protected void deleteCollection(CloudSolrClient cloudSolrClient, CommandLine cli + "there is at least 1 live node in the cluster."); ZkStateReader zkStateReader = ZkStateReader.from(cloudSolrClient); - String collectionName = cli.getOptionValue(COLLECTION_NAME_OPTION); - if (!zkStateReader.getClusterState().hasCollection(collectionName)) { - throw new IllegalArgumentException("Collection " + collectionName + " not found!"); + if (!zkStateReader.getClusterState().hasCollection(params.name)) { + throw new IllegalArgumentException("Collection " + params.name + " not found!"); } - String configName = - zkStateReader.getClusterState().getCollection(collectionName).getConfigName(); - boolean deleteConfig = cli.hasOption(DELETE_CONFIG_OPTION); + String configName = zkStateReader.getClusterState().getCollection(params.name).getConfigName(); + boolean effectiveDeleteConfig = params.deleteConfig; - if (deleteConfig && configName != null) { - if (cli.hasOption(FORCE_OPTION)) { + if (effectiveDeleteConfig && configName != null) { + if (params.force) { log.warn( "Skipping safety checks, configuration directory {} will be deleted with impunity.", configName); @@ -155,35 +195,35 @@ protected void deleteCollection(CloudSolrClient cloudSolrClient, CommandLine cli Optional inUse = collections.stream() - .filter(name -> !name.equals(collectionName)) // ignore this collection + .filter(n -> !n.equals(params.name)) // ignore this collection .filter( - name -> + n -> configName.equals( - zkStateReader.getClusterState().getCollection(name).getConfigName())) + zkStateReader.getClusterState().getCollection(n).getConfigName())) .findFirst(); if (inUse.isPresent()) { - deleteConfig = false; + effectiveDeleteConfig = false; log.warn( "Configuration directory {} is also being used by {}{}", configName, inUse.get(), - "; configuration will not be deleted from ZooKeeper. You can pass the --force-delete-config flag to force delete."); + "; configuration will not be deleted from ZooKeeper. You can pass the --force flag to force delete."); } } } - echoIfVerbose("\nDeleting collection '" + collectionName + "' using V2 Collections API"); + echoIfVerbose("\nDeleting collection '" + params.name + "' using V2 Collections API"); try { - var req = new CollectionsApi.DeleteCollection(collectionName); + var req = new CollectionsApi.DeleteCollection(params.name); var response = req.process(cloudSolrClient); echoIfVerbose(response); } catch (SolrServerException sse) { throw new Exception( - "Failed to delete collection '" + collectionName + "' due to: " + sse.getMessage()); + "Failed to delete collection '" + params.name + "' due to: " + sse.getMessage()); } - if (deleteConfig) { + if (effectiveDeleteConfig) { String configZnode = "/configs/" + configName; try { zkStateReader.getZkClient().clean(configZnode); @@ -197,28 +237,62 @@ protected void deleteCollection(CloudSolrClient cloudSolrClient, CommandLine cli } } - echo(String.format(Locale.ROOT, "\nDeleted collection '%s'", collectionName)); + echo(String.format(Locale.ROOT, "\nDeleted collection '%s'", params.name)); } - protected void deleteCore(CommandLine cli, SolrClient solrClient) throws Exception { - String coreName = cli.getOptionValue(COLLECTION_NAME_OPTION); - - echo("\nDeleting core '" + coreName + "' using V2 Cores API\n"); + private void deleteCore(DeleteParams params, SolrClient solrClient) throws Exception { + echo("\nDeleting core '" + params.name + "' using V2 Cores API\n"); try { - var req = new CoresApi.UnloadCore(coreName); + var req = new CoresApi.UnloadCore(params.name); req.setDeleteIndex(true); req.setDeleteDataDir(true); req.setDeleteInstanceDir(true); var response = req.process(solrClient); echoIfVerbose(response); } catch (SolrServerException sse) { - throw new Exception("Failed to delete core '" + coreName + "' due to: " + sse.getMessage()); + throw new Exception( + "Failed to delete core '" + params.name + "' due to: " + sse.getMessage()); } } @Override public int callTool() throws Exception { - throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + String zkHostArg = + (connectionOptions != null) ? connectionOptions.zkHost : EnvUtils.getProperty("zkHost"); + String solrUrlArg = (connectionOptions != null) ? connectionOptions.solrUrl : null; + DeleteParams params = + new DeleteParams(name, credentialsOptions.credentials, deleteConfig, force); + + if (zkHostArg != null) { + deleteCollection(zkHostArg, params); + } else { + String resolvedSolrUrl; + if (solrUrlArg != null) { + resolvedSolrUrl = CLIUtils.normalizeSolrUrl(solrUrlArg); + } else { + resolvedSolrUrl = CLIUtils.getDefaultSolrUrl(); + CLIO.err( + "Neither --zk-host or --solr-url parameters, nor ZK_HOST env var provided, so assuming solr url is " + + resolvedSolrUrl + + "."); + } + try (var solrClient = + CLIUtils.getSolrClient(resolvedSolrUrl, credentialsOptions.credentials)) { + Map status = StatusTool.reportStatus(solrClient); + @SuppressWarnings("unchecked") + Map cloud = (Map) status.get("cloud"); + if (cloud != null) { + String zookeeper = (String) cloud.get("ZooKeeper"); + if (zookeeper != null && zookeeper.endsWith("(embedded)")) { + zookeeper = zookeeper.substring(0, zookeeper.length() - "(embedded)".length()); + } + deleteCollection(zookeeper, params); + } else { + deleteCore(params, solrClient); + } + } + } + return 0; } } 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 6a6828161ff9..bdf97a9b960b 100755 --- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java +++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java @@ -78,7 +78,9 @@ StopCommand.class, StatusTool.class, VersionTool.class, - ZkTool.class + ZkTool.class, + CreateTool.class, + DeleteTool.class }) public class SolrCLI implements CLIO { diff --git a/solr/core/src/java/org/apache/solr/cli/ToolBase.java b/solr/core/src/java/org/apache/solr/cli/ToolBase.java index 8b3d4304cf3c..467c7b71583a 100644 --- a/solr/core/src/java/org/apache/solr/cli/ToolBase.java +++ b/solr/core/src/java/org/apache/solr/cli/ToolBase.java @@ -19,14 +19,14 @@ import com.fasterxml.jackson.core.JsonProcessingException; import java.util.concurrent.Callable; -import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.OptionGroup; import org.apache.commons.cli.Options; import org.apache.solr.client.solrj.request.json.JacksonContentWriter; import org.apache.solr.util.StartupLoggingUtils; +import picocli.CommandLine; public abstract class ToolBase implements Tool, Callable { - @picocli.CommandLine.Option( + @CommandLine.Option( names = {"-v", "--verbose"}, description = "Enable verbose mode.") private boolean verbose = false; @@ -87,7 +87,7 @@ public OptionGroup getConnectionOptions() { } @Override - public int runTool(CommandLine cli) throws Exception { + public int runTool(org.apache.commons.cli.CommandLine cli) throws Exception { verbose = cli.hasOption(CommonCLIOptions.VERBOSE_OPTION); raiseLogLevelUnlessVerbose(); @@ -117,7 +117,7 @@ private void raiseLogLevelUnlessVerbose() { } @Deprecated - public abstract void runImpl(CommandLine cli) throws Exception; + public abstract void runImpl(org.apache.commons.cli.CommandLine cli) throws Exception; /** * Called by picocli to execute the tool's logic. Each tool must implement this method to support diff --git a/solr/packaging/test/test_create.bats b/solr/packaging/test/test_create.bats index e9199358253e..10b934dc523d 100644 --- a/solr/packaging/test/test_create.bats +++ b/solr/packaging/test/test_create.bats @@ -42,5 +42,5 @@ teardown() { @test "multiple connection options are prevented" { run solr create -c COLL_NAME2 --solr-url http://localhost:${SOLR_PORT} -z localhost:${ZK_PORT} - assert_output --partial "The option 'z' was specified but an option from this group has already been selected: 's'" + assert_output --regexp "(mutually exclusive|already been selected).*(--solr-url|--zk-host|-z|-s)" }