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 02c070a  SUBMARINE-267. Initial job server which defines REST skeleton
02c070a is described below

commit 02c070a41d4af0027249e5ddf09642d0e0d492fe
Author: Zhankun Tang <[email protected]>
AuthorDate: Wed Nov 13 13:19:39 2019 +0800

    SUBMARINE-267. Initial job server which defines REST skeleton
    
    ### What is this PR for?
    The initial job server which defines REST skeleton
    
    ### What type of PR is it?
    Feature
    
    ### Todos
    * [1] - Fix the YAML unit test case issue
    * [2] - Populate the REST API with integration to yarn-submitter or 
k8s-submitter
    * [3] - Add Swagger to easy access the API spec
    
    ### What is the Jira issue?
    https://issues.apache.org/jira/browse/SUBMARINE-267
    
    ### How should this be tested?
    CI unit test is enough for the REST API before we integrate with submitter
    
    ### Screenshots (if appropriate)
    
    ### Questions:
    * Does the licenses files need update? No
    * Are there breaking changes for older versions? No
    * Does this needs documentation? No
    
    Author: Zhankun Tang <[email protected]>
    
    Closes #87 from tangzhankun/submarine-267 and squashes the following 
commits:
    
    5303d3f [Zhankun Tang] Add submarine server core test in Travis CI
    a355ff0 [Zhankun Tang] fix dependency issue for hadoop 3.1 and 3.2
    441317a [Zhankun Tang] Remove workbench server dependency from submarine 
server-core
    ae3cd5f [Zhankun Tang] Fix wrong test case name and inproper log method
    d97cdb7 [Zhankun Tang] SUBMARINE-267. Initial version of job server which 
defines job spec and REST skelon.
---
 .travis.yml                                        |   9 +-
 pom.xml                                            |   3 +-
 .../commons/utils/SubmarineConfiguration.java      |  34 +++-
 submarine-server/pom.xml                           |  68 +++++++
 submarine-server/server-core/pom.xml               | 102 ++++++++++
 .../org/apache/submarine/jobserver/JobServer.java  | 165 ++++++++++++++++
 .../submarine/jobserver/rest/api/JobApi.java       |  93 +++++++++
 .../submarine/jobserver/rest/dao/Component.java    |  69 +++++++
 .../submarine/jobserver/rest/dao/EnvVaraible.java  |  53 +++++
 .../jobserver/rest/dao/JsonExclusionStrategy.java  |  33 ++++
 .../submarine/jobserver/rest/dao/JsonResponse.java | 217 +++++++++++++++++++++
 .../submarine/jobserver/rest/dao/MLJobSpec.java    | 177 +++++++++++++++++
 .../jobserver/rest/dao/RestConstants.java          |  29 +++
 .../rest/provider/YamlEntityProvider.java          |  91 +++++++++
 .../src/main/resources/log4j.properties            |  17 ++
 .../server-core/src/test/java/JobApiTest.java      | 146 ++++++++++++++
 16 files changed, 1300 insertions(+), 6 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 037c9cb..af2a24e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -42,7 +42,7 @@ env:
   global:
     # submarine core does not required by workbench-server integration tests
     # If you need to compile Phadoop-3.1 or Phadoop-3.2, you need to add 
`!submarine-server/server-submitter/submitter-yarnservice` in EXCLUDE_SUBMARINE
-    - 
EXCLUDE_SUBMARINE="!submarine-all,!submarine-client,!submarine-commons,!submarine-commons/commons-runtime,!submarine-dist,!submarine-server/server-submitter/submitter-yarn,!submarine-server/server-submitter/submitter-k8s"
+    - 
EXCLUDE_SUBMARINE="!submarine-all,!submarine-client,!submarine-commons,!submarine-commons/commons-runtime,!submarine-dist,!submarine-server/server-submitter/submitter-yarn,!submarine-server/server-submitter/submitter-k8s,!submarine-server/server-core"
     - 
EXCLUDE_WORKBENCH="!submarine-workbench,!submarine-workbench/workbench-web,!submarine-workbench/workbench-server"
     - 
EXCLUDE_INTERPRETER="!submarine-workbench/interpreter,!submarine-workbench/interpreter/interpreter-engine,!submarine-workbench/interpreter/python-interpreter,!submarine-workbench/interpreter/spark-interpreter""
     - 
EXCLUDE_SUBMODULE_TONY="!submodules/tony,!submodules/tony/tony-mini,!submodules/tony/tony-core,!submodules/tony/tony-proxy,!submodules/tony/tony-portal,!submodules/tony/tony-azkaban,!submodules/tony/tony-cli"
@@ -160,6 +160,13 @@ matrix:
         - npm run e2e -- --protractor-config=e2e/protractor-ci.conf.js
       env: NAME="Build workbench-web-ng"
 
+
+    # Test submarine-server
+    - language: java
+      jdk: "openjdk8"
+      dist: xenial
+      env: NAME="Test submarine-server" PROFILE="-Phadoop-2.9" 
BUILD_FLAG="clean package install -DskipTests" TEST_FLAG="test -DskipRat -am" 
MODULES="-pl ${EXCLUDE_WORKBENCH},${EXCLUDE_INTERPRETER}" TEST_MODULES="-pl 
submarine-server/server-core" TEST_PROJECTS=""
+
 install:
   - mvn --version
   - echo "[$NAME] > mvn $BUILD_FLAG $MODULES $PROFILE -B"
