diff --git a/LICENSE-binary b/LICENSE-binary index d39da9ffb..5c64e0115 100644 --- a/LICENSE-binary +++ b/LICENSE-binary @@ -258,3 +258,4 @@ MIT License org.slf4j:slf4j-api org.slf4j:slf4j-log4j12 org.slf4j:jcl-over-slf4j +com.github.scopt:scopt_* diff --git a/dev/dependencyList b/dev/dependencyList index e434b07f8..07cb372f2 100644 --- a/dev/dependencyList +++ b/dev/dependencyList @@ -49,6 +49,7 @@ metrics-jmx/4.1.1//metrics-jmx-4.1.1.jar metrics-json/4.1.1//metrics-json-4.1.1.jar metrics-jvm/4.1.1//metrics-jvm-4.1.1.jar scala-library/2.12.14//scala-library-2.12.14.jar +scopt_2.12/4.0.1//scopt_2.12-4.0.1.jar simpleclient/0.10.0//simpleclient-0.10.0.jar simpleclient_common/0.10.0//simpleclient_common-0.10.0.jar simpleclient_dropwizard/0.10.0//simpleclient_dropwizard-0.10.0.jar diff --git a/kyuubi-ctl/pom.xml b/kyuubi-ctl/pom.xml index e3f10e049..687ba091f 100644 --- a/kyuubi-ctl/pom.xml +++ b/kyuubi-ctl/pom.xml @@ -65,6 +65,11 @@ curator-recipes + + com.github.scopt + scopt_${scala.binary.version} + + org.apache.zookeeper zookeeper diff --git a/kyuubi-ctl/src/main/java/org/apache/kyuubi/ctl/ServiceControlCliOptionParser.java b/kyuubi-ctl/src/main/java/org/apache/kyuubi/ctl/ServiceControlCliOptionParser.java deleted file mode 100644 index 9a4d34990..000000000 --- a/kyuubi-ctl/src/main/java/org/apache/kyuubi/ctl/ServiceControlCliOptionParser.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * 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 java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -abstract class ServiceControlCliOptionParser { - protected final String CREATE = "create"; - protected final String GET = "get"; - protected final String DELETE = "delete"; - protected final String LIST = "list"; - protected final String SERVER = "server"; - protected final String ENGINE = "engine"; - - protected final String ZK_QUORUM = "--zk-quorum"; - protected final String NAMESPACE = "--namespace"; - protected final String USER = "--user"; - protected final String HOST = "--host"; - protected final String PORT = "--port"; - protected final String VERSION = "--version"; - - // Options that do not take arguments. - protected final String HELP = "--help"; - protected final String VERBOSE = "--verbose"; - - final String[][] opts = { - { ZK_QUORUM }, - { NAMESPACE }, - { USER }, - { HOST }, - { PORT }, - { VERSION }, - }; - - /** - * List of switches (command line options that do not take parameters) recognized by - * kyuubi-ctl. - */ - final String[][] switches = { - { HELP }, - { VERBOSE }, - }; - - /** - * Parse action type and service type. - * - * @return offset of remaining arguments. - */ - protected abstract int parseActionAndService(List 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} + + + com.github.scopt + scopt_${scala.binary.version} + ${scopt.version} +