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 517083e  SUBMARINE-559. [API] Define Swagger API for pre-defined 
template registration/delete, etc.
517083e is described below

commit 517083e79f0bbe723a585831faf74f45aeaee8ed
Author: JohnTing <[email protected]>
AuthorDate: Wed Aug 12 19:18:23 2020 +0800

    SUBMARINE-559. [API] Define Swagger API for pre-defined template 
registration/delete, etc.
    
    ### What is this PR for?
    
    Make basic rest api for management experiment template.
    
    get, post, del, patch
    http://localhost/V1/template
    
    ### What type of PR is it?
    [Bug Fix | Improvement | Feature | Documentation | Hot Fix | Refactoring]
    
    ### Todos
    * [x] - REST API
    * [x] - test
    * [x] - e2e test
    * [x] - parameter Mapping
    
    ### What is the Jira issue?
    https://issues.apache.org/jira/projects/SUBMARINE/issues/SUBMARINE-559
    
    ### How should this be tested?
    * First time? Setup Travis CI as described on 
https://submarine.apache.org/contribution/contributions.html#continuous-integration
    * Strongly recommended: add automated unit tests for any new or changed 
behavior
    * Outline any manual steps to test the PR here.
    
    ### Screenshots (if appropriate)
    
![image](https://user-images.githubusercontent.com/19265751/89300101-46d6a080-d69a-11ea-92b3-795387340313.png)
    
![image](https://user-images.githubusercontent.com/19265751/89300112-4ccc8180-d69a-11ea-825a-782558f91aab.png)
    
![image](https://user-images.githubusercontent.com/19265751/89300145-581fad00-d69a-11ea-9027-032d5fd3dd81.png)
    
![image](https://user-images.githubusercontent.com/19265751/89300186-6372d880-d69a-11ea-97aa-fcf7d015db88.png)
    
    
![image](https://user-images.githubusercontent.com/19265751/89812094-2dd65f80-db72-11ea-9e14-50f86cf35c94.png)
    
![image](https://user-images.githubusercontent.com/19265751/89812153-434b8980-db72-11ea-9233-a1919940a368.png)
    
    ### Questions:
    * Does the licenses files need update? Yes/No
    * Is there breaking changes for older versions? Yes/No
    * Does this needs documentation? Yes/No
    
    Author: JohnTing <[email protected]>
    
    Closes #351 from JohnTing/SUBMARINE-559 and squashes the following commits:
    
    6615be9 [JohnTing] chnge the name contained in the data
    1c77b2b [JohnTing] Merge branch 'master' into SUBMARINE-559
    3064b1e [JohnTing] done
    096fe10 [JohnTing] parameterMapping
    126cba7 [JohnTing] test2
    6e53898 [JohnTing] test
    2ac1b70 [JohnTing] submarine-559
    f54a8e7 [JohnTing] add gitignore
---
 .gitignore                                         |   3 +
 docs/database/submarine-data.sql                   |   6 +
 docs/database/submarine.sql                        |  17 ++
 .../api/experimenttemplate/ExperimentTemplate.java |  45 +++
 .../experimenttemplate/ExperimentTemplateId.java   |  72 +++++
 .../api/spec/ExperimentTemplateParamSpec.java      |  60 ++++
 .../server/api/spec/ExperimentTemplateSpec.java    |  70 +++++
 .../ExperimentTemplateManager.java                 | 329 +++++++++++++++++++++
 .../database/entity/ExperimentTemplateEntity.java  |  46 +++
 .../database/mappers/ExperimentTemplateMapper.java |  39 +++
 .../server/rest/ExperimentTemplateRestApi.java     | 194 ++++++++++++
 .../submarine/server/rest/RestConstants.java       |   7 +
 .../src/main/resources/mybatis-config.xml          |   1 +
 .../database/mappers/ExperimentTemplateMapper.xml  |  87 ++++++
 .../server/rest/ExperimentTemplateRestApiTest.java | 144 +++++++++
 .../experimenttemplate/test_template_1.json        |  43 +++
 .../experimenttemplate/test_template_2.json        |  43 +++
 .../rest/ExperimentTemplateManagerRestApiIT.java   | 149 ++++++++++
 .../workbench-web-ng/src/WEB-INF/web.xml           |   3 +-
 19 files changed, 1357 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index 996243c..96e589b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -95,6 +95,9 @@ submarine-security/spark-security/derby.log
 .classpath
 .settings
 .factorypath
+.vscode/settings.json
+.vscode/tasks.json
 
 # jupyter notebook checkpoints
 .ipynb_checkpoints
+
diff --git a/docs/database/submarine-data.sql b/docs/database/submarine-data.sql
index f63b581..1796638 100644
--- a/docs/database/submarine-data.sql
+++ b/docs/database/submarine-data.sql
@@ -85,3 +85,9 @@ INSERT INTO `params` (`id`, `key`, `value`, `worker_index`) 
VALUES
 -- Records of environment
 -- ----------------------------
 INSERT INTO `environment` VALUES ('environment_1595134205164_0002', 
'my-submarine-test-env','{"name":"my-submarine-env","dockerImage":"continuumio/anaconda3","kernelSpec":{"name":"team_default_python_3.7","channels":["defaults"],"dependencies":["_ipyw_jlab_nb_ext_conf=0.1.0=py37_0","alabaster=0.7.12=py37_0","anaconda=2020.02=py37_0","anaconda-client=1.7.2=py37_0","anaconda-navigator=1.9.12=py37_0"]}}','admin',
 '2020-05-06 14:00:05', 'Jack', '2020-05-06 14:00:14');
+
+-- ----------------------------
+-- Records of experiment_templates
+-- ----------------------------
+INSERT INTO `experiment_template` (`id`, `experimentTemplate_name`, 
`experimentTemplate_spec`, `create_by`, `create_time`, `update_by`, 
`update_time`) VALUES
+('experimentTemplate_1596391902149_0002', 'tf-mnist', '{\"name\": 
\"tf-mnist\", \"author\": \"author0\", \"parameters\": [{\"name\": 
\"input.train_data\", \"required\": \"true\", \"description\": \"train data is 
expected in SVM format, and can be stored in HDFS/S3 \\n\"}, {\"name\": 
\"training.batch_size\", \"value\": \"150\", \"required\": \"false\", 
\"description\": \"This is batch size of training\"}], \"description\": \"This 
is a template to run tf-mnist\\n\", \"experimentSpec\": {\" [...]
diff --git a/docs/database/submarine.sql b/docs/database/submarine.sql
index d9ffa6f..76dfc49 100644
--- a/docs/database/submarine.sql
+++ b/docs/database/submarine.sql
@@ -262,3 +262,20 @@ CREATE TABLE `params` (
   `worker_index` varchar(32) NOT NULL COMMENT '`String` (limit 32 characters). 
Part of *Primary Key* for\r\n    ``metrics`` table.',
   PRIMARY KEY  (`id`, `key`, `worker_index`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+
+-- ----------------------------
+-- Table structure for experiment_templates
+-- ----------------------------
+DROP TABLE IF EXISTS `experiment_template`;
+CREATE TABLE `experiment_template` (
+  `id` varchar(64) NOT NULL,
+  `experimentTemplate_name` varchar(32) NOT NULL,
+  `experimentTemplate_spec` json DEFAULT NULL,
+  `create_by` varchar(32) DEFAULT NULL,
+  `create_time` datetime NOT NULL,
+  `update_by` varchar(32) DEFAULT NULL,
+  `update_time` datetime NOT NULL,
+  PRIMARY KEY `id` (`id`),
+   UNIQUE KEY `experimentTemplate_name` (`experimentTemplate_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
diff --git 
a/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/experimenttemplate/ExperimentTemplate.java
 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/experimenttemplate/ExperimentTemplate.java
new file mode 100644
index 0000000..3fb8b39
--- /dev/null
+++ 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/experimenttemplate/ExperimentTemplate.java
@@ -0,0 +1,45 @@
+/*
+ * 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.experimenttemplate;
+
+import org.apache.submarine.server.api.spec.ExperimentTemplateSpec;
+
+public class ExperimentTemplate {
+
+  private ExperimentTemplateId experimentTemplateId;
+
+  private ExperimentTemplateSpec experimentTemplateSpec;
+  
+  public ExperimentTemplateId getExperimentTemplateId() {
+    return this.experimentTemplateId;
+  }
+
+  public void setExperimentTemplateId(ExperimentTemplateId 
experimentTemplateId) {
+    this.experimentTemplateId = experimentTemplateId;
+  }
+
+  public ExperimentTemplateSpec getExperimentTemplateSpec() {
+    return this.experimentTemplateSpec;
+  }
+
+  public void setExperimentTemplateSpec(ExperimentTemplateSpec 
experimentTemplateSpec) {
+    this.experimentTemplateSpec = experimentTemplateSpec;
+  }
+}
diff --git 
a/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/experimenttemplate/ExperimentTemplateId.java
 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/experimenttemplate/ExperimentTemplateId.java
new file mode 100644
index 0000000..2507a7e
--- /dev/null
+++ 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/experimenttemplate/ExperimentTemplateId.java
@@ -0,0 +1,72 @@
+/*
+ * 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.experimenttemplate;
+
+import org.apache.submarine.commons.utils.AbstractUniqueIdGenerator;
+
+/**
+ * The unique id for experimentTemplate. Formatter:
+ * experimentTemplate_${server_timestamp}_${counter} Such as:
+ * experimentTemplate_1577627710_0001
+ */
+public class ExperimentTemplateId extends 
AbstractUniqueIdGenerator<ExperimentTemplateId> {
+  
+  private static final String EXPERIMENT_ID_PREFIX = "experimentTemplate_";
+
+  /**
+   * Get the object of ExperimentTemplateId.
+   * 
+   * @param experimentTemplateId Id of the experimentTemplate string
+   * @return object
+   */
+  public static ExperimentTemplateId fromString(String experimentTemplateId) {
+    if (experimentTemplateId == null) {
+      return null;
+    }
+    String[] components = experimentTemplateId.split("\\_");
+    if (components.length != 3) {
+      return null;
+    }
+    return ExperimentTemplateId.newInstance(Long.parseLong(components[1]),
+        Integer.parseInt(components[2]));
+  }
+
+  /**
+   * Ge the object of ExperimentTemplateId.
+   * 
+   * @param serverTimestamp the timestamp when the server start
+   * @param id count
+   * @return object
+   */
+  public static ExperimentTemplateId newInstance(long serverTimestamp, int id) 
{
+    ExperimentTemplateId experimentId = new ExperimentTemplateId();
+    experimentId.setServerTimestamp(serverTimestamp);
+    experimentId.setId(id);
+    return experimentId;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder(64);
+    sb.append(EXPERIMENT_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/ExperimentTemplateParamSpec.java
 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/spec/ExperimentTemplateParamSpec.java
new file mode 100644
index 0000000..8a28c63
--- /dev/null
+++ 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/spec/ExperimentTemplateParamSpec.java
@@ -0,0 +1,60 @@
+/*
+ * 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 ExperimentTemplateParamSpec {
+  
+  private String name;
+  private String required;
+  private String description;
+  private String value;
+
+  public String getName() {
+    return this.name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public String getRequired() {
+    return this.required;
+  }
+
+  public void setRequired(String required) {
+    this.required = required;
+  }
+
+  public String getDescription() {
+    return this.description;
+  }
+
+  public void setDescription(String description) {
+    this.description = description;
+  }
+
+  public String getValue() {
+    return this.value;
+  }
+
+  public void setValue(String value) {
+    this.value = value;
+  }
+}
diff --git 
a/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/spec/ExperimentTemplateSpec.java
 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/spec/ExperimentTemplateSpec.java
new file mode 100644
index 0000000..8dc5b58
--- /dev/null
+++ 
b/submarine-server/server-api/src/main/java/org/apache/submarine/server/api/spec/ExperimentTemplateSpec.java
@@ -0,0 +1,70 @@
+/*
+ * 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.List;
+
+public class ExperimentTemplateSpec {
+  private String name;
+  private String author;
+  private String description;
+  private List<ExperimentTemplateParamSpec> parameters;
+  private ExperimentSpec experimentSpec;
+
+  public ExperimentSpec getExperimentSpec() {
+    return this.experimentSpec;
+  }
+
+  public void setExperimentSpec(ExperimentSpec experimentSpec) {
+    this.experimentSpec = experimentSpec;
+  }
+  
+  public String getName() {
+    return this.name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public String getAuthor() {
+    return this.author;
+  }
+
+  public void setAuthor(String author) {
+    this.author = author;
+  }
+
+  public String getDescription() {
+    return this.description;
+  }
+
+  public void setDescription(String description) {
+    this.description = description;
+  }
+
+  public List<ExperimentTemplateParamSpec> getParameters() {
+    return this.parameters;
+  }
+
+  public void setParameters(List<ExperimentTemplateParamSpec> parameters) {
+    this.parameters = parameters;
+  }
+}
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/experimenttemplate/ExperimentTemplateManager.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/experimenttemplate/ExperimentTemplateManager.java
new file mode 100644
index 0000000..feac673
--- /dev/null
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/experimenttemplate/ExperimentTemplateManager.java
@@ -0,0 +1,329 @@
+/*
+ * 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.experimenttemplate;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.ws.rs.core.Response.Status;
+
+import org.apache.ibatis.session.SqlSession;
+import org.apache.submarine.commons.utils.exception.SubmarineRuntimeException;
+import org.apache.submarine.server.SubmarineServer;
+import org.apache.submarine.server.api.experimenttemplate.ExperimentTemplate;
+import org.apache.submarine.server.api.experimenttemplate.ExperimentTemplateId;
+import org.apache.submarine.server.api.spec.ExperimentTemplateParamSpec;
+import org.apache.submarine.server.api.spec.ExperimentTemplateSpec;
+import org.apache.submarine.server.database.utils.MyBatisUtil;
+import 
org.apache.submarine.server.experimenttemplate.database.entity.ExperimentTemplateEntity;
+import 
org.apache.submarine.server.experimenttemplate.database.mappers.ExperimentTemplateMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * ExperimentTemplate Management
+ */
+public class ExperimentTemplateManager {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(ExperimentTemplateManager.class);
+
+  private static volatile ExperimentTemplateManager manager;
+
+  private final AtomicInteger experimentTemplateIdCounter = new 
AtomicInteger(0);
+
+  /**
+   * ExperimentTemplate Cache
+   */
+  private final ConcurrentMap<String, ExperimentTemplate> 
cachedExperimentTemplates = 
+        new ConcurrentHashMap<>();
+
+  /**
+   * Get the singleton instance
+   * 
+   * @return object
+   */
+  public static ExperimentTemplateManager getInstance() {
+    if (manager == null) {
+      synchronized (ExperimentTemplateManager.class) {
+        if (manager == null) {
+          manager = new ExperimentTemplateManager();
+        }
+      }
+    }
+    return manager;
+  }
+
+  private ExperimentTemplateManager() {
+
+  }
+
+  /**
+   * Create ExperimentTemplate
+   * 
+   * @param spec experimentTemplate spec
+   * @return ExperimentTemplate experimentTemplate
+   * @throws SubmarineRuntimeException the service error
+   */
+  public ExperimentTemplate createExperimentTemplate(ExperimentTemplateSpec 
spec) 
+        throws SubmarineRuntimeException {
+    checkSpec(spec);
+    LOG.info("Create ExperimentTemplate using spec: " + spec.toString());
+    return createOrUpdateExperimentTemplate(spec, "c");
+  }
+
+  /**
+   * Update experimentTemplate
+   * 
+   * @param name Name of the experimentTemplate
+   * @param spec experimentTemplate spec
+   * @return ExperimentTemplate experimentTemplate
+   * @throws SubmarineRuntimeException the service error
+   */
+  public ExperimentTemplate updateExperimentTemplate(String name, 
ExperimentTemplateSpec spec)
+      throws SubmarineRuntimeException {
+    ExperimentTemplate tpl = getExperimentTemplateDetails(name);
+    if (tpl == null) {
+      throw new SubmarineRuntimeException(Status.NOT_FOUND.getStatusCode(), 
"ExperimentTemplate not found.");
+    }
+    checkSpec(spec);
+    LOG.info("Update ExperimentTemplate using spec: " + spec.toString());
+    return createOrUpdateExperimentTemplate(spec, "u");
+  }
+
+  private ExperimentTemplate 
createOrUpdateExperimentTemplate(ExperimentTemplateSpec spec, String operation) 
{
+    ExperimentTemplateEntity entity = new ExperimentTemplateEntity();
+    String experimentTemplateId = generateExperimentTemplateId().toString();
+    entity.setId(experimentTemplateId);
+    entity.setExperimentTemplateName(spec.getName());
+    entity.setExperimentTemplateSpec(new 
GsonBuilder().disableHtmlEscaping().create().toJson(spec));
+
+    parameterMapping(entity.getExperimentTemplateSpec());
+
+    try (SqlSession sqlSession = MyBatisUtil.getSqlSession()) {
+      ExperimentTemplateMapper experimentTemplateMapper = 
+            sqlSession.getMapper(ExperimentTemplateMapper.class);
+
+      if (operation.equals("c")) {
+        experimentTemplateMapper.insert(entity);
+      } else {
+        experimentTemplateMapper.update(entity);
+      }
+      sqlSession.commit();
+      
+      ExperimentTemplate experimentTemplate = new ExperimentTemplate();
+      
experimentTemplate.setExperimentTemplateId(ExperimentTemplateId.fromString(experimentTemplateId));
+      experimentTemplate.setExperimentTemplateSpec(spec);
+      
+      // Update cache
+      cachedExperimentTemplates.putIfAbsent(spec.getName(), 
experimentTemplate);
+
+      return experimentTemplate;
+    } catch (Exception e) {
+      LOG.error(e.getMessage(), e);
+      throw new SubmarineRuntimeException(Status.BAD_REQUEST.getStatusCode(),
+          "Unable to process the experimentTemplate spec.");
+    }
+  }
+
+  private ExperimentTemplateId generateExperimentTemplateId() {
+    return 
ExperimentTemplateId.newInstance(SubmarineServer.getServerTimeStamp(),
+        experimentTemplateIdCounter.incrementAndGet());
+  }
+
+  /**
+   * Delete experimentTemplate
+   * 
+   * @param name Name of the experimentTemplate
+   * @return ExperimentTemplate experimentTemplate
+   * @throws SubmarineRuntimeException the service error
+   */
+  public ExperimentTemplate deleteExperimentTemplate(String name) throws 
SubmarineRuntimeException {
+    ExperimentTemplate tpl = getExperimentTemplateDetails(name);
+    if (tpl == null) {
+      throw new SubmarineRuntimeException(Status.NOT_FOUND.getStatusCode(), 
"ExperimentTemplate not found.");
+    }
+
+    LOG.info("Delete ExperimentTemplate for " + name);
+    try (SqlSession sqlSession = MyBatisUtil.getSqlSession()) {
+      ExperimentTemplateMapper experimentTemplateMapper = 
+            sqlSession.getMapper(ExperimentTemplateMapper.class);
+
+      experimentTemplateMapper.delete(name);
+      sqlSession.commit();
+
+      // Invalidate cache
+      cachedExperimentTemplates.remove(name);
+      return tpl;
+    } catch (Exception e) {
+      LOG.error(e.getMessage(), e);
+      throw new SubmarineRuntimeException(Status.BAD_REQUEST.getStatusCode(),
+          "Unable to delete the experimentTemplate.");
+    }
+  }
+
+  /**
+   * Get ExperimentTemplate
+   * 
+   * @param name Name of the experimentTemplate
+   * @return ExperimentTemplate experimentTemplate
+   * @throws SubmarineRuntimeException the service error
+   */
+  public ExperimentTemplate getExperimentTemplate(String name) throws 
SubmarineRuntimeException {
+    ExperimentTemplate experimentTemplate = getExperimentTemplateDetails(name);
+    if (experimentTemplate == null) {
+      throw new SubmarineRuntimeException(Status.NOT_FOUND.getStatusCode(), 
"ExperimentTemplate not found.");
+    }
+    return experimentTemplate;
+  }
+
+  /**
+   * List experimentTemplates
+   * 
+   * @param status experimentTemplate status, if null will return all status
+   * @return experimentTemplate list
+   * @throws SubmarineRuntimeException the service error
+   */
+  public List<ExperimentTemplate> listExperimentTemplates(String status) 
throws SubmarineRuntimeException {
+    List<ExperimentTemplate> tpls = new 
ArrayList<>(cachedExperimentTemplates.values());
+
+    // Is it available in cache?
+    if (tpls != null && tpls.size() != 0) {
+      return tpls;
+    }
+    try (SqlSession sqlSession = MyBatisUtil.getSqlSession()) {
+      ExperimentTemplateMapper experimentTemplateMapper = 
+            sqlSession.getMapper(ExperimentTemplateMapper.class);
+
+      List<ExperimentTemplateEntity> experimentTemplateEntitys = 
experimentTemplateMapper.selectByKey(null);
+      for (ExperimentTemplateEntity experimentTemplateEntity : 
experimentTemplateEntitys) {
+        if (experimentTemplateEntity != null) {
+          ExperimentTemplate tpl = new ExperimentTemplate();
+
+          tpl.setExperimentTemplateSpec(
+                new 
Gson().fromJson(experimentTemplateEntity.getExperimentTemplateSpec(), 
+                ExperimentTemplateSpec.class));
+          tpls.add(tpl);
+          
cachedExperimentTemplates.put(tpl.getExperimentTemplateSpec().getName(), tpl);
+        }
+      }
+    } catch (Exception e) {
+      LOG.error(e.getMessage(), e);
+      throw new SubmarineRuntimeException(Status.BAD_REQUEST.getStatusCode(),
+          "Unable to get the experimentTemplate details.");
+    }
+    return tpls;
+  }
+
+  private void checkSpec(ExperimentTemplateSpec spec) throws 
SubmarineRuntimeException {
+    if (spec == null) {
+      throw new SubmarineRuntimeException(Status.BAD_REQUEST.getStatusCode(), 
+            "Invalid experimentTemplate spec.");
+    }
+  }
+
+  private ExperimentTemplate getExperimentTemplateDetails(String name) throws 
SubmarineRuntimeException {
+
+    // Is it available in cache?
+    ExperimentTemplate tpl = cachedExperimentTemplates.get(name);
+    if (tpl != null) {
+      return tpl;
+    }
+    ExperimentTemplateEntity experimentTemplateEntity;
+    try (SqlSession sqlSession = MyBatisUtil.getSqlSession()) {
+      ExperimentTemplateMapper experimentTemplateMapper = 
+            sqlSession.getMapper(ExperimentTemplateMapper.class);
+
+      experimentTemplateEntity = experimentTemplateMapper.select(name);
+
+      if (experimentTemplateEntity != null) {
+        tpl = new ExperimentTemplate();
+        tpl.setExperimentTemplateSpec(
+            new 
Gson().fromJson(experimentTemplateEntity.getExperimentTemplateSpec(), 
+            ExperimentTemplateSpec.class));
+      }
+    } catch (Exception e) {
+      LOG.error(e.getMessage(), e);
+      throw new SubmarineRuntimeException(Status.BAD_REQUEST.getStatusCode(),
+          "Unable to get the experimentTemplate details.");
+    }
+    return tpl;
+  }
+
+  private ExperimentTemplateSpec parameterMapping(String spec) {
+
+    ExperimentTemplateSpec tplSpec = new Gson().fromJson(spec, 
ExperimentTemplateSpec.class);
+
+    Map<String, String> parmMap = new HashMap<String, String>();
+    for (ExperimentTemplateParamSpec parm : tplSpec.getParameters()) {
+      if (parm.getValue() != null) {
+        parmMap.put(parm.getName(), parm.getValue());
+      } else {
+        parmMap.put(parm.getName(), "");
+      }
+    }
+
+    Pattern pattern = Pattern.compile("\\{\\{(.+?)\\}\\}");
+    StringBuffer sb = new StringBuffer();
+    Matcher matcher = pattern.matcher(spec);
+
+    List<String> unmappedKeys = new ArrayList<String>();
+
+    while (matcher.find()) {
+      final String key = matcher.group(1);
+      final String replacement = parmMap.get(key);
+      if (replacement == null) {
+        unmappedKeys.add(key);
+      }
+      else {
+        matcher.appendReplacement(sb, replacement);
+      }
+      parmMap.remove(key);
+    }
+    matcher.appendTail(sb);
+
+    if (parmMap.size() > 0) {
+      throw new SubmarineRuntimeException(Status.BAD_REQUEST.getStatusCode(),
+            "Parameters contains unused key: " + parmMap.keySet());
+    }
+
+    if (unmappedKeys.size() > 0) {
+      throw new SubmarineRuntimeException(Status.BAD_REQUEST.getStatusCode(),
+          "Template contains unmapped key: " + unmappedKeys);
+    }  
+
+    try {
+      tplSpec = new Gson().fromJson(sb.toString(), 
ExperimentTemplateSpec.class);
+    } catch (Exception e) {
+      throw new SubmarineRuntimeException(Status.BAD_REQUEST.getStatusCode(),
+          "Template mapping fail: " + e.getMessage() + sb.toString());
+    }
+    return tplSpec;
+  }
+}
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/experimenttemplate/database/entity/ExperimentTemplateEntity.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/experimenttemplate/database/entity/ExperimentTemplateEntity.java
new file mode 100644
index 0000000..55cb338
--- /dev/null
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/experimenttemplate/database/entity/ExperimentTemplateEntity.java
@@ -0,0 +1,46 @@
+/*
+ * 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.experimenttemplate.database.entity;
+
+import org.apache.submarine.server.database.entity.BaseEntity;
+
+public class ExperimentTemplateEntity extends BaseEntity {
+
+  private String experimentTemplateName;
+
+  private String experimentTemplateSpec;
+
+  public String getExperimentTemplateName() {
+    return this.experimentTemplateName;
+  }
+
+  public void setExperimentTemplateName(String ExperimentTemplateName) {
+    this.experimentTemplateName = ExperimentTemplateName;
+  }
+
+  public String getExperimentTemplateSpec() {
+    return this.experimentTemplateSpec;
+  }
+
+  public void setExperimentTemplateSpec(String ExperimentTemplateSpec) {
+    this.experimentTemplateSpec = ExperimentTemplateSpec;
+  }
+
+}
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/experimenttemplate/database/mappers/ExperimentTemplateMapper.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/experimenttemplate/database/mappers/ExperimentTemplateMapper.java
new file mode 100644
index 0000000..c0646ce
--- /dev/null
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/experimenttemplate/database/mappers/ExperimentTemplateMapper.java
@@ -0,0 +1,39 @@
+
+/*
+ * 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.experimenttemplate.database.mappers;
+
+import java.util.List;
+
+import 
org.apache.submarine.server.experimenttemplate.database.entity.ExperimentTemplateEntity;
+
+
+public interface ExperimentTemplateMapper {
+
+  ExperimentTemplateEntity select(String ExperimentTemplateName);
+
+  int insert(ExperimentTemplateEntity ExperimentTemplate);
+
+  int update(ExperimentTemplateEntity ExperimentTemplate);
+
+  int delete(String ExperimentTemplateName);
+
+  List<ExperimentTemplateEntity> selectByKey(ExperimentTemplateEntity 
ExperimentTemplate);
+  
+}
diff --git 
a/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/ExperimentTemplateRestApi.java
 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/ExperimentTemplateRestApi.java
new file mode 100644
index 0000000..a20c536
--- /dev/null
+++ 
b/submarine-server/server-core/src/main/java/org/apache/submarine/server/rest/ExperimentTemplateRestApi.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.rest;
+
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.PATCH;
+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 org.apache.submarine.commons.utils.exception.SubmarineRuntimeException;
+import org.apache.submarine.server.api.experimenttemplate.ExperimentTemplate;
+import org.apache.submarine.server.api.spec.ExperimentTemplateSpec;
+import 
org.apache.submarine.server.experimenttemplate.ExperimentTemplateManager;
+import org.apache.submarine.server.response.JsonResponse;
+
+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;
+
+/**
+ * ExperimentTemplate REST API v1. It can accept {@link 
ExperimentTemplateSpec} to create a
+ * experimentTemplate.
+ */
+@Path(RestConstants.V1 + "/" + RestConstants.EXPERIMENT_TEMPLATES)
+@Produces({MediaType.APPLICATION_JSON + "; " + RestConstants.CHARSET_UTF8})
+public class ExperimentTemplateRestApi {
+  private final ExperimentTemplateManager experimentTemplateManager =
+      ExperimentTemplateManager.getInstance();
+
+  /**
+   * Returns the contents of {@link experimentTemplate}.
+   * @param spec experimentTemplate spec
+   * @return the contents of experimentTemplate
+   */
+  @POST
+  @Consumes({RestConstants.MEDIA_TYPE_YAML, MediaType.APPLICATION_JSON})
+  @Operation(summary = "Create a experimentTemplate",
+          tags = {"experimentTemplate"},
+          responses = {
+                  @ApiResponse(description = "successful operation", 
+                      content = @Content(
+                          schema = @Schema(
+                              implementation = JsonResponse.class)))})
+  public Response createExperimentTemplate(ExperimentTemplateSpec spec) {
+    try {
+      ExperimentTemplate experimentTemplate = 
experimentTemplateManager.createExperimentTemplate(spec);
+      return new JsonResponse.Builder<ExperimentTemplate>(Response.Status.OK)
+          .success(true).result(experimentTemplate).build();
+    } catch (SubmarineRuntimeException e) {
+      return parseExperimentTemplateServiceException(e);
+    }
+  }
+  
+  /**
+   * Update experimentTemplate.
+   * @param name Name of the experimentTemplate
+   * @param spec experimentTemplate spec
+   * @return the detailed info about updated experimentTemplate
+   */
+  @PATCH
+  @Path("/{id}")
+  @Consumes({RestConstants.MEDIA_TYPE_YAML, MediaType.APPLICATION_JSON})
+  @Operation(summary = "Update the experimentTemplate with job spec",
+          tags = {"experimentTemplates"},
+          responses = {
+                  @ApiResponse(description = "successful operation", 
+                      content = @Content(
+                          schema = @Schema(
+                              implementation = JsonResponse.class))),
+                  @ApiResponse(
+                      responseCode = "404", 
+                      description = "ExperimentTemplate not found")})
+  public Response updateExperimentTemplate(
+      @PathParam(RestConstants.EXPERIMENT_TEMPLATE_ID) String name,
+      ExperimentTemplateSpec spec) {
+    try {
+      ExperimentTemplate experimentTemplate =
+          experimentTemplateManager.updateExperimentTemplate(name, spec);
+      return new JsonResponse.Builder<ExperimentTemplate>(Response.Status.OK)
+          .success(true).result(experimentTemplate).build();
+    } catch (SubmarineRuntimeException e) {
+      return parseExperimentTemplateServiceException(e);
+    }
+  }
+
+  /**
+   * Returns the experimentTemplate that deleted.
+   * @param name Name of the experimentTemplate
+   * @return the detailed info about deleted experimentTemplate
+   */
+  @DELETE
+  @Path("/{id}")
+  @Operation(summary = "Delete the experimentTemplate",
+          tags = {"experimentTemplates"},
+          responses = {
+                  @ApiResponse(description = "successful operation", 
+                      content = @Content(
+                          schema = @Schema(implementation = 
JsonResponse.class))),
+                  @ApiResponse(
+                      responseCode = "404", description = "ExperimentTemplate 
not found")})
+  public Response deleteExperimentTemplate(
+      @PathParam(RestConstants.EXPERIMENT_TEMPLATE_ID) String name) {
+    try {
+      ExperimentTemplate experimentTemplate = 
experimentTemplateManager.deleteExperimentTemplate(name);
+      return new JsonResponse.Builder<ExperimentTemplate>(Response.Status.OK)
+          .success(true).result(experimentTemplate).build();
+    } catch (SubmarineRuntimeException e) {
+      return parseExperimentTemplateServiceException(e);
+    }
+  }
+  
+  /**
+   * List all experimentTemplates.
+   * @return experimentTemplate list
+   */
+  @GET
+  @Operation(summary = "List of ExperimentTemplates",
+          tags = {"experimentTemplates"},
+          responses = {
+                  @ApiResponse(description = "successful operation", 
+                      content = @Content(
+                          schema = @Schema(
+                              implementation = JsonResponse.class)))})
+  public Response listExperimentTemplate(@QueryParam("status") String status) {
+    try {
+      List<ExperimentTemplate> experimentTemplateList =
+          experimentTemplateManager.listExperimentTemplates(status);
+      return new 
JsonResponse.Builder<List<ExperimentTemplate>>(Response.Status.OK)
+          .success(true).result(experimentTemplateList).build();
+    } catch (SubmarineRuntimeException e) {
+      return parseExperimentTemplateServiceException(e);
+    }
+  }
+
+  /**
+   * Returns details for the given experimentTemplate.
+   * @param name Name of the experimentTemplate
+   * @return the contents of experimentTemplate
+   */
+  @GET
+  @Path("/{id}")
+  @Operation(summary = "Find experimentTemplate by name",
+          tags = {"experimentTemplate"},
+          responses = {
+                  @ApiResponse(description = "successful operation", 
+                      content = @Content(
+                          schema = @Schema(implementation = 
JsonResponse.class))),
+                  @ApiResponse(
+                      responseCode = "404", 
+                      description = "ExperimentTemplate not found")})
+  public Response getExperimentTemplate(
+      @PathParam(RestConstants.EXPERIMENT_TEMPLATE_ID) String name) {
+    try {
+      ExperimentTemplate experimentTemplate = 
experimentTemplateManager.getExperimentTemplate(name);
+      return new JsonResponse.Builder<ExperimentTemplate>(Response.Status.OK)
+          .success(true).result(experimentTemplate).build();
+    } catch (SubmarineRuntimeException e) {
+      return parseExperimentTemplateServiceException(e);
+    }
+  }
+
+  private Response parseExperimentTemplateServiceException(
+      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 ecfce40..96e67f5 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
@@ -45,6 +45,13 @@ public class RestConstants {
   public static final String ENVIRONMENT_ID = "id";
 
   /**
+   * Experimect template
+   */
+  public static final String EXPERIMENT_TEMPLATES = "template";
+  
+  public static final String EXPERIMENT_TEMPLATE_ID = "id";
+
+  /**
    * Notebook
    */
   public static final String NOTEBOOK = "notebook";
diff --git a/submarine-server/server-core/src/main/resources/mybatis-config.xml 
b/submarine-server/server-core/src/main/resources/mybatis-config.xml
index fee836c..06e4991 100755
--- a/submarine-server/server-core/src/main/resources/mybatis-config.xml
+++ b/submarine-server/server-core/src/main/resources/mybatis-config.xml
@@ -67,5 +67,6 @@
     <mapper resource='org/apache/submarine/database/mappers/MetricMapper.xml'/>
     <mapper resource='org/apache/submarine/database/mappers/ParamMapper.xml'/>
     <mapper 
resource='org/apache/submarine/database/mappers/EnvironmentMapper.xml'/>
+    <mapper 
resource='org/apache/submarine/database/mappers/ExperimentTemplateMapper.xml'/>
   </mappers>
 </configuration>
diff --git 
a/submarine-server/server-core/src/main/resources/org/apache/submarine/database/mappers/ExperimentTemplateMapper.xml
 
b/submarine-server/server-core/src/main/resources/org/apache/submarine/database/mappers/ExperimentTemplateMapper.xml
new file mode 100644
index 0000000..9643b57
--- /dev/null
+++ 
b/submarine-server/server-core/src/main/resources/org/apache/submarine/database/mappers/ExperimentTemplateMapper.xml
@@ -0,0 +1,87 @@
+<?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.
+-->
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
"http://mybatis.org/dtd/mybatis-3-mapper.dtd";>
+<mapper 
namespace="org.apache.submarine.server.experimenttemplate.database.mappers.ExperimentTemplateMapper">
+  <resultMap id="BaseEntityResultMap" 
type="org.apache.submarine.server.database.entity.BaseEntity">
+       <id property="id" column="id"/>
+    <result column="create_by" property="createBy"/>
+    <result column="create_time" property="createTime"/>
+    <result column="update_by" property="updateBy"/>
+    <result column="update_time" property="updateTime"/>
+  </resultMap>
+
+  <resultMap id="resultMap" 
type="org.apache.submarine.server.experimenttemplate.database.entity.ExperimentTemplateEntity"
 extends="BaseEntityResultMap">
+    <result column="experimentTemplate_name" jdbcType="VARCHAR" 
property="experimentTemplateName" />
+    <result column="experimentTemplate_spec" jdbcType="VARCHAR" 
property="experimentTemplateSpec" />
+  </resultMap>
+
+  <sql id="Base_Column_List">
+    id, experimentTemplate_name, experimentTemplate_spec, create_by, 
create_time, update_by, update_time
+  </sql>
+
+  <select id="select" parameterType="java.lang.String" resultMap="resultMap">
+    select
+    <include refid="Base_Column_List" />
+    from experiment_template
+    where experimentTemplate_name = #{experimentTemplate_name,jdbcType=VARCHAR}
+  </select>
+
+  <delete id="delete" parameterType="java.lang.String">
+    delete from experiment_template
+    where experimentTemplate_name = #{experimentTemplate_name,jdbcType=VARCHAR}
+  </delete>
+
+  <insert id="insert" 
parameterType="org.apache.submarine.server.experimenttemplate.database.entity.ExperimentTemplateEntity">
+    insert into experiment_template (id, experimentTemplate_name, 
experimentTemplate_spec,
+      create_by, create_time, update_by, update_time)
+    values (#{id,jdbcType=VARCHAR}, 
+    #{experimentTemplateName,jdbcType=VARCHAR}, 
+    #{experimentTemplateSpec,jdbcType=VARCHAR},
+    #{createBy,jdbcType=VARCHAR}, now(), #{updateBy,jdbcType=VARCHAR}, now())
+  </insert>
+
+  <update id="update" 
parameterType="org.apache.submarine.server.experimenttemplate.database.entity.ExperimentTemplateEntity">
+    update experiment_template
+    <set>
+      <if test="experimentTemplateSpec != null">
+        experimentTemplate_spec = #{experimentTemplateSpec,jdbcType=VARCHAR},
+      </if>
+      update_time = now()
+    </set>
+    where experimentTemplate_name = #{experimentTemplateName,jdbcType=VARCHAR}
+  </update>
+
+  <select id="selectByKey" parameterType="java.lang.String" 
resultMap="resultMap">
+    select
+    <include refid="Base_Column_List" />
+    from experiment_template
+    where 1 = 1
+    <if test="id != null">
+      AND id = #{id,jdbcType=VARCHAR}
+    </if>
+    <if test="experimentTemplateName != null">
+      AND experimentTemplate_name = #{experimentTemplateName,jdbcType=VARCHAR}
+    </if>
+    <!-- AND JSON_EXTRACT(`experimentTemplate_spec`, "$.name") ==  -->
+
+
+  </select>
+  
+</mapper>
diff --git 
a/submarine-server/server-core/src/test/java/org/apache/submarine/server/rest/ExperimentTemplateRestApiTest.java
 
b/submarine-server/server-core/src/test/java/org/apache/submarine/server/rest/ExperimentTemplateRestApiTest.java
new file mode 100644
index 0000000..1813609
--- /dev/null
+++ 
b/submarine-server/server-core/src/test/java/org/apache/submarine/server/rest/ExperimentTemplateRestApiTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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 com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import org.apache.submarine.commons.utils.SubmarineConfiguration;
+import org.apache.submarine.server.api.experimenttemplate.ExperimentTemplate;
+import org.apache.submarine.server.api.spec.ExperimentTemplateSpec;
+import org.apache.submarine.server.response.JsonResponse;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import javax.ws.rs.core.Response;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Type;
+
+import static org.junit.Assert.assertEquals;
+
+public class ExperimentTemplateRestApiTest {
+  private static ExperimentTemplateRestApi experimentTemplateStoreApi;
+  private static ExperimentTemplateSpec experimentTemplateSpec;
+
+  private static GsonBuilder gsonBuilder = new GsonBuilder();
+  private static Gson gson = gsonBuilder.setDateFormat("yyyy-MM-dd 
HH:mm:ss").create();
+
+  @BeforeClass
+  public static void init() {
+    SubmarineConfiguration submarineConf = 
SubmarineConfiguration.getInstance();
+    
submarineConf.setMetastoreJdbcUrl("jdbc:mysql://127.0.0.1:3306/submarine_test?" 
+ "useUnicode=true&amp;"
+        + "characterEncoding=UTF-8&amp;" + "autoReconnect=true&amp;" + 
"failOverReadOnly=false&amp;"
+        + "zeroDateTimeBehavior=convertToNull&amp;" + "useSSL=false");
+    submarineConf.setMetastoreJdbcUserName("submarine_test");
+    submarineConf.setMetastoreJdbcPassword("password_test");
+    experimentTemplateStoreApi = new ExperimentTemplateRestApi();  
+  }
+
+  @Before
+  public void createAndUpdateExperimentTemplate() {
+    String body = loadContent("experimenttemplate/test_template_1.json");
+    experimentTemplateSpec = gson.fromJson(body, ExperimentTemplateSpec.class);
+    
+    // Create ExperimentTemplate
+    Response createEnvResponse = 
experimentTemplateStoreApi.createExperimentTemplate(experimentTemplateSpec);
+    assertEquals(Response.Status.OK.getStatusCode(), 
createEnvResponse.getStatus());
+
+    // Update ExperimentTemplate
+    experimentTemplateSpec.setDescription("newdescription");
+    Response updateTplResponse = experimentTemplateStoreApi.
+        updateExperimentTemplate(experimentTemplateSpec.getName(), 
experimentTemplateSpec);
+    assertEquals(Response.Status.OK.getStatusCode(), 
updateTplResponse.getStatus());
+  }
+
+  @After
+  public void deleteExperimentTemplate() {
+
+    String body = loadContent("experimenttemplate/test_template_1.json");
+    experimentTemplateSpec = gson.fromJson(body, ExperimentTemplateSpec.class);
+
+    Response deleteEnvResponse = experimentTemplateStoreApi.
+          deleteExperimentTemplate(experimentTemplateSpec.getName());
+    assertEquals(Response.Status.OK.getStatusCode(), 
deleteEnvResponse.getStatus());
+  }
+
+  @Test
+  public void getExperimentTemplate() {
+
+    String body = loadContent("experimenttemplate/test_template_1.json");
+    experimentTemplateSpec = gson.fromJson(body, ExperimentTemplateSpec.class);
+
+    Response getEnvResponse = experimentTemplateStoreApi.
+          getExperimentTemplate(experimentTemplateSpec.getName());
+    ExperimentTemplate experimentTemplate = 
getExperimentTemplateFromResponse(getEnvResponse);
+    assertEquals(experimentTemplateSpec.getName(), 
experimentTemplate.getExperimentTemplateSpec().getName());
+
+  }
+
+  private ExperimentTemplate getExperimentTemplateFromResponse(Response 
response) {
+    String entity = (String) response.getEntity();
+    Type type = new TypeToken<JsonResponse<ExperimentTemplate>>() {
+        }.getType();
+    JsonResponse<ExperimentTemplate> jsonResponse = gson.fromJson(entity, 
type);
+    return jsonResponse.getResult();
+  }
+
+  @Test
+  public void listExperimentTemplate() {
+
+    String body = loadContent("experimenttemplate/test_template_1.json");
+    experimentTemplateSpec = gson.fromJson(body, ExperimentTemplateSpec.class);
+
+    Response getEnvResponse = 
experimentTemplateStoreApi.listExperimentTemplate("");
+    String entity = (String) getEnvResponse.getEntity();
+    JsonResponse jsonResponse = gson.fromJson(entity, JsonResponse.class);
+    ExperimentTemplate[] experimentTemplates = 
gson.fromJson(gson.toJson(jsonResponse.getResult()),
+        ExperimentTemplate[].class);
+    assertEquals(1, experimentTemplates.length);
+
+    ExperimentTemplate experimentTemplate = experimentTemplates[0];
+    assertEquals(experimentTemplateSpec.getName(), 
experimentTemplate.getExperimentTemplateSpec().getName());
+  }
+
+  protected String loadContent(String resourceName) {
+    StringBuilder content = new StringBuilder();
+    InputStream inputStream =
+        this.getClass().getClassLoader().getResourceAsStream(resourceName);
+    BufferedReader r = new BufferedReader(new InputStreamReader(inputStream));
+    String l;
+    try {
+      while ((l = r.readLine()) != null) {
+        content.append(l).append("\n");
+      }
+      inputStream.close();
+    } catch (IOException e) {
+      Assert.fail(e.toString());
+    }
+    return content.toString();
+  }
+}
diff --git 
a/submarine-server/server-core/src/test/resources/experimenttemplate/test_template_1.json
 
b/submarine-server/server-core/src/test/resources/experimenttemplate/test_template_1.json
new file mode 100644
index 0000000..61448d5
--- /dev/null
+++ 
b/submarine-server/server-core/src/test/resources/experimenttemplate/test_template_1.json
@@ -0,0 +1,43 @@
+{
+  "name": "tf-mnist-test",
+  "author": "author",
+  "description": "This is a template to run tf-mnist\n",
+  "parameters": [
+    {
+      "name": "input.train_data",
+      "required": true,
+      "description": "train data is expected in SVM format, and can be stored 
in HDFS/S3 \n"
+    },
+    {
+      "name": "training.batch_size",
+      "value": 150,
+      "required": false,
+      "description": "This is batch size of training"
+    }
+  ],
+  "experimentSpec": {
+    "meta": {
+      "cmd": "python /var/tf_mnist/mnist_with_summaries.py 
--log_dir=/train/log --learning_rate=0.01 --batch_size={{training.batch_size}}",
+      "name": "tf-mnist-json",
+      "envVars": {
+        "input_path": "{{input.train_data}}", 
+        "ENV2": "ENV2"
+      },
+      "framework": "TensorFlow",
+      "namespace": "default"
+    },
+    "spec": {
+      "Ps": {
+        "replicas": 1,
+        "resources": "cpu=1,memory=1024M"
+      },
+      "Worker": {
+        "replicas": 1,
+        "resources": "cpu=1,memory=1024M"
+      }
+    },
+    "environment": {
+      "image": "gcr.io/kubeflow-ci/tf-mnist-with-summaries:1.0"
+    }
+  }
+}
diff --git 
a/submarine-server/server-core/src/test/resources/experimenttemplate/test_template_2.json
 
b/submarine-server/server-core/src/test/resources/experimenttemplate/test_template_2.json
new file mode 100644
index 0000000..0c8cae1
--- /dev/null
+++ 
b/submarine-server/server-core/src/test/resources/experimenttemplate/test_template_2.json
@@ -0,0 +1,43 @@
+{
+  "name": "tf-mnist-test2",
+  "author": "author",
+  "description": "This is a template to run tf-mnist\n",
+  "parameters": [
+    {
+      "name": "input.train_data",
+      "required": true,
+      "description": "train data is expected in SVM format, and can be stored 
in HDFS/S3 \n"
+    },
+    {
+      "name": "training.batch_size",
+      "value": 150,
+      "required": false,
+      "description": "This is batch size of training"
+    }
+  ],
+  "experimentSpec": {
+    "meta": {
+      "cmd": "python /var/tf_mnist/mnist_with_summaries.py 
--log_dir=/train/log --learning_rate=0.01 --batch_size={{training.batch_size}}",
+      "name": "tf-mnist-json",
+      "envVars": {
+        "input_path": "{{input.train_data}}", 
+        "ENV2": "ENV2"
+      },
+      "framework": "TensorFlow",
+      "namespace": "default"
+    },
+    "spec": {
+      "Ps": {
+        "replicas": 1,
+        "resources": "cpu=1,memory=1024M"
+      },
+      "Worker": {
+        "replicas": 1,
+        "resources": "cpu=1,memory=1024M"
+      }
+    },
+    "environment": {
+      "image": "gcr.io/kubeflow-ci/tf-mnist-with-summaries:1.0"
+    }
+  }
+}
diff --git 
a/submarine-test/test-k8s/src/test/java/org/apache/submarine/rest/ExperimentTemplateManagerRestApiIT.java
 
b/submarine-test/test-k8s/src/test/java/org/apache/submarine/rest/ExperimentTemplateManagerRestApiIT.java
new file mode 100644
index 0000000..87d1c37
--- /dev/null
+++ 
b/submarine-test/test-k8s/src/test/java/org/apache/submarine/rest/ExperimentTemplateManagerRestApiIT.java
@@ -0,0 +1,149 @@
+/*
+ * 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.rest;
+
+import java.io.IOException;
+
+import javax.ws.rs.core.Response;
+
+import org.apache.commons.httpclient.methods.DeleteMethod;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.submarine.server.AbstractSubmarineServerTest;
+import org.apache.submarine.server.api.experimenttemplate.ExperimentTemplate;
+import org.apache.submarine.server.response.JsonResponse;
+import org.apache.submarine.server.rest.RestConstants;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+@SuppressWarnings("rawtypes")
+public class ExperimentTemplateManagerRestApiIT extends 
AbstractSubmarineServerTest {
+
+  protected static String TPL_PATH =
+      "/api/" + RestConstants.V1 + "/" + RestConstants.EXPERIMENT_TEMPLATES;
+  protected static String TPL_NAME = "tf-mnist-test2";
+
+
+  @BeforeClass
+  public static void startUp() throws Exception {
+    Assert.assertTrue(checkIfServerIsRunning());
+  }
+
+  @Test
+  public void testCreateExperimentTemplate() throws Exception {
+    String body = loadContent("experimenttemplate/test_template_2.json");
+    run(body, "application/json");
+    deleteExperimentTemplate();
+  }
+
+  @Test
+  public void testGetExperimentTemplate() throws Exception {
+
+    String body = loadContent("experimenttemplate/test_template_2.json");
+    run(body, "application/json");
+
+    Gson gson = new GsonBuilder().create();
+    GetMethod getMethod = httpGet(TPL_PATH + "/" + TPL_NAME);
+    Assert.assertEquals(Response.Status.OK.getStatusCode(),
+        getMethod.getStatusCode());
+
+    String json = getMethod.getResponseBodyAsString();
+    JsonResponse jsonResponse = gson.fromJson(json, JsonResponse.class);
+    Assert.assertEquals(Response.Status.OK.getStatusCode(),
+        jsonResponse.getCode());
+
+    ExperimentTemplate getExperimentTemplate =
+        gson.fromJson(gson.toJson(jsonResponse.getResult()), 
ExperimentTemplate.class);
+    Assert.assertEquals(TPL_NAME, 
getExperimentTemplate.getExperimentTemplateSpec().getName());
+    deleteExperimentTemplate();
+  }
+
+
+  @Test
+  public void testUpdateExperimentTemplate() throws IOException {
+
+  }
+
+  @Test
+  public void testDeleteExperimentTemplate() throws Exception {
+    String body = loadContent("experimenttemplate/test_template_2.json");
+    run(body, "application/json");
+    deleteExperimentTemplate();
+
+    GetMethod getMethod = httpGet(TPL_PATH + "/" + TPL_NAME);
+    Assert.assertEquals(Response.Status.NOT_FOUND.getStatusCode(),
+        getMethod.getStatusCode());
+
+  }
+
+  @Test
+  public void testListExperimentTemplates() throws IOException {
+
+  }
+
+  protected void deleteExperimentTemplate() throws IOException {
+    Gson gson = new GsonBuilder().create();
+    DeleteMethod deleteMethod = httpDelete(TPL_PATH + "/" + TPL_NAME);
+    Assert.assertEquals(Response.Status.OK.getStatusCode(),
+        deleteMethod.getStatusCode());
+
+    String json = deleteMethod.getResponseBodyAsString();
+    JsonResponse jsonResponse = gson.fromJson(json, JsonResponse.class);
+    Assert.assertEquals(Response.Status.OK.getStatusCode(),
+        jsonResponse.getCode());
+
+    ExperimentTemplate deletedTpl =
+        gson.fromJson(gson.toJson(jsonResponse.getResult()), 
ExperimentTemplate.class);
+    Assert.assertEquals(TPL_NAME, 
deletedTpl.getExperimentTemplateSpec().getName());
+  }
+
+  protected void run(String body, String contentType) throws Exception {
+    Gson gson = new GsonBuilder().create();
+
+    // create
+    LOG.info("Create ExperimentTemplate using ExperimentTemplate REST API");
+    LOG.info(body);
+    PostMethod postMethod = httpPost(TPL_PATH, body, contentType);
+
+    LOG.info(postMethod.getResponseBodyAsString());
+
+    Assert.assertEquals(Response.Status.OK.getStatusCode(),
+        postMethod.getStatusCode());
+
+    String json = postMethod.getResponseBodyAsString();
+    JsonResponse jsonResponse = gson.fromJson(json, JsonResponse.class);
+    Assert.assertEquals(Response.Status.OK.getStatusCode(),
+        jsonResponse.getCode());
+
+        ExperimentTemplate tpl =
+        gson.fromJson(gson.toJson(jsonResponse.getResult()), 
ExperimentTemplate.class);
+    verifyCreateExperimentTemplateApiResult(tpl);
+  }
+
+  protected void verifyCreateExperimentTemplateApiResult(ExperimentTemplate 
tpl)
+      throws Exception {
+    Assert.assertNotNull(tpl.getExperimentTemplateSpec().getName());
+    Assert.assertNotNull(tpl.getExperimentTemplateSpec());
+  }
+}
diff --git a/submarine-workbench/workbench-web-ng/src/WEB-INF/web.xml 
b/submarine-workbench/workbench-web-ng/src/WEB-INF/web.xml
index b969edd..e4050f5 100644
--- a/submarine-workbench/workbench-web-ng/src/WEB-INF/web.xml
+++ b/submarine-workbench/workbench-web-ng/src/WEB-INF/web.xml
@@ -32,7 +32,8 @@
     </init-param>
     <init-param>
       <param-name>openApi.configuration.resourceClasses</param-name>
-      
<param-value>org.apache.submarine.server.rest.ExperimentRestApi</param-value>
+      <param-value>org.apache.submarine.server.rest.ExperimentRestApi, 
+      org.apache.submarine.server.rest.ExperimentTemplateRestApi</param-value>
     </init-param>
     <init-param>
       <param-name>openApi.configuration.scannerClass</param-name>


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

Reply via email to