claws 4 éve
szülő
commit
6dcf187553
21 módosított fájl, 658 hozzáadás és 50 törlés
  1. 6 0
      api/src/main/java/cn/seecoder/api/DevcloudApiAutoConfiguration.java
  2. 14 3
      api/src/main/java/cn/seecoder/api/docker/impl/DockerApiImpl.java
  3. 6 0
      api/src/main/java/cn/seecoder/api/k8s/JobApi.java
  4. 17 0
      api/src/main/java/cn/seecoder/api/k8s/K8sApiConfiguration.java
  5. 4 0
      api/src/main/java/cn/seecoder/api/k8s/K8sConstants.java
  6. 147 0
      api/src/main/java/cn/seecoder/api/k8s/impl/JobApiImpl.java
  7. 92 0
      api/src/main/java/cn/seecoder/api/k8s/model/Job.java
  8. 8 0
      web/src/main/java/cn/seecoder/web/controller/gitlab/CommitController.java
  9. 3 0
      web/src/main/java/cn/seecoder/web/core/pipeline/PipelineException.java
  10. 9 6
      web/src/main/java/cn/seecoder/web/core/pipeline/PipelineFactory.java
  11. 55 33
      web/src/main/java/cn/seecoder/web/core/pipeline/config/HandlerConfigTable.java
  12. 96 0
      web/src/main/java/cn/seecoder/web/core/pipeline/handler/K8sJobHandler.java
  13. 157 0
      web/src/main/java/cn/seecoder/web/core/pipeline/handler/SonarJavaImageBuildHandler.java
  14. 2 1
      web/src/main/java/cn/seecoder/web/core/pipeline/template/PipelineTemplateTable.java
  15. 1 0
      web/src/main/java/cn/seecoder/web/infrastructure/config/WebSecurityConfig.java
  16. 10 0
      web/src/main/java/cn/seecoder/web/service/impl/project/BranchServiceImpl.java
  17. 2 0
      web/src/main/java/cn/seecoder/web/service/project/BranchService.java
  18. 4 4
      web/src/main/resources/application-lzl.yml
  19. 9 3
      web/src/main/resources/sql/data_init.sql
  20. 10 0
      web/src/main/resources/template/config/SONAR_JAVA8.json
  21. 6 0
      web/src/main/resources/template/dockerfile/SONAR_JAVA8

+ 6 - 0
api/src/main/java/cn/seecoder/api/DevcloudApiAutoConfiguration.java

@@ -63,6 +63,12 @@ public class DevcloudApiAutoConfiguration {
         return new DeploymentApiImpl(k8sApiConfiguration().appsV1Api());
     }
 
+    @Bean
+    @ConditionalOnBean(K8sApiConfiguration.class)
+    public JobApi jobApi() {
+        return new JobApiImpl(k8sApiConfiguration().batchV1Api());
+    }
+
     @Bean
     @ConditionalOnBean(K8sApiConfiguration.class)
     public ExecApi execApi(){

+ 14 - 3
api/src/main/java/cn/seecoder/api/docker/impl/DockerApiImpl.java

@@ -4,11 +4,14 @@ package cn.seecoder.api.docker.impl;
 import cn.seecoder.api.ApplicationProperties;
 import com.spotify.docker.client.*;
 import com.spotify.docker.client.auth.ConfigFileRegistryAuthSupplier;
+import com.spotify.docker.client.auth.FixedRegistryAuthSupplier;
 import com.spotify.docker.client.auth.RegistryAuthSupplier;
 import com.spotify.docker.client.exceptions.DockerCertificateException;
 import com.spotify.docker.client.exceptions.DockerException;
 import com.spotify.docker.client.messages.Image;
 import com.spotify.docker.client.messages.ProgressMessage;
+import com.spotify.docker.client.messages.RegistryAuth;
+import com.spotify.docker.client.messages.RegistryConfigs;
 import lombok.extern.apachecommons.CommonsLog;
 import lombok.extern.slf4j.Slf4j;
 
@@ -22,6 +25,7 @@ import java.io.IOException;
 import java.net.URI;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 @Component
@@ -49,9 +53,15 @@ public class DockerApiImpl implements DockerApi {
         }
 
         defaultHandler = new NoActionProgressHandler();
+
+        String registryName = properties.getDocker().getRegistry();
+        String registryUsername = properties.getDocker().getRegistryUsername();
+        String registryPassword = properties.getDocker().getRegistryPassword();
+        RegistryAuth registryAuth = RegistryAuth.builder().serverAddress(registryName).username(registryUsername).password(registryPassword).build();
+        registryAuthSupplier = new FixedRegistryAuthSupplier(registryAuth, RegistryConfigs.create(Collections.singletonMap(registryName, registryAuth)));
         //这里同上,配置文件放在resource的写法会导致打包成jar后无法正常读取配置文件
-        registryAuthSupplier = new ConfigFileRegistryAuthSupplier(new DockerConfigReader(),Paths.get(applicationHome.getDir().getAbsolutePath(), "docker-config.json"));
-        log.info("已读取 "+ applicationHome.getDir().getAbsolutePath()+ " 的docker-config.json文件");
+//        registryAuthSupplier = new ConfigFileRegistryAuthSupplier(new DockerConfigReader(),Paths.get(applicationHome.getDir().getAbsolutePath(), "docker-config.json"));
+//        log.info("已读取 "+ applicationHome.getDir().getAbsolutePath()+ " 的docker-config.json文件");
     }
 
     @Override
@@ -60,7 +70,8 @@ public class DockerApiImpl implements DockerApi {
         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));
+        RegistryAuth registryAuth = registryAuthSupplier.authFor(registry + "/" + imageName + ":" + imageTag);
+        client.push(registry + "/" + imageName + ":" + imageTag, new LogActionProgressHandler(), registryAuth);
     }
 
     @Override

+ 6 - 0
api/src/main/java/cn/seecoder/api/k8s/JobApi.java

@@ -0,0 +1,6 @@
+package cn.seecoder.api.k8s;
+
+import cn.seecoder.api.k8s.model.Job;
+
+public interface JobApi extends AbstractApi<Job>{
+}

