diff --git a/LICENSE-binary b/LICENSE-binary index 7322dec51..065fc6499 100644 --- a/LICENSE-binary +++ b/LICENSE-binary @@ -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 diff --git a/dev/dependencyList b/dev/dependencyList index ab80bcfab..6ec0464c0 100644 --- a/dev/dependencyList +++ b/dev/dependencyList @@ -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 diff --git a/kyuubi-server/pom.xml b/kyuubi-server/pom.xml index 2a7ce2270..e1a9c3312 100644 --- a/kyuubi-server/pom.xml +++ b/kyuubi-server/pom.xml @@ -257,6 +257,11 @@ trino-client + + org.eclipse.jetty + jetty-proxy + + org.glassfish.jersey.test-framework jersey-test-framework-core diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiRestFrontendService.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiRestFrontendService.scala index c9d571008..86cb28aed 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiRestFrontendService.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiRestFrontendService.scala @@ -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/") diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/EngineUIProxyServlet.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/EngineUIProxyServlet.scala new file mode 100644 index 000000000..65fc04f58 --- /dev/null +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/EngineUIProxyServlet.scala @@ -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 + } +} diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/ApiRootResource.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/ApiRootResource.scala index d8b997e86..fc3150355 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/ApiRootResource.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/ApiRootResource.scala @@ -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 + } } diff --git a/kyuubi-server/web-ui/package.json b/kyuubi-server/web-ui/package.json index 131e69b7f..3814c49cd 100644 --- a/kyuubi-server/web-ui/package.json +++ b/kyuubi-server/web-ui/package.json @@ -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": { diff --git a/kyuubi-server/web-ui/src/locales/en_US/index.ts b/kyuubi-server/web-ui/src/locales/en_US/index.ts index 0501078d0..e291b62af 100644 --- a/kyuubi-server/web-ui/src/locales/en_US/index.ts +++ b/kyuubi-server/web-ui/src/locales/en_US/index.ts @@ -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', diff --git a/kyuubi-server/web-ui/src/locales/zh_CN/index.ts b/kyuubi-server/web-ui/src/locales/zh_CN/index.ts index 0e7521794..e6dd8fe62 100644 --- a/kyuubi-server/web-ui/src/locales/zh_CN/index.ts +++ b/kyuubi-server/web-ui/src/locales/zh_CN/index.ts @@ -34,6 +34,7 @@ export default { engine_type: 'Engine 类型', share_level: '共享级别', version: '版本', + engine_ui: 'Engine UI', operation: { text: '操作', delete_confirm: '确认删除', diff --git a/kyuubi-server/web-ui/src/test/unit/views/management/engine/index.spec.ts b/kyuubi-server/web-ui/src/test/unit/views/management/engine/index.spec.ts new file mode 100644 index 000000000..b19c83bd9 --- /dev/null +++ b/kyuubi-server/web-ui/src/test/unit/views/management/engine/index.spec.ts @@ -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/` + ) +}) diff --git a/kyuubi-server/web-ui/src/views/management/engine/index.vue b/kyuubi-server/web-ui/src/views/management/engine/index.vue index cecbde709..430482cdd 100644 --- a/kyuubi-server/web-ui/src/views/management/engine/index.vue +++ b/kyuubi-server/web-ui/src/views/management/engine/index.vue @@ -81,6 +81,22 @@