Преглед изворни кода

init:初始化项目,迁移老旧api

370774330@qq.com пре 5 година
комит
766eec93a8
100 измењених фајлова са 7493 додато и 0 уклоњено
  1. 30 0
      .gitignore
  2. 133 0
      README.md
  3. 117 0
      api/.mvn/wrapper/MavenWrapperDownloader.java
  4. BIN
      api/.mvn/wrapper/maven-wrapper.jar
  5. 2 0
      api/.mvn/wrapper/maven-wrapper.properties
  6. 310 0
      api/mvnw
  7. 182 0
      api/mvnw.cmd
  8. 159 0
      api/pom.xml
  9. 160 0
      api/src/main/java/seecoder/devcloud/api/ApplicationProperties.java
  10. 17 0
      api/src/main/java/seecoder/devcloud/api/docker/DockerApi.java
  11. 90 0
      api/src/main/java/seecoder/devcloud/api/docker/impl/DockerApiImpl.java
  12. 141 0
      api/src/main/java/seecoder/devcloud/api/gitlab/GitlabApi.java
  13. 374 0
      api/src/main/java/seecoder/devcloud/api/gitlab/impl/GitlabApiImpl.java
  14. 18 0
      api/src/main/java/seecoder/devcloud/api/gitlab/model/CommitDetails.java
  15. 19 0
      api/src/main/java/seecoder/devcloud/api/gitlab/model/GitlabGroup.java
  16. 10 0
      api/src/main/java/seecoder/devcloud/api/gitlab/model/GitlabUser.java
  17. 66 0
      api/src/main/java/seecoder/devcloud/api/jenkins/JenkinsApi.java
  18. 195 0
      api/src/main/java/seecoder/devcloud/api/jenkins/impl/JenkinsApiImpl.java
  19. 38 0
      api/src/main/java/seecoder/devcloud/api/jenkins/impl/PoolingJenkinsHttpClient.java
  20. 19 0
      api/src/main/java/seecoder/devcloud/api/jenkins/model/BuildDetails.java
  21. 27 0
      api/src/main/java/seecoder/devcloud/api/jenkins/model/CoverageResult.java
  22. 11 0
      api/src/main/java/seecoder/devcloud/api/jenkins/model/CoverageResultElement.java
  23. 9 0
      api/src/main/java/seecoder/devcloud/api/jenkins/model/MutationResult.java
  24. 18 0
      api/src/main/java/seecoder/devcloud/api/k8s/AbstractApi.java
  25. 7 0
      api/src/main/java/seecoder/devcloud/api/k8s/ConfigMapApi.java
  26. 20 0
      api/src/main/java/seecoder/devcloud/api/k8s/DeploymentApi.java
  27. 40 0
      api/src/main/java/seecoder/devcloud/api/k8s/ExecApi.java
  28. 12 0
      api/src/main/java/seecoder/devcloud/api/k8s/IngressApi.java
  29. 94 0
      api/src/main/java/seecoder/devcloud/api/k8s/K8sApiConfiguration.java
  30. 46 0
      api/src/main/java/seecoder/devcloud/api/k8s/K8sConstants.java
  31. 85 0
      api/src/main/java/seecoder/devcloud/api/k8s/LogApi.java
  32. 8 0
      api/src/main/java/seecoder/devcloud/api/k8s/NamespaceApi.java
  33. 7 0
      api/src/main/java/seecoder/devcloud/api/k8s/PersistentVolumeClaimApi.java
  34. 7 0
      api/src/main/java/seecoder/devcloud/api/k8s/PodApi.java
  35. 83 0
      api/src/main/java/seecoder/devcloud/api/k8s/SecretApi.java
  36. 8 0
      api/src/main/java/seecoder/devcloud/api/k8s/ServiceApi.java
  37. 17 0
      api/src/main/java/seecoder/devcloud/api/k8s/WatchApi.java
  38. 68 0
      api/src/main/java/seecoder/devcloud/api/k8s/exception/K8sApiException.java
  39. 176 0
      api/src/main/java/seecoder/devcloud/api/k8s/impl/ConfigMapApiImpl.java
  40. 200 0
      api/src/main/java/seecoder/devcloud/api/k8s/impl/DeploymentApiImpl.java
  41. 99 0
      api/src/main/java/seecoder/devcloud/api/k8s/impl/ExecApiImpl.java
  42. 192 0
      api/src/main/java/seecoder/devcloud/api/k8s/impl/IngressApiImpl.java
  43. 107 0
      api/src/main/java/seecoder/devcloud/api/k8s/impl/LogApiImpl.java
  44. 145 0
      api/src/main/java/seecoder/devcloud/api/k8s/impl/NamespaceApiImpl.java
  45. 184 0
      api/src/main/java/seecoder/devcloud/api/k8s/impl/PersistentVolumeClaimImpl.java
  46. 136 0
      api/src/main/java/seecoder/devcloud/api/k8s/impl/PodApiImpl.java
  47. 148 0
      api/src/main/java/seecoder/devcloud/api/k8s/impl/SecretApiImpl.java
  48. 174 0
      api/src/main/java/seecoder/devcloud/api/k8s/impl/ServiceApiImpl.java
  49. 72 0
      api/src/main/java/seecoder/devcloud/api/k8s/impl/WatchApiImpl.java
  50. 50 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/ConfigMap.java
  51. 108 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/Container.java
  52. 41 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/ContainerPort.java
  53. 72 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/CpuQuantity.java
  54. 147 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/Deployment.java
  55. 65 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/DeploymentStatus.java
  56. 53 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/HttpGetAction.java
  57. 151 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/Ingress.java
  58. 86 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/K8sAbstractObject.java
  59. 45 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/K8sObject.java
  60. 24 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/K8sObjectRequest.java
  61. 6 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/K8sObjectTransformer.java
  62. 170 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/LabelSelector.java
  63. 36 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/Namespace.java
  64. 87 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/PersistentVolumeClaim.java
  65. 50 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/Pod.java
  66. 10 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/PodStatus.java
  67. 72 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/Service.java
  68. 51 0
      api/src/main/java/seecoder/devcloud/api/k8s/model/ServicePort.java
  69. 41 0
      api/src/main/java/seecoder/devcloud/api/k8s/util/SecretTypeEnum.java
  70. 47 0
      api/src/main/java/seecoder/devcloud/api/k8s/vo/SecretVO.java
  71. 19 0
      api/src/main/java/seecoder/devcloud/api/nexus/AssetApi.java
  72. 79 0
      api/src/main/java/seecoder/devcloud/api/nexus/ComponentApi.java
  73. 187 0
      api/src/main/java/seecoder/devcloud/api/nexus/NexusApi.java
  74. 16 0
      api/src/main/java/seecoder/devcloud/api/nexus/NexusApiBean.java
  75. 53 0
      api/src/main/java/seecoder/devcloud/api/nexus/TaskApi.java
  76. 73 0
      api/src/main/java/seecoder/devcloud/api/nexus/exception/NexusApiException.java
  77. 99 0
      api/src/main/java/seecoder/devcloud/api/nexus/impl/AbstractApi.java
  78. 67 0
      api/src/main/java/seecoder/devcloud/api/nexus/impl/AssetApiImpl.java
  79. 144 0
      api/src/main/java/seecoder/devcloud/api/nexus/impl/ComponentApiImpl.java
  80. 28 0
      api/src/main/java/seecoder/devcloud/api/nexus/impl/Constants.java
  81. 89 0
      api/src/main/java/seecoder/devcloud/api/nexus/impl/TaskApiImpl.java
  82. 17 0
      api/src/main/java/seecoder/devcloud/api/nexus/model/ApiVersion.java
  83. 21 0
      api/src/main/java/seecoder/devcloud/api/nexus/model/Asset.java
  84. 13 0
      api/src/main/java/seecoder/devcloud/api/nexus/model/AssetList.java
  85. 23 0
      api/src/main/java/seecoder/devcloud/api/nexus/model/Component.java
  86. 16 0
      api/src/main/java/seecoder/devcloud/api/nexus/model/ComponentList.java
  87. 9 0
      api/src/main/java/seecoder/devcloud/api/nexus/model/NexusList.java
  88. 25 0
      api/src/main/java/seecoder/devcloud/api/nexus/model/Task.java
  89. 15 0
      api/src/main/java/seecoder/devcloud/api/nexus/model/TaskList.java
  90. 2 0
      api/src/main/resources/application.yml
  91. 117 0
      common/.mvn/wrapper/MavenWrapperDownloader.java
  92. BIN
      common/.mvn/wrapper/maven-wrapper.jar
  93. 2 0
      common/.mvn/wrapper/maven-wrapper.properties
  94. 310 0
      common/mvnw
  95. 182 0
      common/mvnw.cmd
  96. 102 0
      common/pom.xml
  97. 47 0
      common/src/main/java/seecoder/devcloud/common/constant/Constants.java
  98. 15 0
      common/src/main/java/seecoder/devcloud/common/exceptions/AccessDeniedException.java
  99. 9 0
      common/src/main/java/seecoder/devcloud/common/exceptions/AlreadyInExaminationException.java
  100. 22 0
      common/src/main/java/seecoder/devcloud/common/exceptions/Asserts.java

+ 30 - 0
.gitignore

@@ -0,0 +1,30 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**
+!**/src/test/**
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+
+### VS Code ###
+.vscode/

+ 133 - 0
README.md

@@ -0,0 +1,133 @@
+# Seecoder Achitect
+
+## Overview
+
+帮助学生在有限制的情况下,在平台以流水线的方式部署软件实体并使用。
+
+## Module
+
+### core
+
+核心逻辑块,目前以下几个部分
+
+- 流水线构建核心逻辑,采用Pipeline设计模式。
+
+### **API**
+
+学长在老项目中写的,这里进行一个抽取和封装成一个模块,方便以后复用修改
+
+> 注意:部分属性我也不清楚是什么作用,可能是老项目用到的
+>
+> 用的时候只管你用到的属性就好了
+
+对gitlab、docker、k8s、jenkins、nexus的client的一个简单封装,用于与平台内各个组件直接交流
+
+使用spring的自定义配置功能,可在yml自动配置
+
+**usage**
+
+加入依赖
+
+```xml
+<dependency>
+	<groupId>seecoder.devcloud</groupId>
+	<artifactId>api</artifactId>
+	<version>0.0.1-SNAPSHOT</version>
+</dependency>
+
+<!-- 这个依赖用于开启编写yml配置的属性名提示、非必要 -->
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+	<artifactId>spring-boot-configuration-processor</artifactId>
+	<optional>true</optional>
+</dependency>
+```
+
+加入注解,扫描容器,即可使用
+
+```java
+@ComponentScan({"seecoder.devcloud.api", "seecoder.devcloud.common"})
+```
+
+**yml配置示例**
+
+> 配置信息属性具体参考,ApplicationProperties.java文件定义
+
+各个配置属性如何获取从官方文档找对应组件的访问方式,如下列文档说明
+
+再次强调下
+
+**注意:部分属性我也不清楚是什么作用,可能是老项目用到的**
+
+**用的时候只管你用到的属性就好了**
+
+[使用 Kubernetes API 访问集群 | Kubernetes](https://kubernetes.io/zh/docs/tasks/administer-cluster/access-cluster-api/)
+
+```yml
+seecoder:
+  secret: 
+  auth:
+    signingKey: 
+    iss: 
+    aud: 
+    portal: 
+    redirectUri: 
+  gitlab:
+    host: 
+    git-proxy: 
+    webHookProxy: 
+    username: 
+    token: 
+  jenkins:
+    host: 
+    username: 
+    token: 
+    baseImage: 
+  docker:
+    registry: 
+  mail:
+    from: 
+  storage:
+    type: 
+    properties:
+      location: 
+  k8s:
+    api-server: 
+    token: 
+    ingress-host: 
+    public-namespace: 
+    public-mysql-url: 
+    public-mysql-pod: 
+    public-mysql-username: 
+    public-mysql-password: 
+    debugging: 
+  registry:
+    host: 
+    username: 
+    password: 
+    nexus-server: 
+    repo-name: 
+    clean-up-task: 
+```
+
+### web
+
+后端
+
+三层
+
+- controller
+
+- service
+- dao 
+
+dao层采用jpa作为orm框架来开发
+
+### common
+
+为了能正常使用学长的api,从学长代码迁移出来的。
+
+公共类库,通用util、enum、exception之类的。
+
+
+

+ 117 - 0
api/.mvn/wrapper/MavenWrapperDownloader.java

@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+    private static final String WRAPPER_VERSION = "0.5.6";
+    /**
+     * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+     */
+    private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+        + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+    /**
+     * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+     * use instead of the default one.
+     */
+    private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+            ".mvn/wrapper/maven-wrapper.properties";
+
+    /**
+     * Path where the maven-wrapper.jar will be saved to.
+     */
+    private static final String MAVEN_WRAPPER_JAR_PATH =
+            ".mvn/wrapper/maven-wrapper.jar";
+
+    /**
+     * Name of the property which should be used to override the default download url for the wrapper.
+     */
+    private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+    public static void main(String args[]) {
+        System.out.println("- Downloader started");
+        File baseDirectory = new File(args[0]);
+        System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+        // If the maven-wrapper.properties exists, read it and check if it contains a custom
+        // wrapperUrl parameter.
+        File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+        String url = DEFAULT_DOWNLOAD_URL;
+        if(mavenWrapperPropertyFile.exists()) {
+            FileInputStream mavenWrapperPropertyFileInputStream = null;
+            try {
+                mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+                Properties mavenWrapperProperties = new Properties();
+                mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+                url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+            } catch (IOException e) {
+                System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+            } finally {
+                try {
+                    if(mavenWrapperPropertyFileInputStream != null) {
+                        mavenWrapperPropertyFileInputStream.close();
+                    }
+                } catch (IOException e) {
+                    // Ignore ...
+                }
+            }
+        }
+        System.out.println("- Downloading from: " + url);
+
+        File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+        if(!outputFile.getParentFile().exists()) {
+            if(!outputFile.getParentFile().mkdirs()) {
+                System.out.println(
+                        "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+            }
+        }
+        System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+        try {
+            downloadFileFromURL(url, outputFile);
+            System.out.println("Done");
+            System.exit(0);
+        } catch (Throwable e) {
+            System.out.println("- Error downloading");
+            e.printStackTrace();
+            System.exit(1);
+        }
+    }
+
+    private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+        if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+            String username = System.getenv("MVNW_USERNAME");
+            char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+            Authenticator.setDefault(new Authenticator() {
+                @Override
+                protected PasswordAuthentication getPasswordAuthentication() {
+                    return new PasswordAuthentication(username, password);
+                }
+            });
+        }
+        URL website = new URL(urlString);
+        ReadableByteChannel rbc;
+        rbc = Channels.newChannel(website.openStream());
+        FileOutputStream fos = new FileOutputStream(destination);
+        fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+        fos.close();
+        rbc.close();
+    }
+
+}

BIN
api/.mvn/wrapper/maven-wrapper.jar


+ 2 - 0
api/.mvn/wrapper/maven-wrapper.properties

@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar

+ 310 - 0
api/mvnw

@@ -0,0 +1,310 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# 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
+#
+#    https://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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+#   JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+#   M2_HOME - location of maven2's installed home dir
+#   MAVEN_OPTS - parameters passed to the Java VM when running Maven
+#     e.g. to debug Maven itself, use
+#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+  if [ -f /etc/mavenrc ] ; then
+    . /etc/mavenrc
+  fi
+
+  if [ -f "$HOME/.mavenrc" ] ; then
+    . "$HOME/.mavenrc"
+  fi
+
+fi
+
+# OS specific support.  $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+  CYGWIN*) cygwin=true ;;
+  MINGW*) mingw=true;;
+  Darwin*) darwin=true
+    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+    if [ -z "$JAVA_HOME" ]; then
+      if [ -x "/usr/libexec/java_home" ]; then
+        export JAVA_HOME="`/usr/libexec/java_home`"
+      else
+        export JAVA_HOME="/Library/Java/Home"
+      fi
+    fi
+    ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+  if [ -r /etc/gentoo-release ] ; then
+    JAVA_HOME=`java-config --jre-home`
+  fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+  ## resolve links - $0 may be a link to maven's home
+  PRG="$0"
+
+  # need this for relative symlinks
+  while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+      PRG="$link"
+    else
+      PRG="`dirname "$PRG"`/$link"
+    fi
+  done
+
+  saveddir=`pwd`
+
+  M2_HOME=`dirname "$PRG"`/..
+
+  # make it fully qualified
+  M2_HOME=`cd "$M2_HOME" && pwd`
+
+  cd "$saveddir"
+  # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --unix "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME="`(cd "$M2_HOME"; pwd)`"
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+  javaExecutable="`which javac`"
+  if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+    # readlink(1) is not available as standard on Solaris 10.
+    readLink=`which readlink`
+    if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+      if $darwin ; then
+        javaHome="`dirname \"$javaExecutable\"`"
+        javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+      else
+        javaExecutable="`readlink -f \"$javaExecutable\"`"
+      fi
+      javaHome="`dirname \"$javaExecutable\"`"
+      javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+      JAVA_HOME="$javaHome"
+      export JAVA_HOME
+    fi
+  fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+  if [ -n "$JAVA_HOME"  ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+      # IBM's JDK on AIX uses strange locations for the executables
+      JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+      JAVACMD="$JAVA_HOME/bin/java"
+    fi
+  else
+    JAVACMD="`which java`"
+  fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+  echo "Error: JAVA_HOME is not defined correctly." >&2
+  echo "  We cannot execute $JAVACMD" >&2
+  exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+  echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+  if [ -z "$1" ]
+  then
+    echo "Path not specified to find_maven_basedir"
+    return 1
+  fi
+
+  basedir="$1"
+  wdir="$1"
+  while [ "$wdir" != '/' ] ; do
+    if [ -d "$wdir"/.mvn ] ; then
+      basedir=$wdir
+      break
+    fi
+    # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+    if [ -d "${wdir}" ]; then
+      wdir=`cd "$wdir/.."; pwd`
+    fi
+    # end of workaround
+  done
+  echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+  if [ -f "$1" ]; then
+    echo "$(tr -s '\n' ' ' < "$1")"
+  fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+  exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Found .mvn/wrapper/maven-wrapper.jar"
+    fi
+else
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+    fi
+    if [ -n "$MVNW_REPOURL" ]; then
+      jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    else
+      jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    fi
+    while IFS="=" read key value; do
+      case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+      esac
+    done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Downloading from: $jarUrl"
+    fi
+    wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+    if $cygwin; then
+      wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+    fi
+
+    if command -v wget > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found wget ... using wget"
+        fi
+        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+            wget "$jarUrl" -O "$wrapperJarPath"
+        else
+            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+        fi
+    elif command -v curl > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found curl ... using curl"
+        fi
+        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+            curl -o "$wrapperJarPath" "$jarUrl" -f
+        else
+            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+        fi
+
+    else
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Falling back to using Java to download"
+        fi
+        javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+        # For Cygwin, switch paths to Windows format before running javac
+        if $cygwin; then
+          javaClass=`cygpath --path --windows "$javaClass"`
+        fi
+        if [ -e "$javaClass" ]; then
+            if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Compiling MavenWrapperDownloader.java ..."
+                fi
+                # Compiling the Java class
+                ("$JAVA_HOME/bin/javac" "$javaClass")
+            fi
+            if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                # Running the downloader
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Running MavenWrapperDownloader.java ..."
+                fi
+                ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+            fi
+        fi
+    fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+  echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --path --windows "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+  [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+    MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+  $MAVEN_OPTS \
+  -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+  "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

+ 182 - 0
api/mvnw.cmd

@@ -0,0 +1,182 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements.  See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership.  The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License.  You may obtain a copy of the License at
+@REM
+@REM    https://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied.  See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM     e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on"  echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+    IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Found %WRAPPER_JAR%
+    )
+) else (
+    if not "%MVNW_REPOURL%" == "" (
+        SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    )
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Couldn't find %WRAPPER_JAR%, downloading it ...
+        echo Downloading from: %DOWNLOAD_URL%
+    )
+
+    powershell -Command "&{"^
+		"$webclient = new-object System.Net.WebClient;"^
+		"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+		"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+		"}"^
+		"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+		"}"
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Finished downloading %WRAPPER_JAR%
+    )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%

+ 159 - 0
api/pom.xml

