You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by mo...@apache.org on 2020/04/14 15:06:57 UTC

[zeppelin] branch master updated: [ZEPPELIN-4748] Format Spark web ui url dynamically on Kubernetes

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 <le...@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 <le...@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"));
+  }
 }