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

liuxun 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 d43c5cd  SUBMARINE-560. Build an API to manage notebook instances with 
Swagger
d43c5cd is described below

commit d43c5cd6244082f1ff2d90e6cca48ccb37d1214f
Author: Ryan Lo <lowc1...@gmail.com>
AuthorDate: Fri Jul 31 17:12:01 2020 +0800

    SUBMARINE-560. Build an API to manage notebook instances with Swagger
    
    ### What is this PR for?
    User can create his or her notebook instances in specified namespace.
    
    ### What type of PR is it?
    [Feature]
    
    ### Todos
    Will open another PR to finish following todos.
    1.  Add test for Notebook Rest API
    2. User can list/delete notebook instances via REST API
    
    ### What is the Jira issue?
    
[SUBMARINE-560](https://issues.apache.org/jira/projects/SUBMARINE/issues/SUBMARINE-560)
    
    ### How should this be tested?
    [Travis 
CI](https://travis-ci.org/github/lowc1012/submarine/builds/711003012)
    
    ### Screenshots (if appropriate)
    <img width="697" alt="image1" 
src="https://user-images.githubusercontent.com/52355146/88690339-248ad300-d12e-11ea-9cb8-9c9e7f4a5b1c.png";>
    
    <img width="569" alt="image2" 
src="https://user-images.githubusercontent.com/52355146/88690409-39676680-d12e-11ea-8102-b3d92dd99c80.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 <lowc1...@gmail.com>
    
    Closes #353 from lowc1012/SUBMARINE-560 and squashes the following commits:
    
    faf5026 [Ryan Lo] SUBMARINE-560. resolve conflict & change JUPYTER_IMAGE
    785bc64 [Ryan Lo] SUBMARINE-560. fix bug & add "start-notebook.sh" in 
images for jupyter container
    ca77284 [Ryan Lo] SUBMARINE-560. Update K8sSubmitter.java
    ae08107 [Ryan Lo] SUBMARINE-560. WIP. Build an REST API to manage notebook 
instances
---
 dev-support/docker-images/jupyter/Dockerfile       |  53 ++++--
 dev-support/docker-images/jupyter/build.sh         |   6 +-
 .../docker-images/jupyter/start-notebook.sh        |  45 +++++
 .../server/api/{experiment => }/Submitter.java     |  30 +++-
 .../submarine/server/api/notebook/Notebook.java    | 120 +++++++++++++
 .../submarine/server/api/notebook/NotebookId.java  |  69 ++++++++
 .../submarine/server/api/spec/NotebookMeta.java    |  61 +++++++
 .../submarine/server/api/spec/NotebookPodSpec.java | 119 +++++++++++++
 .../submarine/server/api/spec/NotebookSpec.java}   |  62 ++++---
 .../apache/submarine/server/SubmitterManager.java  |   2 +-
 .../server/experiment/ExperimentManager.java       |   2 +-
 .../NotebookIdDeserializer.java}                   |  36 ++--
 .../NotebookIdSerializer.java}                     |  35 ++--
 .../submarine/server/notebook/NotebookManager.java | 139 +++++++++++++++
 .../submarine/server/response/JsonResponse.java    |   5 +
 .../submarine/server/rest/NotebookRestApi.java     | 172 ++++++++++++++++++
 .../submarine/server/rest/RestConstants.java       |  12 +-
 .../server/submitter/k8s/K8sSubmitter.java         |  62 ++++++-
 .../server/submitter/k8s/model/NotebookCR.java     | 115 ++++++++++++
 .../server/submitter/k8s/model/NotebookCRSpec.java |  92 ++++++++++
 .../submitter/k8s/parser/NotebookSpecParser.java   | 194 +++++++++++++++++++++
 .../submitter/k8s/ExperimentSpecParserTest.java    |  35 ++--
 .../server/submitter/k8s/K8SJobSubmitterTest.java  |   4 +-
 .../server/submitter/k8s/MLJobConverterTest.java   |   4 +-
 .../submitter/k8s/NotebookSpecParserTest.java      |  76 ++++++++
 .../server/submitter/k8s/SpecBuilder.java          |  10 +-
 .../src/test/resources/notebook_req.json           |  15 ++
 27 files changed, 1457 insertions(+), 118 deletions(-)

diff --git a/dev-support/docker-images/jupyter/Dockerfile 
b/dev-support/docker-images/jupyter/Dockerfile
index 110ed43..f513a6d 100644
--- a/dev-support/docker-images/jupyter/Dockerfile
+++ b/dev-support/docker-images/jupyter/Dockerfile
@@ -13,9 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-ARG BASE_IMAGE=tensorflow/tensorflow:2.1.0-py3-jupyter
-
-FROM $BASE_IMAGE
+FROM ubuntu:18.04
 
 ARG NB_USER="jovyan"
 ARG NB_UID="1000"
@@ -28,8 +26,8 @@ ENV NB_UID $NB_UID
 ENV NB_GID $NB_GID
 ENV NB_PREFIX $NB_PREFIX
 ENV NB_PORT $NB_PORT
-
-ENV PATH=$HOME/.local/bin:$PATH
+ENV CONDA_DIR=/opt/conda
+ENV PATH=$CONDA_DIR/bin:$PATH
 ENV HOME=/home/$NB_USER
 
 RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -yq 
--no-install-recommends \
@@ -43,10 +41,7 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get 
install -yq --no-in
     sudo \
     locales \
     fonts-liberation \
-    run-one \
-    python3-pip \
-    python3-dev \
-    python3-setuptools && \
+    run-one && \
     apt-get clean && rm -rf /var/lib/apt/lists/*
 
 RUN echo "$LOG_TAG Set locale" && \
@@ -70,12 +65,40 @@ ENV TINI_VERSION v0.19.0
 ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini 
/tini
 RUN mv /tini /usr/local/bin/tini && chmod +x /usr/local/bin/tini
 
-# Install python package
-RUN pip uninstall -y enum34
-RUN pip3 --no-cache-dir install jupyterlab
+# Install conda
+ARG PYTHON_VERSION=default
+ENV MINICONDA_VERSION=4.8.3 \
+    MINICONDA_MD5=751786b92c00b1aeae3f017b781018df \
+    CONDA_VERSION=4.8.3
+
+WORKDIR /tmp
+RUN wget --quiet 
https://repo.continuum.io/miniconda/Miniconda3-py37_${MINICONDA_VERSION}-Linux-x86_64.sh
 && \
+    echo "${MINICONDA_MD5} 
*Miniconda3-py37_${MINICONDA_VERSION}-Linux-x86_64.sh" | md5sum -c - && \
+    /bin/bash Miniconda3-py37_${MINICONDA_VERSION}-Linux-x86_64.sh -f -b -p 
$CONDA_DIR && \
+    rm Miniconda3-py37_${MINICONDA_VERSION}-Linux-x86_64.sh && \
+    echo "conda ${CONDA_VERSION}" >> $CONDA_DIR/conda-meta/pinned && \
+    conda config --system --prepend channels conda-forge && \
+    conda config --system --set auto_update_conda false && \
+    conda config --system --set show_channel_urls true && \
+    conda config --system --set channel_priority strict && \
+    if [ ! $PYTHON_VERSION = 'default' ]; then conda install --yes 
python=$PYTHON_VERSION; fi && \
+    conda list python | grep '^python ' | tr -s ' ' | cut -d '.' -f 1,2 | sed 
's/$/.*/' >> $CONDA_DIR/conda-meta/pinned && \
+    conda install --quiet --yes conda && \
+    conda install --quiet --yes pip && \
+    conda clean --all -f -y && \
+    rm -rf /home/$NB_USER/.cache/yarn
+
+# Install jupyter
+RUN conda install --quiet --yes notebook=6.0.3 && \
+    conda clean --all -f -y
+
+USER root
+RUN mkdir -p $CONDA_DIR && \
+    chown -R ${NB_USER}:users $CONDA_DIR
 
-# Configure container startup
+USER $NB_USER
 EXPOSE $NB_PORT
-USER $NB_UID
 ENTRYPOINT ["tini", "-g", "--"]
