[KYUUBI #3420][UI] Kyuubi Server Proxy Engine UI

### _Why are the changes needed?_

Kyuubi Server Proxy Engine UI

### _How was this patch tested?_
- [ ] Add some test cases that check the changes thoroughly including negative and positive cases if possible

- [x] Add screenshots for manual tests if appropriate

![截屏2023-06-06 10 35 54](https://github.com/apache/kyuubi/assets/52876270/ecbc33aa-11dd-418f-bfef-19aad9e7ea39)

- [ ] [Run test](https://kyuubi.readthedocs.io/en/master/develop_tools/testing.html#running-tests) locally before make a pull request

Closes #4795 from zwangsheng/KYUUBI_3420.

Closes #3420

079dc1c60 [zwangsheng] fix frontend unit test case
6e71b4518 [Cheng Pan] fix
cf7ca5145 [Cheng Pan] Update kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/ApiRootResource.scala
9a91d62a0 [Cheng Pan] polish
a5dcfae18 [zwangsheng] fix
5d4a8c239 [zwangsheng] Rebase
71d22fc9a [zwangsheng] fix
3b0152f33 [zwangsheng] [KYUUBI #3420][UI] Proxy Engnie UI

Lead-authored-by: zwangsheng <2213335496@qq.com>
Co-authored-by: Cheng Pan <chengpan@apache.org>
Co-authored-by: Cheng Pan <pan3793@gmail.com>
Signed-off-by: Cheng Pan <chengpan@apache.org>
This commit is contained in:
zwangsheng 2023-06-06 15:17:40 +08:00 committed by Cheng Pan
parent f398dc2165
commit 4cd00a8777
No known key found for this signature in database
GPG Key ID: 8001952629BCC75D
12 changed files with 170 additions and 2 deletions

View File

@ -261,6 +261,7 @@ io.etcd:jetcd-api
io.etcd:jetcd-common
io.etcd:jetcd-core
io.etcd:jetcd-grpc
org.eclipse.jetty:jetty-client
org.eclipse.jetty:jetty-http
org.eclipse.jetty:jetty-io
org.eclipse.jetty:jetty-security
@ -268,6 +269,7 @@ org.eclipse.jetty:jetty-server
org.eclipse.jetty:jetty-servlet
org.eclipse.jetty:jetty-util-ajax
org.eclipse.jetty:jetty-util
org.eclipse.jetty:jetty-proxy
org.apache.thrift:libfb303
org.apache.thrift:libthrift
org.apache.logging.log4j:log4j-1.2-api

View File

@ -95,8 +95,10 @@ jetcd-api/0.7.3//jetcd-api-0.7.3.jar
jetcd-common/0.7.3//jetcd-common-0.7.3.jar
jetcd-core/0.7.3//jetcd-core-0.7.3.jar
jetcd-grpc/0.7.3//jetcd-grpc-0.7.3.jar
jetty-client/9.4.51.v20230217//jetty-client-9.4.51.v20230217.jar
jetty-http/9.4.51.v20230217//jetty-http-9.4.51.v20230217.jar
jetty-io/9.4.51.v20230217//jetty-io-9.4.51.v20230217.jar
jetty-proxy/9.4.51.v20230217//jetty-proxy-9.4.51.v20230217.jar
jetty-security/9.4.51.v20230217//jetty-security-9.4.51.v20230217.jar
jetty-server/9.4.51.v20230217//jetty-server-9.4.51.v20230217.jar
jetty-servlet/9.4.51.v20230217//jetty-servlet-9.4.51.v20230217.jar

View File

@ -257,6 +257,11 @@
<artifactId>trino-client</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-proxy</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.test-framework</groupId>
<artifactId>jersey-test-framework-core</artifactId>

View File

@ -90,6 +90,9 @@ class KyuubiRestFrontendService(override val serverable: Serverable)
val authenticationFactory = new KyuubiHttpAuthenticationFactory(conf)
server.addHandler(authenticationFactory.httpHandlerWrapperFactory.wrapHandler(contextHandler))
val proxyHandler = ApiRootResource.getEngineUIProxyHandler(this)
server.addHandler(authenticationFactory.httpHandlerWrapperFactory.wrapHandler(proxyHandler))
server.addStaticHandler("org/apache/kyuubi/ui/static", "/static/")
server.addRedirectHandler("/", "/static/")
server.addRedirectHandler("/static", "/static/")

View File

@ -0,0 +1,65 @@
/*
* 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.server.api
import java.net.URL
import javax.servlet.http.HttpServletRequest
import org.eclipse.jetty.client.api.Request
import org.eclipse.jetty.proxy.ProxyServlet
import org.apache.kyuubi.Logging
private[api] class EngineUIProxyServlet extends ProxyServlet with Logging {
override def rewriteTarget(request: HttpServletRequest): String = {
val requestURL = request.getRequestURL
val requestURI = request.getRequestURI
var targetURL = "/no-ui-error"
extractTargetAddress(requestURI).foreach { case (host, port) =>
val targetURI = requestURI.stripPrefix(s"/engine-ui/$host:$port") match {
// for some reason, the proxy can not handle redirect well, as a workaround,
// we simulate the Spark UI redirection behavior and forcibly rewrite the
// empty URI to the Spark Jobs page.
case "" | "/" => "/jobs/"
case path => path
}
targetURL = new URL("http", host, port, targetURI).toString
}
debug(s"rewrite $requestURL => $targetURL")
targetURL
}
override def addXForwardedHeaders(
clientRequest: HttpServletRequest,
proxyRequest: Request): Unit = {
val requestURI = clientRequest.getRequestURI
extractTargetAddress(requestURI).foreach { case (host, port) =>
// SPARK-24209: Knox uses X-Forwarded-Context to notify the application the base path
proxyRequest.header("X-Forwarded-Context", s"/engine-ui/$host:$port")
}
super.addXForwardedHeaders(clientRequest, proxyRequest)
}
private val r = "^/engine-ui/([^/:]+):(\\d+)/?.*".r
private def extractTargetAddress(requestURI: String): Option[(String, Int)] =
requestURI match {
case r(host, port) => Some(host -> port.toInt)
case _ => None
}
}

View File

@ -30,7 +30,7 @@ import org.glassfish.jersey.servlet.ServletContainer
import org.apache.kyuubi.KYUUBI_VERSION
import org.apache.kyuubi.client.api.v1.dto._
import org.apache.kyuubi.server.KyuubiRestFrontendService
import org.apache.kyuubi.server.api.{ApiRequestContext, FrontendServiceContext, OpenAPIConfig}
import org.apache.kyuubi.server.api.{ApiRequestContext, EngineUIProxyServlet, FrontendServiceContext, OpenAPIConfig}
@Path("/v1")
private[v1] class ApiRootResource extends ApiRequestContext {
@ -82,4 +82,13 @@ private[server] object ApiRootResource {
handler.addServlet(holder, "/*")
handler
}
def getEngineUIProxyHandler(fe: KyuubiRestFrontendService): ServletContextHandler = {
val proxyServlet = new EngineUIProxyServlet()
val holder = new ServletHolder(proxyServlet)
val proxyHandler = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
proxyHandler.setContextPath("/engine-ui");
proxyHandler.addServlet(holder, "/*")
proxyHandler
}
}

View File

@ -11,7 +11,7 @@
"lint": "eslint --ext .ts,vue --ignore-path .gitignore .",
"lint-fix": "eslint --fix --ext .ts,vue --ignore-path .gitignore .",
"prettier": "prettier --write \"src/**/*.{vue,ts,tsx}\"",
"test": "vitest",
"test": "vitest --mode development",
"coverage": "vitest run --coverage"
},
"dependencies": {

View File

@ -34,6 +34,7 @@ export default {
engine_type: 'Engine Type',
share_level: 'Share Level',
version: 'Version',
engine_ui: 'Engine UI',
operation: {
text: 'Operation',
delete_confirm: 'Delete Confirm',

View File

@ -34,6 +34,7 @@ export default {
engine_type: 'Engine 类型',
share_level: '共享级别',
version: '版本',
engine_ui: 'Engine UI',
operation: {
text: '操作',
delete_confirm: '确认删除',

View File

@ -0,0 +1,45 @@
/*
* 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.
*/
import Engine from '@/views/management/engine/index.vue'
import { shallowMount } from '@vue/test-utils'
import { createI18n } from 'vue-i18n'
import { getStore } from '@/test/unit/utils'
import { createRouter, createWebHistory } from 'vue-router'
import ElementPlus from 'element-plus'
import { expect, test } from 'vitest'
test('proxy ui', async () => {
expect(Engine).toBeTruthy()
const i18n = createI18n({
legacy: false,
globalInjection: true
})
const mockRouter = createRouter({ history: createWebHistory(), routes: [] })
mockRouter.currentRoute.value.params = {
path: '/management/engine'
}
const wrapper = shallowMount(Engine, {
global: {
plugins: [i18n, mockRouter, getStore(), ElementPlus]
}
})
expect(wrapper.vm.getProxyEngineUI('host:ip')).toEqual(
`${import.meta.env.VITE_APP_DEV_WEB_URL}engine-ui/host:ip/`
)
})

View File

@ -81,6 +81,22 @@
<el-table-column fixed="right" :label="$t('operation.text')" width="120">
<template #default="scope">
<el-space wrap>
<el-tooltip
effect="dark"
:content="
$t('engine_ui') +
': ' +
scope.row.attributes['kyuubi.engine.url']
"
placement="top">
<el-button
type="primary"
icon="Link"
circle
@click="
openEngineUI(scope.row.attributes['kyuubi.engine.url'])
" />
</el-tooltip>
<el-popconfirm
:title="$t('operation.delete_confirm')"
@confirm="handleDeleteEngine(scope.row)">
@ -152,7 +168,20 @@
})
}
function getProxyEngineUI(url: string): string {
url = (url || '').replaceAll(/http:|https:/gi, '')
return `${import.meta.env.VITE_APP_DEV_WEB_URL}engine-ui/${url}/`
}
function openEngineUI(url: string) {
window.open(getProxyEngineUI(url))
}
init()
// export for test
defineExpose({
getProxyEngineUI
})
</script>
<style scoped lang="scss">

View File

@ -1030,6 +1030,12 @@
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-proxy</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_${scala.binary.version}</artifactId>