@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>api</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <packaging>jar</packaging>
+    <name>dev-cloud-api</name>
+    <description>Demo project for Spring Boot</description>
+
+    <properties>
+        <java.version>1.8</java.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
+    </properties>
+
+    <parent>
+        <artifactId>parent</artifactId>
+        <groupId>seecoder.devcloud</groupId>
+        <version>0.0.1-SNAPSHOT</version>
+    </parent>
+
+
+
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+        </dependency>
+        <!-- Api Dependencies -->
+        <!-- use guava-20.0 to solve the dependency conflict between jenkins-client and docker-client -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>20.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.spotify</groupId>
+            <artifactId>docker-client</artifactId>
+            <version>8.16.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>org.gitlab4j</groupId>
+            <artifactId>gitlab4j-api</artifactId>
+            <version>4.8.7</version>
+        </dependency>
+        <dependency>
+            <groupId>com.offbytwo.jenkins</groupId>
+            <artifactId>jenkins-client</artifactId>
+            <version>0.3.8</version>
+        </dependency>
+
+
+        <!-- Hibernate Dependencies -->
+        <dependency>
+            <groupId>org.hibernate.validator</groupId>
+            <artifactId>hibernate-validator</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>javax.validation</groupId>
+            <artifactId>validation-api</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>seecoder.devcloud</groupId>
+            <artifactId>common</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-logging</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+
+        <!-- https://mvnrepository.com/artifact/io.kubernetes/client-java -->
+        <dependency>
+            <groupId>io.kubernetes</groupId>
+            <artifactId>client-java</artifactId>
+            <version>10.0.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-tomcat</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.junit.vintage</groupId>
+                    <artifactId>junit-vintage-engine</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+    </dependencies>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>${spring-boot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                    <encoding>UTF-8</encoding>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.3.7.RELEASE</version>
+                <executions>
+                    <execution>
+                        <id>repackage</id>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 160 - 0
api/src/main/java/seecoder/devcloud/api/ApplicationProperties.java

@@ -0,0 +1,160 @@
+package seecoder.devcloud.api;
+
+import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
+import org.hibernate.validator.constraints.URL;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.validation.annotation.Validated;
+
+import javax.validation.Valid;
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import java.util.HashMap;
+import java.util.Map;
+
+@Data
+@Configuration
+@ConfigurationProperties("seecoder")
+@Validated
+public class ApplicationProperties {
+	@Pattern(regexp = "^[a-zA-Z0-9]+$")
+	@NotEmpty
+	private String secret;
+	@Valid
+	private Gitlab gitlab = new Gitlab();
+	@Valid
+	private Jenkins jenkins = new Jenkins();
+	@Valid
+	private Docker docker = new Docker();
+	@Valid
+	private Mail mail = new Mail();
+	@Valid
+	private Storage storage = new Storage();
+	@Valid
+	private K8s k8s = new K8s();
+	@Valid
+	private Registry registry = new Registry();
+	@Valid
+	private Auth auth = new Auth();
+
+	@Data
+	public static class Gitlab {
+		@URL
+		@NotEmpty
+		private String host;
+		private String gitProxy;
+		@URL
+		@NotEmpty
+		private String webHookProxy;
+		@NotEmpty
+		private String username = "root";
+        @NotNull
+        private Integer userId = 1;
+		@NotEmpty
+		private String token;
+
+		public String getGitProxy() {
+			return StringUtils.defaultString(gitProxy, host);
+		}
+	}
+
+	@Data
+	public static class Jenkins {
+		@URL
+		@NotEmpty
+		private String host;
+		@NotEmpty
+		private String username = "root";
+		@NotEmpty
+		private String token;
+		@NotEmpty
+		private String baseImage = "172.19.51.9:18082/build-base";
+	}
+
+	@Data
+	public static class Docker {
+		@NotEmpty
+		private String host = "unix:///var/run/docker.sock";
+		@NotEmpty
+		private String registry;
+	}
+
+	@Data
+	public static class Mail {
+		@Email
+		@NotEmpty
+		private String from;
+		@NotEmpty
+		private String personal = "MOOCODER";
+	}
+
+	@Data
+	public static class Storage {
+		private String type = "local";
+		private Map<String, String> properties = new HashMap<>();
+	}
+
+	@Data
+	public static class K8s{
+		@URL
+		@NotEmpty
+		private String apiServer;
+		@NotEmpty
+		private String token;
+		@NotEmpty
+		private String ingressHost;
+		@NotEmpty
+		private String publicNamespace;
+		@NotEmpty
+		private String publicMysqlUrl;
+		@NotEmpty
+		private String publicMysqlPod;
+		@NotEmpty
+		private String publicMysqlUsername;
+
+		private String publicMysqlPassword;
+
+		private Boolean validateSSL = false;
+
+		private Boolean debugging = false;
+	}
+
+	@Data
+	public static class Registry{
+		@NotEmpty
+		private String host;
+
+		@NotEmpty
+		private String username;
+
+		@NotEmpty
+		private String password;
+
+		@NotEmpty
+		private String nexusServer;
+
+        @NotEmpty
+        private String repoName = "sec-repo";
+
+		@NotEmpty
+		private String cleanUpTask = "4da7e1e1-2b28-46b5-a080-9d834cc96019";
+	}
+
+	@Data
+	public static class Auth {
+		@NotEmpty
+		private String signingKey;
+		@NotEmpty
+		private String iss = "p.seecoder.cn";
+		@NotEmpty
+		private String aud = "seec-seec";
+		@NotEmpty
+		private String portal = "http://p.seecoder.cn";
+		@NotEmpty
+		private String redirectUri;
+
+	}
+}

+ 17 - 0
api/src/main/java/seecoder/devcloud/api/docker/DockerApi.java

@@ -0,0 +1,17 @@
+package seecoder.devcloud.api.docker;
+
+import com.spotify.docker.client.ProgressHandler;
+import com.spotify.docker.client.messages.Image;
+
+import java.util.List;
+
+public interface DockerApi {
+
+    void buildAndPush(String directory, String imageName, String imageTag, ProgressHandler handler) throws Exception;
+
+    void buildAndPush(String directory, String imageName, String imageTag) throws Exception;
+
+    List<Image> listImage();
+
+    void delete(String imageId) throws Exception;
+}

+ 90 - 0
api/src/main/java/seecoder/devcloud/api/docker/impl/DockerApiImpl.java

@@ -0,0 +1,90 @@
+package seecoder.devcloud.api.docker.impl;
+
+
+import com.spotify.docker.client.DefaultDockerClient;
+import com.spotify.docker.client.DockerClient;
+import com.spotify.docker.client.DockerConfigReader;
+import com.spotify.docker.client.ProgressHandler;
+import com.spotify.docker.client.auth.ConfigFileRegistryAuthSupplier;
+import com.spotify.docker.client.auth.RegistryAuthSupplier;
+import com.spotify.docker.client.exceptions.DockerException;
+import com.spotify.docker.client.messages.Image;
+import com.spotify.docker.client.messages.ProgressMessage;
+import lombok.extern.apachecommons.CommonsLog;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.system.ApplicationHome;
+import org.springframework.stereotype.Component;
+import seecoder.devcloud.api.ApplicationProperties;
+import seecoder.devcloud.api.docker.DockerApi;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+@Component
+@CommonsLog
+public class DockerApiImpl implements DockerApi {
+
+    private final DockerClient client;
+    private final ProgressHandler defaultHandler;
+    private final RegistryAuthSupplier registryAuthSupplier;
+    private final String registry;
+
+    @Autowired
+    public DockerApiImpl(ApplicationProperties properties) throws IOException {
+        registry = properties.getDocker().getRegistry();
+        client = new DefaultDockerClient(properties.getDocker().getHost());
+        defaultHandler = new NoActionProgressHandler();
+        ApplicationHome applicationHome = new ApplicationHome(getClass());
+        Path path = Paths.get(applicationHome.getDir().getAbsolutePath(), "docker-config.json");
+        registryAuthSupplier = new ConfigFileRegistryAuthSupplier(new DockerConfigReader(), path);
+    }
+
+    @Override
+    public void buildAndPush(String directory, String imageName, String imageTag, ProgressHandler handler) throws Exception {
+        String imageId = client.build(new File(directory).toPath(), handler, DockerClient.BuildParam.name(registry + "/" + imageName + ":" + imageTag), DockerClient.BuildParam.forceRm());
+        if (imageId == null) {
+            throw new IOException("Failed to build docker image under [" + directory + "]");
+        }
+        client.push(registry + "/" + imageName + ":" + imageTag, new LogActionProgressHandler(), registryAuthSupplier.authFor(registry + "/" + imageName + ":" + imageTag));
+    }
+
+    @Override
+    public void buildAndPush(String directory, String imageName, String imageTag) throws Exception {
+        buildAndPush(directory, imageName, imageTag, defaultHandler);
+    }
+
+    public List<Image> listImage() {
+        List<Image> images = new ArrayList<>();
+        try {
+            images = client.listImages(DockerClient.ListImagesParam.allImages());
+        } catch (DockerException e) {
+            e.printStackTrace();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        return images;
+    }
+
+    public void delete(String imageId) throws DockerException, InterruptedException {
+        client.removeImage(imageId);
+    }
+
+    private class NoActionProgressHandler implements ProgressHandler {
+        @Override
+        public void progress(ProgressMessage message) {
+            //IGNORE THE MESSAGE
+        }
+    }
+
+    private class LogActionProgressHandler implements ProgressHandler {
+        @Override
+        public void progress(ProgressMessage message) {
+//            if (message.error() != null)
+//                log.error(message.error());
+        }
+    }
+}

+ 141 - 0
api/src/main/java/seecoder/devcloud/api/gitlab/GitlabApi.java

@@ -0,0 +1,141 @@
+package seecoder.devcloud.api.gitlab;
+
+
+
+import seecoder.devcloud.api.gitlab.model.CommitDetails;
+import seecoder.devcloud.api.gitlab.model.GitlabGroup;
+import seecoder.devcloud.api.gitlab.model.GitlabUser;
+import org.gitlab4j.api.GitLabApiException;
+import org.gitlab4j.api.models.CommitAction;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+import java.util.Set;
+
+public interface GitlabApi {
+	GitlabUser createUser(String username, String email, String password) throws GitLabApiException;
+
+
+	//10 => Guest access
+	//20 => Reporter access
+	//30 => Developer access
+	//40 => Maintainer access
+	//50 => Owner access # Only valid for groups
+	GitlabGroup createGroup(String groupName) throws GitLabApiException;
+
+	void addMemberToGroup(Integer groupId, Integer userId) throws GitLabApiException;
+
+	Integer addProjectToGroup(Integer groupId, String projectName) throws GitLabApiException;
+
+	/**
+	 * 创建internal项目,默认登录用户可见,在个人作业中使用,因为个人项目的fork,只能用户来fork,所以设置为internal
+	 * @param name
+	 * @return
+	 * @throws GitLabApiException
+	 */
+	Integer createProject(String name) throws GitLabApiException;
+
+	/**
+	 * 创建私有项目,在团队作业中使用,因为root是在每个小组中的,所以root用户可以把该私有项目fork到指定组
+	 * 目前用户在团队里是master权限,团队的私有项目,目前团队里成员可见
+	 * @param name
+	 * @return
+	 * @throws GitLabApiException
+	 */
+	Integer createPrivateProject(String name) throws GitLabApiException;
+
+	Integer createProjectForGroup(String name, int groupId) throws GitLabApiException;
+
+	Integer forkProject(int userId, int projectId, int namespaceId) throws GitLabApiException;
+
+	Set<String> compare(int projectId, String from, String to) throws GitLabApiException;
+
+	void archiveProject(int projectId) throws GitLabApiException;
+
+	void changePassword(int userId, String password) throws GitLabApiException;
+
+	void deleteUser(int userId) throws GitLabApiException;
+
+	void deleteProject(int projectId) throws GitLabApiException;
+
+	/**
+	 * get file content
+	 * @param projectId
+	 * @param branch
+	 * @param path
+	 * @return
+	 */
+	String getRepositoryFile(int projectId, String branch, String path) throws GitLabApiException, UnsupportedEncodingException;
+
+	/**
+	 * create a commit
+	 * @param projectId
+	 * @param branch
+	 * @param commitMessage
+	 * @param email
+	 * @param username
+	 * @param actions
+	 */
+	void createCommit(int projectId, String branch, String commitMessage, String email, String username, List<CommitAction> actions) throws GitLabApiException;
+
+	/**
+	 * 获取root用户的id
+	 * @return
+	 **/
+	Integer getRootUserId();
+
+    /**
+     * 为文档作业fork项目,主要涉及webhook不同
+     * @param userId
+     * @param projectId
+     * @param namespaceId
+     * @return
+     * @throws GitLabApiException
+     */
+	Integer forkProjectForDocumentWork(int userId, int projectId, int namespaceId) throws GitLabApiException;
+
+    /**
+     * 为代码作业fork项目同时设置WebHook
+     * @param userId
+     * @param projectId
+     * @param namespaceId
+     * @return
+     * @throws GitLabApiException
+     */
+	Integer forkProjectForCodeWork(int userId, int projectId, int namespaceId) throws GitLabApiException;
+
+    /**
+     * 为深度学习作业fork项目并设置WebHook
+     *
+     * @param baseRepoId  需要fork的源项目Id
+     * @param namespaceId namespace的Id
+     * @return fork出的repository的Id
+     */
+    Integer forkProjectForDLWork(int baseRepoId, int namespaceId) throws GitLabApiException;
+
+    /**
+     * 获取某次提交后的文件内容
+     * @param projectId the id of the project in gitlab
+     * @param commit hash of the commit
+     * @param path path of the file
+     * @return
+     * @throws GitLabApiException
+     * @throws IOException
+     */
+    String getRawRepositoryFile(int projectId, String commit, String path) throws GitLabApiException, IOException;
+
+    CommitDetails getCommitMessage(int projectId, String commitHash) throws GitLabApiException;
+
+	/**
+	 * 找到该次commit所对应的branch 因为分支签出的关系,某个commit有可能属于多个分支
+	 * @param projectId
+	 * @param commitHash
+	 * @return
+	 * @throws GitLabApiException
+	 */
+	List<CommitDetails> getAllCommits(int projectId, String commitHash) throws GitLabApiException;
+
+
+
+}

+ 374 - 0
api/src/main/java/seecoder/devcloud/api/gitlab/impl/GitlabApiImpl.java

@@ -0,0 +1,374 @@
+package seecoder.devcloud.api.gitlab.impl;
+
+
+import seecoder.devcloud.api.ApplicationProperties;
+import seecoder.devcloud.api.gitlab.GitlabApi;
+import seecoder.devcloud.api.gitlab.model.CommitDetails;
+import seecoder.devcloud.api.gitlab.model.GitlabGroup;
+import seecoder.devcloud.api.gitlab.model.GitlabUser;
+import org.gitlab4j.api.*;
+import org.gitlab4j.api.models.*;
+import org.gitlab4j.api.models.ImpersonationToken.Scope;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import seecoder.devcloud.common.util.ToolKit;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Component
+public class GitlabApiImpl implements GitlabApi {
+    private final ApplicationProperties.Gitlab gitlab;
+
+    private final GitLabApi server;
+
+    @Autowired
+    public GitlabApiImpl(ApplicationProperties properties) {
+        gitlab = properties.getGitlab();
+        this.server = new GitLabApi(gitlab.getHost(), gitlab.getToken());
+    }
+
+    @Override
+    public synchronized GitlabUser createUser(String username, String email,
+                                              String password) throws GitLabApiException {
+        User user = new User();
+        user.setUsername(username);
+        user.setEmail(email);
+        user.setName(username);
+        user.setSkipConfirmation(true);
+        user = server.getUserApi().createUser(user, password, 1000);
+        GitlabUser gitlabUser = new GitlabUser();
+        gitlabUser.setId(user.getId());
+        gitlabUser.setNamespace(fetchNamespace(username));
+        gitlabUser.setToken(createToken(user.getId()));
+        return gitlabUser;
+    }
+
+    //10 => Guest access
+    //20 => Reporter access
+    //30 => Developer access
+    //40 => Maintainer access
+    //50 => Owner access # Only valid for groups
+    @Override
+    public synchronized GitlabGroup createGroup(String groupName) throws GitLabApiException {
+
+        final Group group = server.getGroupApi().addGroup(groupName, groupName);//todo:Group默认private
+        GitlabGroup gitlabGroup = new GitlabGroup();
+        gitlabGroup.setId(group.getId());
+        final Integer ns1 = fetchNamespace("group", groupName);
+        gitlabGroup.setNamespace(ns1);//todo:Namespace
+
+//		server.getGroupApi().addMember(group.getId(), userId, 50);
+
+        return gitlabGroup;
+    }
+
+    @Override
+    public void addMemberToGroup(Integer groupId, Integer userId) throws GitLabApiException {
+        server.getGroupApi().addMember(groupId, userId, AccessLevel.MASTER);
+    }
+
+    @Override
+    public synchronized Integer addProjectToGroup(Integer groupId, String projectName) throws GitLabApiException {
+        final Project project = server.getProjectApi().createProject(groupId, projectName);
+        return project != null ? project.getId() : null;
+    }
+
+    private Integer fetchNamespace(String username) throws GitLabApiException {
+        Namespace namespace;
+        namespace = server.getNamespaceApi().findNamespaces(username)
+                .stream()
+                .filter(ns -> "user".equals(ns.getKind()))
+                .reduce((ns1, ns2) -> ns1.getId() > ns2.getId() ? ns1 : ns2)
+                .orElse(null);
+        return namespace != null ? namespace.getId() : null;
+    }
+
+    private Integer fetchNamespace(String type, String name) throws GitLabApiException {
+        Namespace namespace;
+        namespace = server.getNamespaceApi().findNamespaces(name)
+                .stream()
+                .filter(ns -> type.equals(ns.getKind()))
+                .reduce((ns1, ns2) -> ns1.getId() > ns2.getId() ? ns1 : ns2)
+                .orElse(null);
+        return namespace != null ? namespace.getId() : null;
+    }
+
+    private String createToken(int userId) throws GitLabApiException {
+        Scope[] scopes = new Scope[]{Scope.API};
+        ImpersonationToken token;
+        token = server.getUserApi().createImpersonationToken(userId, "AES-" + ToolKit.randomUUID(), null, scopes);
+        return token.getToken();
+    }
+
+    @Override
+    public synchronized Integer createProject(String name) throws GitLabApiException {
+        Project project = new Project();
+        project.setName(name);
+        project.setVisibility(Visibility.INTERNAL);
+        project = server.getProjectApi().createProject(project);
+        return project != null ? project.getId() : null;
+    }
+
+    @Override
+    public synchronized Integer createPrivateProject(String name) throws GitLabApiException {
+        Project project = new Project();
+        project.setName(name);
+        project.setVisibility(Visibility.PRIVATE);
+        project = server.getProjectApi().createProject(project);
+        return project != null ? project.getId() : null;
+    }
+
+    @Override
+    public synchronized Integer createProjectForGroup(String name, int groupId) throws GitLabApiException {
+        Project project = server.getProjectApi().createProject(groupId, name);
+        return project != null ? project.getId() : null;
+    }
+
+    @Override
+    public synchronized Integer forkProject(int userId, int projectId, int namespaceId) throws GitLabApiException {
+        Project project;
+        try {
+            server.setSudoAsId(userId);
+            project = server.getProjectApi().forkProject(projectId, namespaceId);
+            project.setVisibility(Visibility.PRIVATE);
+            project = server.getProjectApi().updateProject(project);
+        } finally {
+            server.unsudo();
+        }
+        if (project != null) {
+            createWebHook(project.getId());
+        }
+        return project != null ? project.getId() : null;
+    }
+
+    @Override
+    public synchronized Set<String> compare(int projectId, String from, String to) throws GitLabApiException {
+        CompareResults compareResults = server.getRepositoryApi().compare(projectId, from, to);
+        return compareResults.getDiffs().stream()
+                .map(Diff::getNewPath)
+                .map(p -> {
+                    int end = p.indexOf('/');
+                    if (end == -1) {
+                        end = p.length();
+                    }
+                    return p.substring(0, end);
+                })
+                .collect(Collectors.toSet());
+    }
+
+    private void createWebHook(int id) throws GitLabApiException {
+        String url = gitlab.getWebHookProxy() + "/internal/notify/webhook/" + id;
+        server.getProjectApi().addHook(id, url, true, false, false);
+    }
+
+    @Override
+    public synchronized void archiveProject(int projectId) throws GitLabApiException {
+        server.getProjectApi().archiveProject(projectId);
+    }
+
+    @Override
+    public synchronized void changePassword(int userId, String password) throws GitLabApiException {
+        User user = server.getUserApi().getUser(userId);
+        server.getUserApi().modifyUser(user, password, user.getProjectsLimit());
+    }
+
+    @Override
+    public synchronized void deleteUser(int userId) throws GitLabApiException {
+        server.getUserApi().deleteUser(userId, true);
+    }
+
+    @Override
+    public void deleteProject(int projectId) throws GitLabApiException {
+        server.getProjectApi().deleteProject(projectId);
+    }
+
+    /**
+     * 获得项目下的文件
+     *
+     * @param path 文件的相对路径
+     * @return 文件内容
+     */
+    @Override
+    public String getRepositoryFile(int projectId, String branch,
+                                    String path) throws GitLabApiException, UnsupportedEncodingException {
+        RepositoryFileApi repositoryFileApi = server.getRepositoryFileApi();
+        RepositoryFile repositoryFile = repositoryFileApi.getFile(path, projectId, branch);
+        if (repositoryFile != null) {
+            return new String(Base64.getDecoder().decode(repositoryFile.getContent()), StandardCharsets.UTF_8);
+        }
+        return null;
+    }
+
+    @Override
+    public String getRawRepositoryFile(int projectId, String commit,
+                                       String path) throws GitLabApiException, IOException {
+        RepositoryFileApi repositoryFileApi = server.getRepositoryFileApi();
+        InputStream repositoryFile = repositoryFileApi.getRawFile(projectId, commit, path);
+        InputStreamReader reader = new InputStreamReader(repositoryFile);
+        StringBuilder stringBuilder = new StringBuilder();
+        char[] content = new char[1024];
+        int readCount = 0;
+        while ((readCount = reader.read(content)) > 0) {
+            stringBuilder.append(Arrays.copyOfRange(content, 0, readCount));
+        }
+        return stringBuilder.toString();
+    }
+
+    @Override
+    public void createCommit(int projectId, String branch, String commitMessage, String email, String username,
+                             List<CommitAction> actions) throws GitLabApiException {
+        CommitsApi commitsApi = server.getCommitsApi();
+        commitsApi.createCommit(projectId, branch, commitMessage, null, email, username, actions);
+    }
+
+    @Override
+    public Integer getRootUserId() {
+        return gitlab.getUserId();
+    }
+
+    @Override
+    public synchronized Integer forkProjectForDocumentWork(int userId, int projectId,
+                                                           int namespaceId) throws GitLabApiException {
+        Project project;
+        try {
+            server.setSudoAsId(userId);
+            project = server.getProjectApi().forkProject(projectId, namespaceId);
+            project.setVisibility(Visibility.PRIVATE);
+            project = server.getProjectApi().updateProject(project);
+        } finally {
+            server.unsudo();
+        }
+        if (project != null) {
+            String url = gitlab.getWebHookProxy() + "/internal/notify/webhook/document/" + project.getId();
+            server.getProjectApi().addHook(project, url, true, false, false);
+        }
+        return project != null ? project.getId() : null;
+    }
+
+    @Override
+    public Integer forkProjectForCodeWork(int userId, int projectId, int namespaceId) throws GitLabApiException {
+        Project project;
+        try {
+            server.setSudoAsId(userId);
+            project = server.getProjectApi().forkProject(projectId, namespaceId);
+            project.setVisibility(Visibility.PRIVATE);
+            project = server.getProjectApi().updateProject(project);
+        } finally {
+            server.unsudo();
+        }
+        if (project != null) {
+            // TODO
+            String url_build = gitlab.getWebHookProxy() + "/internal/notify/webhook/code/build/" + project.getId();
+            server.getProjectApi().addHook(project, url_build, true, false, false);
+            // TODO
+            String url_test = gitlab.getWebHookProxy() + "/internal/notify/webhook/code/unit/" + project.getId();
+            server.getProjectApi().addHook(project, url_test, true, false, false);
+        }
+        return project != null ? project.getId() : null;
+    }
+
+    @Override
+    public Integer forkProjectForDLWork(int baseRepoId, int namespaceId) throws GitLabApiException {
+        Project project;
+        ProjectApi projectApi = server.getProjectApi();
+        try {
+            server.setSudoAsId(getRootUserId());
+            project = projectApi.forkProject(baseRepoId, namespaceId);
+            project.setVisibility(Visibility.PRIVATE);
+            project = projectApi.updateProject(project);
+        } finally {
+            server.unsudo();
+        }
+
+        if (project != null) {
+            Integer newProjectId = project.getId();
+            final String webHookUrl = gitlab.getWebHookProxy() + "/internal/notify/webhook/dlwork/" + newProjectId;
+            projectApi.addHook(project, webHookUrl, true, false, false);
+            return newProjectId;
+        }
+        return null;
+    }
+
+
+    @Override
+    public CommitDetails getCommitMessage(int projectId, String commitHash) throws GitLabApiException {
+        CommitsApi commitsApi = server.getCommitsApi();
+        CommitDetails commitDetails = new CommitDetails();
+
+        commitDetails.setProjectId(projectId);
+        commitDetails.setCommitHash(commitHash);
+
+        Commit currentCommit = commitsApi.getCommit(projectId, commitHash);
+
+        RepositoryApi repositoryApi = server.getRepositoryApi();
+        List<Branch> branches = repositoryApi.getBranches(projectId);
+
+        branches.forEach(branch -> {
+            try {
+                List<Commit> commitList = commitsApi.getCommits(projectId, branch.getName(), null);
+                Commit theCommit = commitList.stream().filter(commit -> commit.getId().equals(currentCommit.getId()))
+                        .findFirst().orElse(null);
+
+                if(theCommit!=null){
+                    commitDetails.setBranch(branch.getName());
+                    commitDetails.setCommitMessage(theCommit.getMessage());
+                    commitDetails.setUrl(theCommit.getUrl());
+                    Instant instant = theCommit.getCommittedDate().toInstant();
+                    ZoneId zoneId = ZoneId.systemDefault();
+                    LocalDateTime localDateTime = instant.atZone(zoneId).toLocalDateTime();
+                    commitDetails.setTime(localDateTime);
+                    commitDetails.setAuthor(theCommit.getAuthorName());
+                }
+            } catch (GitLabApiException e) {
+                e.printStackTrace();
+            }
+        });
+
+        return commitDetails;
+    }
+    @Override
+    public List<CommitDetails> getAllCommits(int projectId, String commitHash) throws GitLabApiException {
+        CommitsApi commitsApi = server.getCommitsApi();
+        List<CommitDetails> list = new ArrayList<>();
+
+        Commit currentCommit = commitsApi.getCommit(projectId, commitHash);
+
+        RepositoryApi repositoryApi = server.getRepositoryApi();
+        List<Branch> branches = repositoryApi.getBranches(projectId);
+
+        branches.forEach(branch -> {
+            try {
+                List<Commit> commitList = commitsApi.getCommits(projectId, branch.getName(), null);
+                Commit theCommit = commitList.stream().filter(commit -> commit.getId().equals(currentCommit.getId()))
+                        .findFirst().orElse(null);
+
+                if(theCommit != null){
+                    CommitDetails commitDetails = new CommitDetails();
+                    commitDetails.setProjectId(projectId);
+                    commitDetails.setCommitHash(commitHash);
+                    commitDetails.setBranch(branch.getName());
+                    commitDetails.setCommitMessage(theCommit.getMessage());
+
+                    Instant instant = theCommit.getCommittedDate().toInstant();
+                    ZoneId zoneId = ZoneId.systemDefault();
+                    LocalDateTime localDateTime = instant.atZone(zoneId).toLocalDateTime();
+                    commitDetails.setTime(localDateTime);
+                    commitDetails.setAuthor(theCommit.getAuthorName());
+                    list.add(commitDetails);
+                }
+            } catch (GitLabApiException e) {
+                e.printStackTrace();
+            }
+        });
+        return list;
+    }
+}

+ 18 - 0
api/src/main/java/seecoder/devcloud/api/gitlab/model/CommitDetails.java

@@ -0,0 +1,18 @@
+package seecoder.devcloud.api.gitlab.model;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class CommitDetails {
+    private int projectId;
+    private String commitHash;
+    private LocalDateTime time;
+    private String commitMessage;
+    private String branch;
+    private String author;
+    private String url;
+    private boolean isBranchValid;
+    private boolean isMessageValid;
+}

+ 19 - 0
api/src/main/java/seecoder/devcloud/api/gitlab/model/GitlabGroup.java

@@ -0,0 +1,19 @@
+package seecoder.devcloud.api.gitlab.model;
+import lombok.Data;
+
+/**
+ * @ClassName GitlabGroup
+ * @PackageName com.moekr.moocoder.logic.api.vo
+ * @Author sheen
+ * @Date 2018/12/24
+ * @Version 1.0
+ * @Description //TODO
+ **/
+
+
+@Data
+
+public class GitlabGroup {
+    private Integer id;
+    private Integer namespace;
+}

+ 10 - 0
api/src/main/java/seecoder/devcloud/api/gitlab/model/GitlabUser.java

@@ -0,0 +1,10 @@
+package seecoder.devcloud.api.gitlab.model;
+
+import lombok.Data;
+
+@Data
+public class GitlabUser {
+	private Integer id;
+	private Integer namespace;
+	private String token;
+}

+ 66 - 0
api/src/main/java/seecoder/devcloud/api/jenkins/JenkinsApi.java

@@ -0,0 +1,66 @@
+package seecoder.devcloud.api.jenkins;
+
+import com.offbytwo.jenkins.model.QueueItem;
+import seecoder.devcloud.api.jenkins.model.BuildDetails;
+
+import java.io.IOException;
+import java.util.Map;
+
+public interface JenkinsApi {
+    /**
+     * 创建一个Job,加载配置模板
+     *
+     * @param id Job的Id,其实是Result的Id,也就是repo的Id,参照{@link com.moekr.moocoder.logic.service.impl.exam.ExamServiceImpl#join(int, int)}
+     * @throws IOException
+     */
+    void createJob(int id) throws IOException;
+
+    /**
+     * 通知构建
+     *
+     * @param id       Job的Id,其实是Result的Id,也就是repo的Id,参照{@link com.moekr.moocoder.logic.service.impl.exam.ExamServiceImpl#join(int, int)}
+     * @param paramMap 需要填写的各项参数
+     * @return
+     * @throws IOException
+     */
+    QueueItem invokeBuild(int id, Map<String, String> paramMap) throws IOException;
+
+    /**
+     * 通知构建
+     *
+     * @param jobName       JobName
+     * @param paramMap 需要填写的各项参数
+     * @return
+     * @throws IOException
+     */
+    QueueItem invokeBuild(String jobName, Map<String, String> paramMap) throws IOException;
+
+    /**
+     * 删除Job
+     *
+     * @param id Job的Id,其实是Result的Id,也就是repo的Id,参照{@link com.moekr.moocoder.logic.service.impl.exam.ExamServiceImpl#join(int, int)}
+     * @throws IOException
+     */
+    void deleteJob(int id) throws IOException;
+
+    // waitFinished: 是否等待直到构建完全完成
+    BuildDetails fetchBuildDetails(int id, int buildNumber, String target, boolean waitFinished) throws IOException;
+
+    BuildDetails fetchBuildDetails(String jobName, int buildNumber, String target, boolean waitFinished) throws IOException;
+
+    void createBuildJob(String jobName) throws IOException;
+
+	void createTestJob(String jobName) throws IOException;
+
+    void createFunctionTestJob(String jobName) throws IOException;
+
+    /**
+     * 创建深度学习需要的Job
+     *
+     * @param jobName Job名称
+     */
+    void createDLJob(String jobName) throws IOException;
+
+
+    void deleteJob(String jobName) throws IOException;
+}

+ 195 - 0
api/src/main/java/seecoder/devcloud/api/jenkins/impl/JenkinsApiImpl.java

@@ -0,0 +1,195 @@
+package seecoder.devcloud.api.jenkins.impl;
+
+
+import com.offbytwo.jenkins.JenkinsServer;
+import com.offbytwo.jenkins.client.JenkinsHttpClient;
+import com.offbytwo.jenkins.model.Artifact;
+import com.offbytwo.jenkins.model.BuildWithDetails;
+import com.offbytwo.jenkins.model.QueueItem;
+import com.offbytwo.jenkins.model.QueueReference;
+import seecoder.devcloud.api.jenkins.JenkinsApi;
+import seecoder.devcloud.api.jenkins.model.BuildDetails;
+import seecoder.devcloud.api.jenkins.model.CoverageResult;
+import seecoder.devcloud.api.jenkins.model.MutationResult;
+import seecoder.devcloud.api.ApplicationProperties;
+import lombok.extern.apachecommons.CommonsLog;
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.dom4j.*;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+import java.util.Map;
+
+@Component
+@CommonsLog
+public class JenkinsApiImpl implements JenkinsApi {
+    private final ApplicationProperties properties;
+
+    private JenkinsServer server;
+    private String configTemplate;
+    private String buildConfigTemplate;
+    private String testConfigTemplate;
+    private String functionTestConfigTemplate;
+    private String dlConfigTemplate;
+
+    public JenkinsApiImpl(ApplicationProperties properties) {
+        this.properties = properties;
+    }
+
+    @PostConstruct
+    private void initialize() throws URISyntaxException, IOException {
+        ApplicationProperties.Jenkins jenkins = properties.getJenkins();
+        URI uri = new URI(jenkins.getHost());
+        String username = jenkins.getUsername();
+        String token = jenkins.getToken();
+        JenkinsHttpClient client = new PoolingJenkinsHttpClient(uri, username, token);
+        server = new JenkinsServer(client);
+//        configTemplate = FileUtil.readResource("jenkins/config.xml");
+//        // TODO
+//        buildConfigTemplate = FileUtil.readResource("jenkins/codework/build-config.xml");
+//
+//        testConfigTemplate = FileUtil.readResource("jenkins/codework/test-config.xml");
+//        functionTestConfigTemplate = FileUtil.readResource("jenkins/codework/function-test-config.xml");
+//
+//        dlConfigTemplate = FileUtil.readResource("jenkins/dlConfig.xml");
+    }
+
+    @Override
+    public void createJob(int id) throws IOException {
+        server.createJob(String.valueOf(id), configTemplate);
+    }
+
+    @Override
+    public QueueItem invokeBuild(String jobName, Map<String, String> paramMap) throws IOException {
+        QueueReference reference;
+        if (paramMap == null) {
+            reference = server.getJob(jobName).build();
+        } else {
+            // 0.3.7版本的Jenkins客户端库中build(Map<String, String> params)方法存在BUG,会触发两次构建
+            reference = server.getJob(jobName).build(paramMap, false);
+        }
+        QueueItem item = server.getQueueItem(reference);
+        while (item.getExecutable() == null) {
+            try {
+                Thread.sleep(500);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+            item = server.getQueueItem(reference);
+        }
+        return item;
+    }
+
+    @Override
+    public QueueItem invokeBuild(int id, Map<String, String> paramMap) throws IOException {
+        return invokeBuild(String.valueOf(id), paramMap);
+    }
+
+    @Override
+    public void deleteJob(int id) throws IOException {
+        server.deleteJob(String.valueOf(id));
+    }
+
+    @Override
+    public BuildDetails fetchBuildDetails(String jobName, int buildNumber, String target, boolean waitFinished) throws IOException {
+        BuildDetails result = new BuildDetails();
+        BuildWithDetails build;
+        build = server.getJob(jobName).getBuildByNumber(buildNumber).details();
+        while (waitFinished && (build.getResult() == null || build.getDuration() == 0)) {
+            try {
+                Thread.sleep(500);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+            build = server.getJob(jobName).getBuildByNumber(buildNumber).details();
+        }
+        result.setConsoleOutput(build.getConsoleOutputText());
+        result.setNumber(build.getNumber());
+        result.setDuration(build.getDuration());
+        result.setBuildResult(build.getResult());
+        fetchTargetResult(result, build, target);
+        return result;
+    }
+
+    @Override
+    public BuildDetails fetchBuildDetails(int id, int buildNumber, String target,
+                                          boolean waitFinished) throws IOException {
+        return fetchBuildDetails(String.valueOf(id), buildNumber, target, waitFinished);
+    }
+
+    @Override
+    public void createBuildJob(String jobName) throws IOException {
+        server.createJob(jobName, buildConfigTemplate);
+    }
+
+    @Override
+    public void createTestJob(String jobName) throws IOException {
+        server.createJob(jobName, testConfigTemplate);
+    }
+
+    @Override
+    public void createFunctionTestJob(String jobName) throws IOException {
+        server.createJob(jobName, functionTestConfigTemplate);
+    }
+
+    @Override
+    public void createDLJob(String jobName) throws IOException {
+        server.createJob(jobName, dlConfigTemplate);
+    }
+
+    @Override
+    public void deleteJob(String jobName) throws IOException {
+        server.deleteJob(jobName);
+    }
+
+    private void fetchTargetResult(BuildDetails result, BuildWithDetails build, String target) {
+        try {
+            if ("TEST".equals(target)) {
+                fetchTestResult(result, build);
+            } else if ("COVERAGE".equals(target)) {
+                fetchCoverageResult(result, build);
+            } else if ("MUTATION".equals(target)) {
+                fetchMutationResult(result, build);
+            }
+        } catch (Exception ignore) {
+        }
+    }
+
+    private void fetchTestResult(BuildDetails result, BuildWithDetails build) throws IOException {
+        result.setTestResult(build.getTestResult());
+    }
+
+    private void fetchCoverageResult(BuildDetails result, BuildWithDetails build) throws IOException {
+        result.setCoverageResult(build.getClient().get(build.getUrl() + "/cobertura/?depth=2", CoverageResult.class));
+    }
+
+    private void fetchMutationResult(BuildDetails result, BuildWithDetails build) throws IOException, URISyntaxException, DocumentException {
+        List<Artifact> artifactList = build.getArtifacts();
+        Artifact artifact = artifactList.stream().filter(a -> a.getFileName().equals("mutations.xml")).findFirst().orElse(null);
+        if (artifact != null) {
+            MutationResult mutationResult = new MutationResult();
+            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+            outputStream.write(build.downloadArtifact(artifact));
+            String mutationReport = new String(outputStream.toByteArray());
+            Document document = DocumentHelper.parseText(mutationReport);
+            Element root = document.getRootElement();
+            for (Object element : root.elements("mutation")) {
+                if (element instanceof Element) {
+                    Attribute attribute = ((Element) element).attribute("detected");
+                    if (attribute != null) {
+                        mutationResult.setMutations(mutationResult.getMutations() + 1);
+                        if ("true".equals(attribute.getValue())) {
+                            mutationResult.setDetectedMutations(mutationResult.getDetectedMutations() + 1);
+                        }
+                    }
+                }
+            }
+            result.setMutationResult(mutationResult);
+        }
+    }
+
+}

+ 38 - 0
api/src/main/java/seecoder/devcloud/api/jenkins/impl/PoolingJenkinsHttpClient.java

@@ -0,0 +1,38 @@
+package seecoder.devcloud.api.jenkins.impl;
+
+import com.offbytwo.jenkins.client.JenkinsHttpClient;
+import org.apache.commons.lang.StringUtils;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.impl.auth.BasicScheme;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.protocol.BasicHttpContext;
+import org.apache.http.protocol.HttpContext;
+
+import java.net.URI;
+
+class PoolingJenkinsHttpClient extends JenkinsHttpClient {
+	PoolingJenkinsHttpClient(URI uri, String username, String password) {
+		super(uri, poolingHttpClient(uri, username, password));
+		if (StringUtils.isNotBlank(username)) {
+			HttpContext localContext = new BasicHttpContext();
+			localContext.setAttribute("preemptive-auth", new BasicScheme());
+			super.setLocalContext(localContext);
+		}
+	}
+
+	private static CloseableHttpClient poolingHttpClient(URI uri, String username, String password) {
+		HttpClientBuilder builder = addAuthentication(HttpClientBuilder.create(), uri, username, password);
+		PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager();
+		manager.setMaxTotal(256);
+		manager.setDefaultMaxPerRoute(128);
+		builder.setDefaultRequestConfig(RequestConfig.custom()
+				.setConnectTimeout(3000)
+				.setConnectionRequestTimeout(5000)
+				.setSocketTimeout(10000)
+				.build())
+				.setConnectionManager(manager);
+		return builder.build();
+	}
+}

+ 19 - 0
api/src/main/java/seecoder/devcloud/api/jenkins/model/BuildDetails.java

@@ -0,0 +1,19 @@
+package seecoder.devcloud.api.jenkins.model;
+
+
+import com.offbytwo.jenkins.model.BuildResult;
+import com.offbytwo.jenkins.model.TestResult;
+import lombok.Data;
+import lombok.ToString;
+
+@Data
+@ToString(exclude = "consoleOutput")
+public class BuildDetails {
+	private int number;
+	private long duration;
+	private BuildResult buildResult;
+	private String consoleOutput;
+	private TestResult testResult;
+	private CoverageResult coverageResult;
+	private MutationResult mutationResult;
+}

+ 27 - 0
api/src/main/java/seecoder/devcloud/api/jenkins/model/CoverageResult.java

@@ -0,0 +1,27 @@
+package seecoder.devcloud.api.jenkins.model;
+
+import com.offbytwo.jenkins.model.BaseModel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.util.List;
+
+@Setter
+@EqualsAndHashCode(callSuper = true)
+@ToString
+public class CoverageResult extends BaseModel {
+	private Results results;
+
+	public List<CoverageResultElement> getElements() {
+		return results.getElements();
+	}
+
+	@Data
+	@EqualsAndHashCode
+	@ToString
+	private static class Results {
+		private List<CoverageResultElement> elements;
+	}
+}

+ 11 - 0
api/src/main/java/seecoder/devcloud/api/jenkins/model/CoverageResultElement.java

@@ -0,0 +1,11 @@
+package seecoder.devcloud.api.jenkins.model;
+
+import lombok.Data;
+
+@Data
+public class CoverageResultElement {
+	private String name;
+	private int numerator;
+	private int denominator;
+	private int ratio;
+}

+ 9 - 0
api/src/main/java/seecoder/devcloud/api/jenkins/model/MutationResult.java

@@ -0,0 +1,9 @@
+package seecoder.devcloud.api.jenkins.model;
+
+import lombok.Data;
+
+@Data
+public class MutationResult {
+	private int mutations;
+	private int detectedMutations;
+}

+ 18 - 0
api/src/main/java/seecoder/devcloud/api/k8s/AbstractApi.java

@@ -0,0 +1,18 @@
+package seecoder.devcloud.api.k8s;
+
+
+import seecoder.devcloud.api.k8s.model.K8sAbstractObject;
+import seecoder.devcloud.api.k8s.model.K8sObjectRequest;
+
+import java.util.List;
+
+public interface AbstractApi<T extends K8sAbstractObject> {
+
+    T create(T t);
+
+    T update(T t);
+
+    void delete(T t);
+
+    List<T> getByCondition(K8sObjectRequest request);
+}

+ 7 - 0
api/src/main/java/seecoder/devcloud/api/k8s/ConfigMapApi.java

@@ -0,0 +1,7 @@
+package seecoder.devcloud.api.k8s;
+
+
+import seecoder.devcloud.api.k8s.model.ConfigMap;
+
+public interface ConfigMapApi extends AbstractApi<ConfigMap> {
+}

+ 20 - 0
api/src/main/java/seecoder/devcloud/api/k8s/DeploymentApi.java

@@ -0,0 +1,20 @@
+package seecoder.devcloud.api.k8s;
+
+
+import seecoder.devcloud.api.k8s.exception.K8sApiException;
+import seecoder.devcloud.api.k8s.model.Deployment;
+import seecoder.devcloud.api.k8s.model.DeploymentStatus;
+
+/**
+ * author: rale
+ * createdAt: 12/23/18
+ */
+public interface DeploymentApi extends AbstractApi<Deployment>{
+
+    /**
+     * 查询deployment的状态
+     * @return 如果该deployment不存在,则返回null
+     * @throws K8sApiException SYSTEM_ERROR 系统异常
+     */
+    DeploymentStatus getStatus(String namespace, String name);
+}

+ 40 - 0
api/src/main/java/seecoder/devcloud/api/k8s/ExecApi.java

@@ -0,0 +1,40 @@
+package seecoder.devcloud.api.k8s;
+
+
+
+import seecoder.devcloud.api.k8s.exception.K8sApiException;
+
+import java.util.List;
+
+/**
+ * 进入容器执行指令
+ *
+ * todo 传入输入流,获得输出流
+ */
+public interface ExecApi {
+
+    /**
+     * 选择namespace下podName中的containerName的容器执行commands指令
+     * @param namespace
+     * @param podName
+     * @param containerName
+     * @param commands
+     * @return exitCode=0 则返回true,其它情况返回false
+     * @throws K8sApiException NOT_FOUND 命名空间/pod/container不存在
+     * @throws K8sApiException SYSTEM_ERROR 系统异常
+     */
+    boolean exec(String namespace, String podName, String containerName, List<String> commands);
+
+    /**
+     * 当且仅当pod下只有一个container时,该指令才会执行成功,否则会抛出异常
+     * @param namespace
+     * @param podName
+     * @param commands
+     * @return exitCode=0 则返回true,其它情况返回false
+     * @throws K8sApiException NOT_FOUND 命名空间不存在/pod不存在/pod下没有container
+     * @throws K8sApiException SYSTEM_ERROR pod存在多个container/系统异常/指令执行异常
+     */
+    boolean exec(String namespace, String podName, List<String> commands);
+
+    Process getTerminal(String namespace, String podName);
+}

+ 12 - 0
api/src/main/java/seecoder/devcloud/api/k8s/IngressApi.java

@@ -0,0 +1,12 @@
+package seecoder.devcloud.api.k8s;
+
+
+import seecoder.devcloud.api.k8s.model.Ingress;
+
+/**
+ * author: rale
+ * createdAt: 12/24/18
+ */
+public interface IngressApi extends AbstractApi<Ingress>{
+    
+}

+ 94 - 0
api/src/main/java/seecoder/devcloud/api/k8s/K8sApiConfiguration.java

@@ -0,0 +1,94 @@
+package seecoder.devcloud.api.k8s;
+
+import io.kubernetes.client.Exec;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.apis.AppsV1Api;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.apis.ExtensionsV1beta1Api;
+import io.kubernetes.client.util.Config;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import seecoder.devcloud.api.ApplicationProperties;
+
+
+@Configuration
+public class K8sApiConfiguration {
+
+    private final ApiClient apiClient;
+
+    private final CoreV1Api coreV1Api;
+
+    private final AppsV1Api appsV1Api;
+
+    private final ExtensionsV1beta1Api extensionsV1beta1Api;
+
+    private final Exec exec;
+
+    /**
+     * http读取的超时时间设置为7分钟
+     */
+    private final ApiClient watchApiClient;
+
+    private final CoreV1Api watchCoreV1Api;
+
+    private final AppsV1Api watchAppsV1Api;
+
+    @Autowired
+    public K8sApiConfiguration(ApplicationProperties applicationProperties) {
+        ApplicationProperties.K8s k8s = applicationProperties.getK8s();
+        this.apiClient = Config.fromToken(k8s.getApiServer(), k8s.getToken(), k8s.getValidateSSL());
+        this.apiClient.setDebugging(k8s.getDebugging());
+        io.kubernetes.client.openapi.Configuration.setDefaultApiClient(this.apiClient);
+        this.coreV1Api = new CoreV1Api();
+        this.appsV1Api = new AppsV1Api();
+        this.extensionsV1beta1Api = new ExtensionsV1beta1Api();
+        this.exec = new Exec();
+
+
+        this.watchApiClient = Config.fromToken(k8s.getApiServer(), k8s.getToken(), k8s.getValidateSSL());
+        this.watchCoreV1Api = new CoreV1Api(watchApiClient);
+        this.watchAppsV1Api = new AppsV1Api(watchApiClient);
+    }
+
+    @Bean
+    public ApiClient apiClient() {
+        return apiClient;
+    }
+
+    @Bean
+    public CoreV1Api coreV1Api() {
+        return coreV1Api;
+    }
+
+    @Bean
+    public AppsV1Api appsV1Api() {
+        return appsV1Api;
+    }
+
+    @Bean
+    public ExtensionsV1beta1Api extensionsV1beta1Api() {
+        return extensionsV1beta1Api;
+    }
+
+    @Bean
+    public Exec exec() {
+        return exec;
+    }
+
+
+    @Bean
+    public ApiClient watchApiClient() {
+        return watchApiClient;
+    }
+
+    @Bean
+    public CoreV1Api watchCoreV1Api() {
+        return watchCoreV1Api;
+    }
+
+    @Bean
+    public AppsV1Api watchAppsV1Api() {
+        return watchAppsV1Api;
+    }
+}

+ 46 - 0
api/src/main/java/seecoder/devcloud/api/k8s/K8sConstants.java

@@ -0,0 +1,46 @@
+package seecoder.devcloud.api.k8s;
+
+public class K8sConstants {
+
+    public static final String NAME_PATTERN = "^[0-9a-zA-Z-]{8,}$";
+
+    //标签域名前缀
+    private static final String LABEL_PREFIX = "demo.seec.nju.cn";
+    //课程前缀
+    public static final String TYPE_LABEL = LABEL_PREFIX + "/type";
+    //描述
+    public static final String DESCRIPTION_LABEL = LABEL_PREFIX + "/description";
+    //应用
+    public static final String APPLICATION_LABEL = LABEL_PREFIX + "/app";
+
+    public static final String DEPLOY_ID_LABEL = LABEL_PREFIX + "/deployid";
+    // 用户namespace的label
+    public static final String USERSPACE_LABEL = LABEL_PREFIX +"/userspace";
+    // pod的label
+    public static final String SELECTOR_LABEL = LABEL_PREFIX + "/app";
+    // 非对外service的label
+    public static final String INTERNAL_LABEL = LABEL_PREFIX + "/internal";
+    // 对外service的label
+    public static final String EXTERNAL_LABEL = LABEL_PREFIX + "/external";
+    // 持久卷访问模式
+    public static final String STORAGE_ACCESS_MODE = "ReadWriteMany";
+    // 持久卷存储类型
+    public static final String STORAGE_CLASS = "glusterfs";
+
+    public static final String RESOURCE_STORAGE = "storage";
+
+    public static final String STARTS_AT_LABEL = LABEL_PREFIX + "/start";
+
+    public static final String ENDS_AT_LABEL = LABEL_PREFIX + "/end";
+
+    public static final String PRETTY_FORMAT = "true";
+
+    //cascade清除策略
+    //Deployment必须使用Foreground,否则只会删除Replica Set 不会删除底下的Pod
+    //https://github.com/kubernetes/kubeadm/issues/149#issuecomment-284766613
+    public static final String FOREGROUND_PROPAGATION_POLICY = "Foreground";
+
+    public static final String METADATA_NAME_SELECTOR = "metadata.name=";
+
+    public static final String DB_SECRET_NAME = "db-secret";
+}

+ 85 - 0
api/src/main/java/seecoder/devcloud/api/k8s/LogApi.java

@@ -0,0 +1,85 @@
+package seecoder.devcloud.api.k8s;
+
+import java.time.LocalDateTime;
+
+/**
+ * author: rale
+ * createdAt: 1/6/19
+ */
+public interface LogApi{
+
+    /**
+     * 获取namespace下pod中名为containerName的日志
+     * @param namespace
+     * @param pod
+     * @param containerName
+     * @return
+     */
+    String getAll(String namespace, String pod, String containerName);
+
+    /**
+     * 只适用于pod中仅有单个container的场景,它将会返回该container的日志
+     * 如果pod中有多个container,将抛出异常
+     * @param namespace
+     * @param pod
+     * @return
+     */
+    String getAll(String namespace, String pod);
+
+    /**
+     * 获取最后N条日志
+     * @param namespace
+     * @param pod
+     * @param containerName
+     * @param N
+     * @return
+     */
+    String getLastNLines(String namespace, String pod, String containerName, int N);
+
+    /**
+     * 获取最后N条日志
+     * @param namespace
+     * @param pod
+     * @param N
+     * @return
+     */
+    String getLastNLines(String namespace, String pod, int N);
+
+    /**
+     * 获取至今N秒内的日志
+     * @param namespace
+     * @param pod
+     * @param N
+     * @return
+     */
+    String getBeforeNSeconds(String namespace,String pod, String containerName, int N);
+
+    /**
+     * 获取至今N秒内的日志
+     * @param namespace
+     * @param pod
+     * @param N
+     * @return
+     */
+    String getBeforeNSeconds(String namespace, String pod, int N);
+
+    /**
+     * 获取time时间点之后namespace下pod中container的日志
+     * @param namespace
+     * @param pod
+     * @param container
+     * @param time
+     * @return
+     */
+    String getAfterTime(String namespace, String pod, String container, LocalDateTime time);
+
+    /**
+     * 获取time时间点之后namespace下pod中的日志
+     * 仅适用于pod中仅有一个容器
+     * @param namespace
+     * @param pod
+     * @param time
+     * @return
+     */
+    String getAfterTime(String namespace, String pod, LocalDateTime time);
+}

+ 8 - 0
api/src/main/java/seecoder/devcloud/api/k8s/NamespaceApi.java

@@ -0,0 +1,8 @@
+package seecoder.devcloud.api.k8s;
+
+
+import seecoder.devcloud.api.k8s.model.Namespace;
+
+public interface NamespaceApi extends AbstractApi<Namespace> {
+
+}

+ 7 - 0
api/src/main/java/seecoder/devcloud/api/k8s/PersistentVolumeClaimApi.java

@@ -0,0 +1,7 @@
+package seecoder.devcloud.api.k8s;
+
+
+import seecoder.devcloud.api.k8s.model.PersistentVolumeClaim;
+
+public interface PersistentVolumeClaimApi extends AbstractApi<PersistentVolumeClaim> {
+}

+ 7 - 0
api/src/main/java/seecoder/devcloud/api/k8s/PodApi.java

@@ -0,0 +1,7 @@
+package seecoder.devcloud.api.k8s;
+
+
+import seecoder.devcloud.api.k8s.model.Pod;
+
+public interface PodApi extends AbstractApi<Pod>{
+}

+ 83 - 0
api/src/main/java/seecoder/devcloud/api/k8s/SecretApi.java

@@ -0,0 +1,83 @@
+package seecoder.devcloud.api.k8s;
+
+
+import seecoder.devcloud.api.k8s.util.SecretTypeEnum;
+import seecoder.devcloud.api.k8s.vo.SecretVO;
+
+import java.util.List;
+import java.util.Map;
+
+public interface SecretApi {
+
+    /**
+     * 创建通用密钥,如数据库的用户名,密码,默认type为Opaque
+     * @param namespace
+     * @param name
+     * @param data
+     * @return ApiResult
+     */
+    SecretVO createGenericSecret(String namespace, String name, Map<String, byte[]> data);
+
+    /**
+     * 创建通用密钥,自己定义Secret的type,如Service Account、Opaque、kubernetes.io/dockerconfigjson.
+     * @param namespace
+     * @param name
+     * @param data
+     * @param type
+     * @return ApiResult
+     */
+    SecretVO createGenericSecret(String namespace, String name, Map<String, byte[]> data, SecretTypeEnum type);
+
+    /**
+     * 在namespace下生成私有仓库server的密钥
+     * 密钥名称将自动生成
+     * @param namespace
+     * @param server
+     * @param user
+     * @param password
+     */
+    SecretVO createPrivateRegistrySecret(String namespace, String server, String user, String password);
+
+
+    /**
+     * 在namespace下生成私有仓库server的密钥
+     * 密钥名称自己指定
+     * @param namespace
+     * @param name
+     * @param server
+     * @param user
+     * @param password
+     */
+    SecretVO createPrivateRegistrySecret(String namespace, String name, String server, String user, String password);
+
+    /**
+     * 获取指定namespace下的Secret列表
+     * @param namespace
+     */
+    List<SecretVO> getSecretList(String namespace);
+
+    /**
+     * 获取指定namespace下的指定type的Secret列表
+     *
+     * @param namespace
+     * @param type
+     */
+    List<SecretVO> getSecretListByType(String namespace, SecretTypeEnum type);
+
+
+    /**
+     * 获取指定namespace下的密钥
+     * 返回指定命名的Secret
+     * @param namespace
+     * @param name
+     */
+    SecretVO getSecretByName(String namespace, String name);
+
+    /**
+     * 删除指定namespace下指定名字的Secret
+     * @param namespace
+     * @param name
+     */
+    void deleteSecretByName(String namespace, String name);
+
+}

+ 8 - 0
api/src/main/java/seecoder/devcloud/api/k8s/ServiceApi.java

@@ -0,0 +1,8 @@
+package seecoder.devcloud.api.k8s;
+
+
+import seecoder.devcloud.api.k8s.model.Service;
+
+public interface ServiceApi extends AbstractApi<Service>{
+
+}

+ 17 - 0
api/src/main/java/seecoder/devcloud/api/k8s/WatchApi.java

@@ -0,0 +1,17 @@
+package seecoder.devcloud.api.k8s;
+
+/**
+ * author: rale
+ * createdAt: 1/13/19
+ */
+public interface WatchApi {
+
+    /**
+     * 会监听deployment的状态
+     * 超时时间十分钟
+     */
+    void watchDeployment();
+
+    void watchPod();
+
+}

+ 68 - 0
api/src/main/java/seecoder/devcloud/api/k8s/exception/K8sApiException.java

@@ -0,0 +1,68 @@
+package seecoder.devcloud.api.k8s.exception;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class K8sApiException extends RuntimeException {
+
+    private final int code;
+
+    private final String description;
+
+    public static final Map<Integer, String> codeMap = new HashMap<>();
+
+    public static final int NAMESPACE_NOT_EXIST = 400;
+
+    public static final int DEPLOYMENT_NOT_EXIST = 401;
+
+    public static final int NOT_FOUND = 404;
+
+    public static final int RESOURCE_QUOTA_NOT_EXIST = 402;
+
+    public static final int ALREADY_EXIST = 409;
+    //LimitRange的参数配置:default值不大于max的值,default request的值不小于min的值
+    public static final int INVALID_VALUE = 422;
+
+    public static final int BAD_REQUEST = 200;
+
+    public static final int SYSTEM_ERROR = 500;
+
+    public static final int COMMAND_FAIL = 300;
+
+    public static final int NOT_SUPPORT_METHOD = 100;
+
+
+    public static final K8sApiException K8s_SYSTEM_ERROR_EXCEPTION = new K8sApiException(SYSTEM_ERROR);
+
+    public static final K8sApiException K8s_NAMESPACE_NOT_EXIST = new K8sApiException(NAMESPACE_NOT_EXIST);
+
+    public static final K8sApiException K8s_NOT_SUPPORT_METHOD = new K8sApiException(NOT_SUPPORT_METHOD);
+
+    //系统故障
+    static {
+        codeMap.put(NOT_FOUND, "资源不存在");
+        codeMap.put(RESOURCE_QUOTA_NOT_EXIST, "资源限额不存在");
+        codeMap.put(ALREADY_EXIST, "资源已存在");
+        codeMap.put(INVALID_VALUE, "参数设置无效");
+        codeMap.put(BAD_REQUEST, "请求参数非法");
+        codeMap.put(SYSTEM_ERROR, "系统异常");
+        codeMap.put(COMMAND_FAIL, "指令执行失败");
+        codeMap.put(NOT_SUPPORT_METHOD, "不支持的方法");
+    }
+    public K8sApiException(int code, String description){
+        this.code = code;
+        this.description = description;
+    }
+
+    public K8sApiException(int code) {
+        this.code = code;
+        this.description = codeMap.getOrDefault(code, "操作执行失败");
+    }
+
+
+}

+ 176 - 0
api/src/main/java/seecoder/devcloud/api/k8s/impl/ConfigMapApiImpl.java

@@ -0,0 +1,176 @@
+package seecoder.devcloud.api.k8s.impl;
+
+
+import com.google.gson.JsonSyntaxException;
+
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.models.V1ConfigMap;
+import io.kubernetes.client.openapi.models.V1ConfigMapList;
+import io.kubernetes.client.openapi.models.V1DeleteOptions;
+import io.kubernetes.client.openapi.models.V1DeleteOptionsBuilder;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import seecoder.devcloud.api.k8s.exception.K8sApiException;
+import seecoder.devcloud.api.k8s.ConfigMapApi;
+import seecoder.devcloud.api.k8s.model.ConfigMap;
+import seecoder.devcloud.api.k8s.model.K8sObjectRequest;
+import seecoder.devcloud.api.k8s.model.LabelSelector;
+import seecoder.devcloud.common.util.LoggerUtil;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static seecoder.devcloud.api.k8s.K8sConstants.FOREGROUND_PROPAGATION_POLICY;
+import static seecoder.devcloud.api.k8s.K8sConstants.PRETTY_FORMAT;
+
+
+@Service
+public class ConfigMapApiImpl implements ConfigMapApi {
+
+    private static final Logger logger = LoggerUtil.getLogger(ConfigMapApiImpl.class);
+
+    private CoreV1Api coreV1Api;
+
+    @Autowired
+    public ConfigMapApiImpl(CoreV1Api coreV1Api) {
+        this.coreV1Api = coreV1Api;
+    }
+
+    @Override
+    public ConfigMap create(ConfigMap configMap) {
+        try {
+            V1ConfigMap v1ConfigMap = configMap.toK8sObject();
+            V1ConfigMap result = coreV1Api.createNamespacedConfigMap(configMap.getNamespace(), v1ConfigMap, PRETTY_FORMAT, null, null);
+            return result == null ? null : new ConfigMap(result);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "configMap创建失败, configMap={}, response={}", configMap, e.getResponseBody());
+
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                throw new K8sApiException(K8sApiException.NOT_FOUND, "namespace不存在");
+            } else if (e.getCode() == K8sApiException.ALREADY_EXIST) {
+                throw new K8sApiException(K8sApiException.ALREADY_EXIST, "configMap已经存在");
+            } else {
+                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "configMap创建异常, configMap={}", configMap);
+            throw e;
+        }
+    }
+
+    @Override
+    public ConfigMap update(ConfigMap configMap) {
+        try {
+            V1ConfigMap v1ConfigMap = getV1ConfigMap(configMap.getNamespace(), configMap.getName());
+            configMap.merge(v1ConfigMap);
+            V1ConfigMap result = coreV1Api.replaceNamespacedConfigMap(configMap.getName(), configMap.getNamespace(), v1ConfigMap, PRETTY_FORMAT, null, null);
+            return result == null ? null : new ConfigMap(result);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "configMap更新失败, configMap={}, response={}", configMap, e.getResponseBody());
+
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                throw new K8sApiException(K8sApiException.NOT_FOUND, "namespace不存在");
+            } else if (e.getCode() == K8sApiException.ALREADY_EXIST) {
+                throw new K8sApiException(K8sApiException.ALREADY_EXIST, "configMap已经存在");
+            } else {
+                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "configMap更新异常, configMap={}", configMap);
+            throw e;
+        }
+    }
+
+    @Override
+    public void delete(ConfigMap configMap) {
+        try {
+            V1DeleteOptions v1DeleteOptions = new V1DeleteOptionsBuilder()
+                    .withApiVersion(ConfigMap.API_VERSION)
+                    .withPropagationPolicy(FOREGROUND_PROPAGATION_POLICY)
+                    .build();
+            coreV1Api.deleteNamespacedConfigMap(
+                    configMap.getName(),
+                    configMap.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    FOREGROUND_PROPAGATION_POLICY,
+                    v1DeleteOptions);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "删除ConfigMap失败,namespace={}, name={}, response={}", configMap.getNamespace(), configMap.getName(), e.getResponseBody());
+
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                return;
+            }else {
+                return;
+            }
+        } catch (JsonSyntaxException e) {
+            //do nothing
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "删除ConfigMap异常,namespace={}, name={}", configMap.getNamespace(), configMap.getName());
+            return;
+        }
+    }
+
+    @Override
+    public List<ConfigMap> getByCondition(K8sObjectRequest request) {
+        try {
+            if (request.getName() != null) {
+                V1ConfigMap obj = coreV1Api.readNamespacedConfigMap(
+                        request.getName(),
+                        request.getNamespace(),
+                        PRETTY_FORMAT,
+                        null,
+                        null);
+                return obj == null ? Collections.emptyList() : Collections.singletonList(new ConfigMap(obj));
+            }
+            LabelSelector selector = null;
+            if (request.getSelector() != null) {
+                selector = request.getSelector();
+            }
+            if (request.getLabels() != null) {
+                if (selector == null) {
+                    selector = new LabelSelector();
+                }
+                selector.addAll(request.getLabels());
+            }
+            V1ConfigMapList objList = coreV1Api.listNamespacedConfigMap(
+                    request.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    selector.toJsonString(),
+                    null,
+                    null,
+                    null,
+                    null);
+            return objList.getItems().stream().map(ConfigMap::new).collect(Collectors.toList());
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "查找configMap失败,namespace = {}, response={}",
+                    request.getNamespace(), e.getResponseBody());
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                return Collections.emptyList();
+            }else {
+                return Collections.emptyList();
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "查找configMap失败,namespace = {}, name={}",
+                    request.getNamespace());
+            return Collections.emptyList();
+        }
+    }
+
+    private V1ConfigMap getV1ConfigMap(String namespace, String name) throws ApiException {
+        return coreV1Api.readNamespacedConfigMap(
+                name,
+                namespace,
+                PRETTY_FORMAT,
+                false,
+                false);
+    }
+}

