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

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


The following commit(s) were added to refs/heads/master by this push:
     new d55c857  [ZEPPELIN-4748] Format Spark web ui url dynamically on 
Kubernetes
d55c857 is described below

commit d55c85741ef5833ed89e33128fd0dc42cace9d59
Author: Lee moon soo <leemoon...@gmail.com>
AuthorDate: Thu Apr 9 22:46:52 2020 -0700

    [ZEPPELIN-4748] Format Spark web ui url dynamically on Kubernetes
    
    ### What is this PR for?
    When Zeppelin is running on Kubernetes, SparkUI URL should be dynamically 
generated, while Kubernetes Service name for Spark interpreter Pod is generated 
on runtime. And Ingress controller or reverse-proxy route traffic to SparkUI.
    
    Problem is, depends on those Ingress or reverse proxy configuration, 
different SparkUI url format might be required.
    
    Currently, generated url format is hardcoded to 
"//<PORT>-<SERVICE_NAME>.<SERVICE_DOMAIN>". And letting user set 
'zeppelin.spark.uiWebUrl' with static value doesn't help at all while url is 
decided on runtime.
    
    This PR accept [jinja 
template](https://jinja.palletsprojects.com/en/2.11.x/) string from 
'zeppelin.spark.uiWebUrl' and bind 3 variables 'PORT', 'SERVICE_NAME', 
'SERVICE_DOMAIN'. Therefore any URL pattern required by Ingress/Reverse-proxy 
can be specified. Each variable has values
    
     * PORT - spark ui port
     * SERVICE_NAME - 
[Service](https://kubernetes.io/docs/concepts/services-networking/service/) 
name for Spark Interpreter Pod.
     * SERVICE_DOMAIN - value of SERVICE_DOMAIN env variable.
    
    For example, when spark UI is running on port '4040', Service name for 
Spark interpreter pod is 'spark-wcoyqq', SERVICE_DOMAIN is my.domain.io,
    
    ```
    https://port-{{PORT}}-{{SERVICE_NAME}}.{{SERVICE_DOMAIN}}
    ```
    value on 'zeppelin.spark.uiWebUrl' property will generate Spark UI link 
with address
    
    ```
    https://port-4040-spark-wcoyqq.mydomain.io
    ```
    
    ### What type of PR is it?
    Improvement
    
    ### What is the Jira issue?
    https://issues.apache.org/jira/browse/ZEPPELIN-4748
    
    ### Questions:
    * Does the licenses files need update? no
    * Is there breaking changes for older versions? no
    * Does this needs documentation? yes
    
    Author: Lee moon soo <leemoon...@gmail.com>
    
    Closes #3728 from Leemoonsoo/ZEPPELIN-4748 and squashes the following 
commits:
    
    0103c44c4 [Lee moon soo] update description of the property
    42a51b6d5 [Lee moon soo] set classloader for jinjava to prevent 
"jinjava.javax.el.ELException: Class 
com.hubspot.jinjava.el.ExtendedSyntaxBuilder not found"
    c931253d3 [Lee moon soo] template rendering zeppelin.spark.uiWebUrl 
property value
---
 docs/interpreter/spark.md                          |  6 +++-
 .../src/main/resources/interpreter-setting.json    |  2 +-
 .../launcher/K8sRemoteInterpreterProcess.java      | 30 ++++++++++++++--
 .../launcher/K8sRemoteInterpreterProcessTest.java  | 42 ++++++++++++++++++++++
 4 files changed, 76 insertions(+), 4 deletions(-)

diff --git a/docs/interpreter/spark.md b/docs/interpreter/spark.md
index 3c07e01..1f0aed7 100644
--- a/docs/interpreter/spark.md
+++ b/docs/interpreter/spark.md
@@ -184,7 +184,11 @@ You can also set other Spark properties which are not 
listed in the table. For a
   <tr>
   <td>zeppelin.spark.uiWebUrl</td>
     <td></td>
-    <td>Overrides Spark UI default URL. Value should be a full URL (ex: 
http://{hostName}/{uniquePath}</td>
+    <td>
+      Overrides Spark UI default URL. Value should be a full URL (ex: 
http://{hostName}/{uniquePath}.
+      In Kubernetes mode, value can be Jinja template string with 3 template 
variables 'PORT', 'SERVICE_NAME' and 'SERVICE_DOMAIN'.
+      (ex: http://{{PORT}}-{{SERVICE_NAME}}.{{SERVICE_DOMAIN}})
+     </td>
   </tr>
   <td>spark.webui.yarn.useProxy</td>
     <td>false</td>
diff --git a/spark/interpreter/src/main/resources/interpreter-setting.json 
b/spark/interpreter/src/main/resources/interpreter-setting.json
index f879431..5505f4d 100644
--- a/spark/interpreter/src/main/resources/interpreter-setting.json
+++ b/spark/interpreter/src/main/resources/interpreter-setting.json
@@ -109,7 +109,7 @@
         "envName": null,
         "propertyName": "zeppelin.spark.uiWebUrl",
         "defaultValue": "",
-        "description": "Override Spark UI default URL",
+        "description": "Override Spark UI default URL. In Kubernetes mode, 
value can be Jinja template string with 3 template variables 'PORT', 
'SERVICE_NAME' and 'SERVICE_DOMAIN'. (ex: 
http://{{PORT}}-{{SERVICE_NAME}}.{{SERVICE_DOMAIN}})",
         "type": "string"
       },
       "zeppelin.spark.ui.hidden": {
diff --git 
a/zeppelin-plugins/launcher/k8s-standard/src/main/java/org/apache/zeppelin/interpreter/launcher/K8sRemoteInterpreterProcess.java
 
b/zeppelin-plugins/launcher/k8s-standard/src/main/java/org/apache/zeppelin/interpreter/launcher/K8sRemoteInterpreterProcess.java
index 63b5a3e..959718d 100644
--- 
a/zeppelin-plugins/launcher/k8s-standard/src/main/java/org/apache/zeppelin/interpreter/launcher/K8sRemoteInterpreterProcess.java
+++ 
b/zeppelin-plugins/launcher/k8s-standard/src/main/java/org/apache/zeppelin/interpreter/launcher/K8sRemoteInterpreterProcess.java
@@ -1,6 +1,7 @@
 package org.apache.zeppelin.interpreter.launcher;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
@@ -9,6 +10,7 @@ import java.io.IOException;
 import java.util.*;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import com.hubspot.jinjava.Jinjava;
 import org.apache.commons.exec.ExecuteWatchdog;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcess;
@@ -273,9 +275,15 @@ public class K8sRemoteInterpreterProcess extends 
RemoteInterpreterProcess {
       // configure interpreter property "zeppelin.spark.uiWebUrl" if not 
defined, to enable spark ui through reverse proxy
       String webUrl = (String) properties.get("zeppelin.spark.uiWebUrl");
       if (webUrl == null || webUrl.trim().isEmpty()) {
-        properties.put("zeppelin.spark.uiWebUrl",
-            String.format("//%d-%s.%s", webUiPort, getPodName(), 
envs.get("SERVICE_DOMAIN")));
+        webUrl = "//{{PORT}}-{{SERVICE_NAME}}.{{SERVICE_DOMAIN}}";
       }
+      properties.put("zeppelin.spark.uiWebUrl",
+          sparkUiWebUrlFromTemplate(
+              webUrl,
+              webUiPort,
+              getPodName(),
+              envs.get("SERVICE_DOMAIN")
+          ));
     }
 
     k8sProperties.put("zeppelin.k8s.envs", envs);
@@ -286,6 +294,24 @@ public class K8sRemoteInterpreterProcess extends 
RemoteInterpreterProcess {
   }
 
   @VisibleForTesting
+  String sparkUiWebUrlFromTemplate(String templateString, int port, String 
serviceName, String serviceDomain) {
+    ImmutableMap<String, Object> binding = ImmutableMap.of(
+        "PORT", port,
+        "SERVICE_NAME", serviceName,
+        "SERVICE_DOMAIN", serviceDomain
+    );
+
+    ClassLoader oldCl = Thread.currentThread().getContextClassLoader();
+    try {
+      
Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
+      Jinjava jinja = new Jinjava();
+      return jinja.render(templateString, binding);
+    } finally {
+      Thread.currentThread().setContextClassLoader(oldCl);
+    }
+  }
+
+  @VisibleForTesting
   boolean isSpark() {
     return "spark".equalsIgnoreCase(interpreterGroupName);
   }
diff --git 
a/zeppelin-plugins/launcher/k8s-standard/src/test/java/org/apache/zeppelin/interpreter/launcher/K8sRemoteInterpreterProcessTest.java
 
b/zeppelin-plugins/launcher/k8s-standard/src/test/java/org/apache/zeppelin/interpreter/launcher/K8sRemoteInterpreterProcessTest.java
index d08e73a..9d6b634 100644
--- 
a/zeppelin-plugins/launcher/k8s-standard/src/test/java/org/apache/zeppelin/interpreter/launcher/K8sRemoteInterpreterProcessTest.java
+++ 
b/zeppelin-plugins/launcher/k8s-standard/src/test/java/org/apache/zeppelin/interpreter/launcher/K8sRemoteInterpreterProcessTest.java
@@ -193,4 +193,46 @@ public class K8sRemoteInterpreterProcessTest {
     assertTrue(sparkSubmitOptions.contains("spark.driver.port=" + 
intp.getSparkDriverPort()));
     assertTrue(sparkSubmitOptions.contains("spark.blockManager.port=" + 
intp.getSparkBlockmanagerPort()));
   }
+
+  @Test
+  public void testSparkUiWebUrlTemplate() {
+    // given
+    Kubectl kubectl = mock(Kubectl.class);
+    when(kubectl.getNamespace()).thenReturn("default");
+
+    Properties properties = new Properties();
+    HashMap<String, String> envs = new HashMap<String, String>();
+    envs.put("SERVICE_DOMAIN", "mydomain");
+
+    K8sRemoteInterpreterProcess intp = new K8sRemoteInterpreterProcess(
+        kubectl,
+        new File(".skip"),
+        "interpreter-container:1.0",
+        "shared_process",
+        "spark",
+        "myspark",
+        properties,
+        envs,
+        "zeppelin.server.hostname",
+        "12320",
+        false,
+        "spark-container:1.0",
+        10);
+
+    // when non template url
+    assertEquals("static.url",
+        intp.sparkUiWebUrlFromTemplate(
+            "static.url",
+            4040,
+            "zeppelin-server",
+            "my.domain.com"));
+
+    // when template url
+    assertEquals("//4040-zeppelin-server.my.domain.com",
+        intp.sparkUiWebUrlFromTemplate(
+            "//{{PORT}}-{{SERVICE_NAME}}.{{SERVICE_DOMAIN}}",
+            4040,
+            "zeppelin-server",
+            "my.domain.com"));
+  }
 }

Reply via email to