This is an automated email from the ASF dual-hosted git repository.

ulyssesyou pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-kyuubi.git


The following commit(s) were added to refs/heads/master by this push:
     new a4622301c [KYUUBI #2765][SUB-TASK][KPIP-4] Refactor current kyuubi-ctl
a4622301c is described below

commit a4622301c432702c44897b400c302d00c4549af8
Author: Tianlin Liao <[email protected]>
AuthorDate: Tue May 31 18:41:26 2022 +0800

    [KYUUBI #2765][SUB-TASK][KPIP-4] Refactor current kyuubi-ctl
    
    ### _Why are the changes needed?_
    
    To close #2765
    
    ### _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.apache.org/docs/latest/develop_tools/testing.html#running-tests)
 locally before make a pull request
    
    Closes #2766 from lightning-L/kyuubi-2628.
    
    Closes #2765
    
    749eebd7 [Tianlin Liao] rename ServiceControlXXX to ControlXXX
    842265fb [Tianlin Liao] [KYUUBI #2765] refactor current kyuubi-ctl
    
    Authored-by: Tianlin Liao <[email protected]>
    Signed-off-by: ulysses-you <[email protected]>
---
 .../scala/org/apache/kyuubi/ctl/CliConfig.scala    |  50 ++++
 .../scala/org/apache/kyuubi/ctl/CommandLine.scala  | 140 +++++++++
 .../scala/org/apache/kyuubi/ctl/ControlCli.scala   |  97 +++++++
 .../apache/kyuubi/ctl/ControlCliArguments.scala    |  97 +++++++
 ...arser.scala => ControlCliArgumentsParser.scala} |  24 +-
 ...liException.scala => ControlCliException.scala} |   2 +-
 ...rviceControlCliException.scala => Render.scala} |  18 +-
 .../org/apache/kyuubi/ctl/ServiceControlCli.scala  | 256 -----------------
 .../kyuubi/ctl/ServiceControlCliArguments.scala    | 312 ---------------------
 .../scala/org/apache/kyuubi/ctl/cmd/Command.scala  | 164 +++++++++++
 .../org/apache/kyuubi/ctl/cmd/CreateCommand.scala  |  94 +++++++
 .../org/apache/kyuubi/ctl/cmd/DeleteCommand.scala  |  64 +++++
 .../org/apache/kyuubi/ctl/cmd/GetCommand.scala     |  47 ++++
 .../org/apache/kyuubi/ctl/cmd/ListCommand.scala    |  53 ++++
 ...sSuite.scala => ControlCliArgumentsSuite.scala} |  62 ++--
 ...ControlCliSuite.scala => ControlCliSuite.scala} |  47 ++--
 16 files changed, 883 insertions(+), 644 deletions(-)

diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/CliConfig.scala 
b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/CliConfig.scala
new file mode 100644
index 000000000..1fc04b33b
--- /dev/null
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/CliConfig.scala
@@ -0,0 +1,50 @@
+/*
+ * 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.ctl.ServiceControlAction.ControlAction
+import org.apache.kyuubi.ctl.ServiceControlObject.ControlObject
+
+private[ctl] object ServiceControlAction extends Enumeration {
+  type ControlAction = Value
+  val CREATE, GET, DELETE, LIST = Value
+}
+
+private[ctl] object ServiceControlObject extends Enumeration {
+  type ControlObject = Value
+  val SERVER, ENGINE = Value
+}
+
+case class CliConfig(
+    action: ControlAction = null,
+    service: ControlObject = ServiceControlObject.SERVER,
+    commonOpts: CommonOpts = CommonOpts(),
+    engineOpts: EngineOpts = EngineOpts())
+
+case class CommonOpts(
+    zkQuorum: String = null,
+    namespace: String = null,
+    host: String = null,
+    port: String = null,
+    version: String = null,
+    verbose: Boolean = false)
+
+case class EngineOpts(
+    user: String = null,
+    engineType: String = null,
+    engineSubdomain: String = null,
+    engineShareLevel: String = null)
diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/CommandLine.scala 
b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/CommandLine.scala
new file mode 100644
index 000000000..903c6cd48
--- /dev/null
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/CommandLine.scala
@@ -0,0 +1,140 @@
+/*
+ * 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.{OParser, OParserBuilder}
+
+import org.apache.kyuubi.KYUUBI_VERSION
+
+object CommandLine {
+
+  def getOptionParser(builder: OParserBuilder[CliConfig]): OParser[Unit, 
CliConfig] = {
+    import builder._
+    OParser.sequence(
+      programName("kyuubi-ctl"),
+      head("kyuubi", KYUUBI_VERSION),
+      common(builder),
+      create(builder),
+      get(builder),
+      delete(builder),
+      list(builder),
+      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."))
+  }
+
+  private def common(builder: OParserBuilder[CliConfig]): OParser[_, 
CliConfig] = {
+    import builder._
+    OParser.sequence(
+      opt[String]("zk-quorum").abbr("zk")
+        .action((v, c) => c.copy(commonOpts = c.commonOpts.copy(zkQuorum = v)))
+        .text("The connection string for the zookeeper ensemble," +
+          " using zk quorum manually."),
+      opt[String]('n', "namespace")
+        .action((v, c) => c.copy(commonOpts = c.commonOpts.copy(namespace = 
v)))
+        .text("The namespace, using kyuubi-defaults/conf if absent."),
+      opt[String]('s', "host")
+        .action((v, c) => c.copy(commonOpts = c.commonOpts.copy(host = v)))
+        .text("Hostname or IP address of a service."),
+      opt[String]('p', "port")
+        .action((v, c) => c.copy(commonOpts = c.commonOpts.copy(port = v)))
+        .text("Listening port of a service."),
+      opt[String]('v', "version")
+        .action((v, c) => c.copy(commonOpts = c.commonOpts.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(commonOpts = c.commonOpts.copy(verbose = 
true)))
+        .text("Print additional debug output."))
+  }
+
+  private def create(builder: OParserBuilder[CliConfig]): OParser[_, 
CliConfig] = {
+    import builder._
+    OParser.sequence(
+      note(""),
+      cmd("create")
+        .action((_, c) => c.copy(action = ServiceControlAction.CREATE))
+        .children(
+          serverCmd(builder).text("\tExpose Kyuubi server instance to another 
domain.")))
+  }
+
+  private def get(builder: OParserBuilder[CliConfig]): OParser[_, CliConfig] = 
{
+    import builder._
+    OParser.sequence(
+      note(""),
+      cmd("get")
+        .text("\tGet the service/engine node info, host and port needed.")
+        .action((_, c) => c.copy(action = ServiceControlAction.GET))
+        .children(
+          serverCmd(builder).text("\tGet Kyuubi server info of domain"),
+          engineCmd(builder).text("\tGet Kyuubi engine info belong to a 
user.")))
+
+  }
+
+  private def delete(builder: OParserBuilder[CliConfig]): OParser[_, 
CliConfig] = {
+    import builder._
+    OParser.sequence(
+      note(""),
+      cmd("delete")
+        .text("\tDelete the specified service/engine node, host and port 
needed.")
+        .action((_, c) => c.copy(action = ServiceControlAction.DELETE))
+        .children(
+          serverCmd(builder).text("\tDelete the specified service node for a 
domain"),
+          engineCmd(builder).text("\tDelete the specified engine node for 
user.")))
+
+  }
+
+  private def list(builder: OParserBuilder[CliConfig]): OParser[_, CliConfig] 
= {
+    import builder._
+    OParser.sequence(
+      note(""),
+      cmd("list")
+        .text("\tList all the service/engine nodes for a particular domain.")
+        .action((_, c) => c.copy(action = ServiceControlAction.LIST))
+        .children(
+          serverCmd(builder).text("\tList all the service nodes for a 
particular domain"),
+          engineCmd(builder).text("\tList all the engine nodes for a user")))
+
+  }
+
+  private def serverCmd(builder: OParserBuilder[CliConfig]): OParser[_, 
CliConfig] = {
+    import builder._
+    cmd("server").action((_, c) => c.copy(service = 
ServiceControlObject.SERVER))
+  }
+
+  private def engineCmd(builder: OParserBuilder[CliConfig]): OParser[_, 
CliConfig] = {
+    import builder._
+    cmd("engine").action((_, c) => c.copy(service = 
ServiceControlObject.ENGINE))
+      .children(
+        opt[String]('u', "user")
+          .action((v, c) => c.copy(engineOpts = c.engineOpts.copy(user = v)))
+          .text("The user name this engine belong to."),
+        opt[String]("engine-type").abbr("et")
+          .action((v, c) => c.copy(engineOpts = c.engineOpts.copy(engineType = 
v)))
+          .text("The engine type this engine belong to."),
+        opt[String]("engine-subdomain").abbr("es")
+          .action((v, c) => c.copy(engineOpts = 
c.engineOpts.copy(engineSubdomain = v)))
+          .text("The engine subdomain this engine belong to."),
+        opt[String]("engine-share-level").abbr("esl")
+          .action((v, c) => c.copy(engineOpts = 
c.engineOpts.copy(engineShareLevel = v)))
+          .text("The engine share level this engine belong to."))
+  }
+
+}
diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ControlCli.scala 
b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ControlCli.scala
new file mode 100644
index 000000000..ab87568b4
--- /dev/null
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ControlCli.scala
@@ -0,0 +1,97 @@
+/*
+ * 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.Logging
+
+/**
+ * Main gateway of launching a Kyuubi Ctl action.
+ */
+private[kyuubi] class ControlCli extends Logging {
+
+  def doAction(args: Array[String]): Unit = {
+    // Initialize logging if it hasn't been done yet.
+    // Set log level ERROR
+    initializeLoggerIfNecessary(true)
+
+    val ctlArgs = parseArguments(args)
+
+    // when parse failed, exit
+    if (ctlArgs.cliArgs == null) {
+      sys.exit(1)
+    }
+
+    val verbose = ctlArgs.cliArgs.commonOpts.verbose
+    if (verbose) {
+      super.info(ctlArgs.toString)
+    }
+
+    ctlArgs.command.run()
+  }
+
+  protected def parseArguments(args: Array[String]): ControlCliArguments = {
+    new ControlCliArguments(args)
+  }
+
+}
+
+object ControlCli extends CommandLineUtils with Logging {
+  override def main(args: Array[String]): Unit = {
+    val ctl = new ControlCli() {
+      self =>
+      override protected def parseArguments(args: Array[String]): 
ControlCliArguments = {
+        new ControlCliArguments(args) {
+          override def info(msg: => Any): Unit = self.info(msg)
+
+          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)
+          }
+        }
+      }
+
+      override def info(msg: => Any): Unit = printMessage(msg)
+
+      override def warn(msg: => Any): Unit = printMessage(s"Warning: $msg")
+
+      override def error(msg: => Any): Unit = printMessage(s"Error: $msg")
+
+      override def doAction(args: Array[String]): Unit = {
+        try {
+          super.doAction(args)
+          exitFn(0)
+        } catch {
+          case e: ControlCliException =>
+            exitFn(e.exitCode)
+        }
+      }
+    }
+
+    ctl.doAction(args)
+  }
+
+}
diff --git 
a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ControlCliArguments.scala 
b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ControlCliArguments.scala
new file mode 100644
index 000000000..29d4724d5
--- /dev/null
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ControlCliArguments.scala
@@ -0,0 +1,97 @@
+/*
+ * 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.OParser
+
+import org.apache.kyuubi.Logging
+import org.apache.kyuubi.ctl.cmd._
+
+class ControlCliArguments(args: Seq[String], env: Map[String, String] = 
sys.env)
+  extends ControlCliArgumentsParser with Logging {
+
+  var cliArgs: CliConfig = null
+
+  var command: Command = null
+
+  // Set parameters from command line arguments
+  parse(args)
+
+  lazy val cliParser = parser()
+
+  override def parser(): OParser[Unit, CliConfig] = {
+    val builder = OParser.builder[CliConfig]
+    CommandLine.getOptionParser(builder)
+  }
+
+  private[kyuubi] lazy val effectSetup = new KyuubiOEffectSetup
+
+  override def parse(args: Seq[String]): Unit = {
+    OParser.runParser(cliParser, args, CliConfig()) match {
+      case (result, effects) =>
+        OParser.runEffects(effects, effectSetup)
+        result match {
+          case Some(arguments) =>
+            command = getCommand(arguments)
+            command.preProcess()
+            cliArgs = command.cliArgs
+          case _ =>
+          // arguments are bad, exit
+        }
+    }
+  }
+
+  private def getCommand(cliArgs: CliConfig): Command = {
+    cliArgs.action match {
+      case ServiceControlAction.CREATE => new CreateCommand(cliArgs)
+      case ServiceControlAction.GET => new GetCommand(cliArgs)
+      case ServiceControlAction.DELETE => new DeleteCommand(cliArgs)
+      case ServiceControlAction.LIST => new ListCommand(cliArgs)
+      case _ => null
+    }
+  }
+
+  override def toString: String = {
+    cliArgs.service match {
+      case ServiceControlObject.SERVER =>
+        s"""Parsed arguments:
+           |  action                  ${cliArgs.action}
+           |  service                 ${cliArgs.service}
+           |  zkQuorum                ${cliArgs.commonOpts.zkQuorum}
+           |  namespace               ${cliArgs.commonOpts.namespace}
+           |  host                    ${cliArgs.commonOpts.host}
+           |  port                    ${cliArgs.commonOpts.port}
+           |  version                 ${cliArgs.commonOpts.version}
+           |  verbose                 ${cliArgs.commonOpts.verbose}
+        """.stripMargin
+      case ServiceControlObject.ENGINE =>
+        s"""Parsed arguments:
+           |  action                  ${cliArgs.action}
+           |  service                 ${cliArgs.service}
+           |  zkQuorum                ${cliArgs.commonOpts.zkQuorum}
+           |  namespace               ${cliArgs.commonOpts.namespace}
+           |  user                    ${cliArgs.engineOpts.user}
+           |  host                    ${cliArgs.commonOpts.host}
+           |  port                    ${cliArgs.commonOpts.port}
+           |  version                 ${cliArgs.commonOpts.version}
+           |  verbose                 ${cliArgs.commonOpts.verbose}
+        """.stripMargin
+      case _ => ""
+    }
+  }
+}
diff --git 
a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliArgumentsParser.scala
 
b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ControlCliArgumentsParser.scala
similarity index 58%
rename from 
kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliArgumentsParser.scala
rename to 
kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ControlCliArgumentsParser.scala
index 9c031e0cd..4a6707f39 100644
--- 
a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliArgumentsParser.scala
+++ 
b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ControlCliArgumentsParser.scala
@@ -19,32 +19,12 @@ package org.apache.kyuubi.ctl
 
 import scopt.OParser
 
-import org.apache.kyuubi.ctl.ServiceControlAction.ServiceControlAction
-import org.apache.kyuubi.ctl.ServiceControlObject.ServiceControlObject
-
-abstract private[kyuubi] 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,
-      engineType: String = null,
-      engineSubdomain: String = null,
-      engineShareLevel: String = null)
+abstract private[kyuubi] class ControlCliArgumentsParser {
 
   /**
    * Cli arguments parse rules.
    */