+ 200 - 0
api/src/main/java/seecoder/devcloud/api/k8s/impl/DeploymentApiImpl.java

@@ -0,0 +1,200 @@
+package seecoder.devcloud.api.k8s.impl;
+
+import com.google.gson.JsonSyntaxException;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.AppsV1Api;
+import io.kubernetes.client.openapi.models.V1DeleteOptions;
+import io.kubernetes.client.openapi.models.V1DeleteOptionsBuilder;
+import io.kubernetes.client.openapi.models.V1Deployment;
+import io.kubernetes.client.openapi.models.V1DeploymentList;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import seecoder.devcloud.api.k8s.DeploymentApi;
+import seecoder.devcloud.api.k8s.exception.K8sApiException;
+import seecoder.devcloud.api.k8s.model.Deployment;
+import seecoder.devcloud.api.k8s.model.DeploymentStatus;
+import seecoder.devcloud.api.k8s.model.K8sObjectRequest;
+import seecoder.devcloud.api.k8s.model.LabelSelector;
+import seecoder.devcloud.common.util.LoggerUtil;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static seecoder.devcloud.api.k8s.K8sConstants.FOREGROUND_PROPAGATION_POLICY;
+import static seecoder.devcloud.api.k8s.K8sConstants.PRETTY_FORMAT;
+
+
+/**
+ * author: rale
+ * createdAt: 12/23/18
+ */
+@Service
+public class DeploymentApiImpl implements DeploymentApi {
+
+    private static final Logger logger = LoggerUtil.getLogger(DeploymentApi.class);
+
+    private AppsV1Api appsV1Api;
+
+    @Autowired
+    public DeploymentApiImpl(AppsV1Api appsV1Api) {
+        this.appsV1Api = appsV1Api;
+    }
+
+    /**
+     * 异步创建Deployment
+     * @param deployment
+     * @throws K8sApiException NOT_FOUND namespace不存在
+     * @throws K8sApiException ALREADY_EXIST deployment已经存在
+     * @throws K8sApiException SYSTEM_ERROR 系统异常
+     */
+    @Override
+    public Deployment create(Deployment deployment) {
+        try {
+            V1Deployment v1Deployment = deployment.toK8sObject();
+            V1Deployment result = appsV1Api.createNamespacedDeployment(deployment.getNamespace(),v1Deployment, PRETTY_FORMAT, null, null);
+            return result == null ? null : new Deployment(result);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "deployment创建失败, deployment={}, response={}", deployment, e.getResponseBody());
+
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                throw new K8sApiException(K8sApiException.NOT_FOUND, "namespace不存在");
+            } else if (e.getCode() == K8sApiException.ALREADY_EXIST) {
+                throw new K8sApiException(K8sApiException.ALREADY_EXIST, "deployment已经存在");
+            } else {
+                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "deployment创建异常, deployment={}", deployment);
+            throw e;
+        }
+    }
+
+    /**
+     * 替换现有的Deployment
+     * @param deployment
+     * @throws K8sApiException NOT_FOUND namespace不存在
+     * @throws K8sApiException SYSTEM_ERROR 系统异常
+     * @return
+     */
+    @Override
+    public Deployment update(Deployment deployment) {
+        try {
+            //replace vs patch
+            //replace方法需要明确所有变量的更新值,包括hostIP等,因此需要执行merge方法来合并更新
+            //patch本身会保有原来的配置,但是会用annotation记录更新的部分进行比较 需要生成patch语句
+            //需继续评估二者的选择
+            V1Deployment v1Deployment = getV1Deployment(deployment.getNamespace(), deployment.getName());
+            deployment.merge(v1Deployment);
+            v1Deployment = appsV1Api.replaceNamespacedDeployment(deployment.getName(), deployment.getNamespace(), v1Deployment, PRETTY_FORMAT, null, null);
+            return new Deployment(v1Deployment);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "deployment更新异常, deployment={}, response={}", deployment,  e.getResponseBody());
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                throw new K8sApiException(K8sApiException.NOT_FOUND);
+            } else {
+                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "deployment更新异常, deployment={}", deployment);
+            throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+        }
+    }
+
+    @Override
+    public void delete(Deployment deployment) {
+        try {
+            V1DeleteOptions v1DeleteOptions = new V1DeleteOptionsBuilder()
+                    .withApiVersion(Deployment.API_VERSION)
+                    .withPropagationPolicy(FOREGROUND_PROPAGATION_POLICY)
+                    .build();
+            appsV1Api.deleteNamespacedDeployment(
+                    deployment.getName(),
+                    deployment.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    FOREGROUND_PROPAGATION_POLICY,
+                    v1DeleteOptions);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "删除Deployment失败,namespace={}, name={}, response={}", deployment.getNamespace(), deployment.getName(), e.getResponseBody());
+
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                return;
+            }else {
+                return;
+            }
+        } catch (JsonSyntaxException e) {
+            //do nothing
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "删除Deployment异常,namespace={}, name={}", deployment.getNamespace(), deployment.getName());
+            return;
+        }
+    }
+
+    @Override
+    public List<Deployment> getByCondition(K8sObjectRequest request) {
+        try {
+            if (request.getName() != null) {
+                V1Deployment obj = appsV1Api.readNamespacedDeployment(
+                        request.getName(),
+                        request.getNamespace(),
+                        PRETTY_FORMAT,
+                        null,
+                        null);
+                return obj == null ? Collections.emptyList() : Collections.singletonList(new Deployment(obj));
+            }
+            LabelSelector selector = null;
+            if (request.getSelector() != null) {
+                selector = request.getSelector();
+            }
+            if (request.getLabels() != null) {
+                if (selector == null) {
+                    selector = new LabelSelector();
+                }
+                selector.addAll(request.getLabels());
+            }
+            V1DeploymentList objList = appsV1Api.listNamespacedDeployment(
+                    request.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    selector.toJsonString(),
+                    null,
+                    null,
+                    null,
+                    null);
+            return objList.getItems().stream().map(Deployment::new).collect(Collectors.toList());
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "查找deployment失败,namespace = {}, response={}",
+                    request.getNamespace(), e.getResponseBody());
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                return Collections.emptyList();
+            }else {
+                return Collections.emptyList();
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "查找deployment失败,namespace = {}, name={}",
+                    request.getNamespace());
+            return Collections.emptyList();
+        }
+    }
+
+
+    @Override
+    public DeploymentStatus getStatus(String namespace, String name) {
+        return null;
+    }
+
+    private V1Deployment getV1Deployment(String namespace, String name) throws ApiException {
+        return appsV1Api.readNamespacedDeployment(
+                name,
+                namespace,
+                PRETTY_FORMAT,
+                false,
+                false);
+    }
+}

