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

byronhsu 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 c237717  SUBMARINE-899. ServeModel API in submarine server
c237717 is described below

commit c237717e81088879bb8974719ffa3d27943a7e39
Author: ByronHsu <[email protected]>
AuthorDate: Sun Jul 4 15:08:42 2021 +0800

    SUBMARINE-899. ServeModel API in submarine server
    
    ### What is this PR for?
    For the model saved in model registry (you can view on mlflow UI), we offer 
an API to serve it in the cluster. Under the hood, we create resources (i.e. 
pod, service, deployment, ingressroute) for the `serve` resource.
    Follow the 
[example](https://github.com/apache/submarine/blob/65620913c8d99b774efad56939d1f3255c91c18c/dev-support/examples/nn-pytorch/readme.md)
 to try this new API.
    
    ### What type of PR is it?
    [Feature]
    
    ### Todos
    - [ ]  getServe API: get the status (e.g. ready) for the Serve resource.
    - [ ] error-handling (use mlflow api to check existence)
    - [ ]  deleteServeByID
        - [ ]  create ID for each serve → store in db
    - [ ]  test
    - [ ]  seperate serveManager from experimentManager
    - [ ]  update API doc
    - [ ] Serve UI
    
    ### What is the Jira issue?
    https://issues.apache.org/jira/browse/SUBMARINE-899
    
    ### How should this be tested?
    
    ### Screenshots (if appropriate)
    
    ### Questions:
    * Do the license files need updating? No
    * Are there breaking changes for older versions? No
    * Does this need new documentation? No
    
    Author: ByronHsu <[email protected]>
    
    Signed-off-by: byronhsu <[email protected]>
    
    Closes #641 from ByronHsu/serve and squashes the following commits:
    
    5f935a76 [ByronHsu] fix style
    65620913 [ByronHsu] add serve example
    e94608f0 [ByronHsu] refactor
    2bd97eb1 [ByronHsu] refactor
    776c0898 [ByronHsu] refactor
    de1c08af [ByronHsu] restapi
    1607cc7a [ByronHsu] k8ssubmitter create serve and delete serve
    a20dfef6 [ByronHsu] add more comments
    a62c6824 [ByronHsu] add readiness probe to ensure the container is up
    4404f500 [ByronHsu] remove unused files
    7347486c [ByronHsu] solve unconnection by middleware
    8af51d67 [ByronHsu] flask server works
---
 .github/workflows/deploy_docker_images.yml         |   5 +
 dev-support/docker-images/serve/Dockerfile         |  26 +++
 dev-support/docker-images/serve/build.sh           |  33 +++
 dev-support/examples/nn-pytorch/Dockerfile         |  23 ++
 dev-support/examples/nn-pytorch/build.sh           |  44 ++++
 dev-support/examples/nn-pytorch/model.py           |  35 ++++
 dev-support/examples/nn-pytorch/post.sh            |  39 ++++
 dev-support/examples/nn-pytorch/readme.md          |  65 ++++++
 dev-support/misc/flask/Dockerfile                  |  24 +++
 dev-support/misc/flask/build.sh                    |  33 +++
 dev-support/misc/flask/readme.md                   |   3 +
 dev-support/misc/flask/server.py                   |  28 +++
 dev-support/misc/serve/readme.md                   |   3 +
 dev-support/misc/serve/serve.yaml                  |  91 ++++++++
 helm-charts/submarine/templates/rbac.yaml          |   1 +
 .../org/apache/submarine/server/api/Submitter.java |  19 ++
 .../server/api/experiment/ServeRequest.java        |  84 ++++++++
 .../server/api/experiment/ServeResponse.java}      |  24 +--
 .../server/experiment/ExperimentManager.java       |  29 ++-
 .../submarine/server/rest/ExperimentRestApi.java   |  54 +++++
 .../server/submitter/k8s/K8sSubmitter.java         |  95 ++++++++-
 .../k8s/model/middlewares/Middlewares.java         |  22 +-
 .../k8s/model/middlewares/MiddlewaresSpec.java     |  32 ++-
 .../k8s/model/middlewares/StripPrefix.java         |  83 ++++++++
 .../submitter/k8s/parser/ServeSpecParser.java      | 232 +++++++++++++++++++++
 .../server/submitter/k8s/K8SJobSubmitterTest.java  |  10 +
 26 files changed, 1100 insertions(+), 37 deletions(-)

diff --git a/.github/workflows/deploy_docker_images.yml 
b/.github/workflows/deploy_docker_images.yml
index bcd4331..8a45b49 100644
--- a/.github/workflows/deploy_docker_images.yml
+++ b/.github/workflows/deploy_docker_images.yml
@@ -61,3 +61,8 @@ jobs:
         run: ./dev-support/docker-images/mlflow/build.sh
       - name: Push submarine-mlflow docker image
         run: docker push apache/submarine:mlflow-$SUBMARINE_VERSION
