4 Commits a8bc1ded8d ... 2a14d6bfb2

Auteur SHA1 Bericht Datum
  raledong 2a14d6bfb2 添加查询应用状态的接口 7 jaren geleden
  raledong a3f2a96504 Merge branch 'platform-dev' 7 jaren geleden
  Shifang Lei 5b3a8bf345 获取最近几次 build 记录和最近一次 build 结果的桩 7 jaren geleden
  Shifang Lei f2f22adadb gitlab 提交,触发本系统的 webhook,通过 jenkins pipeline build 对应项目并打包成 docker 镜像,将镜像 push 到 docker registry,并保存镜像地址到数据库 7 jaren geleden
24 gewijzigde bestanden met toevoegingen van 564 en 76 verwijderingen
  1. 15 0
      pom.xml
  2. 3 0
      src/main/java/nju/seec/SEECdemo/SeecDemoApplication.java
  3. 7 0
      src/main/java/nju/seec/SEECdemo/data/dao/assignment/BuildRecordDAO.java
  4. 43 0
      src/main/java/nju/seec/SEECdemo/data/entity/assignment/BuildRecord.java
  5. 2 2
      src/main/java/nju/seec/SEECdemo/logic/api/impl/JenkinsApiImpl.java
  6. 6 0
      src/main/java/nju/seec/SEECdemo/logic/api/k8s/model/Deployment.java
  7. 9 0
      src/main/java/nju/seec/SEECdemo/logic/service/ApplicationService.java
  8. 8 0
      src/main/java/nju/seec/SEECdemo/logic/service/NotifyService.java
  9. 1 1
      src/main/java/nju/seec/SEECdemo/logic/service/ProjectService.java
  10. 37 17
      src/main/java/nju/seec/SEECdemo/logic/service/impl/ApplicationServiceImpl.java
  11. 137 0
      src/main/java/nju/seec/SEECdemo/logic/service/impl/BuildManager.java
  12. 37 0
      src/main/java/nju/seec/SEECdemo/logic/service/impl/NotifyServiceImpl.java
  13. 22 0
      src/main/java/nju/seec/SEECdemo/logic/vo/ApplicationStatusVO.java
  14. 5 0
      src/main/java/nju/seec/SEECdemo/logic/vo/ApplicationVO.java
  15. 2 0
      src/main/java/nju/seec/SEECdemo/util/ApplicationProperties.java
  16. 28 0
      src/main/java/nju/seec/SEECdemo/util/enums/BuildStatus.java
  17. 15 0
      src/main/java/nju/seec/SEECdemo/util/exceptions/AccessDeniedException.java
  18. 36 0
      src/main/java/nju/seec/SEECdemo/util/exceptions/ServiceException.java
  19. 49 0
      src/main/java/nju/seec/SEECdemo/web/controller/NotifyController.java
  20. 40 0
      src/main/java/nju/seec/SEECdemo/web/controller/ProjectController.java
  21. 12 0
      src/main/java/nju/seec/SEECdemo/web/dto/BuildRecordDTO.java
  22. 10 0
      src/main/java/nju/seec/SEECdemo/web/dto/webhook/WebHookDTO.java
  23. 32 56
      src/main/resources/jenkins/config.xml
  24. 8 0
      src/test/java/nju/seec/SEECdemo/service/ApplicationServiceImplTest.java

+ 15 - 0
pom.xml

@@ -160,6 +160,21 @@
 				<groupId>org.springframework.boot</groupId>
 				<artifactId>spring-boot-maven-plugin</artifactId>
 			</plugin>
+
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-jar-plugin</artifactId>
+				<version>3.1.0</version>
+				<configuration>
+					<archive>
+						<manifest>
+							<addClasspath>true</addClasspath>
+							<classpathPrefix>lib/</classpathPrefix>
+							<mainClass>cn.deerowl.Application</mainClass>
+						</manifest>
+					</archive>
+				</configuration>
+			</plugin>
 		</plugins>
 	</build>
 

+ 3 - 0
src/main/java/nju/seec/SEECdemo/SeecDemoApplication.java

@@ -2,15 +2,18 @@ package nju.seec.SEECdemo;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
 import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
 
 import java.time.ZoneId;
 import java.util.TimeZone;
 
+
 @SpringBootApplication
 @EnableJpaRepositories(basePackages = "nju.seec.SEECdemo.data.dao")
 @EnableJpaAuditing
