You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@submarine.apache.org by ji...@apache.org on 2020/09/18 14:32:14 UTC

[submarine] branch master updated: SUBMARINE-618. Create/Delete IngressRoute with Notebook CR

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 3d19524  SUBMARINE-618. Create/Delete IngressRoute with Notebook CR
3d19524 is described below

commit 3d1952442aadb4d7ba74636424cc2c557a3c84e7
Author: Ryan Lo <lo...@gmail.com>
AuthorDate: Thu Sep 17 01:18:52 2020 +0800

    SUBMARINE-618. Create/Delete IngressRoute with Notebook CR
    
    ### What is this PR for?
    Traefik uses CRD (IngressRoute) to retrieve its routing configuration.
    In order to route the traffic to jupyter notebooks, we should add routing rules by creating a IngressRoute witch pair with notebook's service.
    
    ### What type of PR is it?
    [Improvement]
    
    ### Todos
    * [ ] - Task
    
    ### What is the Jira issue?
    [SUBMARINE-618](https://issues.apache.org/jira/projects/SUBMARINE/issues/SUBMARINE-618)
    
    ### How should this be tested?
    [Travis CI](https://travis-ci.org/github/lowc1012/submarine/builds/727777761)
    
    ### Screenshots (if appropriate)
    ![img1](https://user-images.githubusercontent.com/52355146/93370999-382bfd80-f884-11ea-88c1-216012475cad.png)
    <img width="613" alt="img2" src="https://user-images.githubusercontent.com/52355146/93371062-50038180-f884-11ea-99ff-e90f426e8941.png">
    <img width="612" alt="img3" src="https://user-images.githubusercontent.com/52355146/93371068-53970880-f884-11ea-927d-7aeb86e4591b.png">
    
    ### Questions:
    * Does the licenses files need update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    Author: Ryan Lo <lo...@gmail.com>
    
    Closes #404 from lowc1012/SUBMARINE-618 and squashes the following commits:
    
    8081319 [Ryan Lo] SUBMARINE-618. Create/Delete IngressRoute with Notebook CR
---
 helm-charts/submarine/templates/rbac.yaml          |  13 +++
 .../manifests/submarine-cluster/rbac.yaml          |   4 +
 .../server/submitter/k8s/K8sSubmitter.java         |  71 ++++++++++++-
 .../k8s/model/ingressroute/IngressRoute.java       | 114 +++++++++++++++++++++
 .../k8s/model/ingressroute/IngressRouteSpec.java   |  54 ++++++++++
 .../k8s/model/ingressroute/SpecRoute.java          |  65 ++++++++++++
 .../workbench/notebook/notebook.component.html     |   2 +-
 7 files changed, 321 insertions(+), 2 deletions(-)

diff --git a/helm-charts/submarine/templates/rbac.yaml b/helm-charts/submarine/templates/rbac.yaml
index c108122..36015a8 100644
--- a/helm-charts/submarine/templates/rbac.yaml
+++ b/helm-charts/submarine/templates/rbac.yaml
@@ -38,6 +38,19 @@ rules:
   - deletecollection
   - patch
   - update
+- apiGroups:
+  - traefik.containo.us
+  resources:
+  - ingressroutes
+  verbs:
+  - get
+  - list
+  - watch
+  - create
+  - delete
+  - deletecollection
+  - patch
+  - update
 
 ---
 kind: ClusterRoleBinding
diff --git a/submarine-cloud/manifests/submarine-cluster/rbac.yaml b/submarine-cloud/manifests/submarine-cluster/rbac.yaml
index 6687a81..62931d4 100644
--- a/submarine-cloud/manifests/submarine-cluster/rbac.yaml
+++ b/submarine-cloud/manifests/submarine-cluster/rbac.yaml
@@ -35,6 +35,10 @@ items:
           - pytorchjobs
           - notebooks
         verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
+      - apiGroups: ["traefik.containo.us"]
+        resources:
+          - ingressroutes
+        verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
   - apiVersion: rbac.authorization.k8s.io/v1beta1
     kind: ClusterRoleBinding
     metadata:
diff --git a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/K8sSubmitter.java b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/K8sSubmitter.java
index cdc33f0..555e8bb 100644
--- a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/K8sSubmitter.java
+++ b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/K8sSubmitter.java
@@ -23,6 +23,10 @@ import java.io.FileReader;
 import java.io.IOException;
 import java.text.SimpleDateFormat;
 import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
 
 import com.google.gson.Gson;
 import com.google.gson.JsonSyntaxException;
@@ -33,6 +37,7 @@ import io.kubernetes.client.JSON;
 import io.kubernetes.client.apis.CoreV1Api;
 import io.kubernetes.client.apis.CustomObjectsApi;
 import io.kubernetes.client.models.V1DeleteOptionsBuilder;
+import io.kubernetes.client.models.V1ObjectMeta;
 import io.kubernetes.client.models.V1Pod;
 import io.kubernetes.client.models.V1PodList;
 import io.kubernetes.client.models.V1Status;
@@ -48,7 +53,10 @@ import org.apache.submarine.server.api.notebook.Notebook;
 import org.apache.submarine.server.api.spec.ExperimentMeta;
 import org.apache.submarine.server.api.spec.ExperimentSpec;
 import org.apache.submarine.server.api.spec.NotebookSpec;
+import org.apache.submarine.server.submitter.k8s.model.ingressroute.IngressRoute;
+import org.apache.submarine.server.submitter.k8s.model.ingressroute.IngressRouteSpec;
 import org.apache.submarine.server.submitter.k8s.model.NotebookCR;
+import org.apache.submarine.server.submitter.k8s.model.ingressroute.SpecRoute;
 import org.apache.submarine.server.submitter.k8s.parser.NotebookSpecParser;
 import org.apache.submarine.server.submitter.k8s.util.MLJobConverter;
 import org.apache.submarine.server.submitter.k8s.model.MLJob;
@@ -239,10 +247,15 @@ public class K8sSubmitter implements Submitter {
   public Notebook createNotebook(NotebookSpec spec) throws SubmarineRuntimeException {
     Notebook notebook;
     try {
+      // create notebook custom resource
       NotebookCR notebookCR = NotebookSpecParser.parseNotebook(spec);
       Object object = api.createNamespacedCustomObject(notebookCR.getGroup(), notebookCR.getVersion(),
               notebookCR.getMetadata().getNamespace(), notebookCR.getPlural(), notebookCR, "true");
       notebook = parseResponseObject(object);
+
+      // create Traefik custom resource
+      createIngressRoute(notebookCR.getMetadata().getNamespace(), notebookCR.getMetadata().getName());
+
     } catch (JsonSyntaxException e) {
       LOG.error("K8s submitter: parse response object failed by " + e.getMessage(), e);
       throw new SubmarineRuntimeException(500, "K8s Submitter parse upstream response failed.");
@@ -279,6 +292,7 @@ public class K8sSubmitter implements Submitter {
               new V1DeleteOptionsBuilder().withApiVersion(notebookCR.getApiVersion()).build(),
               null, null, null);
       notebook = parseResponseObject(object);
+      deleteIngressRoute(notebookCR.getMetadata().getNamespace(), notebookCR.getMetadata().getName());
     } catch (ApiException e) {
       throw new SubmarineRuntimeException(e.getCode(), e.getMessage());
     }
@@ -297,7 +311,7 @@ public class K8sSubmitter implements Submitter {
       notebook.setName(notebookCR.getMetadata().getName());
       // notebook url
       notebook.setUrl("/notebook/" + notebookCR.getMetadata().getNamespace() + "/" +
-              notebookCR.getMetadata().getName());
+              notebookCR.getMetadata().getName() + "/");
       DateTime createdTime = notebookCR.getMetadata().getCreationTimestamp();
       if (createdTime != null) {
         notebook.setCreatedTime(createdTime.toString());
@@ -329,6 +343,61 @@ public class K8sSubmitter implements Submitter {
     }
   }
 
+  private void createIngressRoute(String namespace, String name) {
+    try {
+      IngressRoute ingressRoute = new IngressRoute();
+      V1ObjectMeta meta = new V1ObjectMeta();
+      meta.setName(name);
+      meta.setNamespace(namespace);
+      ingressRoute.setMetadata(meta);
+      ingressRoute.setSpec(parseIngressRouteSpec(meta.getNamespace(), meta.getName()));
+      api.createNamespacedCustomObject(
+              ingressRoute.getGroup(), ingressRoute.getVersion(),
+              ingressRoute.getMetadata().getNamespace(),
+              ingressRoute.getPlural(), ingressRoute, "true");
+    } catch (ApiException e) {
+      LOG.error("K8s submitter: Create Traefik custom resource object failed by " + e.getMessage(), e);
+      throw new SubmarineRuntimeException(e.getCode(), e.getMessage());
+    } catch (JsonSyntaxException e) {
+      LOG.error("K8s submitter: parse response object failed by " + e.getMessage(), e);
+      throw new SubmarineRuntimeException(500, "K8s Submitter parse upstream response failed.");
+    }
+  }
+
+  private void deleteIngressRoute(String namespace, String name) {
+    try {
+      api.deleteNamespacedCustomObject(
+              IngressRoute.CRD_INGRESSROUTE_GROUP_V1, IngressRoute.CRD_INGRESSROUTE_VERSION_V1,
+              namespace, IngressRoute.CRD_INGRESSROUTE_PLURAL_V1, name,
+              new V1DeleteOptionsBuilder().withApiVersion(IngressRoute.CRD_APIVERSION_V1).build(),
+              null, null, null);
+    } catch (ApiException e) {
+      LOG.error("K8s submitter: Delete Traefik custom resource object failed by " + e.getMessage(), e);
+      throw new SubmarineRuntimeException(e.getCode(), e.getMessage());
+    }
+  }
+
+  private IngressRouteSpec parseIngressRouteSpec(String namespace, String name) {
+    IngressRouteSpec spec = new IngressRouteSpec();
+    Set<String> entryPoints = new HashSet<>();
+    entryPoints.add("web");
+    spec.setEntryPoints(entryPoints);
+
+    SpecRoute route = new SpecRoute();
+    route.setKind("Rule");
+    route.setMatch("PathPrefix(`/notebook/" + namespace + "/" + name + "/`)");
+    Set<Map<String, Object>> serviceMap = new HashSet<>();
+    Map<String, Object> service = new HashMap<>();
+    service.put("name", name);
+    service.put("port", 80);
+    serviceMap.add(service);
+    route.setServices(serviceMap);
+    Set<SpecRoute> routes = new HashSet<>();
+    routes.add(route);
+    spec.setRoutes(routes);
+    return spec;
+  }
+
   private enum ParseOp {
     PARSE_OP_RESULT,
     PARSE_OP_DELETE
diff --git a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/ingressroute/IngressRoute.java b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/ingressroute/IngressRoute.java
new file mode 100644
index 0000000..72c5fc6
--- /dev/null
+++ b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/ingressroute/IngressRoute.java
@@ -0,0 +1,114 @@
+/*
+ * 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.submarine.server.submitter.k8s.model.ingressroute;
+
+import com.google.gson.annotations.SerializedName;
+import io.kubernetes.client.models.V1ObjectMeta;
+
+public class IngressRoute {
+  public static final String CRD_INGRESSROUTE_GROUP_V1 = "traefik.containo.us";
+  public static final String CRD_INGRESSROUTE_VERSION_V1 = "v1alpha1";
+  public static final String CRD_APIVERSION_V1 = CRD_INGRESSROUTE_GROUP_V1 +
+          "/" + CRD_INGRESSROUTE_VERSION_V1;
+  public static final String CRD_INGRESSROUTE_KIND_V1 = "IngressRoute";
+  public static final String CRD_INGRESSROUTE_PLURAL_V1 = "ingressroutes";
+
+  @SerializedName("apiVersion")
+  private String apiVersion;
+
+  @SerializedName("kind")
+  private String kind;
+
+  @SerializedName("metadata")
+  private V1ObjectMeta metadata;
+
+  private transient String group;
+
+  private transient String version;
+
+  private transient String plural;
+
+  @SerializedName("spec")
+  private IngressRouteSpec spec;
+
+  public IngressRoute() {
+    setApiVersion(CRD_APIVERSION_V1);
+    setKind(CRD_INGRESSROUTE_KIND_V1);
+    setPlural(CRD_INGRESSROUTE_PLURAL_V1);
+    setGroup(CRD_INGRESSROUTE_GROUP_V1);
+    setVersion(CRD_INGRESSROUTE_VERSION_V1);
+  }
+
+  public String getApiVersion() {
+    return apiVersion;
+  }
+
+  public void setApiVersion(String apiVersion) {
+    this.apiVersion = apiVersion;
+  }
+
+  public String getKind() {
+    return kind;
+  }
+
+  public void setKind(String kind) {
+    this.kind = kind;
+  }
+
+  public V1ObjectMeta getMetadata() {
+    return metadata;
+  }
+
+  public void setMetadata(V1ObjectMeta metadata) {
+    this.metadata = metadata;
+  }
+
+  public String getPlural() {
+    return plural;
+  }
+
+  public void setPlural(String plural) {
+    this.plural = plural;
+  }
+
+  public String getGroup() {
+    return group;
+  }
+
+  public void setGroup(String group) {
+    this.group = group;
+  }
+
+  public String getVersion() {
+    return version;
+  }
+
+  public void setVersion(String version) {
+    this.version = version;
+  }
+
+  public IngressRouteSpec getSpec() {
+    return spec;
+  }
+
+  public void setSpec(IngressRouteSpec spec) {
+    this.spec = spec;
+  }
+}
diff --git a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/ingressroute/IngressRouteSpec.java b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/ingressroute/IngressRouteSpec.java
new file mode 100644
index 0000000..b29a918
--- /dev/null
+++ b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/ingressroute/IngressRouteSpec.java
@@ -0,0 +1,54 @@
+/*
+ * 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.submarine.server.submitter.k8s.model.ingressroute;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.Set;
+
+public class IngressRouteSpec {
+
+  public IngressRouteSpec() {
+
+  }
+
+  @SerializedName("entryPoints")
+  private Set<String> entryPoints;
+
+  @SerializedName("routes")
+  private Set<SpecRoute> routes;
+
+  public Set<String> getEntryPoints() {
+    return entryPoints;
+  }
+
+  public void setEntryPoints(Set<String> entryPoints) {
+    this.entryPoints = entryPoints;
+  }
+
+  public Set<SpecRoute> getRoutes() {
+    return routes;
+  }
+
+  public void setRoutes(Set<SpecRoute> routes) {
+    this.routes = routes;
+  }
+
+}
diff --git a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/ingressroute/SpecRoute.java b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/ingressroute/SpecRoute.java
new file mode 100644
index 0000000..d6208e7
--- /dev/null
+++ b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/ingressroute/SpecRoute.java
@@ -0,0 +1,65 @@
+/*
+ * 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.submarine.server.submitter.k8s.model.ingressroute;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.Map;
+import java.util.Set;
+
+public class SpecRoute {
+
+  public SpecRoute() {
+
+  }
+
+  @SerializedName("match")
+  private String match;
+
+  @SerializedName("kind")
+  private String kind;
+
+  @SerializedName("services")
+  private Set<Map<String, Object>> services;
+
+  public String getMatch() {
+    return match;
+  }
+
+  public void setMatch(String match) {
+    this.match = match;
+  }
+
+  public String getKind() {
+    return kind;
+  }
+
+  public void setKind(String kind) {
+    this.kind = kind;
+  }
+
+  public Set<Map<String, Object>> getServices() {
+    return services;
+  }
+
+  public void setServices(Set<Map<String, Object>> services) {
+    this.services = services;
+  }
+}
diff --git a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/notebook/notebook.component.html b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/notebook/notebook.component.html
index e1c4f7e..795e286 100644
--- a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/notebook/notebook.component.html
+++ b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/notebook/notebook.component.html
@@ -78,7 +78,7 @@
         <tbody>
           <tr *ngFor="let data of basicTable.data; let i = index">
             <td>
-              <a>{{ data.name }}</a>
+              <a href="{{ data.url }}" target="_blank">{{ data.name }}</a>
             </td>
             <td>{{ data.spec.environment.name }}</td>
             <td>{{ data.spec.environment.dockerImage }}</td>


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@submarine.apache.org
For additional commands, e-mail: dev-help@submarine.apache.org