You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kyuubi.apache.org by ul...@apache.org on 2023/01/04 05:24:29 UTC

[kyuubi] branch master updated: [KYUUBI #3983] [KYUUBI #3982] [FEATURE] introduce refreshing user defaults configs

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/kyuubi.git


The following commit(s) were added to refs/heads/master by this push:
     new 75d0b7f6b [KYUUBI #3983] [KYUUBI  #3982] [FEATURE] introduce refreshing user defaults configs
75d0b7f6b is described below

commit 75d0b7f6b7889b48761c499a697814b4b0bf9613
Author: liangbowen <li...@gf.com.cn>
AuthorDate: Wed Jan 4 13:24:16 2023 +0800

    [KYUUBI #3983] [KYUUBI  #3982] [FEATURE] introduce refreshing user defaults configs
    
    ### _Why are the changes needed?_
    
    to close #3982 .
    
    Introduce feature of refresh user defaults config (as `___${user}___.*` which starts with three continuous underscores "___") from config file via `kyuubi-admin` cli and `refresh/user_defaults_conf` Rest API.
    
    1. add `refreshUserDefaultsConf` methond in KyuubiServer to read user defautls configs from property file and apply config changes to server's KyuubiConf
    3. add `refresh/user_defaults_conf` api to AdminRestApi calling `refreshUserDefaultsConf` of KyuubiServer
    3. add config type `userDefautls` in kyuubi-admin cli refresh command
    
    This feature will
    - help to apply user defaults conf without restarting server or losing connections
    - load latest config for engine launch, e.g. spark related config `spark.*`
    
    It won't
    - affect the components already started and using the clone of server conf
    - affect configs for launched engine instance
    
    ### _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
    
    - [x] [Run test](https://kyuubi.apache.org/docs/latest/develop_tools/testing.html#running-tests) locally before make a pull request
    
    Closes #3983 from bowenliang123/3982-reload-server-conf.
    
    Closes #3983
    
    a8fb0bf2 [liangbowen] fix typo
    b9b80f56 [liangbowen] update
    b47a1541 [liangbowen] minor
    786cb2a0 [liangbowen] add logging for statistics
    0860e3a7 [liangbowen] fix: loadFileDefaults in refreshUserDefaultsConf
    8dbbbcb8 [liangbowen] fix typo
    619acd2e [liangbowen] import
    e405dc8e [liangbowen] fix user defaults key filtering by adding `getAllUserDefaults` to `KyuubiConf`
    ac407bd5 [liangbowen] rename config refresh option to `userDefaultsConf` and extracted to RefreshConfigCommandConfigType
    c65398b2 [liangbowen] fix redundant loadFileDefaults in refreshUserDefaultsConf
    1b046feb [liangbowen] update comments
    e5dd5dbb [liangbowen] typo
    c0a358ba [liangbowen] change to refresh users' config. rename cli command to `refresh config userDefaults`
    bf5448e3 [liangbowen] support reload server config from config file 1. add reloadServerConf in KyuubiServer to read config and put all to config, 2. add "refresh/server_conf" api to AdminRestApi, 3. add config type "serverConf" in kyuubi-admin cli
    
    Authored-by: liangbowen <li...@gf.com.cn>
    Signed-off-by: ulysses-you <ul...@apache.org>
---
 .../org/apache/kyuubi/config/KyuubiConf.scala      | 13 ++++++++++--
 .../ctl/cmd/refresh/RefreshConfigCommand.scala     |  8 +++++++-
 .../apache/kyuubi/ctl/opt/AdminCommandLine.scala   |  4 +++-
 .../kyuubi/ctl/AdminControlCliArgumentsSuite.scala | 12 ++++++++++-
 .../org/apache/kyuubi/client/AdminRestApi.java     |  5 +++++
 .../org/apache/kyuubi/server/KyuubiServer.scala    | 23 ++++++++++++++++++++++
 .../kyuubi/server/api/v1/AdminResource.scala       | 20 +++++++++++++++++++
 .../kyuubi/server/api/v1/AdminResourceSuite.scala  | 18 +++++++++++++++++
 8 files changed, 98 insertions(+), 5 deletions(-)

diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala
index c54a6a4cc..0703412cc 100644
--- a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala
+++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala
@@ -147,6 +147,13 @@ case class KyuubiConf(loadSysDefault: Boolean = true) extends Logging {
     }
   }
 
+  /**
+   * Retrieve user defaults configs in key-value pairs from [[KyuubiConf]] with key prefix "___"
+   */
+  def getAllUserDefaults: Map[String, String] = {
+    getAll.filter { case (k, _) => k.startsWith(USER_DEFAULTS_CONF_QUOTE) }
+  }
+
   /** Copy this object */
   override def clone: KyuubiConf = {
     val cloned = KyuubiConf(false)
@@ -159,11 +166,12 @@ case class KyuubiConf(loadSysDefault: Boolean = true) extends Logging {
   def getUserDefaults(user: String): KyuubiConf = {
     val cloned = KyuubiConf(false)
 
-    for (e <- settings.entrySet().asScala if !e.getKey.startsWith("___")) {
+    for (e <- settings.entrySet().asScala if !e.getKey.startsWith(USER_DEFAULTS_CONF_QUOTE)) {
       cloned.set(e.getKey, e.getValue)
     }
 
-    for ((k, v) <- getAllWithPrefix(s"___${user}___", "")) {
+    for ((k, v) <-
+        getAllWithPrefix(s"$USER_DEFAULTS_CONF_QUOTE${user}$USER_DEFAULTS_CONF_QUOTE", "")) {
       cloned.set(k, v)
     }
     serverOnlyConfEntries.foreach(cloned.unset)
@@ -198,6 +206,7 @@ object KyuubiConf {
   final val KYUUBI_HOME = "KYUUBI_HOME"
   final val KYUUBI_ENGINE_ENV_PREFIX = "kyuubi.engineEnv"
   final val KYUUBI_BATCH_CONF_PREFIX = "kyuubi.batchConf"
+  final val USER_DEFAULTS_CONF_QUOTE = "___"
 
   private[this] val kyuubiConfEntriesUpdateLock = new Object
 
diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/refresh/RefreshConfigCommand.scala b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/refresh/RefreshConfigCommand.scala
index 80d673327..b658c0e45 100644
--- a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/refresh/RefreshConfigCommand.scala
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/refresh/RefreshConfigCommand.scala
@@ -21,6 +21,7 @@ import org.apache.kyuubi.KyuubiException
 import org.apache.kyuubi.client.AdminRestApi
 import org.apache.kyuubi.ctl.RestClientFactory.withKyuubiRestClient
 import org.apache.kyuubi.ctl.cmd.AdminCtlCommand
+import org.apache.kyuubi.ctl.cmd.refresh.RefreshConfigCommandConfigType.{HADOOP_CONF, USER_DEFAULTS_CONF}
 import org.apache.kyuubi.ctl.opt.CliConfig
 import org.apache.kyuubi.ctl.util.{Tabulator, Validator}
 
@@ -33,7 +34,8 @@ class RefreshConfigCommand(cliConfig: CliConfig) extends AdminCtlCommand[String]
     withKyuubiRestClient(normalizedCliConfig, null, conf) { kyuubiRestClient =>
       val adminRestApi = new AdminRestApi(kyuubiRestClient)
       normalizedCliConfig.adminConfigOpts.configType match {
-        case "hadoopConf" => adminRestApi.refreshHadoopConf()
+        case HADOOP_CONF => adminRestApi.refreshHadoopConf()
+        case USER_DEFAULTS_CONF => adminRestApi.refreshUserDefaultsConf()
         case configType => throw new KyuubiException(s"Invalid config type:$configType")
       }
     }
@@ -43,3 +45,7 @@ class RefreshConfigCommand(cliConfig: CliConfig) extends AdminCtlCommand[String]
     info(Tabulator.format("", Array("Response"), Array(Array(resp))))
   }
 }
+object RefreshConfigCommandConfigType {
+  final val HADOOP_CONF = "hadoopConf"
+  final val USER_DEFAULTS_CONF = "userDefaultsConf"
+}
diff --git a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/opt/AdminCommandLine.scala b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/opt/AdminCommandLine.scala
index 524f2954e..59ad7f5fc 100644
--- a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/opt/AdminCommandLine.scala
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/opt/AdminCommandLine.scala
@@ -20,6 +20,7 @@ package org.apache.kyuubi.ctl.opt
 import scopt.{OParser, OParserBuilder}
 
 import org.apache.kyuubi.KYUUBI_VERSION
+import org.apache.kyuubi.ctl.cmd.refresh.RefreshConfigCommandConfigType._
 
 object AdminCommandLine extends CommonCommandLine {
 
@@ -100,6 +101,7 @@ object AdminCommandLine extends CommonCommandLine {
         arg[String]("<configType>")
           .optional()
           .action((v, c) => c.copy(adminConfigOpts = c.adminConfigOpts.copy(configType = v)))
-          .text("The valid config type can be one of the following: hadoopConf."))
+          .text("The valid config type can be one of the following: " +
+            s"$HADOOP_CONF, $USER_DEFAULTS_CONF."))
   }
 }
diff --git a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/AdminControlCliArgumentsSuite.scala b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/AdminControlCliArgumentsSuite.scala
index 03b606d34..afb946e92 100644
--- a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/AdminControlCliArgumentsSuite.scala
+++ b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/AdminControlCliArgumentsSuite.scala
@@ -19,6 +19,7 @@ package org.apache.kyuubi.ctl
 
 import org.apache.kyuubi.{KYUUBI_VERSION, KyuubiFunSuite}
 import org.apache.kyuubi.ctl.cli.AdminControlCliArguments
+import org.apache.kyuubi.ctl.cmd.refresh.RefreshConfigCommandConfigType._
 import org.apache.kyuubi.ctl.opt.{ControlAction, ControlObject}
 
 class AdminControlCliArgumentsSuite extends KyuubiFunSuite with TestPrematureExit {
@@ -64,6 +65,15 @@ class AdminControlCliArgumentsSuite extends KyuubiFunSuite with TestPrematureExi
     assert(opArgs.cliConfig.resource === ControlObject.CONFIG)
     assert(opArgs.cliConfig.adminConfigOpts.configType === "hadoopConf")
 
+    args = Array(
+      "refresh",
+      "config",
+      "userDefaultsConf")
+    val opArgs2 = new AdminControlCliArguments(args)
+    assert(opArgs2.cliConfig.action === ControlAction.REFRESH)
+    assert(opArgs2.cliConfig.resource === ControlObject.CONFIG)
+    assert(opArgs2.cliConfig.adminConfigOpts.configType === "userDefaultsConf")
+
     args = Array(
       "refresh",
       "config",
@@ -137,7 +147,7 @@ class AdminControlCliArgumentsSuite extends KyuubiFunSuite with TestPrematureExi
          |	Refresh the resource.
          |Command: refresh config [<configType>]
          |	Refresh the config with specified type.
-         |  <configType>             The valid config type can be one of the following: hadoopConf.
+         |  <configType>             The valid config type can be one of the following: $HADOOP_CONF, $USER_DEFAULTS_CONF.
          |
          |  -h, --help               Show help message and exit.""".stripMargin
     // scalastyle:on
diff --git a/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/AdminRestApi.java b/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/AdminRestApi.java
index 7d7c341ab..da9782df5 100644
--- a/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/AdminRestApi.java
+++ b/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/AdminRestApi.java
@@ -39,6 +39,11 @@ public class AdminRestApi {
     return this.getClient().post(path, null, client.getAuthHeader());
   }
 
+  public String refreshUserDefaultsConf() {
+    String path = String.format("%s/%s", API_BASE_PATH, "refresh/user_defaults_conf");
+    return this.getClient().post(path, null, client.getAuthHeader());
+  }
+
   public String deleteEngine(
       String engineType, String shareLevel, String subdomain, String hs2ProxyUser) {
     Map<String, Object> params = new HashMap<>();
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiServer.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiServer.scala
index 09f4d5bff..731ad5df6 100644
--- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiServer.scala
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiServer.scala
@@ -19,6 +19,7 @@ package org.apache.kyuubi.server
 
 import scala.util.Properties
 
+import org.apache.commons.lang3.StringUtils
 import org.apache.hadoop.conf.Configuration
 import org.apache.hadoop.security.UserGroupInformation
 
@@ -105,6 +106,28 @@ object KyuubiServer extends Logging {
     val _hadoopConf = KyuubiHadoopUtils.newHadoopConf(new KyuubiConf().loadFileDefaults())
     hadoopConf = _hadoopConf
   }
+
+  private[kyuubi] def refreshUserDefaultsConf(): Unit = kyuubiServer.conf.synchronized {
+    val existedUserDefaults = kyuubiServer.conf.getAllUserDefaults
+    val refreshedUserDefaults = KyuubiConf().loadFileDefaults().getAllUserDefaults
+    var (unsetCount, updatedCount, addedCount) = (0, 0, 0)
+    for ((k, _) <- existedUserDefaults if !refreshedUserDefaults.contains(k)) {
+      kyuubiServer.conf.unset(k)
+      unsetCount = unsetCount + 1
+    }
+    for ((k, v) <- refreshedUserDefaults) {
+      if (existedUserDefaults.contains(k)) {
+        if (!StringUtils.equals(existedUserDefaults.get(k).orNull, v)) {
+          updatedCount = updatedCount + 1
+        }
+      } else {
+        addedCount = addedCount + 1
+      }
+      kyuubiServer.conf.set(k, v)
+    }
+    info(s"Refreshed user defaults configs with changes of " +
+      s"unset: $unsetCount, updated: $updatedCount, added: $addedCount")
+  }
 }
 
 class KyuubiServer(name: String) extends Serverable(name) {
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/AdminResource.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/AdminResource.scala
index 16653be32..a92992e66 100644
--- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/AdminResource.scala
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/AdminResource.scala
@@ -64,6 +64,26 @@ private[v1] class AdminResource extends ApiRequestContext with Logging {
     Response.ok(s"Refresh the hadoop conf for ${fe.connectionUrl} successfully.").build()
   }
 
+  @ApiResponse(
+    responseCode = "200",
+    content = Array(new Content(
+      mediaType = MediaType.APPLICATION_JSON)),
+    description = "refresh the user defaults configs")
+  @POST
+  @Path("refresh/user_defaults_conf")
+  def refreshUserDefaultsConf(): Response = {
+    val userName = fe.getSessionUser(Map.empty[String, String])
+    val ipAddress = fe.getIpAddress
+    info(s"Receive refresh user defaults conf request from $userName/$ipAddress")
+    if (!userName.equals(administrator)) {
+      throw new NotAllowedException(
+        s"$userName is not allowed to refresh the user defaults conf")
+    }
+    info(s"Reloading user defaults conf")
+    KyuubiServer.refreshUserDefaultsConf()
+    Response.ok(s"Refresh the user defaults conf successfully.").build()
+  }
+
   @ApiResponse(
     responseCode = "200",
     content = Array(new Content(
diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/AdminResourceSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/AdminResourceSuite.scala
index 7a31b8c24..bcbdad2ce 100644
--- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/AdminResourceSuite.scala
+++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/AdminResourceSuite.scala
@@ -66,6 +66,24 @@ class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper {
     assert(200 == response.getStatus)
   }
 
+  test("refresh user defaults config of the kyuubi server") {
+    var response = webTarget.path("api/v1/admin/refresh/user_defaults_conf")
+      .request()
+      .post(null)
+    assert(405 == response.getStatus)
+
+    val adminUser = Utils.currentUser
+    val encodeAuthorization = new String(
+      Base64.getEncoder.encode(
+        s"$adminUser:".getBytes()),
+      "UTF-8")
+    response = webTarget.path("api/v1/admin/refresh/user_defaults_conf")
+      .request()
+      .header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization")
+      .post(null)
+    assert(200 == response.getStatus)
+  }
+
   test("delete engine - user share level") {
     val id = UUID.randomUUID().toString
     conf.set(KyuubiConf.ENGINE_SHARE_LEVEL, USER.toString)