aboutsummaryrefslogtreecommitdiffstats
path: root/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliArguments.java
diff options
context:
space:
mode:
Diffstat (limited to 'vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliArguments.java')
-rw-r--r--vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliArguments.java338
1 files changed, 338 insertions, 0 deletions
diff --git a/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliArguments.java b/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliArguments.java
new file mode 100644
index 00000000000..2fc7e5af7b4
--- /dev/null
+++ b/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliArguments.java
@@ -0,0 +1,338 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.feed.client.impl;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+
+import java.io.File;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.OptionalDouble;
+import java.util.OptionalInt;
+
+/**
+ * Parses command line arguments
+ *
+ * @author bjorncs
+ */
+class CliArguments {
+
+ private static final Options optionsDefinition = createOptions();
+
+ private static final String BENCHMARK_OPTION = "benchmark";
+ private static final String CA_CERTIFICATES_OPTION = "ca-certificates";
+ private static final String CERTIFICATE_OPTION = "certificate";
+ private static final String CONNECTIONS_OPTION = "connections";
+ private static final String DISABLE_SSL_HOSTNAME_VERIFICATION_OPTION = "disable-ssl-hostname-verification";
+ private static final String DRYRUN_OPTION = "dryrun";
+ private static final String ENDPOINT_OPTION = "endpoint";
+ private static final String FILE_OPTION = "file";
+ private static final String HEADER_OPTION = "header";
+ private static final String HELP_OPTION = "help";
+ private static final String MAX_STREAMS_PER_CONNECTION = "max-streams-per-connection";
+ private static final String PRIVATE_KEY_OPTION = "private-key";
+ private static final String ROUTE_OPTION = "route";
+ private static final String TIMEOUT_OPTION = "timeout";
+ private static final String TRACE_OPTION = "trace";
+ private static final String VERBOSE_OPTION = "verbose";
+ private static final String SHOW_ERRORS_OPTION = "show-errors";
+ private static final String SHOW_ALL_OPTION = "show-all";
+ private static final String SILENT_OPTION = "silent";
+ private static final String VERSION_OPTION = "version";
+ private static final String STDIN_OPTION = "stdin";
+
+ private final CommandLine arguments;
+
+ private CliArguments(CommandLine arguments) throws CliArgumentsException {
+ validateArgumentCombination(arguments);
+ this.arguments = arguments;
+ }
+
+ static CliArguments fromRawArgs(String[] rawArgs) throws CliArgumentsException {
+ CommandLineParser parser = new DefaultParser();
+ try {
+ return new CliArguments(parser.parse(optionsDefinition, rawArgs));
+ } catch (ParseException e) {
+ throw new CliArgumentsException(e);
+ }
+ }
+
+ private static void validateArgumentCombination(CommandLine args) throws CliArgumentsException {
+ if (!args.hasOption(HELP_OPTION) && !args.hasOption(VERSION_OPTION)) {
+ if (!args.hasOption(ENDPOINT_OPTION)) {
+ throw new CliArgumentsException("Endpoint must be specified");
+ }
+ if (args.hasOption(FILE_OPTION) == args.hasOption(STDIN_OPTION)) {
+ throw new CliArgumentsException(String.format("Either option '%s' or '%s' must be specified", FILE_OPTION, STDIN_OPTION));
+ }
+ if (args.hasOption(CERTIFICATE_OPTION) != args.hasOption(PRIVATE_KEY_OPTION)) {
+ throw new CliArgumentsException(
+ String.format("Both '%s' and '%s' must be specified together", CERTIFICATE_OPTION, PRIVATE_KEY_OPTION));
+ }
+ } else if (args.hasOption(HELP_OPTION) && args.hasOption(VERSION_OPTION)) {
+ throw new CliArgumentsException(String.format("Cannot specify both '%s' and '%s'", HELP_OPTION, VERSION_OPTION));
+ }
+ }
+
+ URI endpoint() throws CliArgumentsException {
+ try {
+ return ((URL) arguments.getParsedOptionValue(ENDPOINT_OPTION)).toURI();
+ } catch (ParseException | URISyntaxException e) {
+ throw new CliArgumentsException("Invalid endpoint: " + e.getMessage(), e);
+ }
+ }
+
+ boolean helpSpecified() { return has(HELP_OPTION); }
+
+ boolean versionSpecified() { return has(VERSION_OPTION); }
+
+ OptionalInt connections() throws CliArgumentsException { return intValue(CONNECTIONS_OPTION); }
+
+ OptionalInt maxStreamsPerConnection() throws CliArgumentsException { return intValue(MAX_STREAMS_PER_CONNECTION); }
+
+ Optional<CertificateAndKey> certificateAndKey() throws CliArgumentsException {
+ Path certificateFile = fileValue(CERTIFICATE_OPTION).orElse(null);
+ Path privateKeyFile = fileValue(PRIVATE_KEY_OPTION).orElse(null);
+ if (privateKeyFile == null && certificateFile == null) return Optional.empty();
+ return Optional.of(new CertificateAndKey(certificateFile, privateKeyFile));
+ }
+
+ Optional<Path> caCertificates() throws CliArgumentsException { return fileValue(CA_CERTIFICATES_OPTION); }
+
+ Optional<Path> inputFile() throws CliArgumentsException {
+ return fileValue(FILE_OPTION);
+ }
+
+ Map<String, String> headers() throws CliArgumentsException {
+ String[] rawArguments = arguments.getOptionValues(HEADER_OPTION);
+ if (rawArguments == null) return Collections.emptyMap();
+ Map<String, String> headers = new HashMap<>();
+ for (String rawArgument : rawArguments) {
+ if (rawArgument.startsWith("\"") || rawArgument.startsWith("'")) {
+ rawArgument = rawArgument.substring(1);
+ }
+ if (rawArgument.endsWith("\"") || rawArgument.endsWith("'")) {
+ rawArgument = rawArgument.substring(0, rawArgument.length() - 1);
+ }
+ int colonIndex = rawArgument.indexOf(':');
+ if (colonIndex == -1) throw new CliArgumentsException("Invalid header: '" + rawArgument + "'");
+ headers.put(rawArgument.substring(0, colonIndex), rawArgument.substring(colonIndex + 1).trim());
+ }
+ return Collections.unmodifiableMap(headers);
+ }
+
+ boolean sslHostnameVerificationDisabled() { return has(DISABLE_SSL_HOSTNAME_VERIFICATION_OPTION); }
+
+ boolean benchmarkModeEnabled() { return has(BENCHMARK_OPTION); }
+
+ boolean showProgress() { return ! has(SILENT_OPTION); }
+
+ boolean showErrors() { return has(SHOW_ERRORS_OPTION) || has(SHOW_ALL_OPTION); }
+
+ boolean showSuccesses() { return has(SHOW_ALL_OPTION); }
+
+ Optional<String> route() { return stringValue(ROUTE_OPTION); }
+
+ OptionalInt traceLevel() throws CliArgumentsException { return intValue(TRACE_OPTION); }
+
+ Optional<Duration> timeout() throws CliArgumentsException {
+ OptionalDouble timeout = doubleValue(TIMEOUT_OPTION);
+ return timeout.isPresent()
+ ? Optional.of(Duration.ofMillis((long)(timeout.getAsDouble()*1000)))
+ : Optional.empty();
+ }
+
+ boolean verboseSpecified() { return has(VERBOSE_OPTION); }
+
+ boolean readFeedFromStandardInput() { return has(STDIN_OPTION); }
+
+ boolean dryrunEnabled() { return has(DRYRUN_OPTION); }
+
+ private OptionalInt intValue(String option) throws CliArgumentsException {
+ try {
+ Number number = (Number) arguments.getParsedOptionValue(option);
+ return number != null ? OptionalInt.of(number.intValue()) : OptionalInt.empty();
+ } catch (ParseException e) {
+ throw newInvalidValueException(option, e);
+ }
+ }
+
+ private Optional<Path> fileValue(String option) throws CliArgumentsException {
+ try {
+ File certificateFile = (File) arguments.getParsedOptionValue(option);
+ if (certificateFile == null) return Optional.empty();
+ return Optional.of(certificateFile.toPath());
+ } catch (ParseException e) {
+ throw newInvalidValueException(option, e);
+ }
+ }
+
+ private Optional<String> stringValue(String option) { return Optional.ofNullable(arguments.getOptionValue(option)); }
+
+ private OptionalDouble doubleValue(String option) throws CliArgumentsException {
+ try {
+ Number number = (Number) arguments.getParsedOptionValue(option);
+ return number != null ? OptionalDouble.of(number.doubleValue()) : OptionalDouble.empty();
+ } catch (ParseException e) {
+ throw newInvalidValueException(option, e);
+ }
+ }
+
+ private boolean has(String option) { return arguments.hasOption(option); }
+
+ private static CliArgumentsException newInvalidValueException(String option, ParseException cause) {
+ return new CliArgumentsException(String.format("Invalid value for '%s': %s", option, cause.getMessage()), cause);
+ }
+
+ private static Options createOptions() {
+ return new Options()
+ .addOption(Option.builder()
+ .longOpt(HELP_OPTION)
+ .build())
+ .addOption(Option.builder()
+ .longOpt(VERSION_OPTION)
+ .build())
+ .addOption(Option.builder()
+ .longOpt(ENDPOINT_OPTION)
+ .desc("URI to feed endpoint")
+ .hasArg()
+ .type(URL.class)
+ .build())
+ .addOption(Option.builder()
+ .longOpt(HEADER_OPTION)
+ .desc("HTTP header on the form 'Name: value'")
+ .hasArgs()
+ .build())
+ .addOption(Option.builder()
+ .longOpt(FILE_OPTION)
+ .type(File.class)
+ .desc("Path to feed file in JSON format")
+ .hasArg()
+ .build())
+ .addOption(Option.builder()
+ .longOpt(CONNECTIONS_OPTION)
+ .desc("Number of concurrent HTTP/2 connections")
+ .hasArg()
+ .type(Number.class)
+ .build())
+ .addOption(Option.builder()
+ .longOpt(MAX_STREAMS_PER_CONNECTION)
+ .desc("Maximum number of concurrent streams per HTTP/2 connection")
+ .hasArg()
+ .type(Number.class)
+ .build())
+ .addOption(Option.builder()
+ .longOpt(CERTIFICATE_OPTION)
+ .desc("Path to PEM encoded X.509 certificate file")
+ .type(File.class)
+ .hasArg()
+ .build())
+ .addOption(Option.builder()
+ .longOpt(PRIVATE_KEY_OPTION)
+ .desc("Path to PEM/PKCS#8 encoded private key file")
+ .type(File.class)
+ .hasArg()
+ .build())
+ .addOption(Option.builder()
+ .longOpt(CA_CERTIFICATES_OPTION)
+ .desc("Path to file containing CA X.509 certificates encoded as PEM")
+ .type(File.class)
+ .hasArg()
+ .build())
+ .addOption(Option.builder()
+ .longOpt(DISABLE_SSL_HOSTNAME_VERIFICATION_OPTION)
+ .desc("Disable SSL hostname verification")
+ .build())
+ .addOption(Option.builder()
+ .longOpt(BENCHMARK_OPTION)
+ .desc("Enable benchmark mode")
+ .build())
+ .addOption(Option.builder()
+ .longOpt(ROUTE_OPTION)
+ .desc("Target Vespa route for feed operations")
+ .hasArg()
+ .build())
+ .addOption(Option.builder()
+ .longOpt(TIMEOUT_OPTION)
+ .desc("Feed operation timeout (in seconds)")
+ .hasArg()
+ .type(Number.class)
+ .build())
+ .addOption(Option.builder()
+ .longOpt(TRACE_OPTION)
+ .desc("The trace level of network traffic. Disabled by default (=0)")
+ .hasArg()
+ .type(Number.class)
+ .build())
+ .addOption(Option.builder()
+ .longOpt(STDIN_OPTION)
+ .desc("Read JSON input from standard input")
+ .build())
+ .addOption(Option.builder()
+ .longOpt(DRYRUN_OPTION)
+ .desc("Enable dryrun mode where each operation succeeds after " + DryrunCluster.DELAY.toMillis() + "ms")
+ .build())
+ .addOption(Option.builder()
+ .longOpt(VERBOSE_OPTION)
+ .desc("Print stack traces on errors")
+ .build())
+ .addOption(Option.builder()
+ .longOpt(SILENT_OPTION)
+ .desc("Disable periodic status printing")
+ .build())
+ .addOption(Option.builder()
+ .longOpt(SHOW_ERRORS_OPTION)
+ .desc("Print every feed operation failure")
+ .build())
+ .addOption(Option.builder()
+ .longOpt(SHOW_ALL_OPTION)
+ .desc("Print the result of every feed operation")
+ .build());
+ }
+
+ void printHelp(OutputStream out) {
+ HelpFormatter formatter = new HelpFormatter();
+ PrintWriter writer = new PrintWriter(out);
+ formatter.printHelp(
+ writer,
+ formatter.getWidth(),
+ "vespa-feed-client <options>",
+ "Vespa feed client",
+ optionsDefinition,
+ formatter.getLeftPadding(),
+ formatter.getDescPadding(),
+ "");
+ writer.flush();
+ }
+
+ static class CliArgumentsException extends Exception {
+ CliArgumentsException(String message, Throwable cause) { super(message, cause); }
+ CliArgumentsException(Throwable cause) { super(cause.getMessage(), cause); }
+ CliArgumentsException(String message) { super(message); }
+ }
+
+ static class CertificateAndKey {
+ final Path certificateFile;
+ final Path privateKeyFile;
+
+ CertificateAndKey(Path certificateFile, Path privateKeyFile) {
+ this.certificateFile = certificateFile;
+ this.privateKeyFile = privateKeyFile;
+ }
+ }
+
+}