+
+      - name: Build submarine serve
+        run: ./dev-support/docker-images/serve/build.sh
+      - name: Push submarine-serve docker image
+        run: docker push apache/submarine:serve-$SUBMARINE_VERSION
diff --git a/dev-support/docker-images/serve/Dockerfile 
b/dev-support/docker-images/serve/Dockerfile
new file mode 100644
index 0000000..041c5e8
--- /dev/null
+++ b/dev-support/docker-images/serve/Dockerfile
@@ -0,0 +1,26 @@
+# 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.
+
+# 
https://github.com/ContinuumIO/docker-images/blob/master/miniconda3/debian/Dockerfile
+FROM continuumio/miniconda3
+
+# mlflow serve dependency
+RUN pip install mlflow boto3
+
+# some envs
+ENV MLFLOW_S3_ENDPOINT_URL="http://submarine-minio-service:9000";
+ENV AWS_ACCESS_KEY_ID="submarine_minio"
+ENV AWS_SECRET_ACCESS_KEY="submarine_minio"
+ENV MLFLOW_TRACKING_URI="http://submarine-mlflow-service:5000";
\ No newline at end of file
diff --git a/dev-support/docker-images/serve/build.sh 
b/dev-support/docker-images/serve/build.sh
new file mode 100755
index 0000000..42115c5
--- /dev/null
+++ b/dev-support/docker-images/serve/build.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+# 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.
+
+set -euxo pipefail
+
+SUBMARINE_VERSION=0.6.0-SNAPSHOT
+SUBMARINE_IMAGE_NAME="apache/submarine:serve-${SUBMARINE_VERSION}"
+
+if [ -L ${BASH_SOURCE-$0} ]; then
+  PWD=$(dirname $(readlink "${BASH_SOURCE-$0}"))
+else
+  PWD=$(dirname ${BASH_SOURCE-$0})
+fi
+export CURRENT_PATH=$(cd "${PWD}">/dev/null; pwd)
+export SUBMARINE_HOME=${CURRENT_PATH}/../../..
+
+# build image
+cd ${CURRENT_PATH}
+echo "Start building the ${SUBMARINE_IMAGE_NAME} docker image ..."
+docker build -t ${SUBMARINE_IMAGE_NAME} .
\ No newline at end of file
diff --git a/dev-support/examples/nn-pytorch/Dockerfile 
b/dev-support/examples/nn-pytorch/Dockerfile
new file mode 100644
index 0000000..437a7e0
--- /dev/null
+++ b/dev-support/examples/nn-pytorch/Dockerfile
@@ -0,0 +1,23 @@
+# 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.
+
+FROM python:3.7.9
+MAINTAINER Apache Software Foundation <[email protected]>
+
+ADD ./tmp/submarine-sdk /opt/
+RUN pip install "torch==1.5.0" "torchvision==0.6.0"
+RUN pip install /opt/pysubmarine
+
+ADD ./model.py /opt
\ No newline at end of file
diff --git a/dev-support/examples/nn-pytorch/build.sh 
b/dev-support/examples/nn-pytorch/build.sh
new file mode 100755
index 0000000..44b8292
--- /dev/null
+++ b/dev-support/examples/nn-pytorch/build.sh
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+# 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.
+
+set -euxo pipefail
+
+SUBMARINE_VERSION=0.6.0-SNAPSHOT
+SUBMARINE_IMAGE_NAME="nn-pytorch:${SUBMARINE_VERSION}"
+
+if [ -L ${BASH_SOURCE-$0} ]; then
+  PWD=$(dirname $(readlink "${BASH_SOURCE-$0}"))
+else
+  PWD=$(dirname ${BASH_SOURCE-$0})
+fi
+export CURRENT_PATH=$(cd "${PWD}">/dev/null; pwd)
+export SUBMARINE_HOME=${CURRENT_PATH}/../../..
+
+if [ -d "${CURRENT_PATH}/tmp" ] # if old tmp folder is still there, delete it.
+then
+  rm -rf "${CURRENT_PATH}/tmp"
+fi
+
+mkdir -p "${CURRENT_PATH}/tmp"
+cp -r "${SUBMARINE_HOME}/submarine-sdk" "${CURRENT_PATH}/tmp"
+
+# build image
+cd ${CURRENT_PATH}
+echo "Start building the ${SUBMARINE_IMAGE_NAME} docker image ..."
+docker build -t ${SUBMARINE_IMAGE_NAME} .
+
+# clean temp file
+rm -rf "${CURRENT_PATH}/tmp"
diff --git a/dev-support/examples/nn-pytorch/model.py 
b/dev-support/examples/nn-pytorch/model.py
new file mode 100644
index 0000000..732d85a
--- /dev/null
+++ b/dev-support/examples/nn-pytorch/model.py
@@ -0,0 +1,35 @@
+"""
+ 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.
+"""
+from submarine import ModelsClient
+import numpy as np
+import torch
+from submarine import ModelsClient
+
+
+class LinearNNModel(torch.nn.Module):
+    def __init__(self):
+        super(LinearNNModel, self).__init__()
+        self.linear = torch.nn.Linear(2, 1)  # One in and one out
+
+    def forward(self, x):
+        y_pred = self.linear(x)
+        return y_pred
+
+if __name__ == "__main__":
+    client = ModelsClient()
+    net = LinearNNModel()
+    client.log_model("simple-nn-model", net)
diff --git a/dev-support/examples/nn-pytorch/post.sh 
b/dev-support/examples/nn-pytorch/post.sh
new file mode 100755
index 0000000..1cdfa83
--- /dev/null
+++ b/dev-support/examples/nn-pytorch/post.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+# 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.
+
+curl -X POST -H "Content-Type: application/json" -d '
+{
+  "meta": {
+    "name": "nn-pytorch-example",
+    "namespace": "default",
+    "framework": "Tensorflow",
+    "cmd": "python /opt/model.py",
+    "envVars": {
+      "ENV_1": "ENV1"
+    }
+    
+  },
+  "environment": {
+    "image": "nn-pytorch:0.6.0-SNAPSHOT"
+  },
+  "spec": {
+    "Worker": {
+      "replicas": 1,
+      "resources": "cpu=1,memory=1024M"
+    }
+  }
+}
+' http://127.0.0.1:32080/api/v1/experiment
\ No newline at end of file
diff --git a/dev-support/examples/nn-pytorch/readme.md 
b/dev-support/examples/nn-pytorch/readme.md
new file mode 100644
index 0000000..f6e7172
--- /dev/null
+++ b/dev-support/examples/nn-pytorch/readme.md
@@ -0,0 +1,65 @@
+# Save_model Example
+
+## Usage
+This is an easy example of saving a pytorch linear model to model registry.
+
+## How to execute
+
+1. Build the docker image
+
+```bash
+./dev-support/examples/nn-pytorch/build.sh
+```
+
+2. Submit a post request
+
+```bash
+./dev-support/examples/nn-pytorch/post.sh
+```
+
+## Serve the model by Serve API
+
+1. Make sure the model is saved in the model registry (viewed on MLflow UI)
+2. Call serve API to create serve resource
+- Request
+  ```
+  curl -X POST -H "Content-Type: application/json" -d '
+  {
+    "modelName":"simple-nn-model",
+    "modelVersion":"1",
+    "namespace":"default"
+  }
+  ' http://127.0.0.1:32080/api/v1/experiment/serve
+  ```
+- Response
+  ```
+  {
+      "status": "OK",
+      "code": 200,
+      "success": true,
+      "message": null,
+      "result": {
+          "url": "/serve/simple-nn-model-1"
+      },
+      "attributes": {}
+  }
+  ```
+
+3. Send data to inference
+- Request
+  ```
+  curl -d '{"data":[[-1, -1]]}' -H 'Content-Type: application/json; 
format=pandas-split' -X POST 
http://127.0.0.1:32080/serve/simple-nn-model-1/invocations
+  ```
+- Response
+  ```
+  [{"0": -0.5663654804229736}]
+  ```
+4. Call serve API to delete serve resource
+- Request
+  ```
+  curl -X DELETE 
http://0.0.0.0:32080/api/v1/experiment/serve?modelName=simple-nn-model&modelVersion=1&namespace=default
+  ```
+- Response
+  ```
+  
{"status":"OK","code":200,"success":true,"message":null,"result":{"url":"/serve/simple-nn-model-1"},"attributes":{}}
+  ```
\ No newline at end of file
diff --git a/dev-support/misc/flask/Dockerfile 
b/dev-support/misc/flask/Dockerfile
new file mode 100644
index 0000000..0125182
--- /dev/null
+++ b/dev-support/misc/flask/Dockerfile
@@ -0,0 +1,24 @@
+# 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.
+
+FROM python:3.7
+MAINTAINER Apache Software Foundation <[email protected]>
+
+RUN pip install flask
+
+ENV FLASK_APP="server.py"
+ADD ./server.py /opt/
+WORKDIR /opt/
+CMD ["flask", "run", "-h", "0.0.0.0", "-p", "5000"]
\ No newline at end of file
diff --git a/dev-support/misc/flask/build.sh b/dev-support/misc/flask/build.sh
new file mode 100755
index 0000000..079cfc1
--- /dev/null
+++ b/dev-support/misc/flask/build.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+# 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.
+
+set -euxo pipefail
+
+SUBMARINE_VERSION=0.6.0-SNAPSHOT
+SUBMARINE_IMAGE_NAME="flask:${SUBMARINE_VERSION}"
+
+if [ -L ${BASH_SOURCE-$0} ]; then
+  PWD=$(dirname $(readlink "${BASH_SOURCE-$0}"))
+else
+  PWD=$(dirname ${BASH_SOURCE-$0})
+fi
+export CURRENT_PATH=$(cd "${PWD}">/dev/null; pwd)
+export SUBMARINE_HOME=${CURRENT_PATH}/../../..
+
+# build image
+cd ${CURRENT_PATH}
+echo "Start building the ${SUBMARINE_IMAGE_NAME} docker image ..."
+docker build -t ${SUBMARINE_IMAGE_NAME} .
\ No newline at end of file
diff --git a/dev-support/misc/flask/readme.md b/dev-support/misc/flask/readme.md
new file mode 100644
index 0000000..647d056
--- /dev/null
+++ b/dev-support/misc/flask/readme.md
@@ -0,0 +1,3 @@
+## Lightweight flask server image
+
+An lightweight flask server for debugging the connectivity in the cluster
\ No newline at end of file
diff --git a/dev-support/misc/flask/server.py b/dev-support/misc/flask/server.py
new file mode 100644
index 0000000..439337c
--- /dev/null
+++ b/dev-support/misc/flask/server.py
@@ -0,0 +1,28 @@
+"""
+ 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.
+"""
+
+from flask import Flask
+
+app = Flask(__name__)
+
[email protected]("/")
+def hello():
+  return "Hello, World!"
+
[email protected]("/invocations")
+def invocation():
+  return "Invocation"
\ No newline at end of file
diff --git a/dev-support/misc/serve/readme.md b/dev-support/misc/serve/readme.md
new file mode 100644
index 0000000..ff0bd84
--- /dev/null
+++ b/dev-support/misc/serve/readme.md
@@ -0,0 +1,3 @@
+## Serve YAML
+
+This is the yaml version of resource created by `submitter.createServe()`.
diff --git a/dev-support/misc/serve/serve.yaml 
b/dev-support/misc/serve/serve.yaml
new file mode 100644
index 0000000..0e4084c
--- /dev/null
+++ b/dev-support/misc/serve/serve.yaml
@@ -0,0 +1,91 @@
+#
+# 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.
+#
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: serve-example # name == ${model-registry}-{version}
+spec:
+  selector:
+    matchLabels:
+      app: serve-example-pod # 
+  template:
+    metadata:
+      labels:
+        app: serve-example-pod
+    spec:
+      containers:
+      - name: serve-example-container
+        image: apache/submarine:serve-0.6.0-SNAPSHOT
+        command:
+          - "mlflow"
+          - "models"
+          - "serve"
+          - "--model-uri"
+          - "models:/simple-nn-model/1"
+          - "--host"
+          - "0.0.0.0" # make it accessible from the outside
+        imagePullPolicy: IfNotPresent
+        ports:
+        - containerPort: 5000
+        readinessProbe: # make container ready until mlflow serving server is 
ready to receive request
+          httpGet:
+            path: /ping # from mlflow scoring_server
+            port: 5000
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: serve-example-service
+spec:
+  type: ClusterIP
+  selector:
+    app: serve-example-pod
+  ports:
+  - protocol: TCP
+    port: 5000 # port on service
+    targetPort: 5000 # port on container
+---
+apiVersion: traefik.containo.us/v1alpha1
+kind: IngressRoute
+metadata:
+  name: serve-example-ingressroute
+spec:
+  entryPoints:
+    - web
+  routes:
+  - kind: Rule
+    match: "PathPrefix(`/serve/mymodel`)"
+    middlewares:
+    - name: stripprefix
+    services:
+    - kind: Service
+      name: serve-example-service
+      port: 5000
+---
+# strip the prefix
+# e.g. Make a HTTP POST: localhost:32080/serve/mymodel/invocations
+#      The serve pod (with ingressroute `/serve/mymodel/`) receives path 
"/serve/mymodel/invocations"
+#      We should strip the prefix and make it become "/invocations"
+apiVersion: traefik.containo.us/v1alpha1
+kind: Middleware
+metadata:
+  name: stripprefix
+spec:
+  stripPrefix:
+    prefixes:
+      - /serve/mymodel
\ No newline at end of file
diff --git a/helm-charts/submarine/templates/rbac.yaml 
b/helm-charts/submarine/templates/rbac.yaml
index 4cefce9..ff77fdf 100644
--- a/helm-charts/submarine/templates/rbac.yaml
+++ b/helm-charts/submarine/templates/rbac.yaml
@@ -42,6 +42,7 @@ rules:
   - traefik.containo.us
   resources:
   - ingressroutes
