Bläddra i källkod

gitlab 提交,触发本系统的 webhook,通过 jenkins pipeline build 对应项目并打包成 docker 镜像,将镜像 push 到 docker registry,并保存镜像地址到数据库

Shifang Lei 7 år sedan
förälder
incheckning
f2f22adadb

+ 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> {
+}

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

@@ -0,0 +1,42 @@
+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
+    @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")
+    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;
+
+}

+ 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 {
 

+ 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!!!");
+        }
+    }
+}

+ 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();
+    }
+}

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

@@ -0,0 +1,13 @@
+package nju.seec.SEECdemo.web.controller;
+
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api")
+public class ProjectController {
+
+    @GetMapping("/test")
+    public String create(){
+        return "success";
+    }
+}

+ 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;
         }
     }
 }