You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kyuubi.apache.org by fe...@apache.org on 2024/04/19 16:32:54 UTC

(kyuubi) branch master updated: [KYUUBI #6321] Support to get Spark Kubernetes app URL

This is an automated email from the ASF dual-hosted git repository.

feiwang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kyuubi.git


The following commit(s) were added to refs/heads/master by this push:
     new 90491fc07 [KYUUBI #6321] Support to get Spark Kubernetes app URL
90491fc07 is described below

commit 90491fc07eb895c9b773cce94dbe54ba636c11f5
Author: Wang, Fei <fw...@ebay.com>
AuthorDate: Fri Apr 19 09:32:46 2024 -0700

    [KYUUBI #6321] Support to get Spark Kubernetes app URL
    
    # :mag: Description
    ## Issue References ๐Ÿ”—
    
    Now for kubernetes application, there is no runtime AppURL return in the application report.
    
    In fact, we can get the driver svc and get the spark-ui port, then build the Spark UI URL same with that of spark.
    
    https://github.com/apache/spark/blob/074ddc2825674edcea1bb7febf2c6d8b27c2e375/resource-managers/kubernetes/core/src/main/scala/org/apache/spark/deploy/k8s/features/DriverServiceFeatureStep.scala#L67
    
    https://github.com/apache/spark/blob/074ddc2825674edcea1bb7febf2c6d8b27c2e375/resource-managers/kubernetes/core/src/main/scala/org/apache/spark/deploy/k8s/features/DriverServiceFeatureStep.scala#L96-L102
    
    Service Selector example:
    ```
      selector:
        kyuubi-unique-tag: bf5fa281-0b2f-4096-aa0a-13a463c147a6
        spark-app-selector: spark-bf074093a7994954a89101eb2831bd1e
    ```
    ## Describe Your Solution ๐Ÿ”ง
    
    Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
    
    ## Types of changes :bookmark:
    
    - [ ] Bugfix (non-breaking change which fixes an issue)
    - [ ] New feature (non-breaking change which adds functionality)
    - [ ] Breaking change (fix or feature that would cause existing functionality to change)
    
    ## Test Plan ๐Ÿงช
    
    #### Behavior Without This Pull Request :coffin:
    
    #### Behavior With This Pull Request :tada:
    
    #### Related Unit Tests
    
    ---
    
    # Checklist ๐Ÿ“
    
    - [ ] This patch was not authored or co-authored using [Generative Tooling](https://www.apache.org/legal/generative-tooling.html)
    
    **Be nice. Be informative.**
    
    Closes #6318 from turboFei/k8s_spark_ui.
    
    Closes #6321
    
    f77999ef0 [Wang, Fei] with label selector does not work
    4e8d5767b [Wang, Fei] typo
    3f1eb545c [Wang, Fei] comments
    6f0d01d94 [Wang, Fei] ut
    9edfde034 [Wang, Fei] ut
    65d5e8175 [Wang, Fei] using pattern
    9b21d30df [Wang, Fei] Support to get kubernetes app url
    
    Authored-by: Wang, Fei <fw...@ebay.com>
    Signed-off-by: Wang, Fei <fw...@ebay.com>
---
 docs/configuration/settings.md                     |  41 ++++----
 .../org/apache/kyuubi/config/KyuubiConf.scala      |  11 ++
 .../engine/KubernetesApplicationOperation.scala    | 112 +++++++++++++++++++--
 .../KubernetesApplicationOperationSuite.scala      |  40 ++++++++
 4 files changed, 176 insertions(+), 28 deletions(-)

diff --git a/docs/configuration/settings.md b/docs/configuration/settings.md
index f102e0a05..c1e418d62 100644
--- a/docs/configuration/settings.md
+++ b/docs/configuration/settings.md
@@ -339,26 +339,27 @@ You can configure the Kyuubi properties in `$KYUUBI_HOME/conf/kyuubi-defaults.co
 
 ### Kubernetes
 
-|                                 Key                                  |         Default         |                                                                                                                                                                         Meaning                                                                                                                                                                          |   Type   | Since |
-|----------------------------------------------------------------------|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------|
-| kyuubi.kubernetes.application.state.container                        | spark-kubernetes-driver | The container name to retrieve the application state from.                                                                                                                                                                                                                                                                                               | string   | 1.8.1 |
-| kyuubi.kubernetes.application.state.source                           | POD                     | The source to retrieve the application state from. The valid values are pod and container. If the source is container and there is container inside the pod with the name of kyuubi.kubernetes.application.state.container, the application state will be from the matched container state. Otherwise, the application state will be from the pod state. | string   | 1.8.1 |
-| kyuubi.kubernetes.authenticate.caCertFile                            | &lt;undefined&gt;       | Path to the CA cert file for connecting to the Kubernetes API server over TLS from the kyuubi. Specify this as a path as opposed to a URI (i.e. do not provide a scheme)                                                                                                                                                                                 | string   | 1.7.0 |
-| kyuubi.kubernetes.authenticate.clientCertFile                        | &lt;undefined&gt;       | Path to the client cert file for connecting to the Kubernetes API server over TLS from the kyuubi. Specify this as a path as opposed to a URI (i.e. do not provide a scheme)                                                                                                                                                                             | string   | 1.7.0 |
-| kyuubi.kubernetes.authenticate.clientKeyFile                         | &lt;undefined&gt;       | Path to the client key file for connecting to the Kubernetes API server over TLS from the kyuubi. Specify this as a path as opposed to a URI (i.e. do not provide a scheme)                                                                                                                                                                              | string   | 1.7.0 |
-| kyuubi.kubernetes.authenticate.oauthToken                            | &lt;undefined&gt;       | The OAuth token to use when authenticating against the Kubernetes API server. Note that unlike, the other authentication options, this must be the exact string value of the token to use for the authentication.                                                                                                                                        | string   | 1.7.0 |
-| kyuubi.kubernetes.authenticate.oauthTokenFile                        | &lt;undefined&gt;       | Path to the file containing the OAuth token to use when authenticating against the Kubernetes API server. Specify this as a path as opposed to a URI (i.e. do not provide a scheme)                                                                                                                                                                      | string   | 1.7.0 |
-| kyuubi.kubernetes.context                                            | &lt;undefined&gt;       | The desired context from your kubernetes config file used to configure the K8s client for interacting with the cluster.                                                                                                                                                                                                                                  | string   | 1.6.0 |
-| kyuubi.kubernetes.context.allow.list                                                          || The allowed kubernetes context list, if it is empty, there is no kubernetes context limitation.                                                                                                                                                                                                                                                          | set      | 1.8.0 |
-| kyuubi.kubernetes.master.address                                     | &lt;undefined&gt;       | The internal Kubernetes master (API server) address to be used for kyuubi.                                                                                                                                                                                                                                                                               | string   | 1.7.0 |
-| kyuubi.kubernetes.namespace                                          | default                 | The namespace that will be used for running the kyuubi pods and find engines.                                                                                                                                                                                                                                                                            | string   | 1.7.0 |
-| kyuubi.kubernetes.namespace.allow.list                                                        || The allowed kubernetes namespace list, if it is empty, there is no kubernetes namespace limitation.                                                                                                                                                                                                                                                      | set      | 1.8.0 |
-| kyuubi.kubernetes.spark.cleanupTerminatedDriverPod.checkInterval     | PT1M                    | Kyuubi server use guava cache as the cleanup trigger with time-based eviction, but the eviction would not happened until any get/put operation happened. This option schedule a daemon thread evict cache periodically.                                                                                                                                  | duration | 1.8.1 |
-| kyuubi.kubernetes.spark.cleanupTerminatedDriverPod.kind              | NONE                    | Kyuubi server will delete the spark driver pod after the application terminates for kyuubi.kubernetes.terminatedApplicationRetainPeriod. Available options are NONE, ALL, COMPLETED and default value is None which means none of the pod will be deleted                                                                                                | string   | 1.8.1 |
-| kyuubi.kubernetes.spark.forciblyRewriteDriverPodName.enabled         | false                   | Whether to forcibly rewrite Spark driver pod name with 'kyuubi-<uuid>-driver'. If disabled, Kyuubi will try to preserve the application name while satisfying K8s' pod name policy, but some vendors may have stricter pod name policies, thus the generated name may become illegal.                                                                    | boolean  | 1.8.1 |
-| kyuubi.kubernetes.spark.forciblyRewriteExecutorPodNamePrefix.enabled | false                   | Whether to forcibly rewrite Spark executor pod name prefix with 'kyuubi-<uuid>'. If disabled, Kyuubi will try to preserve the application name while satisfying K8s' pod name policy, but some vendors may have stricter Pod name policies, thus the generated name may become illegal.                                                                  | boolean  | 1.8.1 |
-| kyuubi.kubernetes.terminatedApplicationRetainPeriod                  | PT5M                    | The period for which the Kyuubi server retains application information after the application terminates.                                                                                                                                                                                                                                                 | duration | 1.7.1 |
-| kyuubi.kubernetes.trust.certificates                                 | false                   | If set to true then client can submit to kubernetes cluster only with token                                                                                                                                                                                                                                                                              | boolean  | 1.7.0 |
+|                                 Key                                  |                                  Default                                   |                                                                                                                                                                         Meaning                                                                                                                                                                        [...]
+|----------------------------------------------------------------------|----------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- [...]
+| kyuubi.kubernetes.application.state.container                        | spark-kubernetes-driver                                                    | The container name to retrieve the application state from.                                                                                                                                                                                                                                                                                             [...]
+| kyuubi.kubernetes.application.state.source                           | POD                                                                        | The source to retrieve the application state from. The valid values are pod and container. If the source is container and there is container inside the pod with the name of kyuubi.kubernetes.application.state.container, the application state will be from the matched container state. Otherwise, the application state will be from the pod stat [...]
+| kyuubi.kubernetes.authenticate.caCertFile                            | &lt;undefined&gt;                                                          | Path to the CA cert file for connecting to the Kubernetes API server over TLS from the kyuubi. Specify this as a path as opposed to a URI (i.e. do not provide a scheme)                                                                                                                                                                               [...]
+| kyuubi.kubernetes.authenticate.clientCertFile                        | &lt;undefined&gt;                                                          | Path to the client cert file for connecting to the Kubernetes API server over TLS from the kyuubi. Specify this as a path as opposed to a URI (i.e. do not provide a scheme)                                                                                                                                                                           [...]
+| kyuubi.kubernetes.authenticate.clientKeyFile                         | &lt;undefined&gt;                                                          | Path to the client key file for connecting to the Kubernetes API server over TLS from the kyuubi. Specify this as a path as opposed to a URI (i.e. do not provide a scheme)                                                                                                                                                                            [...]
+| kyuubi.kubernetes.authenticate.oauthToken                            | &lt;undefined&gt;                                                          | The OAuth token to use when authenticating against the Kubernetes API server. Note that unlike, the other authentication options, this must be the exact string value of the token to use for the authentication.                                                                                                                                      [...]
+| kyuubi.kubernetes.authenticate.oauthTokenFile                        | &lt;undefined&gt;                                                          | Path to the file containing the OAuth token to use when authenticating against the Kubernetes API server. Specify this as a path as opposed to a URI (i.e. do not provide a scheme)                                                                                                                                                                    [...]
+| kyuubi.kubernetes.context                                            | &lt;undefined&gt;                                                          | The desired context from your kubernetes config file used to configure the K8s client for interacting with the cluster.                                                                                                                                                                                                                                [...]
+| kyuubi.kubernetes.context.allow.list                                                                                                             || The allowed kubernetes context list, if it is empty, there is no kubernetes context limitation.                                                                                                                                                                                                                                                        [...]
+| kyuubi.kubernetes.master.address                                     | &lt;undefined&gt;                                                          | The internal Kubernetes master (API server) address to be used for kyuubi.                                                                                                                                                                                                                                                                             [...]
+| kyuubi.kubernetes.namespace                                          | default                                                                    | The namespace that will be used for running the kyuubi pods and find engines.                                                                                                                                                                                                                                                                          [...]
+| kyuubi.kubernetes.namespace.allow.list                                                                                                           || The allowed kubernetes namespace list, if it is empty, there is no kubernetes namespace limitation.                                                                                                                                                                                                                                                    [...]
+| kyuubi.kubernetes.spark.appUrlPattern                                | http://{{SPARK_DRIVER_SVC}}.{{KUBERNETES_NAMESPACE}}.svc:{{SPARK_UI_PORT}} | The pattern to generate the spark on kubernetes application UI URL. The pattern should contain placeholders for the application variables. Available placeholders are `{{SPARK_APP_ID}}`, `{{SPARK_DRIVER_SVC}}`, `{{KUBERNETES_NAMESPACE}}`, `{{KUBERNETES_CONTEXT}}` and `{{SPARK_UI_PORT}}`.                                                        [...]
+| kyuubi.kubernetes.spark.cleanupTerminatedDriverPod.checkInterval     | PT1M                                                                       | Kyuubi server use guava cache as the cleanup trigger with time-based eviction, but the eviction would not happened until any get/put operation happened. This option schedule a daemon thread evict cache periodically.                                                                                                                                [...]
+| kyuubi.kubernetes.spark.cleanupTerminatedDriverPod.kind              | NONE                                                                       | Kyuubi server will delete the spark driver pod after the application terminates for kyuubi.kubernetes.terminatedApplicationRetainPeriod. Available options are NONE, ALL, COMPLETED and default value is None which means none of the pod will be deleted                                                                                              [...]
+| kyuubi.kubernetes.spark.forciblyRewriteDriverPodName.enabled         | false                                                                      | Whether to forcibly rewrite Spark driver pod name with 'kyuubi-<uuid>-driver'. If disabled, Kyuubi will try to preserve the application name while satisfying K8s' pod name policy, but some vendors may have stricter pod name policies, thus the generated name may become illegal.                                                                  [...]
+| kyuubi.kubernetes.spark.forciblyRewriteExecutorPodNamePrefix.enabled | false                                                                      | Whether to forcibly rewrite Spark executor pod name prefix with 'kyuubi-<uuid>'. If disabled, Kyuubi will try to preserve the application name while satisfying K8s' pod name policy, but some vendors may have stricter Pod name policies, thus the generated name may become illegal.                                                                [...]
+| kyuubi.kubernetes.terminatedApplicationRetainPeriod                  | PT5M                                                                       | The period for which the Kyuubi server retains application information after the application terminates.                                                                                                                                                                                                                                               [...]
+| kyuubi.kubernetes.trust.certificates                                 | false                                                                      | If set to true then client can submit to kubernetes cluster only with token                                                                                                                                                                                                                                                                            [...]
 
 ### Lineage
 
diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala
index 397c04935..35a072c10 100644
--- a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala
+++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala
@@ -1285,6 +1285,17 @@ object KyuubiConf {
       .stringConf
       .createWithDefault(KubernetesCleanupDriverPodStrategy.NONE.toString)
 
+  val KUBERNETES_SPARK_APP_URL_PATTERN: ConfigEntry[String] =
+    buildConf("kyuubi.kubernetes.spark.appUrlPattern")
+      .doc("The pattern to generate the spark on kubernetes application UI URL. " +
+        "The pattern should contain placeholders for the application variables. " +
+        "Available placeholders are `{{SPARK_APP_ID}}`, `{{SPARK_DRIVER_SVC}}`, " +
+        "`{{KUBERNETES_NAMESPACE}}`, `{{KUBERNETES_CONTEXT}}` and `{{SPARK_UI_PORT}}`.")
+      .version("1.10.0")
+      .stringConf
+      .createWithDefault(
+        "http://{{SPARK_DRIVER_SVC}}.{{KUBERNETES_NAMESPACE}}.svc:{{SPARK_UI_PORT}}")
+
   object KubernetesCleanupDriverPodStrategy extends Enumeration {
     type KubernetesCleanupDriverPodStrategy = Value
     val NONE, ALL, COMPLETED = Value
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/KubernetesApplicationOperation.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/KubernetesApplicationOperation.scala
index 4cdc07b2e..392de720a 100644
--- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/KubernetesApplicationOperation.scala
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/KubernetesApplicationOperation.scala
@@ -24,7 +24,7 @@ import scala.collection.JavaConverters._
 import scala.util.control.NonFatal
 
 import com.google.common.cache.{Cache, CacheBuilder, RemovalNotification}
-import io.fabric8.kubernetes.api.model.{ContainerState, Pod}
+import io.fabric8.kubernetes.api.model.{ContainerState, Pod, Service}
 import io.fabric8.kubernetes.client.KubernetesClient
 import io.fabric8.kubernetes.client.informers.{ResourceEventHandler, SharedIndexInformer}
 
@@ -44,6 +44,8 @@ class KubernetesApplicationOperation extends ApplicationOperation with Logging {
     new ConcurrentHashMap[KubernetesInfo, KubernetesClient]
   private val enginePodInformers: ConcurrentHashMap[KubernetesInfo, SharedIndexInformer[Pod]] =
     new ConcurrentHashMap[KubernetesInfo, SharedIndexInformer[Pod]]
+  private val engineSvcInformers: ConcurrentHashMap[KubernetesInfo, SharedIndexInformer[Service]] =
+    new ConcurrentHashMap[KubernetesInfo, SharedIndexInformer[Service]]
 
   private var submitTimeout: Long = _
   private var kyuubiConf: KyuubiConf = _
@@ -98,7 +100,10 @@ class KubernetesApplicationOperation extends ApplicationOperation with Logging {
           .withLabel(LABEL_KYUUBI_UNIQUE_KEY)
           .inform(new SparkEnginePodEventHandler(kubernetesInfo))
         info(s"[$kubernetesInfo] Start Kubernetes Client Informer.")
+        val engineSvcInformer = client.services()
+          .inform(new SparkEngineSvcEventHandler(kubernetesInfo))
         enginePodInformers.put(kubernetesInfo, enginePodInformer)
+        engineSvcInformers.put(kubernetesInfo, engineSvcInformer)
         client
 
       case None => throw new KyuubiException(s"Fail to build Kubernetes client for $kubernetesInfo")
@@ -264,6 +269,11 @@ class KubernetesApplicationOperation extends ApplicationOperation with Logging {
     }
     enginePodInformers.clear()
 
+    engineSvcInformers.asScala.foreach { case (_, informer) =>
+      Utils.tryLogNonFatalError(informer.stop())
+    }
+    engineSvcInformers.clear()
+
     if (cleanupTerminatedAppInfoTrigger != null) {
       cleanupTerminatedAppInfoTrigger.invalidateAll()
       cleanupTerminatedAppInfoTrigger = null
@@ -317,22 +327,85 @@ class KubernetesApplicationOperation extends ApplicationOperation with Logging {
     }
   }
 
+  private class SparkEngineSvcEventHandler(kubernetesInfo: KubernetesInfo)
+    extends ResourceEventHandler[Service] {
+
+    override def onAdd(svc: Service): Unit = {
+      if (isSparkEngineSvc(svc)) {
+        updateApplicationUrl(kubernetesInfo, svc)
+      }
+    }
+
+    override def onUpdate(oldSvc: Service, newSvc: Service): Unit = {
+      if (isSparkEngineSvc(newSvc)) {
+        updateApplicationUrl(kubernetesInfo, newSvc)
+      }
+    }
+
+    override def onDelete(svc: Service, deletedFinalStateUnknown: Boolean): Unit = {
+      // do nothing
+    }
+  }
+
   private def isSparkEnginePod(pod: Pod): Boolean = {
     val labels = pod.getMetadata.getLabels
     labels.containsKey(LABEL_KYUUBI_UNIQUE_KEY) && labels.containsKey(SPARK_APP_ID_LABEL)
   }
 
+  private def isSparkEngineSvc(svc: Service): Boolean = {
+    val selectors = svc.getSpec.getSelector
+    selectors.containsKey(LABEL_KYUUBI_UNIQUE_KEY) && selectors.containsKey(SPARK_APP_ID_LABEL)
+  }
+
   private def updateApplicationState(kubernetesInfo: KubernetesInfo, pod: Pod): Unit = {
     val (appState, appError) =
       toApplicationStateAndError(pod, appStateSource, appStateContainer)
     debug(s"Driver Informer changes pod: ${pod.getMetadata.getName} to state: $appState")
-    appInfoStore.put(
-      pod.getMetadata.getLabels.get(LABEL_KYUUBI_UNIQUE_KEY),
-      kubernetesInfo -> ApplicationInfo(
-        id = pod.getMetadata.getLabels.get(SPARK_APP_ID_LABEL),
-        name = pod.getMetadata.getName,
-        state = appState,
-        error = appError))
+    val kyuubiUniqueKey = pod.getMetadata.getLabels.get(LABEL_KYUUBI_UNIQUE_KEY)
+    appInfoStore.synchronized {
+      Option(appInfoStore.get(kyuubiUniqueKey)).map { case (_, appInfo) =>
+        appInfoStore.put(
+          kyuubiUniqueKey,
+          kubernetesInfo -> appInfo.copy(
+            id = pod.getMetadata.getLabels.get(SPARK_APP_ID_LABEL),
+            name = pod.getMetadata.getName,
+            state = appState,
+            error = appError))
+      }.getOrElse {
+        appInfoStore.put(
+          kyuubiUniqueKey,
+          kubernetesInfo -> ApplicationInfo(
+            id = pod.getMetadata.getLabels.get(SPARK_APP_ID_LABEL),
+            name = pod.getMetadata.getName,
+            state = appState,
+            error = appError))
+      }
+    }
+  }
+
+  private def updateApplicationUrl(kubernetesInfo: KubernetesInfo, svc: Service): Unit = {
+    svc.getSpec.getPorts.asScala.find(_.getName == SPARK_UI_PORT_NAME).map(_.getPort).map {
+      sparkUiPort =>
+        val appUrlPattern = kyuubiConf.get(KyuubiConf.KUBERNETES_SPARK_APP_URL_PATTERN)
+        val sparkAppId = svc.getSpec.getSelector.get(SPARK_APP_ID_LABEL)
+        val sparkDriverSvc = svc.getMetadata.getName
+        val kubernetesNamespace = kubernetesInfo.namespace.getOrElse("")
+        val kubernetesContext = kubernetesInfo.context.getOrElse("")
+        val appUrl = buildSparkAppUrl(
+          appUrlPattern,
+          sparkAppId,
+          sparkDriverSvc,
+          kubernetesContext,
+          kubernetesNamespace,
+          sparkUiPort)
+        debug(s"Driver Informer svc: ${svc.getMetadata.getName} app url: $appUrl")
+        val kyuubiUniqueKey = svc.getSpec.getSelector.get(LABEL_KYUUBI_UNIQUE_KEY)
+        appInfoStore.synchronized {
+          Option(appInfoStore.get(kyuubiUniqueKey)).foreach { case (_, appInfo) =>
+            appInfoStore.put(kyuubiUniqueKey, kubernetesInfo -> appInfo.copy(url = Some(appUrl)))
+          }
+        }
+    }.getOrElse(warn(s"Spark UI port not found in service ${svc.getMetadata.getName}"))
   }
 
   private def markApplicationTerminated(pod: Pod): Unit = synchronized {
@@ -350,6 +423,7 @@ object KubernetesApplicationOperation extends Logging {
   val SPARK_APP_ID_LABEL = "spark-app-selector"
   val KUBERNETES_SERVICE_HOST = "KUBERNETES_SERVICE_HOST"
   val KUBERNETES_SERVICE_PORT = "KUBERNETES_SERVICE_PORT"
+  val SPARK_UI_PORT_NAME = "spark-ui"
 
   def toLabel(tag: String): String = s"label: $LABEL_KYUUBI_UNIQUE_KEY=$tag"
 
@@ -415,4 +489,26 @@ object KubernetesApplicationOperation extends Logging {
         "mark the application state as UNKNOWN.")
       UNKNOWN
   }
+
+  /**
+   * Replaces all the {{SPARK_APP_ID}} occurrences with the Spark App Id,
+   * {{SPARK_DRIVER_SVC}} occurrences with the Spark Driver Service name,
+   * {{KUBERNETES_CONTEXT}} occurrences with the Kubernetes Context,
+   * {{KUBERNETES_NAMESPACE}} occurrences with the Kubernetes Namespace,
+   * and {{SPARK_UI_PORT}} occurrences with the Spark UI Port.
+   */
+  private[kyuubi] def buildSparkAppUrl(
+      sparkAppUrlPattern: String,
+      sparkAppId: String,
+      sparkDriverSvc: String,
+      kubernetesContext: String,
+      kubernetesNamespace: String,
+      sparkUiPort: Int): String = {
+    sparkAppUrlPattern
+      .replace("{{SPARK_APP_ID}}", sparkAppId)
+      .replace("{{SPARK_DRIVER_SVC}}", sparkDriverSvc)
+      .replace("{{KUBERNETES_CONTEXT}}", kubernetesContext)
+      .replace("{{KUBERNETES_NAMESPACE}}", kubernetesNamespace)
+      .replace("{{SPARK_UI_PORT}}", sparkUiPort.toString)
+  }
 }
diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/KubernetesApplicationOperationSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/KubernetesApplicationOperationSuite.scala
index 2ea1939d2..ab663a007 100644
--- a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/KubernetesApplicationOperationSuite.scala
+++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/KubernetesApplicationOperationSuite.scala
@@ -56,4 +56,44 @@ class KubernetesApplicationOperationSuite extends KyuubiFunSuite {
     kyuubiConf.unset(KyuubiConf.KUBERNETES_NAMESPACE_ALLOW_LIST.key)
     operation.checkKubernetesInfo(KubernetesInfo(None, Some("ns3")))
   }
+
+  test("build spark app url") {
+    val sparkAppUrlPattern1 = "http://{{SPARK_APP_ID}}.ingress.balabala"
+    val sparkAppUrlPattern2 =
+      "http://{{SPARK_DRIVER_SVC}}.{{KUBERNETES_NAMESPACE}}.svc:{{SPARK_UI_PORT}}"
+    val sparkAppUrlPattern3 =
+      "http://{{SPARK_DRIVER_SVC}}.{{KUBERNETES_NAMESPACE}}.svc" +
+        ".{{KUBERNETES_CONTEXT}}.k8s.io:{{SPARK_UI_PORT}}"
+
+    val sparkAppId = "spark-123"
+    val sparkDriverSvc = "spark-456-driver-svc"
+    val kubernetesContext = "1"
+    val kubernetesNamespace = "kyuubi"
+    val sparkUiPort = 4040
+
+    assert(KubernetesApplicationOperation.buildSparkAppUrl(
+      sparkAppUrlPattern1,
+      sparkAppId,
+      sparkDriverSvc,
+      kubernetesContext,
+      kubernetesNamespace,
+      sparkUiPort) === s"http://$sparkAppId.ingress.balabala")
+
+    assert(KubernetesApplicationOperation.buildSparkAppUrl(
+      sparkAppUrlPattern2,
+      sparkAppId,
+      sparkDriverSvc,
+      kubernetesContext,
+      kubernetesNamespace,
+      sparkUiPort) === s"http://$sparkDriverSvc.$kubernetesNamespace.svc:$sparkUiPort")
+
+    assert(KubernetesApplicationOperation.buildSparkAppUrl(
+      sparkAppUrlPattern3,
+      sparkAppId,
+      sparkDriverSvc,
+      kubernetesContext,
+      kubernetesNamespace,
+      sparkUiPort) ===
+      s"http://$sparkDriverSvc.$kubernetesNamespace.svc.$kubernetesContext.k8s.io:$sparkUiPort")
+  }
 }