diff --git a/pom.xml b/pom.xml
index df70ae0..8936bae 100644
--- a/pom.xml
+++ b/pom.xml
@@ -92,7 +92,7 @@
     <snakeyaml.version>1.16</snakeyaml.version>
     <httpcore.version>4.4.4</httpcore.version>
     <httpclient.version>4.5.2</httpclient.version>
-    <commons-lang.version>2.5</commons-lang.version>
+    <commons-lang.version>2.6</commons-lang.version>
     <commons-lang3.version>3.4</commons-lang3.version>
     <commons-io.version>2.5</commons-io.version>
     <commons-codec.version>1.5</commons-codec.version>
@@ -124,6 +124,7 @@
     <hive.version>2.1.1</hive.version>
     <!--  Submarine on Kubernetes  -->
     <k8s.client-java.version>6.0.1</k8s.client-java.version>
+    <jersey.test-framework>2.27</jersey.test-framework>
   </properties>
 
   <modules>
diff --git 
a/submarine-commons/commons-utils/src/main/java/org/apache/submarine/commons/utils/SubmarineConfiguration.java
 
b/submarine-commons/commons-utils/src/main/java/org/apache/submarine/commons/utils/SubmarineConfiguration.java
index 2ddb50c..44cea26 100644
--- 
a/submarine-commons/commons-utils/src/main/java/org/apache/submarine/commons/utils/SubmarineConfiguration.java
+++ 
b/submarine-commons/commons-utils/src/main/java/org/apache/submarine/commons/utils/SubmarineConfiguration.java
@@ -113,11 +113,11 @@ public class SubmarineConfiguration extends 
XMLConfiguration {
       }
     }
 
-    LOG.info("Server Host: " + conf.getServerAddress());
+    LOG.info("Workbench server Host: " + conf.getServerAddress());
     if (conf.useSsl() == false) {
-      LOG.info("Server Port: " + conf.getServerPort());
+      LOG.info("Workbench server Port: " + conf.getServerPort());
     } else {
-      LOG.info("Server SSL Port: " + conf.getServerSslPort());
+      LOG.info("Workbench server SSL Port: " + conf.getServerSslPort());
     }
 
     return conf;
@@ -127,14 +127,26 @@ public class SubmarineConfiguration extends 
XMLConfiguration {
     return getString(ConfVars.SERVER_ADDR);
   }
 
+  public String getJobServerAddress() {
+    return getString(ConfVars.JOB_SERVER_ADDR);
+  }
+
   public boolean useSsl() {
     return getBoolean(ConfVars.SERVER_SSL);
   }
 
+  public boolean isJobServerSslEnabled() {
+    return getBoolean(ConfVars.JOB_SERVER_SSL);
+  }
+
   public int getServerPort() {
     return getInt(ConfVars.SERVER_PORT);
   }
 
