args);
+
+ /**
+ * Parse a list of kyuubi-ctl command line options.
+ *
+ * See KyuubiCtlArguments.scala for a more formal description of available options.
+ *
+ * @throws IllegalArgumentException If an error is found during parsing.
+ */
+ protected final void parse(List args) {
+ Pattern eqSeparatedOpt = Pattern.compile("(--[^=]+)=(.+)");
+
+ int idx = parseActionAndService(args);
+ for (; idx < args.size(); idx++) {
+ String arg = args.get(idx);
+ String value = null;
+
+ Matcher m = eqSeparatedOpt.matcher(arg);
+ if (m.matches()) {
+ arg = m.group(1);
+ value = m.group(2);
+ }
+
+ // Look for options with a value.
+ String name = findCliOption(arg, opts);
+ if (name != null) {
+ if (value == null) {
+ if (idx == args.size() - 1) {
+ throw new IllegalArgumentException(
+ String.format("Missing argument for option '%s'.", arg));
+ }
+ idx++;
+ value = args.get(idx);
+ }
+ if (!handle(name, value)) {
+ break;
+ }
+ continue;
+ }
+
+ // Look for a switch.
+ name = findCliOption(arg, switches);
+ if (name != null) {
+ if (!handle(name, null)) {
+ break;
+ }
+ continue;
+ }
+
+ handleUnknown(arg);
+ }
+ }
+
+ /**
+ * Callback for when an option with an argument is parsed.
+ *
+ * @param opt The long name of the cli option (might differ from actual command line).
+ * @param value The value. This will be null if the option does not take a value.
+ * @return Whether to continue parsing the argument list.
+ */
+ protected abstract boolean handle(String opt, String value);
+
+ /**
+ * Callback for when an unrecognized option is parsed.
+ *
+ * @param opt Unrecognized option from the command line.
+ * @return Whether to continue parsing the argument list.
+ */
+ protected abstract boolean handleUnknown(String opt);
+
+ private String findCliOption(String name, String[][] available) {
+ for (String[] candidates : available) {
+ for (String candidate : candidates) {
+ if (candidate.equals(name)) {
+ return candidates[0];
+ }
+ }
+ }
+ return null;
+ }
+
+ protected String findSwitches(String name) {
+ return findCliOption(name, switches);
+ }
+}
diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/KyuubiCtl.scala b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/KyuubiCtl.scala
new file mode 100644
index 000000000..25dd50b19
--- /dev/null
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/KyuubiCtl.scala
@@ -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
+}
diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/KyuubiCtlArguments.scala b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/KyuubiCtlArguments.scala
new file mode 100644
index 000000000..06aacadd5
--- /dev/null
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/KyuubiCtlArguments.scala
@@ -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 --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)
+}
diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/KyuubiCtlArgumentsParser.scala b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/KyuubiCtlArgumentsParser.scala
new file mode 100644
index 000000000..e7ace8606
--- /dev/null
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/KyuubiCtlArgumentsParser.scala
@@ -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
diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/KyuubiCtlException.scala b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/KyuubiCtlException.scala
new file mode 100644
index 000000000..c6e77ae26
--- /dev/null
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/KyuubiCtlException.scala
@@ -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")
diff --git a/kyuubi-ctl/src/test/resources/log4j.properties b/kyuubi-ctl/src/test/resources/log4j.properties
new file mode 100644
index 000000000..fee75dc67
--- /dev/null
+++ b/kyuubi-ctl/src/test/resources/log4j.properties
@@ -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
diff --git a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/KyuubiCtlArgumentsSuite.scala b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/KyuubiCtlArgumentsSuite.scala
new file mode 100644
index 000000000..5f4f81d28
--- /dev/null
+++ b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/KyuubiCtlArgumentsSuite.scala
@@ -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)
+ }
+}
diff --git a/pom.xml b/pom.xml
index b1948c383..f04e17343 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,6 +30,7 @@
externals/kyuubi-spark-sql-engine
kyuubi-assembly
kyuubi-common
+ kyuubi-ctl
kyuubi-ha
kyuubi-main
kyuubi-metrics