[KYUUBI #304] Add Kyuubi Ctl arguments parser

![turboFei](https://badgen.net/badge/Hello/turboFei/green) [![Closes #465](https://badgen.net/badge/Preview/Closes%20%23465/blue)](https://github.com/yaooqinn/kyuubi/pull/465) ![919](https://badgen.net/badge/%2B/919/red) ![0](https://badgen.net/badge/-/0/green) ![22](https://badgen.net/badge/commits/22/yellow) ![Test Plan](https://badgen.net/badge/Missing/Test%20Plan/ff0000) [<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:
fwang12 2021-03-29 20:43:13 +08:00 committed by Kent Yao
parent a66a9054ef
commit 0aafc55bba
No known key found for this signature in database
GPG Key ID: F7051850A0AF904D
12 changed files with 919 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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-34128Suppress 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

View File

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

View File

@ -30,6 +30,7 @@
<module>externals/kyuubi-spark-sql-engine</module>
<module>kyuubi-assembly</module>
<module>kyuubi-common</module>
<module>kyuubi-ctl</module>
<module>kyuubi-ha</module>
<module>kyuubi-main</module>
<module>kyuubi-metrics</module>