[KYUUBI #1206][FEATURE] Support GROUP for engine.share.level

<!--
Thanks for sending a pull request!

Here are some tips for you:
  1. If this is your first time, please read our contributor guidelines: https://kyuubi.readthedocs.io/en/latest/community/contributions.html
  2. If the PR is related to an issue in https://github.com/apache/incubator-kyuubi/issues, add '[KYUUBI #XXXX]' in your PR title, e.g., '[KYUUBI #XXXX] Your PR title ...'.
  3. If the PR is unfinished, add '[WIP]' in your PR title, e.g., '[WIP][KYUUBI #XXXX] Your PR title ...'.
-->

### _Why are the changes needed?_
<!--
Please clarify why the changes are needed. For instance,
  1. If you add a feature, you can talk about the use case of it.
  2. If you fix a bug, you can clarify why it is a bug.
-->

The detail locates #1206

### _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.readthedocs.io/en/latest/develop_tools/testing.html#running-tests) locally before make a pull request

Closes #1207 from yaooqinn/1206.

Closes #1206

99e324a9 [Kent Yao] [KYUUBI #1206][FEATURE] Support GROUP for engine.share.level
f41fd944 [Kent Yao] Merge branch 'master' into 1206
a7cb80da [Kent Yao] [KYUUBI #1206][FEATURE] Support GROUP for engine.share.level
6ecef063 [Kent Yao] [KYUUBI #1206][FEATURE] Support GROUP for engine.share.level
d7ff7583 [Kent Yao] [KYUUBI #1206][FEATURE] Support GROUP for engine.share.level
7b3b2f53 [Kent Yao] [KYUUBI #1206][FEATURE] Support GROUP for engine.share.level

Authored-by: Kent Yao <yao@apache.org>
Signed-off-by: ulysses-you <ulyssesyou@apache.org>
This commit is contained in:
Kent Yao 2021-10-12 10:51:44 +08:00 committed by ulysses-you
parent 6160b9de30
commit bf629291bf
No known key found for this signature in database
GPG Key ID: 4C500BC62D576766
8 changed files with 117 additions and 5 deletions

View File

@ -184,7 +184,7 @@ kyuubi\.engine<br>\.operation\.log\.dir<br>\.root|<div style='width: 65pt;word-w
kyuubi\.engine\.pool<br>\.size|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>-1</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>The size of engine pool. Note that, if the size is less than 1, the engine pool will not be enabled; otherwise, the size of the engine pool will be min(this, kyuubi.engine.pool.size.threshold).</div>|<div style='width: 30pt'>int</div>|<div style='width: 20pt'>1.4.0</div>
kyuubi\.engine\.pool<br>\.size\.threshold|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>9</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>This parameter is introduced as a server-side parameter, and controls the upper limit of the engine pool.</div>|<div style='width: 30pt'>int</div>|<div style='width: 20pt'>1.4.0</div>
kyuubi\.engine\.session<br>\.initialize\.sql|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>SHOW DATABASES</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>SemiColon-separated list of SQL statements to be initialized in the newly created engine session before queries. This configuration can not be used in JDBC url due to the limitation of Beeline/JDBC driver.</div>|<div style='width: 30pt'>seq</div>|<div style='width: 20pt'>1.3.0</div>
kyuubi\.engine\.share<br>\.level|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>USER</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>Engines will be shared in different levels, available configs are: <ul> <li>CONNECTION: engine will not be shared but only used by the current client connection</li> <li>USER: engine will be shared by all sessions created by a unique username, see also kyuubi.engine.share.level.subdomain</li> <li>SERVER: the App will be shared by Kyuubi servers</li></ul></div>|<div style='width: 30pt'>string</div>|<div style='width: 20pt'>1.2.0</div>
kyuubi\.engine\.share<br>\.level|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>USER</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>Engines will be shared in different levels, available configs are: <ul> <li>CONNECTION: engine will not be shared but only used by the current client connection</li> <li>USER: engine will be shared by all sessions created by a unique username, see also kyuubi.engine.share.level.subdomain</li> <li>GROUP: engine will be shared by all sessions created by all users belong to the same primary group name. The engine will be launched by the group name as the effective username, so here the group name is kind of special user who is able to visit the compute resources/data of a team. It follows the [Hadoop GroupsMapping](https://reurl.cc/xE61Y5) to map user to a primary group. If the primary group is not found, it fallback to the USER level. <li>SERVER: the App will be shared by Kyuubi servers</li></ul></div>|<div style='width: 30pt'>string</div>|<div style='width: 20pt'>1.2.0</div>
kyuubi\.engine\.share<br>\.level\.sub\.domain|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>&lt;undefined&gt;</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>(deprecated) - Using kyuubi.engine.share.level.subdomain instead</div>|<div style='width: 30pt'>string</div>|<div style='width: 20pt'>1.2.0</div>
kyuubi\.engine\.share<br>\.level\.subdomain|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>&lt;undefined&gt;</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>Allow end-users to create a subdomain for the share level of an engine. A subdomain is a case-insensitive string values in `^[a-zA-Z_-]{1,14}$` form. For example, for `USER` share level, an end-user can share a certain engine within a subdomain, not for all of its clients. End-users are free to create multiple engines in the `USER` share level</div>|<div style='width: 30pt'>string</div>|<div style='width: 20pt'>1.4.0</div>
kyuubi\.engine\.single<br>\.spark\.session|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>false</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>When set to true, this engine is running in a single session mode. All the JDBC/ODBC connections share the temporary views, function registries, SQL configuration and the current database.</div>|<div style='width: 30pt'>boolean</div>|<div style='width: 20pt'>1.3.0</div>

View File

@ -740,6 +740,12 @@ object KyuubiConf {
" connection</li>" +
" <li>USER: engine will be shared by all sessions created by a unique username," +
s" see also ${ENGINE_SHARE_LEVEL_SUBDOMAIN.key}</li>" +
" <li>GROUP: engine will be shared by all sessions created by all users belong to the same" +
" primary group name. The engine will be launched by the group name as the effective" +
" username, so here the group name is kind of special user who is able to visit the" +
" compute resources/data of a team. It follows the" +
" [Hadoop GroupsMapping](https://reurl.cc/xE61Y5) to map user to a primary group. If the" +
" primary group is not found, it fallback to the USER level." +
" <li>SERVER: the App will be shared by Kyuubi servers</li></ul>")
.version("1.2.0")
.fallbackConf(LEGACY_ENGINE_SHARE_LEVEL)

View File

@ -27,6 +27,8 @@ object ShareLevel extends Enumeration {
CONNECTION,
/** DEFAULT level, An APP will be shared for all sessions created by a user */
USER,
/** In this level, An APP will be shared for all sessions created by a user's default group */
GROUP,
/** In this level, All sessions from one or more Kyuubi server's will share one single APP */
SERVER = Value
}

View File

@ -30,7 +30,7 @@ import org.apache.kyuubi.service.authentication.PlainSASLHelper
trait JDBCTestUtils extends KyuubiFunSuite {
protected val dftSchema = "default"
protected val user: String = Utils.currentUser
protected lazy val user: String = Utils.currentUser
protected val patterns = Seq("", "*", "%", null, ".*", "_*", "_%", ".%")
protected def jdbcUrl: String
private var _sessionConfs: Map[String, String] = Map.empty

View File

@ -27,11 +27,12 @@ import com.google.common.annotations.VisibleForTesting
import org.apache.curator.framework.CuratorFramework
import org.apache.curator.framework.recipes.locks.InterProcessSemaphoreMutex
import org.apache.curator.utils.ZKPaths
import org.apache.hadoop.security.UserGroupInformation
import org.apache.kyuubi.{KyuubiSQLException, Logging, Utils}
import org.apache.kyuubi.config.KyuubiConf
import org.apache.kyuubi.config.KyuubiConf._
import org.apache.kyuubi.engine.ShareLevel.{CONNECTION, SERVER, ShareLevel}
import org.apache.kyuubi.engine.ShareLevel.{CONNECTION, GROUP, SERVER, ShareLevel}
import org.apache.kyuubi.engine.spark.SparkProcessBuilder
import org.apache.kyuubi.ha.HighAvailabilityConf.HA_ZK_ENGINE_REF_ID
import org.apache.kyuubi.ha.HighAvailabilityConf.HA_ZK_NAMESPACE
@ -87,6 +88,16 @@ private[kyuubi] class EngineRef(
// Launcher of the engine
private[kyuubi] val appUser: String = shareLevel match {
case SERVER => Utils.currentUser
case GROUP =>
val clientUGI = UserGroupInformation.createRemoteUser(user)
// Similar to `clientUGI.getPrimaryGroupName` (avoid IOE) to get the Primary GroupName of
// the client user mapping to
clientUGI.getGroupNames.headOption match {
case Some(primaryGroup) => primaryGroup
case None =>
warn(s"There is no primary group for $user, use the client user name as group directly")
user
}
case _ => user
}
@ -109,7 +120,10 @@ private[kyuubi] class EngineRef(
* /`serverSpace_CONNECTION`/`user`/`engineRefId`
* For `USER` share level:
* /`serverSpace_USER`/`user`[/`subdomain`]
*
* For `GROUP` share level:
* /`serverSpace_GROUP`/`primary group name`[/`subdomain`]
* For `SERVER` share level:
* /`serverSpace_SERVER`/`kyuubi server user`[/`subdomain`]
*/
@VisibleForTesting
private[kyuubi] lazy val engineSpace: String = shareLevel match {

View File

@ -34,6 +34,7 @@ import org.apache.hadoop.hive.metastore.{HiveMetaException, HiveMetaStore}
import org.apache.hadoop.hive.thrift.{DelegationTokenIdentifier, HadoopThriftAuthBridge, HadoopThriftAuthBridge23}
import org.apache.hadoop.io.Text
import org.apache.hadoop.security.{Credentials, UserGroupInformation}
import org.apache.hadoop.security.authorize.ProxyUsers
import org.apache.thrift.TProcessor
import org.apache.thrift.protocol.TProtocol
import org.scalatest.concurrent.Eventually._
@ -82,6 +83,7 @@ class HiveDelegationTokenProviderSuite extends KerberizedTestHelper {
hiveConf.setVar(METASTORE_USE_THRIFT_SASL, "true")
hiveConf.setVar(METASTORE_KERBEROS_PRINCIPAL, testPrincipal)
hiveConf.setVar(METASTORE_KERBEROS_KEYTAB_FILE, testKeytab)
ProxyUsers.refreshSuperUserGroupsConfiguration(hiveConf)
val metaServer = new LocalMetaServer(hiveConf, classloader)
metaServer.start()
}

View File

@ -20,6 +20,7 @@ package org.apache.kyuubi.engine
import java.util.UUID
import org.apache.curator.utils.ZKPaths
import org.apache.hadoop.security.UserGroupInformation
import org.scalatest.time.SpanSugar.convertIntToGrainOfTime
import org.apache.kyuubi.{KyuubiFunSuite, Utils}
@ -34,7 +35,7 @@ class EngineRefSuite extends KyuubiFunSuite {
import ShareLevel._
private val zkServer = new EmbeddedZookeeper
private val conf = KyuubiConf()
val user = Utils.currentUser
private val user = Utils.currentUser
override def beforeAll(): Unit = {
val zkData = Utils.createTempDir()
@ -87,6 +88,32 @@ class EngineRefSuite extends KyuubiFunSuite {
}
}
test("GROUP shared level engine name") {
val id = UUID.randomUUID().toString
conf.set(KyuubiConf.ENGINE_SHARE_LEVEL, GROUP.toString)
val engineRef = new EngineRef(conf, user, id)
val primaryGroupName = UserGroupInformation.createRemoteUser(user).getPrimaryGroupName
assert(engineRef.engineSpace === ZKPaths.makePath(s"kyuubi_GROUP", primaryGroupName))
assert(engineRef.defaultEngineName === s"kyuubi_GROUP_${primaryGroupName}_$id")
Seq(KyuubiConf.ENGINE_SHARE_LEVEL_SUBDOMAIN,
KyuubiConf.ENGINE_SHARE_LEVEL_SUB_DOMAIN).foreach { k =>
conf.unset(k)
conf.set(k.key, "abc")
val engineRef2 = new EngineRef(conf, user, id)
assert(engineRef2.engineSpace ===
ZKPaths.makePath(s"kyuubi_$GROUP", primaryGroupName, "abc"))
assert(engineRef2.defaultEngineName === s"kyuubi_${GROUP}_${primaryGroupName}_abc_$id")
}
val userName = "Iamauserwithoutgroup"
val newUGI = UserGroupInformation.createRemoteUser(userName)
assert(newUGI.getGroupNames.isEmpty)
val engineRef3 = new EngineRef(conf, userName, id)
assert(engineRef3.engineSpace === ZKPaths.makePath(s"kyuubi_GROUP", userName, "abc"))
assert(engineRef3.defaultEngineName === s"kyuubi_GROUP_${userName}_abc_$id")
}
test("SERVER shared level engine name") {
val id = UUID.randomUUID().toString
conf.set(KyuubiConf.ENGINE_SHARE_LEVEL, SERVER.toString)

View File

@ -0,0 +1,61 @@
/*
* 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.operation
import org.apache.hadoop.security.UserGroupInformation
import org.apache.kyuubi.WithKyuubiServer
import org.apache.kyuubi.config.KyuubiConf
import org.apache.kyuubi.util.KyuubiHadoopUtils
class KyuubiOperationGroupSuite extends WithKyuubiServer with JDBCTests {
override protected def jdbcUrl: String = getJdbcUrl
override protected val conf: KyuubiConf = {
val c = KyuubiConf().set(KyuubiConf.ENGINE_SHARE_LEVEL, "group")
.set("hadoop.user.group.static.mapping.overrides",
s"user1=testGG,group_tt;user2=testGG")
UserGroupInformation.setConfiguration(KyuubiHadoopUtils.newHadoopConf(c))
c.set(s"hadoop.proxyuser.$user.groups", "*")
.set(s"hadoop.proxyuser.$user.hosts", "*")
}
test("ensure two connections in group mode share the same engine started by primary group") {
var r1: String = null
var r2: String = null
withSessionConf(Map("hive.server2.proxy.user" -> "user1"))(Map.empty)(Map.empty) {
withJdbcStatement() { statement =>
val res = statement.executeQuery("set spark.app.name")
assert(res.next())
r1 = res.getString("value")
}
}
withSessionConf(Map("hive.server2.proxy.user" -> "user2"))(Map.empty)(Map.empty) {
withJdbcStatement() { statement =>
val res = statement.executeQuery("set spark.app.name")
assert(res.next())
r2 = res.getString("value")
}
}
assert(r1 != null && r2 != null)
assert(r1 === r2)
assert(r1.startsWith(s"kyuubi_GROUP_testGG"))
}
}