+@EnableConfigurationProperties
 public class SeecDemoApplication {
 
 	public static void main(String[] args) {

+ 7 - 0
src/main/java/nju/seec/SEECdemo/data/dao/assignment/BuildRecordDAO.java

@@ -0,0 +1,7 @@
+package nju.seec.SEECdemo.data.dao.assignment;
+
+import nju.seec.SEECdemo.data.entity.assignment.BuildRecord;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface BuildRecordDAO extends JpaRepository<BuildRecord, Integer> {
+}

+ 43 - 0
src/main/java/nju/seec/SEECdemo/data/entity/assignment/BuildRecord.java

@@ -0,0 +1,43 @@
+package nju.seec.SEECdemo.data.entity.assignment;
+
+import lombok.Data;
+import nju.seec.SEECdemo.util.enums.BuildStatus;
+
+import javax.persistence.*;
+import java.sql.Timestamp;
+
+@Data
+@Entity
+@Table(name = "ENTITY_BUILD_RECORD")
+public class BuildRecord {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id")
+    private Integer id;
+
+    @Basic
+    @Column(name = "number")
+    private Integer number;
+
+    @ManyToOne(targetEntity = CodeWorkResult.class, fetch = FetchType.LAZY)
+    @JoinColumn(name = "code_result", referencedColumnName = "id")
+    private CodeWorkResult codeResult;
+
+    @Basic
+    @Column(name = "build_time")
+    private Timestamp buildTime;
+
+    @Basic
+    @Column(name = "console_output", columnDefinition = "LONGTEXT NOT NULL")
+    private String consoleOutput = "";
+
+    @Basic
+    @Column(name = "image_name")
+    private String imageName = "";
+
+    @Enumerated(EnumType.STRING)
+    @Column(name = "status", columnDefinition = "VARCHAR(255) NOT NULL DEFAULT 'WAITING'")
+    private BuildStatus buildStatus = BuildStatus.WAITING;
+
+}

+ 2 - 2
src/main/java/nju/seec/SEECdemo/logic/api/impl/JenkinsApiImpl.java

@@ -73,7 +73,7 @@ public class JenkinsApiImpl implements JenkinsApi {
 			reference = server.getJob(String.valueOf(id)).build();
 		} else {
 			// 0.3.7版本的Jenkins客户端库中build(Map<String, String> params)方法存在BUG,会触发两次构建
-			reference = server.getJob(String.valueOf(id)).build(paramMap, false);
+			reference = server.getJob(String.valueOf(id)+"-build").build(paramMap, false);
 		}
 		QueueItem item = server.getQueueItem(reference);
 		while (item.getExecutable() == null) {
@@ -96,7 +96,7 @@ public class JenkinsApiImpl implements JenkinsApi {
 	public BuildDetails fetchBuildDetails(int id, int buildNumber, String target, boolean waitFinished) throws IOException {
 		BuildDetails result = new BuildDetails();
 		BuildWithDetails build;
-		build = server.getJob(String.valueOf(id)).getBuildByNumber(buildNumber).details();
+		build = server.getJob(String.valueOf(id)+"-build").getBuildByNumber(buildNumber).details();
 		while (waitFinished && (build.getResult() == null || build.getDuration() == 0)) {
 			try {
 				Thread.sleep(500);

+ 6 - 0
src/main/java/nju/seec/SEECdemo/logic/api/k8s/model/Deployment.java

@@ -4,10 +4,12 @@ import io.kubernetes.client.models.*;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 import lombok.Setter;
+import nju.seec.SEECdemo.util.DateUtil;
 import org.hibernate.validator.constraints.Range;
 import org.springframework.data.annotation.ReadOnlyProperty;
 
 import javax.validation.constraints.NotEmpty;
+import java.time.LocalDateTime;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -43,6 +45,9 @@ public class Deployment {
     @ReadOnlyProperty
     private DeploymentStatus status;
 
+    @ReadOnlyProperty
+    private LocalDateTime createdAt;
+
     public Deployment(V1Deployment v1Deployment) {
         this.name = v1Deployment.getMetadata().getName();
         this.namespace = v1Deployment.getMetadata().getNamespace();
@@ -54,6 +59,7 @@ public class Deployment {
         this.labels = v1Deployment.getMetadata().getLabels();
         this.annotations = v1Deployment.getMetadata().getAnnotations();
         this.status = new DeploymentStatus(v1Deployment.getStatus());
+        this.createdAt = DateUtil.fromJodaDateTime(v1Deployment.getMetadata().getCreationTimestamp());
     }
 
     public V1Deployment toV1Deployment(){

+ 9 - 0
src/main/java/nju/seec/SEECdemo/logic/service/ApplicationService.java

@@ -1,5 +1,6 @@
 package nju.seec.SEECdemo.logic.service;
 
+import nju.seec.SEECdemo.logic.vo.ApplicationStatusVO;
 import nju.seec.SEECdemo.logic.vo.ApplicationVO;
 import nju.seec.SEECdemo.web.dto.ApplicationDTO;
 
@@ -42,4 +43,12 @@ public interface ApplicationService {
      * @param applicationDTO
      */
     ApplicationVO deploy(ApplicationDTO applicationDTO);
+
+    /**
+     * 查询当前应用的状态
+     * @param projectName
+     * @param name
+     * @return
+     */
+    ApplicationStatusVO getStatus(String projectName, String name);
 }

+ 8 - 0
src/main/java/nju/seec/SEECdemo/logic/service/NotifyService.java

@@ -0,0 +1,8 @@
+package nju.seec.SEECdemo.logic.service;
+
+
+public interface NotifyService {
+    void webHook(int id, String checkoutSha);
+
+    void callback(int id, int buildNumber, String imageName);
+}

+ 1 - 1
src/main/java/nju/seec/SEECdemo/logic/service/ProjectService.java

@@ -1,9 +1,9 @@
 package nju.seec.SEECdemo.logic.service;
 
+
 import nju.seec.SEECdemo.logic.vo.ProjectVO;
 
 import java.time.LocalDate;
-import java.util.Date;
 
 public interface ProjectService {
 

+ 37 - 17
src/main/java/nju/seec/SEECdemo/logic/service/impl/ApplicationServiceImpl.java

@@ -2,12 +2,14 @@ package nju.seec.SEECdemo.logic.service.impl;
 
 import nju.seec.SEECdemo.logic.api.k8s.*;
 import nju.seec.SEECdemo.logic.api.k8s.model.Deployment;
+import nju.seec.SEECdemo.logic.api.k8s.model.DeploymentStatus;
 import nju.seec.SEECdemo.logic.api.k8s.model.Ingress;
 import nju.seec.SEECdemo.logic.api.k8s.model.Service;
 import nju.seec.SEECdemo.logic.api.k8s.exception.K8sApiException;
 import nju.seec.SEECdemo.logic.api.k8s.util.SecretTypeEnum;
 import nju.seec.SEECdemo.logic.api.k8s.vo.SecretVO;
 import nju.seec.SEECdemo.logic.service.ApplicationService;
+import nju.seec.SEECdemo.logic.vo.ApplicationStatusVO;
 import nju.seec.SEECdemo.logic.vo.ApplicationVO;
 import nju.seec.SEECdemo.util.LoggerUtil;
 import nju.seec.SEECdemo.web.dto.ApplicationDTO;
@@ -58,6 +60,10 @@ public class ApplicationServiceImpl implements ApplicationService{
 
     @Override
     public ApplicationVO getByName(String projectName, String name) {
+        Deployment deployment = deploymentApi.get(projectName, name);
+        if (deployment == null){
+            //抛出异常
+        }
         return null;
     }
 
@@ -86,23 +92,7 @@ public class ApplicationServiceImpl implements ApplicationService{
             ingressApi.create(ingress);
 
             //构建返回数据
-            ApplicationVO applicationVO = new ApplicationVO();
-            applicationVO.setName(applicationDTO.getName());
-            applicationVO.setProjectName(applicationDTO.getProjectName());
-            if (ingress.getHttpRules() != null) {
-                applicationVO.setUrl(
-                        ingress.getHttpRules().stream()
-                                .map(rule -> ingress.getHttpHost() + rule).collect(Collectors.toList())
-                );
-
-            }
-
-            try {
-                Thread.sleep(100000);
-            } catch (InterruptedException e) {
-                e.printStackTrace();
-            }
-            return applicationVO;
+            return buildApplicationVO(applicationDTO, deployment, ingress);
         } catch (K8sApiException e) {
             //抛出创建失败异常
             deploymentApi.deleteIfExist(applicationDTO.getProjectName(), applicationDTO.getName());
@@ -112,6 +102,13 @@ public class ApplicationServiceImpl implements ApplicationService{
         return null;
     }
 
+    @Override
+    public ApplicationStatusVO getStatus(String projectName, String name) {
+        Deployment deployment = deploymentApi.get(projectName, name);
+        if (deployment == null) return null;//抛出异常
+        return buildApplicationStatusVO(deployment);
+    }
+
     private Deployment buildDeployment(ApplicationDTO applicationDTO) {
         Deployment deployment = applicationDTO.toDeployment();
 
@@ -171,4 +168,27 @@ public class ApplicationServiceImpl implements ApplicationService{
         ingress.setHttpRules(detailDTOS);
         return ingress;
     }
+
+    private ApplicationVO buildApplicationVO(ApplicationDTO applicationDTO, Deployment deployment,  Ingress ingress) {
+        ApplicationVO applicationVO = new ApplicationVO();
+        applicationVO.setName(applicationDTO.getName());
+        applicationVO.setProjectName(applicationDTO.getProjectName());
+        if (ingress.getHttpRules() != null) {
+            applicationVO.setUrl(
+                    ingress.getHttpRules().stream()
+                            .map(rule -> ingress.getHttpHost() + rule).collect(Collectors.toList())
+            );
+
+        }
+        ApplicationStatusVO applicationStatusVO = buildApplicationStatusVO(deployment);
+        applicationVO.setStatus(applicationStatusVO);
+        return applicationVO;
+    }
+
+    private ApplicationStatusVO buildApplicationStatusVO(Deployment deployment) {
+        ApplicationStatusVO applicationStatusVO = new ApplicationStatusVO();
+        applicationStatusVO.setReadyReplicas(deployment.getStatus().getReadyReplicas());
+        applicationStatusVO.setDesiredReplicas(deployment.getReplicas());
+        return applicationStatusVO;
+    }
 }

+ 137 - 0
src/main/java/nju/seec/SEECdemo/logic/service/impl/BuildManager.java

@@ -0,0 +1,137 @@
+package nju.seec.SEECdemo.logic.service.impl;
+
+
+import com.offbytwo.jenkins.model.QueueItem;
+import lombok.extern.apachecommons.CommonsLog;
+import nju.seec.SEECdemo.data.dao.assignment.BuildRecordDAO;
+import nju.seec.SEECdemo.data.dao.assignment.CodeWorkResultDAO;
+import nju.seec.SEECdemo.data.entity.assignment.BuildRecord;
+import nju.seec.SEECdemo.data.entity.assignment.CodeWorkResult;
+import nju.seec.SEECdemo.logic.api.JenkinsApi;
+import nju.seec.SEECdemo.logic.api.vo.BuildDetails;
+import nju.seec.SEECdemo.util.ApplicationProperties;
+import nju.seec.SEECdemo.util.ToolKit;
+import nju.seec.SEECdemo.util.enums.BuildStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+@CommonsLog
+public class BuildManager {
+
+    private final CodeWorkResultDAO codeResultDAO;
+    private final BuildRecordDAO buildRecordDao;
+    private final JenkinsApi jenkinsApi;
+    private final ApplicationProperties properties;
+
+    public BuildManager(CodeWorkResultDAO codeResultDAO, BuildRecordDAO buildRecordDao, JenkinsApi jenkinsApi, ApplicationProperties properties)
+    {
+        this.codeResultDAO = codeResultDAO;
+        this.buildRecordDao = buildRecordDao;
+        this.jenkinsApi = jenkinsApi;
+        this.properties = properties;
+    }
+
+    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+    @Transactional
+    public boolean invokeNextBuild(int codeId,String commitHash) {
+        CodeWorkResult codeResult = codeResultDAO.findById(codeId);
+		if (codeResult == null) return false;
+        try {
+            QueueItem item = jenkinsApi.invokeBuild(codeId, buildParam(codeResult, commitHash));
+            System.out.println(item.getExecutable().getNumber().intValue());
+            return true;
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+//		Record record;
+//		while ((record = nextUnbuiltRecord(result)) != null) {
+//			try {
+//				QueueItem item = jenkinsApi.invokeBuild(resultId, buildParam(record));
+//				record.setNumber(item.getExecutable().getNumber().intValue());
+//				record.setStatus(BuildStatus.RUNNING);
+//				return true;
+//			} catch (Exception e) {
+//				log.error("触发构建" + record.getId() + "时发生异常" + ToolKit.format(e));
+//				record.setConsoleOutput("系统内部发生未知异常,请尝试重新提交!");
+//				record.setStatus(BuildStatus.FAILURE);
+//			} finally {
+//				recordDAO.save(record);
+//			}
+//		}
+        return true;
+    }
+
+    @Transactional
+    public boolean recordBuildResult(int codeId, int buildNumber, String imageName) {
+
+        System.out.println("id " + codeId + " buildNumber " + buildNumber);
+        CodeWorkResult codeResult = codeResultDAO.findById(codeId);
+        if (codeResult == null) return false;
+
+        BuildRecord buildRecord = new BuildRecord();
+
+        try{
+            BuildDetails buildDetails = jenkinsApi.fetchBuildDetails(codeId,buildNumber,"PACKAGE",true);
+            buildRecord.setBuildTime(new Timestamp(System.currentTimeMillis()));
+            buildRecord.setBuildStatus(BuildStatus.SUCCESS);
+            buildRecord.setConsoleOutput(buildDetails.getConsoleOutput());
+            buildRecord.setNumber(buildNumber);
+            buildRecord.setCodeResult(codeResult);
+            buildRecord.setImageName(imageName);
+        }catch (Exception e) {
+			log.error("获取构建记录" + buildNumber + "时发生异常" + ToolKit.format(e));
+            buildRecord.setConsoleOutput("系统内部发生未知异常,请尝试重新提交!");
+            buildRecord.setBuildStatus(BuildStatus.FAILURE);
+		} finally {
+			buildRecordDao.save(buildRecord);
+		}
+
+        return true;
+    }
+
+//    private Record nextUnbuiltRecord(Result result) {
+//        Commit commit = commitDAO.findFirstUnfinishedByResult(result);
+//        if (commit == null) return null;
+//        Record unbuiltRecord = null;
+//        for (Record record : commit.getRecords()) {
+//            switch (record.getStatus()) {
+//                case RUNNING:
+//                    return null;
+//                case WAITING:
+//                    if (unbuiltRecord == null) {
+//                        unbuiltRecord = record;
+//                    }
+//            }
+//        }
+//        return unbuiltRecord;
+//    }
+
+    private Map<String, String> buildParam(CodeWorkResult codeResult,String commitHash) {
+
+        Map<String, String> param = new HashMap<>();
+		param.put("GIT_URL", properties.getGitlab().getHost() + "/" + codeResult.getGroupId() + "/" + codeResult.getCode().getName());
+		param.put("COMMIT_HASH", commitHash);
+		param.put("DOCKER_FILE", "#基于openjdk:8\n" +
+                "FROM openjdk:8-jdk-alpine\n" +
+                "\n" +
+                "VOLUME /tmp\n" +
+                "ARG JAR_FILE=target/build-demo-1.0-SNAPSHOT.jar\n" +
+                "ARG DEPENDENCY=target/dependency\n" +
+                "ARG LIB=target/lib/*\n" +
+                "# 将jar包拷贝进来\n" +
+                "COPY ${JAR_FILE} /app/app.jar\n" +
+                "COPY ${LIB} /app/lib/\n" +
+                "# 将manifest文件拷贝进来\n" +
+                "COPY ${DEPENDENCY}/META-INF /app/META-INF\n" +
+                "\n" +
+                "EXPOSE 8081\n" +
+                "ENTRYPOINT [\"java\",\"-jar\",\"/app/app.jar\"]");
+        return param;
+    }
+}

+ 37 - 0
src/main/java/nju/seec/SEECdemo/logic/service/impl/NotifyServiceImpl.java

@@ -0,0 +1,37 @@
+package nju.seec.SEECdemo.logic.service.impl;
+
+import lombok.extern.apachecommons.CommonsLog;
+import nju.seec.SEECdemo.logic.service.NotifyService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+@Service
+@CommonsLog
+public class NotifyServiceImpl implements NotifyService {
+
+    private final BuildManager buildManager;
+
+    @Autowired
+    public NotifyServiceImpl(BuildManager buildManager) {
+        this.buildManager = buildManager;
+    }
+
+    @Override
+    @Async
+    public void webHook(int id, String commitHash) {
+
+        // 尝试触发第一次构建
+        buildManager.invokeNextBuild(id,commitHash);
+    }
+
+    @Override
+    @Async
+    public void callback(int id, int buildNumber, String imageName) {
+        // 尝试记录构建结果
+        if (buildManager.recordBuildResult(id, buildNumber, imageName)) {
+            // 记录构建结果成功
+            System.out.println("record build result success!!!");
+        }
+    }
+}

+ 22 - 0
src/main/java/nju/seec/SEECdemo/logic/vo/ApplicationStatusVO.java

@@ -0,0 +1,22 @@
+package nju.seec.SEECdemo.logic.vo;
+
+import lombok.Data;
+
+/**
+ * author: rale
+ * createdAt: 1/17/19
+ */
+@Data
+public class ApplicationStatusVO {
+
+    /**
+     * 就绪数
+     */
+    private int readyReplicas;
+
+    /**
+     * 期待数
+     */
+    private int desiredReplicas;
+
+}

+ 5 - 0
src/main/java/nju/seec/SEECdemo/logic/vo/ApplicationVO.java

@@ -2,7 +2,9 @@ package nju.seec.SEECdemo.logic.vo;
 
 import lombok.Data;
 import lombok.EqualsAndHashCode;
+import nju.seec.SEECdemo.logic.api.k8s.model.Deployment;
 
+import java.time.LocalDate;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -20,4 +22,7 @@ public class ApplicationVO {
     //传递回额外的信息
     private Map<String, Object> extra = new HashMap<>();
 
+    private LocalDate createdAt;
+
+    private ApplicationStatusVO status;
 }

+ 2 - 0
src/main/java/nju/seec/SEECdemo/util/ApplicationProperties.java

@@ -5,6 +5,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.hibernate.validator.constraints.URL;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.stereotype.Component;
 import org.springframework.validation.annotation.Validated;
 
 import javax.validation.Valid;
@@ -17,6 +18,7 @@ import java.util.Map;
 
 @Data
 @Configuration
+@Component
 @ConfigurationProperties("moocoder")
 @Validated
 public class ApplicationProperties {

+ 28 - 0
src/main/java/nju/seec/SEECdemo/util/enums/BuildStatus.java

@@ -0,0 +1,28 @@
+package nju.seec.SEECdemo.util.enums;
+
+public enum BuildStatus {
+	/**
+	 * 构建成功,表示没有错误
+	 */
+	SUCCESS,
+	/**
+	 * 构建不稳定,表示有测试错误等非致命错误
+	 */
+	UNSTABLE,
+	/**
+	 * 构建失败,表示有语法错误等致命错误
+	 */
+	FAILURE,
+	/**
+	 * 构建超时,表示运行测试时超时
+	 */
+	TIMEOUT,
+	/**
+	 * 等待构建,表示构建尚未开始
+	 */
+	WAITING,
+	/**
+	 * 正在构建,表示构建正在进行
+	 */
+	RUNNING
+}

+ 15 - 0
src/main/java/nju/seec/SEECdemo/util/exceptions/AccessDeniedException.java

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

+ 36 - 0
src/main/java/nju/seec/SEECdemo/util/exceptions/ServiceException.java

@@ -0,0 +1,36 @@
+package nju.seec.SEECdemo.util.exceptions;
+
+import lombok.Getter;
+import lombok.ToString;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@ToString
+public class ServiceException extends Exception {
+	private int error;
+
+	public ServiceException() {
+		this(HttpStatus.INTERNAL_SERVER_ERROR.value(), HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
+	}
+
+	public ServiceException(int error) {
+		this(error, message(error));
+	}
+
+	public ServiceException(String message) {
+		this(HttpStatus.INTERNAL_SERVER_ERROR.value(), message);
+	}
+
+	public ServiceException(int error, String message) {
+		super(message);
+		this.error = error;
+	}
+
+	private static String message(int error) {
+		try {
+			return HttpStatus.valueOf(error).getReasonPhrase();
+		} catch (IllegalArgumentException e) {
+			return HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase();
+		}
+	}
+}

+ 49 - 0
src/main/java/nju/seec/SEECdemo/web/controller/NotifyController.java

@@ -0,0 +1,49 @@
+package nju.seec.SEECdemo.web.controller;
+
+import nju.seec.SEECdemo.logic.service.NotifyService;
+import nju.seec.SEECdemo.util.ApplicationProperties;
+import nju.seec.SEECdemo.util.exceptions.AccessDeniedException;
+import nju.seec.SEECdemo.util.exceptions.ServiceException;
+import nju.seec.SEECdemo.web.dto.webhook.WebHookDTO;
+import nju.seec.SEECdemo.web.response.EmptyResponse;
+import nju.seec.SEECdemo.web.response.Response;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping(value = "/internal/notify", headers = "X-Moocoder-Secret")
+public class NotifyController {
+    private final String secret;
+
+    private final NotifyService notifyService;
+
+    @Autowired
+    public NotifyController(ApplicationProperties properties, NotifyService notifyService) {
+        this.secret = properties.getSecret();
+        this.notifyService = notifyService;
+    }
+
+    @PostMapping("/webhook/{id:\\d+}")
+    public Response webHook(@PathVariable int id,
+                            @RequestHeader("X-Moocoder-Secret") String secret,
+                            @RequestBody WebHookDTO webHookDTO) throws ServiceException {
+        if (!this.secret.equals(secret)) {
+            throw new AccessDeniedException();
+        }
+        notifyService.webHook(id, webHookDTO.getCheckoutSha());
+        return new EmptyResponse();
+    }
+
+    @PostMapping("/callback/{id:\\d+}/{buildNumber:\\d+}")
+    public Response callback(@PathVariable int id,
+                             @PathVariable int buildNumber,
+                             @RequestHeader("X-Moocoder-Secret") String secret,
+                             @RequestHeader("Image-Name") String imageName
+    ) throws ServiceException {
+        if (!this.secret.equals(secret)) {
+            throw new AccessDeniedException();
+        }
+        notifyService.callback(id, buildNumber, imageName);
+        return new EmptyResponse();
+    }
+}

+ 40 - 0
src/main/java/nju/seec/SEECdemo/web/controller/ProjectController.java

@@ -0,0 +1,40 @@
+package nju.seec.SEECdemo.web.controller;
+
+import nju.seec.SEECdemo.web.dto.BuildRecordDTO;
+import org.springframework.web.bind.annotation.*;
+
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/project")
+public class ProjectController {
+
+    private static final int BUILD_SUCCESS = 1;
+    private static final int BUILD_FAILURE = -1;
+    private static final int UNCOMMITTED = 0;
+
+    @GetMapping("/test")
+    public String create() {
+        return "success";
+    }
+
+    @PostMapping("/{id:\\d+}/recentBuild")
+    public List<BuildRecordDTO> getBuildRecords(@PathVariable int id, @RequestParam(value = "count") int count) {
+
+        List<BuildRecordDTO> recordDTOList = new ArrayList<>();
+        for (int i = 1; i <= count; i++) {
+            BuildRecordDTO recordDTO = new BuildRecordDTO();
+            recordDTO.setId(i);
+            recordDTO.setBuildTime(new Timestamp(System.currentTimeMillis() - i * 10000));
+            recordDTOList.add(recordDTO);
+        }
+        return recordDTOList;
+    }
+
+    @PostMapping("/lastBuild")
+    public int getLastBuildResult() {
+        return BUILD_SUCCESS;
+    }
+}

+ 12 - 0
src/main/java/nju/seec/SEECdemo/web/dto/BuildRecordDTO.java

@@ -0,0 +1,12 @@
+package nju.seec.SEECdemo.web.dto;
+
+import lombok.Data;
+
+import java.sql.Timestamp;
+
+@Data
+public class BuildRecordDTO {
+
+    private int id;
+    private Timestamp buildTime;
+}

+ 10 - 0
src/main/java/nju/seec/SEECdemo/web/dto/webhook/WebHookDTO.java

@@ -0,0 +1,10 @@
+package nju.seec.SEECdemo.web.dto.webhook;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class WebHookDTO {
+	@JsonProperty("checkout_sha")
+	private String checkoutSha;
+}

+ 32 - 56
src/main/resources/jenkins/config.xml

@@ -5,12 +5,12 @@
     <properties>
         <hudson.model.ParametersDefinitionProperty>
             <parameterDefinitions>
-                <hudson.model.TextParameterDefinition>
-                    <name>PROB_TARGET</name>
-                    <description/>
-                    <defaultValue/>
-                    <trim>false</trim>
-                </hudson.model.TextParameterDefinition>
+                <!--<hudson.model.TextParameterDefinition>-->
+                    <!--<name>PROB_TARGET</name>-->
+                    <!--<description/>-->
+                    <!--<defaultValue/>-->
+                    <!--<trim>false</trim>-->
+                <!--</hudson.model.TextParameterDefinition>-->
                 <hudson.model.TextParameterDefinition>
                     <name>GIT_URL</name>
                     <description/>
@@ -24,13 +24,7 @@
                     <trim>false</trim>
                 </hudson.model.TextParameterDefinition>
                 <hudson.model.TextParameterDefinition>
-                    <name>DOCKER_IMAGE</name>
-                    <description/>
-                    <defaultValue/>
-                    <trim>false</trim>
-                </hudson.model.TextParameterDefinition>
-                <hudson.model.TextParameterDefinition>
-                    <name>EXECUTE_SHELL</name>
+                    <name>DOCKER_FILE</name>
                     <description/>
                     <defaultValue/>
                     <trim>false</trim>
@@ -41,7 +35,8 @@
     <definition class="org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition" plugin="workflow-cps@2.53">
         <script>
 pipeline {
-    agent any
+    agent { docker { image &apos;maven:3.5.3&apos; } }
+
     stages {
         stage(&apos;Clean Workspace&apos;) {
             steps {
@@ -50,61 +45,42 @@ pipeline {
         }
         stage(&apos;Pull Code&apos;) {
             steps {
-                git credentialsId: &quot;MOOCODER&quot;, url: &quot;${GIT_URL}&quot;
-            }
-        }
-        stage(&apos;Reset Git HEAD&apos;) {
-            steps {
-                sh &quot;git reset --hard ${COMMIT_HASH}&quot;
-            }
-        }
-        stage(&apos;Run Build&apos;) {
-            agent {
-                docker {
-                    image &quot;${DOCKER_IMAGE}&quot;
-                    args &apos;--network none --cpus 1 -m 1G -h localhost --mount type=tmpfs,destination=/var/ws/tmp,tmpfs-size=134217728&apos;
-                    reuseNode true
-                }
-            }
-            steps {
-                echo &apos;==CONSOLE OUTPUT BEGIN==&apos;
-                catchError {
-                    timeout(1) {
-                        sh &quot;${EXECUTE_SHELL}&quot;
-                    }
-                }
-                echo &apos;==CONSOLE OUTPUT END==&apos;
+                git credentialsId: &apos;MOOCODER&apos;, url: &apos;${GIT_URL}&apos;
             }
         }
-        stage(&apos;Collect Test Report&apos;) {
-            when {
-                environment name: &apos;PROB_TARGET&apos;, value: &apos;TEST&apos;
-            }
-            steps {
-                junit allowEmptyResults: true, testResults: &apos;test-reports/*.xml&apos;
-            }
-        }
-        stage(&apos;Collect Coverage Report&apos;) {
-            when {
-                environment name: &apos;PROB_TARGET&apos;, value: &apos;COVERAGE&apos;
-            }
+        stage(&apos;Prepare&apos;) {
             steps {
-                cobertura autoUpdateHealth: false, autoUpdateStability: false, coberturaReportFile: &apos;coverage-reports/*.xml&apos;, conditionalCoverageTargets: &apos;70, 0, 0&apos;, failNoReports: false, failUnhealthy: false, failUnstable: false, lineCoverageTargets: &apos;80, 0, 0&apos;, maxNumberOfBuilds: 0, methodCoverageTargets: &apos;80, 0, 0&apos;, onlyStable: false, sourceEncoding: &apos;ASCII&apos;, zoomCoverageChart: false
+                writeFile file: &apos;Dockerfile&apos;, text: &quot;${DOCKER_FILE}&quot;
             }
         }
-        stage(&apos;Collect Mutation Report&apos;) {
-            when {
-                environment name: &apos;PROB_TARGET&apos;, value: &apos;MUTATION&apos;
+        stage(&apos;Build&apos;) {
+            environment {
+                def pom = readMavenPom file: &apos;&apos;
+                def docker_host = &quot;${MOOCODER_DOCKER_NEXUS}&quot;
+                def groupId = pom.getGroupId()
+                def artifactId = pom.getArtifactId()
+                def version = pom.getVersion()
+                def img_name = &quot;${groupId}-${artifactId}&quot;
+                def docker_img_name = &quot;${docker_host}/${img_name}-${JOB_NAME}&quot;
+                def docker_img_full_name = &quot;${docker_img_name}:${BUILD_NUMBER}&quot;
             }
             steps {
-                archiveArtifacts allowEmptyArchive: true, artifacts: &apos;mutation-reports/mutations.xml&apos;
+            sh &apos;mvn package -Dmaven.test.skip=true -Dmaven.repo.local=/var/ws/repository/&apos;
+                sh &apos;docker build -t ${docker_img_full_name} &apos; +
+                &apos; --build-arg JAR_FILE=target/${artifactId}-${version}.jar &apos; +
+                &apos; .&apos;
+                sh &apos;docker login -u ${MOOCODER_DOCKER_USER} -p ${MOOCODER_DOCKER_PASS} ${MOOCODER_DOCKER_NEXUS}&apos;
+				sh &apos;docker push ${docker_img_full_name}&apos;
+				sh &apos;curl -s -X POST http://${MOOCODER_HOST}/internal/notify/callback/${JOB_NAME}/${BUILD_NUMBER}&apos; +
+                   &apos; -H &quot;Content-Type: application/json&quot;&apos; +
+                   &apos; -H &quot;X-Moocoder-Secret:${MOOCODER_SECRET}&quot;&apos; +
+                   &apos; -H &quot;Image-Name:${docker_img_full_name}&quot;&apos;
             }
         }
     }
     post {
         always {
             cleanWs()
-            sh &apos;curl -o /dev/null -s -X POST -H &quot;X-Moocoder-Secret: ${MOOCODER_SECRET}&quot; &quot;${MOOCODER_HOST}/internal/notify/callback/${JOB_NAME}/${BUILD_NUMBER}&quot; || :&apos;
         }
     }
 }

+ 8 - 0
src/test/java/nju/seec/SEECdemo/service/ApplicationServiceImplTest.java

@@ -1,6 +1,8 @@
 package nju.seec.SEECdemo.service;
 
 import nju.seec.SEECdemo.logic.service.ApplicationService;
+import nju.seec.SEECdemo.logic.vo.ApplicationStatusVO;
+import nju.seec.SEECdemo.logic.vo.ApplicationVO;
 import nju.seec.SEECdemo.web.dto.ApplicationDTO;
 import nju.seec.SEECdemo.web.dto.ContainerDTO;
 import nju.seec.SEECdemo.web.dto.PortDTO;
@@ -50,4 +52,10 @@ public class ApplicationServiceImplTest {
     public void testDelete() {
         applicationService.delete("demo", "test");
     }
+
+    @Test
+    public void testGetStatus() {
+        ApplicationStatusVO status = applicationService.getStatus("demo2", "test");
+        System.out.println(status);
+    }
 }