-  def parser(): OParser[Unit, CliArguments]
+  def parser(): OParser[Unit, CliConfig]
 
   /**
    * Parse a list of kyuubi-ctl command line options.
diff --git 
a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliException.scala
 b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ControlCliException.scala
similarity index 93%
copy from 
kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliException.scala
copy to 
kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ControlCliException.scala
index b38a7f754..86414f5c0 100644
--- 
a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliException.scala
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ControlCliException.scala
@@ -19,5 +19,5 @@ package org.apache.kyuubi.ctl
 
 import org.apache.kyuubi.KyuubiException
 
-private[kyuubi] case class ServiceControlCliException(exitCode: Int)
+private[kyuubi] case class ControlCliException(exitCode: Int)
   extends KyuubiException(s"Kyuubi service control cli exited with $exitCode")
diff --git 
a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliException.scala
 b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/Render.scala
similarity index 63%
rename from 
kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliException.scala
rename to kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/Render.scala
index b38a7f754..39dd618d3 100644
--- 
a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliException.scala
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/Render.scala
@@ -14,10 +14,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package org.apache.kyuubi.ctl
 
-import org.apache.kyuubi.KyuubiException
+import org.apache.kyuubi.ha.client.ServiceNodeInfo
+
+object Render {
 
-private[kyuubi] case class ServiceControlCliException(exitCode: Int)
-  extends KyuubiException(s"Kyuubi service control cli exited with $exitCode")
+  private[ctl] def renderServiceNodesInfo(
+      title: String,
+      serviceNodeInfo: Seq[ServiceNodeInfo],
+      verbose: Boolean): String = {
+    val header = Seq("Namespace", "Host", "Port", "Version")
+    val rows = serviceNodeInfo.sortBy(_.nodeName).map { sn =>
+      Seq(sn.namespace, sn.host, sn.port.toString, sn.version.getOrElse(""))
+    }
+    Tabulator.format(title, header, rows, verbose)
+  }
+}
diff --git 
a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCli.scala 
b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCli.scala
deleted file mode 100644
index 0c8010740..000000000
--- a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCli.scala
+++ /dev/null
@@ -1,256 +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 scala.collection.mutable.ListBuffer
-
-import org.apache.kyuubi.Logging
-import org.apache.kyuubi.config.KyuubiConf.{ENGINE_SHARE_LEVEL, 
ENGINE_SHARE_LEVEL_SUBDOMAIN, ENGINE_TYPE}
-import org.apache.kyuubi.ha.HighAvailabilityConf._
-import org.apache.kyuubi.ha.client.{DiscoveryClientProvider, ServiceNodeInfo}
-import org.apache.kyuubi.ha.client.DiscoveryClient
-import org.apache.kyuubi.ha.client.DiscoveryPaths
-
-private[ctl] object ServiceControlAction extends Enumeration {
-  type ServiceControlAction = Value
-  val CREATE, GET, DELETE, LIST = Value
-}
-
-private[ctl] object ServiceControlObject extends Enumeration {
-  type ServiceControlObject = Value
-  val SERVER, ENGINE = Value
-}
-
-/**
- * Main gateway of launching a Kyuubi Ctl action.
- */
-private[kyuubi] class ServiceControlCli extends Logging {
-  import DiscoveryClientProvider._
-  import ServiceControlCli._
-
-  private var verbose: Boolean = false
-
-  def doAction(args: Array[String]): Unit = {
-    // Initialize logging if it hasn't been done yet.
-    // Set log level ERROR
-    initializeLoggerIfNecessary(true)
-
-    val ctlArgs = parseArguments(args)
-
-    // when parse failed, exit
-    if (ctlArgs.cliArgs == null) {
-      sys.exit(1)
-    }
-
-    verbose = ctlArgs.cliArgs.verbose
-    if (verbose) {
-      super.info(ctlArgs.toString)
-    }
-    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)
-    }
-  }
-
-  protected def parseArguments(args: Array[String]): 
ServiceControlCliArguments = {
-    new ServiceControlCliArguments(args)
-  }
-
-  /**
-   * Expose Kyuubi server instance to another domain.
-   */
-  private def create(args: ServiceControlCliArguments): Unit = {
-    val kyuubiConf = args.conf
-
-    kyuubiConf.setIfMissing(HA_ADDRESSES, args.cliArgs.zkQuorum)
-    withDiscoveryClient(kyuubiConf) { discoveryClient =>
-      val fromNamespace =
-        DiscoveryPaths.makePath(null, kyuubiConf.get(HA_NAMESPACE))
-      val toNamespace = getZkNamespace(args)
-
-      val currentServerNodes = 
discoveryClient.getServiceNodesInfo(fromNamespace)
-      val exposedServiceNodes = ListBuffer[ServiceNodeInfo]()
-
-      if (currentServerNodes.nonEmpty) {
-        def doCreate(zc: DiscoveryClient): Unit = {
-          currentServerNodes.foreach { sn =>
-            info(s"Exposing server instance:${sn.instance} with 
version:${sn.version}" +
-              s" from $fromNamespace to $toNamespace")
-            val newNodePath = zc.createAndGetServiceNode(
-              kyuubiConf,
-              args.cliArgs.namespace,
-              sn.instance,
-              sn.version,
-              true)
-            exposedServiceNodes += sn.copy(
-              namespace = toNamespace,
-              nodeName = newNodePath.split("/").last)
-          }
-        }
-
-        if (kyuubiConf.get(HA_ADDRESSES) == args.cliArgs.zkQuorum) {
-          doCreate(discoveryClient)
-        } else {
-          kyuubiConf.set(HA_ADDRESSES, args.cliArgs.zkQuorum)
-          withDiscoveryClient(kyuubiConf)(doCreate)
-        }
-      }
-
-      val title = "Created zookeeper service nodes"
-      info(renderServiceNodesInfo(title, exposedServiceNodes, verbose))
-    }
-  }
-
-  /**
-   * List Kyuubi server nodes info.
-   */
-  private def list(args: ServiceControlCliArguments, filterHostPort: Boolean): 
Unit = {
-    withDiscoveryClient(args.conf) { discoveryClient =>
-      val znodeRoot = getZkNamespace(args)
-      val hostPortOpt =
-        if (filterHostPort) {
-          Some((args.cliArgs.host, args.cliArgs.port.toInt))
-        } else None
-      val nodes = getServiceNodes(discoveryClient, znodeRoot, hostPortOpt)
-
-      val title = "Zookeeper service nodes"
-      info(renderServiceNodesInfo(title, nodes, verbose))
-    }
-  }
-
-  private def getServiceNodes(
-      discoveryClient: DiscoveryClient,
-      znodeRoot: String,
-      hostPortOpt: Option[(String, Int)]): Seq[ServiceNodeInfo] = {
-    val serviceNodes = discoveryClient.getServiceNodesInfo(znodeRoot)
-    hostPortOpt match {
-      case Some((host, port)) => serviceNodes.filter { sn =>
-          sn.host == host && sn.port == port
-        }
-      case _ => serviceNodes
-    }
-  }
-
-  /**
-   * Delete zookeeper service node with specified host port.
-   */
-  private def delete(args: ServiceControlCliArguments): Unit = {
-    withDiscoveryClient(args.conf) { discoveryClient =>
-      val znodeRoot = getZkNamespace(args)
-      val hostPortOpt = Some((args.cliArgs.host, args.cliArgs.port.toInt))
-      val nodesToDelete = getServiceNodes(discoveryClient, znodeRoot, 
hostPortOpt)
-
-      val deletedNodes = ListBuffer[ServiceNodeInfo]()
-      nodesToDelete.foreach { node =>
-        val nodePath = s"$znodeRoot/${node.nodeName}"
-        info(s"Deleting zookeeper service node:$nodePath")
-        try {
-          discoveryClient.delete(nodePath)
-          deletedNodes += node
-        } catch {
-          case e: Exception =>
-            error(s"Failed to delete zookeeper service node:$nodePath", e)
-        }
-      }
-
-      val title = "Deleted zookeeper service nodes"
-      info(renderServiceNodesInfo(title, deletedNodes, verbose))
-    }
-  }
-}
-
-object ServiceControlCli extends CommandLineUtils with Logging {
-  override def main(args: Array[String]): Unit = {
-    val ctl = new ServiceControlCli() {
-      self =>
-      override protected def parseArguments(args: Array[String]): 
ServiceControlCliArguments = {
-        new ServiceControlCliArguments(args) {
-          override def info(msg: => Any): Unit = self.info(msg)
-
-          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)
-          }
-        }
-      }
-
-      override def info(msg: => Any): Unit = printMessage(msg)
-
-      override def warn(msg: => Any): Unit = printMessage(s"Warning: $msg")
-
-      override def error(msg: => Any): Unit = printMessage(s"Error: $msg")
-
-      override def doAction(args: Array[String]): Unit = {
-        try {
-          super.doAction(args)
-          exitFn(0)
-        } catch {
-          case e: ServiceControlCliException =>
-            exitFn(e.exitCode)
-        }
-      }
-    }
-
-    ctl.doAction(args)
-  }
-
-  private[ctl] def getZkNamespace(args: ServiceControlCliArguments): String = {
-    args.cliArgs.service match {
-      case ServiceControlObject.SERVER =>
-        DiscoveryPaths.makePath(null, args.cliArgs.namespace)
-      case ServiceControlObject.ENGINE =>
-        val engineType = Some(args.cliArgs.engineType)
-          .filter(_ != null).filter(_.nonEmpty)
-          .getOrElse(args.conf.get(ENGINE_TYPE))
-        val engineSubdomain = Some(args.cliArgs.engineSubdomain)
-          .filter(_ != null).filter(_.nonEmpty)
-          
.getOrElse(args.conf.get(ENGINE_SHARE_LEVEL_SUBDOMAIN).getOrElse("default"))
-        val engineShareLevel = Some(args.cliArgs.engineShareLevel)
-          .filter(_ != null).filter(_.nonEmpty)
-          .getOrElse(args.conf.get(ENGINE_SHARE_LEVEL))
-        // The path of the engine defined in zookeeper comes from
-        // org.apache.kyuubi.engine.EngineRef#engineSpace
-        DiscoveryPaths.makePath(
-          
s"${args.cliArgs.namespace}_${args.cliArgs.version}_${engineShareLevel}_${engineType}",
-          args.cliArgs.user,
-          Array(engineSubdomain))
-    }
-  }
-
-  private[ctl] def renderServiceNodesInfo(
-      title: String,
-      serviceNodeInfo: Seq[ServiceNodeInfo],
-      verbose: Boolean): String = {
-    val header = Seq("Namespace", "Host", "Port", "Version")
-    val rows = serviceNodeInfo.sortBy(_.nodeName).map { sn =>
-      Seq(sn.namespace, sn.host, sn.port.toString, sn.version.getOrElse(""))
-    }
-    Tabulator.format(title, header, rows, verbose)
-  }
-}
diff --git 
a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliArguments.scala
 
