Przeglądaj źródła

fix: 细节修正

wanghongkai 5 lat temu
rodzic
commit
0eae451272
26 zmienionych plików z 824 dodań i 232 usunięć
  1. 1 0
      Dockerfile
  2. 0 0
      docker-config.json
  3. 5 1
      pom.xml
  4. 4 0
      src/main/java/cn/seecoder/paas/SeecoderPaasApplication.java
  5. 7 0
      src/main/java/cn/seecoder/paas/data/dao/EnvironmentDAO.java
  6. 21 0
      src/main/java/cn/seecoder/paas/data/entity/Environment.java
  7. 0 1
      src/main/java/cn/seecoder/paas/service/facade/docker/impl/DockerApiImpl.java
  8. 49 68
      src/main/java/cn/seecoder/paas/service/facade/k8s/impl/IngressApiImpl.java
  9. 2 2
      src/main/java/cn/seecoder/paas/service/facade/k8s/impl/SecretApiImpl.java
  10. 4 0
      src/main/java/cn/seecoder/paas/service/facade/k8s/model/Container.java
  11. 3 3
      src/main/java/cn/seecoder/paas/service/facade/k8s/model/Deployment.java
  12. 205 135
      src/main/java/cn/seecoder/paas/service/impl/EnvironmentServiceImpl.java
  13. 25 1
      src/main/java/cn/seecoder/paas/service/model/vo/ConfigVO.java
  14. 12 0
      src/main/java/cn/seecoder/paas/service/model/vo/EnvironmentVO.java
  15. 92 0
      src/main/java/cn/seecoder/paas/service/scheduler/DeployScheduler.java
  16. 91 0
      src/main/java/cn/seecoder/paas/util/JwtTokenUtil.java
  17. 0 19
      src/main/java/cn/seecoder/paas/util/K8sApiConfiguration.java
  18. 28 0
      src/main/java/cn/seecoder/paas/util/enums/DeployStatus.java
  19. 35 1
      src/main/java/cn/seecoder/paas/web/WebSecurityConfig.java
  20. 29 0
      src/main/java/cn/seecoder/paas/web/security/AuthController.java
  21. 39 0
      src/main/java/cn/seecoder/paas/web/security/JwtAuthService.java
  22. 54 0
      src/main/java/cn/seecoder/paas/web/security/JwtAuthenticationTokenFilter.java
  23. 18 0
      src/main/java/cn/seecoder/paas/web/security/Sha256HexPasswordEncoder.java
  24. 66 0
      src/main/java/cn/seecoder/paas/web/security/User.java
  25. 30 0
      src/main/java/cn/seecoder/paas/web/security/UserDetailServiceImpl.java
  26. 4 1
      src/main/resources/application.yml

+ 1 - 0
Dockerfile

@@ -8,6 +8,7 @@ RUN cd $WORK_PATH && mvn clean package -DskipTests -DfinalName=$PROJECT_NAME
 FROM java:8-jre-alpine
 ENV PROJECT_NAME seecoder-paas
 ENV WORK_PATH /opt/$PROJECT_NAME
+RUN apk add --no-cache git
 WORKDIR /app
 COPY --from=compile_stage $WORK_PATH/target/${PROJECT_NAME}.jar .
 CMD ["sh", "-c", "java -Xdebug -Xrunjdwp:transport=dt_socket,address=8888,server=y,suspend=n -jar -server /app/${PROJECT_NAME}.jar"]

+ 0 - 0
src/main/resources/docker-config.json → docker-config.json


+ 5 - 1
pom.xml

@@ -96,7 +96,11 @@
             <artifactId>org.eclipse.jgit</artifactId>
             <version>5.9.0.202009080501-r</version>
         </dependency>
-
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt</artifactId>
+            <version>0.9.0</version>
+        </dependency>
     </dependencies>
 
     <build>

+ 4 - 0
src/main/java/cn/seecoder/paas/SeecoderPaasApplication.java

@@ -2,10 +2,14 @@ package cn.seecoder.paas;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
 import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.scheduling.annotation.EnableScheduling;
 
 @SpringBootApplication
 @EnableJpaRepositories(basePackages = "cn.seecoder")