+ 99 - 0
api/src/main/java/seecoder/devcloud/api/k8s/impl/ExecApiImpl.java

@@ -0,0 +1,99 @@
+package seecoder.devcloud.api.k8s.impl;
+
+import io.kubernetes.client.Exec;
+import io.kubernetes.client.openapi.ApiException;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import seecoder.devcloud.api.k8s.ExecApi;
+import seecoder.devcloud.api.k8s.exception.K8sApiException;
+import seecoder.devcloud.common.util.LoggerUtil;
+import seecoder.devcloud.api.nexus.exception.NexusApiException;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+
+@Service
+public class ExecApiImpl implements ExecApi {
+
+    private static final Logger logger = LoggerUtil.getLogger(ExecApiImpl.class);
+
+    private static final int DEFAULT_TIME_OUT_IN_SECOND = 30;
+    private final Exec exec;
+
+    @Autowired
+    public ExecApiImpl(Exec exec) {
+        this.exec = exec;
+    }
+
+    @Override
+    public boolean exec(String namespace, String podName, String containerName, List<String> commands) {
+        return false;
+    }
+
+    @Override
+    public boolean exec(String namespace, String podName, List<String> commands){
+        try {
+            final Process proc =
+                    exec.exec(
+                            namespace,
+                            podName,
+                            commands.isEmpty()
+                                    ? new String[]{"sh"}
+                                    : commands.toArray(new String[commands.size()]),
+                            true,
+                            false);
+            proc.waitFor(DEFAULT_TIME_OUT_IN_SECOND, TimeUnit.SECONDS);
+            proc.destroy();
+            return proc.exitValue() == 0;
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "指令执行异常, namespace={}, podName={}, commands={}, response={}",
+                namespace, podName, commands, e.getResponseBody());
+            if (e.getCode() == NexusApiException.NOT_FOUND) {
+                throw new K8sApiException(NexusApiException.NOT_FOUND, "容器不存在");
+            }else{
+                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (InterruptedException | IOException e) {
+            LoggerUtil.error(logger, e, "指令执行异常, namespace={}, podName={}, commands={}",
+                    namespace, podName, commands);
+            throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "指令执行异常, namespace={}, podName={}, commands={}",
+                    namespace, podName, commands);
+        }
+        return false;
+    }
+
+    @Override
+    public Process getTerminal(String namespace, String podName) {
+        try {
+            final Process proc =
+                    exec.exec(
+                            namespace,
+                            podName,
+                            new String[]{"sh"},
+                            true,
+                            true);
+            return proc;
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "指令执行异常, namespace={}, podName={}, response={}",
+                    namespace, podName, e.getResponseBody());
+            if (e.getCode() == NexusApiException.NOT_FOUND) {
+                throw new K8sApiException(NexusApiException.NOT_FOUND, "容器不存在");
+            }else{
+                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (IOException e) {
+            LoggerUtil.error(logger, e, "指令执行异常, namespace={}, podName={}",
+                    namespace, podName);
+            throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "指令执行异常, namespace={}, podName={}",
+                    namespace, podName);
+        }
+        return null;
+    }
+}

+ 192 - 0
api/src/main/java/seecoder/devcloud/api/k8s/impl/IngressApiImpl.java

@@ -0,0 +1,192 @@
+package seecoder.devcloud.api.k8s.impl;
+
+
+import com.google.gson.JsonSyntaxException;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.ExtensionsV1beta1Api;
+import io.kubernetes.client.openapi.models.ExtensionsV1beta1Ingress;
+import io.kubernetes.client.openapi.models.ExtensionsV1beta1IngressList;
+import io.kubernetes.client.openapi.models.V1DeleteOptions;
+import io.kubernetes.client.openapi.models.V1DeleteOptionsBuilder;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import seecoder.devcloud.api.k8s.IngressApi;
+import seecoder.devcloud.api.k8s.exception.K8sApiException;
+import seecoder.devcloud.api.k8s.model.Ingress;
+import seecoder.devcloud.api.k8s.model.K8sObjectRequest;
+import seecoder.devcloud.api.k8s.model.LabelSelector;
+import seecoder.devcloud.common.util.LoggerUtil;
+import seecoder.devcloud.api.nexus.exception.NexusApiException;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static seecoder.devcloud.api.k8s.K8sConstants.FOREGROUND_PROPAGATION_POLICY;
+import static seecoder.devcloud.api.k8s.K8sConstants.PRETTY_FORMAT;
+
+
+/**
+ * author: rale
+ * createdAt: 12/24/18
+ */
+@Service
+public class IngressApiImpl implements IngressApi {
+    private static final Logger logger = LoggerUtil.getLogger(IngressApi.class);
+
+    private final ExtensionsV1beta1Api extensionsV1beta1Api;
+
+    @Autowired
+    public IngressApiImpl(ExtensionsV1beta1Api extensionsV1beta1Api){
+        this.extensionsV1beta1Api = extensionsV1beta1Api;
+    }
+
+    /**
+     * 创建Ingress资源
+     * @param ingress
+     * @throws K8sApiException NOT_FOUND 命名空间不存在
+     * @throws K8sApiException ALREADY_EXIST ingress已经存在
+     * @throws K8sApiException SYSTEM_ERROR 系统异常
+     */
+    @Override
+    public Ingress create(Ingress ingress) {
+        ExtensionsV1beta1Ingress ExtensionsV1beta1Ingress = ingress.toK8sObject();
+
+        try {
+            ExtensionsV1beta1Ingress result = extensionsV1beta1Api.createNamespacedIngress(
+                    ingress.getNamespace(),
+                    ExtensionsV1beta1Ingress,
+                    PRETTY_FORMAT,
+                    null,
+                    null);
+            return result == null ? null : new Ingress(result);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "ingress 创建失败,ingress={}, response={}", ingress, e.getResponseBody());
+            if (e.getCode() == NexusApiException.NOT_FOUND) {
+                throw new K8sApiException(NexusApiException.NOT_FOUND);
+            } else if (e.getCode() == K8sApiException.ALREADY_EXIST) {
+                throw new K8sApiException(K8sApiException.ALREADY_EXIST);
+            } else {
+                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "ingress 创建异常,ingress={}", ingress);
+            throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+        }
+    }
+
+
+    /**
+     * 更新Ingress资源
+     * @param ingress
+     * @throws K8sApiException NOT_FOUND 资源不存在
+     * @throws K8sApiException SYSTEM_ERROR 系统异常
+     * @return
+     * @todo bugfix replace 需要merge字段
+     */
+    @Override
+    public Ingress update(Ingress ingress) {
+        ExtensionsV1beta1Ingress ExtensionsV1beta1Ingress = ingress.toK8sObject();
+        try {
+            ExtensionsV1beta1Ingress = extensionsV1beta1Api.replaceNamespacedIngress(
+                    ingress.getName(),
+                    ingress.getNamespace(),
+                    ExtensionsV1beta1Ingress,
+                    PRETTY_FORMAT,
+                    null,
+                    null);
+            return new Ingress(ExtensionsV1beta1Ingress);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "ingress更新失败,ingress={}, response={}", ingress, e.getResponseBody());
+            if (e.getCode() == NexusApiException.NOT_FOUND) {
+                throw new K8sApiException(NexusApiException.NOT_FOUND, "资源不存在");
+            }else {
+                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "ingress更新失败,ingress={}", ingress);
+            throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+        }
+    }
+
+    @Override
+    public void delete(Ingress ingress) {
+        try {
+            V1DeleteOptions v1DeleteOptions = new V1DeleteOptionsBuilder()
+                    .withApiVersion(Ingress.API_VERSION)
+                    .withPropagationPolicy(FOREGROUND_PROPAGATION_POLICY)
+                    .build();
+            extensionsV1beta1Api.deleteNamespacedIngress(
+                    ingress.getName(),
+                    ingress.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    FOREGROUND_PROPAGATION_POLICY,
+                    v1DeleteOptions);
+        }catch (ApiException e) {
+            LoggerUtil.error(logger, e, "ingress 删除失败,name={}, namespace={}, response={}", ingress.getName(), ingress.getNamespace(), e.getResponseBody());
+            if (e.getCode() == NexusApiException.NOT_FOUND) {
+                return;
+            }else {
+                return;
+            }
+        }catch (JsonSyntaxException e) {
+            //do nothing
+        }catch (Exception e) {
+            LoggerUtil.error(logger, e, "ingress 删除异常,name={}, namespace={}", ingress.getName(), ingress.getNamespace());
+            return;
+        }
+    }
+
+    @Override
+    public List<Ingress> getByCondition(K8sObjectRequest request) {
+        try {
+            if (request.getName() != null) {
+                ExtensionsV1beta1Ingress obj = extensionsV1beta1Api.readNamespacedIngress(
+                        request.getName(),
+                        request.getNamespace(),
+                        PRETTY_FORMAT,
+                        null,
+                        null);
+                return obj == null ? Collections.emptyList() : Collections.singletonList(new Ingress(obj));
+            }
+            LabelSelector selector = null;
+            if (request.getSelector() != null) {
+                selector = request.getSelector();
+            }
+            if (request.getLabels() != null) {
+                if (selector == null) {
+                    selector = new LabelSelector();
+                }
+                selector.addAll(request.getLabels());
+            }
+            ExtensionsV1beta1IngressList objList = extensionsV1beta1Api.listNamespacedIngress(
+                    request.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    selector.toJsonString(),
+                    null,
+                    null,
+                    null,
+                    null);
+            return objList.getItems().stream().map(Ingress::new).collect(Collectors.toList());
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "查找ingresses失败,namespace = {}, response={}",
+                    request.getNamespace(), e.getResponseBody());
+            if (e.getCode() == NexusApiException.NOT_FOUND) {
+                return Collections.emptyList();
+            } else {
+                return Collections.emptyList();
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "查找ingresses失败,namespace = {}, name={}",
+                    request.getNamespace());
+            return Collections.emptyList();
+        }
+    }
+}

+ 107 - 0
api/src/main/java/seecoder/devcloud/api/k8s/impl/LogApiImpl.java

@@ -0,0 +1,107 @@
+package seecoder.devcloud.api.k8s.impl;
+
+
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import seecoder.devcloud.api.k8s.LogApi;
+import seecoder.devcloud.api.k8s.exception.K8sApiException;
+import seecoder.devcloud.common.util.LoggerUtil;
+
+import java.time.LocalDateTime;
+
+import static seecoder.devcloud.api.k8s.K8sConstants.PRETTY_FORMAT;
+
+
+/**
+ * author: rale
+ * createdAt: 1/6/19
+ */
+@Service
+public class LogApiImpl implements LogApi {
+
+    private static final Logger logger = LoggerUtil.getLogger(LogApiImpl.class);
+
+    private final CoreV1Api coreV1Api;
+
+    @Autowired
+    public LogApiImpl(CoreV1Api coreV1Api) {
+        this.coreV1Api = coreV1Api;
+    }
+
+    @Override
+    public String getAll(String namespace, String pod, String containerName) {
+        return get(namespace, pod, containerName, null, null);
+    }
+
+    @Override
+    public String getAll(String namespace, String pod) {
+        return getAll(namespace, pod, null);
+    }
+
+    @Override
+    public String getLastNLines(String namespace, String pod, String containerName, int N) {
+        return get(namespace, pod, containerName, N, null);
+    }
+
+    @Override
+    public String getLastNLines(String namespace, String pod, int N) {
+        return getLastNLines(namespace, pod, null, N);
+    }
+
+    @Override
+    public String getBeforeNSeconds(String namespace, String pod, String containerName, int N) {
+        return get(namespace, pod, containerName, null, N);
+    }
+
+    @Override
+    public String getBeforeNSeconds(String namespace, String pod, int N) {
+        return getBeforeNSeconds(namespace, pod, null, N);
+    }
+
+    @Override
+    public String getAfterTime(String namespace, String pod, String container, LocalDateTime time) {
+
+        return null;
+    }
+
+    @Override
+    public String getAfterTime(String namespace, String pod, LocalDateTime time) {
+        return null;
+    }
+
+    private String get(String namespace, String pod, String containerName, Integer NLastLines, Integer sinceNSecondsBefore) {
+        try {
+            return coreV1Api.readNamespacedPodLog(
+                    pod,
+                    namespace,
+                    containerName,
+                    null,
+                    null,
+                    null,
+                    PRETTY_FORMAT,
+                    false,
+                    sinceNSecondsBefore,
+                    NLastLines,
+                    null);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "获取日志异常,namespace={}, pod={}, containerName={}, NLastLines={}, sinceNSecondsBefore={}",
+                    namespace, pod, containerName, NLastLines, sinceNSecondsBefore);
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                throw new K8sApiException(K8sApiException.NOT_FOUND);
+            } if (e.getCode() == K8sApiException.BAD_REQUEST) {
+                throw new K8sApiException(K8sApiException.BAD_REQUEST);
+            }else {
+                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "获取日志异常,namespace={}, pod={}, containerName={}",
+                    namespace, pod, containerName);
+            throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+        }
+
+    }
+
+}

+ 145 - 0
api/src/main/java/seecoder/devcloud/api/k8s/impl/NamespaceApiImpl.java

@@ -0,0 +1,145 @@
+package seecoder.devcloud.api.k8s.impl;
+
+
+import com.google.gson.JsonSyntaxException;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.models.V1DeleteOptions;
+import io.kubernetes.client.openapi.models.V1DeleteOptionsBuilder;
+import io.kubernetes.client.openapi.models.V1Namespace;
+import io.kubernetes.client.openapi.models.V1NamespaceList;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import seecoder.devcloud.api.k8s.NamespaceApi;
+import seecoder.devcloud.api.k8s.exception.K8sApiException;
+import seecoder.devcloud.api.k8s.model.K8sObjectRequest;
+import seecoder.devcloud.api.k8s.model.LabelSelector;
+import seecoder.devcloud.api.k8s.model.Namespace;
+import seecoder.devcloud.common.util.LoggerUtil;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static seecoder.devcloud.api.k8s.K8sConstants.FOREGROUND_PROPAGATION_POLICY;
+import static seecoder.devcloud.api.k8s.K8sConstants.PRETTY_FORMAT;
+
+
+@Service
+public class NamespaceApiImpl implements NamespaceApi {
+
+    private static final Logger logger = LoggerUtil.getLogger(NamespaceApi.class);
+
+    private final CoreV1Api coreV1Api;
+
+    @Autowired
+    public NamespaceApiImpl(CoreV1Api coreV1Api) {
+        this.coreV1Api = coreV1Api;
+    }
+
+    /**
+     * 新建命名空间
+     * @param namespace 命名空间名称需要唯一
+     * @return
+     * @throws K8sApiException 命名空间已经存在
+     * @throws K8sApiException 系统异常
+     */
+    @Override
+    public Namespace create(Namespace namespace) {
+        V1Namespace v1Namespace = namespace.toK8sObject();
+        try {
+            V1Namespace result = coreV1Api.createNamespace(v1Namespace, PRETTY_FORMAT, null, null);
+            return result == null ? null : new Namespace(result);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "Namespace创建失败,namespace={}, v1Namespace={}, response={}", namespace, v1Namespace, e.getResponseBody());
+            if (e.getCode() == K8sApiException.ALREADY_EXIST) {
+                throw new K8sApiException(K8sApiException.ALREADY_EXIST);
+            } else {
+                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "Namespace创建异常, namespace={}, v1Namespace={}", namespace, v1Namespace);
+            throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+        }
+    }
+
+    @Override
+    public Namespace update(Namespace namespace) {
+        throw K8sApiException.K8s_NOT_SUPPORT_METHOD;
+    }
+
+    @Override
+    public void delete(Namespace namespace) {
+        try {
+            V1DeleteOptions v1DeleteOptions = new V1DeleteOptionsBuilder()
+                    .withApiVersion(Namespace.API_VERSION)
+                    .withPropagationPolicy(FOREGROUND_PROPAGATION_POLICY)
+                    .build();
+            coreV1Api.deleteNamespace(
+                    namespace.getName(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    FOREGROUND_PROPAGATION_POLICY,
+                    v1DeleteOptions);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "Namespace删除失败,name={}, response={}", namespace.getName() , e.getResponseBody());
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                return;
+            }else {
+                return;
+            }
+        } catch (JsonSyntaxException e){
+            //do nothing
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "Namespace删除异常, name={}", namespace.getName());
+            return;
+        }
+    }
+
+    @Override
+    public List<Namespace> getByCondition(K8sObjectRequest request) {
+        try {
+            if (request.getName() != null) {
+                V1Namespace v1Namespace = coreV1Api.readNamespace(
+                        request.getName(),
+                        PRETTY_FORMAT,
+                        null,
+                        null);
+                return v1Namespace == null ? Collections.emptyList() : Collections.singletonList(new Namespace(v1Namespace));
+            }
+            LabelSelector selector = null;
+            if (request.getSelector() != null) {
+                selector = request.getSelector();
+            }
+            if (request.getLabels() != null) {
+                if (selector == null) {
+                    selector = new LabelSelector();
+                }
+                selector.addAll(request.getLabels());
+            }
+            V1NamespaceList v1NamespaceList = coreV1Api.listNamespace(
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    selector.toJsonString(),
+                    null,
+                    null,
+                    null,
+                    null);
+            List<Namespace> namespaces =  v1NamespaceList.getItems().stream().map(Namespace::new).collect(Collectors.toList());
+            LoggerUtil.info(logger,"result={}", namespaces);
+            return namespaces;
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "获取全部Namespace异常, response={}", e.getResponseBody());
+            return Collections.emptyList();
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "查询所有Namespace失败");
+            return Collections.emptyList();
+        }
+    }
+
+}

+ 184 - 0
api/src/main/java/seecoder/devcloud/api/k8s/impl/PersistentVolumeClaimImpl.java