b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliArguments.scala
deleted file mode 100644
index efa22cb58..000000000
--- 
a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/ServiceControlCliArguments.scala
+++ /dev/null
@@ -1,312 +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.net.InetAddress
-
-import scopt.OParser
-
-import org.apache.kyuubi.{KYUUBI_VERSION, KyuubiException, Logging}
-import org.apache.kyuubi.config.KyuubiConf
-import org.apache.kyuubi.ha.HighAvailabilityConf._
-
-class ServiceControlCliArguments(args: Seq[String], env: Map[String, String] = 
sys.env)
-  extends ServiceControlCliArgumentsParser with Logging {
-
-  var cliArgs: CliArguments = null
-
-  val conf = KyuubiConf().loadFileDefaults()
-
-  // Set parameters from command line arguments
-  parse(args)
-
-  lazy val cliParser = parser()
-
-  override def parser(): OParser[Unit, CliArguments] = {
-    val builder = OParser.builder[CliArguments]
-    import builder._
-
-    // 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 engineTypeOps = opt[String]("engine-type").abbr("et")
-      .action((v, c) => c.copy(engineType = v))
-      .text("The engine type this engine belong to.")
-
-    val engineSubdomainOps = opt[String]("engine-subdomain").abbr("es")
-      .action((v, c) => c.copy(engineSubdomain = v))
-      .text("The engine subdomain this engine belong to.")
-
-    val engineShareLevelOps = opt[String]("engine-share-level").abbr("esl")
-      .action((v, c) => c.copy(engineShareLevel = v))
-      .text("The engine share level 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)
-              .children(engineTypeOps)
-              .children(engineSubdomainOps)
-              .children(engineShareLevelOps)
-              .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)
-              .children(engineTypeOps)
-              .children(engineSubdomainOps)
-              .children(engineShareLevelOps)
-              .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)
-              .children(engineTypeOps)
-              .children(engineSubdomainOps)
-              .children(engineShareLevelOps)
-              .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_ADDRESSES.key).foreach { v =>
-        if (arguments.verbose) {
-          super.info(s"Zookeeper quorum is not specified, use value from 
default conf:$v")
-        }
-        arguments = arguments.copy(zkQuorum = v)
-      }
-    }
-
-    if (arguments.namespace == null) {
-      arguments = arguments.copy(namespace = conf.get(HA_NAMESPACE))
-      if (arguments.verbose) {
-        super.info(s"Zookeeper namespace is not specified, use value from 
default conf:" +
-          s"${arguments.namespace}")
-      }
-    }
-
-    if (arguments.version == null) {
-      if (arguments.verbose) {
-        super.info(s"version is not specified, use built-in 
KYUUBI_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 = {
-    cliArgs.action match {
-      case ServiceControlAction.CREATE => validateCreateActionArguments()
-      case ServiceControlAction.GET => validateGetDeleteActionArguments()
-      case ServiceControlAction.DELETE => validateGetDeleteActionArguments()
-      case ServiceControlAction.LIST => validateListActionArguments()
-      case _ => // do nothing
-    }
-  }
-
-  private def validateCreateActionArguments(): Unit = {
-    if (cliArgs.service != ServiceControlObject.SERVER) {
-      fail("Only support expose Kyuubi server instance to another domain")
-    }
-    validateZkArguments()
-
-    val defaultNamespace = conf.getOption(HA_NAMESPACE.key)
-      .getOrElse(HA_NAMESPACE.defaultValStr)
-    if (defaultNamespace.equals(cliArgs.namespace)) {
-      fail(
-        s"""
-           |Only support expose Kyuubi server instance to another domain, a 
different namespace
-           |than the default namespace [$defaultNamespace] should be specified.
-        """.stripMargin)
-    }
-  }
-
-  private def validateGetDeleteActionArguments(): Unit = {
-    validateZkArguments()
-    validateHostAndPort()
-    validateUser()
-    mergeArgsIntoKyuubiConf()
-  }
-
-  private def validateListActionArguments(): Unit = {
-    validateZkArguments()
-    cliArgs.service match {
-      case ServiceControlObject.ENGINE => validateUser()
-      case _ =>
-    }
-    mergeArgsIntoKyuubiConf()
-  }
-
-  private def mergeArgsIntoKyuubiConf(): Unit = {
-    conf.set(HA_ADDRESSES.key, cliArgs.zkQuorum)
-    conf.set(HA_NAMESPACE.key, cliArgs.namespace)
-  }
-
-  private def validateZkArguments(): Unit = {
-    if (cliArgs.zkQuorum == null) {
-      fail("Zookeeper quorum is not specified and no default value to load")
-    }
-    if (cliArgs.namespace == null) {
-      fail("Zookeeper namespace is not specified and no default value to load")
-    }
-  }
-
-  private def validateHostAndPort(): Unit = {
-    if (cliArgs.host == null) {
-      fail("Must specify host for service")
-    }
-    if (cliArgs.port == null) {
-      fail("Must specify port for service")
-    }
-
-    try {
-      InetAddress.getByName(cliArgs.host)
-      cliArgs = cliArgs.copy(host = cliArgs.host)
-    } catch {
-      case _: Exception =>
-        fail(s"Unknown host: ${cliArgs.host}")
-    }
-
-    try {
-      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: ${cliArgs.port}")
-    }
-  }
-
-  private def validateUser(): Unit = {
-    if (cliArgs.service == ServiceControlObject.ENGINE && cliArgs.user == 
null) {
-      fail("Must specify user name for engine, please use -u or --user.")
-    }
-  }
-
-  override def toString: String = {
-    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 _ => ""
-    }
-  }
-
-  private def fail(msg: String): Unit = throw new KyuubiException(msg)
-}
diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/Command.scala 
b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/Command.scala
new file mode 100644
index 000000000..0850ff400
--- /dev/null
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/Command.scala
@@ -0,0 +1,164 @@
+/*
+ * 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.cmd
+
+import java.net.InetAddress
+
+import org.apache.kyuubi.{KYUUBI_VERSION, KyuubiException, Logging}
+import org.apache.kyuubi.config.KyuubiConf
+import org.apache.kyuubi.config.KyuubiConf.{ENGINE_SHARE_LEVEL, 
ENGINE_SHARE_LEVEL_SUBDOMAIN, ENGINE_TYPE}
+import org.apache.kyuubi.ctl.{CliConfig, ServiceControlObject}
+import org.apache.kyuubi.ctl.ControlCli.printMessage
+import org.apache.kyuubi.ha.HighAvailabilityConf._
+import org.apache.kyuubi.ha.client.{DiscoveryClient, DiscoveryPaths, 
ServiceNodeInfo}
+
+abstract class Command(var cliArgs: CliConfig) extends Logging {
+
+  val conf = KyuubiConf().loadFileDefaults()
+
+  val verbose = cliArgs.commonOpts.verbose
+
+  def preProcess(): Unit = {
+    this.cliArgs = useDefaultPropertyValueIfMissing()
+    validateArguments()
+  }
+
+  /** Ensure that required fields exists. Call this only once all defaults are 
loaded. */
+  def validateArguments(): Unit
+
+  def run(): Unit
+
+  def fail(msg: String): Unit = throw new KyuubiException(msg)
+
+  protected def validateZkArguments(): Unit = {
+    if (cliArgs.commonOpts.zkQuorum == null) {
+      fail("Zookeeper quorum is not specified and no default value to load")
+    }
+    if (cliArgs.commonOpts.namespace == null) {
+      fail("Zookeeper namespace is not specified and no default value to load")
+    }
+  }
+
+  protected def validateHostAndPort(): Unit = {
+    if (cliArgs.commonOpts.host == null) {
+      fail("Must specify host for service")
+    }
+    if (cliArgs.commonOpts.port == null) {
+      fail("Must specify port for service")
+    }
+
+    try {
+      InetAddress.getByName(cliArgs.commonOpts.host)
+    } catch {
+      case _: Exception =>
+        fail(s"Unknown host: ${cliArgs.commonOpts.host}")
+    }
+
+    try {
+      if (cliArgs.commonOpts.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: 
${cliArgs.commonOpts.port}")
+    }
+  }
+
+  protected def validateUser(): Unit = {
+    if (cliArgs.service == ServiceControlObject.ENGINE && 
cliArgs.engineOpts.user == null) {
+      fail("Must specify user name for engine, please use -u or --user.")
+    }
+  }
+
+  protected def mergeArgsIntoKyuubiConf(): Unit = {
+    conf.set(HA_ADDRESSES.key, cliArgs.commonOpts.zkQuorum)
+    conf.set(HA_NAMESPACE.key, cliArgs.commonOpts.namespace)
+  }
+
+  private def useDefaultPropertyValueIfMissing(): CliConfig = {
+    var arguments: CliConfig = cliArgs.copy()
+    if (cliArgs.commonOpts.zkQuorum == null) {
+      conf.getOption(HA_ADDRESSES.key).foreach { v =>
+        if (verbose) {
+          super.info(s"Zookeeper quorum is not specified, use value from 
default conf:$v")
+        }
+        arguments = arguments.copy(commonOpts = 
arguments.commonOpts.copy(zkQuorum = v))
+      }
+    }
+
+    if (arguments.commonOpts.namespace == null) {
+      arguments = arguments.copy(commonOpts =
+        arguments.commonOpts.copy(namespace = conf.get(HA_NAMESPACE)))
+      if (verbose) {
+        super.info(s"Zookeeper namespace is not specified, use value from 
default conf:" +
+          s"${arguments.commonOpts.namespace}")
+      }
+    }
+
+    if (arguments.commonOpts.version == null) {
+      if (verbose) {
+        super.info(s"version is not specified, use built-in 
KYUUBI_VERSION:$KYUUBI_VERSION")
+      }
+      arguments = arguments.copy(commonOpts = 
arguments.commonOpts.copy(version = KYUUBI_VERSION))
+    }
+    arguments
+  }
+
+  private[ctl] def getZkNamespace(): String = {
+    cliArgs.service match {
+      case ServiceControlObject.SERVER =>
+        DiscoveryPaths.makePath(null, cliArgs.commonOpts.namespace)
+      case ServiceControlObject.ENGINE =>
+        val engineType = Some(cliArgs.engineOpts.engineType)
+          .filter(_ != null).filter(_.nonEmpty)
+          .getOrElse(conf.get(ENGINE_TYPE))
+        val engineSubdomain = Some(cliArgs.engineOpts.engineSubdomain)
+          .filter(_ != null).filter(_.nonEmpty)
+          
.getOrElse(conf.get(ENGINE_SHARE_LEVEL_SUBDOMAIN).getOrElse("default"))
+        val engineShareLevel = Some(cliArgs.engineOpts.engineShareLevel)
+          .filter(_ != null).filter(_.nonEmpty)
+          .getOrElse(conf.get(ENGINE_SHARE_LEVEL))
+        // The path of the engine defined in zookeeper comes from
+        // org.apache.kyuubi.engine.EngineRef#engineSpace
+        DiscoveryPaths.makePath(
+          s"${cliArgs.commonOpts.namespace}_${cliArgs.commonOpts.version}_" +
+            s"${engineShareLevel}_${engineType}",
+          cliArgs.engineOpts.user,
+          Array(engineSubdomain))
+    }
+  }
+
+  private[ctl] def getServiceNodes(
+      discoveryClient: DiscoveryClient,
+      znodeRoot: String,
+      hostPortOpt: Option[(String, Int)]): Seq[ServiceNodeInfo] = {
+    val serviceNodes = discoveryClient.getServiceNodesInfo(znodeRoot)
+    hostPortOpt match {
+      case Some((host, port)) => serviceNodes.filter { sn =>
+          sn.host == host && sn.port == port
+        }
+      case _ => serviceNodes
+    }
+  }
+
+  override def info(msg: => Any): Unit = printMessage(msg)
+
+  override def warn(msg: => Any): Unit = printMessage(s"Warning: $msg")
+
+  override def error(msg: => Any): Unit = printMessage(s"Error: $msg")
+
+}
diff --git 
a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/CreateCommand.scala 
b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/CreateCommand.scala
new file mode 100644
index 000000000..63bf7d092
--- /dev/null
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/CreateCommand.scala
@@ -0,0 +1,94 @@
+/*
+ * 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.cmd
+
+import scala.collection.mutable.ListBuffer
+
+import org.apache.kyuubi.ctl.{CliConfig, Render, ServiceControlObject}
+import org.apache.kyuubi.ha.HighAvailabilityConf._
+import org.apache.kyuubi.ha.client.{DiscoveryClient, DiscoveryPaths, 
ServiceNodeInfo}
+import org.apache.kyuubi.ha.client.DiscoveryClientProvider.withDiscoveryClient
+
+class CreateCommand(cliConfig: CliConfig) extends Command(cliConfig) {
+
+  def validateArguments(): Unit = {
+    if (cliArgs.service != ServiceControlObject.SERVER) {
+      fail("Only support expose Kyuubi server instance to another domain")
+    }
+    validateZkArguments()
+
+    val defaultNamespace = conf.getOption(HA_NAMESPACE.key)
+      .getOrElse(HA_NAMESPACE.defaultValStr)
+    if (defaultNamespace.equals(cliArgs.commonOpts.namespace)) {
+      fail(
+        s"""
+           |Only support expose Kyuubi server instance to another domain, a 
different namespace
+           |than the default namespace [$defaultNamespace] should be specified.
+        """.stripMargin)
+    }
+
+  }
+
+  def run(): Unit = {
+    create()
+  }
+
+  /**
+   * Expose Kyuubi server instance to another domain.
+   */
+  private def create(): Unit = {
+    val kyuubiConf = conf
+
+    kyuubiConf.setIfMissing(HA_ADDRESSES, cliArgs.commonOpts.zkQuorum)
+    withDiscoveryClient(kyuubiConf) { discoveryClient =>
+      val fromNamespace =
+        DiscoveryPaths.makePath(null, kyuubiConf.get(HA_NAMESPACE))
+      val toNamespace = getZkNamespace()
+
+      val currentServerNodes = 
discoveryClient.getServiceNodesInfo(fromNamespace)
+      val exposedServiceNodes = ListBuffer[ServiceNodeInfo]()
+
+      if (currentServerNodes.nonEmpty) {
+        def doCreate(zc: DiscoveryClient): Unit = {
+          currentServerNodes.foreach { sn =>
+            info(s"Exposing server instance:${sn.instance} with 
version:${sn.version}" +
+              s" from $fromNamespace to $toNamespace")
+            val newNodePath = zc.createAndGetServiceNode(
+              kyuubiConf,
+              cliArgs.commonOpts.namespace,
+              sn.instance,
+              sn.version,
+              true)
+            exposedServiceNodes += sn.copy(
+              namespace = toNamespace,
+              nodeName = newNodePath.split("/").last)
+          }
+        }
+
+        if (kyuubiConf.get(HA_ADDRESSES) == cliArgs.commonOpts.zkQuorum) {
+          doCreate(discoveryClient)
+        } else {
+          kyuubiConf.set(HA_ADDRESSES, cliArgs.commonOpts.zkQuorum)
+          withDiscoveryClient(kyuubiConf)(doCreate)
+        }
+      }
+
+      val title = "Created zookeeper service nodes"
+      info(Render.renderServiceNodesInfo(title, exposedServiceNodes, verbose))
+    }
+  }
+}
diff --git 
a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/DeleteCommand.scala 
b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/DeleteCommand.scala
new file mode 100644
index 000000000..d76f65bc3
--- /dev/null
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/DeleteCommand.scala
@@ -0,0 +1,64 @@
+/*
+ * 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.cmd
+
+import scala.collection.mutable.ListBuffer
+
+import org.apache.kyuubi.ctl.{CliConfig, Render}
+import org.apache.kyuubi.ha.client.DiscoveryClientProvider.withDiscoveryClient
+import org.apache.kyuubi.ha.client.ServiceNodeInfo
+
+class DeleteCommand(cliConfig: CliConfig) extends Command(cliConfig) {
+
+  def validateArguments(): Unit = {
+    validateZkArguments()
+    validateHostAndPort()
+    validateUser()
+    mergeArgsIntoKyuubiConf()
+  }
+
+  override def run(): Unit = {
+    delete()
+  }
+
+  /**
+   * Delete zookeeper service node with specified host port.
+   */
+  private def delete(): Unit = {
+    withDiscoveryClient(conf) { discoveryClient =>
+      val znodeRoot = getZkNamespace()
+      val hostPortOpt = Some((cliArgs.commonOpts.host, 
cliArgs.commonOpts.port.toInt))
+      val nodesToDelete = getServiceNodes(discoveryClient, znodeRoot, 
hostPortOpt)
+
+      val deletedNodes = ListBuffer[ServiceNodeInfo]()
+      nodesToDelete.foreach { node =>
+        val nodePath = s"$znodeRoot/${node.nodeName}"
+        info(s"Deleting zookeeper service node:$nodePath")
+        try {
+          discoveryClient.delete(nodePath)
+          deletedNodes += node
+        } catch {
+          case e: Exception =>
+            error(s"Failed to delete zookeeper service node:$nodePath", e)
+        }
+      }
+
+      val title = "Deleted zookeeper service nodes"
+      info(Render.renderServiceNodesInfo(title, deletedNodes, verbose))
+    }
+  }
+}
diff --git 
a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/GetCommand.scala 
b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/GetCommand.scala
new file mode 100644
index 000000000..745495504
--- /dev/null
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/GetCommand.scala
@@ -0,0 +1,47 @@
+/*
+ * 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.cmd
+
+import org.apache.kyuubi.ctl.{CliConfig, Render}
+import org.apache.kyuubi.ha.client.DiscoveryClientProvider.withDiscoveryClient
+
+class GetCommand(cliConfig: CliConfig) extends Command(cliConfig) {
+
+  override def validateArguments(): Unit = {
+    validateZkArguments()
+    validateHostAndPort()
+    validateUser()
+    mergeArgsIntoKyuubiConf()
+  }
+
+  override def run(): Unit = list(filterHostPort = true)
+
+  private def list(filterHostPort: Boolean): Unit = {
+    withDiscoveryClient(conf) { discoveryClient =>
+      val znodeRoot = getZkNamespace()
+      val hostPortOpt =
+        if (filterHostPort) {
+          Some((cliArgs.commonOpts.host, cliArgs.commonOpts.port.toInt))
+        } else None
+      val nodes = getServiceNodes(discoveryClient, znodeRoot, hostPortOpt)
+
+      val title = "Zookeeper service nodes"
+      info(Render.renderServiceNodesInfo(title, nodes, verbose))
+    }
+  }
+
+}
diff --git 
a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/ListCommand.scala 
b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/ListCommand.scala
new file mode 100644
index 000000000..6c539dc22
--- /dev/null
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/ListCommand.scala
@@ -0,0 +1,53 @@
+/*
+ * 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.cmd
+
+import org.apache.kyuubi.ctl.{CliConfig, Render, ServiceControlObject}
+import org.apache.kyuubi.ha.client.DiscoveryClientProvider.withDiscoveryClient
+
+class ListCommand(cliConfig: CliConfig) extends Command(cliConfig) {
+
+  override def validateArguments(): Unit = {
+    validateZkArguments()
+    cliArgs.service match {
+      case ServiceControlObject.ENGINE => validateUser()
+      case _ =>
+    }
+    mergeArgsIntoKyuubiConf()
+  }
+
+  override def run(): Unit = {
+    list(filterHostPort = false)
+  }
+
+  /**
+   * List Kyuubi server nodes info.
+   */
+  private def list(filterHostPort: Boolean): Unit = {
+    withDiscoveryClient(conf) { discoveryClient =>
+      val znodeRoot = getZkNamespace()
+      val hostPortOpt =
+        if (filterHostPort) {
+          Some((cliArgs.commonOpts.host, cliArgs.commonOpts.port.toInt))
+        } else None
+      val nodes = getServiceNodes(discoveryClient, znodeRoot, hostPortOpt)
+
+      val title = "Zookeeper service nodes"
+      info(Render.renderServiceNodesInfo(title, nodes, verbose))
+    }
+  }
+}
diff --git 
a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ServiceControlCliArgumentsSuite.scala
 
