args);
-
- /**
- * Parse a list of kyuubi-ctl command line options.
- *
- * See KyuubiCtlArguments.scala for a more formal description of available options.
- *
- * @throws IllegalArgumentException If an error is found during parsing.
- */
- protected final void parse(List args) {
- Pattern eqSeparatedOpt = Pattern.compile("(--[^=]+)=(.+)");
-
- int idx = parseActionAndService(args);
- for (; idx < args.size(); idx++) {
- String arg = args.get(idx);
- String value = null;
-
- Matcher m = eqSeparatedOpt.matcher(arg);
- if (m.matches()) {
- arg = m.group(1);
- value = m.group(2);
- }
-
- // Look for options with a value.
- String name = findCliOption(arg, opts);
- if (name != null) {
- if (value == null) {
- if (idx == args.size() - 1) {
- throw new IllegalArgumentException(
- String.format("Missing argument for option '%s'.", arg));
- }
- idx++;
- value = args.get(idx);
- }
- if (!handle(name, value)) {
- break;
- }
- continue;
- }
-
- // Look for a switch.
- name = findCliOption(arg, switches);
- if (name != null) {
- if (!handle(name, null)) {
- break;
- }
- continue;
- }
-
- handleUnknown(arg);
- }
- }
-
- /**
- * Callback for when an option with an argument is parsed.
- *
- * @param opt The long name of the cli option (might differ from actual command line).
- * @param value The value. This will be null if the option does not take a value.
- * @return Whether to continue parsing the argument list.
- */
- protected abstract boolean handle(String opt, String value);
-
- /**
- * Callback for when an unrecognized option is parsed.
- *
- * @param opt Unrecognized option from the command line.
- * @return Whether to continue parsing the argument list.
- */
- protected abstract boolean handleUnknown(String opt);
-
- private String findCliOption(String name, String[][] available) {
- for (String[] candidates : available) {
- for (String candidate : candidates) {
- if (candidate.equals(name)) {
- return candidates[0];
- }
- }
- }
- return null;
- }
-
- protected String findSwitches(String name) {
- return findCliOption(name, switches);
- }
-}
diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/CommandLineUtils.scala b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/CommandLineUtils.scala
index 57244182f..ea660ca1c 100644
--- a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/CommandLineUtils.scala
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/CommandLineUtils.scala
@@ -75,7 +75,7 @@ private[kyuubi] object Tabulator {
}
def format(title: String, header: Seq[String], rows: Seq[Seq[String]],
- verbose: Boolean): String = {
+ verbose: Boolean): String = {
val data = if (verbose) Seq(header).union(rows) else rows
val sb = new StringBuilder
val numCols = header.size
diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/KyuubiOEffectSetup.scala b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/KyuubiOEffectSetup.scala
new file mode 100644
index 000000000..d5f1e09d1
--- /dev/null
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/KyuubiOEffectSetup.scala
@@ -0,0 +1,29 @@
+/*
+ * 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.kyuubi.ctl
+
+import scopt.DefaultOEffectSetup
+
+import org.apache.kyuubi.Logging
+
+class KyuubiOEffectSetup extends DefaultOEffectSetup with Logging {
+ override def displayToOut(msg: String): Unit = info(msg)
+ override def displayToErr(msg: String): Unit = error(msg)
+ override def reportError(msg: String): Unit = info(msg)
+ override def reportWarning(msg: String): Unit = warn(msg)
+}
diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCli.scala b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCli.scala
index ddfd37fa2..b870f0977 100644
--- a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCli.scala
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCli.scala
@@ -29,7 +29,7 @@ import org.apache.kyuubi.ha.client.{ServiceDiscovery, ServiceNodeInfo}
private[ctl] object ServiceControlAction extends Enumeration {
type ServiceControlAction = Value
- val CREATE, GET, DELETE, LIST, HELP = Value
+ val CREATE, GET, DELETE, LIST = Value
}
private[ctl] object ServiceControlObject extends Enumeration {
@@ -39,7 +39,6 @@ private[ctl] object ServiceControlObject extends Enumeration {
/**
* Main gateway of launching a Kyuubi Ctl action.
- * See usage in [[ServiceControlCliArguments.printUsageAndExit]].
*/
private[kyuubi] class ServiceControlCli extends Logging {
import ServiceControlCli._
@@ -53,16 +52,21 @@ private[kyuubi] class ServiceControlCli extends Logging {
initializeLoggerIfNecessary(true)
val ctlArgs = parseArguments(args)
- verbose = ctlArgs.verbose
+
+ // when parse failed, exit
+ if (ctlArgs.cliArgs == null) {
+ sys.exit(1)
+ }
+
+ verbose = ctlArgs.cliArgs.verbose
if (verbose) {
super.info(ctlArgs.toString)
}
- ctlArgs.action match {
+ ctlArgs.cliArgs.action match {
case ServiceControlAction.CREATE => create(ctlArgs)
case ServiceControlAction.LIST => list(ctlArgs, filterHostPort = false)
case ServiceControlAction.GET => list(ctlArgs, filterHostPort = true)
case ServiceControlAction.DELETE => delete(ctlArgs)
- case ServiceControlAction.HELP => printUsage(ctlArgs)
}
}
@@ -76,7 +80,7 @@ private[kyuubi] class ServiceControlCli extends Logging {
private def create(args: ServiceControlCliArguments): Unit = {
val kyuubiConf = args.conf
- kyuubiConf.setIfMissing(HA_ZK_QUORUM, args.zkQuorum)
+ kyuubiConf.setIfMissing(HA_ZK_QUORUM, args.cliArgs.zkQuorum)
withZkClient(kyuubiConf) { zkClient =>
val fromNamespace = ZKPaths.makePath(null, kyuubiConf.get(HA_ZK_NAMESPACE))
val toNamespace = getZkNamespace(args)
@@ -90,17 +94,17 @@ private[kyuubi] class ServiceControlCli extends Logging {
info(s"Exposing server instance:${sn.instance} with version:${sn.version}" +
s" from $fromNamespace to $toNamespace")
val newNode = createZkServiceNode(
- kyuubiConf, zc, args.namespace, sn.instance, sn.version, true)
+ kyuubiConf, zc, args.cliArgs.namespace, sn.instance, sn.version, true)
exposedServiceNodes += sn.copy(
namespace = toNamespace,
nodeName = newNode.getActualPath.split("/").last)
}
}
- if (kyuubiConf.get(HA_ZK_QUORUM) == args.zkQuorum) {
+ if (kyuubiConf.get(HA_ZK_QUORUM) == args.cliArgs.zkQuorum) {
doCreate(zkClient)
} else {
- kyuubiConf.set(HA_ZK_QUORUM, args.zkQuorum)
+ kyuubiConf.set(HA_ZK_QUORUM, args.cliArgs.zkQuorum)
withZkClient(kyuubiConf)(doCreate)
}
}
@@ -116,7 +120,9 @@ private[kyuubi] class ServiceControlCli extends Logging {
private def list(args: ServiceControlCliArguments, filterHostPort: Boolean): Unit = {
withZkClient(args.conf) { zkClient =>
val znodeRoot = getZkNamespace(args)
- val hostPortOpt = if (filterHostPort) Some((args.host, args.port.toInt)) else None
+ val hostPortOpt = if (filterHostPort) {
+ Some((args.cliArgs.host, args.cliArgs.port.toInt))
+ } else None
val nodes = getServiceNodes(zkClient, znodeRoot, hostPortOpt)
val title = "Zookeeper service nodes"
@@ -143,7 +149,7 @@ private[kyuubi] class ServiceControlCli extends Logging {
private def delete(args: ServiceControlCliArguments): Unit = {
withZkClient(args.conf) { zkClient =>
val znodeRoot = getZkNamespace(args)
- val hostPortOpt = Some((args.host, args.port.toInt))
+ val hostPortOpt = Some((args.cliArgs.host, args.cliArgs.port.toInt))
val nodesToDelete = getServiceNodes(zkClient, znodeRoot, hostPortOpt)
val deletedNodes = ListBuffer[ServiceNodeInfo]()
@@ -163,10 +169,6 @@ private[kyuubi] class ServiceControlCli extends Logging {
info(renderServiceNodesInfo(title, deletedNodes, verbose))
}
}
-
- private def printUsage(args: ServiceControlCliArguments): Unit = {
- args.printUsageAndExit(0)
- }
}
object ServiceControlCli extends CommandLineUtils with Logging {
@@ -181,6 +183,16 @@ object ServiceControlCli extends CommandLineUtils with Logging {
override def warn(msg: => Any): Unit = self.warn(msg)
override def error(msg: => Any): Unit = self.error(msg)
+
+ override private[kyuubi] lazy val effectSetup = new KyuubiOEffectSetup {
+ override def displayToOut(msg: String): Unit = self.info(msg)
+
+ override def displayToErr(msg: String): Unit = self.info(msg)
+
+ override def reportError(msg: String): Unit = self.info(msg)
+
+ override def reportWarning(msg: String): Unit = self.warn(msg)
+ }
}
}
@@ -205,11 +217,11 @@ object ServiceControlCli extends CommandLineUtils with Logging {
}
private[ctl] def getZkNamespace(args: ServiceControlCliArguments): String = {
- args.service match {
+ args.cliArgs.service match {
case ServiceControlObject.SERVER =>
- ZKPaths.makePath(null, args.namespace)
+ ZKPaths.makePath(null, args.cliArgs.namespace)
case ServiceControlObject.ENGINE =>
- ZKPaths.makePath(s"${args.namespace}_${ShareLevel.USER}", args.user)
+ ZKPaths.makePath(s"${args.cliArgs.namespace}_${ShareLevel.USER}", args.cliArgs.user)
}
}
diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliArguments.scala b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliArguments.scala
index cbde1ea19..075a2691d 100644
--- a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliArguments.scala
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliArguments.scala
@@ -18,90 +18,182 @@
package org.apache.kyuubi.ctl
import java.net.InetAddress
-import java.util.{List => JList}
-import scala.collection.JavaConverters._
+import scopt.OParser
import org.apache.kyuubi.{KYUUBI_VERSION, KyuubiException, Logging}
import org.apache.kyuubi.config.KyuubiConf
-import org.apache.kyuubi.ctl.ServiceControlAction._
-import org.apache.kyuubi.ctl.ServiceControlObject._
import org.apache.kyuubi.ha.HighAvailabilityConf._
class ServiceControlCliArguments(args: Seq[String], env: Map[String, String] = sys.env)
extends ServiceControlCliArgumentsParser with Logging {
- var action: ServiceControlAction = null
- var service: ServiceControlObject = null
- var zkQuorum: String = null
- var namespace: String = null
- var user: String = null
- var host: String = null
- var port: String = null
- var version: String = null
- var verbose: Boolean = false
+
+ var cliArgs: CliArguments = null
val conf = KyuubiConf().loadFileDefaults()
// Set parameters from command line arguments
- parse(args.asJava)
+ parse(args)
- // Use default property value if not set
- useDefaultPropertyValueIfMissing()
+ lazy val cliParser = parser()
- validateArguments()
+ override def parser(): OParser[Unit, CliArguments] = {
+ val builder = OParser.builder[CliArguments]
+ import builder._
- private def useDefaultPropertyValueIfMissing(): Unit = {
- if (zkQuorum == null) {
+ // Options after action and service
+ val ops = OParser.sequence(
+ opt[String]("zk-quorum").abbr("zk")
+ .action((v, c) => c.copy(zkQuorum = v))
+ .text("The connection string for the zookeeper ensemble," +
+ " using zk quorum manually."),
+ opt[String]('n', "namespace")
+ .action((v, c) => c.copy(namespace = v))
+ .text("The namespace, using kyuubi-defaults/conf if absent."),
+ opt[String]('s', "host")
+ .action((v, c) => c.copy(host = v))
+ .text("Hostname or IP address of a service."),
+ opt[String]('p', "port")
+ .action((v, c) => c.copy(port = v))
+ .text("Listening port of a service."),
+ opt[String]('v', "version")
+ .action((v, c) => c.copy(version = v))
+ .text("Using the compiled KYUUBI_VERSION default," +
+ " change it if the active service is running in another."),
+ opt[Unit]('b', "verbose")
+ .action((_, c) => c.copy(verbose = true))
+ .text("Print additional debug output."))
+
+ // for engine service only
+ val userOps = opt[String]('u', "user")
+ .action((v, c) => c.copy(user = v))
+ .text("The user name this engine belong to.")
+
+ val serverCmd =
+ cmd("server").action((_, c) => c.copy(service = ServiceControlObject.SERVER))
+ val engineCmd =
+ cmd("engine").action((_, c) => c.copy(service = ServiceControlObject.ENGINE))
+
+ val CtlParser = {
+ OParser.sequence(
+ programName("kyuubi-ctl"),
+ head("kyuubi", KYUUBI_VERSION),
+ ops,
+ note(""),
+ cmd("create")
+ .action((_, c) => c.copy(action = ServiceControlAction.CREATE))
+ .children(
+ serverCmd.text("\tExpose Kyuubi server instance to another domain.")),
+ note(""),
+ cmd("get")
+ .action((_, c) => c.copy(action = ServiceControlAction.GET))
+ .text("\tGet the service/engine node info, host and port needed.")
+ .children(
+ serverCmd.text("\tGet Kyuubi server info of domain"),
+ engineCmd
+ .children(userOps)
+ .text("\tGet Kyuubi engine info belong to a user.")),
+ note(""),
+ cmd("delete")
+ .action((_, c) => c.copy(action = ServiceControlAction.DELETE))
+ .text("\tDelete the specified service/engine node, host and port needed.")
+ .children(
+ serverCmd.text("\tDelete the specified service node for a domain"),
+ engineCmd
+ .children(userOps)
+ .text("\tDelete the specified engine node for user.")),
+ note(""),
+ cmd("list")
+ .action((_, c) => c.copy(action = ServiceControlAction.LIST))
+ .text("\tList all the service/engine nodes for a particular domain.")
+ .children(
+ serverCmd.text("\tList all the service nodes for a particular domain"),
+ engineCmd
+ .children(userOps)
+ .text("\tList all the engine nodes for a user")),
+ checkConfig(f => {
+ if (f.action == null) failure("Must specify action command: [create|get|delete|list].")
+ else success
+ }),
+ note(""),
+ help('h', "help").text("Show help message and exit.")
+ )
+ }
+ CtlParser
+ }
+
+ private[kyuubi] lazy val effectSetup = new KyuubiOEffectSetup
+
+ override def parse(args: Seq[String]): Unit = {
+ OParser.runParser(cliParser, args, CliArguments()) match {
+ case (result, effects) =>
+ OParser.runEffects(effects, effectSetup)
+ result match {
+ case Some(arguments) =>
+ // use default property value if not set
+ cliArgs = useDefaultPropertyValueIfMissing(arguments).copy()
+
+ // validate arguments
+ validateArguments()
+ case _ =>
+ // arguments are bad, exit
+ }
+ }
+ }
+
+ private def useDefaultPropertyValueIfMissing(value: CliArguments): CliArguments = {
+ var arguments: CliArguments = value.copy()
+ if (value.zkQuorum == null) {
conf.getOption(HA_ZK_QUORUM.key).foreach { v =>
- if (verbose) {
+ if (arguments.verbose) {
super.info(s"Zookeeper quorum is not specified, use value from default conf:$v")
}
- zkQuorum = v
+ arguments = arguments.copy(zkQuorum = v)
}
}
// for create action, it only expose Kyuubi service instance to another domain,
// so we do not use namespace from default conf
- if (action != ServiceControlAction.CREATE && namespace == null) {
- namespace = conf.get(HA_ZK_NAMESPACE)
- if (verbose) {
- super.info(s"Zookeeper namespace is not specified, use value from default conf:$namespace")
+ if (arguments.action != ServiceControlAction.CREATE && arguments.namespace == null) {
+ arguments = arguments.copy(namespace = conf.get(HA_ZK_NAMESPACE))
+ if (arguments.verbose) {
+ super.info(s"Zookeeper namespace is not specified, use value from default conf:" +
+ s"${arguments.namespace}")
}
}
- if (version == null) {
- if (verbose) {
+ if (arguments.version == null) {
+ if (arguments.verbose) {
super.info(s"version is not specified, use built-in KYUUBI_VERSION:$KYUUBI_VERSION")
}
- version = KYUUBI_VERSION
+ arguments = arguments.copy(version = KYUUBI_VERSION)
}
+ arguments
}
/** Ensure that required fields exists. Call this only once all defaults are loaded. */
private def validateArguments(): Unit = {
- service = Option(service).getOrElse(ServiceControlObject.SERVER)
- action match {
+ cliArgs.action match {
case ServiceControlAction.CREATE => validateCreateActionArguments()
case ServiceControlAction.GET => validateGetDeleteActionArguments()
case ServiceControlAction.DELETE => validateGetDeleteActionArguments()
case ServiceControlAction.LIST => validateListActionArguments()
- case ServiceControlAction.HELP =>
- case _ => printUsageAndExit(-1)
+ case _ => // do nothing
}
}
private def validateCreateActionArguments(): Unit = {
- if (service != ServiceControlObject.SERVER) {
+ if (cliArgs.service != ServiceControlObject.SERVER) {
fail("Only support expose Kyuubi server instance to another domain")
}
validateZkArguments()
val defaultNamespace = conf.getOption(HA_ZK_NAMESPACE.key)
- if (defaultNamespace.isEmpty || defaultNamespace.get.equals(namespace)) {
+ if (defaultNamespace.isEmpty || defaultNamespace.get.equals(cliArgs.namespace)) {
fail(
s"""
- |Only support expose Kyuubi server instance to another domain, but the default
- |namespace is [$defaultNamespace] and specified namespace is [$namespace]
+ |Only support expose Kyuubi server instance to another domain, but the default
+ |namespace is [$defaultNamespace] and specified namespace is [${cliArgs.namespace}]
""".stripMargin)
}
}
@@ -115,20 +207,24 @@ class ServiceControlCliArguments(args: Seq[String], env: Map[String, String] = s
private def validateListActionArguments(): Unit = {
validateZkArguments()
+ cliArgs.service match {
+ case ServiceControlObject.ENGINE => validateUser()
+ case _ =>
+ }
mergeArgsIntoKyuubiConf()
}
private def mergeArgsIntoKyuubiConf(): Unit = {
- conf.set(HA_ZK_QUORUM.key, zkQuorum)
- conf.set(HA_ZK_NAMESPACE.key, namespace)
+ conf.set(HA_ZK_QUORUM.key, cliArgs.zkQuorum)
+ conf.set(HA_ZK_NAMESPACE.key, cliArgs.namespace)
}
private def validateZkArguments(): Unit = {
- if (zkQuorum == null) {
+ if (cliArgs.zkQuorum == null) {
fail("Zookeeper quorum is not specified and no default value to load")
}
- if (namespace == null) {
- if (action == ServiceControlAction.CREATE) {
+ if (cliArgs.namespace == null) {
+ if (cliArgs.action == ServiceControlAction.CREATE) {
fail("Zookeeper namespace is not specified")
} else {
fail("Zookeeper namespace is not specified and no default value to load")
@@ -137,200 +233,62 @@ class ServiceControlCliArguments(args: Seq[String], env: Map[String, String] = s
}
private def validateHostAndPort(): Unit = {
- if (host == null) {
+ if (cliArgs.host == null) {
fail("Must specify host for service")
}
- if (port == null) {
+ if (cliArgs.port == null) {
fail("Must specify port for service")
}
try {
- host = InetAddress.getByName(host).getCanonicalHostName
+ cliArgs = cliArgs.copy(host = InetAddress.getByName(cliArgs.host).getCanonicalHostName)
} catch {
case _: Exception =>
- fail(s"Unknown host: $host")
+ fail(s"Unknown host: ${cliArgs.host}")
}
try {
- if (port.toInt <= 0 ) {
+ if (cliArgs.port.toInt <= 0 ) {
fail(s"Specified port should be a positive number")
}
} catch {
case _: NumberFormatException =>
- fail(s"Specified port is not a valid integer number: $port")
+ fail(s"Specified port is not a valid integer number: ${cliArgs.port}")
}
}
private def validateUser(): Unit = {
- if (service == ServiceControlObject.ENGINE && user == null) {
- fail("Must specify user name for engine")
+ if (cliArgs.service == ServiceControlObject.ENGINE && cliArgs.user == null) {
+ fail("Must specify user name for engine, please use -u or --user.")
}
}
- private[ctl] def printUsageAndExit(exitCode: Int, unknownParam: Any = null): Unit = {
- if (unknownParam != null) {
- info("Unknown/unsupported param " + unknownParam)
- }
- val command =
- s"""
- |Kyuubi Ver $KYUUBI_VERSION.
- |Usage: kyuubi-ctl --zk-quorum ...
- |--namespace ... --user ... --host ... --port ... --version""".stripMargin
- info(command)
-
- info(
- s"""
- |Command:
- | - create expose a service to a namespace on the zookeeper cluster of
- | zk quorum manually
- | - get get the service node info
- | - delete delete the specified serviceNode
- | - list list all the service nodes for a particular domain
- |
- |Service:
- | - server default
- | - engine
- |
- |Arguments:
- | --zk-quorum The connection string for the zookeeper ensemble, using
- | kyuubi-defaults/conf if absent
- | --namespace the namespace, using kyuubi-defaults/conf if absent
- | --host hostname or IP address of a service
- | --port listening port of a service
- | --version using the compiled KYUUBI_VERSION default, change it if the
- | active service is running in another
- | --user for engine service only, the user name this engine belong to
- | --help Show this help message and exit.
- | --verbose Print additional debug output.
- """.stripMargin
- )
-
- throw new ServiceControlCliException(exitCode)
- }
-
- override protected def parseActionAndService(args: JList[String]): Int = {
- if (args.isEmpty) {
- printUsageAndExit(-1)
- }
-
- var actionParsed = false
- var serviceParsed = false
- var offset = 0
-
- while(offset < args.size() && needContinueHandle() && !(actionParsed && serviceParsed)) {
- val arg = args.get(offset)
- if (!actionParsed) {
- arg match {
- case CREATE =>
- action = ServiceControlAction.CREATE
- actionParsed = true
- case GET =>
- action = ServiceControlAction.GET
- actionParsed = true
- case DELETE =>
- action = ServiceControlAction.DELETE
- actionParsed = true
- case LIST =>
- action = ServiceControlAction.LIST
- actionParsed = true
- case _ => findSwitches(arg) match {
- case HELP =>
- action = ServiceControlAction.HELP
- actionParsed = true
- case VERBOSE =>
- verbose = true
- case _ =>
- printUsageAndExit(-1, arg)
- }
- }
- offset += 1
- } else if (needContinueHandle() && !serviceParsed) {
- arg match {
- case SERVER =>
- service = ServiceControlObject.SERVER
- serviceParsed = true
- offset += 1
- case ENGINE =>
- service = ServiceControlObject.ENGINE
- serviceParsed = true
- offset += 1
- case _ => findSwitches(arg) match {
- case HELP =>
- action = ServiceControlAction.HELP
- offset += 1
- case VERBOSE =>
- verbose = true
- offset += 1
- case _ =>
- service = ServiceControlObject.SERVER
- serviceParsed = true
- }
- }
- }
- }
- offset
- }
-
override def toString: String = {
- s"""Parsed arguments:
- | action $action
- | service $service
- | zkQuorum $zkQuorum
- | namespace $namespace
- | user $user
- | host $host
- | port $port
- | version $version
- | verbose $verbose
- """.stripMargin
- }
-
- private def needContinueHandle(): Boolean = {
- action != ServiceControlAction.HELP
- }
-
- /** Fill in values by parsing user options. */
- override protected def handle(opt: String, value: String): Boolean = {
- if (!needContinueHandle()) {
- return false
- }
- opt match {
- case ZK_QUORUM =>
- zkQuorum = value
-
- case NAMESPACE =>
- namespace = value
-
- case USER =>
- user = value
-
- case HOST =>
- host = value
-
- case PORT =>
- port = value
-
- case VERSION =>
- version = value
-
- case HELP =>
- action = ServiceControlAction.HELP
-
- case VERBOSE =>
- verbose = true
-
- case _ =>
- fail(s"Unexpected argument '$opt'.")
- }
- needContinueHandle()
- }
-
- override protected def handleUnknown(opt: String): Boolean = {
- if (!needContinueHandle()) {
- false
- } else {
- printUsageAndExit(-1, opt)
- false
+ cliArgs.service match {
+ case ServiceControlObject.SERVER =>
+ s"""Parsed arguments:
+ | action ${cliArgs.action}
+ | service ${cliArgs.service}
+ | zkQuorum ${cliArgs.zkQuorum}
+ | namespace ${cliArgs.namespace}
+ | host ${cliArgs.host}
+ | port ${cliArgs.port}
+ | version ${cliArgs.version}
+ | verbose ${cliArgs.verbose}
+ """.stripMargin
+ case ServiceControlObject.ENGINE =>
+ s"""Parsed arguments:
+ | action ${cliArgs.action}
+ | service ${cliArgs.service}
+ | zkQuorum ${cliArgs.zkQuorum}
+ | namespace ${cliArgs.namespace}
+ | user ${cliArgs.user}
+ | host ${cliArgs.host}
+ | port ${cliArgs.port}
+ | version ${cliArgs.version}
+ | verbose ${cliArgs.verbose}
+ """.stripMargin
+ case _ => ""
}
}
diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliArgumentsParser.scala b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliArgumentsParser.scala
index 1167a4e75..6934c47b7 100644
--- a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliArgumentsParser.scala
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliArgumentsParser.scala
@@ -17,5 +17,36 @@
package org.apache.kyuubi.ctl
-private[kyuubi] abstract class ServiceControlCliArgumentsParser
- extends ServiceControlCliOptionParser
+import scopt.OParser
+
+import org.apache.kyuubi.ctl.ServiceControlAction.ServiceControlAction
+import org.apache.kyuubi.ctl.ServiceControlObject.ServiceControlObject
+
+private[kyuubi] abstract class ServiceControlCliArgumentsParser {
+
+ /**
+ * Description of available options
+ */
+ case class CliArguments(
+ action: ServiceControlAction = null,
+ service: ServiceControlObject = ServiceControlObject.SERVER,
+ zkQuorum: String = null,
+ namespace: String = null,
+ user: String = null,
+ host: String = null,
+ port: String = null,
+ version: String = null,
+ verbose: Boolean = false)
+
+ /**
+ * Cli arguments parse rules.
+ */
+ def parser(): OParser[Unit, CliArguments]
+
+ /**
+ * Parse a list of kyuubi-ctl command line options.
+ *
+ * @throws IllegalArgumentException If an error is found during parsing.
+ */
+ def parse(args: Seq[String]): Unit
+}
diff --git a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ServiceControlCliArgumentsSuite.scala b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ServiceControlCliArgumentsSuite.scala
index 8e70a8479..d38f9ab44 100644
--- a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ServiceControlCliArgumentsSuite.scala
+++ b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ServiceControlCliArgumentsSuite.scala
@@ -45,6 +45,29 @@ class ServiceControlCliArgumentsSuite extends KyuubiFunSuite {
}
}
+ /** Check whether the script exits and the given search string is printed. */
+ private def testHelpExit(args: Array[String], searchString: String): Unit = {
+ val logAppender = new LogAppender("test premature exit")
+ withLogAppender(logAppender) {
+ val thread = new Thread {
+ override def run(): Unit = try {
+ new ServiceControlCliArguments(args) {
+ override private[kyuubi] lazy val effectSetup = new KyuubiOEffectSetup {
+ // nothing to do, to handle out stream.
+ override def terminate(exitState: Either[String, Unit]): Unit = ()
+ }
+ }
+ } catch {
+ case e: Exception =>
+ error(e)
+ }
+ }
+ thread.start()
+ thread.join()
+ assert(logAppender.loggingEvents.exists(_.getRenderedMessage.contains(searchString)))
+ }
+ }
+
test("test basic kyuubi service arguments parser") {
Seq("get", "list", "delete").foreach { op =>
Seq("server", "engine").foreach { service =>
@@ -58,14 +81,14 @@ class ServiceControlCliArgumentsSuite extends KyuubiFunSuite {
"--version", KYUUBI_VERSION
)
val opArgs = new ServiceControlCliArguments(args)
- assert(opArgs.action.toString.equalsIgnoreCase(op))
- assert(opArgs.service.toString.equalsIgnoreCase(service))
- assert(opArgs.zkQuorum == zkQuorum)
- assert(opArgs.namespace == namespace)
- assert(opArgs.user == user)
- assert(opArgs.host == host)
- assert(opArgs.port == port)
- assert(opArgs.version == KYUUBI_VERSION)
+ assert(opArgs.cliArgs.action.toString.equalsIgnoreCase(op))
+ assert(opArgs.cliArgs.service.toString.equalsIgnoreCase(service))
+ assert(opArgs.cliArgs.zkQuorum == zkQuorum)
+ assert(opArgs.cliArgs.namespace == namespace)
+ assert(opArgs.cliArgs.user == user)
+ assert(opArgs.cliArgs.host == host)
+ assert(opArgs.cliArgs.port == port)
+ assert(opArgs.cliArgs.version == KYUUBI_VERSION)
}
}
@@ -77,56 +100,36 @@ class ServiceControlCliArgumentsSuite extends KyuubiFunSuite {
op, service,
"--zk-quorum", zkQuorum,
"--namespace", s"${namespace}_new",
- "--user", user,
"--host", host,
"--port", port,
"--version", KYUUBI_VERSION
)
val opArgs = new ServiceControlCliArguments(args)
- assert(opArgs.action.toString.equalsIgnoreCase(op))
- assert(opArgs.service.toString.equalsIgnoreCase(service))
- assert(opArgs.zkQuorum == zkQuorum)
- assert(opArgs.namespace == newNamespace)
- assert(opArgs.user == user)
- assert(opArgs.host == host)
- assert(opArgs.port == port)
- assert(opArgs.version == KYUUBI_VERSION)
+ assert(opArgs.cliArgs.action.toString.equalsIgnoreCase(op))
+ assert(opArgs.cliArgs.service.toString.equalsIgnoreCase(service))
+ assert(opArgs.cliArgs.zkQuorum == zkQuorum)
+ assert(opArgs.cliArgs.namespace == newNamespace)
+ assert(opArgs.cliArgs.host == host)
+ assert(opArgs.cliArgs.port == port)
+ assert(opArgs.cliArgs.version == KYUUBI_VERSION)
}
}
- test("treat --help as action") {
- val args = Seq("--help")
- val opArgs = new ServiceControlCliArguments(args)
- assert(opArgs.action == ServiceControlAction.HELP)
- assert(opArgs.version == KYUUBI_VERSION)
-
- val args2 = Seq(
- "create", "server",
- s"--user=$user",
- "--host", host,
- "--verbose",
- "--help",
- "--port", port
- )
- val opArgs2 = new ServiceControlCliArguments(args2)
- assert(opArgs2.action == ServiceControlAction.HELP)
- assert(opArgs2.user == user)
- assert(opArgs2.host == host)
- assert(opArgs2.verbose)
- }
-
test("prints usage on empty input") {
- testPrematureExit(Array.empty[String], "Usage: kyuubi-ctl")
- testPrematureExit(Array("--verbose"), "Usage: kyuubi-ctl")
+ testPrematureExit(Array.empty[String], "Must specify action command: [create|get|delete|list].")
+ testPrematureExit(Array("--verbose"), "Must specify action command: [create|get|delete|list].")
}
test("prints error with unrecognized options") {
- testPrematureExit(Array("create", "--unknown"), "Unknown/unsupported param --unknown")
- testPrematureExit(Array("--unknown"), "Unknown/unsupported param --unknown")
+ testPrematureExit(Array("create", "--unknown"), "Unknown option --unknown")
+ testPrematureExit(Array("--unknown"), "Unknown option --unknown")
}
test("test invalid arguments") {
- testPrematureExit(Array("create", "--user"), "Missing argument for option '--user'")
+ // for server, user option is not support
+ testPrematureExit(Array("create", "--user"), "Unknown option --user")
+ // for engine, user option need a value
+ testPrematureExit(Array("get", "engine", "--user"), "Missing value after --user")
}
test("test extra unused arguments") {
@@ -134,7 +137,7 @@ class ServiceControlCliArgumentsSuite extends KyuubiFunSuite {
"list",
"extraArg1", "extraArg2"
)
- testPrematureExit(args, "Unknown/unsupported param extraArg1")
+ testPrematureExit(args, "Unknown argument 'extraArg1'")
}
test("test list action arguments") {
@@ -149,7 +152,7 @@ class ServiceControlCliArgumentsSuite extends KyuubiFunSuite {
"--namespace", namespace
)
val opArgs = new ServiceControlCliArguments(args2)
- assert(opArgs.action == ServiceControlAction.LIST)
+ assert(opArgs.cliArgs.action == ServiceControlAction.LIST)
}
test("test get/delete action arguments") {
@@ -191,32 +194,10 @@ class ServiceControlCliArgumentsSuite extends KyuubiFunSuite {
"--port", port
)
val opArgs6 = new ServiceControlCliArguments(args5)
- assert(opArgs6.action.toString.equalsIgnoreCase(op))
+ assert(opArgs6.cliArgs.action.toString.equalsIgnoreCase(op))
}
}
- test("test with switches at head") {
- val args = Seq("--verbose", "list", "engine", "--zk-quorum", zkQuorum, "--namespace",
- namespace)
- val opArgs = new ServiceControlCliArguments(args)
- assert(opArgs.verbose)
- assert(opArgs.action == ServiceControlAction.LIST)
- assert(opArgs.service == ServiceControlObject.ENGINE)
-
- val args2 = Seq("list", "--verbose", "engine", "--zk-quorum", zkQuorum, "--namespace",
- namespace)
- val opArgs2 = new ServiceControlCliArguments(args2)
- assert(opArgs2.verbose)
- assert(opArgs2.action == ServiceControlAction.LIST)
- assert(opArgs2.service == ServiceControlObject.ENGINE)
-
- val args3 = Seq("list", "--verbose", "--help", "engine", "--zk-quorum", zkQuorum,
- "--namespace", namespace)
- val opArgs3 = new ServiceControlCliArguments(args3)
- assert(opArgs3.verbose)
- assert(opArgs3.action == ServiceControlAction.HELP)
- }
-
test("test with unknown host") {
val args = Array(
"get", "server",
@@ -263,14 +244,15 @@ class ServiceControlCliArgumentsSuite extends KyuubiFunSuite {
"--namespace", newNamespace
)
val opArgs2 = new ServiceControlCliArguments(args2)
- assert(opArgs2.action.toString.equalsIgnoreCase(op))
+ assert(opArgs2.cliArgs.action.toString.equalsIgnoreCase(op))
val args4 = Array(
op, "engine",
"--zk-quorum", zkQuorum,
"--namespace", newNamespace
)
- testPrematureExit(args4, "Only support expose Kyuubi server instance to another domain")
+ // engine is not support, expect scopt print Unknown argument.
+ testPrematureExit(args4, "Unknown argument 'engine'")
}
}
@@ -280,7 +262,92 @@ class ServiceControlCliArgumentsSuite extends KyuubiFunSuite {
"--zk-quorum", zkQuorum
)
val opArgs = new ServiceControlCliArguments(args)
- assert(opArgs.namespace == namespace)
- assert(opArgs.version == KYUUBI_VERSION)
+ assert(opArgs.cliArgs.namespace == namespace)
+ assert(opArgs.cliArgs.version == KYUUBI_VERSION)
+ }
+
+ test("test use short options") {
+ Seq("get", "list", "delete").foreach { op =>
+ Seq("server", "engine").foreach { service =>
+ val args = Seq(
+ op, service,
+ "-zk", zkQuorum,
+ "-n", namespace,
+ "-u", user,
+ "-s", host,
+ "-p", port,
+ "-v", KYUUBI_VERSION
+ )
+ val opArgs = new ServiceControlCliArguments(args)
+ assert(opArgs.cliArgs.action.toString.equalsIgnoreCase(op))
+ assert(opArgs.cliArgs.service.toString.equalsIgnoreCase(service))
+ assert(opArgs.cliArgs.zkQuorum == zkQuorum)
+ assert(opArgs.cliArgs.namespace == namespace)
+ assert(opArgs.cliArgs.user == user)
+ assert(opArgs.cliArgs.host == host)
+ assert(opArgs.cliArgs.port == port)
+ assert(opArgs.cliArgs.version == KYUUBI_VERSION)
+ }
+ }
+
+ // test verbose
+ val args2 = Array(
+ "list",
+ "-zk", zkQuorum,
+ "-b"
+ )
+ val opArgs3 = new ServiceControlCliArguments(args2)
+ assert(opArgs3.cliArgs.verbose)
+ }
+
+ test("test --help") {
+ // some string is too long for check style
+ val zkHelpString = "The connection string for the zookeeper ensemble, using zk quorum manually."
+ val versionHelpString = "Using the compiled KYUUBI_VERSION default," +
+ " change it if the active service is running in another."
+ val helpString =
+ s"""kyuubi $KYUUBI_VERSION
+ |Usage: kyuubi-ctl [create|get|delete|list] [options]
+ |
+ | -zk, --zk-quorum
+ | $zkHelpString
+ | -n, --namespace The namespace, using kyuubi-defaults/conf if absent.
+ | -s, --host Hostname or IP address of a service.
+ | -p, --port Listening port of a service.
+ | -v, --version $versionHelpString
+ | -b, --verbose Print additional debug output.
+ |
+ |Command: create [server]
+ |
+ |Command: create server
+ |${"\t"}Expose Kyuubi server instance to another domain.
+ |
+ |Command: get [server|engine] [options]
+ |${"\t"}Get the service/engine node info, host and port needed.
+ |Command: get server
+ |${"\t"}Get Kyuubi server info of domain
+ |Command: get engine
+ |${"\t"}Get Kyuubi engine info belong to a user.
+ | -u, --user The user name this engine belong to.
+ |
+ |Command: delete [server|engine] [options]
+ |${"\t"}Delete the specified service/engine node, host and port needed.
+ |Command: delete server
+ |${"\t"}Delete the specified service node for a domain
+ |Command: delete engine
+ |${"\t"}Delete the specified engine node for user.
+ | -u, --user The user name this engine belong to.
+ |
+ |Command: list [server|engine] [options]
+ |${"\t"}List all the service/engine nodes for a particular domain.
+ |Command: list server
+ |${"\t"}List all the service nodes for a particular domain
+ |Command: list engine
+ |${"\t"}List all the engine nodes for a user
+ | -u, --user The user name this engine belong to.
+ |
+ | -h, --help Show help message and exit.""".stripMargin
+
+ testHelpExit(Array("--help"), helpString)
}
}
diff --git a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ServiceControlCliSuite.scala b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ServiceControlCliSuite.scala
index 3a6736c73..c68455a1f 100644
--- a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ServiceControlCliSuite.scala
+++ b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ServiceControlCliSuite.scala
@@ -121,7 +121,7 @@ class ServiceControlCliSuite extends KyuubiFunSuite with TestPrematureExit {
/** Get the rendered service node info without title */
private def getRenderedNodesInfoWithoutTitle(nodesInfo: Seq[ServiceNodeInfo],
- verbose: Boolean): String = {
+ verbose: Boolean): String = {
val renderedInfo = renderServiceNodesInfo("", nodesInfo, verbose)
if (verbose) {
renderedInfo.substring(renderedInfo.indexOf("|"))
@@ -130,11 +130,6 @@ class ServiceControlCliSuite extends KyuubiFunSuite with TestPrematureExit {
}
}
- test("test help") {
- val args = Array("--help")
- testPrematureExit(args, "Usage: kyuubi-ctl")
- }
-
test("test expose to same namespace or not specified namespace") {
conf
.unset(KyuubiConf.SERVER_KEYTAB)
diff --git a/pom.xml b/pom.xml
index 01e6b2bba..107e14ac0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -84,6 +84,7 @@
9.4.41.v20210516
5.1.4
0.10.0
+ 4.0.1
3.1.2
spark-${spark.version}-bin-hadoop${hadoop.binary.version}.tgz
https://archive.apache.org/dist/spark/spark-${spark.version}
@@ -1168,6 +1169,12 @@
activation
${javax-activation.version}