-CMD ["sh","-c", "jupyter notebook --notebook-dir=/home/${NB_USER} --ip=0.0.0.0 
--no-browser --allow-root --port=${NB_PORT} --NotebookApp.token='' 
--NotebookApp.password='' --NotebookApp.allow_origin='*' 
--NotebookApp.base_url=${NB_PREFIX}"]
+WORKDIR ${HOME}
+CMD ["start-notebook.sh"]
+COPY start-notebook.sh /usr/local/bin
diff --git a/dev-support/docker-images/jupyter/build.sh 
b/dev-support/docker-images/jupyter/build.sh
index 200f0eb..977a6cc 100755
--- a/dev-support/docker-images/jupyter/build.sh
+++ b/dev-support/docker-images/jupyter/build.sh
@@ -16,7 +16,7 @@
 
 set -euxo pipefail
 
-TF_JUPYTER_IMAGE="apache/submarine:tf2.1.0-jupyter"
+JUPYTER_IMAGE="apache/submarine:jupyter-notebook-0.5.0-SNAPSHOT"
 
 if [ -L ${BASH_SOURCE-$0} ]; then
   PWD=$(dirname $(readlink "${BASH_SOURCE-$0}"))
@@ -27,6 +27,6 @@ export CURRENT_PATH=$(cd "${PWD}">/dev/null; pwd)
 SUBMARINE_HOME=${CURRENT_PATH}/../../..
 
 # build image
-echo "Start building the ${TF_JUPYTER_IMAGE} docker image ..."
+echo "Start building the ${JUPYTER_IMAGE} docker image ..."
 cd ${CURRENT_PATH}
-docker build -t ${TF_JUPYTER_IMAGE} .
+docker build -t ${JUPYTER_IMAGE} .
diff --git a/dev-support/docker-images/jupyter/start-notebook.sh 
b/dev-support/docker-images/jupyter/start-notebook.sh
new file mode 100755
index 0000000..e39def5
--- /dev/null
+++ b/dev-support/docker-images/jupyter/start-notebook.sh
@@ -0,0 +1,45 @@
+#!/bin/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.
+#
+# description: Start and stop daemon script for.
+#
+
+set -euo pipefail
+
+if [[ -n "${ENVIRONMENT_COMMAND:-}" ]]; then
+  /bin/bash -c "${ENVIRONMENT_COMMAND}"
+fi
+
+NOTEBOOK_ARGS="--ip=0.0.0.0 --no-browser --allow-root --NotebookApp.token='' 
--NotebookApp.password='' --NotebookApp.allow_origin='*'"
+NB_USER="${NB_USER:-"jovyan"}"
+NB_PREFIX="${NB_PREFIX:-"/"}"
+NB_PORT="${NB_PORT:-8888}"
+
+if [[ -n "${NB_USER}" ]]; then
+  NOTEBOOK_ARGS="--notebook-dir=/home/${NB_USER} ${NOTEBOOK_ARGS}"
+fi
+
+if [[ -n "${NB_PORT}" ]]; then
+  NOTEBOOK_ARGS="--port=${NB_PORT} ${NOTEBOOK_ARGS}"
+fi
+
+if [[ -n "${NB_PREFIX}" ]]; then
+  NOTEBOOK_ARGS="--NotebookApp.base_url=${NB_PREFIX} ${NOTEBOOK_ARGS}"
+fi
+
+/bin/bash -c "jupyter notebook ${NOTEBOOK_ARGS}"
diff --git 
a/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/experiment/Submitter.java
 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/Submitter.java
similarity index 73%
rename from 
submarine-server/server-api/src/main/java/org/apache/submarine/server/api/experiment/Submitter.java
rename to 
submarine-server/server-api/src/main/java/org/apache/submarine/server/api/Submitter.java
index 401fb01..8c7ea49 100644
--- 
a/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/experiment/Submitter.java
+++ 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/Submitter.java
@@ -17,11 +17,15 @@
  * under the License.
  */
 
-package org.apache.submarine.server.api.experiment;
+package org.apache.submarine.server.api;
 
 import org.apache.submarine.commons.utils.SubmarineConfiguration;
 import org.apache.submarine.commons.utils.exception.SubmarineRuntimeException;
+import org.apache.submarine.server.api.experiment.Experiment;
+import org.apache.submarine.server.api.experiment.ExperimentLog;
+import org.apache.submarine.server.api.notebook.Notebook;
 import org.apache.submarine.server.api.spec.ExperimentSpec;
+import org.apache.submarine.server.api.spec.NotebookSpec;
 
 /**
  * The submitter should implement this interface.
@@ -81,4 +85,28 @@ public interface Submitter {
    * @throws SubmarineRuntimeException running error
    */
   ExperimentLog getExperimentLogName(ExperimentSpec spec, String id) throws 
SubmarineRuntimeException;
+
+  /**
+   * Create a notebook with spec
+   * @param spec notebook spec
+   * @return object
+   * @throws SubmarineRuntimeException running error
+   */
+  Notebook createNotebook(NotebookSpec spec) throws SubmarineRuntimeException;
+
+  /**
+   * Find a notebook with spec
+   * @param spec spec
+   * @return object
+   * @throws SubmarineRuntimeException running error
+   */
+  Notebook findNotebook(NotebookSpec spec) throws SubmarineRuntimeException;
+
+  /**
+   * Delete a notebook with spec
+   * @param spec spec
+   * @return object
+   * @throws SubmarineRuntimeException running error
+   */
+  Notebook deleteNotebook(NotebookSpec spec) throws SubmarineRuntimeException;
 }
diff --git 
a/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/notebook/Notebook.java
 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/notebook/Notebook.java
new file mode 100644
index 0000000..b71aded
--- /dev/null
+++ 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/notebook/Notebook.java
@@ -0,0 +1,120 @@
+/*
+ * 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.notebook;
+
+import org.apache.submarine.server.api.spec.NotebookSpec;
+
+/**
+ * The notebook instance in submarine
+ */
+public class Notebook {
+  private NotebookId notebookId;
+  private String name;
+  private String uid;
+  private String url;
+  private String status;
+  private String createdTime;
+  private String deletedTime;
+  private NotebookSpec spec;
+
+  public NotebookId getNotebookId() {
+    return notebookId;
+  }
+
+  public void setNotebookId(NotebookId notebookId) {
+    this.notebookId = notebookId;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public String getUid() {
+    return uid;
+  }
+
+  public void setUid(String uid) {
+    this.uid = uid;
+  }
+
+  public String getUrl() {
+    return url;
+  }
+
+  public void setUrl(String url) {
+    this.url = url;
+  }
+
+  public String getStatus() {
+    return status;
+  }
+
+  public void setStatus(String status) {
+    this.status = status;
+  }
+
+  public String getCreatedTime() {
+    return createdTime;
+  }
+
+  public void setCreatedTime(String createdTime) {
+    this.createdTime = createdTime;
+  }
+
+  public String getDeletedTime() {
+    return deletedTime;
+  }
+
+  public void setDeletedTime(String deletedTime) {
+    this.deletedTime = deletedTime;
+  }
+
+  public NotebookSpec getSpec() {
+    return spec;
+  }
+
+  public void setSpec(NotebookSpec spec) {
+    this.spec = spec;
+  }
+
+  public enum Status {
+    STATUS_CREATED("Created"),
+    STATUS_DELETED("Deleted");
+
+    private String value;
+    Status(String value) {
+      this.value = value;
+    }
+
+    public String getValue() {
+      return value;
+    }
+
+    @Override
+    public String toString() {
+      return value;
+    }
+  }
+
+}
diff --git 
a/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/notebook/NotebookId.java
 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/notebook/NotebookId.java
new file mode 100644
index 0000000..34f0a10
--- /dev/null
+++ 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/notebook/NotebookId.java
@@ -0,0 +1,69 @@
+/*
+ * 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.notebook;
+
+import org.apache.submarine.commons.utils.AbstractUniqueIdGenerator;
+
+/**
+ * The unique id for notebook. Formatter:
+ * notebook_${server_timestamp}_${counter} Such as:
+ * notebook_1577627710_0001
+ */
+public class NotebookId extends AbstractUniqueIdGenerator<NotebookId> {
+  private static final String NOTEBOOK_ID_PREFIX = "notebook_";
+
+  /**
+   * Get the object of NotebookId.
+   * @param notebookId string
+   * @return object
+   */
+  public static NotebookId fromString(String notebookId) {
+    if (notebookId == null) {
+      return null;
+    }
+    String[] components = notebookId.split("\\_");
+    if (components.length != 3) {
+      return null;
+    }
+    return NotebookId.newInstance(Long.parseLong(components[1]), 
Integer.parseInt(components[2]));
+  }
+
+  /**
+   * Get the object of NotebookId.
+   * @param serverTimestamp timestamp
+   * @param id count
+   * @return object
+   */
+  public static NotebookId newInstance(long serverTimestamp, int id) {
+    NotebookId notebookId = new NotebookId();
+    notebookId.setServerTimestamp(serverTimestamp);
+    notebookId.setId(id);
+    return notebookId;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder(64);
+    sb.append(NOTEBOOK_ID_PREFIX).append(getServerTimestamp()).append("_");
+    format(sb, getId());
+    return sb.toString();
+  }
+
+}
diff --git 
a/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/spec/NotebookMeta.java
 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/spec/NotebookMeta.java
new file mode 100644
index 0000000..b8d314c
--- /dev/null
+++ 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/spec/NotebookMeta.java
@@ -0,0 +1,61 @@
+/*
+ * 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.spec;
+
+public class NotebookMeta {
+  private String name;
+  private String namespace;
+
+  public NotebookMeta() {
+
+  }
+
+  /**
+   * Get the notebook name which is unique within a namespace.
+   * @return notebook name
+   */
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * Name must be unique within a namespace. Is required when creating 
notebook.
+   * @param name notebook name
+   */
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  /**
+   * Get the namespace which defines the isolated space for each notebook.
+   * @return namespace
+   */
+  public String getNamespace() {
+    return namespace;
+  }
+
+  /**
+   * Namespace defines the space within each name must be unique.
+   * @param namespace namespace
+   */
+  public void setNamespace(String namespace) {
+    this.namespace = namespace;
+  }
+}
diff --git 
a/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/spec/NotebookPodSpec.java
 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/spec/NotebookPodSpec.java
