[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  - [ ] [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:
parent
f398dc2165
commit
4cd00a8777
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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/")
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -34,6 +34,7 @@ export default {
|
||||
engine_type: 'Engine 类型',
|
||||
share_level: '共享级别',
|
||||
version: '版本',
|
||||
engine_ui: 'Engine UI',
|
||||
operation: {
|
||||
text: '操作',
|
||||
delete_confirm: '确认删除',
|
||||
|
||||
@ -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/`
|
||||
)
|
||||
})
|
||||
@ -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">
|
||||
|
||||
6
pom.xml
6
pom.xml
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user