@@ -0,0 +1,184 @@
+package seecoder.devcloud.api.k8s.impl;
+
+
+import com.google.gson.JsonSyntaxException;
+import io.kubernetes.client.custom.V1Patch;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.models.V1DeleteOptions;
+import io.kubernetes.client.openapi.models.V1DeleteOptionsBuilder;
+import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim;
+import io.kubernetes.client.openapi.models.V1PersistentVolumeClaimList;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import seecoder.devcloud.api.k8s.DeploymentApi;
+import seecoder.devcloud.api.k8s.PersistentVolumeClaimApi;
+import seecoder.devcloud.api.k8s.exception.K8sApiException;
+import seecoder.devcloud.api.k8s.model.K8sObjectRequest;
+import seecoder.devcloud.api.k8s.model.LabelSelector;
+import seecoder.devcloud.api.k8s.model.PersistentVolumeClaim;
+import seecoder.devcloud.common.util.LoggerUtil;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static seecoder.devcloud.api.k8s.K8sConstants.FOREGROUND_PROPAGATION_POLICY;
+import static seecoder.devcloud.api.k8s.K8sConstants.PRETTY_FORMAT;
+
+
+@Service
+public class PersistentVolumeClaimImpl implements PersistentVolumeClaimApi {
+
+    private static final Logger logger = LoggerUtil.getLogger(DeploymentApi.class);
+
+    private final CoreV1Api coreV1Api;
+
+    @Autowired
+    public PersistentVolumeClaimImpl(CoreV1Api coreV1Api) {
+        this.coreV1Api = coreV1Api;
+    }
+
+    @Override
+    public PersistentVolumeClaim create(PersistentVolumeClaim persistentVolumeClaim) {
+        try {
+            V1PersistentVolumeClaim v1PersistentVolumeClaim = persistentVolumeClaim.toK8sObject();
+            V1PersistentVolumeClaim result = coreV1Api.createNamespacedPersistentVolumeClaim(
+                    persistentVolumeClaim.getNamespace(),
+                    v1PersistentVolumeClaim,
+                    PRETTY_FORMAT,
+                    null,
+                    null);
+            return result == null ? null : new PersistentVolumeClaim(result);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "pvc 创建失败, persistentVolumeClaim={}, response={}", persistentVolumeClaim, e.getResponseBody());
+
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                throw new K8sApiException(K8sApiException.NOT_FOUND, "namespace不存在");
+            } else if (e.getCode() == K8sApiException.ALREADY_EXIST) {
+                throw new K8sApiException(K8sApiException.ALREADY_EXIST, "persistentVolumeClaim已经存在");
+            } else {
+                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "pvc 创建异常, persistentVolumeClaim={}", persistentVolumeClaim);
+            throw e;
+        }
+    }
+
+
+    @Override
+    public PersistentVolumeClaim update(PersistentVolumeClaim persistentVolumeClaim) {
+        try {
+            V1PersistentVolumeClaim v1PersistentVolumeClaim = getV1PersistenceVolumeClaim(persistentVolumeClaim.getNamespace(), persistentVolumeClaim.getName());
+            persistentVolumeClaim.merge(v1PersistentVolumeClaim);
+            V1Patch v1Patch = new V1Patch("[{\"op\":\"replace\",\"path\":\"/spec/resources/requests/storage\",\"value\":\""+ persistentVolumeClaim.getQuantity().toSuffixedString() +"\"}]");
+            V1PersistentVolumeClaim result = coreV1Api.patchNamespacedPersistentVolumeClaim(persistentVolumeClaim.getName(), persistentVolumeClaim.getNamespace(), v1Patch, PRETTY_FORMAT, null, null, null);
+            return result == null ? null : new PersistentVolumeClaim(result);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "persistentVolumeClaim 更新失败, persistentVolumeClaim={}, response={}", persistentVolumeClaim, e.getResponseBody());
+
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                throw new K8sApiException(K8sApiException.NOT_FOUND, "namespace不存在");
+            } else if (e.getCode() == K8sApiException.ALREADY_EXIST) {
+                throw new K8sApiException(K8sApiException.ALREADY_EXIST, "persistentVolumeClaim 已经存在");
+            } else {
+                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "persistentVolumeClaim 更新异常, persistentVolumeClaim={}", persistentVolumeClaim);
+            throw e;
+        }
+    }
+
+    @Override
+    public void delete(PersistentVolumeClaim persistentVolumeClaim) {
+        try {
+            V1DeleteOptions v1DeleteOptions = new V1DeleteOptionsBuilder()
+                    .withApiVersion(PersistentVolumeClaim.API_VERSION)
+                    .withPropagationPolicy(FOREGROUND_PROPAGATION_POLICY)
+                    .build();
+            coreV1Api.deleteNamespacedPersistentVolumeClaim(
+                    persistentVolumeClaim.getName(),
+                    persistentVolumeClaim.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    FOREGROUND_PROPAGATION_POLICY,
+                    v1DeleteOptions);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "删除 PersistentVolumeClaim 失败,namespace={}, name={}, response={}", persistentVolumeClaim.getNamespace(), persistentVolumeClaim.getName(), e.getResponseBody());
+
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                return;
+            }else {
+                return;
+            }
+        } catch (JsonSyntaxException e) {
+            //do nothing
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "删除 PersistentVolumeClaim 异常,namespace={}, name={}", persistentVolumeClaim.getNamespace(), persistentVolumeClaim.getName());
+            return;
+        }
+    }
+
+    @Override
+    public List<PersistentVolumeClaim> getByCondition(K8sObjectRequest request) {
+        try {
+            if (request.getName() != null) {
+                V1PersistentVolumeClaim obj = coreV1Api.readNamespacedPersistentVolumeClaim(
+                        request.getName(),
+                        request.getNamespace(),
+                        PRETTY_FORMAT,
+                        null,
+                        null);
+                return obj == null ? Collections.emptyList() : Collections.singletonList(new PersistentVolumeClaim(obj));
+            }
+            LabelSelector selector = null;
+            if (request.getSelector() != null) {
+                selector = request.getSelector();
+            }
+            if (request.getLabels() != null) {
+                if (selector == null) {
+                    selector = new LabelSelector();
+                }
+                selector.addAll(request.getLabels());
+            }
+            V1PersistentVolumeClaimList objList = coreV1Api.listNamespacedPersistentVolumeClaim(
+                    request.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    selector.toJsonString(),
+                    null,
+                    null,
+                    null,
+                    null);
+            return objList.getItems().stream().map(PersistentVolumeClaim::new).collect(Collectors.toList());
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "查找 persistentVolumeClaim 失败,namespace = {}, response={}",
+                    request.getNamespace(), e.getResponseBody());
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                return Collections.emptyList();
+            }else {
+                return Collections.emptyList();
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "查找 persistentVolumeClaim 失败,namespace = {}, name={}",
+                    request.getNamespace());
+            return Collections.emptyList();
+        }
+    }
+
+    private V1PersistentVolumeClaim getV1PersistenceVolumeClaim(String namespace, String name) throws ApiException {
+        return coreV1Api.readNamespacedPersistentVolumeClaim(
+                name,
+                namespace,
+                PRETTY_FORMAT,
+                false,
+                false);
+    }
+}

+ 136 - 0
api/src/main/java/seecoder/devcloud/api/k8s/impl/PodApiImpl.java

@@ -0,0 +1,136 @@
+package seecoder.devcloud.api.k8s.impl;
+
+
+import com.google.gson.JsonSyntaxException;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.models.V1DeleteOptions;
+import io.kubernetes.client.openapi.models.V1DeleteOptionsBuilder;
+import io.kubernetes.client.openapi.models.V1Pod;
+import io.kubernetes.client.openapi.models.V1PodList;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import seecoder.devcloud.api.k8s.PodApi;
+import seecoder.devcloud.api.k8s.model.K8sObjectRequest;
+import seecoder.devcloud.api.k8s.model.LabelSelector;
+import seecoder.devcloud.api.k8s.model.Namespace;
+import seecoder.devcloud.api.k8s.model.Pod;
+import seecoder.devcloud.common.util.LoggerUtil;
+import seecoder.devcloud.api.k8s.exception.K8sApiException;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static seecoder.devcloud.api.k8s.K8sConstants.FOREGROUND_PROPAGATION_POLICY;
+import static seecoder.devcloud.api.k8s.K8sConstants.PRETTY_FORMAT;
+
+
+/**
+ * author: rale
+ * createdAt: 3/4/19
+ */
+@Service
+public class PodApiImpl implements PodApi {
+
+    private static final Logger logger = LoggerUtil.getLogger(PodApiImpl.class);
+
+    private final CoreV1Api coreV1Api;
+
+    @Autowired
+    public PodApiImpl(CoreV1Api coreV1Api) {
+        this.coreV1Api = coreV1Api;
+    }
+
+    @Override
+    public Pod create(Pod pod) {
+        throw  K8sApiException.K8s_NOT_SUPPORT_METHOD;
+    }
+
+    @Override
+    public Pod update(Pod pod) {
+        throw  K8sApiException.K8s_NOT_SUPPORT_METHOD;
+    }
+
+    @Override
+    public void delete(Pod pod) {
+        try {
+            V1DeleteOptions v1DeleteOptions = new V1DeleteOptionsBuilder()
+                    .withApiVersion(Namespace.API_VERSION)
+                    .withPropagationPolicy(FOREGROUND_PROPAGATION_POLICY)
+                    .build();
+            coreV1Api.deleteNamespacedPod(
+                    pod.getName(),
+                    pod.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    FOREGROUND_PROPAGATION_POLICY,
+                    v1DeleteOptions);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "Pod删除失败,name={}, response={}", pod.getName() , e.getResponseBody());
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                return;
+            }else {
+                return;
+            }
+        } catch (JsonSyntaxException e){
+            //do nothing
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "Pod删除异常, name={}", pod.getName());
+            return;
+        }
+    }
+
+    @Override
+    public List<Pod> getByCondition(K8sObjectRequest request) {
+        try {
+            if (request.getName() != null) {
+                V1Pod obj = coreV1Api.readNamespacedPod(
+                        request.getName(),
+                        request.getNamespace(),
+                        PRETTY_FORMAT,
+                        null,
+                        null);
+                return obj == null ? Collections.emptyList() : Collections.singletonList(new Pod(obj));
+            }
+            LabelSelector selector = null;
+            if (request.getSelector() != null) {
+                selector = request.getSelector();
+            }
+            if (request.getLabels() != null) {
+                if (selector == null) {
+                    selector = new LabelSelector();
+                }
+                selector.addAll(request.getLabels());
+            }
+            V1PodList v1Pods = coreV1Api.listNamespacedPod(
+                    request.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    selector.toJsonString(),
+                    null,
+                    null,
+                    null,
+                    null);
+            return v1Pods.getItems().stream().map(Pod::new).collect(Collectors.toList());
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "查找pod失败,namespace = {}, response={}",
+                    request.getNamespace(), e.getResponseBody());
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                return Collections.emptyList();
+            }else {
+                return Collections.emptyList();
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "查找pod失败,namespace = {}, name={}",
+                    request.getNamespace());
+            return Collections.emptyList();
+        }
+    }
+
+}

+ 148 - 0
api/src/main/java/seecoder/devcloud/api/k8s/impl/SecretApiImpl.java

@@ -0,0 +1,148 @@
+package seecoder.devcloud.api.k8s.impl;
+
+
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.models.*;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import seecoder.devcloud.api.k8s.SecretApi;
+import seecoder.devcloud.api.k8s.exception.K8sApiException;
+import seecoder.devcloud.api.k8s.util.SecretTypeEnum;
+import seecoder.devcloud.api.k8s.vo.SecretVO;
+import seecoder.devcloud.common.util.LoggerUtil;
+import seecoder.devcloud.api.k8s.K8sConstants;
+
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+
+@Service
+public class SecretApiImpl implements SecretApi {
+
+    private static final Logger logger = LoggerUtil.getLogger(SecretApi.class);
+
+    @Autowired
+    private CoreV1Api coreV1Api;
+
+    @Override
+    public SecretVO createGenericSecret(String namespace, String name, Map<String, byte[]> data) throws K8sApiException {
+        return createGenericSecret(namespace, name, data, SecretTypeEnum.Generic);
+    }
+
+    @Override
+    public SecretVO createGenericSecret(String namespace, String name, Map<String, byte[]> data, SecretTypeEnum type) throws K8sApiException {
+
+        //Secret默认创建的Type为Opaque,base64编码格式的Secret,用来储存密码、密钥等。
+
+        V1Secret v1Secret = new V1Secret();
+        V1ObjectMeta meta = new V1ObjectMeta();
+        meta.setName(name);
+        v1Secret.setApiVersion("v1");
+        meta.setNamespace(namespace);
+        v1Secret.setData(data);
+        v1Secret.setKind("Secret");
+        v1Secret.setMetadata(meta);
+        v1Secret.setType(type.getValue());
+
+        try {
+            return new SecretVO(coreV1Api.createNamespacedSecret(namespace, v1Secret, K8sConstants.PRETTY_FORMAT, null, null));
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "Secret创建失败, Secret={}, response={}", v1Secret, e.getResponseBody());
+            switch (e.getCode()) {
+                case K8sApiException.NAMESPACE_NOT_EXIST:
+                    throw new K8sApiException(K8sApiException.NAMESPACE_NOT_EXIST);
+                case K8sApiException.ALREADY_EXIST:
+                    throw new K8sApiException(K8sApiException.ALREADY_EXIST);
+                default:
+                    throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        }
+    }
+
+    @Override
+    public SecretVO createPrivateRegistrySecret(String namespace, String server, String user, String password) throws K8sApiException {
+        //dockerSecret 统一的名称
+        String name = "docker-registry-secret";
+        return createPrivateRegistrySecret(namespace, name, server, user, password);
+
+    }
+
+    @Override
+    public SecretVO createPrivateRegistrySecret(String namespace, String name, String server, String user, String password) throws K8sApiException {
+        Map<String, byte[]> data = new HashMap<>();
+
+        String authInfo = user + ":" + password;
+        String auth = Base64.getEncoder().encodeToString(authInfo.getBytes());
+        String dockerConfigJson =
+                "{\"auths\":{\"" + server + "\":{\"username\":\"" + user +
+                        "\",\"password\":\"" + password +
+                        "\",\"auth\":\"" + auth + "\"}}}";
+        data.put(".dockerconfigjson", dockerConfigJson.getBytes());
+
+        return createGenericSecret(namespace, name, data, SecretTypeEnum.REGISTRY);
+    }
+
+    @Override
+    public List<SecretVO> getSecretList(String namespace) throws K8sApiException {
+        try {
+            V1SecretList v1SecretList = coreV1Api.listNamespacedSecret(
+                    namespace,
+                    K8sConstants.PRETTY_FORMAT,
+                    null,
+                    "",
+                    null,
+                    null,
+                    200,
+                    "",
+                    10,
+                    false);
+            return v1SecretList.getItems().stream().map(SecretVO::new).collect(Collectors.toList());
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "Secret列表获取失败, response={}", e.getResponseBody());
+            if (e.getCode() == K8sApiException.NAMESPACE_NOT_EXIST) {
+                throw new K8sApiException(K8sApiException.NAMESPACE_NOT_EXIST);
+            } else {
+                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        }
+    }
+
+    @Override
+    public List<SecretVO> getSecretListByType(String namespace, SecretTypeEnum type) throws K8sApiException {
+        return getSecretList(namespace)
+                .stream()
+                .filter(secretVO -> type == secretVO.getSecretType())
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public SecretVO getSecretByName(String namespace, String name) throws K8sApiException {
+        return getSecretList(namespace)
+                .stream()
+                .filter(secretVO -> StringUtils.equals(secretVO.getName(), name))
+                .findAny()
+                .orElse(null);
+    }
+
+    @Override
+    public void deleteSecretByName(String namespace, String name) throws K8sApiException {
+        V1DeleteOptions body = new V1DeleteOptions();
+        try {
+            //暂时没有用到deleteNamespacedSecret()中的其他参数。
+            V1Status v1Status = coreV1Api.deleteNamespacedSecret(name, namespace, K8sConstants.PRETTY_FORMAT, null, 5, false, "", body);
+        } catch (ApiException e) {
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                return;
+            } else {
+                return;
+            }
+        }
+    }
+}
+

+ 174 - 0
api/src/main/java/seecoder/devcloud/api/k8s/impl/ServiceApiImpl.java

@@ -0,0 +1,174 @@
+package seecoder.devcloud.api.k8s.impl;
+
+
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.models.V1DeleteOptions;
+import io.kubernetes.client.openapi.models.V1DeleteOptionsBuilder;
+import io.kubernetes.client.openapi.models.V1Service;
+import io.kubernetes.client.openapi.models.V1ServiceList;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import seecoder.devcloud.api.k8s.ServiceApi;
+import seecoder.devcloud.api.k8s.exception.K8sApiException;
+import seecoder.devcloud.api.k8s.model.K8sObjectRequest;
+import seecoder.devcloud.api.k8s.model.LabelSelector;
+import seecoder.devcloud.api.k8s.model.Service;
+import seecoder.devcloud.common.util.LoggerUtil;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static seecoder.devcloud.api.k8s.K8sConstants.FOREGROUND_PROPAGATION_POLICY;
+import static seecoder.devcloud.api.k8s.K8sConstants.PRETTY_FORMAT;
+
+
+@org.springframework.stereotype.Service
+public class ServiceApiImpl implements ServiceApi {
+    private static final Logger logger = LoggerUtil.getLogger(ServiceApi.class);
+
+    @Autowired
+    private CoreV1Api coreV1Api;
+
+    /**
+     * 创建Service
+     * @param service
+     * @throws K8sApiException NOT_FOUND namespace不存在
+     * @throws K8sApiException ALREADY_EXISTS service已经存在
+     * @throws K8sApiException SYSTEM_ERROR 系统异常
+     */
+    @Override
+    public Service create(Service service) {
+        try {
+            V1Service v1Service = service.toK8sObject();
+            v1Service = coreV1Api.createNamespacedService(service.getNamespace(), v1Service, PRETTY_FORMAT, null, null);
+            return v1Service == null ? null : new Service(v1Service);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "service创建失败, service={}, response={}", service, e.getResponseBody());
+            if (e.getCode() == K8sApiException.ALREADY_EXIST) {
+                throw new K8sApiException(K8sApiException.ALREADY_EXIST, "Service已经存在");
+            }else if (e.getCode() == K8sApiException.NOT_FOUND){
+                throw new K8sApiException(K8sApiException.NOT_FOUND, "Namespace不存在");
+            }else {
+                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        }
+    }
+
+
+    /**
+     * 更新Service
+     * @param service
+     * @throws K8sApiException NOT_FOUND namespace不存在
+     * @throws K8sApiException SYSTEM_ERROR 系统异常
+     * @return
+     */
+    @Override
+    public Service update(Service service) {
+
+        try {
+            V1Service v1Service = getV1Service(service.getNamespace(), service.getName());
+            service.merge(v1Service);
+            v1Service = coreV1Api.replaceNamespacedService(service.getName(), service.getNamespace(), v1Service, PRETTY_FORMAT, null, null);
+            return v1Service == null ? null : new Service(v1Service);
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "service替换失败, service={}, response={}", service, e.getResponseBody());
+            if (e.getCode() == K8sApiException.NOT_FOUND){
+                throw new K8sApiException(K8sApiException.NOT_FOUND, "Service不存在");
+            }else {
+                throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "service替换异常, service={}", service);
+            throw K8sApiException.K8s_SYSTEM_ERROR_EXCEPTION;
+        }
+    }
+
+    @Override
+    public void delete(Service service) {
+        try {
+            V1DeleteOptions v1DeleteOptions = new V1DeleteOptionsBuilder()
+                    .withApiVersion(Service.API_VERSION)
+                    .withPropagationPolicy(FOREGROUND_PROPAGATION_POLICY)
+                    .build();
+            coreV1Api.deleteNamespacedService(
+                    service.getName(),
+                    service.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    FOREGROUND_PROPAGATION_POLICY,
+                    v1DeleteOptions);
+        }catch (ApiException e) {
+            LoggerUtil.error(logger, e, "Service删除失败,namespace={}, name={}, response={}",
+                    service.getNamespace(), service.getName(), e.getResponseBody());
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                return;
+            }else {
+                return;
+            }
+        }catch (Exception e) {
+            LoggerUtil.error(logger, e, "Service删除异常,namespace={}, name={}, response={}", service.getNamespace(), service.getName());
+        }
+    }
+
+    @Override
+    public List<Service> getByCondition(K8sObjectRequest request) {
+        try {
+            if (request.getName() != null) {
+                V1Service obj = coreV1Api.readNamespacedService(
+                        request.getName(),
+                        request.getNamespace(),
+                        PRETTY_FORMAT,
+                        null,
+                        null);
+                return obj == null ? Collections.emptyList() : Collections.singletonList(new Service(obj));
+            }
+            LabelSelector selector = null;
+            if (request.getSelector() != null) {
+                selector = request.getSelector();
+            }
+            if (request.getLabels() != null) {
+                if (selector == null) {
+                    selector = new LabelSelector();
+                }
+                selector.addAll(request.getLabels());
+            }
+            V1ServiceList objList = coreV1Api.listNamespacedService(
+                    request.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    selector.toJsonString(),
+                    null,
+                    null,
+                    null,
+                    null);
+            return objList.getItems().stream().map(Service::new).collect(Collectors.toList());
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "查找pod失败,namespace = {}, response={}",
+                    request.getNamespace(), e.getResponseBody());
+            if (e.getCode() == K8sApiException.NOT_FOUND) {
+                return Collections.emptyList();
+            }else {
+                return Collections.emptyList();
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "查找pod失败,namespace = {}, name={}",
+                    request.getNamespace());
+            return Collections.emptyList();
+        }
+    }
+
+    private V1Service getV1Service(String namespace, String name) throws ApiException {
+        return coreV1Api.readNamespacedService(
+                name,
+                namespace,
+                PRETTY_FORMAT,
+                null,
+                null);
+    }
+}

+ 72 - 0
api/src/main/java/seecoder/devcloud/api/k8s/impl/WatchApiImpl.java