+ 17 - 0
api/src/main/java/cn/seecoder/api/k8s/K8sApiConfiguration.java

@@ -4,6 +4,7 @@ import cn.seecoder.api.ApplicationProperties;
 import io.kubernetes.client.Exec;
 import io.kubernetes.client.openapi.ApiClient;
 import io.kubernetes.client.openapi.apis.AppsV1Api;
+import io.kubernetes.client.openapi.apis.BatchV1Api;
 import io.kubernetes.client.openapi.apis.CoreV1Api;
 import io.kubernetes.client.openapi.apis.ExtensionsV1beta1Api;
 import io.kubernetes.client.util.Config;
@@ -20,6 +21,8 @@ public class K8sApiConfiguration {
 
     private final AppsV1Api appsV1Api;
 
+    private final BatchV1Api batchV1Api;
+
     private final ExtensionsV1beta1Api extensionsV1beta1Api;
 
     private final Exec exec;
@@ -33,6 +36,8 @@ public class K8sApiConfiguration {
 
     private final AppsV1Api watchAppsV1Api;
 
+    private final BatchV1Api watchBatchV1Api;
+
     public K8sApiConfiguration(ApplicationProperties applicationProperties) {
         ApplicationProperties.K8s k8s = applicationProperties.getK8s();
         this.apiClient = Config.fromToken(k8s.getApiServer(), k8s.getToken(), k8s.getValidateSSL());
@@ -40,6 +45,7 @@ public class K8sApiConfiguration {
         io.kubernetes.client.openapi.Configuration.setDefaultApiClient(this.apiClient);
         this.coreV1Api = new CoreV1Api();
         this.appsV1Api = new AppsV1Api();
+        this.batchV1Api = new BatchV1Api();
         this.extensionsV1beta1Api = new ExtensionsV1beta1Api();
         this.exec = new Exec();
 
@@ -47,6 +53,7 @@ public class K8sApiConfiguration {
         this.watchApiClient = Config.fromToken(k8s.getApiServer(), k8s.getToken(), k8s.getValidateSSL());
         this.watchCoreV1Api = new CoreV1Api(watchApiClient);
         this.watchAppsV1Api = new AppsV1Api(watchApiClient);
+        this.watchBatchV1Api = new BatchV1Api(watchApiClient);
     }
 
     @Bean
@@ -64,6 +71,11 @@ public class K8sApiConfiguration {
         return appsV1Api;
     }
 
+    @Bean
+    public BatchV1Api batchV1Api() {
+        return batchV1Api;
+    }
+
     @Bean
     public ExtensionsV1beta1Api extensionsV1beta1Api() {
         return extensionsV1beta1Api;
@@ -89,4 +101,9 @@ public class K8sApiConfiguration {
     public AppsV1Api watchAppsV1Api() {
         return watchAppsV1Api;
     }
+
+    @Bean
+    public BatchV1Api watchBatchV1Api() {
+        return watchBatchV1Api;
+    }
 }

+ 4 - 0
api/src/main/java/cn/seecoder/api/k8s/K8sConstants.java

@@ -13,6 +13,8 @@ public class K8sConstants {
     //应用
     public static final String APPLICATION_LABEL = LABEL_PREFIX + "/app";
 
+    public static final String JOB_LABEL = LABEL_PREFIX + "/job";
+
     public static final String DEPLOY_ID_LABEL = LABEL_PREFIX + "/deployid";
     // 用户namespace的label
     public static final String USERSPACE_LABEL = LABEL_PREFIX +"/userspace";
@@ -51,6 +53,8 @@ public class K8sConstants {
 
     public final static String DEPLOYMENT_SUFFIX = "-deployment";
 
+    public final static String JOB_SUFFIX = "-job";
+
     public final static String SERVICE_SUFFIX = "-service";
 
     public final static String EXTERNAL_SERVICE_SUFFIX = "-external-service";

+ 147 - 0
api/src/main/java/cn/seecoder/api/k8s/impl/JobApiImpl.java

@@ -0,0 +1,147 @@
+package cn.seecoder.api.k8s.impl;
+
+import cn.seecoder.api.k8s.JobApi;
+import cn.seecoder.api.k8s.exception.K8sApiException;
+import cn.seecoder.api.k8s.model.Job;
+import cn.seecoder.api.k8s.model.K8sObjectRequest;
+
+import cn.seecoder.api.k8s.model.LabelSelector;
+import cn.seecoder.common.util.LoggerUtil;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.BatchV1Api;
+import io.kubernetes.client.openapi.models.V1DeleteOptions;
+import io.kubernetes.client.openapi.models.V1DeleteOptionsBuilder;
+import io.kubernetes.client.openapi.models.V1Job;
+import io.kubernetes.client.openapi.models.V1JobList;
+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;
+
+import static cn.seecoder.api.k8s.K8sConstants.FOREGROUND_PROPAGATION_POLICY;
+import static cn.seecoder.api.k8s.K8sConstants.PRETTY_FORMAT;
+import static cn.seecoder.api.k8s.exception.K8sApiException.*;
+
+@Service
+public class JobApiImpl implements JobApi {
+
+    private static final Logger logger = LoggerUtil.getLogger(JobApi.class);
+
+    private BatchV1Api batchV1Api;
+
+    @Autowired
+    public JobApiImpl(BatchV1Api batchV1Api){
+        this.batchV1Api = batchV1Api;
+    }
+
+    @Override
+    public Job create(Job job) {
+        try {
+            V1Job v1Job = job.toK8sObject();
+            V1Job resultJob = batchV1Api.createNamespacedJob(job.getNamespace(), v1Job, PRETTY_FORMAT, null, null);
+            return resultJob == null ? null : new Job(resultJob);
+        } catch (ApiException e){
+            LoggerUtil.error(logger, e, "job创建失败, job={}, response={}", job, e.getResponseBody());
+            if (e.getCode() == NOT_FOUND) {
+                throw new K8sApiException(NOT_FOUND, "namespace不存在");
+            } else if (e.getCode() == ALREADY_EXIST) {
+                throw new K8sApiException(ALREADY_EXIST, "job已经存在");
+            } else {
+                throw K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (Exception e){
+            LoggerUtil.error(logger, e, "job创建异常, job={}", job);
+            throw e;
+        }
+    }
+
+    @Override
+    public Job update(Job job) {
+        throw K8s_NOT_SUPPORT_METHOD;
+    }
+
+    @Override
+    public void delete(Job job) {
+        try {
+            V1DeleteOptions v1DeleteOptions = new V1DeleteOptionsBuilder()
+                    .withApiVersion(Job.API_VERSION)
+                    .withPropagationPolicy(FOREGROUND_PROPAGATION_POLICY)
+                    .build();
+            batchV1Api.deleteNamespacedJob(
+                    job.getName(),
+                    job.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    FOREGROUND_PROPAGATION_POLICY,
+                    v1DeleteOptions
+            );
+        } catch (ApiException e){
+            LoggerUtil.error(logger, e, "删除Job失败,namespace={}, name={}, response={}", job.getNamespace(), job.getName(), e.getResponseBody());
+
+            if (e.getCode() == NOT_FOUND) {
+                throw new K8sApiException(NOT_FOUND, "namespace不存在");
+            }else {
+                throw K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (Exception e){
+            LoggerUtil.error(logger, e, "删除Job失败,namespace={}, name={}, response={}", job.getNamespace());
+            throw e;
+        }
+    }
+
+    @Override
+    public List<Job> getByCondition(K8sObjectRequest request) {
+        try {
+            if (request.getName() != null) {
+                V1Job v1Job = batchV1Api.readNamespacedJob(
+                        request.getName(),
+                        request.getNamespace(),
+                        PRETTY_FORMAT,
+                        null,
+                        null
+                );
+                return v1Job == null ? Collections.emptyList() : Collections.singletonList(new Job(v1Job));
+            }
+            LabelSelector selector = null;
+            if (request.getSelector() != null) {
+                selector = request.getSelector();
+            }
+            if (request.getLabels() != null) {
+                if (selector == null) {
+                    selector = new LabelSelector();
+                }
+                selector.addAll(request.getLabels());
+            }
+            V1JobList jobList = batchV1Api.listNamespacedJob(
+                    request.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    selector.toJsonString(),
+                    null,
+                    null,
+                    null,
+                    null
+            );
+            return jobList.getItems().stream().map(Job::new).collect(Collectors.toList());
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "查找job失败,namespace = {}, response={}",
+                    request.getNamespace(), e.getResponseBody());
+            if (e.getCode() == NOT_FOUND) {
+                return Collections.emptyList();
+            }else {
+                return Collections.emptyList();
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "查找job失败,namespace = {}, name={}",
+                    request.getNamespace());
+            return Collections.emptyList();
+        }
+    }
+}

+ 92 - 0
api/src/main/java/cn/seecoder/api/k8s/model/Job.java

@@ -0,0 +1,92 @@
+package cn.seecoder.api.k8s.model;
+
+import io.kubernetes.client.openapi.models.*;
+import lombok.*;
+import org.springframework.data.annotation.ReadOnlyProperty;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@EqualsAndHashCode(callSuper=true)
+public class Job extends K8sAbstractObject<V1Job>{
+
+    public static final String API_VERSION = "batch/v1";
+    private static final String KIND = "Job";
+    private static final Integer TTL_SECONDS_AFTER_FINISHED = 100;
+    private static final Long ACTIVE_DEADLINE_SECONDS = 3600L;
+
+    @Builder.Default
+    private List<Container> containers = new ArrayList<>();
+
+    @Builder.Default
+    private List<String> imagePullSecrets = new ArrayList<>();
+
+    @Builder.Default
+    private Long timeout = ACTIVE_DEADLINE_SECONDS;
+
+    @Builder.Default
+    private Integer ttlFinished = TTL_SECONDS_AFTER_FINISHED;
+
+    @ReadOnlyProperty
+    private V1JobStatus status;
+
+    public Job(V1Job v1Job) {
+        super(v1Job);
+        this.containers = v1Job.getSpec().getTemplate().getSpec().getContainers() == null ? null : v1Job.getSpec().getTemplate().getSpec().getContainers()
+                .stream().map(Container::new).collect(Collectors.toList());
+        this.imagePullSecrets = v1Job.getSpec().getTemplate().getSpec().getImagePullSecrets() == null ? null : v1Job.getSpec().getTemplate().getSpec().getImagePullSecrets()
+                .stream().map(V1LocalObjectReference::getName).collect(Collectors.toList());
+        this.timeout = v1Job.getSpec().getActiveDeadlineSeconds();
+        this.ttlFinished = v1Job.getSpec().getTtlSecondsAfterFinished();
+        this.status = v1Job.getStatus();
+    }
+
+    private V1PodTemplateSpec toPodSpecTemplate(){
+        V1ObjectMeta v1ObjectMeta = new V1ObjectMetaBuilder()
+                .withLabels(getLabels())
+                .withAnnotations(getAnnotations())
+                .build();
+//        V1PodSpec v1PodSpec = new V1PodSpecBuilder()
+//                .addAllToContainers(containers == null ? null : containers.stream().map(Container::toK8sObject).collect(Collectors.toList()))
+//                .withImagePullSecrets(references)
+//                .withVolumes(volumes)
+//                .withNodeSelector(nodeSelectors)
+//                .build();
+        List<V1LocalObjectReference> references = imagePullSecrets == null ? null : imagePullSecrets
+                .stream()
+                .map(secret -> new V1LocalObjectReference().name(secret))
+                .collect(Collectors.toList());
+        V1PodSpec v1PodSpec = new V1PodSpecBuilder()
+                .withImagePullSecrets(references)
+                .addAllToContainers(containers == null ? null : containers.stream().map(Container::toK8sObject).collect(Collectors.toList()))
+                .withRestartPolicy("Never")
+                .build();
+        return new V1PodTemplateSpecBuilder()
+                .withMetadata(v1ObjectMeta)
+                .withSpec(v1PodSpec)
+                .build();
+    }
+
+    private V1JobSpec toV1JobSpec(){
+        return new V1JobSpecBuilder()
+                .withTtlSecondsAfterFinished(ttlFinished)
+                .withActiveDeadlineSeconds(timeout)
+                .withTemplate(this.toPodSpecTemplate())
+                .build();
+    }
+
+    @Override
+    public V1Job toK8sObject() {
+        return new V1JobBuilder()
+                .withApiVersion(getApiVersion() == null ? API_VERSION : getApiVersion())
+                .withKind(getKind() == null ? KIND : getKind())
+                .withMetadata(this.toV1ObjectMeta())
+                .withSpec(this.toV1JobSpec())
+                .build();
+    }
+}

+ 8 - 0
web/src/main/java/cn/seecoder/web/controller/gitlab/CommitController.java

@@ -60,6 +60,14 @@ public class CommitController {
         this.seecoderGitlabApi = seecoderGitlabApi;
     }
 
+    @ApiOperation(value = "sonarqube回调接口", httpMethod = "POST")
+    @PostMapping("/sonar_hook")
+    @ApiImplicitParam(name = "hahaVO", dataType = "object", paramType = "body")
+    public Response<String> sonarHook(@RequestBody Object haha) throws ServiceException{
+        System.out.println();
+        return Response.buildSuccess("yes");
+    }
+
 
     /**
      * @author chenyz

+ 3 - 0
web/src/main/java/cn/seecoder/web/core/pipeline/PipelineException.java

@@ -10,12 +10,15 @@ public class PipelineException extends Exception{
     public static final String TEMPLATE_NAME_NOT_EXIST = "Pipeline模板名不存在";
     public static final String CONFIG_TRANSFER_ERROR = "流水线配置转换发生错误";
     public static final String GIT_CLONE_ERROR = "从仓库拉取代码发生错误";
+    public static final String BRANCH_NOT_FOUND_ERROR = "branch不存在";
+    public static final String PROJECT_NOT_FOUND_ERROR = "project id对应的项目不存在";
     public static final String DOCKERFILE_CREATE_ERROR = "创建dockerfile文件错误";
     public static final String NGINX_CONFIG_CREATE_ERROR = "创建nginx配置文件错误";
     public static final String IMAGE_BUILD_ERROR = "构建镜像构建推送到仓库错误";
     public static final String INGRESS_CREATE_ERROR = "K8S ingress 创建错误";
     public static final String SERVICE_CREATE_ERROR = "K8S service 创建错误";
     public static final String DEPLOYMENT_CREATE_ERROR = "K8S deployment 创建错误";
+    public static final String JOB_CREATE_ERROR = "K8S job 创建错误";
     public static final String LIMIT_RANGE_CREATE_ERROR = "K8S limit range 创建错误";
 
     public static final String NAMESPACE_CREATE_ERROR = "K8S namespace 创建错误";

+ 9 - 6
web/src/main/java/cn/seecoder/web/core/pipeline/PipelineFactory.java

@@ -19,12 +19,12 @@ import java.util.List;
 public class PipelineFactory {
     /**
      * 流水线初始化
-     *
+     * <p>
      * 注意 namespace deployName 只能为 小写英文字母 . - _ 的组合,这是因为docker镜像名的限制
      *
-     * @param configJson   configJson 流水线配置的json字符串
-     * @param namespace    流水线的k8s namespace,devcloud后端采用Project中的"name-id"作为namespace
-     * @param deployName   流水线部署k8s的应用名称,devcloud后端采用Pipeline中的name作为project name
+     * @param configJson configJson 流水线配置的json字符串
+     * @param namespace  流水线的k8s namespace,devcloud后端采用Project中的"name-id"作为namespace
+     * @param deployName 流水线部署k8s的应用名称,devcloud后端采用Pipeline中的name作为project name
      * @return
      */
     public static Pipeline init(Integer id, String configJson, String namespace, String deployName) throws PipelineException {
@@ -57,13 +57,16 @@ public class PipelineFactory {
             });
             for (ObjectNode node : handlers) {
                 //用于判断此步骤是否启用
-                if (!node.get("active").asBoolean()){
+                if (!node.get("active").asBoolean()) {
                     continue;
                 }
                 String name = node.get("name").toString().replaceAll("\"", "");
                 JsonNode configs = node.get("configs");
+                // 每个步骤并不一定对应一个Handler,而是可能对应多个Handler
                 cur.setNextHandler(HandlerConfigTable.transToHandler(name, configs));
-                cur = cur.getNextHandler();
+                while (cur.getNextHandler() != null) {
+                    cur = cur.getNextHandler();
+                }
             }
         } catch (Exception e) {
             throw new PipelineException(HttpStatus.SC_BAD_REQUEST, PipelineException.CONFIG_TRANSFER_ERROR, e);

+ 55 - 33
web/src/main/java/cn/seecoder/web/core/pipeline/config/HandlerConfigTable.java

@@ -7,16 +7,14 @@ import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.http.HttpStatus;
+
 import java.lang.reflect.Field;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
+import java.util.*;
 
 /**
  * @author PuHong Weng
  * @date 2021/3/10
- * @description:
- * 记录流水线配置json中 name到具体Handler的映射
+ * @description: 记录流水线配置json中 name到具体Handler的映射
  */
 @Slf4j
 public class HandlerConfigTable {
@@ -24,44 +22,68 @@ public class HandlerConfigTable {
     /**
      * 根据名称映射对应的流水线执行类
      */
-    private static final Map<String, Class<?>> handlerConfigMap;
+    private static final Map<String, List<Class<?>>> handlerConfigMap;
 
 
     static {
         handlerConfigMap = new HashMap<>();
-        handlerConfigMap.put("node-image-build", NodeImageBuildHandler.class);
-        handlerConfigMap.put("java-image-build", JavaImageBuildHandler.class);
-        handlerConfigMap.put("k8s-deployment", K8sDeploymentHandler.class);
-        handlerConfigMap.put("k8s-service", K8sServiceHandler.class);
-        handlerConfigMap.put("k8s-ingress", K8sIngressHandler.class);
-        handlerConfigMap.put("k8s-namespace", K8sNamespaceHandler.class);
-        handlerConfigMap.put("deploy", DeployHandler.class);
-        handlerConfigMap.put("api-test", ApiTestHandler.class);
-//        handlerConfigMap.put("interface-test", InterfaceTestHandler.class);
-        handlerConfigMap.put("mysql-deploy", MysqlDeployHandler.class);
+        handlerConfigMap.put("node-image-build", Collections.singletonList(NodeImageBuildHandler.class));
+        handlerConfigMap.put("java-image-build", Collections.singletonList(JavaImageBuildHandler.class));
+        handlerConfigMap.put("k8s-deployment", Collections.singletonList(K8sDeploymentHandler.class));
+        handlerConfigMap.put("k8s-service", Collections.singletonList(K8sServiceHandler.class));
+        handlerConfigMap.put("k8s-ingress", Collections.singletonList(K8sIngressHandler.class));
+        handlerConfigMap.put("k8s-namespace", Collections.singletonList(K8sNamespaceHandler.class));
+        handlerConfigMap.put("deploy", Collections.singletonList(DeployHandler.class));
+        handlerConfigMap.put("api-test", Collections.singletonList(ApiTestHandler.class));
+        handlerConfigMap.put("interface-test", Collections.singletonList(InterfaceTestHandler.class));
+        handlerConfigMap.put("mysql-deploy", Collections.singletonList(MysqlDeployHandler.class));
+        handlerConfigMap.put("sonar-java-image-build", Collections.singletonList(SonarJavaImageBuildHandler.class));
+        handlerConfigMap.put("k8s-job", Collections.singletonList(K8sJobHandler.class));
+
+        handlerConfigMap.put("sonar-java", Arrays.asList(SonarJavaImageBuildHandler.class, K8sJobHandler.class));
+        handlerConfigMap.put("sonar-java-deploy", Arrays.asList(SonarJavaImageBuildHandler.class, K8sJobHandler.class));
     }
 
     public static Handler transToHandler(String name, JsonNode config) throws Exception {
-        Handler handler;
+        Handler firstHandler = null;
+        Handler lastHandler = null;
         try {
-            handler = (Handler) handlerConfigMap.get(name).newInstance();
+            List<Class<?>> handlers = handlerConfigMap.get(name);
+            // 对所有Handler注入依赖
+            for (Class<?> handlerClass : handlers) {
+                Handler handler = (Handler) handlerClass.newInstance();
+                //通过反射注入配置对应Handler类
+                Iterator<Map.Entry<String, JsonNode>> jsonNodeIterator = config.fields();
+                while (jsonNodeIterator.hasNext()) {
+                    Map.Entry<String, JsonNode> entry = jsonNodeIterator.next();
+                    String propertyName = entry.getKey();
+                    JsonNode propertyValues = entry.getValue();
+                    ObjectMapper mapper = new ObjectMapper();
+                    // 遍历json,去handler中获取对应的成员变量,因此要处理找不到变量的情况
+                    Field field;
+                    try {
+                        field = handler.getClass().getDeclaredField(propertyName);
+                        field.setAccessible(true);
+                        //注意,这种写法无法自动注入List<xx>等的字段,因为泛型信息由于java的机制被删除了
+                        // 所以最好还是传入一些基本类型或者String,在对应的handler自己解析
+                        field.set(handler, mapper.readValue(propertyValues.traverse(), field.getType()));
+                    } catch (NoSuchFieldException ignored){
+                        // 如果Handler中没有对应json中的config属性,跳过,直接取下一个属性。
+                        // 注意java中异常当场处理之后会从try后面继续运行
+                    }
+                }
+                // 设置 Handler
+                if (firstHandler == null) {
+                    firstHandler = handler;
+                }
+                if (lastHandler != null) {
+                    lastHandler.setNextHandler(handler);
+                }
+                lastHandler = handler;
+            }
         } catch (InstantiationException | IllegalAccessException e) {
             throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.CONFIG_TRANSFER_ERROR, e);
         }
-        //通过反射注入配置对应Handler类
-        Iterator<Map.Entry<String, JsonNode>> jsonNodeIterator = config.fields();
-        while (jsonNodeIterator.hasNext()) {
-            Map.Entry<String, JsonNode> entry = jsonNodeIterator.next();
-            String propertyName = entry.getKey();
-            JsonNode propertyValues = entry.getValue();
-            ObjectMapper mapper = new ObjectMapper();
-            Field field ;
-            field = handler.getClass().getDeclaredField(propertyName);
-            field.setAccessible(true);
-            //注意,这种写法无法自动注入List<xx>等的字段,因为泛型信息由于java的机制被删除了
-            // 所以最好还是传入一些基本类型或者String,在对应的handler自己解析
-            field.set(handler, mapper.readValue(propertyValues.traverse(), field.getType()));
-        }
-        return handler;
+        return firstHandler;
     }
 }

+ 96 - 0
web/src/main/java/cn/seecoder/web/core/pipeline/handler/K8sJobHandler.java

@@ -0,0 +1,96 @@
+package cn.seecoder.web.core.pipeline.handler;
+
+
+import cn.seecoder.api.ApplicationProperties;
+import cn.seecoder.api.k8s.JobApi;
+import cn.seecoder.api.k8s.K8sConstants;
+import cn.seecoder.api.k8s.SecretApi;
+import cn.seecoder.api.k8s.exception.K8sApiException;
+import cn.seecoder.api.k8s.model.Container;
+import cn.seecoder.api.k8s.model.Job;
+import cn.seecoder.api.k8s.model.K8sObjectRequest;
+import cn.seecoder.api.k8s.vo.SecretVO;
+import cn.seecoder.common.util.SpringUtil;
+import cn.seecoder.web.core.pipeline.Context;
+import cn.seecoder.web.core.pipeline.PipelineException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.HttpStatus;
+
+import java.util.Collections;
+import java.util.List;
+
+@Slf4j
+public class K8sJobHandler extends AbstractHandler{
+
+    private final SecretApi secretApi;
+    private final JobApi jobApi;
+    private final ApplicationProperties applicationProperties;
+    private final static String DEFAULT_IMAGE_PULL_SECRET_NAME = "seecoder-devcloud-image-pull-secret";
+
+    public K8sJobHandler() {
+        this.applicationProperties = SpringUtil.getBean(ApplicationProperties.class);
+        secretApi = SpringUtil.getBean(SecretApi.class);
+        jobApi = SpringUtil.getBean(JobApi.class);
+    }
+
+    @Override
+    public void process(Context context) throws PipelineException {
+        // 0 变量
+        // imageName 是 namespace-deployName-sonar
+        // 例如 semobile id=248 项目的 imageName=semobile-248-haha-sonar
+        String imageName = context.getConfigs().get("imageName");
+        // imageTag 是镜像创建的时间
+        String imageTag = context.getConfigs().get("imageTag");
+        // namespace 是项目的 name 拼接上项目的 id
+        // 例如 semobile 项目 id 为 248, 则namespace = semobile-248
+        String namespace = context.getNamespace();
+        // deployName 是流水线的 name
+        String deployName = context.getDeployName();
+        // K8sConstants.JOB_SUFFIX = -job, 例如流水线叫做haha, jobname = haha-job
+        String jobName = deployName + K8sConstants.JOB_SUFFIX;
+
+        // 1 配置容器
+        Container container = Container.builder()
+                .image(applicationProperties.getDocker().getRegistry() + "/" + imageName + ":" + imageTag)
+                .name(deployName + K8sConstants.CONTAINER_SUFFIX)
+                .build();
+
+        // 2 构建Job
+        Job job = Job.builder()
+                .containers(Collections.singletonList(container))
+                .build();
+        job.setName(jobName);
+        job.setNamespace(namespace);
+        job.setLabel(K8sConstants.JOB_LABEL, deployName);
+
+        // 2 检查和配置 Secret
+        SecretVO secretVO = secretApi.getSecretByName(namespace, DEFAULT_IMAGE_PULL_SECRET_NAME);
+        if (secretVO == null) {
+            secretApi.createPrivateRegistrySecret(
+                    namespace,
+                    DEFAULT_IMAGE_PULL_SECRET_NAME,
+                    applicationProperties.getDocker().getRegistry(),
+                    applicationProperties.getDocker().getRegistryUsername(),
+                    applicationProperties.getDocker().getRegistryPassword()
+            );
+        }
+        job.setImagePullSecrets(Collections.singletonList(DEFAULT_IMAGE_PULL_SECRET_NAME));
+
+        // 3 创建Job
+        try {
+            List<Job> jobs = jobApi.getByCondition(K8sObjectRequest.builder()
+                    .namespace(namespace)
+                    .name(jobName)
+                    .build());
+            if (jobs.size() == 0){
+                jobApi.create(job);
+                log.info(String.format("job创建成功: [namespace: %s, deployName: %s]", namespace, deployName));
+            } else {
+                log.info("job已存在!放弃创建");
+            }
+        } catch (K8sApiException e) {
+            context.appendErrorResult(PipelineException.JOB_CREATE_ERROR, e);
+            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.JOB_CREATE_ERROR, e);
+        }
+    }
+}

+ 157 - 0
web/src/main/java/cn/seecoder/web/core/pipeline/handler/SonarJavaImageBuildHandler.java

@@ -0,0 +1,157 @@
+package cn.seecoder.web.core.pipeline.handler;
+
+import cn.seecoder.api.docker.DockerApi;
+import cn.seecoder.api.git.GitApi;
+import cn.seecoder.common.util.SpringUtil;
+import cn.seecoder.web.core.pipeline.Context;
+import cn.seecoder.web.core.pipeline.PipelineException;
+import cn.seecoder.web.core.pipeline.template.PipelineTemplateTable;
+import cn.seecoder.web.model.vo.project.ProjectVO;
+import cn.seecoder.web.service.project.BranchService;
+import cn.seecoder.web.service.project.ProjectService;
+import com.nju.edu.gitlab.vo.BranchVO;
+import com.spotify.docker.client.ProgressHandler;
+import com.spotify.docker.client.exceptions.DockerException;
+import com.spotify.docker.client.messages.ProgressMessage;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang.StringUtils;
+import org.apache.http.HttpStatus;
+import org.eclipse.jgit.api.Git;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.Date;
+import java.util.List;
+
+@Slf4j
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class SonarJavaImageBuildHandler extends AbstractHandler{
+
+    private final GitApi gitApi;
+    private final DockerApi dockerApi;
+    private final ProjectService projectService;
+    private final BranchService branchService;
+
+    // properties from pipeline config json
+//    private String repoUrl;
+    private String branchName;
+    private int projectId;
+//    private String commitHash;
+
+    public SonarJavaImageBuildHandler() {
+        this.gitApi = SpringUtil.getBean(GitApi.class);
+        this.dockerApi = SpringUtil.getBean(DockerApi.class);
+        this.projectService = SpringUtil.getBean(ProjectService.class);
+        this.branchService = SpringUtil.getBean(BranchService.class);
+    }
+
+    @Override
+    public void process(Context context) throws PipelineException {
+        // 0. prepare
+        String templateName = PipelineTemplateTable.SONAR_JAVA8;
+        String imageName = context.getNamespace() + '-' + context.getDeployName() + "-sonar";
+        String imageTag = String.valueOf(new Date().getTime());
+        ProjectVO project = projectService.getProjectById(projectId);
+        if (project == null) {
+            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.PROJECT_NOT_FOUND_ERROR);
+        }
+        String repoUrl = project.getGitRemoteUrl();
+        // convert url for local test
+        UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(repoUrl);
+        repoUrl = builder.scheme("https").host("seecoder-gitlab-gitlab.seec.seecoder.cn").path(".git/").toUriString();
+        BranchVO branch = branchService.getBranchById(projectId, branchName);
+        if (branch == null) {
+            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.BRANCH_NOT_FOUND_ERROR);
+        }
+        String commitHash = branch.getCommit().getId();
+
+        // 1. 有git从git拿到地址仓库
+        Path directory;
+        try {
+            //注:temp文件/目录系统会定时删,实际文件名喂 prefix + 它生成的suffix随机数,所以不会命名冲突
+            directory = Files.createTempDirectory("seecoder-devcloud-");
+            Git git = gitApi.clone(repoUrl, directory.toString());
+            git.checkout().setCreateBranch(false).setStartPoint("origin/" + branchName).setName("origin/" + branchName).call();
+        } catch (Exception e) {
+            context.appendErrorResult(PipelineException.GIT_CLONE_ERROR, e);
+            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.GIT_CLONE_ERROR, e);
+        }
+
+        //2. 帮助学生创建dockerfile和maven的setting.xml文件, 读取学生项目architect名字
+        Path dockerfile = Paths.get(directory.toString(), "Dockerfile");
+        Path mavenSetting = Paths.get(directory.toString(), "settings.xml");
+        try {
+            Files.deleteIfExists(dockerfile);
+            Files.deleteIfExists(mavenSetting);
+            PipelineTemplateTable.copyTemplateDockerfile(templateName, dockerfile);
+            PipelineTemplateTable.copyTemplateMavenSetting(mavenSetting);
+        } catch (IOException e) {
+            context.appendErrorResult(PipelineException.DOCKERFILE_CREATE_ERROR, e);
+            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.DOCKERFILE_CREATE_ERROR, e);
+        }
+
+        //3. 修改dockerfile内部需要替换的内容
+        List<String> dockerfileLines = null;
+        try {
+            dockerfileLines = Files.readAllLines(dockerfile);
+            StringBuilder sb = new StringBuilder();
+            for (String line : dockerfileLines) {
+                line = line
+                        .replace("%commitHash%", commitHash)
+                        .replace("%projectId%", Integer.toString(projectId))
+                        .replace("%imageName%",imageName)
+                        .replace("%imageTag%", imageTag);
+
+                sb.append(line).append("\n");
+            }
+            Files.write(dockerfile, sb.toString().getBytes(), StandardOpenOption.WRITE);
+        } catch (IOException e) {
+            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.DOCKERFILE_MODIFY_ERROR, e);
+        }
+
+        //4. 镜像构建
+        StringBuilder sb = new StringBuilder();
+        final int DEFAULT_BUFFER_LENGTH = 200;
+        final long MAX_BUFFER_SIZE = 1 << 18;
+        try {
+            dockerApi.buildAndPush(directory.toString(), imageName, imageTag, new ProgressHandler() {
+                @Override
+                public void progress(ProgressMessage message) throws DockerException {
+                    String value = message.stream();
+                    if (value == null) {
+                        value = "";
+                    }
+                    if (!StringUtils.isEmpty(message.error())) {
+                        value += "\n" + message.error();
+                    }
+                    sb.append(value);
+                    if (sb.length() > DEFAULT_BUFFER_LENGTH) {
+                        context.appendResult(sb.toString());
+                        if (context.getResult().length() > MAX_BUFFER_SIZE) {
+                            context.setResult(context.getResult().substring((int) (context.getResult().length() - MAX_BUFFER_SIZE)));
+                        }
+                        sb.setLength(0);
+                    }
+                }
+            });
+        } catch (Exception e) {
+            log.error("镜像构建失败,项目目录为:{}", directory.toString());
+            context.appendErrorResult(PipelineException.IMAGE_BUILD_ERROR, e);
+            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.IMAGE_BUILD_ERROR, e);
+        }
+
+        //5. 将镜像名字加入context
+        context.getConfigs().put("imageName", imageName);
+        context.getConfigs().put("imageTag", imageTag);
+        context.appendSuccessResult("镜像构建成功,已推送进仓库");
+    }
+}

+ 2 - 1
web/src/main/java/cn/seecoder/web/core/pipeline/template/PipelineTemplateTable.java

@@ -31,6 +31,7 @@ public class PipelineTemplateTable {
     public static final String NODE_14 = "NODE_14";
     public static final String NODE_16 = "NODE_16";
     public static final String MYSQL = "MYSQL";
+    public static final String SONAR_JAVA8 = "SONAR_JAVA8";
 
 
     /**
@@ -44,7 +45,7 @@ public class PipelineTemplateTable {
         templateNames.add(NODE_16);
         templateNames.add(SPRINGBOOT_JAVA8);
         templateNames.add(MYSQL);
-
+        templateNames.add(SONAR_JAVA8);
     }
 
     public static void validateTemplateName(String name) throws PipelineException {

+ 1 - 0
web/src/main/java/cn/seecoder/web/infrastructure/config/WebSecurityConfig.java

@@ -144,6 +144,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
                 .antMatchers("/**/*swagger*/**").permitAll()
                 .antMatchers("/**/*api-docs*/**").permitAll()
                 .antMatchers("/**/hook").permitAll()
+                .antMatchers("/**/sonar_hook").permitAll()
                 .antMatchers("/**/query/**").permitAll()
                 // 跨域的 Options 请求进行放行
                 .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()

+ 10 - 0
web/src/main/java/cn/seecoder/web/service/impl/project/BranchServiceImpl.java

@@ -26,4 +26,14 @@ public class BranchServiceImpl implements BranchService {
         }
         return branches;
     }
+
+    @Override
+    public BranchVO getBranchById(Integer projectId, String branchName) {
+        for (BranchVO branchVO : this.getAllBranches(projectId)) {
+            if (branchVO.getName().equals(branchName)) {
+                return branchVO;
+            }
+        }
+        return null;
+    }
 }

+ 2 - 0
web/src/main/java/cn/seecoder/web/service/project/BranchService.java

@@ -6,4 +6,6 @@ import java.util.List;
 
 public interface BranchService {
     List<BranchVO> getAllBranches(Integer projectId);
+
+    BranchVO getBranchById(Integer projectId, String branchName);
 }

+ 4 - 4
web/src/main/resources/application-lzl.yml

@@ -35,7 +35,7 @@ seecoder:
   jwt:
     secret: UenM4KGb6DskRWUenM4KGb6DskRWUenM4KGb6DskRWUenM4KGb6DskRWUenM4KGb6DskRWUenM4KGb6DskRWUenM4KGb6DskRWUenM4KGb6DskRWUenM4KGb6DskRW
   k8s:
-    api-server: https://192.168.99.105:8443
+    api-server: https://101.133.232.246:8443
     token: eyJhbGciOiJSUzI1NiIsImtpZCI6IjBmSXc4RlRjNUpBYVpGYWc4QzRDY1MzUWJad1JMdFhKcDI1cngzWGtiRlkifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZXZjbG91ZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJkZWZhdWx0LXRva2VuLXhqYmR3Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImRlZmF1bHQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJlZWNmMjg5OS1jZjNmLTQ5ZTItOWZjMS1kZTM3ZGM1YmQ0YjIiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGV2Y2xvdWQ6ZGVmYXVsdCJ9.kKYqrK-Y31cG5r6oAPGsu1BqD4H40hdaFW5xoSvm9gGzO6XBwgznxXswhBpJFtAi3DVpfukgx1XXSdouB8-z9uA6wRKiBKqmbVYy3GjXfEh1WUlvKxbu-JSA3UisTFdJwfaP_mUjoMUWLgdNm2R2bW2qLMsT0fO54cQf8WSjX3lDW2h4CBec7rq3nVqC4zWn3VATpHYTJg7he-yNKbDvqJCgpNtmC-fWdXwPm2vTjB6sXYcCiDdspRAPB7yhU3T-up7AZPBpXdHiBPLvjTHEBHnnMDtgO-EN4e46KDaIa2STMgNTYm1to-rzxeUQCkxUjBRWNdkyozD_hOFih1ULrw
     debugging: true
     ingressHostSuffix: .devcloud.seec.seecoder.cn
@@ -45,10 +45,10 @@ seecoder:
     token: wXDeETv2Kmt-JhiApJ9H
   git:
     username: root
-    password: NJU67software
+    password: 1qaz0158Seecoder67
   docker:
-    host: https://192.168.99.105:2376
-    registry: 172.19.51.6:8092
+    host: https://localhost:2376
+    registry: 106.14.170.111:8092
     registryUsername: admin
     registryPassword: NJU67nexus
     tls: true

+ 9 - 3
web/src/main/resources/sql/data_init.sql

@@ -1,5 +1,5 @@
-delete from devcloud.stage_config;
-delete from devcloud.stage;
+delete from devcloud.stage_config where true;
+delete from devcloud.stage where true;
 
 INSERT INTO devcloud.stage (id, title, description, name, skippable) VALUES (1, 'Java应用构建', '后端Java应用构建打包过程。项目需使用Maven作为项目管理工具。构建过程包含Maven test测试过程,需确保单元测试的100%正确性,单元测试结果将输出到打包日志中。此步骤将打包出一个可运行的服务器程序jar包,并构建生成一个可运行在平台内云服务器的docker镜像。请作为流水线的第一步。这里学生仅作粗略了解,无需深入配置。', 'java-image-build', 0);
 INSERT INTO devcloud.stage (id, title, description, name, skippable) VALUES (2, 'Node.js应用构建', '前端应用构建打包过程。需提供对应的Nginx配置文件,将请求转发给平台集群内的后端。请作为流水线的第一步。这里学生仅作粗略了解,无需深入配置。', 'node-image-build', 0);
@@ -10,6 +10,8 @@ INSERT INTO devcloud.stage (id, title, description, name, skippable) VALUES (6,
 INSERT INTO devcloud.stage (id, title, description, name, skippable) VALUES (7, '自动化api测试', '可指定自己在api测试界面所设定的api测试,并在流水线部署的末尾执行这些api测试用例,并自动记录结果。这个步骤可选择跳过。', 'api-test', 1);
 INSERT INTO devcloud.stage (id, title, description, name, skippable) VALUES (8, '应用部署', '可自动在平台部署一个服务实例,并对外开放访问方式供使用。', 'deploy', 0);
 INSERT INTO devcloud.stage (id, title, description, name, skippable) VALUES (9, 'Mysql部署', '可自动在平台部署一个Mysql服务实例。', 'mysql-deploy', 0);
+INSERT INTO devcloud.stage (id, title, description, name, skippable) VALUES (10, 'Java代码检查镜像创建', '后端Java代码静态质量检查,检查后结果会展示在构建详情和提交记录中。也可以在Java构建中选择此阶段进行检查。', 'sonar-java', 0);
+INSERT INTO devcloud.stage (id, title, description, name, skippable) VALUES (11, 'Java代码检查镜像创建', '后端Java代码静态质量检查,检查后结果会展示在构建详情和提交记录中。也可以单独创建质量检查流水线', 'sonar-java-deploy', 1);
 
 
 INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (1, 'repoUrl', '#todo', 1, 1);
@@ -29,4 +31,8 @@ INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (
 INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (15, 'access_url', '{pipelineName}.{projectName}-{projectId}.seec.seecoder.cn', 0, 8);
 INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (16, 'port', '3306', 0, 9);
 INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (17, 'username', 'root', 0, 9);
-INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (18, 'password', 'root', 1, 9);
+INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (18, 'password', 'root', 1, 9);
+INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (19, 'branchName', '#todo', 1, 10);
+INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (20, 'projectId', '{projectId}', 0, 10);
+INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (21, 'branchName', 'master', 1, 11);
+INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (22, 'projectId', '{projectId}', 0, 11);

+ 10 - 0
web/src/main/resources/template/config/SONAR_JAVA8.json

@@ -0,0 +1,10 @@
+[
+  {
+    "name": "sonar-java",
+    "active": true,
+    "configs": {
+      "branchName": "#todo",
+      "projectId": "#auto"
+    }
+  }
+]

+ 6 - 0
web/src/main/resources/template/dockerfile/SONAR_JAVA8

@@ -0,0 +1,6 @@
+FROM maven:3.8-jdk-11
+ENV WORK_PATH /opt/sonar_test
+COPY settings.xml /root/.m2/settings.xml
+COPY . $WORK_PATH
+#RUN cd $WORK_PATH && mvn clean package -DskipTests
+CMD cd $WORK_PATH && mvn clean verify sonar:sonar  -Dsonar.projectKey=hh  -Dsonar.host.url=https://sonarqube-test.seec.seecoder.cn  -Dsonar.login=c8a0e03bff522764c370588f3e8f5a3e86854ad7 -Dsonar.analysis.projectId=%projectId% -Dsonar.analysis.commitHash=%commitHash% -Dsonar.analysis.imageName=%imageName% -Dsonar.analysis.imageTag=%imageTag%