Skip to content

Commit

Permalink
[SPARK-50022][CORE][UI][3.5] Fix MasterPage to hide App UI links wh…
Browse files Browse the repository at this point in the history
…en UI is disabled

### What changes were proposed in this pull request?

This PR aims to fix `MasterPage` to hide App UI links when UI is disabled.

Previously, the link leads the following errors if a user clicks it.
<img width="997" alt="Screenshot 2024-10-17 at 22 06 22" src="https://github.com/user-attachments/assets/e53ba01f-533f-4d42-a488-830afaf40efa">

### Why are the changes needed?

**1. PREPARATION**

```
$ cat conf/spark-defaults.conf
spark.ui.reverseProxy true
spark.ui.reverseProxyUrl http://localhost:8080

$ sbin/start-master.sh

$ sbin/start-worker.sh spark://$(hostname):7077

$ bin/spark-shell --master spark://$(hostname):7077 -c spark.ui.enabled=false
```

**2. BEFORE**

<img width="340" alt="Screenshot 2024-10-17 at 22 01 16" src="https://github.com/user-attachments/assets/3069e43d-ba8c-4d36-8101-65e10b420879">

**3. AFTER**

<img width="345" alt="Screenshot 2024-10-17 at 22 04 12" src="https://github.com/user-attachments/assets/b9feba47-90fb-4557-803c-94eaa8ce62e1">

### Does this PR introduce _any_ user-facing change?

The previous behavior shows HTTP 500 error.

### How was this patch tested?

Pass the CIs with a newly added test case.

### Was this patch authored or co-authored using generative AI tooling?

No.

Closes apache#48549 from dongjoon-hyun/SPARK-50022-3.5.

Authored-by: Dongjoon Hyun <dongjoon@apache.org>
Signed-off-by: Dongjoon Hyun <dongjoon@apache.org>
  • Loading branch information
dongjoon-hyun committed Oct 18, 2024
1 parent 3a4ebae commit d24393b
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import javax.servlet.http.HttpServletRequest

import scala.xml.Node

import org.apache.commons.lang3.StringUtils
import org.json4s.JValue

import org.apache.spark.deploy.DeployMessages.{KillDriverResponse, MasterStateResponse, RequestKillDriver, RequestMasterState}
Expand Down Expand Up @@ -289,7 +290,7 @@ private[ui] class MasterPage(parent: MasterWebUI) extends WebUIPage("") {
</td>
<td>
{
if (app.isFinished) {
if (app.isFinished || StringUtils.isBlank(app.desc.appUiUrl)) {
app.desc.name
} else {
<a href={UIUtils.makeHref(parent.master.reverseProxy,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import org.apache.spark.rpc.{RpcEndpointRef, RpcEnv}
import org.apache.spark.util.Utils

class MasterWebUISuite extends SparkFunSuite {
import MasterWebUISuite._

val conf = new SparkConf().set(DECOMMISSION_ENABLED, true)
val securityMgr = new SecurityManager(conf)
Expand Down Expand Up @@ -113,12 +114,14 @@ class MasterWebUISuite extends SparkFunSuite {
private def convPostDataToString(data: Map[String, String]): String = {
convPostDataToString(data.toSeq)
}
}

object MasterWebUISuite {
/**
* Send an HTTP request to the given URL using the method and the body specified.
* Return the connection object.
*/
private def sendHttpRequest(
private[ui] def sendHttpRequest(
url: String,
method: String,
body: String = ""): HttpURLConnection = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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.spark.deploy.master.ui

import java.util.Date
import javax.servlet.http.HttpServletResponse.SC_OK

import scala.io.Source

import org.mockito.Mockito.{mock, when}

import org.apache.spark.{SecurityManager, SparkConf, SparkFunSuite}
import org.apache.spark.deploy.DeployMessages.{MasterStateResponse, RequestMasterState}
import org.apache.spark.deploy.master._
import org.apache.spark.deploy.master.ui.MasterWebUISuite._
import org.apache.spark.internal.config.DECOMMISSION_ENABLED
import org.apache.spark.internal.config.UI.UI_KILL_ENABLED
import org.apache.spark.rpc.{RpcEndpointRef, RpcEnv}
import org.apache.spark.util.Utils

class ReadOnlyMasterWebUISuite extends SparkFunSuite {

import org.apache.spark.deploy.DeployTestUtils._

val conf = new SparkConf()
.set(UI_KILL_ENABLED, false)
.set(DECOMMISSION_ENABLED, false)
val securityMgr = new SecurityManager(conf)
val rpcEnv = mock(classOf[RpcEnv])
val master = mock(classOf[Master])
val masterEndpointRef = mock(classOf[RpcEndpointRef])
when(master.securityMgr).thenReturn(securityMgr)
when(master.conf).thenReturn(conf)
when(master.rpcEnv).thenReturn(rpcEnv)
when(master.self).thenReturn(masterEndpointRef)
val desc1 = createAppDesc().copy(name = "WithUI")
val desc2 = desc1.copy(name = "WithoutUI", appUiUrl = "")
val app1 = new ApplicationInfo(new Date().getTime, "app1", desc1, new Date(), null, Int.MaxValue)
val app2 = new ApplicationInfo(new Date().getTime, "app2", desc2, new Date(), null, Int.MaxValue)
val state = new MasterStateResponse(
"host", 8080, None, Array.empty, Array(app1, app2), Array.empty,
Array.empty, Array.empty, RecoveryState.ALIVE)
when(masterEndpointRef.askSync[MasterStateResponse](RequestMasterState)).thenReturn(state)
val masterWebUI = new MasterWebUI(master, 0)

override def beforeAll(): Unit = {
super.beforeAll()
masterWebUI.bind()
}

override def afterAll(): Unit = {
try {
masterWebUI.stop()
} finally {
super.afterAll()
}
}

test("SPARK-50022: Fix 'MasterPage' to hide App UI links when UI is disabled") {
val url = s"http://${Utils.localHostNameForURI()}:${masterWebUI.boundPort}/"
val conn = sendHttpRequest(url, "GET")
assert(conn.getResponseCode === SC_OK)
val result = Source.fromInputStream(conn.getInputStream).mkString
assert(result.contains("<a href=\"appUiUrl\">WithUI</a>"))
assert(result.contains(" WithoutUI\n"))
}
}

0 comments on commit d24393b

Please sign in to comment.