b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ControlCliArgumentsSuite.scala
similarity index 87%
rename from 
kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ServiceControlCliArgumentsSuite.scala
rename to 
kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ControlCliArgumentsSuite.scala
index 4e02d22ab..9f424a742 100644
--- 
a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ServiceControlCliArgumentsSuite.scala
+++ 
b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ControlCliArgumentsSuite.scala
@@ -20,7 +20,7 @@ package org.apache.kyuubi.ctl
 import org.apache.kyuubi.{KYUUBI_VERSION, KyuubiFunSuite}
 import org.apache.kyuubi.ha.HighAvailabilityConf.HA_NAMESPACE
 
-class ServiceControlCliArgumentsSuite extends KyuubiFunSuite {
+class ControlCliArgumentsSuite extends KyuubiFunSuite {
   val zkQuorum = "localhost:2181"
   val namespace = "kyuubi"
   val user = "kyuubi"
@@ -34,7 +34,7 @@ class ServiceControlCliArgumentsSuite extends KyuubiFunSuite {
       val thread = new Thread {
         override def run(): Unit =
           try {
-            new ServiceControlCliArguments(args)
+            new ControlCliArguments(args)
           } catch {
             case e: Exception =>
               error(e)
@@ -54,7 +54,7 @@ class ServiceControlCliArgumentsSuite extends KyuubiFunSuite {
       val thread = new Thread {
         override def run(): Unit =
           try {
-            new ServiceControlCliArguments(args) {
+            new ControlCliArguments(args) {
               override private[kyuubi] lazy val effectSetup = new 
KyuubiOEffectSetup {
                 // nothing to do, to handle out stream.
                 override def terminate(exitState: Either[String, Unit]): Unit 
= ()
@@ -90,15 +90,15 @@ class ServiceControlCliArgumentsSuite extends 
KyuubiFunSuite {
           port,
           "--version",
           KYUUBI_VERSION)
-        val opArgs = new ServiceControlCliArguments(args)
+        val opArgs = new ControlCliArguments(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)
+        assert(opArgs.cliArgs.commonOpts.zkQuorum == zkQuorum)
+        assert(opArgs.cliArgs.commonOpts.namespace == namespace)
+        assert(opArgs.cliArgs.engineOpts.user == user)
+        assert(opArgs.cliArgs.commonOpts.host == host)
+        assert(opArgs.cliArgs.commonOpts.port == port)
+        assert(opArgs.cliArgs.commonOpts.version == KYUUBI_VERSION)
       }
     }
 
@@ -119,14 +119,14 @@ class ServiceControlCliArgumentsSuite extends 
KyuubiFunSuite {
         port,
         "--version",
         KYUUBI_VERSION)
-      val opArgs = new ServiceControlCliArguments(args)
+      val opArgs = new ControlCliArguments(args)
       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)
+      assert(opArgs.cliArgs.commonOpts.zkQuorum == zkQuorum)
+      assert(opArgs.cliArgs.commonOpts.namespace == newNamespace)
+      assert(opArgs.cliArgs.commonOpts.host == host)
+      assert(opArgs.cliArgs.commonOpts.port == port)
+      assert(opArgs.cliArgs.commonOpts.version == KYUUBI_VERSION)
     }
   }
 
@@ -166,7 +166,7 @@ class ServiceControlCliArgumentsSuite extends 
KyuubiFunSuite {
       zkQuorum,
       "--namespace",
       namespace)
-    val opArgs = new ServiceControlCliArguments(args2)
+    val opArgs = new ControlCliArguments(args2)
     assert(opArgs.cliArgs.action == ServiceControlAction.LIST)
   }
 
@@ -218,7 +218,7 @@ class ServiceControlCliArgumentsSuite extends 
KyuubiFunSuite {
         host,
         "--port",
         port)
-      val opArgs6 = new ServiceControlCliArguments(args5)
+      val opArgs6 = new ControlCliArguments(args5)
       assert(opArgs6.cliArgs.action.toString.equalsIgnoreCase(op))
     }
   }
@@ -281,7 +281,7 @@ class ServiceControlCliArgumentsSuite extends 
KyuubiFunSuite {
         zkQuorum,
         "--namespace",
         newNamespace)
-      val opArgs2 = new ServiceControlCliArguments(args2)
+      val opArgs2 = new ControlCliArguments(args2)
       assert(opArgs2.cliArgs.action.toString.equalsIgnoreCase(op))
 
       val args4 = Array(
@@ -301,9 +301,9 @@ class ServiceControlCliArgumentsSuite extends 
KyuubiFunSuite {
       "list",
       "--zk-quorum",
       zkQuorum)
-    val opArgs = new ServiceControlCliArguments(args)
-    assert(opArgs.cliArgs.namespace == namespace)
-    assert(opArgs.cliArgs.version == KYUUBI_VERSION)
+    val opArgs = new ControlCliArguments(args)
+    assert(opArgs.cliArgs.commonOpts.namespace == namespace)
+    assert(opArgs.cliArgs.commonOpts.version == KYUUBI_VERSION)
   }
 
   test("test use short options") {
@@ -324,15 +324,15 @@ class ServiceControlCliArgumentsSuite extends 
KyuubiFunSuite {
           port,
           "-v",
           KYUUBI_VERSION)
-        val opArgs = new ServiceControlCliArguments(args)
+        val opArgs = new ControlCliArguments(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)
+        assert(opArgs.cliArgs.commonOpts.zkQuorum == zkQuorum)
+        assert(opArgs.cliArgs.commonOpts.namespace == namespace)
+        assert(opArgs.cliArgs.engineOpts.user == user)
+        assert(opArgs.cliArgs.commonOpts.host == host)
+        assert(opArgs.cliArgs.commonOpts.port == port)
+        assert(opArgs.cliArgs.commonOpts.version == KYUUBI_VERSION)
       }
     }
 
@@ -342,8 +342,8 @@ class ServiceControlCliArgumentsSuite extends 
KyuubiFunSuite {
       "-zk",
       zkQuorum,
       "-b")
-    val opArgs3 = new ServiceControlCliArguments(args2)
-    assert(opArgs3.cliArgs.verbose)
+    val opArgs3 = new ControlCliArguments(args2)
+    assert(opArgs3.cliArgs.commonOpts.verbose)
   }
 
   test("test --help") {
diff --git 
a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ServiceControlCliSuite.scala 
b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ControlCliSuite.scala
similarity index 91%
rename from 
kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ServiceControlCliSuite.scala
rename to kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ControlCliSuite.scala
index 64e231095..f1a9984b9 100644
--- 
a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ServiceControlCliSuite.scala
+++ b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/ControlCliSuite.scala
@@ -49,7 +49,7 @@ trait TestPrematureExit {
   private[kyuubi] def testPrematureExit(
       input: Array[String],
       searchString: String,
-      mainObject: CommandLineUtils = ServiceControlCli): Unit = {
+      mainObject: CommandLineUtils = ControlCli): Unit = {
     val printStream = new BufferPrintStream()
     mainObject.printStream = printStream
 
@@ -84,9 +84,8 @@ trait TestPrematureExit {
   }
 }
 
-class ServiceControlCliSuite extends KyuubiFunSuite with TestPrematureExit {
+class ControlCliSuite extends KyuubiFunSuite with TestPrematureExit {
   import DiscoveryClientProvider._
-  import ServiceControlCli._
 
   val zkServer = new EmbeddedZookeeper()
   val conf: KyuubiConf = KyuubiConf()
@@ -95,7 +94,7 @@ class ServiceControlCliSuite extends KyuubiFunSuite with 
TestPrematureExit {
   val host = "localhost"
   val port = "10000"
   val user = "kyuubi"
-  val ctl = new ServiceControlCli()
+  val ctl = new ControlCli()
   val counter = new AtomicInteger(0)
 
   override def beforeAll(): Unit = {
@@ -123,7 +122,7 @@ class ServiceControlCliSuite extends KyuubiFunSuite with 
TestPrematureExit {
   private def getRenderedNodesInfoWithoutTitle(
       nodesInfo: Seq[ServiceNodeInfo],
       verbose: Boolean): String = {
-    val renderedInfo = renderServiceNodesInfo("", nodesInfo, verbose)
+    val renderedInfo = Render.renderServiceNodesInfo("", nodesInfo, verbose)
     if (verbose) {
       renderedInfo.substring(renderedInfo.indexOf("|"))
     } else {
@@ -198,7 +197,7 @@ class ServiceControlCliSuite extends KyuubiFunSuite with 
TestPrematureExit {
     val title = "test render"
     val nodes = Seq(
       ServiceNodeInfo("/kyuubi", "serviceNode", "localhost", 10000, 
Some("version"), None))
-    val renderedInfo = renderServiceNodesInfo(title, nodes, true)
+    val renderedInfo = Render.renderServiceNodesInfo(title, nodes, true)
     val expected = {
       s"\n               $title               " +
         """
@@ -264,7 +263,8 @@ class ServiceControlCliSuite extends KyuubiFunSuite with 
TestPrematureExit {
       zkServer.getConnectString,
       "--namespace",
       namespace)
-    assert(getZkNamespace(new ServiceControlCliArguments(arg1)) == 
s"/$namespace")
+    val scArgs1 = new ControlCliArguments(arg1)
+    assert(scArgs1.command.getZkNamespace() == s"/$namespace")
 
     val arg2 = Array(
       "list",
@@ -275,7 +275,8 @@ class ServiceControlCliSuite extends KyuubiFunSuite with 
TestPrematureExit {
       namespace,
       "--user",
       user)
-    assert(getZkNamespace(new ServiceControlCliArguments(arg2)) ==
+    val scArgs2 = new ControlCliArguments(arg2)
+    assert(scArgs2.command.getZkNamespace() ==
       s"/${namespace}_${KYUUBI_VERSION}_USER_SPARK_SQL/$user/default")
   }
 
@@ -414,7 +415,8 @@ class ServiceControlCliSuite extends KyuubiFunSuite with 
TestPrematureExit {
       namespace,
       "--user",
       user)
-    assert(getZkNamespace(new ServiceControlCliArguments(arg1)) ==
+    val scArgs1 = new ControlCliArguments(arg1)
+    assert(scArgs1.command.getZkNamespace() ==
       s"/${namespace}_${KYUUBI_VERSION}_USER_SPARK_SQL/$user/default")
 
     val arg2 = Array(
@@ -428,7 +430,8 @@ class ServiceControlCliSuite extends KyuubiFunSuite with 
TestPrematureExit {
       user,
       "--engine-type",
       "FLINK_SQL")
-    assert(getZkNamespace(new ServiceControlCliArguments(arg2)) ==
+    val scArgs2 = new ControlCliArguments(arg2)
+    assert(scArgs2.command.getZkNamespace() ==
       s"/${namespace}_${KYUUBI_VERSION}_USER_FLINK_SQL/$user/default")
 
     val arg3 = Array(
@@ -442,7 +445,8 @@ class ServiceControlCliSuite extends KyuubiFunSuite with 
TestPrematureExit {
       user,
       "--engine-type",
       "TRINO")
-    assert(getZkNamespace(new ServiceControlCliArguments(arg3)) ==
+    val scArgs3 = new ControlCliArguments(arg3)
+    assert(scArgs3.command.getZkNamespace() ==
       s"/${namespace}_${KYUUBI_VERSION}_USER_TRINO/$user/default")
 
     val arg4 = Array(
@@ -458,7 +462,8 @@ class ServiceControlCliSuite extends KyuubiFunSuite with 
TestPrematureExit {
       "SPARK_SQL",
       "--engine-subdomain",
       "sub_1")
-    assert(getZkNamespace(new ServiceControlCliArguments(arg4)) ==
+    val scArgs4 = new ControlCliArguments(arg4)
+    assert(scArgs4.command.getZkNamespace() ==
       s"/${namespace}_${KYUUBI_VERSION}_USER_SPARK_SQL/$user/sub_1")
 
     val arg5 = Array(
@@ -476,7 +481,8 @@ class ServiceControlCliSuite extends KyuubiFunSuite with 
TestPrematureExit {
       "SPARK_SQL",
       "--engine-subdomain",
       "sub_1")
-    assert(getZkNamespace(new ServiceControlCliArguments(arg5)) ==
+    val scArgs5 = new ControlCliArguments(arg5)
+    assert(scArgs5.command.getZkNamespace() ==
       s"/${namespace}_1.5.0_USER_SPARK_SQL/$user/sub_1")
   }
 
@@ -490,7 +496,8 @@ class ServiceControlCliSuite extends KyuubiFunSuite with 
TestPrematureExit {
       namespace,
       "--user",
       user)
-    assert(getZkNamespace(new ServiceControlCliArguments(arg1)) ==
+    val scArgs1 = new ControlCliArguments(arg1)
+    assert(scArgs1.command.getZkNamespace() ==
       s"/${namespace}_${KYUUBI_VERSION}_USER_SPARK_SQL/$user/default")
 
     val arg2 = Array(
@@ -504,7 +511,8 @@ class ServiceControlCliSuite extends KyuubiFunSuite with 
TestPrematureExit {
       user,
       "--engine-share-level",
       "CONNECTION")
-    assert(getZkNamespace(new ServiceControlCliArguments(arg2)) ==
+    val scArgs2 = new ControlCliArguments(arg2)
+    assert(scArgs2.command.getZkNamespace() ==
       s"/${namespace}_${KYUUBI_VERSION}_CONNECTION_SPARK_SQL/$user/default")
 
     val arg3 = Array(
@@ -518,7 +526,8 @@ class ServiceControlCliSuite extends KyuubiFunSuite with 
TestPrematureExit {
       user,
       "--engine-share-level",
       "USER")
-    assert(getZkNamespace(new ServiceControlCliArguments(arg3)) ==
+    val scArgs3 = new ControlCliArguments(arg3)
+    assert(scArgs3.command.getZkNamespace() ==
       s"/${namespace}_${KYUUBI_VERSION}_USER_SPARK_SQL/$user/default")
 
     val arg4 = Array(
@@ -532,7 +541,8 @@ class ServiceControlCliSuite extends KyuubiFunSuite with 
TestPrematureExit {
       user,
       "--engine-share-level",
       "GROUP")
-    assert(getZkNamespace(new ServiceControlCliArguments(arg4)) ==
+    val scArgs4 = new ControlCliArguments(arg4)
+    assert(scArgs4.command.getZkNamespace() ==
       s"/${namespace}_${KYUUBI_VERSION}_GROUP_SPARK_SQL/$user/default")
 
     val arg5 = Array(
@@ -546,7 +556,8 @@ class ServiceControlCliSuite extends KyuubiFunSuite with 
TestPrematureExit {
       user,
       "--engine-share-level",
       "SERVER")
-    assert(getZkNamespace(new ServiceControlCliArguments(arg5)) ==
+    val scArgs5 = new ControlCliArguments(arg5)
+    assert(scArgs5.command.getZkNamespace() ==
       s"/${namespace}_${KYUUBI_VERSION}_SERVER_SPARK_SQL/$user/default")
   }
 }

Reply via email to