Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion solr/core/src/java/org/apache/solr/cli/ApiTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@
*
* <p>Used to send an arbitrary HTTP request to a Solr API endpoint.
*/
@SuppressWarnings("UnnecessarilyFullyQualified")
@picocli.CommandLine.Command(
name = "api",
description = "Used to send an arbitrary HTTP GET request to a Solr API endpoint.",
footerHeading = "%nExamples:%n",
footer = {
" # Send a GET request to a Solr API endpoint",
" bin/solr api -s http://localhost:8983/api/cluster"
})
public class ApiTool extends ToolBase {

private static final Option SOLR_URL_OPTION =
Expand All @@ -48,6 +57,21 @@ public class ApiTool extends ToolBase {
/** Parameters for the api command, independent of the command line parser. */
record ApiParams(String getUrl, String credentials) {}

// --- picocli fields ---

@picocli.CommandLine.Option(
names = {"-s", "--solr-url"},
required = true,
paramLabel = "URL",
description = "Send a GET request to a Solr API endpoint.")
private String solrUrlOpt;

@picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;

public ApiTool() {
this(new DefaultToolRuntime());
}

public ApiTool(ToolRuntime runtime) {
super(runtime);
}
Expand Down Expand Up @@ -130,6 +154,8 @@ public static ModifiableSolrParams getSolrParamsFromUri(URI uri) {

@Override
public int callTool() throws Exception {
throw new UnsupportedOperationException("This tool does not yet support PicoCli");
ApiParams params = new ApiParams(solrUrlOpt, credentialsOptions.credentials);
callApi(params);
return 0;
}
}
169 changes: 168 additions & 1 deletion solr/core/src/java/org/apache/solr/cli/AssertTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@
* Supports assert command in the bin/solr script. Asserts various conditions and exists with error
* code if there are failures, else continues with no output.
*/
@SuppressWarnings("UnnecessarilyFullyQualified")
@picocli.CommandLine.Command(
name = "assert",
description =
"Asserts various conditions and exits with an error code if there are failures, else"
+ " continues with no output.",
footerHeading = "%nExamples:%n",
footer = {
" # Assert Solr is running before continuing",
" bin/solr assert --started http://localhost:8983 --timeout 5000",
"",
" # Assert we are not running as root",
" bin/solr assert --not-root"
})
public class AssertTool extends ToolBase {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private String message = null;
Expand Down Expand Up @@ -188,6 +202,109 @@ record AssertParams(
String credentials,
List<Assertion> assertions) {}

// --- picocli fields ---

static class RootOptions {
@picocli.CommandLine.Option(
names = "--root",
description = "Asserts that we are the root user.")
boolean isRoot;

@picocli.CommandLine.Option(
names = "--not-root",
description = "Asserts that we are NOT the root user.")
boolean isNotRoot;
}

static class RunningOptions {
@picocli.CommandLine.Option(
names = "--started",
paramLabel = "url",
description = "Asserts that Solr is running on a certain URL. Default timeout is 1000ms.")
String startedUrl;

@picocli.CommandLine.Option(
names = "--not-started",
paramLabel = "url",
description =
"Asserts that Solr is NOT running on a certain URL. Default timeout is 1000ms.")
String notStartedUrl;
}

static class DirectoryOptions {
@picocli.CommandLine.Option(
names = "--exists",
paramLabel = "directory",
description = "Asserts that directory <directory> exists.")
String existsDir;

@picocli.CommandLine.Option(
names = "--not-exists",
paramLabel = "directory",
description = "Asserts that directory <directory> does NOT exist.")
String notExistsDir;
}

static class CloudOptions {
@picocli.CommandLine.Option(
names = "--cloud",
paramLabel = "url",
description =
"Asserts that Solr is running in cloud mode. Also fails if Solr not running. URL"
+ " should be for root Solr path.")
String cloudUrl;

@picocli.CommandLine.Option(
names = "--not-cloud",
paramLabel = "url",
description =
"Asserts that Solr is not running in cloud mode. Also fails if Solr not running. "
+ " URL should be for root Solr path.")
String notCloudUrl;
}

@picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
private RootOptions rootOptions;

@picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
private RunningOptions runningOptions;

@picocli.CommandLine.Option(
names = "--same-user",
paramLabel = "directory",
description = "Asserts that we run as same user that owns <directory>.")
private String sameUserOpt;

@picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
private DirectoryOptions directoryOptions;

@picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
private CloudOptions cloudOptions;

@picocli.CommandLine.Option(
names = "--message",
paramLabel = "message",
description = "Exception message to be used in place of the default error message.")
private String messageOpt;

@picocli.CommandLine.Option(
names = "--timeout",
paramLabel = "ms",
defaultValue = "1000",
description = "Timeout in ms for commands supporting a timeout.")
private long timeoutOpt;

@picocli.CommandLine.Option(
names = "--exitcode",
description = "Return an exit code instead of printing error message on assert fail.")
private boolean exitCodeOpt;

@picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions;

public AssertTool() {
this(new DefaultToolRuntime());
}

public AssertTool(ToolRuntime runtime) {
super(runtime);
}
Expand Down Expand Up @@ -496,7 +613,57 @@ private static boolean runningSolrIsCloud(String url, String credentials) throws

@Override
public int callTool() throws Exception {
throw new UnsupportedOperationException("This tool does not yet support PicoCli");
List<Assertion> assertions = new ArrayList<>();
if (rootOptions != null && rootOptions.isRoot) {
assertions.add(new Assertion.RootUser());
}
if (rootOptions != null && rootOptions.isNotRoot) {
assertions.add(new Assertion.NotRootUser());
}
if (directoryOptions != null && directoryOptions.existsDir != null) {
assertions.add(new Assertion.DirExists(directoryOptions.existsDir));
}
if (directoryOptions != null && directoryOptions.notExistsDir != null) {
assertions.add(new Assertion.DirNotExists(directoryOptions.notExistsDir));
}
if (sameUserOpt != null) {
assertions.add(new Assertion.SameUser(sameUserOpt));
}
if (runningOptions != null && runningOptions.startedUrl != null) {
assertions.add(new Assertion.SolrRunning(runningOptions.startedUrl));
}
if (runningOptions != null && runningOptions.notStartedUrl != null) {
assertions.add(new Assertion.SolrNotRunning(runningOptions.notStartedUrl));
}
if (cloudOptions != null && cloudOptions.cloudUrl != null) {
assertions.add(new Assertion.CloudMode(cloudOptions.cloudUrl));
}
if (cloudOptions != null && cloudOptions.notCloudUrl != null) {
assertions.add(new Assertion.NotCloudMode(cloudOptions.notCloudUrl));
}

try {
return runAssert(
new AssertParams(
messageOpt,
timeoutOpt,
exitCodeOpt,
credentialsOptions.credentials,
List.copyOf(assertions)));
} catch (Exception exc) {
// Mirrors the commons-cli path's runTool() override: an assertion failure or other error
// with a message becomes exit code 100, not the ToolBase default of 1.
String excMsg = exc.getMessage();
if (excMsg == null) {
throw exc;
}
if (isVerbose()) {
CLIO.err("\nERROR: " + exc + "\n");
} else {
CLIO.err("\nERROR: " + excMsg + "\n");
}
return 100;
}
}

public static class AssertionFailureException extends Exception {
Expand Down
71 changes: 71 additions & 0 deletions solr/core/src/java/org/apache/solr/cli/CLIUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,77 @@ public static boolean hasConnectionOption(CommandLine cli) {
|| cli.hasOption(CommonCLIOptions.ZK_HOST_OPTION);
}

/**
* Resolves the base Solr URL for a picocli tool's {@link ConnectionOptions}, honoring whichever
* of {@code --solr-url}, {@code --solr-connection} or {@code --zk-host} was given. Mirrors {@link
* #normalizeSolrUrl(CommandLine)}, printing the same fallback warning when none was given.
*/
public static String resolveSolrUrl(ConnectionOptions connectionOptions, String credentials)
throws Exception {
if (connectionOptions != null && connectionOptions.solrUrl != null) {
return normalizeSolrUrl(connectionOptions.solrUrl);
}
String connectionString =
connectionOptions != null
? (connectionOptions.solrConnection != null
? connectionOptions.solrConnection
: connectionOptions.zkHost)
: null;
if (connectionString != null) {
return solrUrlFromConnection(
CloudSolrClient.CloudSolrClientConnection.parse(connectionString), credentials);
}
String defaultSolrUrl = 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 "
+ defaultSolrUrl
+ ".");
return defaultSolrUrl;
}

/**
* Resolves a Solr connection from a picocli tool's {@link ConnectionOptions}, mirroring {@link
* #getSolrConnection(CommandLine)}: an explicit {@code --solr-connection} or {@code --zk-host}
* wins outright, otherwise a running Solr instance (found via {@code --solr-url} or the default
* URL) is queried to see if it reports a ZooKeeper connection (SolrCloud mode), returning {@code
* null} if it does not.
*/
public static CloudSolrClient.CloudSolrClientConnection resolveSolrConnection(
ConnectionOptions connectionOptions, String credentials) throws Exception {
if (connectionOptions != null && connectionOptions.solrConnection != null) {
return CloudSolrClient.CloudSolrClientConnection.parse(connectionOptions.solrConnection);
}
if (connectionOptions != null && connectionOptions.zkHost != null) {
var zkSolrConnection =
CloudSolrClient.CloudSolrClientConnection.parse(connectionOptions.zkHost);
if (!zkSolrConnection.isZookeeper()) {
throw new IOException(
String.format(
Locale.ROOT,
"Expected ZooKeeper connection string, but got: '%s'.",
connectionOptions.zkHost));
}
return zkSolrConnection;
}
String resolvedSolrUrl =
(connectionOptions != null && connectionOptions.solrUrl != null)
? normalizeSolrUrl(connectionOptions.solrUrl)
: getDefaultSolrUrl();
try (SolrClient solrClient = getSolrClient(resolvedSolrUrl, credentials)) {
Map<String, Object> status = StatusTool.reportStatus(solrClient);
@SuppressWarnings("unchecked")
Map<String, Object> cloud = (Map<String, Object>) status.get("cloud");
if (cloud == null) {
return null;
}
String zookeeper = (String) cloud.get("ZooKeeper");
if (zookeeper.endsWith("(embedded)")) {
zookeeper = zookeeper.substring(0, zookeeper.length() - "(embedded)".length());
}
return CloudSolrClient.CloudSolrClientConnection.parse(zookeeper);
}
}

/**
* Get the value of the specified CLI option with fallback to system property and default value.
*
Expand Down
Loading
Loading