new file mode 100644
index 0000000..64e78db
--- /dev/null
+++ 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/spec/NotebookPodSpec.java
@@ -0,0 +1,119 @@
+/*
+ * 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.spec;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class NotebookPodSpec {
+
+  private Map<String, String> envVars;
+  private String resources;
+
+  // should ignored in JSON Serialization
+  private transient Map<String, String> resourceMap;
+
+  public NotebookPodSpec() {
+
+  }
+
+  /**
+   * Get envVars
+   * @return envVars
+   */
+  public Map<String, String> getEnvVars() {
+    return envVars;
+  }
+
+  /**
+   * Set envVars
+   * @param envVars environment variables for container
+   */
+  public void setEnvVars(Map<String, String> envVars) {
+    this.envVars = envVars;
+  }
+
+  /**
+   * Get the resources for container.
+   * Resource type
+   * cpu: vCPU/Core
+   * memory: E/Ei, P/Pi, T/Ti, G/Gi, M/Mi, K/Ki
+   * nvidia.com/gpu: GPUs (not possible to request a fraction of a GPU)
+   * such as: cpu=1,memory=2Gi,nvidia.com/gpu=1
+   * @return resources resources for container
+   */
+  public String getResources() {
+    return resources;
+  }
+
+  /**
+   * Set the resource for container
+   * Resource type
+   * cpu: vCPU/Core
+   * memory: E/Ei, P/Pi, T/Ti, G/Gi, M/Mi, K/Ki
+   * nvidia.com/gpu: GPUs (not possible to request a fraction of a GPU)
+   * such as: cpu=1,memory=2Gi,nvidia.com/gpu=1
+   * @param resources resources for container
+   */
+  public void setResources(String resources) {
+    this.resources = resources;
+    parseResources();
+  }
+
+  /**
+   * Parse resources
+   */
+  public void parseResources() {
+    if (resources != null) {
+      resourceMap = new HashMap<>();
+      for (String item : resources.split(",")) {
+        String[] r = item.split("=");
+        if (r.length == 2) {
+          resourceMap.put(r[0], r[1]);
+        }
+      }
+    }
+  }
+
+  /**
+   * Get the cpu reserved by the notebook server
+   * @return String or null
+   */
+  public String getCpu() {
+    return resourceMap.get("cpu");
+  }
+
+  /**
+   * Get the memory reserved by the notebook server
+   * @return String or null
+   */
+  public String getMemory() {
+    return resourceMap.get("memory");
+  }
+
+  /**
+   * Get the gpu reserved by the notebook server
+   * @return String or null
+   */
+  public String getGpu() {
+    return resourceMap.get("nvidia.com/gpu");
+  }
+
+}
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/RestConstants.java
 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/spec/NotebookSpec.java
similarity index 50%
copy from 
submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/RestConstants.java
copy to 
submarine-server/server-api/src/main/java/org/apache/submarine/server/api/spec/NotebookSpec.java
index f7be478..3a5a5f6 100644
--- 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/RestConstants.java
+++ 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/spec/NotebookSpec.java
@@ -17,30 +17,40 @@
  * under the License.
  */
 