@@ -0,0 +1,72 @@
+package seecoder.devcloud.api.k8s.impl;
+
+import com.google.common.reflect.TypeToken;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.AppsV1Api;
+import io.kubernetes.client.openapi.models.V1Deployment;
+import io.kubernetes.client.util.Watch;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import seecoder.devcloud.api.k8s.WatchApi;
+
+import java.io.IOException;
+
+import static seecoder.devcloud.api.k8s.K8sConstants.PRETTY_FORMAT;
+
+
+/**
+ * author: rale
+ * createdAt: 1/13/19
+ */
+@Service
+public class WatchApiImpl implements WatchApi {
+
+    private ApiClient watchApiClient;
+
+    private AppsV1Api watchAppsV1Api;
+
+    @Autowired
+    public WatchApiImpl(ApiClient watchApiClient, AppsV1Api watchAppsV1Api) {
+        this.watchApiClient = watchApiClient;
+        this.watchAppsV1Api = watchAppsV1Api;
+    }
+    @Override
+    public void watchDeployment() {
+        try {
+            Watch<V1Deployment> watch = Watch.createWatch(
+                    watchApiClient,
+                    watchAppsV1Api.listNamespacedDeploymentCall(
+                            "demo2",
+                            PRETTY_FORMAT,
+                            null,
+                            null,
+                            null,
+                            "demo.seec.nju.cn/app=test",
+                            null,
+                            null,
+                            null,
+                            true,
+                            null
+                             ),
+                    new TypeToken<Watch.Response<V1Deployment>>(){}.getType()
+
+            );
+            watch.forEach(response -> {
+                System.out.printf("%s pod : %s %s%n", response.type, response.object.getMetadata().getName(), response.object.getStatus());
+                try {
+                    watch.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            });
+        } catch (ApiException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public void watchPod() {
+
+    }
+}

+ 50 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/ConfigMap.java

@@ -0,0 +1,50 @@
+package seecoder.devcloud.api.k8s.model;
+
+
+
+import io.kubernetes.client.openapi.models.V1ConfigMap;
+import io.kubernetes.client.openapi.models.V1ConfigMapBuilder;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class ConfigMap extends K8sAbstractObject<V1ConfigMap> {
+
+    public static final String API_VERSION = "v1";
+
+    public static final String KIND = "ConfigMap";
+
+    private Map<String, String> data = new HashMap<>();
+
+
+    public ConfigMap(V1ConfigMap v1ConfigMap) {
+        super(v1ConfigMap);
+        this.data = v1ConfigMap.getData();
+    }
+
+    public void merge(V1ConfigMap v1ConfigMap) {
+        v1ConfigMap.getMetadata().setName(getName());
+        v1ConfigMap.getMetadata().setNamespace(this.getNamespace());
+        v1ConfigMap.getMetadata().setLabels(this.getLabels());
+        v1ConfigMap.getMetadata().setAnnotations(this.getAnnotations());
+        v1ConfigMap.setData(this.getData());
+    }
+
+    @Override
+    public V1ConfigMap toK8sObject() {
+        return new V1ConfigMapBuilder()
+                .withApiVersion(getApiVersion() == null ? API_VERSION : getApiVersion())
+                .withKind(getKind() == null ? KIND : getKind())
+                .withMetadata(toV1ObjectMeta())
+                .withData(data)
+                .build();
+    }
+}

+ 108 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/Container.java

@@ -0,0 +1,108 @@
+package seecoder.devcloud.api.k8s.model;
+
+import io.kubernetes.client.openapi.models.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+
+/**
+ * author: rale
+ * createdAt: 1/3/19
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class Container implements K8sObjectTransformer<V1Container> {
+
+    private static final String IMAGE_PULL_POLICY = "IfNotPresent";
+
+    private String name;
+
+    private String image;
+
+    /* 启动指令的参数 */
+    private List<String> args;
+
+    /* 启动指令 */
+    private List<String> command;
+
+    /* 环境变量 */
+    private Map<String, String> env = new HashMap<>();
+
+    private Set<ContainerPort> ports = new HashSet<>();
+
+    private List<VolumeMount> volumeMounts = new ArrayList<>();
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    @Builder
+    public static class VolumeMount implements K8sObjectTransformer<V1VolumeMount> {
+
+        String name;
+
+        String mountPath;
+
+        String subPath;
+
+        Boolean readOnly = true;
+
+        public VolumeMount(V1VolumeMount v1VolumeMount) {
+            this.name = v1VolumeMount.getName();
+            this.mountPath = v1VolumeMount.getMountPath();
+            this.readOnly = v1VolumeMount.getReadOnly();
+            this.subPath = v1VolumeMount.getSubPath();
+        }
+
+        @Override
+        public V1VolumeMount toK8sObject() {
+            return new V1VolumeMountBuilder()
+                    .withName(name)
+                    .withMountPath(mountPath)
+                    .withSubPath(subPath)
+                    .withReadOnly(readOnly)
+                    .build();
+        }
+    }
+
+    public Container(V1Container v1Container) {
+        this.name = v1Container.getName();
+        this.image = v1Container.getImage();
+        this.ports = v1Container.getPorts() == null ? null : v1Container.getPorts().stream().map(ContainerPort::new).collect(Collectors.toSet());
+        this.args = v1Container.getArgs();
+        this.command = v1Container.getCommand();
+        this.volumeMounts = v1Container.getVolumeMounts() != null ? v1Container.getVolumeMounts().stream().map(VolumeMount::new).collect(Collectors.toList()) : null;
+        if (v1Container.getEnv() != null && !v1Container.getEnv().isEmpty()) {
+            this.env = v1Container.getEnv().stream().collect(Collectors.toMap(V1EnvVar::getName, V1EnvVar::getValue));
+        }
+    }
+
+    @Override
+    public V1Container toK8sObject() {
+        return new V1ContainerBuilder()
+                .withName(name)
+                .withImage(image)
+                .withImagePullPolicy(IMAGE_PULL_POLICY)
+                .withArgs(args)
+                .withCommand(command)
+                .withPorts(ports == null ? null : ports.stream().map(ContainerPort::toK8sObject).collect(Collectors.toList()))
+                .withVolumeMounts(volumeMounts == null ? null : this.volumeMounts.stream().map(VolumeMount::toK8sObject).collect(Collectors.toList()))
+                .withEnv(env == null ? null : env
+                        .entrySet()
+                        .stream()
+                        .map(entry ->
+                                new V1EnvVarBuilder()
+                                        .withName(entry.getKey())
+                                        .withValue(entry.getValue())
+                                        .build())
+                        .collect(Collectors.toList())
+                )
+                .build();
+    }
+}

+ 41 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/ContainerPort.java

@@ -0,0 +1,41 @@
+package seecoder.devcloud.api.k8s.model;
+
+
+import io.kubernetes.client.openapi.models.V1ContainerPort;
+import io.kubernetes.client.openapi.models.V1ContainerPortBuilder;
+import lombok.*;
+
+
+/**
+ * author: rale
+ * createdAt: 12/23/18
+ */
+@Data
+@EqualsAndHashCode
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class ContainerPort implements K8sObjectTransformer<V1ContainerPort> {
+
+    private String name;
+
+    private int port;
+
+    private String protocol;
+
+    public ContainerPort(V1ContainerPort v1ContainerPort) {
+        this.setName(v1ContainerPort.getName());
+        this.setPort(v1ContainerPort.getContainerPort());
+        this.setProtocol(v1ContainerPort.getProtocol());
+    }
+
+    @Override
+    public V1ContainerPort toK8sObject() {
+        V1ContainerPort v1ContainerPort = new V1ContainerPortBuilder()
+                .withContainerPort(port)
+                .withName(name)
+                .withProtocol(protocol)
+                .build();
+        return v1ContainerPort;
+    }
+}

+ 72 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/CpuQuantity.java

@@ -0,0 +1,72 @@
+package seecoder.devcloud.api.k8s.model;
+
+import io.kubernetes.client.custom.Quantity;
+import lombok.Getter;
+
+/**
+ * CPU配额数量,默认单位为m,即CPU时间
+ */
+@Getter
+public class CpuQuantity {
+
+    private final Quantity quantity;
+
+    private final Unit unit;
+
+    private static final Unit DEFAULT_UNIT = Unit.Mi;
+    //Cpu=0
+    public static final CpuQuantity EMPTY = new CpuQuantity(0);
+    //Cpu的默认分配额度
+    public static final CpuQuantity DEFAULT = new CpuQuantity(50);
+
+    //Cpu单位
+    public enum Unit{
+        Mi("m");
+
+        private String code;
+
+        Unit(String code) {
+            this.code = code;
+        }
+
+        public String getCode(){
+            return this.code;
+        }
+    }
+
+
+
+    public CpuQuantity(int quantity){
+        this.quantity = Quantity.fromString(quantity + DEFAULT_UNIT.getCode());
+        this.unit = DEFAULT_UNIT;
+    }
+
+    public CpuQuantity(long quantity) {
+        this.quantity = Quantity.fromString(quantity + DEFAULT_UNIT.getCode());
+        this.unit = DEFAULT_UNIT;
+    }
+
+
+    public CpuQuantity(Quantity quantity) {
+        this.quantity = quantity;
+        this.unit = DEFAULT_UNIT;
+    }
+
+    public Quantity toQuantity(){
+        return quantity;
+    }
+
+    public String getQuantity() {
+        return quantity.toSuffixedString();
+    }
+
+    public static CpuQuantity getCpuQuantity(Quantity quantity) {
+        if (quantity == null) {
+            return null;
+        } else {
+            return new CpuQuantity(quantity);
+        }
+    }
+
+
+}

+ 147 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/Deployment.java

@@ -0,0 +1,147 @@
+package seecoder.devcloud.api.k8s.model;
+
+import io.kubernetes.client.openapi.models.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.validator.constraints.Range;
+import org.springframework.data.annotation.ReadOnlyProperty;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * author: rale
+ * createdAt: 1/3/19
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class Deployment extends K8sAbstractObject<V1Deployment> {
+
+    public static final String API_VERSION = "apps/v1";
+
+    private static final String KIND = "Deployment";
+
+    public static final int DEFAULT_PROGRESS_DEADLINE_SECONDS = 600;
+
+    public static final int DEFAULT_REVISION_HISTORY_LIMIT = 0;
+
+    public static final String DEFAULT_UPDATE_STRATEGY = "Recreate";
+
+    private List<Container> containers = new ArrayList<>();
+
+    private List<String> imagePullSecrets = new ArrayList<>();
+
+    private List<V1Volume> volumes = new ArrayList<>();
+
+    private Map<String, String> nodeSelectors = new HashMap<>();
+    @Range(min = 1, max = 3)
+    private int replicas;
+
+    /**
+     * 超过timeout时间未完成启动则提示启动超时
+     */
+    private int timeout = DEFAULT_PROGRESS_DEADLINE_SECONDS;
+
+    /**
+     * 保留历史replica set的个数
+     */
+    private int revisionHistoryLimit = DEFAULT_REVISION_HISTORY_LIMIT;
+
+    @ReadOnlyProperty
+    private V1DeploymentStatus status;
+
+    public Deployment(V1Deployment v1Deployment) {
+        super(v1Deployment);
+        this.containers = v1Deployment.getSpec().getTemplate().getSpec().getContainers() == null ? null : v1Deployment.getSpec().getTemplate().getSpec().getContainers()
+                .stream().map(Container::new).collect(Collectors.toList());
+        this.imagePullSecrets = v1Deployment.getSpec().getTemplate().getSpec().getImagePullSecrets() == null ? null : v1Deployment.getSpec().getTemplate().getSpec().getImagePullSecrets()
+                .stream().map(V1LocalObjectReference::getName).collect(Collectors.toList());
+        this.replicas = v1Deployment.getSpec().getReplicas();
+        this.timeout = v1Deployment.getSpec().getProgressDeadlineSeconds();
+        this.revisionHistoryLimit = v1Deployment.getSpec().getRevisionHistoryLimit();
+        this.volumes =  v1Deployment.getSpec().getTemplate().getSpec().getVolumes();
+        this.nodeSelectors = v1Deployment.getSpec().getTemplate().getSpec().getNodeSelector();
+        this.status = v1Deployment.getStatus();
+    }
+
+    private V1LabelSelector toDeploymentV1LabelSelector() {
+        return new V1LabelSelectorBuilder()
+                .withMatchLabels(getLabels())
+                .build();
+    }
+
+    private V1PodTemplateSpec toPodSpecTemplate(){
+        V1ObjectMeta v1ObjectMeta = new V1ObjectMetaBuilder()
+                .withLabels(getLabels())
+                .withAnnotations(getAnnotations())
+                .build();
+        List<V1LocalObjectReference> references = imagePullSecrets == null ? null : imagePullSecrets
+                .stream()
+                .map(secret -> new V1LocalObjectReference().name(secret))
+                .collect(Collectors.toList());
+        V1PodSpec v1PodSpec = new V1PodSpecBuilder()
+                .addAllToContainers(containers == null ? null : containers.stream().map(Container::toK8sObject).collect(Collectors.toList()))
+                .withImagePullSecrets(references)
+                .withVolumes(volumes)
+                .withNodeSelector(nodeSelectors)
+                .build();
+        V1PodTemplateSpec v1PodTemplateSpec = new V1PodTemplateSpecBuilder()
+                .withMetadata(v1ObjectMeta)
+                .withSpec(v1PodSpec)
+                .build();
+        return v1PodTemplateSpec;
+    }
+
+    private V1DeploymentSpec toV1DeploymentSpec(){
+//        V1RollingUpdateDeployment rollingUpdateDeployment = new V1RollingUpdateDeploymentBuilder()
+//                .withMaxSurge(new IntOrString(0))
+//                .withMaxUnavailable(new IntOrString("100%"))
+//                .build();
+
+
+        return new V1DeploymentSpecBuilder()
+                .withReplicas(replicas)
+                .withProgressDeadlineSeconds(timeout)
+                .withStrategy(new V1DeploymentStrategy().type(DEFAULT_UPDATE_STRATEGY))
+//                .withStrategy(new V1DeploymentStrategy().rollingUpdate(rollingUpdateDeployment))
+                .withRevisionHistoryLimit(revisionHistoryLimit)
+                .withSelector(this.toDeploymentV1LabelSelector())
+                .withTemplate(this.toPodSpecTemplate())
+                .build();
+    }
+
+    public void merge(V1Deployment v1Deployment) {
+        v1Deployment.getMetadata().setName(getName());
+        v1Deployment.getMetadata().setNamespace(this.getNamespace());
+        v1Deployment.getMetadata().setLabels(this.getLabels());
+        v1Deployment.getMetadata().setAnnotations(this.getAnnotations());
+        v1Deployment.getSpec().setReplicas(this.replicas);
+        v1Deployment.getSpec().setProgressDeadlineSeconds(timeout);
+        v1Deployment.getSpec().setRevisionHistoryLimit(revisionHistoryLimit);
+        v1Deployment.getSpec().getTemplate().getSpec().setContainers(containers == null ? null : this.containers.stream().map(Container::toK8sObject).collect(Collectors.toList()));
+        v1Deployment.getSpec().getTemplate().getSpec().setVolumes(volumes == null ? null : this.volumes);
+        v1Deployment.getSpec().getTemplate().getSpec().setImagePullSecrets(
+                imagePullSecrets == null ? null : this.imagePullSecrets
+                .stream()
+                .map(secret -> new V1LocalObjectReference().name(secret))
+                .collect(Collectors.toList()));
+        v1Deployment.getSpec().getTemplate().getSpec().setNodeSelector(nodeSelectors);
+    }
+
+    @Override
+    public V1Deployment toK8sObject() {
+        return new V1DeploymentBuilder()
+                .withApiVersion(getApiVersion() == null ? API_VERSION : getApiVersion())
+                .withKind(getKind() == null ? KIND : getKind())
+                .withMetadata(this.toV1ObjectMeta())
+                .withSpec(this.toV1DeploymentSpec())
+                .build();
+    }
+}

+ 65 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/DeploymentStatus.java

@@ -0,0 +1,65 @@
+package seecoder.devcloud.api.k8s.model;
+
+import io.kubernetes.client.openapi.models.V1DeploymentCondition;
+import io.kubernetes.client.openapi.models.V1DeploymentStatus;
+import lombok.Data;
+
+/**
+ * author: rale
+ * createdAt: 1/16/19
+ * 如果要凭借该状态
+ */
+@Data
+public class DeploymentStatus {
+
+    /**
+     * 当前为就绪状态的容器
+     */
+    private int readyReplicas;
+
+    /**
+     * 当前不可用的容器
+     */
+    private int unavailableReplicas;
+
+    /**
+     * 当前为最新版本的容器
+     */
+    private int updatedReplicas;
+
+    /**
+     * 是否满足了最低可用服务数量
+     */
+    private boolean available;
+
+    /**
+     * 当前deployment是否能够正常执行
+     */
+    private boolean progressing;
+
+    /**
+     * progressing为false时的几种情况
+     * 1. ProgressDeadlineExceeded 启动超时
+     * 2.
+     */
+    private String progressingMessage;
+
+    public DeploymentStatus(V1DeploymentStatus deploymentStatus) {
+        this.readyReplicas = deploymentStatus.getReadyReplicas() == null ? 0 : deploymentStatus.getReadyReplicas();
+        this.unavailableReplicas = deploymentStatus.getUnavailableReplicas() == null ? 0 : deploymentStatus.getUnavailableReplicas();
+        this.updatedReplicas = deploymentStatus.getUpdatedReplicas() == null ? 0 : deploymentStatus.getUpdatedReplicas();
+        for (V1DeploymentCondition condition : deploymentStatus.getConditions()) {
+            if (condition.getType().equals("Available")) {
+                available = condition.getStatus().equals("True");
+            }else if (condition.getType().equals("Progressing")) {
+                progressing = condition.getStatus().equals("True");
+                progressingMessage = condition.getMessage();
+            }
+        }
+    }
+
+    public boolean isAvailable() {
+        return this.available;
+    }
+
+}

+ 53 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/HttpGetAction.java

@@ -0,0 +1,53 @@
+package seecoder.devcloud.api.k8s.model;
+
+import io.kubernetes.client.custom.IntOrString;
+import io.kubernetes.client.openapi.models.V1HTTPGetAction;
+import io.kubernetes.client.openapi.models.V1HTTPGetActionBuilder;
+import io.kubernetes.client.openapi.models.V1HTTPHeader;
+import io.kubernetes.client.openapi.models.V1HTTPHeaderBuilder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * author: rale
+ * createdAt: 1/13/19
+ *  封装HTTP get请求
+ */
+@Data
+@NoArgsConstructor
+public class HttpGetAction {
+
+    /*默认值为容器的host名称,即容器名称*/
+    private String host;
+
+    private Map<String, String> httpHeaders = Collections.EMPTY_MAP;
+
+    private String path;
+
+    private int port;
+
+    public HttpGetAction(V1HTTPGetAction v1HTTPGetAction) {
+        this.host = v1HTTPGetAction.getHost();
+        if (v1HTTPGetAction.getHttpHeaders() != null) {
+            httpHeaders = v1HTTPGetAction.getHttpHeaders().stream().collect(Collectors.toMap(V1HTTPHeader::getName, V1HTTPHeader::getValue));
+        }
+        this.path = v1HTTPGetAction.getPath();
+        this.port = v1HTTPGetAction.getPort().getIntValue();
+    }
+
+    public V1HTTPGetAction toV1HTTPGetAction() {
+        return new V1HTTPGetActionBuilder()
+                .withHost(host)
+                .withHttpHeaders(
+                        httpHeaders.entrySet().stream()
+                                .map(entry -> new V1HTTPHeaderBuilder().withName(entry.getKey()).withValue(entry.getValue()).build())
+                                .collect(Collectors.toList()))
+                .withPath(path)
+                .withPort(new IntOrString(port))
+                .build();
+    }
+}

+ 151 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/Ingress.java

@@ -0,0 +1,151 @@
+package seecoder.devcloud.api.k8s.model;
+
+import io.kubernetes.client.custom.IntOrString;
+import io.kubernetes.client.openapi.models.*;
+import lombok.*;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * author: rale
+ * createdAt: 1/5/19
+ * TODO: 3/25/19 添加标签字段 以后应当是一个分支上的代码可以创建一个deployment
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class Ingress extends K8sAbstractObject<ExtensionsV1beta1Ingress> {
+
+    public static final String API_VERSION = "extensions/v1beta1";
+
+    private static final String KIND = "Ingress";
+
+    public static final String REWRITE_ANNOTATION = "nginx.ingress.kubernetes.io/rewrite-target";
+
+    public static final String DEFAULT_REWRITE_PATH = "/";
+
+    //重定向的路径,默认为根路径
+    private static final String redirectPath = DEFAULT_REWRITE_PATH;
+
+    /*Ingress能暴露HTTP和HTTPS协议口,目前暂不支持HTTPS协议*/
+    private List<IngressRule> httpRules = new ArrayList<>();
+
+    public Ingress(ExtensionsV1beta1Ingress extensionsV1beta1Ingress) {
+        super(extensionsV1beta1Ingress);
+        List<ExtensionsV1beta1IngressRule> rules = extensionsV1beta1Ingress.getSpec().getRules();
+        if (rules != null && !rules.isEmpty()) {
+            httpRules = rules.stream()
+                    .filter(Objects::nonNull)
+                    .map(IngressRule::new)
+                    .collect(Collectors.toList());
+        }
+    }
+
+    @Override
+    public ExtensionsV1beta1Ingress toK8sObject() {
+        ExtensionsV1beta1IngressSpec ExtensionsV1beta1IngressSpec = new ExtensionsV1beta1IngressSpecBuilder()
+                .withRules(toExtensionsV1beta1IngressRules())
+                .build();
+        ExtensionsV1beta1Ingress ExtensionsV1beta1Ingress = new ExtensionsV1beta1IngressBuilder()
+                .withApiVersion(getApiVersion() == null ? API_VERSION : getApiVersion())
+                .withKind(getKind() == null ? KIND : getKind())
+                .withMetadata(this.toV1ObjectMeta())
+                .withSpec(ExtensionsV1beta1IngressSpec)
+                .build();
+        return ExtensionsV1beta1Ingress;
+    }
+
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @EqualsAndHashCode
+    @Builder
+    public static class IngressPath implements K8sObjectTransformer<ExtensionsV1beta1HTTPIngressPath> {
+
+        private String path;
+
+        private String serviceName;
+
+        private int port;
+
+        public IngressPath(ExtensionsV1beta1HTTPIngressPath path) {
+            this.path = path.getPath();
+            this.serviceName = path.getBackend().getServiceName();
+            this.port = path.getBackend().getServicePort().getIntValue();
+        }
+
+        @Override
+        public ExtensionsV1beta1HTTPIngressPath toK8sObject() {
+            ExtensionsV1beta1HTTPIngressPath v1beta1HTTPIngressPath = new ExtensionsV1beta1HTTPIngressPath();
+            v1beta1HTTPIngressPath.setPath(getPath());
+
+            ExtensionsV1beta1IngressBackend ExtensionsV1beta1IngressBackend = new ExtensionsV1beta1IngressBackend();
+            ExtensionsV1beta1IngressBackend.setServiceName(serviceName);
+            ExtensionsV1beta1IngressBackend.setServicePort(new IntOrString(port));
+
+            v1beta1HTTPIngressPath.setBackend(ExtensionsV1beta1IngressBackend);
+
+            return v1beta1HTTPIngressPath;
+        }
+    }
+
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @EqualsAndHashCode
+    @Builder
+    public static class IngressRule implements K8sObjectTransformer<ExtensionsV1beta1IngressRule> {
+
+        private String host;
+
+        private Set<IngressPath> ruleValues = new HashSet<>();
+
+        public IngressRule(String host) {
+            this.host = host;
+        }
+
+        public void addRuleValue(IngressPath ingressPath) {
+            this.ruleValues.add(ingressPath);
+        }
+
+        public IngressRule(ExtensionsV1beta1IngressRule ExtensionsV1beta1IngressRule) {
+            this.host = ExtensionsV1beta1IngressRule.getHost();
+            if (ExtensionsV1beta1IngressRule.getHttp() != null) {
+                this.ruleValues = ExtensionsV1beta1IngressRule.getHttp() != null && ExtensionsV1beta1IngressRule.getHttp()
+                        .getPaths() != null ? ExtensionsV1beta1IngressRule.getHttp()
+                        .getPaths()
+                        .stream()
+                        .map(IngressPath::new)
+                        .collect(Collectors.toSet()) : null;
+            }
+
+        }
+
+        private ExtensionsV1beta1HTTPIngressRuleValue toV1beta1HTTPIngressRuleValue() {
+            List<ExtensionsV1beta1HTTPIngressPath> paths = ruleValues == null ? null : ruleValues.stream()
+                    .map(IngressPath::toK8sObject)
+                    .collect(Collectors.toList());
+
+            return new ExtensionsV1beta1HTTPIngressRuleValueBuilder()
+                    .withPaths(paths)
+                    .build();
+        }
+
+        @Override
+        public ExtensionsV1beta1IngressRule toK8sObject() {
+            return new ExtensionsV1beta1IngressRuleBuilder()
+                    .withHost(host)
+                    .withHttp(toV1beta1HTTPIngressRuleValue())
+                    .build();
+        }
+    }
+
+    private List<ExtensionsV1beta1IngressRule> toExtensionsV1beta1IngressRules() {
+        return httpRules.stream()
+                .map(IngressRule::toK8sObject)
+                .collect(Collectors.toList());
+
+    }
+}

+ 86 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/K8sAbstractObject.java

@@ -0,0 +1,86 @@
+package seecoder.devcloud.api.k8s.model;
+
+import io.kubernetes.client.common.KubernetesObject;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@NoArgsConstructor
+@Data
+@ToString
+public abstract class K8sAbstractObject<T extends KubernetesObject> implements K8sObject, K8sObjectTransformer<T> {
+
+    private String apiVersion;
+
+    private String kind;
+
+    private String name;
+
+    private String namespace;
+
+    private Map<String, String> labels = new HashMap<>();
+
+    private Map<String, String> annotations = new HashMap<>();
+
+    public K8sAbstractObject(T t) {
+        this.apiVersion = t.getApiVersion();
+        this.kind = t.getKind();
+        this.name = t.getMetadata().getName();
+        this.namespace = t.getMetadata().getNamespace();
+        this.annotations = t.getMetadata().getAnnotations();
+        this.labels = t.getMetadata().getLabels();
+    }
+
+    @Override
+    public Map<String, String> getAnnotations() {
+        return annotations;
+    }
+
+    @Override
+    public String getAnnotation(String key) {
+        return annotations.get(key);
+    }
+
+    @Override
+    public void setAnnotation(String key, String value) {
+        annotations.put(key, value);
+    }
+
+    @Override
+    public Map<String, String> getLabels() {
+        return labels;
+    }
+
+    @Override
+    public String getLabel(String key) {
+        return labels.get(key);
+    }
+
+    @Override
+    public void setLabel(String key, String value) {
+        labels.put(key, value);
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public String getNamespace() {
+        return namespace;
+    }
+
+    @Override
+    public String getApiVersion() {
+        return apiVersion;
+    }
+
+    @Override
+    public String getKind() {
+        return kind;
+    }
+}

+ 45 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/K8sObject.java

@@ -0,0 +1,45 @@
+package seecoder.devcloud.api.k8s.model;
+
+
+
+
+import io.kubernetes.client.openapi.models.V1ObjectMeta;
+import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder;
+
+import java.util.Map;
+
+public interface K8sObject {
+
+    String DEFAULT_NAMESPACE = "default";
+
+    String getApiVersion();
+
+    String getKind();
+
+    String getName();
+
+    Map<String, String> getLabels();
+
+    String getLabel(String key);
+
+    void setLabel(String key, String value);
+
+    Map<String, String> getAnnotations();
+
+    String getAnnotation(String key);
+
+    void setAnnotation(String key, String value);
+
+    default String getNamespace() {
+      return DEFAULT_NAMESPACE;
+    }
+
+    default V1ObjectMeta toV1ObjectMeta() {
+        return new V1ObjectMetaBuilder()
+                .withName(getName())
+                .withNamespace(getNamespace())
+                .withAnnotations(getAnnotations())
+                .withLabels(getLabels())
+                .build();
+    }
+}

+ 24 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/K8sObjectRequest.java

@@ -0,0 +1,24 @@
+package seecoder.devcloud.api.k8s.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Data
+@AllArgsConstructor
+@Builder
+public class K8sObjectRequest {
+
+    private String name;
+
+    private String namespace;
+
+    private Map<String, String> labels = new HashMap<>();
+
+    private Map<String, String> annotations = new HashMap<>();
+
+    private LabelSelector selector;
+}

+ 6 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/K8sObjectTransformer.java

@@ -0,0 +1,6 @@
+package seecoder.devcloud.api.k8s.model;
+
+public interface K8sObjectTransformer<T> {
+
+    T toK8sObject();
+}

+ 170 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/LabelSelector.java

@@ -0,0 +1,170 @@
+package seecoder.devcloud.api.k8s.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.util.*;
+
+/**
+ * author: rale
+ * createdAt: 1/7/19
+ *
+ * 标签选择器
+ * 支持以下几种语法
+ * key IN value1,value2 ... 筛选出键为key且值在value中的资源 key equals value 等价于key in value
+ * key NOT_IN value1, value2 ... 筛选出键为key且值不在value中,或者不存在键为key的键值对的资源 key not equals value 等价于 key not in value
+ * Key EXISTS 筛选出包含键key的资源
+ * Key NOT_EXISTS 筛选出不包含键key的资源
+ *
+ * 本选择器不会删除同名的key
+ */
+@NoArgsConstructor
+public class LabelSelector {
+
+    public enum Operator{
+
+        IN {
+            @Override
+            public String toJsonString(MatchExpressionItem matchExpressionItem) {
+                Set<String> values = matchExpressionItem.getValues();
+                if (values == null || values.size() == 0) return "";
+                if (values.size() == 1) return matchExpressionItem.getKey() + "=" + values.iterator().next();
+                StringBuilder resultValue = new StringBuilder("(");
+                for (String value : matchExpressionItem.getValues()) {
+                    if (resultValue.length() > 1) {
+                        resultValue.append(",");
+                    }
+                    resultValue.append(value);
+                }
+                resultValue.append(")");
+                return matchExpressionItem.getKey() + " in " + resultValue;
+            }
+        },
+
+        NOT_IN {
+            @Override
+            public String toJsonString(MatchExpressionItem matchExpressionItem) {
+                Set<String> values = matchExpressionItem.getValues();
+                if (values == null || values.size() == 0) return "";
+                if (values.size() == 1) return matchExpressionItem.getKey() + "!=" + values.iterator().next();
+                StringBuilder resultValue = new StringBuilder("(");
+                for (String value : matchExpressionItem.getValues()) {
+                    if (resultValue.length() > 1) {
+                        resultValue.append(",");
+                    }
+                    resultValue.append(value);
+                }
+                resultValue.append(")");
+                return matchExpressionItem.getKey() + " notin " + resultValue;
+            }
+        },
+
+        EXISTS {
+            @Override
+            public String toJsonString(MatchExpressionItem matchExpressionItem) {
+                return matchExpressionItem.getKey();
+            }
+        },
+
+        NOT_EXISTS {
+            @Override
+            public String toJsonString(MatchExpressionItem matchExpressionItem) {
+                return "!" + matchExpressionItem.getKey();
+            }
+        },
+
+        EQUALS {
+            @Override
+            public String toJsonString(MatchExpressionItem matchExpressionItem) {
+                Set<String> values = matchExpressionItem.getValues();
+                if (values == null || values.size() == 0) return "";
+                return matchExpressionItem.getKey() + "=" + values.iterator().next();
+            }
+        }
+        ;
+
+        public abstract String toJsonString(MatchExpressionItem matchExpressionItem);
+    }
+
+
+    private List<MatchExpressionItem> matchExpressionItems;
+
+
+    public void addMatchExpression(String key, Operator operator, Set<String> values) {
+        addMatchExpressionItem(new MatchExpressionItem(key, operator, values));
+    }
+
+
+    public void addMatchExpression(String key, Operator operator, String... values) {
+        Set<String> valueSet = new HashSet<>(Arrays.asList(values));
+        addMatchExpressionItem(new MatchExpressionItem(key, operator, valueSet));
+    }
+
+    public void addAll(Map<String, String> labels) {
+        if (labels == null) return;
+        labels.entrySet().forEach(entry -> {
+            addMatchExpressionItem(new MatchExpressionItem(entry.getKey(), Operator.EQUALS, Collections.singleton(entry.getValue())));
+        });
+    }
+
+
+    private void addMatchExpressionItem(MatchExpressionItem matchExpressionItem) {
+        if (matchExpressionItem == null) {
+            return;
+        }
+        if (matchExpressionItems == null) {
+            matchExpressionItems = new LinkedList<>();
+        }
+        matchExpressionItems.add(matchExpressionItem);
+    }
+
+    @Data
+    @EqualsAndHashCode
+    @NoArgsConstructor
+    @AllArgsConstructor
+    private static class MatchExpressionItem{
+
+        private String key;
+
+        private Operator operator;
+
+        private Set<String> values;
+
+        private String toJsonString() {
+            return operator == null ? "" : operator.toJsonString(this);
+        }
+
+    }
+
+    public String toJsonString() {
+        StringBuilder sb = new StringBuilder();
+
+        if (matchExpressionItems != null && !matchExpressionItems.isEmpty()) {
+            for (MatchExpressionItem matchExpressionItem : matchExpressionItems) {
+                if (sb.length() > 0) {
+                    sb.append(",");
+                }
+                sb.append(matchExpressionItem.toJsonString());
+            }
+        }
+        return sb.toString();
+    }
+
+    public static String toJsonString(Map<String, String> labels) {
+        StringBuilder result = new StringBuilder();
+        for (Map.Entry<String, String> label : labels.entrySet()) {
+            if (result.length() > 0) {
+                result.append(",");
+            }
+            result.append(toJsonString(label.getKey(), label.getValue()));
+        }
+        return result.toString();
+    }
+
+    public static String toJsonString(String key, String value) {
+        return key + "=" + value;
+    }
+
+}

+ 36 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/Namespace.java

@@ -0,0 +1,36 @@
+package seecoder.devcloud.api.k8s.model;
+
+
+import io.kubernetes.client.openapi.models.V1Namespace;
+import io.kubernetes.client.openapi.models.V1NamespaceBuilder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import seecoder.devcloud.common.util.DateUtil;
+
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+public class Namespace extends K8sAbstractObject<V1Namespace> {
+
+    private static final String KIND = "Namespace";
+
+    public static final String API_VERSION = "v1";
+
+    private LocalDateTime createdAt;
+
+    public Namespace(V1Namespace v1Namespace) {
+        super(v1Namespace);
+        this.createdAt = DateUtil.fromJodaDateTime(v1Namespace.getMetadata().getCreationTimestamp());
+    }
+
+    public V1Namespace toK8sObject() {
+        V1Namespace v1Namespace = new V1NamespaceBuilder()
+                .withApiVersion(getApiVersion() == null ? API_VERSION : getApiVersion())
+                .withKind(getKind() == null ? KIND : getKind())
+                .withMetadata(this.toV1ObjectMeta())
+                .build();
+        return v1Namespace;
+    }
+
+}

+ 87 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/PersistentVolumeClaim.java

@@ -0,0 +1,87 @@
+package seecoder.devcloud.api.k8s.model;
+
+import io.kubernetes.client.custom.Quantity;
+import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim;
+import io.kubernetes.client.openapi.models.V1PersistentVolumeClaimBuilder;
+import io.kubernetes.client.openapi.models.V1PersistentVolumeClaimSpecBuilder;
+import io.kubernetes.client.openapi.models.V1ResourceRequirementsBuilder;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class PersistentVolumeClaim extends K8sAbstractObject<V1PersistentVolumeClaim> {
+
+    public static final String API_VERSION = "v1";
+
+    public static final String KIND = "PersistentVolumeClaim";
+
+    private String storageClassName;
+
+    private String volumeMode;
+
+    private List<String> accessModes = new ArrayList<>();
+
+    private Quantity quantity;
+
+    private static final Quantity DEFAULT_QUANTITY = Quantity.fromString("1Gi");
+
+    private static final String STORAGE_KEY = "storage";
+
+    public PersistentVolumeClaim(V1PersistentVolumeClaim v1PersistentVolumeClaim) {
+        super(v1PersistentVolumeClaim);
+        accessModes = v1PersistentVolumeClaim.getSpec().getAccessModes();
+        storageClassName = v1PersistentVolumeClaim.getSpec().getStorageClassName();
+        volumeMode = v1PersistentVolumeClaim.getSpec().getVolumeMode();
+        if (v1PersistentVolumeClaim.getSpec().getResources() != null && v1PersistentVolumeClaim.getSpec().getResources().getRequests() != null) {
+            quantity = v1PersistentVolumeClaim.getSpec().getResources().getRequests().get(STORAGE_KEY);
+        }
+    }
+
+    public void merge(V1PersistentVolumeClaim v1PersistentVolumeClaim) {
+        v1PersistentVolumeClaim.getMetadata().setName(getName());
+        v1PersistentVolumeClaim.getMetadata().setNamespace(this.getNamespace());
+        v1PersistentVolumeClaim.getMetadata().setLabels(this.getLabels());
+        v1PersistentVolumeClaim.getMetadata().setAnnotations(this.getAnnotations());
+        v1PersistentVolumeClaim.setSpec(
+                new V1PersistentVolumeClaimSpecBuilder()
+                        .withAccessModes(accessModes)
+                        .withVolumeMode(volumeMode)
+                        .withStorageClassName(storageClassName)
+                        .withResources(
+                                new V1ResourceRequirementsBuilder()
+                                        .withRequests(Collections.singletonMap(STORAGE_KEY, quantity == null ? DEFAULT_QUANTITY : quantity))
+                                        .build()
+                        )
+                        .build());
+    }
+
+    @Override
+    public V1PersistentVolumeClaim toK8sObject() {
+        return new V1PersistentVolumeClaimBuilder()
+                .withApiVersion(API_VERSION)
+                .withKind(KIND)
+                .withMetadata(toV1ObjectMeta())
+                .withSpec(
+                    new V1PersistentVolumeClaimSpecBuilder()
+                        .withAccessModes(accessModes)
+                        .withVolumeMode(volumeMode)
+                        .withStorageClassName(storageClassName)
+                        .withResources(
+                                new V1ResourceRequirementsBuilder()
+                                .withRequests(Collections.singletonMap(STORAGE_KEY, quantity == null ? DEFAULT_QUANTITY : quantity))
+                                .build()
+                        )
+                        .build()
+                )
+                .build();
+    }
+}

+ 50 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/Pod.java

@@ -0,0 +1,50 @@
+package seecoder.devcloud.api.k8s.model;
+
+import io.kubernetes.client.openapi.models.V1Pod;
+import io.kubernetes.client.openapi.models.V1PodBuilder;
+import io.kubernetes.client.openapi.models.V1PodStatus;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.ReadOnlyProperty;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * author: rale
+ * createdAt: 3/4/19
+ */
+@Data
+@EqualsAndHashCode
+@NoArgsConstructor
+@AllArgsConstructor
+public class Pod extends K8sAbstractObject<V1Pod> {
+
+    public static final String API_VERSION = "v1";
+    public static final String KIND = "Pod";
+
+    @ReadOnlyProperty
+    private List<Container> containers;
+
+    @ReadOnlyProperty
+    private V1PodStatus podStatus;
+
+
+    public Pod(V1Pod v1Pod) {
+        super(v1Pod);
+        this.containers = v1Pod.getSpec().getContainers().stream().map(Container::new).collect(Collectors.toList());
+        this.podStatus = v1Pod.getStatus();
+    }
+
+    @Override
+    public V1Pod toK8sObject() {
+        return new V1PodBuilder()
+                .withApiVersion(getApiVersion() == null ? API_VERSION : getApiVersion())
+                .withKind(getKind() == null ? KIND : getKind())
+                .withMetadata(this.toV1ObjectMeta())
+                .withSpec(null)
+                .build();
+    }
+}

+ 10 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/PodStatus.java

@@ -0,0 +1,10 @@
+package seecoder.devcloud.api.k8s.model;
+
+/**
+ * author: rale
+ * createdAt: 3/4/19
+ */
+public class PodStatus {
+
+
+}

+ 72 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/Service.java

@@ -0,0 +1,72 @@
+package seecoder.devcloud.api.k8s.model;
+
+import io.kubernetes.client.openapi.models.V1Service;
+import io.kubernetes.client.openapi.models.V1ServiceBuilder;
+import io.kubernetes.client.openapi.models.V1ServiceSpec;
+import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class Service extends K8sAbstractObject<V1Service> {
+
+    public static final String API_VERSION = "v1";
+
+    private static final String KIND = "Service";
+
+    private String name;
+
+    private String namespace;
+
+    private Map<String, String> labels = new HashMap<>();
+
+    private Map<String, String> annotations = new HashMap<>();
+
+    private Set<ServicePort> servicePorts = new HashSet<>();
+
+    private Map<String, String> selectors = new HashMap<>();
+
+    public Service(V1Service v1Service) {
+        super(v1Service);
+        this.servicePorts = v1Service.getSpec().getPorts() == null ? null : v1Service.getSpec().getPorts().stream().map(ServicePort::new).collect(Collectors.toSet());
+        this.selectors = v1Service.getSpec().getSelector();
+    }
+
+    public void merge(V1Service v1Service) {
+        v1Service.getMetadata().setNamespace(this.namespace);
+        v1Service.getMetadata().setName(this.name);
+        v1Service.getSpec().setPorts(servicePorts == null ? null : servicePorts.stream().map(ServicePort::toV1ServicePort).collect(Collectors.toList()));
+        v1Service.getSpec().setSelector(this.selectors);
+    }
+
+    private V1ServiceSpec toV1ServiceSpec() {
+        V1ServiceSpec serviceSpec = new V1ServiceSpecBuilder()
+                .withSelector(selectors)
+                .withPorts(servicePorts == null ? null : servicePorts.stream().map(ServicePort::toV1ServicePort).collect(Collectors.toList()))
+                .build();
+        return serviceSpec;
+    }
+
+    @Override
+    public V1Service toK8sObject() {
+        V1Service v1Service = new V1ServiceBuilder()
+                .withApiVersion(getApiVersion() == null ? API_VERSION : getApiVersion())
+                .withKind(getKind() == null ? KIND : getKind())
+                .withMetadata(this.toV1ObjectMeta())
+                .withSpec(this.toV1ServiceSpec())
+                .build();
+        return v1Service;
+    }
+
+}

+ 51 - 0
api/src/main/java/seecoder/devcloud/api/k8s/model/ServicePort.java

@@ -0,0 +1,51 @@
+package seecoder.devcloud.api.k8s.model;
+
+import io.kubernetes.client.custom.IntOrString;
+import io.kubernetes.client.openapi.models.V1ServicePort;
+import io.kubernetes.client.openapi.models.V1ServicePortBuilder;
+import lombok.*;
+import org.hibernate.validator.constraints.Range;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * author: rale
+ * createdAt: 12/23/18
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode
+@AllArgsConstructor
+@Builder
+public class ServicePort {
+
+    @NotNull
+    private String name;
+
+    /**
+     * 服务端口号
+     */
+    @Range(min = 1, max = 633)
+    private int port;
+
+    /**
+     * 容器端口号
+     */
+    @NotNull
+    private int targetPort;
+
+    public ServicePort(V1ServicePort v1ServicePort) {
+        this.name = v1ServicePort.getName();
+        this.port = v1ServicePort.getPort();
+        this.targetPort = v1ServicePort.getTargetPort() == null ? -1 : v1ServicePort.getTargetPort().getIntValue();
+    }
+
+    public V1ServicePort toV1ServicePort(){
+        return new V1ServicePortBuilder()
+                .withName(name)
+                .withPort(port)
+                .withTargetPort(new IntOrString(targetPort))
+                .build();
+    }
+
+}

+ 41 - 0
api/src/main/java/seecoder/devcloud/api/k8s/util/SecretTypeEnum.java

@@ -0,0 +1,41 @@
+package seecoder.devcloud.api.k8s.util;
+
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * author: rale
+ * createdAt: 1/3/19
+ */
+public enum SecretTypeEnum {
+    /*基本类型如数据库用户名密码,KEY-VALUE形式*/
+    Generic("Opaque"),
+
+    /*私有仓库的验证信息*/
+    REGISTRY("kubernetes.io/dockerconfigjson"),
+
+    /*账户验证信息,暂未用到*/
+    SERVICE_ACCOUNT("Service Account");
+
+    private String value;
+
+    SecretTypeEnum(String value) {
+        this.value = value;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public boolean equals(String value) {
+        return StringUtils.equals(this.value, value);
+    }
+
+    public static SecretTypeEnum getByValue(String value) {
+        for (SecretTypeEnum secretTypeEnum : SecretTypeEnum.values()){
+            if (StringUtils.equals(secretTypeEnum.getValue(), value)) {
+                return secretTypeEnum;
+            }
+        }
+        return null;
+    }
+}

+ 47 - 0
api/src/main/java/seecoder/devcloud/api/k8s/vo/SecretVO.java

@@ -0,0 +1,47 @@
+package seecoder.devcloud.api.k8s.vo;
+
+
+import io.kubernetes.client.openapi.models.V1Secret;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.util.CollectionUtils;
+import seecoder.devcloud.api.k8s.util.SecretTypeEnum;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * author: rale
+ * createdAt: 1/3/19
+ */
+@Data
+@NoArgsConstructor
+public class SecretVO {
+
+    /**
+     * Secret的名字 格式:DeployEnv-projectName-DB_SECRET_NAME
+     */
+    private String name;
+
+    private String namespace;
+
+    private Map<String, String> data;
+
+    private SecretTypeEnum secretType;
+
+    public SecretVO(V1Secret v1Secret) {
+        this.name = v1Secret.getMetadata().getName();
+        this.namespace = v1Secret.getMetadata().getNamespace();
+        this.data = new HashMap<>();
+        if (!CollectionUtils.isEmpty(v1Secret.getStringData())) {
+            data.putAll(v1Secret.getStringData());
+        }
+        if (!CollectionUtils.isEmpty(v1Secret.getData())) {
+            for (Map.Entry<String, byte[]> entry : v1Secret.getData().entrySet()){
+                data.put(entry.getKey(), new String(entry.getValue(), StandardCharsets.UTF_8));
+            }
+        }
+        this.secretType = SecretTypeEnum.getByValue(v1Secret.getType());
+    }
+}

+ 19 - 0
api/src/main/java/seecoder/devcloud/api/nexus/AssetApi.java

@@ -0,0 +1,19 @@
+package seecoder.devcloud.api.nexus;
+
+
+
+import seecoder.devcloud.api.nexus.exception.NexusApiException;
+import seecoder.devcloud.api.nexus.model.Asset;
+import seecoder.devcloud.api.nexus.model.AssetList;
+
+import java.util.List;
+
+public interface AssetApi {
+
+    AssetList listAssets(String repository, String continuationToken) throws NexusApiException;
+
+    List<Asset> listAllAssets(String repository) throws NexusApiException;
+
+    Asset readAsset(String id) throws NexusApiException;
+
+}

+ 79 - 0
api/src/main/java/seecoder/devcloud/api/nexus/ComponentApi.java

@@ -0,0 +1,79 @@
+package seecoder.devcloud.api.nexus;
+
+
+
+import seecoder.devcloud.api.nexus.exception.NexusApiException;
+import seecoder.devcloud.api.nexus.model.Component;
+import seecoder.devcloud.api.nexus.model.ComponentList;
+
+import java.util.List;
+
+public interface ComponentApi {
+
+    /**
+     * 查看仓库下的component列表,continuationToken是上一次请求返回的,用来查询下一页的内容
+     * 如果continuation为空 默认查找第一页的内容
+     * @param repo 不能为空
+     * @param continuationToken 可以为空
+     * @return
+     * @throws NexusApiException 如果仓库不存在,抛出404
+     * @throws NexusApiException 如果continuationToken有误,抛出500
+     */
+    ComponentList listComponents(String repo, String continuationToken) throws NexusApiException;
+
+    /**
+     * 获取所有的Component
+     * 具体的实现是采用continuationToken通过HTTP发出下一页请求并将请求结果合并到结果集中
+     * 如果仓库中Component较多且变化频繁,强烈建议不要使用该接口
+     * @param repo
+     * @return
+     * @throws NexusApiException 如果仓库不存在,抛出404
+     */
+    List<Component> listAllComponents(String repo) throws NexusApiException;
+
+    /**
+     * 根据id获取component
+     * @param id 不能为空
+     * @return
+     * @throws NexusApiException 如果组件不存在 抛出404
+     */
+    Component readComponent(String id) throws NexusApiException;
+
+    /**
+     * 根据仓库名,镜像名和默认镜像标签latest找到Docker镜像Component
+     * @param repository 不能为空
+     * @param imageName 不能为空
+     * @return
+     * @throws NexusApiException 如果镜像不存在 抛出404
+     */
+    Component findDockerImage(String repository, String imageName) throws NexusApiException;
+
+    /**
+     * 根据仓库名,镜像名和镜像标签找到Docker镜像Component
+     * @param repository 不能为空
+     * @param imageName 不能为空
+     * @param tag 不能为空
+     * @return
+     * @throws NexusApiException 如果镜像不存在,抛出404
+     */
+    Component findDockerImage(String repository, String imageName, String tag) throws NexusApiException;
+
+    /**
+     * 根据仓库名,镜像名找到所有的Docker镜像Component
+     * continuationToken是上一次请求返回的,用来查询下一页的内容
+     * 如果continuation为空 默认查找第一页的内容
+     * @param repository 不能为空
+     * @param imageName 不能为空
+     * @param continuationToken 可以为空
+     * @return
+     * @throws NexusApiException 如果continuationToken有误,抛出500
+     */
+    ComponentList findDockerImages(String repository, String imageName, String continuationToken) throws NexusApiException;
+
+    /**
+     * 根据组件ID,删除组件
+     * @param id 不能为空
+     * @throws NexusApiException 如果组件不存在,抛出404
+     */
+    void deleteComponent(String id) throws NexusApiException;
+}

+ 187 - 0
api/src/main/java/seecoder/devcloud/api/nexus/NexusApi.java

@@ -0,0 +1,187 @@
+package seecoder.devcloud.api.nexus;
+
+
+
+import seecoder.devcloud.api.nexus.impl.AssetApiImpl;
+import seecoder.devcloud.api.nexus.impl.ComponentApiImpl;
+import seecoder.devcloud.api.nexus.impl.TaskApiImpl;
+import seecoder.devcloud.api.nexus.model.ApiVersion;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.ClientHttpRequestInterceptor;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+public class NexusApi {
+
+    private static final String DEFAULT_USER = "admin";
+    private static final String DEFAULT_PASSWORD = "admin123";
+    private static final String CONTENT_TYPE = "application/json";
+
+    private ApiVersion apiVersion;
+    private String username = DEFAULT_USER;
+    private String password = DEFAULT_PASSWORD;
+    private String nexusServerUrl;
+    private String restApiUrl;
+    private AssetApi assetApi;
+    private ComponentApi componentApi;
+    private TaskApi taskApi;
+
+    private RestTemplate restTemplate; //惰性调用getRestTemplate方法初始化
+
+    public NexusApi(String nexusServerUrl) {
+        this(nexusServerUrl, ApiVersion.V1);
+    }
+
+    public NexusApi(String nexusServerUrl, ApiVersion apiVersion) {
+        this(nexusServerUrl, apiVersion, DEFAULT_USER, DEFAULT_PASSWORD);
+    }
+
+    public NexusApi(String nexusServerUrl, ApiVersion apiVersion, String username, String password) {
+        this.nexusServerUrl = nexusServerUrl.endsWith("/") ? nexusServerUrl.replaceAll("/$", "") : nexusServerUrl;
+        this.nexusServerUrl = nexusServerUrl.startsWith("http") ? nexusServerUrl : "http://" + nexusServerUrl;
+        this.apiVersion = apiVersion;
+        this.username = username;
+        this.password = password;
+    }
+
+    public AssetApi getAssetApi(){
+        if (assetApi == null) {
+            synchronized (this) {
+                if (assetApi == null) {
+                    assetApi = new AssetApiImpl(this);
+                }
+            }
+        }
+        return this.assetApi;
+    }
+
+    public ComponentApi getComponentApi(){
+        if (componentApi == null) {
+            synchronized (this) {
+                if (componentApi == null) {
+                    componentApi = new ComponentApiImpl(this);
+                }
+            }
+        }
+        return this.componentApi;
+    }
+
+    public TaskApi getTaskApi(){
+        if (taskApi == null) {
+            synchronized (this) {
+                if (taskApi == null) {
+                    taskApi = new TaskApiImpl(this);
+                }
+            }
+        }
+        return this.taskApi;
+    }
+
+    public ApiVersion getApiVersion() {
+        return apiVersion;
+    }
+
+    public <T> ResponseEntity<T> get(String path, Class<T> responseType, Map<String, List<String>> params, Object... pathVariables) {
+        String url = buildUri(path, params, pathVariables);
+        log.debug("生成的url={}", url);
+        return getRestTemplate().getForEntity(url, responseType);
+    }
+
+    public <T> ResponseEntity<T> get(String path, Class<T> responseType, Map<String, List<String>> params, Map<String, Object> pathVariables) {
+        String url = buildUri(path, params, pathVariables);
+        log.debug("生成的url={}", url);
+        return getRestTemplate().getForEntity(url, responseType);
+    }
+
+    public <T> ResponseEntity<T> post(String path, Class<T> responseType, Object data, Map<String, List<String>> params, Object... pathVariables) {
+        String url = buildUri(path, params, pathVariables);
+        return getRestTemplate().postForEntity(url, data, responseType);
+    }
+
+    public <T> ResponseEntity<T> post(String path, Class<T> responseType, Object data, Map<String, List<String>> params, Map<String, Object> pathVariables) {
+        String url = buildUri(path, params, pathVariables);
+        return getRestTemplate().postForEntity(url, data, responseType);
+    }
+
+    public ResponseEntity<Void> delete(String path, Map<String, List<String>> params, Object... pathVariables) {
+        String url = buildUri(path, params, pathVariables);
+        return getRestTemplate().exchange(url, HttpMethod.DELETE, HttpEntity.EMPTY, Void.class);
+    }
+
+    public ResponseEntity<Void> delete(String path, Map<String, List<String>> params, Map<String, Object> pathVariables) {
+        String url = buildUri(path, params, pathVariables);
+        return getRestTemplate().exchange(url, HttpMethod.DELETE, HttpEntity.EMPTY, Void.class);
+    }
+
+    private String buildUri(String path, Map<String, List<String>> params, Object... pathVariables) {
+        path = validatePath(path);
+        MultiValueMap<String, String> paramMap = toMultiValueMap(params);
+        return UriComponentsBuilder.fromHttpUrl(getRestApiUrl())
+                .path(path)
+                .queryParams(paramMap)
+                .build(pathVariables)
+                .toString();
+    }
+    private String buildUri(String path, Map<String, List<String>> params, Map<String, Object> pathVariables) {
+        path = validatePath(path);
+        MultiValueMap<String, String> paramMap = toMultiValueMap(params);
+        return UriComponentsBuilder.fromHttpUrl(getRestApiUrl())
+                .path(path)
+                .queryParams(paramMap)
+                .build(pathVariables)
+                .toString();
+    }
+
+
+    private String validatePath(String path) {
+        return path.startsWith("/") ? path : "/" + path;
+    }
+
+    private MultiValueMap<String, String> toMultiValueMap(Map<String, List<String>> params) {
+        return (params == null) ?
+                CollectionUtils.toMultiValueMap(Collections.EMPTY_MAP) :
+                CollectionUtils.toMultiValueMap(params);
+    }
+
+    private String getRestApiUrl() {
+        if (this.restApiUrl == null) {
+            synchronized (this) {
+                if (this.restApiUrl == null) {
+                    restApiUrl = this.nexusServerUrl + ApiVersion.V1.getApiNamespace();
+                }
+            }
+
+        }
+        return this.restApiUrl;
+    }
+
+    private RestTemplate getRestTemplate() {
+        if (this.restTemplate == null) {
+            synchronized (this) {
+                if (this.restTemplate == null) {
+                    this.restTemplate = new RestTemplateBuilder()
+                            .basicAuthentication(username, password)
+                            .additionalInterceptors((ClientHttpRequestInterceptor) (httpRequest, bytes, clientHttpRequestExecution) -> {
+                                HttpHeaders headers = httpRequest.getHeaders();
+                                headers.add("Accept", CONTENT_TYPE);
+                                return clientHttpRequestExecution.execute(httpRequest, bytes);
+                            })
+                            .build();
+                }
+            }
+        }
+        return restTemplate;
+    }
+}

+ 16 - 0
api/src/main/java/seecoder/devcloud/api/nexus/NexusApiBean.java

@@ -0,0 +1,16 @@
+package seecoder.devcloud.api.nexus;
+
+import seecoder.devcloud.api.ApplicationProperties;
+import seecoder.devcloud.api.nexus.model.ApiVersion;
+import org.springframework.context.annotation.Bean;
+import org.springframework.stereotype.Component;
+
+@Component
+public class NexusApiBean {
+
+    @Bean
+    public NexusApi getNexusApi(ApplicationProperties applicationProperties) {
+        return new NexusApi(applicationProperties.getRegistry().getNexusServer(), ApiVersion.V1, applicationProperties.getRegistry().getUsername(), applicationProperties.getRegistry().getPassword());
+    }
+
+}

+ 53 - 0
api/src/main/java/seecoder/devcloud/api/nexus/TaskApi.java

@@ -0,0 +1,53 @@
+package seecoder.devcloud.api.nexus;
+
+
+
+import seecoder.devcloud.api.nexus.exception.NexusApiException;
+import seecoder.devcloud.api.nexus.model.Task;
+import seecoder.devcloud.api.nexus.model.TaskList;
+
+import java.util.List;
+
+public interface TaskApi {
+
+    /**
+     * 根据continuationToken获取下一页的Task列表
+     * 如果continuationToken为空 则获取第一页的Task列表
+     * @param continuationToken 可以为空
+     * @return
+     * @throws NexusApiException 500 非法continuationToken
+     */
+    TaskList listTasks(String continuationToken) throws NexusApiException;
+
+    /**
+     * 获取所有的任务
+     * 具体实现采用continuationToken获取下一页Task列表,直到continuationToken为空
+     * 如果task数量较多,不要频繁使用该接口机
+     * @return
+     * @throws NexusApiException
+     */
+    List<Task> listAllTasks() throws NexusApiException;
+
+    /**
+     * 根据id获取任务的详细信息
+     * @param id 不能为空
+     * @return
+     * @throws NexusApiException 404 任务不存在
+     */
+    Task readTask(String id) throws NexusApiException;
+
+    /**
+     * 根据id启动任务
+     * @param id 不能为空
+     * @throws NexusApiException 404 任务不存在
+     */
+    void runTask(String id) throws NexusApiException;
+
+    /**
+     * 根据id停止任务
+     * @param id 不能为空
+     * @throws NexusApiException 404 任务不存在
+     * @throws NexusApiException 409 任务无法停止或已经停止
+     */
+    void stopTask(String id) throws NexusApiException;
+}

+ 73 - 0
api/src/main/java/seecoder/devcloud/api/nexus/exception/NexusApiException.java

@@ -0,0 +1,73 @@
+package seecoder.devcloud.api.nexus.exception;
+
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class NexusApiException extends Exception {
+
+    private int code;
+
+    private String message;
+
+    public static final int INSUFFICIENT_PERMISSION = 403;
+    public static final int NOT_FOUND = 404;
+    public static final int CONFLICT = 409;
+    public static final int INVALID_PARAMS = 422;
+    public static final int INTERNAL_SERVER_ERROR = 500;
+
+    public static final String INSUFFICIENT_PERMISSION_DESCRIPTION = "权限不足";
+    public static final String NOT_FOUND_DESCRIPTION = "资源不存在";
+    public static final String INVALID_PARAMS_DESCRIPTION = "非法参数";
+    public static final String INTERNAL_SERVER_ERROR_DESCRIPTION = "内部服务异常";
+    public static final String CONFLICT_DESCRIPTION = "请求与服务器端目标资源的当前状态相冲突";
+
+    public static final Map<Integer, String> error;
+    static {
+        error = new HashMap<>();
+        error.put(INSUFFICIENT_PERMISSION, INSUFFICIENT_PERMISSION_DESCRIPTION);
+        error.put(NOT_FOUND, NOT_FOUND_DESCRIPTION);
+        error.put(INVALID_PARAMS, INVALID_PARAMS_DESCRIPTION);
+        error.put(INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR_DESCRIPTION);
+        error.put(CONFLICT, CONFLICT_DESCRIPTION);
+    }
+
+    public NexusApiException(int code, String message) {
+        super(message);
+        this.code = code;
+        this.message = message;
+    }
+
+    public NexusApiException(int code) {
+        this.code = error.containsKey(code) ? code : INTERNAL_SERVER_ERROR;
+        this.message = error.get(this.code);
+    }
+
+    public NexusApiException(Exception e) {
+        super(e);
+        code = INTERNAL_SERVER_ERROR;
+        message = e.getMessage();
+    }
+
+
+    @Override
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public void setCode(int code) {
+        this.code = code;
+    }
+
+    public boolean isNotFound() {
+        return this.getCode() == NOT_FOUND;
+    }
+}

+ 99 - 0
api/src/main/java/seecoder/devcloud/api/nexus/impl/AbstractApi.java

@@ -0,0 +1,99 @@
+package seecoder.devcloud.api.nexus.impl;
+
+
+import seecoder.devcloud.api.nexus.NexusApi;
+import seecoder.devcloud.api.nexus.exception.NexusApiException;
+import seecoder.devcloud.api.nexus.model.ApiVersion;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.HttpClientErrorException;
+
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+public abstract class AbstractApi implements Constants{
+
+    private final NexusApi nexusApi;
+
+    public AbstractApi(NexusApi nexusApi) {
+        this.nexusApi = nexusApi;
+    }
+
+    /**
+     * 默认处理资源不存在和无权限执行异常,其它异常可以在具体的子接口中覆盖重写
+     * @param responseEntity
+     * @param <T>
+     * @return
+     * @throws NexusApiException
+     */
+    protected <T> ResponseEntity<T> validate(ResponseEntity<T> responseEntity) throws NexusApiException {
+        if (responseEntity.getStatusCode().value() == NexusApiException.NOT_FOUND) {
+            throw new NexusApiException(NexusApiException.NOT_FOUND);
+        } else if (responseEntity.getStatusCode().value() == NexusApiException.INSUFFICIENT_PERMISSION) {
+            throw new NexusApiException(NexusApiException.INSUFFICIENT_PERMISSION);
+        } else if (responseEntity.getStatusCodeValue() == NexusApiException.INTERNAL_SERVER_ERROR) {
+            throw new NexusApiException(NexusApiException.INTERNAL_SERVER_ERROR);
+        } else if (responseEntity.getStatusCodeValue() == NexusApiException.INVALID_PARAMS) {
+            throw new NexusApiException(NexusApiException.INVALID_PARAMS);
+        }
+        return responseEntity;
+    }
+
+    protected  <T> ResponseEntity<T> get(String path, Class<T> responseType, Map<String, List<String>> params, Object... pathVariables) throws NexusApiException {
+        return validate(nexusApi.get(path, responseType, params, pathVariables));
+    }
+
+    protected  <T> ResponseEntity<T> get(String path, Class<T> responseType, Map<String, List<String>> params, Map<String, Object> pathVariables) throws NexusApiException {
+        return validate(nexusApi.get(path, responseType, params, pathVariables));
+    }
+
+    protected  <T> ResponseEntity<T> post(String path, Class<T> responseType, Object data, Map<String, List<String>> params, Object... pathVariables) throws NexusApiException {
+        return validate(nexusApi.post(path, responseType, data, params, pathVariables));
+    }
+
+    protected  <T> ResponseEntity<T> post(String path, Class<T> responseType, Object data, Map<String, List<String>> params, Map<String, Object> pathVariables) throws NexusApiException {
+        return validate(nexusApi.post(path, responseType, data, params, pathVariables));
+    }
+
+    protected ResponseEntity<Void> delete(String path, Map<String, List<String>> params, Object... pathVariables) throws NexusApiException {
+        return validate(nexusApi.delete(path, params, pathVariables));
+    }
+
+    protected ResponseEntity<Void> delete(String path, Map<String, List<String>> params, Map<String, Object> pathVariables) throws NexusApiException {
+        return validate(nexusApi.delete(path, params, pathVariables));
+    }
+
+    protected NexusApiException handle(Exception thrown) {
+        log.error("Nexus异常", thrown);
+        if (thrown instanceof NexusApiException) {
+            return ((NexusApiException) thrown);
+        }
+        if (thrown instanceof HttpClientErrorException) {
+            HttpClientErrorException e = (HttpClientErrorException) thrown;
+            return new NexusApiException(e.getRawStatusCode());
+        }
+        return (new NexusApiException(thrown));
+    }
+
+    protected ApiVersion getApiVersion() {
+        return nexusApi.getApiVersion();
+    }
+
+    protected boolean isApiVersion(ApiVersion apiVersion) {
+        return nexusApi.getApiVersion() == apiVersion;
+    }
+
+    protected NexusApi getNexusApi(){
+        return this.nexusApi;
+    }
+
+    protected void validateId(String id) throws NexusApiException {
+        if (id == null || id.isEmpty()) throw new NexusApiException(NexusApiException.INVALID_PARAMS, "id不能为空");
+    }
+
+    protected void validateRepo(String repo) throws NexusApiException {
+        if (repo == null || repo.isEmpty()) throw new NexusApiException(NexusApiException.INVALID_PARAMS, "仓库名称不能为空");
+    }
+
+}

+ 67 - 0
api/src/main/java/seecoder/devcloud/api/nexus/impl/AssetApiImpl.java

@@ -0,0 +1,67 @@
+package seecoder.devcloud.api.nexus.impl;
+
+
+
+import seecoder.devcloud.api.nexus.AssetApi;
+import seecoder.devcloud.api.nexus.NexusApi;
+import seecoder.devcloud.api.nexus.exception.NexusApiException;
+import seecoder.devcloud.api.nexus.model.Asset;
+import seecoder.devcloud.api.nexus.model.AssetList;
+
+import java.util.*;
+
+public class AssetApiImpl extends AbstractApi implements AssetApi {
+
+    private static final String LIST_ASSETS_PATH = ASSET_PATH;
+    private static final String GET_ASSET_PATH = ASSET_PATH + "/{id}";
+
+    public AssetApiImpl(NexusApi nexusApi) {
+        super(nexusApi);
+    }
+
+    @Override
+    public AssetList listAssets(String repository, String continuationToken) throws NexusApiException {
+        try {
+            validateRepo(repository);
+            Map<String, List<String>> params = new HashMap<>();
+            params.put(REQUEST_PARAM_REPOSITORY, Collections.singletonList(repository));
+            if (continuationToken != null && !continuationToken.isEmpty()) {
+                params.put(REQUEST_PARAM_CONTINUATION_TOKEN, Collections.singletonList(continuationToken));
+            }
+            return get(LIST_ASSETS_PATH, AssetList.class, params).getBody();
+        } catch (Exception e) {
+            throw handle(e);
+        }
+    }
+
+    @Override
+    public List<Asset> listAllAssets(String repository) throws NexusApiException {
+        try {
+            validateRepo(repository);
+            List<Asset> result = new ArrayList<>();
+            String continuationToken = "";
+            AssetList assetList;
+            boolean hasNext = true;
+            while (hasNext) {
+                 assetList = listAssets(repository, continuationToken);
+                result.addAll(assetList.getItems());
+                continuationToken = assetList.getContinuationToken();
+                hasNext = (continuationToken != null);
+            }
+            return result;
+        } catch (Exception e) {
+            throw handle(e);
+        }
+    }
+
+    @Override
+    public Asset readAsset(String id) throws NexusApiException {
+        try {
+            validateId(id);
+            return get(GET_ASSET_PATH, Asset.class, null, id).getBody();
+        }catch (Exception e) {
+            throw handle(e);
+        }
+    }
+
+}

+ 144 - 0
api/src/main/java/seecoder/devcloud/api/nexus/impl/ComponentApiImpl.java

@@ -0,0 +1,144 @@
+package seecoder.devcloud.api.nexus.impl;
+
+
+
+import seecoder.devcloud.api.nexus.ComponentApi;
+import seecoder.devcloud.api.nexus.NexusApi;
+import seecoder.devcloud.api.nexus.exception.NexusApiException;
+import seecoder.devcloud.api.nexus.model.Component;
+import seecoder.devcloud.api.nexus.model.ComponentList;
+import org.springframework.http.ResponseEntity;
+
+import java.util.*;
+
+public class ComponentApiImpl extends AbstractApi implements ComponentApi {
+
+    private static final String LIST_COMPONENTS_PATH = COMPONENTS_PATH;
+    private static final String GET_COMPONENT_PATH = COMPONENTS_PATH + "/{id}";
+    private static final String DELETE_COMPONENT_PATH = COMPONENTS_PATH + "/{id}";
+    private static final String SEARCH_COMPONENT_PATH = SEARCH_PATH;
+    private static final String DOCKER_IMAGE_DEFAULT_VERSION = "latest";
+
+    public ComponentApiImpl(NexusApi nexusApi) {
+        super(nexusApi);
+    }
+
+    @Override
+    public ComponentList listComponents(String repo, String continuationToken) throws NexusApiException {
+        try {
+            validateRepo(repo);
+            Map<String, List<String>> params = new HashMap<>();
+            params.put(REQUEST_PARAM_REPOSITORY, Collections.singletonList(repo));
+            if (continuationToken != null && !continuationToken.isEmpty()) {
+                params.put(REQUEST_PARAM_CONTINUATION_TOKEN, Collections.singletonList(continuationToken));
+            }
+            ResponseEntity<ComponentList> responseEntity = get(LIST_COMPONENTS_PATH, ComponentList.class, params);
+            return responseEntity.getBody();
+        }catch (Exception e) {
+            throw handle(e);
+        }
+    }
+
+    @Override
+    public List<Component> listAllComponents(String repo) throws NexusApiException {
+        try {
+            validateRepo(repo);
+            List<Component> result = new ArrayList<>();
+            String continuationToken = "";
+            ComponentList componentList;
+            boolean hasNext = true;
+            while (hasNext) {
+                componentList = listComponents(repo, continuationToken);
+                result.addAll(componentList.getItems());
+                continuationToken = componentList.getContinuationToken();
+                hasNext = (continuationToken != null);
+            }
+            return result;
+        } catch (Exception e) {
+            throw handle(e);
+        }
+    }
+
+    @Override
+    public Component readComponent(String id) throws NexusApiException {
+        try {
+            validateId(id);
+            ResponseEntity<Component> responseEntity = get(GET_COMPONENT_PATH, Component.class, null, id);
+            return responseEntity.getBody();
+        }catch (Exception e) {
+            throw handle(e);
+        }
+    }
+
+    @Override
+    public Component findDockerImage(String repository, String imageName) throws NexusApiException {
+        return findDockerImage(repository, imageName, DOCKER_IMAGE_DEFAULT_VERSION);
+    }
+
+    @Override
+    public Component findDockerImage(String repository, String imageName, String imageTag) throws NexusApiException {
+        try {
+            validateRepo(repository);
+            validateName(imageName);
+            validateVersion(imageTag);
+
+            Map<String, List<String>> params = new HashMap<>();
+            params.put(REQUEST_PARAM_REPOSITORY, Collections.singletonList(repository));
+            params.put(REQUEST_PARAM_DOCKER_IMAGE_NAME, Collections.singletonList(imageName));
+            params.put(REQUEST_PARAM_DOCKER_IMAGE_TAG, Collections.singletonList(imageTag));
+            ResponseEntity<ComponentList> responseEntity = get(SEARCH_COMPONENT_PATH, ComponentList.class, params);
+
+            ComponentList componentList = responseEntity.getBody();
+            Component result = null;
+            if (!componentList.getItems().isEmpty()) {
+                result = componentList.getItems().get(0);
+            }
+            if (result == null) {
+                throw new NexusApiException(NexusApiException.NOT_FOUND);
+            }
+            return result;
+        } catch (Exception e) {
+            throw handle(e);
+        }
+    }
+
+    @Override
+    public ComponentList findDockerImages(String repository, String imageName, String continuationToken) throws NexusApiException {
+        try {
+            validateRepo(repository);
+            validateName(imageName);
+
+            Map<String, List<String>> params = new HashMap<>();
+            params.put(REQUEST_PARAM_REPOSITORY, Collections.singletonList(repository));
+            params.put(REQUEST_PARAM_DOCKER_IMAGE_NAME, Collections.singletonList(imageName));
+            if (continuationToken != null && !continuationToken.isEmpty()) {
+                params.put(REQUEST_PARAM_CONTINUATION_TOKEN, Collections.singletonList(continuationToken));
+            }
+            ResponseEntity<ComponentList> responseEntity = get(SEARCH_COMPONENT_PATH, ComponentList.class, params);
+
+            ComponentList componentList = responseEntity.getBody();
+            return componentList;
+        } catch (Exception e) {
+            throw handle(e);
+        }
+    }
+
+    @Override
+    public void deleteComponent(String id) throws NexusApiException {
+        try {
+            validateId(id);
+            delete(DELETE_COMPONENT_PATH, null, id);
+        }catch (Exception e) {
+            throw handle(e);
+        }
+    }
+
+
+    private void validateName(String name) throws NexusApiException {
+        if (name == null || name.isEmpty()) throw new NexusApiException(NexusApiException.INVALID_PARAMS, "镜像名称不能为空");
+    }
+
+    private void validateVersion(String version) throws NexusApiException {
+        if (version == null || version.isEmpty()) throw new NexusApiException(NexusApiException.INVALID_PARAMS, "版本不能为空");
+    }
+}

+ 28 - 0
api/src/main/java/seecoder/devcloud/api/nexus/impl/Constants.java

@@ -0,0 +1,28 @@
+package seecoder.devcloud.api.nexus.impl;
+
+/**
+ * author: rale
+ * createdAt: 3/25/19
+ */
+public interface Constants {
+
+    String ASSET_PATH = "/assets";
+    String BLOL_STORE_PATH = "/blobstores";
+    String COMPONENTS_PATH = "/components";
+    String FORMATS_PATH = "/formats";
+    String READONLY_PATH = "/read-only";
+    String REPOSITORIES_PATH = "/repositories";
+    String SCRIPT_PATH = "/script";
+    String SEARCH_PATH = "/search";
+    String SUPPORT_PATH = "/support";
+    String TASKS_PATH = "/tasks";
+
+    String REQUEST_PARAM_ID = "id";
+    String REQUEST_PARAM_CONTINUATION_TOKEN = "continuationToken";
+    String REQUEST_PARAM_REPOSITORY = "repository";
+    String REQUEST_PARAM_DOCKER_IMAGE_NAME = "docker.imageName";
+    String REQUEST_PARAM_DOCKER_IMAGE_TAG = "docker.imageTag";
+
+    String DOCKER_COMPONENT_FORMAT = "docker";
+    String MAVEN2_COMPONENT_FORMAT = "maven2";
+}

+ 89 - 0
api/src/main/java/seecoder/devcloud/api/nexus/impl/TaskApiImpl.java

@@ -0,0 +1,89 @@
+package seecoder.devcloud.api.nexus.impl;
+
+
+import seecoder.devcloud.api.nexus.NexusApi;
+import seecoder.devcloud.api.nexus.TaskApi;
+import seecoder.devcloud.api.nexus.exception.NexusApiException;
+import seecoder.devcloud.api.nexus.model.Task;
+import seecoder.devcloud.api.nexus.model.TaskList;
+import org.springframework.http.ResponseEntity;
+
+import java.util.*;
+
+public class TaskApiImpl extends AbstractApi implements TaskApi {
+
+    private static final String LIST_TASKS_PATH = TASKS_PATH;
+    private static final String GET_TASK_PATH = TASKS_PATH + "/{id}";
+    private static final String RUN_TASK_PATH = TASKS_PATH + "/{id}/run";
+    private static final String STOP_TASK_PATH = TASKS_PATH + "/{id}/stop";
+
+    public TaskApiImpl(NexusApi nexusApi) {
+        super(nexusApi);
+    }
+
+    @Override
+    public TaskList listTasks(String continuationToken) throws NexusApiException {
+        try {
+            Map<String, List<String>> params = new HashMap<>();
+            if (continuationToken != null && !continuationToken.isEmpty()) {
+                params.put(REQUEST_PARAM_CONTINUATION_TOKEN, Collections.singletonList(continuationToken));
+            }
+            return get(LIST_TASKS_PATH, TaskList.class, params).getBody();
+        } catch (Exception e) {
+            throw handle(e);
+        }
+    }
+
+    @Override
+    public List<Task> listAllTasks() throws NexusApiException {
+        try {
+            List<Task> result = new ArrayList<>();
+            String continuationToken = "";
+            TaskList taskList;
+            boolean hasNext = true;
+            while (hasNext) {
+                taskList = listTasks(continuationToken);
+                result.addAll(taskList.getItems());
+                continuationToken = taskList.getContinuationToken();
+                hasNext = continuationToken != null;
+            }
+            return result;
+        } catch (Exception e) {
+            throw handle(e);
+        }
+    }
+
+    @Override
+    public Task readTask(String id) throws NexusApiException {
+        try {
+            validateId(id);
+            return get(GET_TASK_PATH, Task.class, null, id).getBody();
+        } catch (Exception e) {
+            throw handle(e);
+        }
+    }
+
+    @Override
+    public void runTask(String id) throws NexusApiException {
+        try {
+            validateId(id);
+            post(RUN_TASK_PATH, Void.class, null,null, id);
+        } catch (Exception e) {
+            throw handle(e);
+        }
+    }
+
+    @Override
+    public void stopTask(String id) throws NexusApiException {
+        try {
+            validateId(id);
+            ResponseEntity<Void> responseEntity = post(STOP_TASK_PATH, Void.class, null,null, id);
+            if (responseEntity.getStatusCodeValue() == NexusApiException.CONFLICT) {
+                throw new NexusApiException(NexusApiException.CONFLICT, "无法停止该任务");
+            }
+        } catch (Exception e) {
+            throw handle(e);
+        }
+    }
+
+}

+ 17 - 0
api/src/main/java/seecoder/devcloud/api/nexus/model/ApiVersion.java

@@ -0,0 +1,17 @@
+package seecoder.devcloud.api.nexus.model;
+
+/**
+ * author: rale
+ * createdAt: 3/21/19
+ */
+public enum ApiVersion {
+    V1;
+
+    private ApiVersion() {
+
+    }
+
+    public String getApiNamespace() {
+        return ("/service/rest/" + name().toLowerCase());
+    }
+}

+ 21 - 0
api/src/main/java/seecoder/devcloud/api/nexus/model/Asset.java

@@ -0,0 +1,21 @@
+package seecoder.devcloud.api.nexus.model;
+
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+public class Asset{
+
+    private String downloadUrl;
+
+    private String path;
+
+    private String id;
+
+    private String repository;
+
+    private String format;
+
+    private Map<String, Object> checksum;
+}

+ 13 - 0
api/src/main/java/seecoder/devcloud/api/nexus/model/AssetList.java

@@ -0,0 +1,13 @@
+package seecoder.devcloud.api.nexus.model;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AssetList extends NexusList{
+
+    private List<Asset> items;
+}

+ 23 - 0
api/src/main/java/seecoder/devcloud/api/nexus/model/Component.java

@@ -0,0 +1,23 @@
+package seecoder.devcloud.api.nexus.model;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class Component {
+
+    private String id;
+
+    private String repository;
+
+    private String format;
+
+    private String group;
+
+    private String name;
+
+    private String version;
+
+    private List<Asset> assets;
+}

+ 16 - 0
api/src/main/java/seecoder/devcloud/api/nexus/model/ComponentList.java

@@ -0,0 +1,16 @@
+package seecoder.devcloud.api.nexus.model;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ComponentList extends NexusList{
+
+    private List<Component> items = new ArrayList<>();
+}

+ 9 - 0
api/src/main/java/seecoder/devcloud/api/nexus/model/NexusList.java

@@ -0,0 +1,9 @@
+package seecoder.devcloud.api.nexus.model;
+
+import lombok.Data;
+
+@Data
+public abstract class NexusList {
+
+    protected String continuationToken;
+}

+ 25 - 0
api/src/main/java/seecoder/devcloud/api/nexus/model/Task.java

@@ -0,0 +1,25 @@
+package seecoder.devcloud.api.nexus.model;
+
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class Task {
+
+    private String id;
+
+    private String name;
+
+    private String type;
+
+    private String message;
+
+    private String currentState;
+
+    private String lastRunResult;
+
+    private Date nextRun;
+
+    private Date lastRun;
+}

+ 15 - 0
api/src/main/java/seecoder/devcloud/api/nexus/model/TaskList.java

@@ -0,0 +1,15 @@
+package seecoder.devcloud.api.nexus.model;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.util.List;
+
+@Data
+@ToString(callSuper = true)
+@EqualsAndHashCode(callSuper = true)
+public class TaskList extends NexusList{
+
+    private List<Task> items;
+}

+ 2 - 0
api/src/main/resources/application.yml

@@ -0,0 +1,2 @@
+logging:
+  level: info

+ 117 - 0
common/.mvn/wrapper/MavenWrapperDownloader.java

@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+    private static final String WRAPPER_VERSION = "0.5.6";
+    /**
+     * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+     */
+    private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+        + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+    /**
+     * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+     * use instead of the default one.
+     */
+    private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+            ".mvn/wrapper/maven-wrapper.properties";
+
+    /**
+     * Path where the maven-wrapper.jar will be saved to.
+     */
+    private static final String MAVEN_WRAPPER_JAR_PATH =
+            ".mvn/wrapper/maven-wrapper.jar";
+
+    /**
+     * Name of the property which should be used to override the default download url for the wrapper.
+     */
+    private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+    public static void main(String args[]) {
+        System.out.println("- Downloader started");
+        File baseDirectory = new File(args[0]);
+        System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+        // If the maven-wrapper.properties exists, read it and check if it contains a custom
+        // wrapperUrl parameter.
+        File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+        String url = DEFAULT_DOWNLOAD_URL;
+        if(mavenWrapperPropertyFile.exists()) {
+            FileInputStream mavenWrapperPropertyFileInputStream = null;
+            try {
+                mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+                Properties mavenWrapperProperties = new Properties();
+                mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+                url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+            } catch (IOException e) {
+                System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+            } finally {
+                try {
+                    if(mavenWrapperPropertyFileInputStream != null) {
+                        mavenWrapperPropertyFileInputStream.close();
+                    }
+                } catch (IOException e) {
+                    // Ignore ...
+                }
+            }
+        }
+        System.out.println("- Downloading from: " + url);
+
+        File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+        if(!outputFile.getParentFile().exists()) {
+            if(!outputFile.getParentFile().mkdirs()) {
+                System.out.println(
+                        "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+            }
+        }
+        System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+        try {
+            downloadFileFromURL(url, outputFile);
+            System.out.println("Done");
+            System.exit(0);
+        } catch (Throwable e) {
+            System.out.println("- Error downloading");
+            e.printStackTrace();
+            System.exit(1);
+        }
+    }
+
+    private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+        if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+            String username = System.getenv("MVNW_USERNAME");
+            char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+            Authenticator.setDefault(new Authenticator() {
+                @Override
+                protected PasswordAuthentication getPasswordAuthentication() {
+                    return new PasswordAuthentication(username, password);
+                }
+            });
+        }
+        URL website = new URL(urlString);
+        ReadableByteChannel rbc;
+        rbc = Channels.newChannel(website.openStream());
+        FileOutputStream fos = new FileOutputStream(destination);
+        fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+        fos.close();
+        rbc.close();
+    }
+
+}

BIN
common/.mvn/wrapper/maven-wrapper.jar


+ 2 - 0
common/.mvn/wrapper/maven-wrapper.properties

@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar

+ 310 - 0
common/mvnw

@@ -0,0 +1,310 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# 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
+#
+#    https://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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+#   JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+#   M2_HOME - location of maven2's installed home dir
+#   MAVEN_OPTS - parameters passed to the Java VM when running Maven
+#     e.g. to debug Maven itself, use
+#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+  if [ -f /etc/mavenrc ] ; then
+    . /etc/mavenrc
+  fi
+
+  if [ -f "$HOME/.mavenrc" ] ; then
+    . "$HOME/.mavenrc"
+  fi
+
+fi
+
+# OS specific support.  $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+  CYGWIN*) cygwin=true ;;
+  MINGW*) mingw=true;;
+  Darwin*) darwin=true
+    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+    if [ -z "$JAVA_HOME" ]; then
+      if [ -x "/usr/libexec/java_home" ]; then
+        export JAVA_HOME="`/usr/libexec/java_home`"
+      else
+        export JAVA_HOME="/Library/Java/Home"
+      fi
+    fi
+    ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+  if [ -r /etc/gentoo-release ] ; then
+    JAVA_HOME=`java-config --jre-home`
+  fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+  ## resolve links - $0 may be a link to maven's home
+  PRG="$0"
+
+  # need this for relative symlinks
+  while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+      PRG="$link"
+    else
+      PRG="`dirname "$PRG"`/$link"
+    fi
+  done
+
+  saveddir=`pwd`
+
+  M2_HOME=`dirname "$PRG"`/..
+
+  # make it fully qualified
+  M2_HOME=`cd "$M2_HOME" && pwd`
+
+  cd "$saveddir"
+  # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --unix "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME="`(cd "$M2_HOME"; pwd)`"
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+  javaExecutable="`which javac`"
+  if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+    # readlink(1) is not available as standard on Solaris 10.
+    readLink=`which readlink`
+    if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+      if $darwin ; then
+        javaHome="`dirname \"$javaExecutable\"`"
+        javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+      else
+        javaExecutable="`readlink -f \"$javaExecutable\"`"
+      fi
+      javaHome="`dirname \"$javaExecutable\"`"
+      javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+      JAVA_HOME="$javaHome"
+      export JAVA_HOME
+    fi
+  fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+  if [ -n "$JAVA_HOME"  ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+      # IBM's JDK on AIX uses strange locations for the executables
+      JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+      JAVACMD="$JAVA_HOME/bin/java"
+    fi
+  else
+    JAVACMD="`which java`"
+  fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+  echo "Error: JAVA_HOME is not defined correctly." >&2
+  echo "  We cannot execute $JAVACMD" >&2
+  exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+  echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+  if [ -z "$1" ]
+  then
+    echo "Path not specified to find_maven_basedir"
+    return 1
+  fi
+
+  basedir="$1"
+  wdir="$1"
+  while [ "$wdir" != '/' ] ; do
+    if [ -d "$wdir"/.mvn ] ; then
+      basedir=$wdir
+      break
+    fi
+    # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+    if [ -d "${wdir}" ]; then
+      wdir=`cd "$wdir/.."; pwd`
+    fi
+    # end of workaround
+  done
+  echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+  if [ -f "$1" ]; then
+    echo "$(tr -s '\n' ' ' < "$1")"
+  fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+  exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Found .mvn/wrapper/maven-wrapper.jar"
+    fi
+else
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+    fi
+    if [ -n "$MVNW_REPOURL" ]; then
+      jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    else
+      jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    fi
+    while IFS="=" read key value; do
+      case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+      esac
+    done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Downloading from: $jarUrl"
+    fi
+    wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+    if $cygwin; then
+      wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+    fi
+
+    if command -v wget > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found wget ... using wget"
+        fi
+        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+            wget "$jarUrl" -O "$wrapperJarPath"
+        else
+            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+        fi
+    elif command -v curl > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found curl ... using curl"
+        fi
+        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+            curl -o "$wrapperJarPath" "$jarUrl" -f
+        else
+            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+        fi
+
+    else
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Falling back to using Java to download"
+        fi
+        javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+        # For Cygwin, switch paths to Windows format before running javac
+        if $cygwin; then
+          javaClass=`cygpath --path --windows "$javaClass"`
+        fi
+        if [ -e "$javaClass" ]; then
+            if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Compiling MavenWrapperDownloader.java ..."
+                fi
+                # Compiling the Java class
+                ("$JAVA_HOME/bin/javac" "$javaClass")
+            fi
+            if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                # Running the downloader
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Running MavenWrapperDownloader.java ..."
+                fi
+                ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+            fi
+        fi
+    fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+  echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --path --windows "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+  [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+    MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+  $MAVEN_OPTS \
+  -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+  "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

+ 182 - 0
common/mvnw.cmd

@@ -0,0 +1,182 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements.  See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership.  The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License.  You may obtain a copy of the License at
+@REM
+@REM    https://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied.  See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM     e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on"  echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+    IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Found %WRAPPER_JAR%
+    )
+) else (
+    if not "%MVNW_REPOURL%" == "" (
+        SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    )
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Couldn't find %WRAPPER_JAR%, downloading it ...
+        echo Downloading from: %DOWNLOAD_URL%
+    )
+
+    powershell -Command "&{"^
+		"$webclient = new-object System.Net.WebClient;"^
+		"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+		"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+		"}"^
+		"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+		"}"
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Finished downloading %WRAPPER_JAR%
+    )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%