+  public int getJobServerPort() {
+    return getInt(ConfVars.JOB_SERVER_PORT);
+  }
+
   @VisibleForTesting
   public void setServerPort(int port) {
     properties.put(ConfVars.SERVER_PORT.getVarName(), String.valueOf(port));
@@ -144,6 +156,14 @@ public class SubmarineConfiguration extends 
XMLConfiguration {
     return getInt(ConfVars.SERVER_SSL_PORT);
   }
 
+  public int getJobServerSslPort() {
+    return getInt(ConfVars.JOB_SERVER_SSL_PORT);
+  }
+
+  public String getJobServerUrlPrefix() {
+    return getString(ConfVars.JOB_SERVER_REST_URL_PREFIX);
+  }
+
   public String getKeyStorePath() {
     String path = getString(ConfVars.SSL_KEYSTORE_PATH);
     return path;
@@ -419,7 +439,13 @@ public class SubmarineConfiguration extends 
XMLConfiguration {
     JDBC_PASSWORD("jdbc.password", "password"),
     WORKBENCH_WEBSOCKET_MAX_TEXT_MESSAGE_SIZE(
         "workbench.websocket.max.text.message.size", "1024000"),
-    WORKBENCH_WEB_WAR("workbench.web.war", 
"submarine-workbench/workbench-web/dist");
+    WORKBENCH_WEB_WAR("workbench.web.war", 
"submarine-workbench/workbench-web/dist"),
+    // submarine job server settings
+    JOB_SERVER_SSL("job.server.ssl", false),
+    JOB_SERVER_SSL_PORT("job.server.ssl.port", 8443),
+    JOB_SERVER_ADDR("job.server.port", "0.0.0.0"),
+    JOB_SERVER_PORT("job.server.port", 8765),
+    JOB_SERVER_REST_URL_PREFIX("job.server.rest.prefix", "/*");
 
     private String varName;
     @SuppressWarnings("rawtypes")
diff --git a/submarine-server/pom.xml b/submarine-server/pom.xml
index 0e7629a..a367703 100644
--- a/submarine-server/pom.xml
+++ b/submarine-server/pom.xml
@@ -35,6 +35,7 @@
 
   <modules>
     <module>server-submitter</module>
+    <module>server-core</module>
   </modules>
 
   <dependencyManagement>
@@ -43,6 +44,73 @@
         <groupId>org.apache.submarine</groupId>
         <artifactId>commons-runtime</artifactId>
         <version>${project.version}</version>
+        <exclusions>
+          <exclusion>
+            <groupId>commons-logging</groupId>
+            <artifactId>commons-logging</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>com.fasterxml.jackson.module</groupId>
+            <artifactId>jackson-module-jaxb-annotations</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-util</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.submarine</groupId>
+        <artifactId>commons-utils</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.slf4j</groupId>
+        <artifactId>slf4j-api</artifactId>
+        <version>${slf4j.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.slf4j</groupId>
+        <artifactId>slf4j-log4j12</artifactId>
+        <version>${slf4j.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.yaml</groupId>
+        <artifactId>snakeyaml</artifactId>
+        <version>${snakeyaml.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.glassfish.jersey.test-framework</groupId>
+        <artifactId>jersey-test-framework-core</artifactId>
+        <version>${jersey.test-framework}</version>
+        <exclusions>
+          <exclusion>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+          </exclusion>
+        </exclusions>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+        <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
+        <version>${jersey.test-framework}</version>
+        <exclusions>
+          <exclusion>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+          </exclusion>
+        </exclusions>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>com.google.code.gson</groupId>
+        <artifactId>gson</artifactId>
+        <version>${gson.version}</version>
       </dependency>
     </dependencies>
   </dependencyManagement>
diff --git a/submarine-server/server-core/pom.xml 
b/submarine-server/server-core/pom.xml
new file mode 100644
index 0000000..84c075e
--- /dev/null
+++ b/submarine-server/server-core/pom.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <parent>
+        <artifactId>submarine-server</artifactId>
+        <groupId>org.apache.submarine</groupId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>server-core</artifactId>
+    <name>Submarine: Submarine Server Core</name>
+    <dependencies>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-server</artifactId>
+            <version>${jetty.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-servlet</artifactId>
+            <version>${jetty.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.containers</groupId>
+            <artifactId>jersey-container-servlet-core</artifactId>
+            <version>${jersey.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.core</groupId>
+            <artifactId>jersey-server</artifactId>
+            <version>${jersey.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.inject</groupId>
+            <artifactId>jersey-hk2</artifactId>
+            <version>${jersey.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.media</groupId>
+            <artifactId>jersey-media-json-jackson</artifactId>
+            <version>${jersey.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.submarine</groupId>
+            <artifactId>commons-utils</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.submarine</groupId>
+            <artifactId>commons-runtime</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.yaml</groupId>
+            <artifactId>snakeyaml</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework</groupId>
+            <artifactId>jersey-test-framework-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+            <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+        </dependency>
+    </dependencies>
+</project>
\ No newline at end of file
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/JobServer.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/JobServer.java
new file mode 100644
index 0000000..298081e
--- /dev/null
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/JobServer.java
@@ -0,0 +1,165 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.jobserver;
+
+
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.util.thread.ThreadPool;
+import org.glassfish.jersey.servlet.ServletContainer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.submarine.commons.utils.SubmarineConfiguration;
+
+
+/**
+ * The ml job server. It will load the classes in rest package when
+ * bootstrap with related configurable settings.
+ * */
+public class JobServer {
+
+  private static final Logger LOG = LoggerFactory.getLogger(JobServer.class);
+
+  private SubmarineConfiguration conf = SubmarineConfiguration.create();
+
+  private Server jobServer;
+
+  public void start() {
+    ServletContextHandler context = new
+        ServletContextHandler(ServletContextHandler.SESSIONS);
+    context.setContextPath("/");
+    setupServer(conf);;
+    jobServer.setHandler(context);
+
+    // Job API servlet
+    ServletHolder apiServlet = context.addServlet(ServletContainer.class,
+        conf.getJobServerUrlPrefix());
+    apiServlet.setInitOrder(1);
+    apiServlet.setInitParameter("jersey.config.server.provider.packages",
+        "org.apache.submarine.jobserver.rest");
+
+    try {
+      jobServer.start();
+      LOG.info("Submarine job server started");
+      jobServer.join();
+    } catch (Exception e) {
+      LOG.error("Submarine job server failed to start");
+      e.printStackTrace();
+    } finally {
+      jobServer.destroy();
+      LOG.info("Submarine job server stopped");
+    }
+  }
+
+  public static void main(String[] args) throws Exception {
+    new JobServer().start();
+  }
+
+  private Server setupServer(SubmarineConfiguration conf) {
+    ThreadPool threadPool =
+        new QueuedThreadPool(
+            conf.getInt(SubmarineConfiguration
+                .ConfVars.SERVER_JETTY_THREAD_POOL_MAX),
+            conf.getInt(SubmarineConfiguration
+                .ConfVars.SERVER_JETTY_THREAD_POOL_MIN),
+            conf.getInt(SubmarineConfiguration
+                .ConfVars.SERVER_JETTY_THREAD_POOL_TIMEOUT));
+    jobServer = new Server(threadPool);
+    ServerConnector connector;
+
+    if (conf.isJobServerSslEnabled()) {
+      LOG.debug("Enabling SSL for submarine job server on port "
+          + conf.getServerSslPort());
+      HttpConfiguration httpConfig = new HttpConfiguration();
+      httpConfig.setSecureScheme("https");
+      httpConfig.setSecurePort(conf.getServerSslPort());
+      httpConfig.setOutputBufferSize(32768);
+      httpConfig.setResponseHeaderSize(8192);
+      httpConfig.setSendServerVersion(true);
+
+      HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig);
+      SecureRequestCustomizer src = new SecureRequestCustomizer();
+      httpsConfig.addCustomizer(src);
+
+      connector = new ServerConnector(
+          jobServer,
+          new SslConnectionFactory(getSslContextFactory(conf),
+              HttpVersion.HTTP_1_1.asString()),
+          new HttpConnectionFactory(httpsConfig));
+    } else {
+      connector = new ServerConnector(jobServer);
+    }
+
+    configureRequestHeaderSize(conf, connector);
+    // Set some timeout options to make debugging easier.
+    int timeout = 1000 * 30;
+    connector.setIdleTimeout(timeout);
+    connector.setSoLingerTime(-1);
+    connector.setHost(conf.getJobServerAddress());
+    if (conf.useSsl()) {
+      connector.setPort(conf.getJobServerSslPort());
+    } else {
+      connector.setPort(conf.getJobServerPort());
+    }
+
+    jobServer.addConnector(connector);
+    return jobServer;
+  }
+
+  private static SslContextFactory getSslContextFactory(
+      SubmarineConfiguration conf) {
+    SslContextFactory sslContextFactory = new SslContextFactory();
+
+    // Set keystore
+    sslContextFactory.setKeyStorePath(conf.getKeyStorePath());
+    sslContextFactory.setKeyStoreType(conf.getKeyStoreType());
+    sslContextFactory.setKeyStorePassword(conf.getKeyStorePassword());
+    sslContextFactory.setKeyManagerPassword(conf.getKeyManagerPassword());
+
+    if (conf.useClientAuth()) {
+      sslContextFactory.setNeedClientAuth(conf.useClientAuth());
+
+      // Set truststore
+      sslContextFactory.setTrustStorePath(conf.getTrustStorePath());
+      sslContextFactory.setTrustStoreType(conf.getTrustStoreType());
+      sslContextFactory.setTrustStorePassword(conf.getTrustStorePassword());
+    }
+
+    return sslContextFactory;
+  }
+
+  private static void configureRequestHeaderSize(
+      SubmarineConfiguration conf, ServerConnector connector) {
+    HttpConnectionFactory cf =
+        (HttpConnectionFactory) connector
+            .getConnectionFactory(HttpVersion.HTTP_1_1.toString());
+    int requestHeaderSize = conf.getJettyRequestHeaderSize();
+    cf.getHttpConfiguration().setRequestHeaderSize(requestHeaderSize);
+  }
+
+}
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/api/JobApi.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/api/JobApi.java
new file mode 100644
index 0000000..8b842f4
--- /dev/null
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/api/JobApi.java
@@ -0,0 +1,93 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.jobserver.rest.api;
+
+import org.apache.submarine.jobserver.rest.dao.JsonResponse;
+import org.apache.submarine.jobserver.rest.dao.MLJobSpec;
+import org.apache.submarine.jobserver.rest.dao.RestConstants;
+
+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.core.MediaType;
+import javax.ws.rs.core.Response;
+
+/**
+ * ML job rest API v1. It can accept MLJobSpec to create a job.
+ * To create a job:
+ * POST    /api/v1/jobs
+ *
+ * To list the jobs
+ * GET     /api/v1/jobs
+ *
+ * To get a specific job
+ * GET     /api/v1/jobs/{id}
+ *
+ * To delete a job by id
+ * DELETE  /api/v1/jobs/{id}
+ * */
+@Path(RestConstants.V1 + "/" + RestConstants.JOBS)
+@Produces({MediaType.APPLICATION_JSON + "; " + RestConstants.CHARSET_UTF8})
+public class JobApi {
+
+  // A ping test to verify the job server is up.
+  @Path(RestConstants.PING)
+  @GET
+  @Consumes(MediaType.APPLICATION_JSON)
+  public Response ping() {
+    return new JsonResponse.Builder<String>(Response.Status.OK)
+        .success(true).result("Pong").build();
+  }
+
+  @POST
+  @Consumes({RestConstants.MEDIA_TYPE_YAML, MediaType.APPLICATION_JSON})
+  public Response submitJob(MLJobSpec jobSpec) {
+    // Submit the job spec through submitter
+    return new JsonResponse.Builder<MLJobSpec>(Response.Status.ACCEPTED)
+        .success(true).result(jobSpec).build();
+  }
+
+  @GET
+  @Path("{" + RestConstants.JOB_ID + "}")
+  public Response listJob(@PathParam(RestConstants.JOB_ID) String id) {
+    // Query the job status though submitter
+    return new JsonResponse.Builder<MLJobSpec>(Response.Status.OK)
+        .success(true).result(id).build();
+  }
+
+  @GET
+  public Response listAllJob() {
+    // Query all the job status though submitter
+    return new JsonResponse.Builder<MLJobSpec>(Response.Status.OK)
+        .success(true).build();
+  }
+
+  @DELETE
+  @Path("{" + RestConstants.JOB_ID + "}")
+  public Response deleteJob(@PathParam(RestConstants.JOB_ID) String id) {
+    // Delete the job though submitter
+    return new JsonResponse.Builder<MLJobSpec>(Response.Status.OK)
+        .success(true).result(id).build();
+  }
+
+}
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/dao/Component.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/dao/Component.java
new file mode 100644
index 0000000..a2c1458
--- /dev/null
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/dao/Component.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.jobserver.rest.dao;
+
+
+/**
+ * One component contains role, count and resources.
+ * The role name could be tensorflow ps, Pytorch master or tensorflow worker
+ * The count is the count of the role instance
+ * The resource is the memory/vcore/gpu resource strings in the format:
+ * "memory=2048M,vcore=4,nvidia.com/gpu=1"
+ */
+
+public class Component {
+
+  public String getRole() {
+    return role;
+  }
+
+  public void setRole(String role) {
+    this.role = role;
+  }
+
+  public String getCount() {
+    return count;
+  }
+
+  public void setCount(String count) {
+    this.count = count;
+  }
+
+  public String getResources() {
+    return resources;
+  }
+
+  public void setResources(String resources) {
+    this.resources = resources;
+  }
+
+  String role;
+  String count;
+  String resources;
+
+  public Component() {}
+
+  public Component(String r, String ct, String res) {
+    this.count = ct;
+    this.role = r;
+    this.resources = res;
+  }
+
+}
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/dao/EnvVaraible.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/dao/EnvVaraible.java
new file mode 100644
index 0000000..24a4ae2
--- /dev/null
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/dao/EnvVaraible.java
@@ -0,0 +1,53 @@
+/*
+ * 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.jobserver.rest.dao;
+
+// A process level environment variable.
+public class EnvVaraible {
+
+  public String getKey() {
+    return key;
+  }
+
+  public void setKey(String key) {
+    this.key = key;
+  }
+
+  public String getValue() {
+    return value;
+  }
+
+  public void setValue(String value) {
+    this.value = value;
+  }
+
+  String key;
+  String value;
+
+  public EnvVaraible() {}
+
+  public EnvVaraible(String k, String v) {
+    this.key = k;
+    this.value = v;
+  }
+
+
+
+}
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/dao/JsonExclusionStrategy.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/dao/JsonExclusionStrategy.java
new file mode 100644
index 0000000..c24e413
--- /dev/null
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/dao/JsonExclusionStrategy.java
@@ -0,0 +1,33 @@
+/*
+ * 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.jobserver.rest.dao;
+
+import com.google.gson.ExclusionStrategy;
+import com.google.gson.FieldAttributes;
+
+public class JsonExclusionStrategy implements ExclusionStrategy {
+  public boolean shouldSkipClass(Class<?> arg0) {
+    return false;
+  }
+
+  public boolean shouldSkipField(FieldAttributes f) {
+    return false;
+  }
+}
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/dao/JsonResponse.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/dao/JsonResponse.java
new file mode 100644
index 0000000..46726cf
--- /dev/null
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/dao/JsonResponse.java
@@ -0,0 +1,217 @@
+/*
+ * 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.jobserver.rest.dao;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.TypeAdapter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.core.NewCookie;
+import javax.ws.rs.core.Response;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Json response builder.
+ *
+ * @param <T> can be an object or a ListResult
+ */
+public class JsonResponse<T> {
+  private static final Logger LOG = 
LoggerFactory.getLogger(JsonResponse.class);
+
+  private final javax.ws.rs.core.Response.Status status;
+  private final int code;
+  private final Boolean success;
+  private final String message;
+  private final T result;
+  private final transient ArrayList<NewCookie> cookies;
+  private final transient boolean pretty = false;
+  private final Map<String, Object> attributes;
+
+  private static Gson safeGson = null;
+
+  private static final String CGLIB_PROPERTY_PREFIX = "\\$cglib_prop_";
+
+  private JsonResponse(Builder builder) {
+    this.status = builder.status;
+    this.code = builder.code;
+    this.success = builder.success;
+    this.message = builder.message;
+    this.attributes = builder.attributes;
+    this.result = (T) builder.result;
+    this.cookies = builder.cookies;
+  }
+
+  public T getResult() {
+    return result;
+  }
+
+  public Boolean getSuccess() {
+    return success;
+  }
+
+  @VisibleForTesting
+  public Map<String, Object> getAttributes() {
+    return attributes;
+  }
+
+  public static class Builder<T> {
+    private javax.ws.rs.core.Response.Status status;
+    private int code;
+    private Boolean success;
+    private String message;
+    private T result;
+    private Map<String, Object> attributes = new HashMap<>();
+    private transient ArrayList<NewCookie> cookies;
+    private transient boolean pretty = false;
+
+    public Builder(javax.ws.rs.core.Response.Status status) {
+      this.status = status;
+      this.code = status.getStatusCode();
+    }
+
+    public Builder(int code) {
+      this.code = code;
+    }
+
+    public Builder attribute(String key, Object value) {
+      this.attributes.put(key, value);
+      return this;
+    }
+
+    public Builder success(Boolean success) {
+      this.success = success;
+      return this;
+    }
+
+    public Builder message(String message) {
+      this.message = message;
+      return this;
+    }
+
+    public Builder result(T result) {
+      this.result = result;
+      return this;
+    }
+
+    public Builder code(int code) {
+      this.code = code;
+      return this;
+    }
+
+    public Builder cookies(ArrayList<NewCookie> newCookies) {
+      if (cookies == null) {
+        cookies = new ArrayList<>();
+      }
+      cookies.addAll(newCookies);
+      return this;
+    }
+
+    public javax.ws.rs.core.Response build() {
+      JsonResponse jsonResponse = new JsonResponse(this);
+      return jsonResponse.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    if (safeGson == null) {
+      GsonBuilder gsonBuilder = new GsonBuilder();
+      if (pretty) {
+        gsonBuilder.setPrettyPrinting();
+      }
+      gsonBuilder.setExclusionStrategies(new JsonExclusionStrategy());
+
+      // Trick to get the DefaultDateTypeAdatpter instance
+      // Create a first instance a Gson
+      Gson gson = gsonBuilder.setDateFormat("yyyy-MM-dd HH:mm:ss").create();
+
+      // Get the date adapter
+      TypeAdapter<Date> dateTypeAdapter = gson.getAdapter(Date.class);
+
+      // Ensure the DateTypeAdapter is null safe
+      TypeAdapter<Date> safeDateTypeAdapter = dateTypeAdapter.nullSafe();
+
+      safeGson = new GsonBuilder()
+          .registerTypeAdapter(Date.class, safeDateTypeAdapter)
+          .serializeNulls().create();
+    }
+
+//    boolean haveDictAnnotation = false;
+//    try {
+//      if (null != getResult()) {
+//        haveDictAnnotation = DictAnnotation.parseDictAnnotation(getResult());
+//      }
+//    } catch (Exception e) {
+//      LOG.error(e.getMessage(), e);
+//    }
+
+    String json = safeGson.toJson(this);
+//    if (haveDictAnnotation) {
+//      json = json.replaceAll(CGLIB_PROPERTY_PREFIX, "");
+//    }
+
+    return json;
+  }
+
+  private synchronized javax.ws.rs.core.Response build() {
+    Response.ResponseBuilder r = 
javax.ws.rs.core.Response.status(status).entity(this.toString());
+    if (cookies != null) {
+      for (NewCookie nc : cookies) {
+        r.cookie(nc);
+      }
+    }
+    return r.build();
+  }
+
+  // list result type response
+  // Used to return a list of records
+  public static class ListResult<T> {
+    private List<T> records;
+    private long total;
+
+    public ListResult(List<T> records, long total) {
+      this.records = records;
+      this.total = total;
+    }
+
+    public List<T> getRecords() {
+      return records;
+    }
+
+    public void setRecords(List<T> records) {
+      this.records = records;
+    }
+
+    public long getTotal() {
+      return total;
+    }
+
+    public void setTotal(long total) {
+      this.total = total;
+    }
+  }
+}
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/dao/MLJobSpec.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/dao/MLJobSpec.java
new file mode 100644
index 0000000..26f7fa8
--- /dev/null
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/dao/MLJobSpec.java
@@ -0,0 +1,177 @@
+/*
+ * 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.jobserver.rest.dao;
+
+/**
+ * The machine learning job spec the submarine job server can accept.
+ * */
+public class MLJobSpec {
+
+  String apiVersion;
+  // The engine type the job will use, can be tensorflow or pytorch
+  String type;
+  // The engine version, not image version/tag. For instance, tensorflow v1.13
+  String version;
+  /**
+   * Advanced property. The RM this job will submit to, k8s or yarn.
+   * Could be the path of yarn’s config file or k8s kubeconfig file
+   * This should be settled when deploy submarine job server.
+   */
+  String rmConfig;
+
+  /**
+   * Advanced property. The image should be inferred from type and version.
+   * The normal user should not set this.
+   * */
+  String dockerImage;
+
+  // The process aware environment variable
+  EnvVaraible[] envVars;
+
+  // The components this cluster will consists.
+  Component[] components;
+
+  // The user id who submit job
+  String uid;
+
+  /**
+   * The queue this job will submitted to.
+   * In YARN, we call it queue. In k8s, no such concept.
+   * It could be namespace’s name.
+   */
+  String queue;
+
+  /**
+   * The user-specified job name for easy search
+   * */
+  String name;
+
+  /**
+   * This could be file/directory which contains multiple python scripts.
+   * We should solve dependencies distribution in k8s or yarn.
+   * Or we could build images for each projects before submitting the job
+   * */
+  String projects;
+
+  /**
+   * The command uses to start the job. This is very job-specific.
+   * We assume the cmd is the same for all containers in a cluster
+   * */
+  String cmd;
+
+  public MLJobSpec() {}
+
+  public String getUid() {
+    return uid;
+  }
+
+  public void setUid(String uid) {
+    this.uid = uid;
+  }
+
+  public String getQueue() {
+    return queue;
+  }
+
+  public void setQueue(String queue) {
+    this.queue = queue;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public String getProjects() {
+    return projects;
+  }
+
+  public void setProjects(String projects) {
+    this.projects = projects;
+  }
+  public String getCmd() {
+    return cmd;
+  }
+
+  public void setCmd(String cmd) {
+    this.cmd = cmd;
+  }
+
+  public Component[] getComponents() {
+    return components;
+  }
+
+  public void setComponents(
+      Component[] components) {
+    this.components = components;
+  }
+  public String getType() {
+    return type;
+  }
+
+  public void setType(String type) {
+    this.type = type;
+  }
+
+  public String getVersion() {
+    return version;
+  }
+
+  public void setVersion(String version) {
+    this.version = version;
+  }
+
+  public String getRmConfig() {
+    return rmConfig;
+  }
+
+  public void setRmConfig(String rmConfig) {
+    this.rmConfig = rmConfig;
+  }
+
+  public String getDockerImage() {
+    return dockerImage;
+  }
+
+  public void setDockerImage(String dockerImage) {
+    this.dockerImage = dockerImage;
+  }
+
+  public EnvVaraible[] getEnvVars() {
+    return envVars;
+  }
+
+  public void setEnvVars(EnvVaraible[] envVars) {
+    this.envVars = envVars;
+  }
+
+  public String getApiVersion() {
+    return apiVersion;
+  }
+
+  public void setApiVersion(String apiVersion) {
+    this.apiVersion = apiVersion;
+  }
+
+
+}
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/dao/RestConstants.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/dao/RestConstants.java
new file mode 100644
index 0000000..a33a931
--- /dev/null
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/dao/RestConstants.java
@@ -0,0 +1,29 @@
+/*
+ * 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.jobserver.rest.dao;
+
+public class RestConstants {
+  public final static String V1 = "v1";
+  public final static String JOBS = "jobs";
+  public final static String JOB_ID = "id";
+  public final static String PING = "ping";
+  public final static String MEDIA_TYPE_YAML = "application/yaml";
+  public final static String CHARSET_UTF8 = "charset=utf-8";
+}
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/provider/YamlEntityProvider.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/provider/YamlEntityProvider.java
new file mode 100644
index 0000000..3343cc8
--- /dev/null
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/jobserver/rest/provider/YamlEntityProvider.java
@@ -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.
+ */
+
+package org.apache.submarine.jobserver.rest.provider;
+
+import org.yaml.snakeyaml.Yaml;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyReader;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.Scanner;
+
+@Provider
+@Consumes({"application/yaml", MediaType.TEXT_PLAIN})
+@Produces({"application/yaml", MediaType.TEXT_PLAIN})
+public class YamlEntityProvider<T> implements MessageBodyWriter<T>, 
MessageBodyReader<T> {
+
+  @Override
+  public boolean isReadable(Class<?> type, Type genericType,
+      Annotation[] annotations,
+      MediaType mediaType) {
+    return true;
+  }
+
+  @Override
+  public T readFrom(Class<T> type, Type genericType, Annotation[] annotations,
+      MediaType mediaType,
+      MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
+      throws WebApplicationException {
+    Yaml yaml = new Yaml();
+    T t = yaml.loadAs(toString(entityStream), type);
+    return t;
+  }
+
+  public static String toString(InputStream inputStream) {
+    return new Scanner(inputStream, "UTF-8")
+        .useDelimiter("\\A").next();
+  }
+
+  @Override
+  public boolean isWriteable(Class<?> type, Type genericType,
+      Annotation[] annotations,
+      MediaType mediaType) {
+    return true;
+  }
+
+  @Override
+  public long getSize(T t, Class<?> type, Type genericType,
+      Annotation[] annotations,
+      MediaType mediaType) {
+    return -1;
+  }
+
+  @Override
+  public void writeTo(T t, Class<?> type, Type genericType,
+      Annotation[] annotations,
+      MediaType mediaType, MultivaluedMap<String, Object> httpHeaders,
+      OutputStream entityStream) throws IOException, WebApplicationException {
+    Yaml yaml = new Yaml();
+    OutputStreamWriter writer = new OutputStreamWriter(entityStream);
+    yaml.dump(t, writer);
+    writer.close();
+  }
+}
diff --git a/submarine-server/server-core/src/main/resources/log4j.properties 
b/submarine-server/server-core/src/main/resources/log4j.properties
new file mode 100644
index 0000000..55e02b6
--- /dev/null
+++ b/submarine-server/server-core/src/main/resources/log4j.properties
@@ -0,0 +1,17 @@
+# Licensed 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. See accompanying LICENSE file.
+log4j.rootLogger = info, stdout
+
+log4j.appender.stdout = org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.Target = System.out
+log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd 
HH:mm:ss,SSS} method:%l%n%m%n
diff --git a/submarine-server/server-core/src/test/java/JobApiTest.java 
b/submarine-server/server-core/src/test/java/JobApiTest.java
new file mode 100644
index 0000000..6a7ef12
--- /dev/null
+++ b/submarine-server/server-core/src/test/java/JobApiTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.
+ */
+
+import com.google.gson.Gson;
+import org.apache.submarine.jobserver.rest.dao.JsonResponse;
+import org.apache.submarine.jobserver.rest.dao.RestConstants;
+import org.apache.submarine.jobserver.rest.api.JobApi;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.Test;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import static org.junit.Assert.assertEquals;
+
+public class JobApiTest extends JerseyTest {
+
+  @Override
+  protected Application configure() {
+    return new ResourceConfig(JobApi.class);
+  }
+
+  @Test
+  public void testJobServerPing() {
+    String str = "Pong";
+    Response response = target(RestConstants.V1 + "/"
+        + RestConstants.JOBS + "/" + RestConstants.PING)
+        .request()
+        .get();
+    Gson gson = new Gson();
+    JsonResponse r = gson.fromJson(response.readEntity(String.class),
+        JsonResponse.class);
+    assertEquals("Response code should be 200 ",
+        Response.Status.OK.getStatusCode(), response.getStatus());
+    assertEquals("Response message should be " + str,
+        str, r.getResult().toString());
+  }
+
+  // Test job created with correct JSON input
+  @Test
+  public void testCreateJobWhenJsonInputIsCorrectThenResponseCodeAccepted() {
+    String jobSpec = "{\"type\": \"tensorflow\", \"version\":\"v1.13\"}";
+    Response response = target(RestConstants.V1 + "/" + RestConstants.JOBS)
+        .request()
+        .post(Entity.entity(jobSpec, MediaType.APPLICATION_JSON));
+
+    assertEquals("Http Response should be 202 ",
+        Response.Status.ACCEPTED.getStatusCode(), response.getStatus());
+  }
+
+  // Test job created with incorrect JSON input
+  @Test
+  public void testCreateJobWhenJsonInputIsWrongThenResponseCodeBadRequest() {
+    String jobSpec = "{\"ttype\": \"tensorflow\", \"version\":\"v1.13\"}";
+    Response response = target(RestConstants.V1 + "/" + RestConstants.JOBS)
+        .request()
+        .post(Entity.entity(jobSpec, MediaType.APPLICATION_JSON));
+
+    assertEquals("Http Response should be 400 ",
+        Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+  }
+
+  // Test get job list
+  @Test
+  public void testGetJobList() {
+    Response response = target(RestConstants.V1 + "/" + RestConstants.JOBS)
+        .request()
+        .get();
+
+    assertEquals("Http Response should be 200 ",
+        Response.Status.OK.getStatusCode(), response.getStatus());
+  }
+
+  // Test get job by id
+  @Test
+  public void testGetJobById() {
+    String jobId = "job1";
+    Response response = target(RestConstants.V1 + "/"
+        + RestConstants.JOBS + "/" + jobId)
+        .request()
+        .get();
+    Gson gson = new Gson();
+    JsonResponse r = gson.fromJson(response.readEntity(String.class),
+        JsonResponse.class);
+    assertEquals("Http Response should be 200 ",
+        Response.Status.OK.getStatusCode(), response.getStatus());
+    assertEquals("Job id should be " + jobId,
+        jobId, r.getResult().toString());
+  }
+
+  // Test delete job by id
+  @Test
+  public void testDeleteJobById() {
+    String jobId = "job1";
+    Response response = target(RestConstants.V1 + "/"
+        + RestConstants.JOBS + "/" + jobId)
+        .request()
+        .delete();
+    Gson gson = new Gson();
+    JsonResponse r = gson.fromJson(response.readEntity(String.class),
+        JsonResponse.class);
+    assertEquals("Http Response should be 200 ",
+        Response.Status.OK.getStatusCode(), response.getStatus());
+    assertEquals("Deleted job id should be " + jobId,
+        jobId, r.getResult().toString());
+  }
+
+  /**
+   * FiXME. The manual YAML test with postman works but failed here.
+   * We need to figure out why the YAML entity provider not work in this test.
+   * */
+  @Test
+  public void testCreateJobWhenYamlInputIsCorrectThenResponseCodeAccepted() {
+//    Client client = ClientBuilder.newBuilder()
+//        .register(new YamlEntityProvider<>()).build();
+//    this.setClient(client);
+//    String jobSpec = "type: tf";
+//    Response response = target(RestConstants.V1 + "/"
+//        + RestConstants.JOBS + "/" + "test")
+//        .request()
+//        .put(Entity.entity(jobSpec, "application/yaml"));
+//
+//    assertEquals("Http Response should be 202 ",
+//        Response.Status.ACCEPTED.getStatusCode(), response.getStatus());
+  }
+
+}


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

Reply via email to