[KYUUBI #753] Use scopt to parse arguments

<!--
Thanks for sending a pull request!

Here are some tips for you:
  1. If this is your first time, please read our contributor guidelines: https://kyuubi.readthedocs.io/en/latest/community/contributions.html
  2. If the PR is related to an issue in https://github.com/NetEase/kyuubi/issues, add '[KYUUBI #XXXX]' in your PR title, e.g., '[KYUUBI #XXXX] Your PR title ...'.
  3. If the PR is unfinished, add '[WIP]' in your PR title, e.g., '[WIP][KYUUBI #XXXX] Your PR title ...'.
-->

### _Why are the changes needed?_
<!--
Please clarify why the changes are needed. For instance,
  1. If you add a feature, you can talk about the use case of it.
  2. If you fix a bug, you can clarify why it is a bug.
-->
Use [`scopt`](https://github.com/scopt/scopt) replace custom parser to parse arguments.

### _How was this patch tested?_
- [X] Add some test cases that check the changes thoroughly including negative and positive cases if possible

- [ ] Add screenshots for manual tests if appropriate

- [ ] [Run test](https://kyuubi.readthedocs.io/en/latest/tools/testing.html#running-tests) locally before make a pull request

Closes #754 from hddong/kyuubi-753.

Closes #753

3d2645d8 [hongdongdong] fix check style
fd181452 [hongdongdong] fix output
ef7c44e4 [hongdongdong] use scopt help
682b3a2f [hongdongdong] [KYUUBI#753] Use scopt to parse arguments

Authored-by: hongdongdong <hongdongdong@cmss.chinamobile.com>
Signed-off-by: Kent Yao <yao@apache.org>
This commit is contained in:
hongdongdong 2021-07-12 17:51:11 +08:00 committed by Kent Yao
parent 5e53748bb5
commit 16b93e4720
No known key found for this signature in database
GPG Key ID: F7051850A0AF904D
12 changed files with 417 additions and 461 deletions

View File

@ -258,3 +258,4 @@ MIT License
org.slf4j:slf4j-api
org.slf4j:slf4j-log4j12
org.slf4j:jcl-over-slf4j
com.github.scopt:scopt_*

View File

@ -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

View File

@ -65,6 +65,11 @@
<artifactId>curator-recipes</artifactId>
</dependency>
<dependency>
<groupId>com.github.scopt</groupId>
<artifactId>scopt_${scala.binary.version}</artifactId>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>

View File

@ -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<String> args);
/**
* Parse a list of kyuubi-ctl command line options.
* <p>
* 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<String> 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 <i>null</i> 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);
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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 <create|get|delete|list> <server|engine> --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 _ => ""
}
}

View File

@ -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
}

View File

@ -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 <value>
| $zkHelpString
| -n, --namespace <value> The namespace, using kyuubi-defaults/conf if absent.
| -s, --host <value> Hostname or IP address of a service.
| -p, --port <value> Listening port of a service.
| -v, --version <value> $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 <value> 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 <value> 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 <value> The user name this engine belong to.
|
| -h, --help Show help message and exit.""".stripMargin
testHelpExit(Array("--help"), helpString)
}
}

View File

@ -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)

View File

@ -84,6 +84,7 @@
<jetty.version>9.4.41.v20210516</jetty.version>
<ldapsdk.version>5.1.4</ldapsdk.version>
<prometheus.version>0.10.0</prometheus.version>
<scopt.version>4.0.1</scopt.version>
<spark.version>3.1.2</spark.version>
<spark.archive.name>spark-${spark.version}-bin-hadoop${hadoop.binary.version}.tgz</spark.archive.name>
<spark.archive.mirror>https://archive.apache.org/dist/spark/spark-${spark.version}</spark.archive.mirror>
@ -1168,6 +1169,12 @@
<artifactId>activation</artifactId>
<version>${javax-activation.version}</version>
</dependency>
<dependency>
<groupId>com.github.scopt</groupId>
<artifactId>scopt_${scala.binary.version}</artifactId>
<version>${scopt.version}</version>
</dependency>
</dependencies>
</dependencyManagement>