sunchao commented on code in PR #58243:
URL: https://github.com/apache/spark/pull/58243#discussion_r3926551464


##########
core/src/main/scala/org/apache/spark/deploy/master/ui/MasterWebUI.scala:
##########
@@ -64,10 +63,19 @@ class MasterWebUI(
     addStaticHandler(MasterWebUI.STATIC_RESOURCE_DIR)
     addRenderLogHandler(this, master.conf)
     if (killEnabled) {
+      val isProxy = master.conf.get(UI_REVERSE_PROXY) &&
+        master.conf.get(PROXY_REDIRECT_URI).isEmpty
+      val killRedirectTarget = if (isProxy) {
+        master.conf.get(UI_REVERSE_PROXY_URL).map(_.stripSuffix("/") + 
"/").getOrElse("/")

Review Comment:
   [P2] Avoid doubling path prefixes in frontend-proxy kill redirects
   
   With `spark.ui.reverseProxy=true`, `spark.ui.reverseProxyUrl=/spark`, and no 
`spark.ui.proxyRedirectUri`, this passes `/spark/` to `createRedirectHandler`, 
which resolves it to `http://<master>/spark/`. A frontend configured as 
`location /spark/ { proxy_pass http://<master>/; }` then applies its normal 
redirect rewriting and adds `/spark/` again. An actual nginx probe returned 
`/spark/spark/` on this revision, versus `/spark/` on the parent, breaking 
navigation after a kill. This affects both kill endpoints and the documented 
path-only proxy URL configuration. Please preserve the server-relative public 
destination or otherwise avoid embedding the public prefix in an 
upstream-absolute redirect that the frontend rewrites.



##########
core/src/test/scala/org/apache/spark/deploy/master/ui/MasterWebUISuite.scala:
##########
@@ -128,6 +128,78 @@ class MasterWebUISuite extends SparkFunSuite {
       denyWebUI.stop()
     }
   }
+
+  test("SPARK-58893: kill application redirect location with reverse proxy") {
+    val reverseProxyConf = new SparkConf()
+      .set(DECOMMISSION_ENABLED, true)
+      .set(UI_REVERSE_PROXY, true)
+      .set(UI_REVERSE_PROXY_URL, "http://proxyhost:8080/myproxy";)
+    val mockMaster = mock(classOf[Master])
+    when(mockMaster.securityMgr).thenReturn(securityMgr)
+    when(mockMaster.conf).thenReturn(reverseProxyConf)
+    when(mockMaster.rpcEnv).thenReturn(rpcEnv)
+    when(mockMaster.self).thenReturn(masterEndpointRef)
+
+    val activeApp = new ApplicationInfo(
+      new Date().getTime, "app-proxy-0", createAppDesc(), new Date(), null, 
Int.MaxValue)
+    val appMap = HashMap[String, ApplicationInfo]((activeApp.id, activeApp))
+    when(mockMaster.idToApp).thenReturn(appMap)
+
+    val webUI = new MasterWebUI(mockMaster, 0)
+    try {
+      webUI.bind()
+      val url = 
s"http://${Utils.localHostNameForURI()}:${webUI.boundPort}/app/kill/"
+      val body = convPostDataToString(Map(("id", activeApp.id), ("terminate", 
"true")))
+      val conn = new 
URI(url).toURL.openConnection().asInstanceOf[HttpURLConnection]
+      conn.setInstanceFollowRedirects(false)
+      conn.setRequestMethod("POST")
+      conn.setDoOutput(true)
+      conn.setRequestProperty("Content-Type", 
"application/x-www-form-urlencoded")
+      val out = new DataOutputStream(conn.getOutputStream)
+      out.write(body.getBytes(StandardCharsets.UTF_8))
+      out.close()
+      assert(conn.getResponseCode === 302)
+      assert(conn.getHeaderField("Location") === 
"http://proxyhost:8080/myproxy/";)
+    } finally {
+      webUI.stop()
+    }
+  }
+
+  test("SPARK-58893: honor reverseProxy=false when choosing kill redirect") {
+    val noProxyConf = new SparkConf()
+      .set(DECOMMISSION_ENABLED, true)
+      .set(UI_REVERSE_PROXY, false)
+      .set(UI_REVERSE_PROXY_URL, "http://proxyhost:8080/myproxy";)
+    val mockMaster = mock(classOf[Master])
+    when(mockMaster.securityMgr).thenReturn(securityMgr)
+    when(mockMaster.conf).thenReturn(noProxyConf)
+    when(mockMaster.rpcEnv).thenReturn(rpcEnv)
+    when(mockMaster.self).thenReturn(masterEndpointRef)
+
+    val activeApp = new ApplicationInfo(
+      new Date().getTime, "app-proxy-1", createAppDesc(), new Date(), null, 
Int.MaxValue)
+    val appMap = HashMap[String, ApplicationInfo]((activeApp.id, activeApp))
+    when(mockMaster.idToApp).thenReturn(appMap)
+
+    val webUI = new MasterWebUI(mockMaster, 0)
+    try {
+      webUI.bind()
+      val url = 
s"http://${Utils.localHostNameForURI()}:${webUI.boundPort}/app/kill/"
+      val body = convPostDataToString(Map(("id", activeApp.id), ("terminate", 
"true")))
+      val conn = new 
URI(url).toURL.openConnection().asInstanceOf[HttpURLConnection]
+      conn.setInstanceFollowRedirects(false)
+      conn.setRequestMethod("POST")
+      conn.setDoOutput(true)
+      conn.setRequestProperty("Content-Type", 
"application/x-www-form-urlencoded")
+      val out = new DataOutputStream(conn.getOutputStream)
+      out.write(body.getBytes(StandardCharsets.UTF_8))
+      out.close()
+      assert(conn.getResponseCode === 302)
+      assert(conn.getHeaderField("Location") === "/")

Review Comment:
   [P2] Expect the absolute local root in the disabled-proxy test
   
   `createRedirectHandler` resolves `/` against the request URL before calling 
`sendRedirect`, so this response contains `http://<local-host>:<bound-port>/`, 
not the literal `/`. The new assertion therefore fails even when disabled-proxy 
behavior is correct. This is present in the current CI annotations and 
reproduced locally: the suite passed six tests and failed this one with 
`http://localhost:<port>/` versus `/`. Please assert the absolute local root, 
or compare both the redirect origin and its `/` path.



##########
core/src/main/scala/org/apache/spark/SparkContext.scala:
##########
@@ -2405,6 +2416,12 @@ class SparkContext(config: SparkConf) extends Logging {
     ResourceProfile.clearDefaultProfile()
     // Unset YARN mode system env variable, to allow switching between cluster 
types.
     SparkContext.clearActiveContext()
+    if (_conf.get(UI_REVERSE_PROXY)) {
+      _previousProxyBase match {
+        case Some(oldBase) => System.setProperty("spark.ui.proxyBase", oldBase)
+        case None => System.clearProperty("spark.ui.proxyBase")

Review Comment:
   [P2] Preserve the proxy base when startup fails before the snapshot
   
   `_previousProxyBase` remains `None` until initialization reaches the proxy 
setup block. If construction fails earlier, the exception handler still invokes 
`stop()`, and this branch clears an existing `spark.ui.proxyBase` even though 
this context never changed it. For example, with `/gateway` already set, 
constructing a context with reverse proxy enabled but a missing `spark.master` 
clears the property on this revision; the same probe preserves it on the 
parent. The JVM consequently loses its externally supplied proxy prefix after 
the failed attempt. Please distinguish "not captured/changed" from "captured as 
absent", and restore or clear the property only after this context has taken 
ownership of it.



##########
core/src/main/scala/org/apache/spark/ui/JettyUtils.scala:
##########
@@ -222,6 +244,15 @@ private[spark] object JettyUtils extends Logging {
           val newHeader = createProxyLocationHeader(headerValue, clientRequest,
             serverResponse.getRequest().getURI())
           if (newHeader != null) {
+            if (normalizedReverseProxyUrl.nonEmpty) {
+              val scheme = clientRequest.getScheme
+              val host = Option(clientRequest.getHeader("host")).getOrElse("")
+              val rootProxyPrefix = s"$scheme://$host/proxy/"
+              if (newHeader.startsWith(rootProxyPrefix)) {
+                val rest = newHeader.substring(rootProxyPrefix.length)
+                return s"$normalizedReverseProxyUrl/proxy/$rest"

Review Comment:
   [P2] Update the worker UI fixture for the changed redirect behavior
   
   The existing `MasterWorkerUISuite` test "master/worker web ui available 
behind front-end reverseProxy" configures `http://proxyhost:8080/path/to/spark` 
but fetches `/proxy/<worker>/json` through the direct master address. The 
worker's `/json` to `/json/` redirect now points to that configured external 
URL, and `Source.fromURL` follows it to the nonexistent `proxyhost`, timing 
out. The focused test passes on the parent and fails on this revision, both 
locally and in CI. Please adapt the fixture along with this change, using a 
reachable frontend or checking redirects without following them and fetching 
content through the local route. The failure reflects the fixture's 
assumptions, rather than showing that external redirect rewriting itself should 
be removed.



##########
core/src/main/scala/org/apache/spark/ui/UIUtils.scala:
##########
@@ -201,7 +201,14 @@ private[spark] object UIUtils extends Logging {
       request: HttpServletRequest,
       basePath: String = "",
       resource: String = ""): String = {
-    uiRoot(request) + basePath + resource
+    val root = uiRoot(request).stripSuffix("/")
+    val cleanBase = if (basePath.startsWith("/")) basePath
+                    else if (basePath.nonEmpty) "/" + basePath

Review Comment:
   [P2] Update the storage fixture for normalized base paths
   
   `StoragePageSuite` currently mocks `storageTab.basePath` as 
`http://localhost:4040`. This normalization changes the generated link to 
`/http://localhost:4040/storage/rdd/?id=1`, so its existing `rddTable` 
assertions fail. The suite passes 5/5 on the parent and 4/5 on this revision; 
the same failure is reported by CI. Production callers supply an empty or 
rooted context path, so this fixture does not establish a production 
absolute-URL use case. Please update the mock to a realistic context path and 
adjust its link expectations, or explicitly preserve and test absolute-base 
behavior if that remains part of this helper's contract.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to