+ 102 - 0
common/pom.xml

@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>common</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <packaging>jar</packaging>
+    <name>dev-cloud-common</name>
+    <description>Demo project for Spring Boot</description>
+
+    <parent>
+        <artifactId>parent</artifactId>
+        <groupId>seecoder.devcloud</groupId>
+        <version>0.0.1-SNAPSHOT</version>
+    </parent>
+
+    <properties>
+        <java.version>1.8</java.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <spring-boot.version>2.3.4.RELEASE</spring-boot.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>joda-time</groupId>
+            <artifactId>joda-time</artifactId>
+            <version>2.9.3</version>
+            <scope>compile</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.junit.vintage</groupId>
+                    <artifactId>junit-vintage-engine</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+    </dependencies>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>${spring-boot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                    <encoding>UTF-8</encoding>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.3.4.RELEASE</version>
+                <executions>
+                    <execution>
+                        <id>repackage</id>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 47 - 0
common/src/main/java/seecoder/devcloud/common/constant/Constants.java

@@ -0,0 +1,47 @@
+package seecoder.devcloud.common.constant;
+
+public abstract class Constants {
+
+    public static final String NAME_PATTERN = "^[0-9a-zA-Z-]{8,}$";
+
+    //标签域名前缀
+    private static final String LABEL_PREFIX = "demo.seec.nju.cn";
+    //课程前缀
+    public static final String TYPE_LABEL = LABEL_PREFIX + "/type";
+    //描述
+    public static final String DESCRIPTION_LABEL = LABEL_PREFIX + "/description";
+    //应用
+    public static final String APPLICATION_LABEL = LABEL_PREFIX + "/app";
+
+    public static final String DEPLOY_ID_LABEL = LABEL_PREFIX + "/deployid";
+    // 用户namespace的label
+    public static final String USERSPACE_LABEL = LABEL_PREFIX +"/userspace";
+    // pod的label
+    public static final String SELECTOR_LABEL = LABEL_PREFIX + "/app";
+    // 非对外service的label
+    public static final String INTERNAL_LABEL = LABEL_PREFIX + "/internal";
+    // 对外service的label
+    public static final String EXTERNAL_LABEL = LABEL_PREFIX + "/external";
+    // 持久卷访问模式
+    public static final String STORAGE_ACCESS_MODE = "ReadWriteMany";
+    // 持久卷存储类型
+    public static final String STORAGE_CLASS = "glusterfs";
+
+    public static final String RESOURCE_STORAGE = "storage";
+
+    public static final String STARTS_AT_LABEL = LABEL_PREFIX + "/start";
+
+    public static final String ENDS_AT_LABEL = LABEL_PREFIX + "/end";
+
+    public static final String PRETTY_FORMAT = "true";
+
+    //cascade清除策略
+    //Deployment必须使用Foreground,否则只会删除Replica Set 不会删除底下的Pod
+    //https://github.com/kubernetes/kubeadm/issues/149#issuecomment-284766613
+    public static final String FOREGROUND_PROPAGATION_POLICY = "Foreground";
+
+    public static final String METADATA_NAME_SELECTOR = "metadata.name=";
+
+    public static final String DB_SECRET_NAME = "db-secret";
+
+}

+ 15 - 0
common/src/main/java/seecoder/devcloud/common/exceptions/AccessDeniedException.java

@@ -0,0 +1,15 @@
+package seecoder.devcloud.common.exceptions;
+
+import org.springframework.http.HttpStatus;
+
+public class AccessDeniedException extends ServiceException {
+	private static final HttpStatus HTTP_STATUS = HttpStatus.FORBIDDEN;
+
+	public AccessDeniedException() {
+		this(HTTP_STATUS.getReasonPhrase());
+	}
+
+	public AccessDeniedException(String message) {
+		super(HTTP_STATUS.value(), message);
+	}
+}

+ 9 - 0
common/src/main/java/seecoder/devcloud/common/exceptions/AlreadyInExaminationException.java

@@ -0,0 +1,9 @@
+package seecoder.devcloud.common.exceptions;
+
+public class AlreadyInExaminationException extends ConflictException {
+	private static final String MESSAGE = "已经参加了所选考试!";
+
+	public AlreadyInExaminationException() {
+		super(MESSAGE);
+	}
+}

+ 22 - 0
common/src/main/java/seecoder/devcloud/common/exceptions/Asserts.java

@@ -0,0 +1,22 @@
+package seecoder.devcloud.common.exceptions;
+
+public abstract class Asserts {
+	public static void notNull(Object object) throws EntityNotFoundException {
+		if (object == null) {
+			throw new EntityNotFoundException();
+		}
+	}
+
+	public static void notNull(Object object, String message) throws EntityNotFoundException {
+		if (object == null) {
+			throw new EntityNotFoundException(message);
+		}
+	}
+
+	public static void isNull(Object object, String message) throws EntityConflictException {
+		if (object != null) {
+			throw new EntityConflictException(message);
+		}
+	}
+
+}

Неке датотеке нису приказане због велике количине промена