-package org.apache.submarine.server.rest;
-
-public class RestConstants {
-  public static final String V1 = "v1";
-  public static final String EXPERIMENT = "experiment";
-  public static final String ID = "id";
-  public static final String PING = "ping";
-  public static final String MEDIA_TYPE_YAML = "application/yaml";
-  public static final String CHARSET_UTF8 = "charset=utf-8";
-
-  public static final String METASTORE = "metastore";
-
-  public static final String CLUSTER = "cluster";
-  public static final String ADDRESS = "address";
-
-  public static final String NODES = "nodes";
-  public static final String NODE = "node";
-
-  public static final String LOGS = "logs";
-  
-  /**
-   * Environment
-   */
-  public static final String ENVIRONMENT = "environment";
-  
-  public static final String ENVIRONMENT_ID = "id";
+package org.apache.submarine.server.api.spec;
+
+public class NotebookSpec {
+
+  private NotebookMeta meta;
+  private EnvironmentSpec environment;
+  private NotebookPodSpec spec;
+
+  public NotebookSpec() {
+
+  }
+
+  public NotebookMeta getMeta() {
+    return meta;
+  }
+
+  public void setMeta(NotebookMeta meta) {
+    this.meta = meta;
+  }
+
+  public EnvironmentSpec getEnvironment() {
+    return environment;
+  }
+
+  public void setEnvironment(EnvironmentSpec environmentSpec) {
+    this.environment = environmentSpec;
+  }
+
+  public NotebookPodSpec getSpec() {
+    return spec;
+  }
+
+  public void setSpec(NotebookPodSpec podSpec) {
+    this.spec = podSpec;
+  }
+
 }
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/SubmitterManager.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/SubmitterManager.java
index 7748b3e..f2577bf 100644
--- 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/SubmitterManager.java
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/SubmitterManager.java
@@ -21,7 +21,7 @@ package org.apache.submarine.server;
 
 import org.apache.submarine.commons.utils.SubmarineConfVars;
 import org.apache.submarine.commons.utils.SubmarineConfiguration;
-import org.apache.submarine.server.api.experiment.Submitter;
+import org.apache.submarine.server.api.Submitter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
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 93b7947..346f77c 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
@@ -33,7 +33,7 @@ import org.apache.submarine.server.SubmarineServer;
 import org.apache.submarine.server.SubmitterManager;
 import org.apache.submarine.server.api.experiment.Experiment;
 import org.apache.submarine.server.api.experiment.ExperimentId;
-import org.apache.submarine.server.api.experiment.Submitter;
+import org.apache.submarine.server.api.Submitter;
 import org.apache.submarine.server.api.experiment.ExperimentLog;
 import org.apache.submarine.server.api.spec.ExperimentSpec;
 import org.slf4j.Logger;
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/RestConstants.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/gson/NotebookIdDeserializer.java
similarity index 50%
copy from 
submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/RestConstants.java
copy to 
submarine-server/server-core/src/main/java/org/apache/submarine/server/gson/NotebookIdDeserializer.java
index f7be478..df3a8c9 100644
--- 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/RestConstants.java
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/gson/NotebookIdDeserializer.java
@@ -17,30 +17,20 @@
  * under the License.
  */
 
-package org.apache.submarine.server.rest;
+package org.apache.submarine.server.gson;
 
-public class RestConstants {
-  public static final String V1 = "v1";
-  public static final String EXPERIMENT = "experiment";
-  public static final String ID = "id";
-  public static final String PING = "ping";
-  public static final String MEDIA_TYPE_YAML = "application/yaml";
-  public static final String CHARSET_UTF8 = "charset=utf-8";
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import org.apache.submarine.server.api.notebook.NotebookId;
 
-  public static final String METASTORE = "metastore";
+import java.lang.reflect.Type;
 
-  public static final String CLUSTER = "cluster";
-  public static final String ADDRESS = "address";
-
-  public static final String NODES = "nodes";
-  public static final String NODE = "node";
-
-  public static final String LOGS = "logs";
-  
-  /**
-   * Environment
-   */
-  public static final String ENVIRONMENT = "environment";
-  
-  public static final String ENVIRONMENT_ID = "id";
+public class NotebookIdDeserializer implements JsonDeserializer<NotebookId> {
+  @Override
+  public NotebookId deserialize(JsonElement json, Type typeOfT, 
JsonDeserializationContext context)
+          throws JsonParseException {
+    return NotebookId.fromString(json.getAsJsonPrimitive().getAsString());
+  }
 }
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/RestConstants.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/gson/NotebookIdSerializer.java
similarity index 50%
copy from 
submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/RestConstants.java
copy to 
submarine-server/server-core/src/main/java/org/apache/submarine/server/gson/NotebookIdSerializer.java
index f7be478..f5baeb6 100644
--- 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/RestConstants.java
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/gson/NotebookIdSerializer.java
@@ -17,30 +17,19 @@
  * under the License.
  */
 
-package org.apache.submarine.server.rest;
+package org.apache.submarine.server.gson;
 
-public class RestConstants {
-  public static final String V1 = "v1";
-  public static final String EXPERIMENT = "experiment";
-  public static final String ID = "id";
-  public static final String PING = "ping";
-  public static final String MEDIA_TYPE_YAML = "application/yaml";
-  public static final String CHARSET_UTF8 = "charset=utf-8";
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import org.apache.submarine.server.api.notebook.NotebookId;
 
-  public static final String METASTORE = "metastore";
+import java.lang.reflect.Type;
 
-  public static final String CLUSTER = "cluster";
-  public static final String ADDRESS = "address";
-
-  public static final String NODES = "nodes";
-  public static final String NODE = "node";
-
-  public static final String LOGS = "logs";
-  
-  /**
-   * Environment
-   */
-  public static final String ENVIRONMENT = "environment";
-  
-  public static final String ENVIRONMENT_ID = "id";
+public class NotebookIdSerializer implements JsonSerializer<NotebookId> {
+  @Override
+  public JsonElement serialize(NotebookId src, Type typeOfSrc, 
JsonSerializationContext context) {
+    return new JsonPrimitive(src.toString());
+  }
 }
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/notebook/NotebookManager.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/notebook/NotebookManager.java
new file mode 100644
index 0000000..5547c23
--- /dev/null
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/notebook/NotebookManager.java
@@ -0,0 +1,139 @@
+/*
+ * 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.notebook;
+
+import org.apache.submarine.commons.utils.exception.SubmarineRuntimeException;
+import org.apache.submarine.server.SubmarineServer;
+import org.apache.submarine.server.SubmitterManager;
+import org.apache.submarine.server.api.Submitter;
+import org.apache.submarine.server.api.notebook.Notebook;
+import org.apache.submarine.server.api.notebook.NotebookId;
+import org.apache.submarine.server.api.spec.NotebookSpec;
+
+import javax.ws.rs.core.Response;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class NotebookManager {
+
+  private static volatile NotebookManager manager;
+
+  private final Submitter submitter;
+
+  private NotebookManager(Submitter submitter) {
+    this.submitter = submitter;
+  }
+
+  private final AtomicInteger notebookCounter = new AtomicInteger(0);
+
+  /**
+   * Used to cache the specs by the notebook id.
+   *  key: the string of notebook id
+   *  value: Notebook object
+   */
+  private final ConcurrentMap<String, Notebook> cachedNotebookMap = new 
ConcurrentHashMap<>();
+
+  /**
+   * Get the singleton instance
+   * @return object
+   */
+  public static NotebookManager getInstance() {
+    if (manager == null) {
+      synchronized (NotebookManager.class) {
+        if (manager == null) {
+          manager = new NotebookManager(SubmitterManager.loadSubmitter());
+        }
+      }
+    }
+    return manager;
+  }
+
+  /**
+   * Create a notebook instance
+   * @param spec NotebookSpec
+   * @return object
+   * @throws SubmarineRuntimeException the service error
+   */
+  public Notebook createNotebook(NotebookSpec spec) throws 
SubmarineRuntimeException {
+    checkNotebookSpec(spec);
+    Notebook notebook = submitter.createNotebook(spec);
+    notebook.setNotebookId(generateNotebookId());
+    notebook.setSpec(spec);
+    cachedNotebookMap.putIfAbsent(notebook.getNotebookId().toString(), 
notebook);
+    return notebook;
+  }
+
+  /**
+   * List notebook instances
+   * @param status status, if null will return all notebooks
+   * @return list
+   * @throws SubmarineRuntimeException the service error
+   */
+  public List<Notebook> listNotebooksByStatus(String status) throws 
SubmarineRuntimeException {
+    //TODO(ryan): implement the method
+    return null;
+  }
+
+  /**
+   * Get a notebook instance
+   * @param id notebook id
+   * @return object
+   * @throws SubmarineRuntimeException the service error
+   */
+  public Notebook getNotebook(String id) throws SubmarineRuntimeException {
+    //TODO(ryan): implement the method
+    return null;
+  }
+
+  /**
+   * Delete the notebook instance
+   * @param id notebook id
+   * @return object
+   * @throws SubmarineRuntimeException the service error
+   */
+  public Notebook deleteNotebook(String id) throws SubmarineRuntimeException {
+    //TODO(ryan): implement the method
+    return null;
+  }
+
+  /**
+   * Generate a unique notebook id
+   * @return notebook id
+   */
+  private NotebookId generateNotebookId() {
+    return NotebookId.newInstance(SubmarineServer.getServerTimeStamp(),
+            notebookCounter.incrementAndGet());
+  }
+
+  /**
+   * Check if notebook spec is valid
+   * @param spec notebook spec
+   */
+  private void checkNotebookSpec(NotebookSpec spec) {
+    //TODO(ryan): The method need to be improved
+    if (spec == null) {
+      throw new SubmarineRuntimeException(Response.Status.OK.getStatusCode(),
+              "Invalid. Notebook Spec object is null.");
+    }
+  }
+
+}
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/response/JsonResponse.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/response/JsonResponse.java
index b4a4e19..27d4cd1 100644
--- 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/response/JsonResponse.java
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/response/JsonResponse.java
@@ -24,8 +24,11 @@ import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import com.google.gson.TypeAdapter;
 import org.apache.submarine.server.api.experiment.ExperimentId;
+import org.apache.submarine.server.api.notebook.NotebookId;
 import org.apache.submarine.server.gson.ExperimentIdDeserializer;
 import org.apache.submarine.server.gson.ExperimentIdSerializer;
+import org.apache.submarine.server.gson.NotebookIdDeserializer;
+import org.apache.submarine.server.gson.NotebookIdSerializer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -171,6 +174,8 @@ public class JsonResponse<T> {
           .registerTypeAdapter(Date.class, safeDateTypeAdapter)
           .registerTypeAdapter(ExperimentId.class, new 
ExperimentIdSerializer())
           .registerTypeAdapter(ExperimentId.class, new 
ExperimentIdDeserializer())
+          .registerTypeAdapter(NotebookId.class, new NotebookIdSerializer())
+          .registerTypeAdapter(NotebookId.class, new NotebookIdDeserializer())
           .serializeNulls()
           .create();
     }
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/NotebookRestApi.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/NotebookRestApi.java
new file mode 100644
index 0000000..ac61cb2
--- /dev/null
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/NotebookRestApi.java
@@ -0,0 +1,172 @@
+/*
+ * 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.rest;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+
+import org.apache.submarine.commons.utils.exception.SubmarineRuntimeException;
+import org.apache.submarine.server.api.notebook.Notebook;
+import org.apache.submarine.server.api.spec.NotebookSpec;
+import org.apache.submarine.server.notebook.NotebookManager;
+import org.apache.submarine.server.response.JsonResponse;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.List;
+
+/**
+ * Notebook Service REST API v1
+ */
+@Path(RestConstants.V1 + "/" + RestConstants.NOTEBOOK)
+@Produces({MediaType.APPLICATION_JSON + "; " + RestConstants.CHARSET_UTF8})
+public class NotebookRestApi {
+
+  /* Notebook manager  */
+  private final NotebookManager notebookManager = 
NotebookManager.getInstance();
+
+  /**
+   * Return the Pong message for test the connectivity
+   * @return Pong message
+   */
+  @GET
+  @Path(RestConstants.PING)
+  @Consumes(MediaType.APPLICATION_JSON)
+  @Operation(summary = "Ping submarine server",
+          tags = {"notebook"},
+          description = "Return the Pong message for test the connectivity",
+          responses = {
+                  @ApiResponse(responseCode = "200", description = "successful 
operation",
+                          content = @Content(schema = @Schema(implementation = 
String.class)))})
+  public Response ping() {
+    return new JsonResponse.Builder<String>(Response.Status.OK)
+            .success(true).result("Pong").build();
+  }
+
+  /**
+   * Create a notebook with spec
+   * @param spec notebook spec
+   * @return the detailed info about created notebook
+   */
+  @POST
+  @Consumes({RestConstants.MEDIA_TYPE_YAML, MediaType.APPLICATION_JSON})
+  @Operation(
+          summary = "Create a notebook instance",
+          tags = {"notebook"},
+          responses = {
+                  @ApiResponse(description = "successful operation", content = 
@Content(
+                          schema = @Schema(implementation = 
JsonResponse.class)))})
+  public Response createNotebook(NotebookSpec spec) {
+    try {
+      Notebook notebook = notebookManager.createNotebook(spec);
+      return new 
JsonResponse.Builder<Notebook>(Response.Status.OK).success(true)
+              .message("Create a notebook instance").result(notebook).build();
+    } catch (SubmarineRuntimeException e) {
+      return parseNotebookServiceException(e);
+    }
+  }
+
+  /**
+   * List all notebooks created by the user
+   * @param status status
+   * @return notebook list
+   */
+  @GET
+  @Operation(
+          summary = "List notebooks",
+          tags = {"notebook"},
+          responses = {
+                  @ApiResponse(description = "successful operation", content = 
@Content(
+                          schema = @Schema(implementation = 
JsonResponse.class)))})
+  public Response listNotebooks(@QueryParam("status") String status) {
+    try {
+      List<Notebook> notebookList = 
notebookManager.listNotebooksByStatus(status);
+      return new 
JsonResponse.Builder<List<Notebook>>(Response.Status.OK).success(true)
+              .result(notebookList).build();
+    } catch (SubmarineRuntimeException e) {
+      return parseNotebookServiceException(e);
+    }
+  }
+
+  /**
+   * Get detailed info about the notebook by notebook id
+   * @param id notebook id
+   * @return detailed info about the notebook
+   */
+  @GET
+  @Path("/{id}")
+  @Operation(
+          summary = "Get detailed info about the notebook",
+          tags = {"notebook"},
+          responses = {
+                  @ApiResponse(
+                          description = "successful operation", content = 
@Content(
+                          schema = @Schema(implementation = 
JsonResponse.class))),
+                  @ApiResponse(responseCode = "404", description = "Notebook 
not found")})
+  public Response getNotebook(@PathParam(RestConstants.NOTEBOOK_ID) String id) 
{
+    try {
+      Notebook notebook = notebookManager.getNotebook(id);
+      return new 
JsonResponse.Builder<Notebook>(Response.Status.OK).success(true)
+              .result(notebook).build();
+    } catch (SubmarineRuntimeException e) {
+      return parseNotebookServiceException(e);
+    }
+  }
+
+  /**
+   * Delete the notebook with notebook id
+   * @param id notebook id
+   * @return the detailed info about deleted notebook
+   */
+  @DELETE
+  @Path("/{id}")
+  @Operation(
+          summary = "Delete the notebook",
+          tags = {"notebook"},
+          responses = {
+                  @ApiResponse(
+                          description = "successful operation", content = 
@Content(
+                          schema = @Schema(implementation = 
JsonResponse.class))),
+                  @ApiResponse(responseCode = "404", description = "Notebook 
not found")})
+  public Response deleteNotebook(@PathParam(RestConstants.NOTEBOOK_ID) String 
id) {
+    try {
+      Notebook notebook = notebookManager.deleteNotebook(id);
+      return new 
JsonResponse.Builder<Notebook>(Response.Status.OK).success(true)
+              .result(notebook).build();
+    } catch (SubmarineRuntimeException e) {
+      return parseNotebookServiceException(e);
+    }
+  }
+
+  private Response parseNotebookServiceException(SubmarineRuntimeException e) {
+    return new 
JsonResponse.Builder<String>(e.getCode()).message(e.getMessage()).build();
+  }
+
+}
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/RestConstants.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/RestConstants.java
index f7be478..ecfce40 100644
--- 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/RestConstants.java
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/RestConstants.java
@@ -36,11 +36,19 @@ public class RestConstants {
   public static final String NODE = "node";
 
   public static final String LOGS = "logs";
-  
+
   /**
    * Environment
    */
   public static final String ENVIRONMENT = "environment";
-  
+
   public static final String ENVIRONMENT_ID = "id";
+
+  /**
+   * Notebook
+   */
+  public static final String NOTEBOOK = "notebook";
+
+  public static final String NOTEBOOK_ID = "id";
+
 }
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 9b495bf..5fe17ad 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
@@ -38,14 +38,19 @@ import io.kubernetes.client.util.KubeConfig;
 import org.apache.submarine.commons.utils.SubmarineConfiguration;
 import org.apache.submarine.commons.utils.exception.SubmarineRuntimeException;
 import org.apache.submarine.server.api.exception.InvalidSpecException;
-import org.apache.submarine.server.api.experiment.Submitter;
+import org.apache.submarine.server.api.Submitter;
 import org.apache.submarine.server.api.experiment.Experiment;
 import org.apache.submarine.server.api.experiment.ExperimentLog;
+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.NotebookCR;
+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;
 import org.apache.submarine.server.submitter.k8s.parser.ExperimentSpecParser;
+import org.joda.time.DateTime;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -227,6 +232,61 @@ public class K8sSubmitter implements Submitter {
     return experimentLog;
   }
 
+  @Override
+  public Notebook createNotebook(NotebookSpec spec) throws 
SubmarineRuntimeException {
+    Notebook notebook;
+    try {
+      NotebookCR notebookCR = NotebookSpecParser.parseNotebook(spec);
+      Object object = api.createNamespacedCustomObject(notebookCR.getGroup(), 
notebookCR.getVersion(),
+              notebookCR.getMetadata().getNamespace(), notebookCR.getPlural(), 
notebookCR, "true");
+      notebook = parseResponseObject(object);
+    } 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.");
+    } catch (ApiException e) {
+      LOG.error("K8s submitter: parse Notebook object failed by " + 
e.getMessage(), e);
+      throw new SubmarineRuntimeException(e.getCode(), e.getMessage());
+    }
+    return notebook;
+  }
+
+  @Override
+  public Notebook findNotebook(NotebookSpec spec) throws 
SubmarineRuntimeException {
+    // TODO(ryan): Implement this method
+    return null;
+  }
+
+  @Override
+  public Notebook deleteNotebook(NotebookSpec spec) throws 
SubmarineRuntimeException {
+    // TODO(ryan): Implement this method
+    return null;
+  }
+
+  private Notebook parseResponseObject(Object obj) throws 
SubmarineRuntimeException {
+    Gson gson = new JSON().getGson();
+    String jsonString = gson.toJson(obj);
+    LOG.info("Upstream response JSON: {}", jsonString);
+    Notebook notebook;
+    try {
+      notebook = new Notebook();
+      NotebookCR notebookCR = gson.fromJson(jsonString, NotebookCR.class);
+      notebook.setUid(notebookCR.getMetadata().getUid());
+      notebook.setName(notebookCR.getMetadata().getName());
+      // notebook url
+      notebook.setUrl("/notebook/" + notebookCR.getMetadata().getNamespace() + 
"/" +
+              notebookCR.getMetadata().getName());
+      DateTime createdTime = notebookCR.getMetadata().getCreationTimestamp();
+      if (createdTime != null) {
+        notebook.setCreatedTime(createdTime.toString());
+        notebook.setStatus(Notebook.Status.STATUS_CREATED.getValue());
+      }
+    } 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.");
+    }
+    return notebook;
+  }
+
   private String getJobLabelSelector(ExperimentSpec experimentSpec) {
     // TODO(JohnTing): SELECTOR_KEY should be obtained from individual models 
in MLJOB
     if (experimentSpec.getMeta().getFramework()
diff --git 
a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/NotebookCR.java
 
b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/NotebookCR.java
new file mode 100644
index 0000000..96295d0
--- /dev/null
+++ 
b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/NotebookCR.java
@@ -0,0 +1,115 @@
+/*
+ * 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;
+
+import com.google.gson.annotations.SerializedName;
+import io.kubernetes.client.models.V1ObjectMeta;
+
+public class NotebookCR {
+
+  public static final String CRD_NOTEBOOK_VERSION_V1 = "v1";
+  public static final String CRD_NOTEBOOK_GROUP_V1 = "kubeflow.org";
+  public static final String CRD_APIVERSION_V1 = CRD_NOTEBOOK_GROUP_V1 + "/" + 
CRD_NOTEBOOK_VERSION_V1;
+  public static final String CRD_NOTEBOOK_KIND_V1 = "Notebook";
+  public static final String CRD_NOTEBOOK_PLURAL_V1 = "notebooks";
+
+  @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 NotebookCRSpec spec;
+
+  public NotebookCR() {
+    setApiVersion(CRD_APIVERSION_V1);
+    setKind(CRD_NOTEBOOK_KIND_V1);
+    setPlural(CRD_NOTEBOOK_PLURAL_V1);
+    setGroup(CRD_NOTEBOOK_GROUP_V1);
+    setVersion(CRD_NOTEBOOK_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 getGroup() {
+    return this.group;
+  }
+
+  public void setGroup(String group) {
+    this.group = group;
+  }
+
+  public String getVersion() {
+    return this.version;
+  }
+
+  public void setVersion(String version) {
+    this.version = version;
+  }
+
+  public String getPlural() {
+    return this.plural;
+  }
+
+  public void setPlural(String plural) {
+    this.plural = plural;
+  }
+
+  public NotebookCRSpec getSpec() {
+    return spec;
+  }
+
+  public void setSpec(NotebookCRSpec spec) {
+    this.spec = spec;
+  }
+
+}
diff --git 
a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/NotebookCRSpec.java
 
b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/NotebookCRSpec.java
new file mode 100644
index 0000000..05eb1dd
--- /dev/null
+++ 
b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/model/NotebookCRSpec.java
@@ -0,0 +1,92 @@
+/*
+ * 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;
+
+import com.google.gson.annotations.SerializedName;
+import io.kubernetes.client.models.V1PodTemplateSpec;
+
+import java.math.BigDecimal;
+
+public class NotebookCRSpec {
+
+  public NotebookCRSpec() {
+
+  }
+
+  @SerializedName("template")
+  private V1PodTemplateSpec template;
+
+  /**
+   * Get the pod template
+   * @return pod template spec
+   */
+  public V1PodTemplateSpec getTemplate() {
+    return template;
+  }
+
+  /**
+   * Set the pod template
+   * @param template pod template
+   */
+  public void setTemplate(V1PodTemplateSpec template) {
+    this.template = template;
+  }
+
+  /**
+   * Get memory resource for container
+   * @return memory in Gi
+   */
+  public String getContainerMemory() {
+    V1PodTemplateSpec podSpec = getTemplate();
+    return String.join(" ",
+            podSpec.getSpec().getContainers().get(0)
+                    .getResources().getLimits().get("memory").
+                    getNumber().divide(BigDecimal.valueOf(1024 * 1024 * 
1024)).toString() + "Gi");
+  }
+
+  /**
+   * Get CPU resource for container
+   * @return CPU in VCores
+   */
+  public String getContainerCpu() {
+    V1PodTemplateSpec podSpec = getTemplate();
+    return podSpec.getSpec().getContainers().get(0)
+            .getResources().getLimits().get("cpu").getNumber().toString();
+  }
+
+  /**
+   * Get GPU resource for container
+   * @return GPU
+   */
+  public String getContainerGpu() {
+    V1PodTemplateSpec podSpec = getTemplate();
+    return podSpec.getSpec().getContainers().get(0)
+            
.getResources().getLimits().get("nvidia.com/gpu").getNumber().toString();
+  }
+
+  /**
+   * Get the image name
+   * @return image name
+   */
+  public String getContainerImageName() {
+    V1PodTemplateSpec podSpec = getTemplate();
+    return podSpec.getSpec().getContainers().get(0).getImage();
+  }
+}
diff --git 
a/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/parser/NotebookSpecParser.java
 
b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/parser/NotebookSpecParser.java
new file mode 100644
index 0000000..e606688
--- /dev/null
+++ 
b/submarine-server/server-submitter/submitter-k8s/src/main/java/org/apache/submarine/server/submitter/k8s/parser/NotebookSpecParser.java
@@ -0,0 +1,194 @@
+/*
+ * 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 io.kubernetes.client.custom.Quantity;
+import io.kubernetes.client.models.V1Container;
+import io.kubernetes.client.models.V1EnvVar;
+import io.kubernetes.client.models.V1ObjectMeta;
+import io.kubernetes.client.models.V1PodTemplateSpec;
+import io.kubernetes.client.models.V1PodSpec;
+import io.kubernetes.client.models.V1ResourceRequirements;
+
+import org.apache.submarine.commons.utils.SubmarineConfVars;
+import org.apache.submarine.commons.utils.SubmarineConfiguration;
+import org.apache.submarine.server.api.environment.Environment;
+import org.apache.submarine.server.api.spec.KernelSpec;
+import org.apache.submarine.server.api.spec.NotebookPodSpec;
+import org.apache.submarine.server.api.spec.NotebookSpec;
+import org.apache.submarine.server.environment.EnvironmentManager;
+import org.apache.submarine.server.submitter.k8s.model.NotebookCR;
+import org.apache.submarine.server.submitter.k8s.model.NotebookCRSpec;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class NotebookSpecParser {
+
+  private static SubmarineConfiguration conf =
+          SubmarineConfiguration.getInstance();
+
+
+  public static NotebookCR parseNotebook(NotebookSpec spec) {
+    NotebookCR notebookCR = new NotebookCR();
+    notebookCR.setMetadata(parseMetadata(spec));
+    notebookCR.setSpec(parseNotebookCRSpec(spec));
+    return notebookCR;
+  }
+
+  private static V1ObjectMeta parseMetadata(NotebookSpec spec) {
+    V1ObjectMeta meta = new V1ObjectMeta();
+    meta.setName(spec.getMeta().getName());
+    meta.setNamespace(spec.getMeta().getNamespace());
+    return meta;
+  }
+
+  private static NotebookCRSpec parseNotebookCRSpec(NotebookSpec spec) {
+    NotebookCRSpec CRSpec = new NotebookCRSpec();
+    CRSpec.setTemplate(parseTemplateSpec(spec));
+    return CRSpec;
+  }
+
+  private static V1PodTemplateSpec parseTemplateSpec(NotebookSpec 
notebookSpec) {
+    NotebookPodSpec notebookPodSpec = notebookSpec.getSpec();
+    V1PodTemplateSpec podTemplateSpec = new V1PodTemplateSpec();
+    V1PodSpec podSpec = new V1PodSpec();
+    // Set container
+    List<V1Container> containers = new ArrayList<>();
+    V1Container container = new V1Container();
+    container.setName(notebookSpec.getMeta().getName());
+
+    // Environment variables
+    if (notebookPodSpec.getEnvVars() == null) {
+      container.setEnv(parseEnvVars(notebookPodSpec));
+    }
+
+    // Environment
+    if (getEnvironment(notebookSpec) != null) {
+      String baseImage = 
getEnvironment(notebookSpec).getEnvironmentSpec().getDockerImage();
+      KernelSpec kernel = 
getEnvironment(notebookSpec).getEnvironmentSpec().getKernelSpec();
+      container.setImage(baseImage);
+      if (kernel.getDependencies().size() > 0) {
+        String condaVersionValidationCommand = 
generateCondaVersionValidateCommand();
+        StringBuffer createCommand = new StringBuffer();
+        String condaEnvironmentName = kernel.getName();
+
+        createCommand.append("conda create -q -y -n " + condaEnvironmentName);
+        for (String channel : kernel.getChannels()) {
+          createCommand.append(" ");
+          createCommand.append("-c");
+          createCommand.append(" ");
+          createCommand.append(channel);
+        }
+        for (String dependency : kernel.getDependencies()) {
+          createCommand.append(" ");
+          createCommand.append(dependency);
+        }
+
+        String activateEnvCommand = "source activate " + condaEnvironmentName;
+        String pathCommand = "PATH=/opt/conda/envs/env/bin:$PATH";
+        String finalCommand = condaVersionValidationCommand +
+                " && " + createCommand.toString() + " && "
+                + activateEnvCommand + " && " + pathCommand;
+        V1EnvVar envCommand = new V1EnvVar();
+        envCommand.setName("ENVIRONMENT_COMMAND");
+        envCommand.setValue(finalCommand);
+        container.addEnvItem(envCommand);
+      }
+    }
+
+    // Resources
+    if (notebookPodSpec.getResources() != null) {
+      V1ResourceRequirements resources = new V1ResourceRequirements();
+      resources.setLimits(parseResources(notebookPodSpec));
+      container.setResources(resources);
+    }
+
+    containers.add(container);
+    podSpec.setContainers(containers);
+
+    podTemplateSpec.setSpec(podSpec);
+    return podTemplateSpec;
+  }
+
+  private static List<V1EnvVar> parseEnvVars(NotebookPodSpec podSpec) {
+    if (podSpec.getEnvVars() == null)
+      return null;
+    List<V1EnvVar> envVars = new ArrayList<>();
+    for (Map.Entry<String, String> entry : podSpec.getEnvVars().entrySet()) {
+      V1EnvVar env = new V1EnvVar();
+      env.setName(entry.getKey());
+      env.setValue(entry.getValue());
+      envVars.add(env);
+    }
+    return envVars;
+  }
+
+  private static Map<String, Quantity> parseResources(NotebookPodSpec podSpec) 
{
+
+    Map<String, Quantity> resources = new HashMap<>();
+    podSpec.setResources(podSpec.getResources());
+
+    if (podSpec.getCpu() != null) {
+      resources.put("cpu", new Quantity(podSpec.getCpu()));
+    }
+    if (podSpec.getMemory() != null) {
+      resources.put("memory", new Quantity(podSpec.getMemory()));
+    }
+    if (podSpec.getGpu() != null) {
+      resources.put("nvidia.com/gpu", new Quantity(podSpec.getGpu()));
+    }
+    return resources;
+  }
+
+  private static Environment getEnvironment(NotebookSpec notebookSpec) {
+    if (notebookSpec.getEnvironment().getName() != null) {
+      EnvironmentManager environmentManager = EnvironmentManager.getInstance();
+      return environmentManager
+              .getEnvironment(notebookSpec.getEnvironment().getName());
+    } else {
+      return null;
+    }
+  }
+
+  private static String generateCondaVersionValidateCommand() {
+    String currentVersion = "currentVersion=$(conda -V | cut -f2 -d' ');";
+    String minVersion = "minVersion=\""
+            + 
conf.getString(SubmarineConfVars.ConfVars.ENVIRONMENT_CONDA_MIN_VERSION)
+            + "\";";
+    String maxVersion = "maxVersion=\""
+            + 
conf.getString(SubmarineConfVars.ConfVars.ENVIRONMENT_CONDA_MAX_VERSION)
+            + "\";";
+    StringBuffer condaVersionValidationCommand = new StringBuffer();
+    condaVersionValidationCommand.append(minVersion);
+    condaVersionValidationCommand.append(maxVersion);
+    condaVersionValidationCommand.append(currentVersion);
+    condaVersionValidationCommand.append("if [ \"$(printf '%s\\n' "
+            + "\"$minVersion\" \"$maxVersion\" \"$currentVersion\" | sort -V "
+            + "| head -n2 | tail -1 )\" != \"$currentVersion\" ]; then echo "
+            + "\"Conda version should be between " + minVersion + " and "
+            + maxVersion + "\"; exit 1; else echo \"Conda current version is "
+            + currentVersion + ". Moving forward with env creation and "
+            + "activation.\"; fi");
+    return condaVersionValidationCommand.toString();
+  }
+}
diff --git 
a/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/ExperimentSpecParserTest.java
 
b/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/ExperimentSpecParserTest.java
index 8517671..c44bec9 100644
--- 
a/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/ExperimentSpecParserTest.java
+++ 
b/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/ExperimentSpecParserTest.java
@@ -49,14 +49,14 @@ import io.kubernetes.client.models.V1Container;
 
 
 public class ExperimentSpecParserTest extends SpecBuilder {
-  
+
   private static SubmarineConfiguration conf =
       SubmarineConfiguration.getInstance();
-  
+
   @Test
   public void testValidTensorFlowExperiment() throws IOException,
       URISyntaxException, InvalidSpecException {
-    ExperimentSpec experimentSpec = buildFromJsonFile(tfJobReqFile);
+    ExperimentSpec experimentSpec = (ExperimentSpec) 
buildFromJsonFile(ExperimentSpec.class, tfJobReqFile);
     TFJob tfJob = (TFJob) ExperimentSpecParser.parseJob(experimentSpec);
     validateMetadata(experimentSpec.getMeta(), tfJob.getMetadata(),
         ExperimentMeta.SupportedMLFramework.TENSORFLOW.getName().toLowerCase()
@@ -74,7 +74,7 @@ public class ExperimentSpecParserTest extends SpecBuilder {
   @Test
   public void testInvalidTensorFlowExperiment() throws IOException,
       URISyntaxException {
-    ExperimentSpec experimentSpec = buildFromJsonFile(tfJobReqFile);
+    ExperimentSpec experimentSpec = (ExperimentSpec) 
buildFromJsonFile(ExperimentSpec.class, tfJobReqFile);
     // Case 1. Invalid framework name
     experimentSpec.getMeta().setFramework("fooframework");
     try {
@@ -85,7 +85,7 @@ public class ExperimentSpecParserTest extends SpecBuilder {
     }
 
     // Case 2. Invalid TensorFlow replica name. It can only be "ps" "worker" 
"chief" and "Evaluator"
-    experimentSpec = buildFromJsonFile(tfJobReqFile);
+    experimentSpec =  (ExperimentSpec) buildFromJsonFile(ExperimentSpec.class, 
tfJobReqFile);
     experimentSpec.getSpec().put("foo", 
experimentSpec.getSpec().get(TFJobReplicaType.Ps.getTypeName()));
     experimentSpec.getSpec().remove(TFJobReplicaType.Ps.getTypeName());
     try {
@@ -99,7 +99,8 @@ public class ExperimentSpecParserTest extends SpecBuilder {
   @Test
   public void testValidPyTorchExperiment() throws IOException,
       URISyntaxException, InvalidSpecException {
-    ExperimentSpec experimentSpec = buildFromJsonFile(pytorchJobReqFile);
+    ExperimentSpec experimentSpec =
+            (ExperimentSpec) buildFromJsonFile(ExperimentSpec.class, 
pytorchJobReqFile);
     PyTorchJob pyTorchJob = (PyTorchJob) 
ExperimentSpecParser.parseJob(experimentSpec);
     validateMetadata(experimentSpec.getMeta(), pyTorchJob.getMetadata(),
         ExperimentMeta.SupportedMLFramework.PYTORCH.getName().toLowerCase()
@@ -117,7 +118,8 @@ public class ExperimentSpecParserTest extends SpecBuilder {
   @Test
   public void testInvalidPyTorchJobSpec() throws IOException,
       URISyntaxException {
-    ExperimentSpec experimentSpec = buildFromJsonFile(pytorchJobReqFile);
+    ExperimentSpec experimentSpec =
+            (ExperimentSpec) buildFromJsonFile(ExperimentSpec.class, 
pytorchJobReqFile);
     // Case 1. Invalid framework name
     experimentSpec.getMeta().setFramework("fooframework");
     try {
@@ -128,7 +130,7 @@ public class ExperimentSpecParserTest extends SpecBuilder {
     }
 
     // Case 2. Invalid PyTorch replica name. It can only be "master" and 
"worker"
-    experimentSpec = buildFromJsonFile(pytorchJobReqFile);
+    experimentSpec = (ExperimentSpec) buildFromJsonFile(ExperimentSpec.class, 
pytorchJobReqFile);
     experimentSpec.getSpec().put("ps", experimentSpec.getSpec().get(
         PyTorchJobReplicaType.Master.getTypeName()));
     
experimentSpec.getSpec().remove(PyTorchJobReplicaType.Master.getTypeName());
@@ -188,7 +190,7 @@ public class ExperimentSpecParserTest extends SpecBuilder {
     Assert.assertEquals(expectedMasterContainerCpu,
         actualMasterContainerCpu);
   }
-  
+
   @Test
   public void testValidPyTorchJobSpecWithEnv()
       throws IOException, URISyntaxException, InvalidSpecException {
@@ -217,7 +219,8 @@ public class ExperimentSpecParserTest extends SpecBuilder {
 
     environmentManager.createEnvironment(spec);
 
-    ExperimentSpec jobSpec = buildFromJsonFile(pytorchJobWithEnvReqFile);
+    ExperimentSpec jobSpec =
+            (ExperimentSpec) buildFromJsonFile(ExperimentSpec.class, 
pytorchJobWithEnvReqFile);
     PyTorchJob pyTorchJob = (PyTorchJob) 
ExperimentSpecParser.parseJob(jobSpec);
 
     MLJobReplicaSpec mlJobReplicaSpec = pyTorchJob.getSpec().getReplicaSpecs()
@@ -230,7 +233,7 @@ public class ExperimentSpecParserTest extends SpecBuilder {
 
     Assert.assertEquals("/bin/bash", initContainer.getCommand().get(0));
     Assert.assertEquals("-c", initContainer.getCommand().get(1));
-    
+
     String minVersion = "minVersion=\""
         + conf.getString(
             SubmarineConfVars.ConfVars.ENVIRONMENT_CONDA_MIN_VERSION)
@@ -241,20 +244,20 @@ public class ExperimentSpecParserTest extends SpecBuilder 
{
         + "\";";
     String currentVersion = "currentVersion=$(conda -V | cut -f2 -d' ');";
     Assert.assertEquals(
-        minVersion + maxVersion + currentVersion 
+        minVersion + maxVersion + currentVersion
             + "if [ \"$(printf '%s\\n' \"$minVersion\" \"$maxVersion\" "
                + "\"$currentVersion\" | sort -V | head -n2 | tail -1 )\" "
-                    + "!= \"$currentVersion\" ]; then echo \"Conda version " + 
-                    "should be between minVersion=\"4.0.1\"; " + 
+                    + "!= \"$currentVersion\" ]; then echo \"Conda version " +
+                    "should be between minVersion=\"4.0.1\"; " +
                     "and maxVersion=\"4.10.10\";\"; exit 1; else echo "
                     + "\"Conda current version is " + currentVersion + ". "
                         + "Moving forward with env creation and activation.\"; 
"
-                        + "fi && " + 
+                        + "fi && " +
         "conda create -n " + kernelName + " -c " + channel + " " + dependency
             + " && " + "echo \"source activate " + kernelName + "\" > 
~/.bashrc"
             + " && " + "PATH=/opt/conda/envs/env/bin:$PATH",
         initContainer.getCommand().get(2));
-    
+
     environmentManager.deleteEnvironment(envName);
   }
 }
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 0a232ce..a27f88e 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
@@ -59,14 +59,14 @@ public class K8SJobSubmitterTest extends SpecBuilder {
   @Test
   public void testRunPyTorchJobPerRequest() throws URISyntaxException,
       IOException, SubmarineRuntimeException {
-    ExperimentSpec spec = buildFromJsonFile(pytorchJobReqFile);
+    ExperimentSpec spec = (ExperimentSpec) 
buildFromJsonFile(ExperimentSpec.class, pytorchJobReqFile);
     run(spec);
   }
 
   @Test
   public void testRunTFJobPerRequest() throws URISyntaxException,
       IOException, SubmarineRuntimeException {
-    ExperimentSpec spec = buildFromJsonFile(tfJobReqFile);
+    ExperimentSpec spec = (ExperimentSpec) 
buildFromJsonFile(ExperimentSpec.class, tfJobReqFile);
     run(spec);
   }
 
diff --git 
a/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/MLJobConverterTest.java
 
b/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/MLJobConverterTest.java
index 75267ba..851f658 100644
--- 
a/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/MLJobConverterTest.java
+++ 
b/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/MLJobConverterTest.java
@@ -45,7 +45,7 @@ public class MLJobConverterTest extends SpecBuilder {
   @Test
   public void testMLJob2Job() throws IOException, URISyntaxException, 
InvalidSpecException {
     // Accepted Status
-    ExperimentSpec spec = buildFromJsonFile(tfJobReqFile);
+    ExperimentSpec spec = (ExperimentSpec) 
buildFromJsonFile(ExperimentSpec.class, tfJobReqFile);
     MLJob mlJob = ExperimentSpecParser.parseJob(spec);
     V1JobStatus status = new V1JobStatusBuilder().build();
     mlJob.setStatus(status);
@@ -98,7 +98,7 @@ public class MLJobConverterTest extends SpecBuilder {
   @Test
   public void testMLJob2DeleteOptions() throws IOException, URISyntaxException,
       InvalidSpecException {
-    ExperimentSpec spec = buildFromJsonFile(tfJobReqFile);
+    ExperimentSpec spec = (ExperimentSpec) 
buildFromJsonFile(ExperimentSpec.class, tfJobReqFile);
     MLJob mlJob = ExperimentSpecParser.parseJob(spec);
     V1DeleteOptions options = MLJobConverter.toDeleteOptionsFromMLJob(mlJob);
     Assert.assertNotNull(options);
diff --git 
a/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/NotebookSpecParserTest.java
 
b/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/NotebookSpecParserTest.java
new file mode 100644
index 0000000..05de939
--- /dev/null
+++ 
b/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/NotebookSpecParserTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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;
+
+import io.kubernetes.client.models.V1ObjectMeta;
+import org.apache.submarine.server.api.spec.NotebookMeta;
+import org.apache.submarine.server.api.spec.NotebookPodSpec;
+import org.apache.submarine.server.api.spec.NotebookSpec;
+import org.apache.submarine.server.submitter.k8s.model.NotebookCR;
+import org.apache.submarine.server.submitter.k8s.model.NotebookCRSpec;
+import org.apache.submarine.server.submitter.k8s.parser.NotebookSpecParser;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+public class NotebookSpecParserTest extends SpecBuilder {
+
+  @Test
+  public void testValidNotebook() throws IOException, URISyntaxException {
+    NotebookSpec notebookSpec = (NotebookSpec) 
buildFromJsonFile(NotebookSpec.class, notebookReqFile);
+    NotebookCR notebook = NotebookSpecParser.parseNotebook(notebookSpec);
+
+    validateMetadata(notebookSpec.getMeta(), notebook.getMetadata());
+    validateEnvironment(notebookSpec, notebook.getSpec());
+    validatePodSpec(notebookSpec.getSpec(), notebook);
+  }
+
+  private void validateMetadata(NotebookMeta meta, V1ObjectMeta actualMeta) {
+    Assert.assertEquals(meta.getName(), actualMeta.getName());
+    Assert.assertEquals(meta.getNamespace(), actualMeta.getNamespace());
+  }
+
+  private void validateEnvironment(NotebookSpec spec, NotebookCRSpec 
actualPodSpec) {
+    String expectedImage = spec.getEnvironment().getImage();
+    String actualImage = actualPodSpec.getContainerImageName();
+    Assert.assertEquals(expectedImage, actualImage);
+  }
+
+  private void validatePodSpec(NotebookPodSpec podSpec, NotebookCR notebook) {
+    NotebookCRSpec notebookCRSpec = null;
+    if (notebook != null) {
+      notebookCRSpec = notebook.getSpec();
+    }
+    Assert.assertNotNull(notebookCRSpec);
+
+    // mem
+    String expectedContainerMem = podSpec.getMemory();
+    String actualContainerMem = notebookCRSpec.getContainerMemory();
+    Assert.assertEquals(expectedContainerMem, actualContainerMem);
+
+    // cpu
+    String expectedContainerCpu = podSpec.getCpu();
+    String actualContainerCpu = notebookCRSpec.getContainerCpu();
+    Assert.assertEquals(expectedContainerCpu, actualContainerCpu);
+  }
+
+}
diff --git 
a/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/SpecBuilder.java
 
b/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/SpecBuilder.java
index 50c83f8..11546ab 100644
--- 
a/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/SpecBuilder.java
+++ 
b/submarine-server/server-submitter/submitter-k8s/src/test/java/org/apache/submarine/server/submitter/k8s/SpecBuilder.java
@@ -30,6 +30,7 @@ import java.nio.file.Files;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import org.apache.submarine.server.api.spec.ExperimentSpec;
+import org.apache.submarine.server.api.spec.NotebookSpec;
 
 public abstract class SpecBuilder {
   // The spec files in test/resources
@@ -38,13 +39,18 @@ public abstract class SpecBuilder {
   protected final String pytorchJobWithEnvReqFile = 
"/pytorch_job_req_env.json";
   protected final String pytorchJobWithInvalidEnvReqFile =
       "/pytorch_job_req_invalid_env.json";
+  protected final String notebookReqFile = "/notebook_req.json";
 
-  protected ExperimentSpec buildFromJsonFile(String filePath) throws 
IOException,
+  protected Object buildFromJsonFile(Object obj, String filePath) throws 
IOException,
       URISyntaxException {
     Gson gson = new GsonBuilder().create();
     try (Reader reader = 
Files.newBufferedReader(getCustomJobSpecFile(filePath).toPath(),
         StandardCharsets.UTF_8)) {
-      return gson.fromJson(reader, ExperimentSpec.class);
+      if (obj.equals(NotebookSpec.class)) {
+        return gson.fromJson(reader, NotebookSpec.class);
+      } else {
+        return gson.fromJson(reader, ExperimentSpec.class);
+      }
     }
   }
 
diff --git 
a/submarine-server/server-submitter/submitter-k8s/src/test/resources/notebook_req.json
 
b/submarine-server/server-submitter/submitter-k8s/src/test/resources/notebook_req.json
new file mode 100644
index 0000000..e1dace6
--- /dev/null
+++ 
b/submarine-server/server-submitter/submitter-k8s/src/test/resources/notebook_req.json
@@ -0,0 +1,15 @@
+{
+  "meta": {
+    "name": "my-nb",
+    "namespace": "default"
+  },
+  "environment": {
+    "image": "apache/submarine:tf2.1.0-jupyter"
+  },
+  "spec": {
+    "envVars": {
+      "env": "env"
+    },
+    "resources": "cpu=1,memory=1Gi"
+  }
+}


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

Reply via email to