diff --git a/oh-my-scheduler-server/src/main/java/com/github/kfcfans/oms/server/common/utils/OmsFileUtils.java b/oh-my-scheduler-server/src/main/java/com/github/kfcfans/oms/server/common/utils/OmsFileUtils.java index 948fddef..7a06569f 100644 --- a/oh-my-scheduler-server/src/main/java/com/github/kfcfans/oms/server/common/utils/OmsFileUtils.java +++ b/oh-my-scheduler-server/src/main/java/com/github/kfcfans/oms/server/common/utils/OmsFileUtils.java @@ -120,9 +120,9 @@ public class OmsFileUtils { * 计算文件的 MD5 * @param f 文件 * @return md5 - * @throws Exception 异常 + * @throws IOException 异常 */ - public static String md5(File f) throws Exception { + public static String md5(File f) throws IOException { String md5; try(FileInputStream fis = new FileInputStream(f)) { md5 = DigestUtils.md5DigestAsHex(fis); diff --git a/oh-my-scheduler-server/src/main/java/com/github/kfcfans/oms/server/service/ContainerService.java b/oh-my-scheduler-server/src/main/java/com/github/kfcfans/oms/server/service/ContainerService.java index b69c2a3b..fc67699f 100644 --- a/oh-my-scheduler-server/src/main/java/com/github/kfcfans/oms/server/service/ContainerService.java +++ b/oh-my-scheduler-server/src/main/java/com/github/kfcfans/oms/server/service/ContainerService.java @@ -15,16 +15,20 @@ import com.github.kfcfans.oms.server.persistence.core.repository.ContainerInfoRe import com.github.kfcfans.oms.server.service.ha.ClusterStatusHolder; import com.github.kfcfans.oms.server.service.ha.WorkerManagerService; import com.github.kfcfans.oms.server.service.lock.LockService; +import com.github.kfcfans.oms.server.web.request.SaveContainerInfoRequest; import com.google.common.collect.Lists; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.FileFilterUtils; import org.apache.commons.io.filefilter.IOFileFilter; +import org.apache.commons.lang3.time.DateFormatUtils; +import org.apache.commons.lang3.time.DateUtils; import org.apache.maven.shared.invoker.*; import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.data.mongodb.gridfs.GridFsResource; @@ -32,12 +36,15 @@ import org.springframework.data.mongodb.gridfs.GridFsTemplate; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import javax.websocket.RemoteEndpoint; import javax.websocket.Session; import java.io.File; +import java.io.IOException; import java.util.Collection; +import java.util.Date; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; @@ -63,6 +70,78 @@ public class ContainerService { // 并发部署的机器数量 private static final int DEPLOY_BATCH_NUM = 50; + // 部署间隔 + private static final long DEPLOY_MIN_INTERVAL = 10 * 60 * 1000; + // 时间格式 + private static final String TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; + + /** + * 保存容器 + * @param request 容器保存请求 + */ + public void save(SaveContainerInfoRequest request) { + ContainerInfoDO container; + Long originId = request.getId(); + if (originId != null) { + container = containerInfoRepository.findById(originId).orElseThrow(() -> new IllegalArgumentException("can't find container by id: " + originId)); + }else { + container = new ContainerInfoDO(); + container.setGmtCreate(new Date()); + } + BeanUtils.copyProperties(request, container); + container.setGmtModified(new Date()); + container.setSourceType(request.getSourceType().getV()); + container.setStatus(request.getStatus().getV()); + + // 文件上传形式的 sourceInfo 为该文件的 md5 值,Git形式的 md5 在部署阶段生成 + if (request.getSourceType() == ContainerSourceType.JarFile) { + container.setMd5(request.getSourceInfo()); + } + containerInfoRepository.saveAndFlush(container); + } + + /** + * 上传用于部署的容器的 Jar 文件 + * @param file 接受的文件 + * @return 该文件的 md5 值 + * @throws IOException 异常 + */ + public String uploadContainerJarFile(MultipartFile file) throws IOException { + + String workerDirStr = OmsFileUtils.genTemporaryPath(); + String tmpFileStr = workerDirStr + "tmp.jar"; + + File workerDir = new File(workerDirStr); + File tmpFile = new File(tmpFileStr); + + try { + // 下载到本地 + FileUtils.forceMkdirParent(tmpFile); + file.transferTo(tmpFile); + + // TODO:检验 jar 是否合法 + + // 生成MD5 + String md5 = OmsFileUtils.md5(tmpFile); + String fileName = genContainerJarName(md5); + + // 上传到 mongoDB + if (gridFsTemplate != null) { + OmsFileUtils.storeFile2GridFS(gridFsTemplate, tmpFile, fileName, null); + } + + // 将文件拷贝到正确的路径 + String finalFileStr = OmsFileUtils.genContainerJarPath() + fileName; + File finalFile = new File(finalFileStr); + FileUtils.forceDelete(finalFile); + FileUtils.moveFile(tmpFile, finalFile); + + return md5; + + }finally { + CommonUtils.executeIgnoreException(() -> FileUtils.forceDelete(workerDir)); + } + } /** * 获取构建容器所需要的 Jar 文件 @@ -109,11 +188,29 @@ public class ContainerService { } ContainerInfoDO container = containerInfoOpt.get(); + Date lastDeployTime = container.getLastDeployTime(); + if (lastDeployTime != null) { + if ((System.currentTimeMillis() - lastDeployTime.getTime()) < DEPLOY_MIN_INTERVAL) { + remote.sendText("SYSTEM: [warn] deploy too frequent, last deploy time is: " + DateFormatUtils.format(lastDeployTime, TIME_PATTERN)); + } + } + // 准备文件 File jarFile = prepareJarFile(container, session); + if (jarFile == null) { + remote.sendText("SYSTEM: prepare jarFile failed!"); + return; + } + double sizeMB = 1.0 * jarFile.length() / FileUtils.ONE_MB; remote.sendText(String.format("SYSTEM: the jarFile(size=%fMB) is prepared and ready to be deployed to the worker.", sizeMB)); + // 修改数据库,更新 MD5和最新部署时间 + Date now = new Date(); + container.setGmtModified(now); + container.setLastDeployTime(now); + containerInfoRepository.saveAndFlush(container); + // 开始部署(需要分批进行) Set workerAddressList = WorkerManagerService.getActiveWorkerInfo(container.getAppId()).keySet(); if (workerAddressList.isEmpty()) { @@ -156,67 +253,68 @@ public class ContainerService { File workerDir = new File(workerDirStr); FileUtils.forceMkdir(workerDir); - // git clone - remote.sendText("SYSTEM: start to git clone the code repo, using config: " + container.getSourceInfo()); - GitRepoInfo gitRepoInfo = JsonUtils.parseObject(container.getSourceInfo(), GitRepoInfo.class); + try { + // git clone + remote.sendText("SYSTEM: start to git clone the code repo, using config: " + container.getSourceInfo()); + GitRepoInfo gitRepoInfo = JsonUtils.parseObject(container.getSourceInfo(), GitRepoInfo.class); - CloneCommand cloneCommand = Git.cloneRepository() - .setDirectory(workerDir) - .setURI(gitRepoInfo.getRepo()) - .setBranch(gitRepoInfo.getBranch()); - if (!StringUtils.isEmpty(gitRepoInfo.getUsername())) { - CredentialsProvider credentialsProvider = new UsernamePasswordCredentialsProvider(gitRepoInfo.getUsername(), gitRepoInfo.getPassword()); - cloneCommand.setCredentialsProvider(credentialsProvider); + CloneCommand cloneCommand = Git.cloneRepository() + .setDirectory(workerDir) + .setURI(gitRepoInfo.getRepo()) + .setBranch(gitRepoInfo.getBranch()); + if (!StringUtils.isEmpty(gitRepoInfo.getUsername())) { + CredentialsProvider credentialsProvider = new UsernamePasswordCredentialsProvider(gitRepoInfo.getUsername(), gitRepoInfo.getPassword()); + cloneCommand.setCredentialsProvider(credentialsProvider); + } + cloneCommand.call(); + + // mvn clean package -DskipTests -U + remote.sendText("SYSTEM: git clone successfully, star to compile the project."); + Invoker mvnInvoker = new DefaultInvoker(); + InvocationRequest ivkReq = new DefaultInvocationRequest(); + ivkReq.setGoals(Lists.newArrayList("clean", "package", "-DskipTests", "-U")); + ivkReq.setBaseDirectory(workerDir); + ivkReq.setOutputHandler(remote::sendText); + + mvnInvoker.execute(ivkReq); + + String targetDirStr = workerDirStr + "/target"; + File targetDir = new File(targetDirStr); + IOFileFilter fileFilter = FileFilterUtils.asFileFilter((dir, name) -> name.endsWith("jar-with-dependencies.jar")); + Collection jarFile = FileUtils.listFiles(targetDir, fileFilter, null); + + if (CollectionUtils.isEmpty(jarFile)) { + remote.sendText("SYSTEM: can't find packaged jar(maybe maven build failed), so deploy failed."); + return null; + } + + File jarWithDependency = jarFile.iterator().next(); + String md5 = OmsFileUtils.md5(jarWithDependency); + // 更新 MD5 + container.setMd5(md5); + + String jarFileName = genContainerJarName(md5); + GridFsResource resource = gridFsTemplate.getResource(jarFileName); + + if (!resource.exists()) { + remote.sendText("SYSTEM: can't find the jar resource in remote, maybe this is a new version, start to upload new version."); + OmsFileUtils.storeFile2GridFS(gridFsTemplate, jarWithDependency, jarFileName, null); + remote.sendText("SYSTEM: upload to GridFS successfully~"); + } + + // 将文件从临时工作目录移动到正式目录 + String localFileStr = OmsFileUtils.genContainerJarPath() + jarFileName; + File localFile = new File(localFileStr); + if (localFile.exists()) { + FileUtils.forceDelete(localFile); + } + FileUtils.copyFile(jarWithDependency, localFile); + + return localFile; + }finally { + // 删除工作区数据 + FileUtils.forceDelete(workerDir); } - cloneCommand.call(); - - // mvn clean package -DskipTests -U - remote.sendText("SYSTEM: git clone successfully, star to compile the project."); - Invoker mvnInvoker = new DefaultInvoker(); - InvocationRequest ivkReq = new DefaultInvocationRequest(); - ivkReq.setGoals(Lists.newArrayList("clean", "package", "-DskipTests", "-U")); - ivkReq.setBaseDirectory(workerDir); - ivkReq.setOutputHandler(remote::sendText); - - InvocationResult mvnResult = mvnInvoker.execute(ivkReq); - if (mvnResult.getExitCode() != 0) { - throw mvnResult.getExecutionException(); - } - - String targetDirStr = workerDirStr + "/target"; - File targetDir = new File(targetDirStr); - IOFileFilter fileFilter = FileFilterUtils.asFileFilter((dir, name) -> name.endsWith("jar-with-dependencies.jar")); - Collection jarFile = FileUtils.listFiles(targetDir, fileFilter, null); - - if (CollectionUtils.isEmpty(jarFile)) { - remote.sendText("SYSTEM: can't find packaged jar, deploy failed!"); - throw new RuntimeException("can't find packaged jar"); - } - - File jarWithDependency = jarFile.iterator().next(); - String md5 = OmsFileUtils.md5(jarWithDependency); - // 更新 MD5 - container.setMd5(md5); - - String jarFileName = genContainerJarName(md5); - GridFsResource resource = gridFsTemplate.getResource(jarFileName); - - if (!resource.exists()) { - remote.sendText("SYSTEM: can't find the jar resource in remote, maybe this is a new version, start to upload new version."); - OmsFileUtils.storeFile2GridFS(gridFsTemplate, jarWithDependency, jarFileName, null); - remote.sendText("SYSTEM: upload to GridFS successfully~"); - } - - // 将文件从临时工作目录移动到正式目录 - String localFileStr = OmsFileUtils.genContainerJarPath() + jarFileName; - File localFile = new File(localFileStr); - FileUtils.forceDelete(localFile); - FileUtils.copyFile(jarWithDependency, localFile); - - // 删除工作区数据 - FileUtils.forceDelete(workerDir); - - return localFile; } // 先查询本地是否存在目标 Jar 文件 @@ -232,7 +330,7 @@ public class ContainerService { GridFsResource resource = gridFsTemplate.getResource(jarFileName); if (!resource.exists()) { remote.sendText(String.format("SYSTEM: can't find %s in local disk and GridFS, deploy failed!", jarFileName)); - throw new RuntimeException("can't find jar"); + return null; } remote.sendText("SYSTEM: start to download jar file from GridFS......"); OmsFileUtils.gridFs2File(resource, localFile); diff --git a/oh-my-scheduler-server/src/main/java/com/github/kfcfans/oms/server/web/controller/ContainerController.java b/oh-my-scheduler-server/src/main/java/com/github/kfcfans/oms/server/web/controller/ContainerController.java index 91955263..a175562a 100644 --- a/oh-my-scheduler-server/src/main/java/com/github/kfcfans/oms/server/web/controller/ContainerController.java +++ b/oh-my-scheduler-server/src/main/java/com/github/kfcfans/oms/server/web/controller/ContainerController.java @@ -1,7 +1,6 @@ package com.github.kfcfans.oms.server.web.controller; import com.github.kfcfans.oms.common.response.ResultDTO; -import com.github.kfcfans.oms.server.common.constans.ContainerSourceType; import com.github.kfcfans.oms.server.common.utils.ContainerTemplateGenerator; import com.github.kfcfans.oms.server.common.utils.OmsFileUtils; import com.github.kfcfans.oms.server.persistence.core.model.ContainerInfoDO; @@ -12,21 +11,14 @@ import com.github.kfcfans.oms.server.web.request.SaveContainerInfoRequest; import com.github.kfcfans.oms.server.web.response.ContainerInfoVO; import com.google.common.collect.Lists; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.mongodb.gridfs.GridFsTemplate; -import org.springframework.util.DigestUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; import java.io.*; -import java.util.Date; import java.util.List; -import java.util.UUID; import java.util.stream.Collectors; /** @@ -40,8 +32,6 @@ import java.util.stream.Collectors; @RequestMapping("/container") public class ContainerController { - private GridFsTemplate gridFsTemplate; - @Resource private ContainerInfoRepository containerInfoRepository; @@ -63,57 +53,16 @@ public class ContainerController { } @PostMapping("/jarUpload") - public ResultDTO fileUpload(@RequestParam("file")MultipartFile file) throws Exception { + public ResultDTO fileUpload(@RequestParam("file") MultipartFile file) throws Exception { if (file == null || file.isEmpty()) { return ResultDTO.failed("empty file"); } - - // 1. 本地持久化 - String path = OmsFileUtils.genContainerJarPath(); - String tmpFileName = UUID.randomUUID().toString() + ".jar"; - tmpFileName = StringUtils.replace(tmpFileName, "-", ""); - File jarFile = new File(path + tmpFileName); - FileUtils.forceMkdirParent(jarFile); - - file.transferTo(jarFile); - log.debug("[ContainerController] upload jarFile({}) to local disk success.", tmpFileName); - - // 2. 检查是否符合标准(是否为Jar,是否符合 template) - - // 3. 生成MD5 - String md5 = OmsFileUtils.md5(jarFile); - - // 3. 推送到 mongoDB - if (gridFsTemplate != null) { - } - - return ResultDTO.success(md5); + return ResultDTO.success(containerService.uploadContainerJarFile(file)); } @PostMapping("/save") public ResultDTO saveContainer(@RequestBody SaveContainerInfoRequest request) { - - ContainerInfoDO containerInfoDO; - if (request.getId() == null) { - containerInfoDO = new ContainerInfoDO(); - containerInfoDO.setGmtModified(new Date()); - }else { - containerInfoDO = containerInfoRepository.findById(request.getId()).orElseThrow(() -> new IllegalArgumentException("can't find container by id: " + request.getId())); - } - BeanUtils.copyProperties(request, containerInfoDO); - - containerInfoDO.setSourceType(request.getSourceType().getV()); - containerInfoDO.setStatus(request.getStatus().getV()); - containerInfoDO.setGmtCreate(new Date()); - - // git clone -> mvn clean package -> md5 生成文件名称 - if (request.getSourceType() == ContainerSourceType.Git) { - - }else { - containerInfoDO.setMd5(request.getSourceInfo()); - } - - containerInfoRepository.saveAndFlush(containerInfoDO); + containerService.save(request); return ResultDTO.success(null); } @@ -143,9 +92,4 @@ public class ContainerController { BeanUtils.copyProperties(containerInfoDO, vo); return vo; } - - @Autowired(required = false) - public void setGridFsTemplate(GridFsTemplate gridFsTemplate) { - this.gridFsTemplate = gridFsTemplate; - } } diff --git a/oh-my-scheduler-server/src/main/resources/application-daily.properties b/oh-my-scheduler-server/src/main/resources/application-daily.properties index c96fcbd5..b60df3f6 100644 --- a/oh-my-scheduler-server/src/main/resources/application-daily.properties +++ b/oh-my-scheduler-server/src/main/resources/application-daily.properties @@ -10,7 +10,7 @@ spring.datasource.core.hikari.maximum-pool-size=20 spring.datasource.core.hikari.minimum-idle=5 ####### mongoDB配置,非核心依赖,可移除 ####### -spring.data.mongodb.uri=mongodb://remotehost:27017/oms +spring.data.mongodb.uri=mongodb://remotehost:27017/oms-daily ####### 邮件配置(启用邮件报警则需要) ####### spring.mail.host=smtp.qq.com diff --git a/oh-my-scheduler-server/src/main/resources/application-pre.properties b/oh-my-scheduler-server/src/main/resources/application-pre.properties index 2c6656ef..a8794cb7 100644 --- a/oh-my-scheduler-server/src/main/resources/application-pre.properties +++ b/oh-my-scheduler-server/src/main/resources/application-pre.properties @@ -10,7 +10,7 @@ spring.datasource.core.hikari.maximum-pool-size=20 spring.datasource.core.hikari.minimum-idle=5 ####### mongoDB配置,非核心依赖,可移除 ####### -spring.data.mongodb.uri=mongodb://remotehost:27017/oms +spring.data.mongodb.uri=mongodb://remotehost:27017/oms-pre ####### 邮件配置(启用邮件报警则需要) ####### spring.mail.host=smtp.qq.com diff --git a/oh-my-scheduler-server/src/main/resources/application-product.properties b/oh-my-scheduler-server/src/main/resources/application-product.properties index aff70523..9dbfb517 100644 --- a/oh-my-scheduler-server/src/main/resources/application-product.properties +++ b/oh-my-scheduler-server/src/main/resources/application-product.properties @@ -10,7 +10,7 @@ spring.datasource.core.hikari.maximum-pool-size=20 spring.datasource.core.hikari.minimum-idle=5 ####### mongoDB配置,非核心依赖,可移除 ####### -spring.data.mongodb.uri=mongodb://remotehost:27017/oms +spring.data.mongodb.uri=mongodb://remotehost:27017/oms-product ####### 邮件配置(启用邮件报警则需要) ####### spring.mail.host=smtp.qq.com