+  - middlewares
   verbs:
   - get
   - list
diff --git 
a/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/Submitter.java
 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/Submitter.java
index 31f02a0..6185ba4 100644
--- 
a/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/Submitter.java
+++ 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/Submitter.java
@@ -25,6 +25,8 @@ import org.apache.submarine.server.api.experiment.Experiment;
 import org.apache.submarine.server.api.experiment.ExperimentLog;
 import org.apache.submarine.server.api.experiment.TensorboardInfo;
 import org.apache.submarine.server.api.experiment.MlflowInfo;
+import org.apache.submarine.server.api.experiment.ServeRequest;
+import org.apache.submarine.server.api.experiment.ServeResponse;
 import org.apache.submarine.server.api.notebook.Notebook;
 import org.apache.submarine.server.api.spec.ExperimentSpec;
 import org.apache.submarine.server.api.spec.NotebookSpec;
@@ -123,6 +125,23 @@ public interface Submitter {
   List<Notebook> listNotebook(String id) throws SubmarineRuntimeException;
 
   /**
+   * Create Serve with spec
+   * @param ServeRequest
+   * @return object
+   * @throws SubmarineRuntimeException running error
+   */
+  ServeResponse createServe(ServeRequest spec) throws 
SubmarineRuntimeException;
+
+
+  /**
+   * Delete Serve with spec
+   * @param ServeRequest
+   * @return object
+   * @throws SubmarineRuntimeException running error
+   */
+  ServeResponse deleteServe(ServeRequest spec) throws 
SubmarineRuntimeException;
+
+  /**
    * Get tensorboard meta data
    * @param
    * @return object
diff --git 
a/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/experiment/ServeRequest.java
 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/experiment/ServeRequest.java
new file mode 100644
index 0000000..857fa33
--- /dev/null
+++ 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/experiment/ServeRequest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.api.experiment;
+
+public class ServeRequest {
+  private String modelName;
+  private String modelVersion;
+  private String namespace;
+
+
+  public ServeRequest() {
+  }
+
+  public ServeRequest(String modelName, String modelVersion, String namespace) 
{
+    this.modelName = modelName;
+    this.modelVersion = modelVersion;
+    this.namespace = namespace;
+  }
+
+  public String getModelName() {
+    return this.modelName;
+  }
+
+  public void setModelName(String modelName) {
+    this.modelName = modelName;
+  }
+
+  public String getModelVersion() {
+    return this.modelVersion;
+  }
+
+  public void setModelVersion(String modelVersion) {
+    this.modelVersion = modelVersion;
+  }
+
+  public String getNamespace() {
+    return this.namespace;
+  }
+
+  public void setNamespace(String namespace) {
+    this.namespace = namespace;
+  }
+
+  public ServeRequest modelName(String modelName) {
+    setModelName(modelName);
+    return this;
+  }
+
+  public ServeRequest modelVersion(String modelVersion) {
+    setModelVersion(modelVersion);
+    return this;
+  }
+
+  public ServeRequest namespace(String namespace) {
+    setNamespace(namespace);
+    return this;
+  }
+
+  @Override
+  public String toString() {
+    return "{" +
+      " modelName='" + getModelName() + "'" +
+      ", modelVersion='" + getModelVersion() + "'" +
+      ", namespace='" + getNamespace() + "'" +
+      "}";
+  }
+}
diff --git 
a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/middlewares/MiddlewaresSpec.java
 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/experiment/ServeResponse.java
similarity index 62%
copy from 
submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/middlewares/MiddlewaresSpec.java
copy to 
submarine-server/server-api/src/main/java/org/apache/submarine/server/api/experiment/ServeResponse.java
index 4f435ac..f0ccc6c 100644
--- 
a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/middlewares/MiddlewaresSpec.java
+++ 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/experiment/ServeResponse.java
@@ -17,25 +17,21 @@
  * under the License.
  */
 
-package org.apache.submarine.server.submitter.k8s.model.middlewares;
+package org.apache.submarine.server.api.experiment;
 
-import com.google.gson.annotations.SerializedName;
+public class ServeResponse {
+  public String url;
 
-import java.util.Map;
-
-public class MiddlewaresSpec {
-
-  @SerializedName("replacePathRegex")
-  private Map<String, String> replacePathRegex;
-
-  public MiddlewaresSpec() {
+  public String getUrl() {
+    return this.url;
   }
 
-  public Map<String, String> getReplacePathRegex() {
-    return replacePathRegex;
+  public void setUrl(String url) {
+    this.url = url;
   }
 
-  public void setReplacePathRegex(Map<String, String> replacePathRegex) {
-    this.replacePathRegex = replacePathRegex;
+  public ServeResponse url(String url) {
+    setUrl(url);
+    return this;
   }
 }
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/experiment/ExperimentManager.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/experiment/ExperimentManager.java
index 1662306..6c62a30 100644
--- 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/experiment/ExperimentManager.java
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/experiment/ExperimentManager.java
@@ -40,6 +40,8 @@ import org.apache.submarine.server.api.Submitter;
 import org.apache.submarine.server.api.experiment.ExperimentLog;
 import org.apache.submarine.server.api.experiment.TensorboardInfo;
 import org.apache.submarine.server.api.experiment.MlflowInfo;
+import org.apache.submarine.server.api.experiment.ServeRequest;
+import org.apache.submarine.server.api.experiment.ServeResponse;
 import org.apache.submarine.server.api.spec.ExperimentSpec;
 import org.apache.submarine.server.experiment.database.ExperimentEntity;
 import org.apache.submarine.server.experiment.database.ExperimentService;
@@ -126,7 +128,6 @@ public class ExperimentManager {
     return experiment;
   }
 
-
   /**
    * Get experiment
    *
@@ -296,6 +297,32 @@ public class ExperimentManager {
     return submitter.getMlflowInfo();
   }
 
+  /**
+   * Create serve
+   *
+   * @param spec spec
+   * @return object
+   * @throws SubmarineRuntimeException the service error
+   */
+  public ServeResponse createServe(ServeRequest spec) throws 
SubmarineRuntimeException {
+    // TODO(byronhsu): use mlflow api to make sure the model exists. 
Otherwise, raise exception.
+    ServeResponse serve = submitter.createServe(spec);
+    return serve;
+  }
+
+  /**
+   * Delete serve
+   *
+   * @param spec spec
+   * @return object
+   * @throws SubmarineRuntimeException the service error
+   */
+  public ServeResponse deleteServe(ServeRequest spec) throws 
SubmarineRuntimeException {
+    ServeResponse serve = submitter.deleteServe(spec);
+    return serve;
+  }
+
+
   private void checkSpec(ExperimentSpec spec) throws SubmarineRuntimeException 
{
     if (spec == null) {
       throw new SubmarineRuntimeException(Status.OK.getStatusCode(), "Invalid 
experiment spec.");
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/ExperimentRestApi.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/ExperimentRestApi.java
index d914824..588486f 100644
--- 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/ExperimentRestApi.java
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/ExperimentRestApi.java
@@ -43,6 +43,8 @@ import 
org.apache.submarine.commons.utils.exception.SubmarineRuntimeException;
 import org.apache.submarine.server.api.experiment.Experiment;
 import org.apache.submarine.server.api.experiment.TensorboardInfo;
 import org.apache.submarine.server.api.experiment.MlflowInfo;
+import org.apache.submarine.server.api.experiment.ServeRequest;
+import org.apache.submarine.server.api.experiment.ServeResponse;
 import org.apache.submarine.server.experiment.ExperimentManager;
 import 
org.apache.submarine.server.experimenttemplate.ExperimentTemplateManager;
 import org.apache.submarine.server.api.experiment.ExperimentLog;
@@ -297,6 +299,58 @@ public class ExperimentRestApi {
     }
   }
 
+  /**
+   * Returns the contents of {@link Serve} that submitted by user.
+   *
+   * @param spec spec
+   * @return the contents of serve
+   */
+
+  @POST
+  @Path("/serve")
+  @Consumes({RestConstants.MEDIA_TYPE_YAML, MediaType.APPLICATION_JSON})
+  @Operation(summary = "Create an serve",
+      tags = {"serve"},
+      responses = {
+          @ApiResponse(description = "successful operation", content = 
@Content(
+              schema = @Schema(implementation = JsonResponse.class)))})
+  public Response createServe(ServeRequest spec) {
+    try {
+      ServeResponse serve = experimentManager.createServe(spec);
+      return new 
JsonResponse.Builder<ServeResponse>(Response.Status.OK).success(true)
+          .result(serve).build();
+    } catch (SubmarineRuntimeException e) {
+      return parseExperimentServiceException(e);
+    }
+  }
+
+  /**
+   * Delete {@link Serve} that submitted by user.
+   *
+   * @param spec spec
+   * @return the contents of serve
+   */
+
+  @DELETE
+  @Path("/serve")
+  @Operation(summary = "Delete a serve",
+      tags = {"serve"},
+      responses = {
+          @ApiResponse(description = "successful operation", content = 
@Content(
+              schema = @Schema(implementation = JsonResponse.class)))})
+  public Response deleteServe(@QueryParam("modelName") String modelName, 
+      @QueryParam("modelVersion") String modelVersion, 
@QueryParam("namespace") String namespace) {
+    try {
+      ServeRequest spec = new ServeRequest()
+          
.modelName(modelName).modelVersion(modelVersion).namespace(namespace);
+      ServeResponse serve = experimentManager.deleteServe(spec);
+      return new 
JsonResponse.Builder<ServeResponse>(Response.Status.OK).success(true)
+          .result(serve).build();
+    } catch (SubmarineRuntimeException e) {
+      return parseExperimentServiceException(e);
+    }
+  }
+
   private Response parseExperimentServiceException(SubmarineRuntimeException 
e) {
     return new JsonResponse.Builder<String>(e.getCode())
       .message(e.getMessage().equals("Conflict") ? "Duplicated experiment 
name" : e.getMessage()).build();
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 396c872..0e7accf 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
@@ -44,6 +44,7 @@ import io.kubernetes.client.models.V1PersistentVolumeClaim;
 import io.kubernetes.client.models.V1PersistentVolumeClaimVolumeSource;
 import io.kubernetes.client.models.V1Pod;
 import io.kubernetes.client.models.V1PodList;
+import io.kubernetes.client.models.V1Service;
 import io.kubernetes.client.models.V1Status;
 import io.kubernetes.client.util.ClientBuilder;
 import io.kubernetes.client.util.KubeConfig;
@@ -55,6 +56,8 @@ import org.apache.submarine.server.api.experiment.Experiment;
 import org.apache.submarine.server.api.experiment.ExperimentLog;
 import org.apache.submarine.server.api.experiment.TensorboardInfo;
 import org.apache.submarine.server.api.experiment.MlflowInfo;
+import org.apache.submarine.server.api.experiment.ServeRequest;
+import org.apache.submarine.server.api.experiment.ServeResponse;
 import org.apache.submarine.server.api.notebook.Notebook;
 import org.apache.submarine.server.api.spec.ExperimentMeta;
 import org.apache.submarine.server.api.spec.ExperimentSpec;
@@ -64,8 +67,10 @@ import 
org.apache.submarine.server.submitter.k8s.model.NotebookCR;
 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.ingressroute.SpecRoute;
+import org.apache.submarine.server.submitter.k8s.model.middlewares.Middlewares;
 import org.apache.submarine.server.submitter.k8s.parser.ExperimentSpecParser;
 import org.apache.submarine.server.submitter.k8s.parser.NotebookSpecParser;
+import org.apache.submarine.server.submitter.k8s.parser.ServeSpecParser;
 import org.apache.submarine.server.submitter.k8s.parser.VolumeSpecParser;
 import org.apache.submarine.server.submitter.k8s.util.MLJobConverter;
 import org.apache.submarine.server.submitter.k8s.util.NotebookUtils;
@@ -125,7 +130,7 @@ public class K8sSubmitter implements Submitter {
       appsV1Api = new AppsV1Api();
     }
 
-    // client.setDebugging(true);
+    client.setDebugging(true);
   }
 
   @Override
@@ -463,6 +468,72 @@ public class K8sSubmitter implements Submitter {
     return notebookList;
   }
 
+  @Override
+  public ServeResponse createServe(ServeRequest spec) 
+      throws SubmarineRuntimeException {
+    String modelName = spec.getModelName();
+    String modelVersion = spec.getModelVersion();
+    String namespace = spec.getNamespace();
+
+    ServeSpecParser parser = new ServeSpecParser(modelName, modelVersion, 
namespace);
+    V1Deployment deployment = parser.getDeployment();
+    V1Service svc = parser.getService();
+    IngressRoute ingressRoute = parser.getIngressRoute();
+    Middlewares middleware = parser.getMiddlewares();
+    ServeResponse serveInfo = new ServeResponse().url(parser.getRoutePath());
+
+    try {
+      appsV1Api.createNamespacedDeployment(namespace, deployment, "true", 
null, null);
+      coreApi.createNamespacedService(namespace, svc, "true", null, null);
+
+      api.createNamespacedCustomObject(
+            middleware.getGroup(), middleware.getVersion(),
+            middleware.getMetadata().getNamespace(),
+            middleware.getPlural(), middleware, "true");
+            
+      api.createNamespacedCustomObject(
+            ingressRoute.getGroup(), ingressRoute.getVersion(),
+            ingressRoute.getMetadata().getNamespace(),
+            ingressRoute.getPlural(), ingressRoute, "true");
+      return serveInfo;
+    } catch (ApiException e) {
+      throw new SubmarineRuntimeException(e.getCode(), e.getMessage());
+    }
+  }
+
+  @Override
+  public ServeResponse deleteServe(ServeRequest spec) 
+      throws SubmarineRuntimeException {
+    String modelName = spec.getModelName();
+    String modelVersion = spec.getModelVersion();
+    String namespace = spec.getNamespace();
+
+    ServeSpecParser parser = new ServeSpecParser(modelName, modelVersion, 
namespace);
+    IngressRoute ingressRoute = parser.getIngressRoute();
+    Middlewares middleware = parser.getMiddlewares();
+    ServeResponse serveInfo = new ServeResponse().url(parser.getRoutePath());
+
+    try {
+      appsV1Api.deleteNamespacedDeployment(parser.getGeneralName(), namespace, 
"true",
+          null, null, null, null, null);
+      coreApi.deleteNamespacedService(parser.getSvcName(), namespace, "true",
+          null, null, null, null, null);
+      api.deleteNamespacedCustomObject(
+          middleware.getGroup(), middleware.getVersion(),
+          middleware.getMetadata().getNamespace(), middleware.getPlural(), 
parser.getMiddlewareName(),
+          new 
V1DeleteOptionsBuilder().withApiVersion(middleware.getApiVersion()).build(),
+          null, null, null);
+      api.deleteNamespacedCustomObject(
+          ingressRoute.getGroup(), ingressRoute.getVersion(),
+          ingressRoute.getMetadata().getNamespace(), ingressRoute.getPlural(), 
parser.getRouteName(),
+          new 
V1DeleteOptionsBuilder().withApiVersion(ingressRoute.getApiVersion()).build(),
+          null, null, null);
+      return serveInfo;
+    } catch (ApiException e) {
+      throw new SubmarineRuntimeException(e.getCode(), e.getMessage());
+    }
+  }
+
   public void createPersistentVolume(String pvName, String hostPath, String 
storage) throws ApiException {
     V1PersistentVolume pv = VolumeSpecParser.parsePersistentVolume(pvName, 
hostPath, storage);
 
@@ -491,12 +562,15 @@ public class K8sSubmitter implements Submitter {
     } catch (JsonSyntaxException e) {
       if (e.getCause() instanceof IllegalStateException) {
         IllegalStateException ise = (IllegalStateException) e.getCause();
-        if (ise.getMessage() != null && ise.getMessage().contains("Expected a 
string but was BEGIN_OBJECT"))
+        if (ise.getMessage() != null && ise.getMessage().contains("Expected a 
string but was BEGIN_OBJECT")) {
           LOG.debug("Catching exception because of issue " +
-            "https://github.com/kubernetes-client/java/issues/86";, e);
-        else throw e;
+              "https://github.com/kubernetes-client/java/issues/86";, e);
+        } else {
+          throw e;
+        }
+      } else {
+        throw e;
       }
-      else throw e;
     }
   }
 
@@ -532,12 +606,15 @@ public class K8sSubmitter implements Submitter {
     } catch (JsonSyntaxException e) {
       if (e.getCause() instanceof IllegalStateException) {
         IllegalStateException ise = (IllegalStateException) e.getCause();
-        if (ise.getMessage() != null && ise.getMessage().contains("Expected a 
string but was BEGIN_OBJECT"))
+        if (ise.getMessage() != null && ise.getMessage().contains("Expected a 
string but was BEGIN_OBJECT")) {
           LOG.debug("Catching exception because of issue " +
-            "https://github.com/kubernetes-client/java/issues/86";, e);
-        else throw e;
+              "https://github.com/kubernetes-client/java/issues/86";, e);
+        } else {
+          throw e;
+        }
+      } else {
+        throw e;
       }
-      else throw e;
     }
   }
 
diff --git 
a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/middlewares/Middlewares.java
 
b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/middlewares/Middlewares.java
index 1736206..7b3f731 100644
--- 
a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/middlewares/Middlewares.java
+++ 
b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/middlewares/Middlewares.java
@@ -39,7 +39,7 @@ public class Middlewares {
   private String kind;
 
   @SerializedName("metadata")
-  private V1ObjectMeta metedata;
+  private V1ObjectMeta metadata;
 
   @SerializedName("spec")
   private MiddlewaresSpec spec;
@@ -75,12 +75,12 @@ public class Middlewares {
     this.kind = kind;
   }
 
-  public V1ObjectMeta getMetedata() {
-    return metedata;
+  public V1ObjectMeta getMetadata() {
+    return metadata;
   }
 
-  public void setMetedata(V1ObjectMeta metedata) {
-    this.metedata = metedata;
+  public void setMetadata(V1ObjectMeta metadata) {
+    this.metadata = metadata;
   }
 
   public MiddlewaresSpec getSpec() {
@@ -114,4 +114,16 @@ public class Middlewares {
   public void setPlural(String plural) {
     this.plural = plural;
   }
+
+
+  @Override
+  public String toString() {
+    return "{" +
+      " apiVersion='" + getApiVersion() + "'" +
+      ", kind='" + getKind() + "'" +
+      ", metadata='" + getMetadata() + "'" +
+      ", spec='" + getSpec() + "'" +
+      "}";
+  }
+
 }
diff --git 
a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/middlewares/MiddlewaresSpec.java
 
b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/middlewares/MiddlewaresSpec.java
index 4f435ac..657dd9a 100644
--- 
a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/middlewares/MiddlewaresSpec.java
+++ 
b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/middlewares/MiddlewaresSpec.java
@@ -21,21 +21,37 @@ package 
org.apache.submarine.server.submitter.k8s.model.middlewares;
 
 import com.google.gson.annotations.SerializedName;
 
-import java.util.Map;
-
 public class MiddlewaresSpec {
 
-  @SerializedName("replacePathRegex")
-  private Map<String, String> replacePathRegex;
+  @SerializedName("stripPrefix")
+  private StripPrefix stripPrefix;
+  
+  // other middlewares
 
   public MiddlewaresSpec() {
   }
 
-  public Map<String, String> getReplacePathRegex() {
-    return replacePathRegex;
+  public MiddlewaresSpec(StripPrefix stripPrefix) {
+    this.stripPrefix = stripPrefix;
+  }
+
+  public StripPrefix getStripPrefix() {
+    return this.stripPrefix;
+  }
+
+  public void setStripPrefix(StripPrefix stripPrefix) {
+    this.stripPrefix = stripPrefix;
+  }
+
+  public MiddlewaresSpec stripPrefix(StripPrefix stripPrefix) {
+    setStripPrefix(stripPrefix);
+    return this;
   }
 
-  public void setReplacePathRegex(Map<String, String> replacePathRegex) {
-    this.replacePathRegex = replacePathRegex;
+  @Override
+  public String toString() {
+    return "{" +
+      " stripPrefix='" + getStripPrefix() + "'" +
+      "}";
   }
 }
diff --git 
a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/middlewares/StripPrefix.java
 
b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/middlewares/StripPrefix.java
new file mode 100644
index 0000000..78eec45
--- /dev/null
+++ 
b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/middlewares/StripPrefix.java
@@ -0,0 +1,83 @@
+/*
+ * 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.middlewares;
+
+import java.util.List;
+
+import com.google.gson.annotations.SerializedName;
+
+
+public class StripPrefix {
+
+  @SerializedName("prefixes")
+  private List<String> prefixes;
+
+  @SerializedName("forceSlash")
+  private Boolean forceSlash;
+  
+
+  public StripPrefix() {
+    forceSlash = true; // default to true
+  }
+
+  public StripPrefix(List<String> prefixes, Boolean forceSlash) {
+    this.prefixes = prefixes;
+    this.forceSlash = forceSlash;
+  }
+
+  public List<String> getPrefixes() {
+    return this.prefixes;
+  }
+
+  public void setPrefixes(List<String> prefixes) {
+    this.prefixes = prefixes;
+  }
+
+  public Boolean isForceSlash() {
+    return this.forceSlash;
+  }
+
+  public Boolean getForceSlash() {
+    return this.forceSlash;
+  }
+
+  public void setForceSlash(Boolean forceSlash) {
+    this.forceSlash = forceSlash;
+  }
+
+  public StripPrefix prefixes(List<String> prefixes) {
+    setPrefixes(prefixes);
+    return this;
+  }
+
+  public StripPrefix forceSlash(Boolean forceSlash) {
+    setForceSlash(forceSlash);
+    return this;
+  }
+
+  @Override
+  public String toString() {
+    return "{" +
+      " prefixes='" + getPrefixes() + "'" +
+      ", forceSlash='" + isForceSlash() + "'" +
+      "}";
+  }
+  
+}
diff --git 
a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/parser/ServeSpecParser.java
 
b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/parser/ServeSpecParser.java
new file mode 100644
index 0000000..875aaff
--- /dev/null
+++ 
b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/parser/ServeSpecParser.java
@@ -0,0 +1,232 @@
+/*
+ * 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.parser;
+
+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.ingressroute.SpecRoute;
+import org.apache.submarine.server.submitter.k8s.model.middlewares.Middlewares;
+import 
org.apache.submarine.server.submitter.k8s.model.middlewares.MiddlewaresSpec;
+import org.apache.submarine.server.submitter.k8s.model.middlewares.StripPrefix;
+
+import io.kubernetes.client.custom.IntOrString;
+import io.kubernetes.client.models.V1Container;
+import io.kubernetes.client.models.V1ContainerPort;
+import io.kubernetes.client.models.V1Deployment;
+import io.kubernetes.client.models.V1DeploymentSpec;
+import io.kubernetes.client.models.V1HTTPGetAction;
+import io.kubernetes.client.models.V1LabelSelector;
+import io.kubernetes.client.models.V1ObjectMeta;
+import io.kubernetes.client.models.V1PodSpec;
+import io.kubernetes.client.models.V1PodTemplateSpec;
+import io.kubernetes.client.models.V1Probe;
+import io.kubernetes.client.models.V1Service;
+import io.kubernetes.client.models.V1ServicePort;
+import io.kubernetes.client.models.V1ServiceSpec;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+public class ServeSpecParser {
+
+  // names
+  String generalName;
+  String podName;
+  String containerName;
+  String routeName;
+  String svcName;
+  String middlewareName;
+
+  // path
+  String routePath;
+
+  // model_uri
+  String modelURI;
+
+  // cluster related
+  String namespace;
+  int PORT = 5000; // mlflow serve server is listening on 5000
+
+  // constructor
+  public ServeSpecParser(String modelName, String modelVersion, String 
namespace) {
+    // names assignment
+    generalName = modelName + "-" + modelVersion;
+    podName = generalName + "-pod";
+    containerName = generalName + "-container";
+    routeName = generalName + "-ingressroute";
+    svcName = generalName + "-service";
+    middlewareName = generalName + "-middleware";
+    // path assignment
+    routePath = String.format("/serve/%s", generalName);
+    // uri assignment
+    modelURI = String.format("models:/%s/%s", modelName, modelVersion);
+    // nameSpace
+    this.namespace = namespace;
+  }
+
+  public V1Deployment getDeployment() {
+    // Container related
+    // TODO(byronhsu) This should not be hard-coded.
+    final String serveImage = 
+        "apache/submarine:serve-0.6.0-SNAPSHOT"; 
+
+    ArrayList<String> cmds = new ArrayList<>(
+        Arrays.asList("mlflow", "models", "serve", 
+        "--model-uri", modelURI, "--host", "0.0.0.0")
+    );
+
+    V1Deployment deployment = new V1Deployment();
+
+    V1ObjectMeta deploymentMetedata = new V1ObjectMeta();
+    deploymentMetedata.setName(generalName);
+    deployment.setMetadata(deploymentMetedata);
+    
+    V1DeploymentSpec deploymentSpec = new V1DeploymentSpec();
+    deploymentSpec.setSelector(
+        new V1LabelSelector().matchLabels(Collections.singletonMap("app", 
podName)) // match the template
+    );
+
+    V1PodTemplateSpec deploymentTemplateSpec = new V1PodTemplateSpec();
+    deploymentTemplateSpec.setMetadata(
+        new V1ObjectMeta().labels(Collections.singletonMap("app", podName)) // 
bind to replicaset and service
+    );
+
+    V1PodSpec deploymentTemplatePodSpec = new V1PodSpec();
+
+    V1Container container = new V1Container();
+    container.setName(containerName);
+    container.setImage(serveImage);
+    container.setCommand(cmds);
+    container.setImagePullPolicy("IfNotPresent");
+    container.addPortsItem(new V1ContainerPort().containerPort(PORT));
+    container.setReadinessProbe(
+        new V1Probe().httpGet(new V1HTTPGetAction().path("/ping").port(new 
IntOrString(PORT)))
+    );
+
+    
+    deploymentTemplatePodSpec.addContainersItem(container);
+    deploymentTemplateSpec.setSpec(deploymentTemplatePodSpec);
+    deploymentSpec.setTemplate(deploymentTemplateSpec);
+    deployment.setSpec(deploymentSpec);
+
+    return deployment;
+  }
+  public V1Service getService() {
+    V1Service svc = new V1Service();
+    svc.metadata(new V1ObjectMeta().name(svcName));
+
+    V1ServiceSpec svcSpec = new V1ServiceSpec();
+    svcSpec.setSelector(Collections.singletonMap("app", podName)); // bind to 
pod
+    svcSpec.addPortsItem(new V1ServicePort().protocol("TCP").targetPort(
+        new IntOrString(PORT)).port(PORT));
+    svc.setSpec(svcSpec);
+    return svc;
+  }
+
+  public IngressRoute getIngressRoute() {
+    IngressRoute ingressRoute = new IngressRoute();
+    ingressRoute.setMetadata(
+        new V1ObjectMeta().name(routeName).namespace((namespace))
+    );
+
+    IngressRouteSpec ingressRouteSpec = new IngressRouteSpec();
+    ingressRouteSpec.setEntryPoints(new 
HashSet<>(Collections.singletonList("web")));
+    SpecRoute specRoute = new SpecRoute();
+    specRoute.setKind("Rule");
+    specRoute.setMatch(String.format("PathPrefix(`%s`)", routePath));
+
+    Map<String, Object> service = new HashMap<String, Object>() {{
+        put("kind", "Service");
+        put("name", svcName);
+        put("port", PORT);
+        put("namespace", namespace);
+      }};
+
+    specRoute.setServices(new HashSet<Map<String, Object>>() {{
+        add(service);
+      }});
+    
+    Map<String, String> middleware = new HashMap<String, String>() {{
+        put("name", middlewareName);
+      }};
+  
+    specRoute.setMiddlewares(new HashSet<Map<String, String>>() {{
+        add(middleware);
+      }});
+
+    ingressRouteSpec.setRoutes(new HashSet<SpecRoute>() {{
+        add(specRoute);
+      }});
+
+    ingressRoute.setSpec(ingressRouteSpec);
+    return ingressRoute;
+  }
+
+  public Middlewares getMiddlewares() {
+    Middlewares middleware = new Middlewares();
+    middleware.setMetadata(new 
V1ObjectMeta().name(middlewareName).namespace(namespace));
+    
+    MiddlewaresSpec middlewareSpec = new MiddlewaresSpec().stripPrefix(
+        new StripPrefix().prefixes(Arrays.asList(routePath))
+    );
+    middleware.setSpec(middlewareSpec);
+    return middleware;
+  }
+
+
+  public String getGeneralName() {
+    return this.generalName;
+  }
+
+  public String getPodName() {
+    return this.podName;
+  }
+
+  public String getContainerName() {
+    return this.containerName;
+  }
+  public String getRouteName() {
+    return this.routeName;
+  }
+
+  public String getSvcName() {
+    return this.svcName;
+  }
+
+  public String getMiddlewareName() {
+    return this.middlewareName;
+  }
+
+  public String getRoutePath() {
+    return this.routePath;
+  }
+
+  public String getModelURI() {
+    return this.modelURI;
+  }
+
+  public String getNamespace() {
+    return this.namespace;
+  }
+}
diff --git 
a/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/K8SJobSubmitterTest.java
 
b/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/K8SJobSubmitterTest.java
index af7a841..ca8d0d8 100644
--- 
a/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/K8SJobSubmitterTest.java
+++ 
b/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/K8SJobSubmitterTest.java
@@ -27,9 +27,11 @@ import 
org.apache.submarine.commons.utils.exception.SubmarineRuntimeException;
 import org.apache.submarine.server.api.experiment.Experiment;
 import org.apache.submarine.server.api.experiment.TensorboardInfo;
 import org.apache.submarine.server.api.experiment.MlflowInfo;
+import org.apache.submarine.server.api.experiment.ServeRequest;
 import org.apache.submarine.server.api.spec.ExperimentSpec;
 import org.junit.Assert;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -59,6 +61,14 @@ public class K8SJobSubmitterTest extends SpecBuilder {
     submitter.initialize(null);
   }
 
+  @Ignore // TODO(byronhsu): make sure saving a model before create serve
+  @Test
+  public void tmpTest() {
+    ServeRequest request = new ServeRequest()
+          .modelName("simple-nn-model").modelVersion("1").namespace("default");
+    submitter.createServe(request);
+  }
+
   @Test
   public void testRunPyTorchJobPerRequest() throws URISyntaxException, 
IOException,
       SubmarineRuntimeException {

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to