+@EnableJpaAuditing
+@EnableScheduling
 public class SeecoderPaasApplication {
 
     public static void main(String[] args) {

+ 7 - 0
src/main/java/cn/seecoder/paas/data/dao/EnvironmentDAO.java

@@ -1,9 +1,12 @@
 package cn.seecoder.paas.data.dao;
 
 import cn.seecoder.paas.data.entity.Environment;
+import cn.seecoder.paas.util.enums.BuildStatus;
+import cn.seecoder.paas.util.enums.DeployStatus;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.stereotype.Repository;
 
+import java.time.LocalDateTime;
 import java.util.List;
 
 @Repository
@@ -11,4 +14,8 @@ public interface EnvironmentDAO extends JpaRepository<Environment, Integer> {
 
     List<Environment> findAllByAppId(Integer appId);
 
+    List<Environment> findAllByBuildStatusAndBuildStartTimeBefore(BuildStatus buildStatus, LocalDateTime startTime);
+
+    List<Environment> findAllByDeployStatus(DeployStatus deployStatus);
+
 }

+ 21 - 0
src/main/java/cn/seecoder/paas/data/entity/Environment.java

@@ -2,6 +2,7 @@ package cn.seecoder.paas.data.entity;
 
 import cn.seecoder.paas.util.enums.BuildStatus;
 import cn.seecoder.paas.util.enums.BuildType;
+import cn.seecoder.paas.util.enums.DeployStatus;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
@@ -46,6 +47,26 @@ public class Environment {
     @Column(name = "build_output", columnDefinition = "MEDIUMTEXT")
     private String buildOutput;
 
+    @Basic
+    @Column(name = "build_start_time")
+    private LocalDateTime buildStartTime;
+
+    @Basic
+    @Column(name = "image")
+    private String image;
+
+    @Basic
+    @Column(name = "deploy_status")
+    private DeployStatus deployStatus = DeployStatus.NOT_STARTED;
+
+    @Basic
+    @Column(name = "deploy_start_time")
+    private LocalDateTime deployStartTime;
+
+    @Basic
+    @Column(name = "deploy_output", columnDefinition = "TEXT")
+    private String deployOutput;
+
     @CreatedDate
     @Column(name = "created_at")
     private LocalDateTime createdAt;

+ 0 - 1
src/main/java/cn/seecoder/paas/service/facade/docker/impl/DockerApiImpl.java

@@ -49,7 +49,6 @@ public class DockerApiImpl implements DockerApi {
             throw new IOException("Failed to build docker image under [" + directory + "]");
         }
         client.push(registry + "/" + imageName + ":" + imageTag, new LogActionProgressHandler(), registryAuthSupplier.authFor(registry + "/" + imageName + ":" + imageTag));
-
     }
 
     @Override

+ 49 - 68
src/main/java/cn/seecoder/paas/service/facade/k8s/impl/IngressApiImpl.java

@@ -1,6 +1,7 @@
 package cn.seecoder.paas.service.facade.k8s.impl;
 
 import cn.seecoder.paas.service.facade.k8s.model.K8sObjectRequest;
+import cn.seecoder.paas.service.facade.k8s.model.LabelSelector;
 import com.google.gson.JsonSyntaxException;
 import cn.seecoder.paas.service.facade.k8s.IngressApi;
 import cn.seecoder.paas.service.facade.k8s.exception.K8sApiException;
@@ -13,6 +14,7 @@ import org.slf4j.Logger;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -107,27 +109,22 @@ public class IngressApiImpl implements IngressApi {
 
     @Override
     public void delete(Ingress ingress) {
-
-    }
-
-    @Override
-    public List<Ingress> getByCondition(K8sObjectRequest request) {
-        return null;
-    }
-
-
-    /**
-     * 删除namespace命名空间下的ingress
-     * @param namespace
-     * @param name
-     * @throws K8sApiException NOT_FOUND 命名空间/ingress不存在
-     * @throws K8sApiException SYSTEM_ERROR 系统异常
-     */
-    public void delete(String namespace, String name) {
         try {
-            deleteIngress(namespace, name);
+            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={}", name, namespace, e.getResponseBody());
+            LoggerUtil.error(logger, e, "ingress 删除失败,name={}, namespace={}, response={}", ingress.getName(), ingress.getNamespace(), e.getResponseBody());
             if (e.getCode() == NOT_FOUND) {
                 throw new K8sApiException(NOT_FOUND, "ingress不存在");
             }else {
@@ -136,73 +133,57 @@ public class IngressApiImpl implements IngressApi {
         }catch (JsonSyntaxException e) {
             //do nothing
         }catch (Exception e) {
-            LoggerUtil.error(logger, e, "ingress 删除异常,name={}, namespace={}", name, namespace);
+            LoggerUtil.error(logger, e, "ingress 删除异常,name={}, namespace={}", ingress.getName(), ingress.getNamespace());
             throw e;
         }
     }
 
-    public List<Ingress> getAll(String namespace) {
+    @Override
+    public List<Ingress> getByCondition(K8sObjectRequest request) {
         try {
-            ExtensionsV1beta1IngressList ingresses = extensionsV1beta1Api.listNamespacedIngress(
-                    namespace,
+            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,
-                    null,
+                    selector.toJsonString(),
                     null,
                     null,
                     null,
                     null);
-            return ingresses.getItems().stream().map(Ingress::new).collect(Collectors.toList());
-        } catch (ApiException e) {
-            LoggerUtil.error(logger, e, "查询namespace下所有ingress异常, namespace={}, response={}",
-                    namespace, e.getResponseBody());
-            throw K8s_SYSTEM_ERROR_EXCEPTION;
-        } catch (Exception e) {
-            LoggerUtil.error(logger, e, "查询namespace下所有ingress异常, namespace={}",
-                    namespace);
-            throw K8s_SYSTEM_ERROR_EXCEPTION;
-        }
-    }
-
-    /**
-     * 查找位于namespace下名称为name的Ingress
-     * @param namespace
-     * @param name
-     * @throws K8sApiException SYSTEM_ERROR 系统异常
-     * @return 不存在返回null
-     */
-    public Ingress get(String namespace, String name) {
-        try {
-            ExtensionsV1beta1Ingress ExtensionsV1beta1Ingress = extensionsV1beta1Api.readNamespacedIngress(name, namespace, PRETTY_FORMAT, false, false);
-            return ExtensionsV1beta1Ingress == null ? null : new Ingress(ExtensionsV1beta1Ingress);
+            return objList.getItems().stream().map(Ingress::new).collect(Collectors.toList());
         } catch (ApiException e) {
-            if (e.getCode() != NOT_FOUND) {
-                LoggerUtil.error(logger, e, "ingress 查找失败,name={}, namespace={}, response={}", name, namespace, e.getResponseBody());
+            LoggerUtil.error(logger, e, "查找ingresses失败,namespace = {}, response={}",
+                    request.getNamespace(), e.getResponseBody());
+            if (e.getCode() == NOT_FOUND) {
+                throw new K8sApiException(NOT_FOUND);
+            }else {
                 throw K8s_SYSTEM_ERROR_EXCEPTION;
             }
         } catch (Exception e) {
-            LoggerUtil.error(logger, e, "ingress 查找失败,name={}, namespace={}", name, namespace);
+            LoggerUtil.error(logger, e, "查找ingresses失败,namespace = {}, name={}",
+                    request.getNamespace());
             throw K8s_SYSTEM_ERROR_EXCEPTION;
         }
-        return null;
-    }
-
-
-    private void deleteIngress(String namespace, String name) throws ApiException{
-        V1DeleteOptions v1DeleteOptions = new V1DeleteOptionsBuilder()
-                .withApiVersion(Ingress.API_VERSION)
-                .withPropagationPolicy(FOREGROUND_PROPAGATION_POLICY)
-                .build();
-        extensionsV1beta1Api.deleteNamespacedIngress(
-                name,
-                namespace,
-                PRETTY_FORMAT,
-                null,
-                null,
-                null,
-                FOREGROUND_PROPAGATION_POLICY,
-                v1DeleteOptions);
     }
 }

+ 2 - 2
src/main/java/cn/seecoder/paas/service/facade/k8s/impl/SecretApiImpl.java

@@ -98,7 +98,7 @@ public class SecretApiImpl implements SecretApi {
                     null,
                     "",
                     null,
-                    "",
+                    null,
                     200,
                     "",
                     10,
@@ -128,7 +128,7 @@ public class SecretApiImpl implements SecretApi {
                 .stream()
                 .filter(secretVO -> StringUtils.equals(secretVO.getName(), name))
                 .findAny()
-                .get();
+                .orElse(null);
     }
 
     @Override

+ 4 - 0
src/main/java/cn/seecoder/paas/service/facade/k8s/model/Container.java

@@ -49,12 +49,15 @@ public class Container implements K8sObjectTransformer<V1Container> {
 
         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
@@ -62,6 +65,7 @@ public class Container implements K8sObjectTransformer<V1Container> {
             return new V1VolumeMountBuilder()
                     .withName(name)
                     .withMountPath(mountPath)
+                    .withSubPath(subPath)
                     .withReadOnly(readOnly)
                     .build();
         }

+ 3 - 3
src/main/java/cn/seecoder/paas/service/facade/k8s/model/Deployment.java

@@ -26,11 +26,11 @@ public class Deployment extends K8sAbstractObject<V1Deployment> {
 
     private static final String KIND = "Deployment";
 
-    private static final int DEFAULT_PROGRESS_DEADLINE_SECONDS = 600;
+    public static final int DEFAULT_PROGRESS_DEADLINE_SECONDS = 600;
 
-    private static final int DEFAULT_REVISION_HISTORY_LIMIT = 0;
+    public static final int DEFAULT_REVISION_HISTORY_LIMIT = 0;
 
-    private static final String DEFAULT_UPDATE_STRATEGY = "Recreate";
+    public static final String DEFAULT_UPDATE_STRATEGY = "Recreate";
 
     private List<Container> containers = new ArrayList<>();
 

+ 205 - 135
src/main/java/cn/seecoder/paas/service/impl/EnvironmentServiceImpl.java

@@ -20,10 +20,7 @@ import cn.seecoder.paas.util.ApplicationProperties;
 import cn.seecoder.paas.util.AsyncWrapper;
 import cn.seecoder.paas.util.LoggerUtil;
 import cn.seecoder.paas.util.ServiceException;
-import cn.seecoder.paas.util.enums.BuildStatus;
-import cn.seecoder.paas.util.enums.BuildType;
-import cn.seecoder.paas.util.enums.ConfigBelongsToType;
-import cn.seecoder.paas.util.enums.ResourceLabel;
+import cn.seecoder.paas.util.enums.*;
 import com.spotify.docker.client.ProgressHandler;
 import com.spotify.docker.client.exceptions.DockerException;
 import com.spotify.docker.client.messages.ProgressMessage;
@@ -39,6 +36,7 @@ import javax.transaction.Transactional;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.time.LocalDateTime;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -121,7 +119,7 @@ public class EnvironmentServiceImpl implements EnvironmentService {
             if (environment == null) {
                 throw ServiceException.BAD_REQUEST;
             }
-            BeanUtils.copyProperties(environmentVO, environment, "id", "appId", "buildStatus", "buildOutput");
+            BeanUtils.copyProperties(environmentVO, environment, "id", "appId", "buildStatus", "buildOutput", "deployStatus", "image", "buildStartTime", "deployStartTime");
         }
         EnvironmentVO result = EnvironmentConverter.convertToVO(environmentDAO.save(environment));
         ConfigVO resultConfigVO;
@@ -186,8 +184,8 @@ public class EnvironmentServiceImpl implements EnvironmentService {
         if (result == null) {
             throw ServiceException.BAD_REQUEST;
         }
-        if (result.getBuildStatus() == BuildStatus.BUILDING) {
-            throw new ServiceException("101", "存在正在进行的构建!");
+        if (result.getBuildStatus() == BuildStatus.BUILDING || result.getDeployStatus() == DeployStatus.DEPLOYING) {
+            throw new ServiceException("101", "存在正在进行的构建或部署!");
         }
         Config config = configDAO.findByConfigBelongsAndAndEntityId(ConfigBelongsToType.ENVIRONMENT, id);
         if (config == null) {
@@ -199,6 +197,8 @@ public class EnvironmentServiceImpl implements EnvironmentService {
         }
         result.setBuildOutput("");
         result.setBuildStatus(BuildStatus.BUILDING);
+        result.setBuildStartTime(LocalDateTime.now());
+        result.setDeployStatus(DeployStatus.NOT_STARTED);
         asyncWrapper.asyncInvoke(() -> buildAsync(id));
         return EnvironmentConverter.convertToVO(environmentDAO.save(result));
     }
@@ -223,151 +223,221 @@ public class EnvironmentServiceImpl implements EnvironmentService {
         return res;
     }
 
-    void buildAsync(Integer id) throws ServiceException {
+    void buildAsync(Integer id) {
         Environment result = environmentDAO.findById(id).get();
-        Config config = configDAO.findByConfigBelongsAndAndEntityId(ConfigBelongsToType.ENVIRONMENT, id);
-        Application application = applicationDAO.findById(result.getAppId()).get();
-        ConfigVO.ConfigContent configContent = ConfigConverter.convertToVO(config).getConfig();
-        String labelKey = ResourceLabel.RESOURCE.getCode();
-        String labelValue = ResourceLabel.RESOURCE.getGenerator().gen("environment", String.valueOf(result.getAppId()), String.valueOf(result.getId()));
-        Map<String, String> labelsMap = Collections.singletonMap(labelKey, labelValue);
-        String deploymentNamespace = applicationProperties.getDeploymentNamespace();
-        String deploymentHost = applicationProperties.getDeploymentHost();
-        String imageName = result.getBuildTypeValue();
-        // 1. 有git从git拿到地址进行构建并push
-        if (result.getBuildType() == BuildType.FROM_BRANCH_OR_COMMIT) {
-            try {
-                Path directory = Files.createTempDirectory("seecoder-paas-");
-                Git git = gitApi.clone(application.getGitUrl(), directory.toString());
-                if (Files.exists(Paths.get(directory.toString(), "Dockerfile"))) {
-                    git.reset().setMode(ResetCommand.ResetType.HARD).setRef(result.getBuildTypeValue()).call();
-                    String imageTag = String.valueOf(new Date().getTime());
-                    dockerApi.buildAndPush(directory.toString(), "seecoder-paas-" + labelValue, imageTag, new ProgressHandler() {
-                        @Override
-                        public void progress(ProgressMessage message) throws DockerException {
-                            result.setBuildOutput(message.stream());
-                            environmentDAO.save(result);
+        try {
+            Config config = configDAO.findByConfigBelongsAndAndEntityId(ConfigBelongsToType.ENVIRONMENT, id);
+            Application application = applicationDAO.findById(result.getAppId()).get();
+            ConfigVO.ConfigContent configContent = ConfigConverter.convertToVO(config).getConfig();
+            String labelKey = ResourceLabel.RESOURCE.getCode();
+            String labelValue = ResourceLabel.RESOURCE.getGenerator().gen("environment", String.valueOf(result.getAppId()), String.valueOf(result.getId()));
+            Map<String, String> labelsMap = Collections.singletonMap(labelKey, labelValue);
+            String deploymentNamespace = applicationProperties.getDeploymentNamespace();
+            String deploymentHost = applicationProperties.getDeploymentHost();
+            String imageName = result.getBuildTypeValue();
+            // 1. 有git从git拿到地址进行构建并push
+            if (result.getBuildType() == BuildType.FROM_BRANCH_OR_COMMIT) {
+                try {
+                    Path directory = Files.createTempDirectory("seecoder-paas-");
+                    Git git = gitApi.clone(application.getGitUrl(), directory.toString());
+                    if (Files.exists(Paths.get(directory.toString(), "Dockerfile"))) {
+                        git.reset().setMode(ResetCommand.ResetType.HARD).setRef(result.getBuildTypeValue()).call();
+                        String imageTag = String.valueOf(new Date().getTime());
+                        StringBuilder sb = new StringBuilder();
+                        final int DEFAULT_BUFFER_LENGTH = 200;
+                        final long MAX_BUFFER_SIZE = 1 << 18;
+                        dockerApi.buildAndPush(directory.toString(), "seecoder-paas-" + labelValue, imageTag, new ProgressHandler() {
+                            @Override
+                            public void progress(ProgressMessage message) throws DockerException {
+                                sb.append(message.stream());
+                                if (sb.length() > DEFAULT_BUFFER_LENGTH) {
+                                    result.setBuildOutput(result.getBuildOutput() + sb.toString());
+                                    if (result.getBuildOutput().length() > MAX_BUFFER_SIZE) {
+                                        result.setBuildOutput(result.getBuildOutput().substring((int) (result.getBuildOutput().length() - MAX_BUFFER_SIZE)));
+                                    }
+                                    sb.setLength(0);
+                                    environmentDAO.save(result);
+                                }
+                            }
+                        });
+                        if (sb.length() > 0) {
+                            result.setBuildOutput(result.getBuildOutput() + sb.toString());
                         }
-                    });
-                    imageName = applicationProperties.getDocker().getRegistry() + "/seecoder-paas-" + labelValue + ":" + imageTag;
-                    result.setBuildStatus(BuildStatus.SUCCESS);
-                    environmentDAO.save(result);
-                } else {
+                        imageName = applicationProperties.getK8s().getImageRegistry() + "/seecoder-paas-" + labelValue + ":" + imageTag;
+                        result.setBuildStatus(BuildStatus.SUCCESS);
+                        result.setDeployStatus(DeployStatus.DEPLOYING);
+                        result.setDeployStartTime(LocalDateTime.now());
+                        result.setImage(imageName);
+                        environmentDAO.save(result);
+                    } else {
+                        result.setBuildStatus(BuildStatus.FAIL);
+                        result.setBuildOutput("找不到构建文件Dockerfile!");
+                        environmentDAO.save(result);
+                        throw new ServiceException("102", "找不到构建文件Dockerfile!");
+                    }
+                } catch (Exception e) {
                     result.setBuildStatus(BuildStatus.FAIL);
-                    result.setBuildOutput("找不到构建文件Dockerfile!");
+                    result.setBuildOutput(e.getLocalizedMessage());
                     environmentDAO.save(result);
-                    throw new ServiceException("102", "找不到构建文件Dockerfile!");
+                    LoggerUtil.error(logger, e, "构建失败!gitUrl={}, branchOrCommit={}", application.getGitUrl(), result.getBuildTypeValue());
+                    return;
                 }
-            } catch (Exception e) {
-                result.setBuildStatus(BuildStatus.FAIL);
-                result.setBuildOutput(e.getLocalizedMessage());
-                environmentDAO.save(result);
-                LoggerUtil.error(logger, "构建失败!gitUrl={}, branchOrCommit={}", application.getGitUrl(), result.getBuildTypeValue());
             }
-        }
 
-        String[] ports = configContent.getServicePort().split(",");
-        if (configContent.getHostPrefix() != null) {
-            for (String port : ports) {
-                // ingress
-                List<Ingress> ingresses = ingressApi.getByCondition(K8sObjectRequest.builder()
-                        .name(labelValue + "-" + port)
-                        .namespace(deploymentNamespace).labels(labelsMap).build());
-                Ingress ingress = new Ingress();
-                ingress.setNamespace(deploymentNamespace);
-                ingress.setName(labelValue + "-" + port);
-                ingress.setLabels(labelsMap);
-                ingress.setHttpRules(Collections.singletonList(
-                        Ingress.IngressRule.builder().host(configContent.getHostPrefix() + "-" + port + "." + deploymentHost).ruleValues(
-                                Collections.singleton(Ingress.IngressPath.builder()
-                                        .path("/")
-                                        .serviceName(labelValue)
-                                        .port(Integer.parseInt(port))
-                                        .build())
-                        ).build()
-                ));
-                if (CollectionUtils.isEmpty(ingresses) || ingresses.size() != 1) {
-                    ingressApi.create(ingress);
-                } else {
-                    ingressApi.update(ingress);
+            String[] ports = configContent.getServicePort().split(",");
+            if (configContent.getHostPrefix() != null) {
+                for (String port : ports) {
+                    // ingress
+                    List<Ingress> ingresses = ingressApi.getByCondition(K8sObjectRequest.builder()
+                            .name(labelValue + "-" + port)
+                            .namespace(deploymentNamespace).labels(labelsMap).build());
+                    Ingress ingress = new Ingress();
+                    ingress.setNamespace(deploymentNamespace);
+                    ingress.setName(labelValue + "-" + port);
+                    ingress.setLabels(labelsMap);
+                    ingress.setHttpRules(Collections.singletonList(
+                            Ingress.IngressRule.builder().host(configContent.getHostPrefix() + "-" + port + "." + deploymentHost).ruleValues(
+                                    Collections.singleton(Ingress.IngressPath.builder()
+                                            .path("/")
+                                            .serviceName(labelValue)
+                                            .port(Integer.parseInt(port))
+                                            .build())
+                            ).build()
+                    ));
+                    if (CollectionUtils.isEmpty(ingresses) || ingresses.size() != 1) {
+                        ingressApi.create(ingress);
+                    } else {
+                        ingressApi.update(ingress);
+                    }
                 }
             }
-        }
-        // service
-        List<Service> svcs = serviceApi.getByCondition(K8sObjectRequest.builder().namespace(deploymentNamespace).labels(labelsMap).build());
-        Service svc = Service.builder().name(labelValue).labels(labelsMap).namespace(deploymentNamespace).selectors(labelsMap).servicePorts(
-                Arrays.stream(ports).map(port ->
-                        ServicePort.builder()
-                                .name(labelValue + "-" + port)
-                                .port(Integer.parseInt(port))
-                                .targetPort(Integer.parseInt(port))
-                                .build()).collect(Collectors.toSet())).build();
-        if (CollectionUtils.isEmpty(svcs) || svcs.size() != 1) {
-            serviceApi.create(svc);
-        } else {
-            serviceApi.update(svc);
-        }
-        // 检查secret,没有则创建
-        SecretVO secretVO = secretApi.getSecretByName(deploymentNamespace, DEFAULT_IMAGE_PULL_SECRET_NAME);
-        if (secretVO == null) {
-            secretApi.createPrivateRegistrySecret(
+            // service
+            List<Service> svcs = serviceApi.getByCondition(K8sObjectRequest.builder().namespace(deploymentNamespace).labels(labelsMap).build());
+            Service svc = Service.builder().name(labelValue).labels(labelsMap).namespace(deploymentNamespace).selectors(labelsMap).servicePorts(
+                    Arrays.stream(ports).map(port ->
+                            ServicePort.builder()
+                                    .name(labelValue + "-" + port)
+                                    .port(Integer.parseInt(port))
+                                    .targetPort(Integer.parseInt(port))
+                                    .build()).collect(Collectors.toSet())).build();
+            if (CollectionUtils.isEmpty(svcs) || svcs.size() != 1) {
+                serviceApi.create(svc);
+            } else {
+                serviceApi.update(svc);
+            }
+            // 检查secret,没有则创建
+            SecretVO secretVO = secretApi.getSecretByName(deploymentNamespace, DEFAULT_IMAGE_PULL_SECRET_NAME);
+            if (secretVO == null) {
+                secretApi.createPrivateRegistrySecret(
                         deploymentNamespace,
                         DEFAULT_IMAGE_PULL_SECRET_NAME,
                         applicationProperties.getK8s().getImageRegistry(),
                         applicationProperties.getDocker().getRegistryUsername(),
                         applicationProperties.getDocker().getRegistryPassword()
-                    );
-        }
-        // configMap
-        List<ConfigMap> configMaps = configMapApi.getByCondition(K8sObjectRequest.builder().namespace(deploymentNamespace).labels(labelsMap).build());
-        ConfigMap configMap = ConfigMap.builder().data(configContent.getMounts()).build();
-        configMap.setName(labelValue);
-        configMap.setNamespace(deploymentNamespace);
-        configMap.setLabel(labelKey, labelValue);
-        if (CollectionUtils.isEmpty(configMaps) || configMaps.size() != 1) {
-            configMapApi.create(configMap);
-        } else {
-            configMapApi.update(configMap);
-        }
-        // deployment
-        List<Deployment> deployments = deploymentApi.getByCondition(K8sObjectRequest.builder().namespace(deploymentNamespace).labels(labelsMap).build());
-        Deployment deployment = Deployment.builder()
-                .imagePullSecrets(Collections.singletonList(DEFAULT_IMAGE_PULL_SECRET_NAME))
-                .replicas(configContent.getReplicas() == null ? 1 : configContent.getReplicas())
-                .volumes(Collections.singletonList(new V1VolumeBuilder().withName(labelValue).withConfigMap(
+                );
+            }
+            // configMap
+            List<ConfigMap> configMaps = configMapApi.getByCondition(K8sObjectRequest.builder().namespace(deploymentNamespace).labels(labelsMap).build());
+            ConfigMap configMap = ConfigMap.builder().data(configContent.getMountsConfigMaps()).build();
+            configMap.setName(labelValue);
+            configMap.setNamespace(deploymentNamespace);
+            configMap.setLabel(labelKey, labelValue);
+            if (CollectionUtils.isEmpty(configMaps) || configMaps.size() != 1) {
+                configMapApi.create(configMap);
+            } else {
+                configMapApi.update(configMap);
+            }
+            // deployment
+            List<Deployment> deployments = deploymentApi.getByCondition(K8sObjectRequest.builder().namespace(deploymentNamespace).labels(labelsMap).build());
+            List<V1Volume> volumes = new ArrayList<>();
+            List<Container.VolumeMount> volumeMounts = new ArrayList<>();
+            if (!CollectionUtils.isEmpty(configMap.getData())) {
+                volumes.add(new V1VolumeBuilder().withName(labelValue).withConfigMap(
                         new V1ConfigMapVolumeSourceBuilder().withName(labelValue).withItems(
                                 configMap.getData() == null ? null :
-                                        configMap.getData().keySet().stream().map(path -> new V1KeyToPathBuilder().withKey(path).withPath(path).build()).collect(Collectors.toList())
+                                        configMap.getData().keySet().stream()
+                                                .map(path -> {
+                                                    String relativePath = path.replaceAll(ConfigVO.ConfigContent.PATH_DELIMITER, "/");
+                                                    String[] arrs = relativePath.split("/");
+                                                    return new V1KeyToPathBuilder()
+                                                            .withKey(path)
+                                                            .withPath(
+                                                                    arrs[arrs.length - 1]
+                                                            )
+                                                            .build();
+                                                })
+                                                .collect(Collectors.toList())
                         ).build()
-                ).build()))
-                .containers(Collections.singletonList(
-                        Container.builder()
-                                .env(configContent.getEnvs())
-                                .ports(
-                                        Arrays.stream(ports).map(port ->
-                                                ContainerPort.builder()
-                                                        .name(labelValue + "-" + port)
-                                                        .port(Integer.parseInt(port))
-                                                        .protocol("TCP")
-                                                        .build()).collect(Collectors.toSet())
-                                        )
-                                .args(configContent.getRunArgs() == null ? null : Arrays.asList(configContent.getRunArgs().split(" ")))
-                                .command(configContent.getRunCommands() == null ? null : Arrays.asList(configContent.getRunCommands().split(" ")))
-                                .name(labelValue)
-                                .image(imageName)
-                                .volumeMounts(Collections.singletonList(Container.VolumeMount.builder()
-                                        .name(labelValue)
-                                        .mountPath("/")
-                                        .build()))
-                                .build()))
-                .build();
-        deployment.setName(labelValue);
-        deployment.setNamespace(deploymentNamespace);
-        deployment.setLabel(labelKey, labelValue);
-        if (CollectionUtils.isEmpty(deployments) || deployments.size() != 1) {
-            deploymentApi.create(deployment);
-        } else {
-            deploymentApi.update(deployment);
+                ).build());
+                volumeMounts.addAll(configMap.getData().keySet().stream()
+                        .map(path -> {
+                            String relativePath = path.replaceAll(ConfigVO.ConfigContent.PATH_DELIMITER, "/");
+                            String[] arrs = relativePath.split("/");
+                            return Container.VolumeMount.builder()
+                                    .name(labelValue)
+                                    .mountPath(relativePath)
+                                    .subPath(arrs[arrs.length - 1])
+                                    .build();
+                        })
+                        .collect(Collectors.toList()));
+            }
+            if (!CollectionUtils.isEmpty(configContent.getHostPath())) {
+                for (Map.Entry<String, String> entry : configContent.getHostPath().entrySet()) {
+                    String name = labelValue + "-" + entry.getKey().replaceAll("/", "-");
+                    volumes.add(new V1VolumeBuilder().withName(name).withHostPath(
+                            new V1HostPathVolumeSourceBuilder().withPath(entry.getKey()).build()
+                    ).build());
+                    volumeMounts.add(Container.VolumeMount.builder()
+                            .name(name)
+                            .mountPath(entry.getValue())
+                            .build());
+                }
+            }
+
+            Deployment deployment = Deployment.builder()
+                    .imagePullSecrets(Collections.singletonList(DEFAULT_IMAGE_PULL_SECRET_NAME))
+                    .timeout(Deployment.DEFAULT_PROGRESS_DEADLINE_SECONDS)
+                    .revisionHistoryLimit(Deployment.DEFAULT_REVISION_HISTORY_LIMIT)
+                    .replicas(configContent.getReplicas() == null ? 1 : configContent.getReplicas())
+                    .volumes(volumes)
+                    .containers(Collections.singletonList(
+                            Container.builder()
+                                    .env(configContent.getEnvs())
+                                    .ports(
+                                            Arrays.stream(ports).map(port ->
+                                                    ContainerPort.builder()
+                                                            .name(application.getId() + "a" + result.getId() + "e" + port)
+                                                            .port(Integer.parseInt(port))
+                                                            .protocol("TCP")
+                                                            .build()).collect(Collectors.toSet())
+                                    )
+                                    .args(configContent.getRunArgs() == null ? null : Arrays.asList(configContent.getRunArgs().split(" ")))
+                                    .command(configContent.getRunCommands() == null ? null : Arrays.asList(configContent.getRunCommands().split(" ")))
+                                    .name(labelValue)
+                                    .image(imageName)
+                                    .volumeMounts(volumeMounts)
+                                    .build()))
+                    .build();
+            deployment.setName(labelValue);
+            deployment.setNamespace(deploymentNamespace);
+            deployment.setLabel(labelKey, labelValue);
+            if (CollectionUtils.isEmpty(deployments) || deployments.size() != 1) {
+                deploymentApi.create(deployment);
+            } else {
+                deploymentApi.update(deployment);
+            }
+        } catch (Exception e) {
+            if (result.getBuildStatus() == BuildStatus.BUILDING) {
+                result.setBuildStatus(BuildStatus.FAIL);
+                result.setBuildOutput(e.getLocalizedMessage());
+            }
+            if (result.getDeployStatus() == DeployStatus.DEPLOYING) {
+                result.setDeployStatus(DeployStatus.FAIL);
+                result.setDeployOutput(e.getLocalizedMessage());
+            }
+            environmentDAO.save(result);
+            LoggerUtil.error(logger, e, "流程失败!");
+            return;
         }
     }
 }

+ 25 - 1
src/main/java/cn/seecoder/paas/service/model/vo/ConfigVO.java

@@ -8,6 +8,7 @@ import lombok.Data;
 import lombok.NoArgsConstructor;
 
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
 
 @Data
@@ -26,7 +27,7 @@ public class ConfigVO {
     private static final ConfigContent EMPTY_CONFIG_CONTENT;
 
     static {
-        EMPTY_CONFIG_CONTENT = new ConfigContent(null, Collections.emptyMap(), null, null, Collections.emptyMap(), null, null);
+        EMPTY_CONFIG_CONTENT = new ConfigContent(null, Collections.emptyMap(), null, null, Collections.emptyMap(), Collections.emptyMap(), null, null);
     }
 
     public static ConfigVO getEmptyConfigVO(ConfigBelongsToType configBelongs, Integer entityId) {
@@ -41,6 +42,10 @@ public class ConfigVO {
     @AllArgsConstructor
     @NoArgsConstructor
     public static class ConfigContent {
+
+        @JsonIgnore
+        public static final String PATH_DELIMITER = "-spdelimiter-";
+
         /**
          * 服务端口
          */
@@ -71,6 +76,13 @@ public class ConfigVO {
         @JsonProperty("mounts")
         private Map<String, String> mounts;
 
+        /**
+         * key:hostPath
+         * value:mountPath
+         */
+        @JsonProperty("hostPath")
+        private Map<String, String> hostPath;
+
         @JsonProperty("hostPrefix")
         private String hostPrefix;
 
@@ -81,6 +93,18 @@ public class ConfigVO {
         public boolean isValid() {
             return servicePort != null && hostPrefix != null;
         }
+
+        @JsonIgnore()
+        public Map<String, String> getMountsConfigMaps() {
+            if (this.mounts == null) {
+                return Collections.emptyMap();
+            }
+            Map<String, String> maps = new HashMap<>();
+            for (String key : this.mounts.keySet()) {
+                maps.put(key.replaceAll("/", PATH_DELIMITER), this.mounts.get(key));
+            }
+            return maps;
+        }
     }
 
 }

+ 12 - 0
src/main/java/cn/seecoder/paas/service/model/vo/EnvironmentVO.java

@@ -3,10 +3,12 @@ package cn.seecoder.paas.service.model.vo;
 import cn.seecoder.paas.service.facade.k8s.model.Pod;
 import cn.seecoder.paas.util.enums.BuildStatus;
 import cn.seecoder.paas.util.enums.BuildType;
+import cn.seecoder.paas.util.enums.DeployStatus;
 import lombok.Data;
 
 import javax.validation.constraints.NotBlank;
 import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
 import java.util.List;
 
 @Data
@@ -28,6 +30,16 @@ public class EnvironmentVO {
 
     private BuildStatus buildStatus;
 
+    private String image;
+
+    private DeployStatus deployStatus;
+
+    private LocalDateTime buildStartTime;
+
+    private LocalDateTime deployStartTime;
+
+    private String deployOutput;
+
     private String buildOutput;
 
     private ConfigVO config;

+ 92 - 0
src/main/java/cn/seecoder/paas/service/scheduler/DeployScheduler.java

@@ -0,0 +1,92 @@
+package cn.seecoder.paas.service.scheduler;
+
+import cn.seecoder.paas.data.dao.EnvironmentDAO;
+import cn.seecoder.paas.data.entity.Environment;
+import cn.seecoder.paas.service.facade.k8s.DeploymentApi;
+import cn.seecoder.paas.service.facade.k8s.model.Deployment;
+import cn.seecoder.paas.service.facade.k8s.model.K8sObjectRequest;
+import cn.seecoder.paas.util.ApplicationProperties;
+import cn.seecoder.paas.util.enums.BuildStatus;
+import cn.seecoder.paas.util.enums.DeployStatus;
+import cn.seecoder.paas.util.enums.ResourceLabel;
+import io.kubernetes.client.openapi.models.V1DeploymentCondition;
+import io.kubernetes.client.openapi.models.V1DeploymentStatus;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+@Component
+public class DeployScheduler {
+
+    private final EnvironmentDAO environmentDAO;
+
+    private final DeploymentApi deploymentApi;
+
+    private final ApplicationProperties applicationProperties;
+
+    @Autowired
+    public DeployScheduler(EnvironmentDAO environmentDAO, DeploymentApi deploymentApi, ApplicationProperties applicationProperties) {
+        this.environmentDAO = environmentDAO;
+        this.deploymentApi = deploymentApi;
+        this.applicationProperties = applicationProperties;
+    }
+
+    @Scheduled(fixedDelay = 30000)
+    public void checkDeployStatus() {
+        LocalDateTime startTime = LocalDateTime.now().minusMinutes(10);
+        List<Environment> environments = environmentDAO.findAllByBuildStatusAndBuildStartTimeBefore(BuildStatus.BUILDING, startTime);
+        if (!CollectionUtils.isEmpty(environments)) {
+            for (Environment environment : environments) {
+                environment.setBuildStatus(BuildStatus.FAIL);
+                environment.setBuildOutput("Timeout interrupt.");
+                environmentDAO.save(environment);
+            }
+        }
+        environments = environmentDAO.findAllByDeployStatus(DeployStatus.DEPLOYING);
+        if (!CollectionUtils.isEmpty(environments)) {
+            for (Environment environment : environments) {
+                if (environment.getDeployStartTime().isBefore(startTime)) {
+                    environment.setDeployStatus(DeployStatus.FAIL);
+                    environment.setBuildOutput("Timeout interrupt.");
+                    environmentDAO.save(environment);
+                } else {
+                    String labelValue = ResourceLabel.RESOURCE.getGenerator().gen("environment", String.valueOf(environment.getAppId()), String.valueOf(environment.getId()));
+                    List<Deployment> deployments = deploymentApi.getByCondition(K8sObjectRequest
+                            .builder()
+                            .name(labelValue)
+                            .namespace(applicationProperties.getDeploymentNamespace())
+                            .build());
+                    if (!CollectionUtils.isEmpty(deployments)) {
+                        V1DeploymentStatus status = deployments.get(0).getStatus();
+                        List<V1DeploymentCondition> conditions = status.getConditions();
+                        if (!CollectionUtils.isEmpty(conditions)) {
+                            Collections.sort(conditions, Comparator.comparingLong(c -> ((V1DeploymentCondition)c).getLastUpdateTime().getMillis()).reversed());
+                            V1DeploymentCondition condition = conditions.get(0);
+                            switch (condition.getType()) {
+                                case "Available":
+                                    environment.setDeployStatus(DeployStatus.SUCCESS);
+                                    environment.setDeployOutput(condition.getMessage());
+                                    break;
+                                case "Progressing":
+                                    environment.setDeployStatus(DeployStatus.DEPLOYING);
+                                    environment.setDeployOutput(condition.getMessage());
+                                    break;
+                                default:
+                                    environment.setDeployStatus(DeployStatus.FAIL);
+                                    environment.setDeployOutput(condition.getMessage());
+                                    break;
+                            }
+                            environmentDAO.save(environment);
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 91 - 0
src/main/java/cn/seecoder/paas/util/JwtTokenUtil.java

@@ -0,0 +1,91 @@
+package cn.seecoder.paas.util;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+@Data
+@ConfigurationProperties(prefix = "paas.jwt")
+@Component
+public class JwtTokenUtil {
+    private String secret;
+    private Long expiration;
+
+    public String generateToken(UserDetails userDetails) {
+        Map<String, Object> claims = new HashMap<>(2);
+        claims.put("sub", userDetails.getUsername());
+        claims.put("created", new Date());
+        return generateToken(claims);
+    }
+
+    public String getUsernameFromToken(String token) {
+        String username;
+        try {
+            Claims claims = getClaimsFromToken(token);
+            username = claims.getSubject();
+        } catch (Exception e) {
+            username = null;
+        }
+        return username;
+    }
+
+    public Boolean isTokenExpired(String token) {
+        try {
+            Claims claims = getClaimsFromToken(token);
+            Date expiration = claims.getExpiration();
+            return expiration.before(new Date());
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /**
+     * 刷新令牌
+     *
+     * @param token 原令牌
+     * @return 新令牌
+     */
+    public String refreshToken(String token) {
+        String refreshedToken;
+        try {
+            Claims claims = getClaimsFromToken(token);
+            claims.put("created", new Date());
+            refreshedToken = generateToken(claims);
+        } catch (Exception e) {
+            refreshedToken = null;
+        }
+        return refreshedToken;
+    }
+
+    public Boolean validateToken(String token, UserDetails userDetails) {
+        String username = getUsernameFromToken(token);
+        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
+    }
+
+    private String generateToken(Map<String, Object> claims) {
+        Date expirationDate = new Date(System.currentTimeMillis() + expiration);
+        return Jwts.builder().setClaims(claims)
+                .setExpiration(expirationDate)
+                .signWith(SignatureAlgorithm.HS512, secret)
+                .compact();
+    }
+
+    private Claims getClaimsFromToken(String token) {
+        Claims claims;
+        try {
+            claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
+        } catch (Exception e) {
+            claims = null;
+        }
+        return claims;
+    }
+
+}

+ 0 - 19
src/main/java/cn/seecoder/paas/util/K8sApiConfiguration.java

@@ -50,25 +50,6 @@ public class K8sApiConfiguration {
         this.watchApiClient = Config.fromToken(k8s.getApiServer(), k8s.getToken(), k8s.isValidateSSL());
         this.watchCoreV1Api = new CoreV1Api(watchApiClient);
         this.watchAppsV1Api = new AppsV1Api(watchApiClient);
-
-        try {
-            V1PodList v1PodList = coreV1Api.listPodForAllNamespaces(
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null,
-                    null
-                    );
-            for (V1Pod item : v1PodList.getItems()) {
-                System.out.println(item.getMetadata().getName());
-            }
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
     }
 
     @Bean

+ 28 - 0
src/main/java/cn/seecoder/paas/util/enums/DeployStatus.java

@@ -0,0 +1,28 @@
+package cn.seecoder.paas.util.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum DeployStatus {
+
+    NOT_STARTED("NOT_STARTED", "未开始"),
+    SUCCESS("SUCCESS", "成功"),
+    DEPLOYING("DEPLOYING", "正在部署"),
+    FAIL("FAIL", "失败"),
+    ;
+
+    private String code;
+
+    private String desc;
+
+    public static DeployStatus getByCode(String code) {
+        for (DeployStatus objs : DeployStatus.values()) {
+            if (objs.getCode().equals(code)) {
+                return objs;
+            }
+        }
+        return null;
+    }
+}

+ 35 - 1
src/main/java/cn/seecoder/paas/web/WebSecurityConfig.java

@@ -1,15 +1,49 @@
 package cn.seecoder.paas.web;
 
+import cn.seecoder.paas.web.security.JwtAuthenticationTokenFilter;
+import cn.seecoder.paas.web.security.User;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 
 @Configuration
 @EnableWebSecurity
 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+
+    private final JwtAuthenticationTokenFilter filter;
+
+    @Autowired
+    public WebSecurityConfig(JwtAuthenticationTokenFilter filter) {
+        super();
+        this.filter = filter;
+    }
+
     @Override
     protected void configure(HttpSecurity http) throws Exception {
-        http.csrf().disable();
+        http
+                .cors()
+                .and()
+                .sessionManagement()
+                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+                .and()
+                .addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
+                .csrf().disable()
+                .authorizeRequests()
+                .antMatchers("/api/auth/**").permitAll()
+                .antMatchers("/api/**").hasAuthority(User.ADMIN_AUTHORITY.getAuthority())
+        .and();
     }
+
+    @Bean
+    @Override
+    public AuthenticationManager authenticationManagerBean() throws Exception {
+        return super.authenticationManagerBean();
+    }
+
 }

+ 29 - 0
src/main/java/cn/seecoder/paas/web/security/AuthController.java

@@ -0,0 +1,29 @@
+package cn.seecoder.paas.web.security;
+
+import cn.seecoder.paas.web.model.Response;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/auth")
+public class AuthController {
+
+    private final JwtAuthService jwtAuthService;
+
+    @Autowired
+    public AuthController(JwtAuthService jwtAuthService) {
+        this.jwtAuthService = jwtAuthService;
+    }
+
+    @PostMapping("")
+    public Response login(@RequestBody Map<String, String> map) {
+        String username = map.get("username");
+        String password = map.get("password");
+        return Response.buildSuccess(jwtAuthService.login(username, password));
+    }
+}

+ 39 - 0
src/main/java/cn/seecoder/paas/web/security/JwtAuthService.java

@@ -0,0 +1,39 @@
+package cn.seecoder.paas.web.security;
+
+import cn.seecoder.paas.util.JwtTokenUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class JwtAuthService {
+
+    private final AuthenticationManager authenticationManager;
+
+    private final UserDetailsService userDetailsService;
+
+    private final JwtTokenUtil jwtTokenUtil;
+
+    @Autowired
+    public JwtAuthService(AuthenticationManager authenticationManager, @Qualifier("paasUserDetailService") UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil) {
+        this.authenticationManager = authenticationManager;
+        this.userDetailsService = userDetailsService;
+        this.jwtTokenUtil = jwtTokenUtil;
+    }
+
+    public String login(String username, String password) {
+        UsernamePasswordAuthenticationToken upToken =
+                new UsernamePasswordAuthenticationToken(username, password);
+        Authentication authentication = authenticationManager.authenticate(upToken);
+        SecurityContextHolder.getContext().setAuthentication(authentication);
+        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
+        return jwtTokenUtil.generateToken(userDetails);
+    }
+
+}

+ 54 - 0
src/main/java/cn/seecoder/paas/web/security/JwtAuthenticationTokenFilter.java

@@ -0,0 +1,54 @@
+package cn.seecoder.paas.web.security;
+
+import cn.seecoder.paas.util.JwtTokenUtil;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Component
+public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
+
+    private final UserDetailsService userDetailsService;
+
+    private final JwtTokenUtil jwtTokenUtil;
+
+    @Autowired
+    public JwtAuthenticationTokenFilter(JwtTokenUtil jwtTokenUtil, @Qualifier("paasUserDetailService") UserDetailsService userDetailsService) {
+        this.jwtTokenUtil = jwtTokenUtil;
+        this.userDetailsService = userDetailsService;
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
+        String authHeader = httpServletRequest.getHeader("Authorization");
+        if (StringUtils.isNotEmpty(authHeader)) {
+            String username = jwtTokenUtil.getUsernameFromToken(authHeader);
+            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
+                if (jwtTokenUtil.validateToken(authHeader, userDetails)) {
+                    //加载用户、角色、权限信息,Spring Security根据这些信息判断接口的访问权限
+                    UsernamePasswordAuthenticationToken authentication
+                            = new UsernamePasswordAuthenticationToken(userDetails, null,
+                            userDetails.getAuthorities());
+                    authentication.setDetails(new WebAuthenticationDetailsSource()
+                            .buildDetails(httpServletRequest));
+                    SecurityContextHolder.getContext().setAuthentication(authentication);
+                }
+            }
+        }
+        filterChain.doFilter(httpServletRequest, httpServletResponse);
+    }
+}

+ 18 - 0
src/main/java/cn/seecoder/paas/web/security/Sha256HexPasswordEncoder.java

@@ -0,0 +1,18 @@
+package cn.seecoder.paas.web.security;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Component;
+
+@Component
+public class Sha256HexPasswordEncoder implements PasswordEncoder {
+    @Override
+    public String encode(CharSequence rawPassword) {
+        return DigestUtils.sha256Hex(rawPassword != null ? rawPassword.toString() : StringUtils.EMPTY);
+    }
+
+    @Override
+    public boolean matches(CharSequence rawPassword, String encodedPassword) {
+        return StringUtils.equals(encode(rawPassword), encodedPassword);
+    }
+}

+ 66 - 0
src/main/java/cn/seecoder/paas/web/security/User.java

@@ -0,0 +1,66 @@
+package cn.seecoder.paas.web.security;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+public class User implements UserDetails {
+
+    private String username;
+
+    private String password;
+
+    private List<GrantedAuthority> authorities;
+
+
+    public static final GrantedAuthority ADMIN_AUTHORITY = new SimpleGrantedAuthority("PAAS_ADMIN");
+
+    public User(String username, String password, List<GrantedAuthority> authorities) {
+        this.username = username;
+        this.password = DigestUtils.sha256Hex(password);
+        this.authorities = authorities;
+    }
+
+    @Override
+    public Collection<? extends GrantedAuthority> getAuthorities() {
+        return authorities;
+    }
+
+    @Override
+    public String getPassword() {
+        return password;
+    }
+
+    @Override
+    public String getUsername() {
+        return username;
+    }
+
+    @Override
+    public boolean isAccountNonExpired() {
+        return true;
+    }
+
+    @Override
+    public boolean isAccountNonLocked() {
+        return true;
+    }
+
+    @Override
+    public boolean isCredentialsNonExpired() {
+        return true;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return true;
+    }
+}

+ 30 - 0
src/main/java/cn/seecoder/paas/web/security/UserDetailServiceImpl.java

@@ -0,0 +1,30 @@
+package cn.seecoder.paas.web.security;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.security.SecurityProperties;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+
+@Service("paasUserDetailService")
+public class UserDetailServiceImpl implements UserDetailsService {
+
+    private final SecurityProperties securityProperties;
+
+    @Autowired
+    public UserDetailServiceImpl(SecurityProperties securityProperties) {
+        this.securityProperties = securityProperties;
+    }
+
+    @Override
+    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
+        if (s.equals(securityProperties.getUser().getName())) {
+            return new User(s, securityProperties.getUser().getPassword(), Collections.singletonList(User.ADMIN_AUTHORITY));
+        } else {
+            throw new UsernameNotFoundException("未知用户");
+        }
+    }
+}

+ 4 - 1
src/main/resources/application.yml

@@ -18,11 +18,14 @@ spring:
       password: NJU67software
 
 paas:
+  jwt:
+    secret: seecoder-paas
+    expiration: 3600000
   git:
     username: WangHongKai
     password: 1!2@3#4$
   docker:
-    host: unix://var/run/docker.sock
+    host: unix:///var/run/docker.sock
     registry: nexus-api.seecii.cn:80
     registryUsername: admin
     registryPassword: admin123