[KYUUBI #304] Add Kyuubi Ctl arguments parser
 [](https://github.com/yaooqinn/kyuubi/pull/465)     [<img width="16" alt="Powered by Pull Request Badge" src="https://user-images.githubusercontent.com/1393946/111216524-d2bb8e00-85d4-11eb-821b-ed4c00989c02.png">](https://pullrequestbadge.com/?utm_medium=github&utm_source=yaooqinn&utm_campaign=badge_info)<!-- PR-BADGE: PLEASE DO NOT REMOVE THIS COMMENT --> <!-- 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/yaooqinn/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?_ ``` bin/kyuubi-service <create|get|delete|list> <server|engine> --zkAddress ... --namespace ... --user ... --host ... --port ... --version Operations: - create - expose a service to a namespace, this case is rare but sometimes we may want one server to be reached in 2 or more namespaces by different user groups - get - get the service node info - delete - delete the specified serviceNode - list - list all the service nodes for a particular domain Role: - server default - engine Args: --zkAddress - one of the zk ensemble address, using kyuubi-defaults/conf if absent --namespace - the namespace, using kyuubi-defaults/conf if absent --user - --host --port --version ``` ### _How was this patch tested?_ UT Closes #465 from turboFei/KYUUBI_304_CMD. Closes #304 4bab34b [fwang12] retest pleaes 8083a12 [fwang12] to increase code converage c7e51a2 [fwang12] complete 7249cd6 [fwang12] add ut c809c27 [fwang12] enable set verbose at first bb3cbb6 [fwang12] fix ut 604820d [fwang12] validate for each action a01ac1f [fwang12] fix scala style issue 76c9b4c [fwang12] save 7139dd5 [fwang12] increase test converage 318ebce [fwang12] add more ut 72978a6 [fwang12] save 2931f93 [fwang12] address comments 10b855d [fwang12] save 420912a [fwang12] treat help as an action b27d0a6 [fwang12] treat help as action 896e20d [fwang12] add ctl module into codecov ea43d69 [fwang12] rename kyuubi-service to kyuubi-ctl 65a0e30 [fwang12] save db718b0 [fwang12] refactor 41b503e [fwang12] Add kyuubi-ctl arguments parser cb3f6a8 [fwang12] with log appender Authored-by: fwang12 <fwang12@ebay.com> Signed-off-by: Kent Yao <yao@apache.org>
This commit is contained in:
parent
a66a9054ef
commit
0aafc55bba
@ -38,6 +38,12 @@
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.kyuubi</groupId>
|
||||
<artifactId>kyuubi-ctl</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.kyuubi</groupId>
|
||||
<artifactId>kyuubi-ha</artifactId>
|
||||
|
||||
@ -57,6 +57,12 @@
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.kyuubi</groupId>
|
||||
<artifactId>kyuubi-ctl</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.hadoop</groupId>
|
||||
<artifactId>hadoop-client-api</artifactId>
|
||||
|
||||
@ -17,7 +17,11 @@
|
||||
|
||||
package org.apache.kyuubi
|
||||
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
|
||||
// scalastyle:off
|
||||
import org.apache.log4j.{Appender, AppenderSkeleton, Level, Logger}
|
||||
import org.apache.log4j.spi.LoggingEvent
|
||||
import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, FunSuite, Outcome}
|
||||
import org.scalatest.concurrent.Eventually
|
||||
|
||||
@ -52,4 +56,43 @@ trait KyuubiFunSuite extends FunSuite
|
||||
info(s"\n\n===== FINISHED $shortSuiteName: '$testName' =====\n")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a log appender and optionally sets a log level to the root logger or the logger with
|
||||
* the specified name, then executes the specified function, and in the end removes the log
|
||||
* appender and restores the log level if necessary.
|
||||
*/
|
||||
protected def withLogAppender(
|
||||
appender: Appender,
|
||||
loggerName: Option[String] = None,
|
||||
level: Option[Level] = None)(
|
||||
f: => Unit): Unit = {
|
||||
val logger = loggerName.map(Logger.getLogger).getOrElse(Logger.getRootLogger)
|
||||
val restoreLevel = logger.getLevel
|
||||
logger.addAppender(appender)
|
||||
if (level.isDefined) {
|
||||
logger.setLevel(level.get)
|
||||
}
|
||||
try f finally {
|
||||
logger.removeAppender(appender)
|
||||
if (level.isDefined) {
|
||||
logger.setLevel(restoreLevel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LogAppender(msg: String = "", maxEvents: Int = 1000) extends AppenderSkeleton {
|
||||
val loggingEvents = new ArrayBuffer[LoggingEvent]()
|
||||
|
||||
override def append(loggingEvent: LoggingEvent): Unit = {
|
||||
if (loggingEvents.size >= maxEvents) {
|
||||
val loggingInfo = if (msg == "") "." else s" while logging $msg."
|
||||
throw new IllegalStateException(
|
||||
s"Number of events reached the limit of $maxEvents$loggingInfo")
|
||||
}
|
||||
loggingEvents.append(loggingEvent)
|
||||
}
|
||||
override def close(): Unit = {}
|
||||
override def requiresLayout(): Boolean = false
|
||||
}
|
||||
}
|
||||
|
||||
106
kyuubi-ctl/pom.xml
Normal file
106
kyuubi-ctl/pom.xml
Normal file
@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.apache.kyuubi</groupId>
|
||||
<artifactId>kyuubi</artifactId>
|
||||
<version>1.2.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>kyuubi-ctl</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Kyuubi Project Control</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.kyuubi</groupId>
|
||||
<artifactId>kyuubi-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.kyuubi</groupId>
|
||||
<artifactId>kyuubi-ha</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.hadoop</groupId>
|
||||
<artifactId>hadoop-client-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.hadoop</groupId>
|
||||
<artifactId>hadoop-client-runtime</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.curator</groupId>
|
||||
<artifactId>curator-framework</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.curator</groupId>
|
||||
<artifactId>curator-recipes</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.zookeeper</groupId>
|
||||
<artifactId>zookeeper</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Begin: for EmbeddedZkServer -->
|
||||
<dependency>
|
||||
<groupId>org.apache.curator</groupId>
|
||||
<artifactId>curator-test</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- End: for EmbeddedZkServer -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.kyuubi</groupId>
|
||||
<artifactId>kyuubi-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>test-jar</type>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.hadoop</groupId>
|
||||
<artifactId>hadoop-minikdc</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.directory.server</groupId>
|
||||
<artifactId>apacheds-service</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<outputDirectory>target/scala-${scala.binary.version}/classes</outputDirectory>
|
||||
<testOutputDirectory>target/scala-${scala.binary.version}/test-classes</testOutputDirectory>
|
||||
</build>
|
||||
</project>
|
||||
@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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 KyuubiCtlOptionParser {
|
||||
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_ADDRESS = "--zkAddress";
|
||||
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_ADDRESS, "-zk" },
|
||||
{ NAMESPACE, "-ns" },
|
||||
{ USER, "-u" },
|
||||
{ HOST, "-h" },
|
||||
{ PORT, "-p" },
|
||||
{ VERSION, "-V" },
|
||||
};
|
||||
|
||||
/**
|
||||
* List of switches (command line options that do not take parameters) recognized by
|
||||
* kyuubi-ctl.
|
||||
*/
|
||||
final String[][] switches = {
|
||||
{ HELP, "-I" },
|
||||
{ VERBOSE, "-v" },
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
private[ctl] object KyuubiCtlAction extends Enumeration {
|
||||
type KyuubiCtlAction = Value
|
||||
val CREATE, GET, DELETE, LIST, HELP = Value
|
||||
}
|
||||
|
||||
private[ctl] object KyuubiCtlActionService extends Enumeration {
|
||||
type KyuubiCtlActionService = Value
|
||||
val SERVER, ENGINE = Value
|
||||
}
|
||||
@ -0,0 +1,292 @@
|
||||
/*
|
||||
* 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 => JList}
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
import org.apache.kyuubi.{KYUUBI_VERSION, KyuubiException, Logging, Utils}
|
||||
import org.apache.kyuubi.ctl.KyuubiCtlAction._
|
||||
import org.apache.kyuubi.ctl.KyuubiCtlActionService._
|
||||
import org.apache.kyuubi.ha.HighAvailabilityConf._
|
||||
|
||||
class KyuubiCtlArguments(args: Seq[String], env: Map[String, String] = sys.env)
|
||||
extends KyuubiCtlArgumentsParser with Logging {
|
||||
var action: KyuubiCtlAction = null
|
||||
var service: KyuubiCtlActionService = null
|
||||
var zkAddress: 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
|
||||
|
||||
/** Default properties present in the currently defined default file. */
|
||||
lazy val defaultKyuubiProperties: Map[String, String] = {
|
||||
val maybeConfigFile = Utils.getDefaultPropertiesFile(env)
|
||||
if (verbose) {
|
||||
info(s"Using properties file: $maybeConfigFile")
|
||||
}
|
||||
val defaultProperties = Utils.getPropertiesFromFile(maybeConfigFile)
|
||||
if (verbose) {
|
||||
defaultProperties.foreach { case (k, v) =>
|
||||
info(s"Adding default property: $k=$v")
|
||||
}
|
||||
}
|
||||
defaultProperties
|
||||
}
|
||||
|
||||
// Set parameters from command line arguments
|
||||
parse(args.asJava)
|
||||
|
||||
// Use default property value if not set
|
||||
useDefaultPropertyValueIfMissing()
|
||||
|
||||
validateArguments()
|
||||
|
||||
if (verbose) {
|
||||
info(toString)
|
||||
}
|
||||
|
||||
private def useDefaultPropertyValueIfMissing(): Unit = {
|
||||
if (zkAddress == null) {
|
||||
zkAddress = defaultKyuubiProperties.getOrElse(HA_ZK_QUORUM.key, null)
|
||||
}
|
||||
if (nameSpace == null) {
|
||||
nameSpace = defaultKyuubiProperties.getOrElse(HA_ZK_NAMESPACE.key, null)
|
||||
}
|
||||
if (version == null) {
|
||||
version = KYUUBI_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
/** Ensure that required fields exists. Call this only once all defaults are loaded. */
|
||||
private def validateArguments(): Unit = {
|
||||
service = Option(service).getOrElse(KyuubiCtlActionService.SERVER)
|
||||
action match {
|
||||
case KyuubiCtlAction.CREATE => validateCreateGetDeleteArguments()
|
||||
case KyuubiCtlAction.GET => validateCreateGetDeleteArguments()
|
||||
case KyuubiCtlAction.DELETE => validateCreateGetDeleteArguments()
|
||||
case KyuubiCtlAction.LIST => validateListArguments()
|
||||
case KyuubiCtlAction.HELP =>
|
||||
case _ => printUsageAndExit(-1)
|
||||
}
|
||||
}
|
||||
|
||||
private def validateCreateGetDeleteArguments(): Unit = {
|
||||
if (zkAddress == null) {
|
||||
fail("Zookeeper address is not specified and no default value to load")
|
||||
}
|
||||
if (nameSpace == null) {
|
||||
fail("Zookeeper namespace is not specified and no default value to load")
|
||||
}
|
||||
if (version == null) {
|
||||
fail("Kyuubi version is not specified and could not found KYUUBI_VERSION in " +
|
||||
"kyuubi-build-info")
|
||||
}
|
||||
if (host == null) {
|
||||
fail("Must specify host for service")
|
||||
}
|
||||
if (port == null) {
|
||||
fail("Must specify port for service")
|
||||
}
|
||||
if (service == KyuubiCtlActionService.ENGINE && user == null) {
|
||||
fail("Must specify user name for engine")
|
||||
}
|
||||
}
|
||||
|
||||
private def validateListArguments(): Unit = {
|
||||
if (zkAddress == null) {
|
||||
fail("Zookeeper address is not specified and no default value to load")
|
||||
}
|
||||
if (nameSpace == null) {
|
||||
fail("Zookeeper namespace is not specified and no default value to load")
|
||||
}
|
||||
}
|
||||
|
||||
private def printUsageAndExit(exitCode: Int, unknownParam: Any = null): Unit = {
|
||||
if (unknownParam != null) {
|
||||
info("Unknown/unsupported param " + unknownParam)
|
||||
}
|
||||
val command = sys.env.getOrElse("_KYUUBI_CMD_USAGE",
|
||||
s"""
|
||||
|Kyuubi Ver $KYUUBI_VERSION.
|
||||
|Usage: kyuubi-ctl <create|get|delete|list> <server|engine> --zkAddress ...
|
||||
|--namespace ... --user ... --host ... --port ... --version""".stripMargin)
|
||||
info(command)
|
||||
|
||||
info(
|
||||
s"""
|
||||
|Command:
|
||||
| - create expose a service to a namespace on the zookeeper cluster of
|
||||
| zkAddress 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:
|
||||
| --zkAddress, -zk one of the zk ensemble address, using kyuubi-defaults/conf
|
||||
| if absent
|
||||
| --namespace, -ns the namespace, using kyuubi-defaults/conf if absent
|
||||
| --host, -h hostname or IP address of a service
|
||||
| --port, -p listening port of a service
|
||||
| --version, -V using the compiled KYUUBI_VERSION default, change it if the
|
||||
| active service is running in another
|
||||
| --user, -u for engine service only, the user name this engine belong to
|
||||
| --help, -I Show this help message and exit.
|
||||
| --verbose, -v Print additional debug output.
|
||||
""".stripMargin
|
||||
)
|
||||
|
||||
throw new KyuubiCtlException(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 = KyuubiCtlAction.CREATE
|
||||
actionParsed = true
|
||||
case GET =>
|
||||
action = KyuubiCtlAction.GET
|
||||
actionParsed = true
|
||||
case DELETE =>
|
||||
action = KyuubiCtlAction.DELETE
|
||||
actionParsed = true
|
||||
case LIST =>
|
||||
action = KyuubiCtlAction.LIST
|
||||
actionParsed = true
|
||||
case _ => findSwitches(arg) match {
|
||||
case HELP =>
|
||||
action = KyuubiCtlAction.HELP
|
||||
actionParsed = true
|
||||
case VERBOSE =>
|
||||
verbose = true
|
||||
case _ =>
|
||||
printUsageAndExit(-1, arg)
|
||||
}
|
||||
}
|
||||
offset += 1
|
||||
} else if (needContinueHandle() && !serviceParsed) {
|
||||
arg match {
|
||||
case SERVER =>
|
||||
service = KyuubiCtlActionService.SERVER
|
||||
serviceParsed = true
|
||||
offset += 1
|
||||
case ENGINE =>
|
||||
service = KyuubiCtlActionService.ENGINE
|
||||
serviceParsed = true
|
||||
offset += 1
|
||||
case _ => findSwitches(arg) match {
|
||||
case HELP =>
|
||||
action = KyuubiCtlAction.HELP
|
||||
offset += 1
|
||||
case VERBOSE =>
|
||||
verbose = true
|
||||
offset += 1
|
||||
case _ =>
|
||||
service = KyuubiCtlActionService.SERVER
|
||||
serviceParsed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
offset
|
||||
}
|
||||
|
||||
override def toString: String = {
|
||||
s"""Parsed arguments:
|
||||
| action $action
|
||||
| service $service
|
||||
| zkAddress $zkAddress
|
||||
| namespace $nameSpace
|
||||
| user $user
|
||||
| host $host
|
||||
| port $port
|
||||
| version $version
|
||||
| verbose $verbose
|
||||
""".stripMargin
|
||||
}
|
||||
|
||||
private def needContinueHandle(): Boolean = {
|
||||
action != KyuubiCtlAction.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_ADDRESS =>
|
||||
zkAddress = value
|
||||
|
||||
case NAMESPACE =>
|
||||
nameSpace = value
|
||||
|
||||
case USER =>
|
||||
user = value
|
||||
|
||||
case HOST =>
|
||||
host = value
|
||||
|
||||
case PORT =>
|
||||
port = value
|
||||
|
||||
case VERSION =>
|
||||
version = value
|
||||
|
||||
case HELP =>
|
||||
action = KyuubiCtlAction.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
|
||||
}
|
||||
}
|
||||
|
||||
private def fail(msg: String): Unit = throw new KyuubiException(msg)
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
private[kyuubi] abstract class KyuubiCtlArgumentsParser extends KyuubiCtlOptionParser
|
||||
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 org.apache.kyuubi.KyuubiException
|
||||
|
||||
class KyuubiCtlException(exitCode: Int)
|
||||
extends KyuubiException(s"Kyuubi Ctl exited with $exitCode")
|
||||
40
kyuubi-ctl/src/test/resources/log4j.properties
Normal file
40
kyuubi-ctl/src/test/resources/log4j.properties
Normal file
@ -0,0 +1,40 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
# Set everything to be logged to the file target/unit-tests.log
|
||||
log4j.rootLogger=DEBUG, CA, FA
|
||||
|
||||
#Console Appender
|
||||
log4j.appender.CA=org.apache.log4j.ConsoleAppender
|
||||
log4j.appender.CA.layout=org.apache.log4j.PatternLayout
|
||||
log4j.appender.CA.layout.ConversionPattern=%d{HH:mm:ss.SSS} %p %c: %m%n
|
||||
log4j.appender.CA.Threshold = FATAL
|
||||
|
||||
#File Appender
|
||||
log4j.appender.FA=org.apache.log4j.FileAppender
|
||||
log4j.appender.FA.append=false
|
||||
log4j.appender.FA.file=target/unit-tests.log
|
||||
log4j.appender.FA.layout=org.apache.log4j.PatternLayout
|
||||
log4j.appender.FA.layout.ConversionPattern=%d{HH:mm:ss.SSS} %t %p %c{2}: %m%n
|
||||
|
||||
# Set the logger level of File Appender to WARN
|
||||
log4j.appender.FA.Threshold = DEBUG
|
||||
|
||||
# SPARK-34128:Suppress undesirable TTransportException warnings involved in THRIFT-4805
|
||||
log4j.appender.console.filter.1=org.apache.log4j.varia.StringMatchFilter
|
||||
log4j.appender.console.filter.1.StringToMatch=Thrift error occurred during processing of message
|
||||
log4j.appender.console.filter.1.AcceptOnMatch=false
|
||||
@ -0,0 +1,204 @@
|
||||
/*
|
||||
* 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 org.apache.kyuubi.{KYUUBI_VERSION, KyuubiFunSuite}
|
||||
|
||||
class KyuubiCtlArgumentsSuite extends KyuubiFunSuite {
|
||||
val zkAddress = "localhost:2181"
|
||||
val namespace = "kyuubi"
|
||||
val user = "kyuubi"
|
||||
val host = "localhost"
|
||||
val port = "10000"
|
||||
|
||||
/** Check whether the script exits and the given search string is printed. */
|
||||
private def testPrematureExit(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 KyuubiCtlArguments(args)
|
||||
} 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("create", "get", "list", "delete").foreach { command =>
|
||||
Seq("server", "engine").foreach { service =>
|
||||
val args = Seq(
|
||||
command, service,
|
||||
"--zkAddress", zkAddress,
|
||||
"--namespace", namespace,
|
||||
"--user", user,
|
||||
"--host", host,
|
||||
"--port", port,
|
||||
"--version", KYUUBI_VERSION
|
||||
)
|
||||
val opArgs = new KyuubiCtlArguments(args)
|
||||
assert(opArgs.action.toString.equalsIgnoreCase(command))
|
||||
assert(opArgs.service.toString.equalsIgnoreCase(service))
|
||||
assert(opArgs.zkAddress == zkAddress)
|
||||
assert(opArgs.nameSpace == namespace)
|
||||
assert(opArgs.user == user)
|
||||
assert(opArgs.host == host)
|
||||
assert(opArgs.port == port)
|
||||
assert(opArgs.version == KYUUBI_VERSION)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("treat --help as action") {
|
||||
val args = Seq("-I")
|
||||
val opArgs = new KyuubiCtlArguments(args)
|
||||
assert(opArgs.action == KyuubiCtlAction.HELP)
|
||||
assert(opArgs.version == KYUUBI_VERSION)
|
||||
|
||||
val args2 = Seq(
|
||||
"create", "server",
|
||||
s"--user=$user",
|
||||
"-h", host,
|
||||
"--verbose",
|
||||
"--help",
|
||||
"--port", port
|
||||
)
|
||||
val opArgs2 = new KyuubiCtlArguments(args2)
|
||||
assert(opArgs2.action == KyuubiCtlAction.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("-v"), "Usage: kyuubi-ctl")
|
||||
}
|
||||
|
||||
test("prints error with unrecognized options") {
|
||||
testPrematureExit(Array("create", "--unkonwn"), "Unknown/unsupported param --unkonwn")
|
||||
testPrematureExit(Array("--unkonwn"), "Unknown/unsupported param --unkonwn")
|
||||
}
|
||||
|
||||
test("test invalid arguments") {
|
||||
testPrematureExit(Array("create", "--user"), "Missing argument for option '--user'")
|
||||
}
|
||||
|
||||
test("test extra unused arguments") {
|
||||
val args = Array(
|
||||
"list",
|
||||
"extraArg1", "extraArg2"
|
||||
)
|
||||
testPrematureExit(args, "Unknown/unsupported param extraArg1")
|
||||
}
|
||||
|
||||
test("test list action arguments") {
|
||||
val args = Array(
|
||||
"list"
|
||||
)
|
||||
testPrematureExit(args, "Zookeeper address is not specified")
|
||||
|
||||
val args2 = Array(
|
||||
"list",
|
||||
"--zkAddress", zkAddress
|
||||
)
|
||||
testPrematureExit(args2, "Zookeeper namespace is not specified")
|
||||
|
||||
val args3 = Array(
|
||||
"list",
|
||||
"--zkAddress", zkAddress,
|
||||
"--namespace", namespace
|
||||
)
|
||||
val opArgs = new KyuubiCtlArguments(args3)
|
||||
assert(opArgs.action == KyuubiCtlAction.LIST)
|
||||
}
|
||||
|
||||
test("test create/get/delete action arguments") {
|
||||
Seq("create", "get", "delete").foreach { op =>
|
||||
val args = Array(
|
||||
op
|
||||
)
|
||||
testPrematureExit(args, "Zookeeper address is not specified")
|
||||
|
||||
val args2 = Array(
|
||||
op,
|
||||
"--zkAddress", zkAddress
|
||||
)
|
||||
testPrematureExit(args2, "Zookeeper namespace is not specified")
|
||||
|
||||
val args3 = Array(
|
||||
op,
|
||||
"--zkAddress", zkAddress,
|
||||
"--namespace", namespace
|
||||
)
|
||||
testPrematureExit(args3, "Must specify host")
|
||||
|
||||
val args4 = Array(
|
||||
op,
|
||||
"--zkAddress", zkAddress,
|
||||
"--namespace", namespace,
|
||||
"-h", host
|
||||
)
|
||||
testPrematureExit(args4, "Must specify port")
|
||||
|
||||
val args5 = Array(
|
||||
op, "engine",
|
||||
"--zkAddress", zkAddress,
|
||||
"--namespace", namespace,
|
||||
"-h", host,
|
||||
"-p", port
|
||||
)
|
||||
testPrematureExit(args5, "Must specify user name for engine")
|
||||
|
||||
val args6 = Array(
|
||||
op, "server",
|
||||
"--zkAddress", zkAddress,
|
||||
"--namespace", namespace,
|
||||
"-h", host,
|
||||
"-p", port
|
||||
)
|
||||
val opArgs6 = new KyuubiCtlArguments(args6)
|
||||
assert(opArgs6.action.toString.equalsIgnoreCase(op))
|
||||
}
|
||||
}
|
||||
|
||||
test("test with switches at head") {
|
||||
val args = Seq("--verbose", "list", "engine", "-zk", zkAddress, "-ns", namespace)
|
||||
val opArgs = new KyuubiCtlArguments(args)
|
||||
assert(opArgs.verbose)
|
||||
assert(opArgs.action == KyuubiCtlAction.LIST)
|
||||
assert(opArgs.service == KyuubiCtlActionService.ENGINE)
|
||||
|
||||
val args2 = Seq("list", "-v", "engine", "-zk", zkAddress, "-ns", namespace)
|
||||
val opArgs2 = new KyuubiCtlArguments(args2)
|
||||
assert(opArgs2.verbose)
|
||||
assert(opArgs2.action == KyuubiCtlAction.LIST)
|
||||
assert(opArgs2.service == KyuubiCtlActionService.ENGINE)
|
||||
|
||||
val args3 = Seq("list", "--verbose", "--help", "engine", "-zk", zkAddress, "-ns", namespace)
|
||||
val opArgs3 = new KyuubiCtlArguments(args3)
|
||||
assert(opArgs3.verbose)
|
||||
assert(opArgs3.action == KyuubiCtlAction.HELP)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user