反序列化问题。

This commit is contained in:
dengqichen 2024-12-24 15:18:31 +08:00
parent 7ad20a0cbd
commit 201d9ca054
23 changed files with 609 additions and 366 deletions

View File

@ -31,12 +31,6 @@ public class EnvironmentDTO extends BaseDTO {
@Schema(description = "环境描述")
private String envDesc;
@Schema(description = "构建方式JENKINS-Jenkins构建, GITLAB_RUNNER-GitLab Runner构建, GITHUB_ACTION-GitHub Action构建")
private BuildTypeEnum buildType;
@Schema(description = "部署方式K8S-Kubernetes集群部署, DOCKER-Docker容器部署, VM-虚拟机部署")
private DeployTypeEnum deployType;
@Schema(description = "排序号")
@NotNull(message = "排序号不能为空")
private Integer sort;

View File

@ -1,5 +1,6 @@
package com.qqchen.deploy.backend.deploy.dto;
import com.qqchen.deploy.backend.deploy.enums.ProjectGroupTypeEnum;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@ -15,6 +16,9 @@ public class ProjectGroupDTO extends BaseDTO {
// @NotNull(message = "租户ID不能为空")
private Long tenantCode;
@NotNull(message = "项目组类型不能为空")
private ProjectGroupTypeEnum type;
@NotBlank(message = "项目组编码不能为空")
private String projectGroupCode;
@ -25,6 +29,8 @@ public class ProjectGroupDTO extends BaseDTO {
private List<EnvironmentDTO> environments;
private List<ApplicationDTO> applications;
@NotBlank(message = "项目组状态不能为空")
private String projectGroupStatus;

View File

@ -43,20 +43,6 @@ public class Environment extends Entity<Long> {
@Column(name = "env_desc")
private String envDesc;
/**
* 构建方式
*/
@Enumerated(EnumType.STRING)
@Column(name = "build_type")
private BuildTypeEnum buildType;
/**
* 部署方式
*/
@Enumerated(EnumType.STRING)
@Column(name = "deploy_type")
private DeployTypeEnum deployType;
/**
* 排序号
*/

View File

@ -1,5 +1,6 @@
package com.qqchen.deploy.backend.deploy.entity;
import com.qqchen.deploy.backend.deploy.enums.ProjectGroupTypeEnum;
import com.qqchen.deploy.backend.framework.domain.Entity;
import jakarta.persistence.*;
import lombok.Data;
@ -17,8 +18,8 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true)
@jakarta.persistence.Entity
@Table(name = "deploy_project_group")
@SQLDelete(sql = "UPDATE deploy_project_group SET deleted = true WHERE id = ?; DELETE FROM deploy_project_group_environment WHERE project_group_id = ?")
@Where(clause = "deleted = false")
//@SQLDelete(sql = "UPDATE deploy_project_group SET deleted = TRUE WHERE id = ?; DELETE FROM deploy_project_group_environment WHERE project_group_id = ?")
//@Where(clause = "deleted = false")
public class ProjectGroup extends Entity<Long> {
/**
@ -27,12 +28,17 @@ public class ProjectGroup extends Entity<Long> {
@Column(name = "tenant_code")
private Long tenantCode;
@Column(name = "type", nullable = false)
@Enumerated(EnumType.STRING)
private ProjectGroupTypeEnum type;
/**
* 项目组编码
*/
@Column(name = "project_group_code", nullable = false)
private String projectGroupCode;
/**
* 项目组名称
*/
@ -67,4 +73,9 @@ public class ProjectGroup extends Entity<Long> {
inverseJoinColumns = @JoinColumn(name = "environment_id")
)
private List<Environment> environments = new ArrayList<>();
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "project_group_id")
private List<Application> applications = new ArrayList<>();
}

View File

@ -0,0 +1,10 @@
package com.qqchen.deploy.backend.deploy.enums;
// Jenkins构建状态枚举
public enum JenkinsBuildStatus {
SUCCESS, // 构建成功
FAILURE, // 构建失败
IN_PROGRESS,// 构建中
ABORTED, // 构建被取消
NOT_FOUND // 构建不存在
}

View File

@ -0,0 +1,19 @@
package com.qqchen.deploy.backend.deploy.enums;
import lombok.Getter;
@Getter
public enum ProjectGroupTypeEnum {
PRODUCT("PRODUCT", "产品"),
PROJECT("PROJECT", "项目");
private final String code;
private final String description;
ProjectGroupTypeEnum(String code, String description) {
this.code = code;
this.description = description;
}
}

View File

@ -0,0 +1,19 @@
package com.qqchen.deploy.backend.deploy.integration;
import com.qqchen.deploy.backend.deploy.enums.JenkinsBuildStatus;
import com.qqchen.deploy.backend.deploy.integration.response.JenkinsBuildInfoResponse;
import com.qqchen.deploy.backend.deploy.integration.response.JenkinsQueueBuildInfoResponse;
import com.qqchen.deploy.backend.deploy.integration.response.JenkinsCrumbIssuerResponse;
public interface IJenkinsServiceIntegration extends IExternalSystemIntegration {
JenkinsCrumbIssuerResponse getJenkinsCrumbIssue();
String buildWithParameters();
JenkinsQueueBuildInfoResponse getQueuedBuildInfo(String queueId);
JenkinsBuildStatus getBuildStatus(String jobName, Integer buildNumber);
}

View File

@ -1,58 +0,0 @@
package com.qqchen.deploy.backend.deploy.integration.impl;
import com.qqchen.deploy.backend.deploy.integration.IExternalSystemIntegration;
import com.qqchen.deploy.backend.deploy.entity.ExternalSystem;
import com.qqchen.deploy.backend.system.enums.ExternalSystemTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Base64;
@Slf4j
@Service
public class JenkinsIntegration implements IExternalSystemIntegration {
private final RestTemplate restTemplate = new RestTemplate();
@Override
public boolean testConnection(ExternalSystem system) {
try {
String url = system.getUrl() + "/api/json";
HttpHeaders headers = createHeaders(system);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
String.class
);
return response.getStatusCode() == HttpStatus.OK;
} catch (Exception e) {
log.error("Failed to connect to Jenkins: {}", system.getUrl(), e);
return false;
}
}
private HttpHeaders createHeaders(ExternalSystem system) {
HttpHeaders headers = new HttpHeaders();
switch (system.getAuthType()) {
case BASIC -> {
String auth = system.getUsername() + ":" + system.getPassword();
byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes());
headers.set("Authorization", "Basic " + new String(encodedAuth));
}
case TOKEN -> headers.set("Authorization", "Bearer " + system.getToken());
case OAUTH -> headers.set("Authorization", "Bearer " + system.getToken());
}
return headers;
}
@Override
public ExternalSystemTypeEnum getSystemType() {
return ExternalSystemTypeEnum.JENKINS;
}
}

View File

@ -0,0 +1,221 @@
package com.qqchen.deploy.backend.deploy.integration.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.deploy.entity.ExternalSystem;
import com.qqchen.deploy.backend.deploy.enums.JenkinsBuildStatus;
import com.qqchen.deploy.backend.deploy.integration.IJenkinsServiceIntegration;
import com.qqchen.deploy.backend.deploy.integration.response.JenkinsBuildInfoResponse;
import com.qqchen.deploy.backend.deploy.integration.response.JenkinsQueueBuildInfoResponse;
import com.qqchen.deploy.backend.deploy.integration.response.JenkinsCrumbIssuerResponse;
import com.qqchen.deploy.backend.deploy.repository.IExternalSystemRepository;
import com.qqchen.deploy.backend.system.enums.ExternalSystemTypeEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Base64;
import java.util.Map;
@Slf4j
@Service
public class JenkinsServiceIntegration implements IJenkinsServiceIntegration {
@Resource
private IExternalSystemRepository systemRepository;
private final RestTemplate restTemplate = new RestTemplate();
@Override
public boolean testConnection(ExternalSystem system) {
try {
String url = system.getUrl() + "/api/json";
HttpHeaders headers = createHeaders(system);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
String.class
);
return response.getStatusCode() == HttpStatus.OK;
} catch (Exception e) {
log.error("Failed to connect to Jenkins: {}", system.getUrl(), e);
return false;
}
}
private HttpHeaders createHeaders(ExternalSystem system) {
HttpHeaders headers = new HttpHeaders();
switch (system.getAuthType()) {
case BASIC -> {
String auth = system.getUsername() + ":" + system.getPassword();
byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes());
headers.set("Authorization", "Basic " + new String(encodedAuth));
}
case TOKEN -> headers.set("Authorization", "Bearer " + system.getToken());
case OAUTH -> headers.set("Authorization", "Bearer " + system.getToken());
}
return headers;
}
private HttpEntity<String> createHttpEntity(ExternalSystem jenkins) {
HttpHeaders headers = new HttpHeaders();
// 根据认证类型设置认证信息
switch (jenkins.getAuthType()) {
case BASIC -> {
// Basic认证
String auth = jenkins.getUsername() + ":" + jenkins.getPassword();
byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes());
String authHeader = "Basic " + new String(encodedAuth);
headers.set("Authorization", authHeader);
}
case TOKEN -> {
// Token认证
headers.set("Authorization", "Bearer " + jenkins.getToken());
}
default -> throw new RuntimeException("Unsupported authentication type: " + jenkins.getAuthType());
}
// 设置接受JSON响应
headers.set("Accept", "application/json");
return new HttpEntity<>(headers);
}
@Override
public ExternalSystemTypeEnum getSystemType() {
return ExternalSystemTypeEnum.JENKINS;
}
@Override
public JenkinsCrumbIssuerResponse getJenkinsCrumbIssue() {
ExternalSystem externalSystem = systemRepository.findById(1L).orElseThrow(() -> new RuntimeException("没有找到三方系统"));
String url = externalSystem.getUrl() + "/crumbIssuer/api/json";
HttpHeaders headers = createHeaders(externalSystem);
HttpEntity<String> entity = new HttpEntity<>(headers);
return convertResponse(restTemplate.exchange(url, HttpMethod.GET, entity, String.class));
}
@Override
public String buildWithParameters() {
JenkinsCrumbIssuerResponse jenkinsCrumbIssue = getJenkinsCrumbIssue();
ExternalSystem externalSystem = systemRepository.findById(1L).orElseThrow(() -> new RuntimeException("没有找到三方系统"));
HttpHeaders headers = createHeaders(externalSystem);
headers.set("Jenkins-Crumb", jenkinsCrumbIssue.getCrumb());
headers.set("Cookie", jenkinsCrumbIssue.getCookie());
HttpEntity<String> entity = new HttpEntity<>(headers);
String url = externalSystem.getUrl() + String.format("/job/%s/buildWithParameters", "scp-meta");
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
// 2. 从Location头获取队列ID
String location = response.getHeaders().getFirst("Location");
if (location == null) {
throw new RuntimeException("未获取到构建队列信息");
}
return extractQueueId(location);
}
@Override
public JenkinsQueueBuildInfoResponse getQueuedBuildInfo(String queueId) {
ExternalSystem externalSystem = systemRepository.findById(1L).orElseThrow(() -> new RuntimeException("没有找到三方系统"));
String queueUrl = String.format("%s/queue/item/%s/api/json", externalSystem.getUrl().trim(), queueId);
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(queueUrl, HttpMethod.GET, createHttpEntity(externalSystem), new ParameterizedTypeReference<>() {
});
Map<String, Object> queueInfo = response.getBody();
if (queueInfo == null) {
throw new RuntimeException("Failed to get queue information");
}
// 检查是否被取消
Boolean cancelled = (Boolean) queueInfo.get("cancelled");
if (Boolean.TRUE.equals(cancelled)) {
throw new RuntimeException("Build was cancelled in queue");
}
// 检查是否已开始执行
Map<String, Object> executable = (Map<String, Object>) queueInfo.get("executable");
if (executable != null) {
return new JenkinsQueueBuildInfoResponse(
((Number) executable.get("number")).intValue(),
(String) executable.get("url")
);
}
// 还在队列中
return null;
}
@Override
public JenkinsBuildStatus getBuildStatus(String jobName, Integer buildNumber) {
try {
ExternalSystem externalSystem = systemRepository.findById(1L).orElseThrow(() -> new RuntimeException("没有找到三方系统"));
String url = String.format("%s/job/%s/%d/api/json", externalSystem.getUrl().trim(), jobName, buildNumber);
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
url,
HttpMethod.GET,
createHttpEntity(externalSystem),
new ParameterizedTypeReference<>() {
}
);
if (response.getBody() == null) {
return JenkinsBuildStatus.NOT_FOUND;
}
String result = (String) response.getBody().get("result");
if (result == null) {
return JenkinsBuildStatus.IN_PROGRESS;
}
return switch (result) {
case "SUCCESS" -> JenkinsBuildStatus.SUCCESS;
case "FAILURE" -> JenkinsBuildStatus.FAILURE;
case "ABORTED" -> JenkinsBuildStatus.ABORTED;
default -> JenkinsBuildStatus.IN_PROGRESS;
};
} catch (Exception e) {
log.error("Failed to get build status: job={}, buildNumber={}", jobName, buildNumber, e);
throw new RuntimeException("Failed to get build status", e);
}
}
private String extractQueueId(String location) {
// location格式: http://jenkins-url/queue/item/12345/
return location.replaceAll(".*/item/(\\d+)/.*", "$1");
}
public JenkinsCrumbIssuerResponse convertResponse(ResponseEntity<String> response) {
// 1. 从响应体中解析JSON
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode;
try {
jsonNode = objectMapper.readTree(response.getBody());
} catch (JsonProcessingException e) {
throw new RuntimeException("解析Jenkins响应失败", e);
}
// 2. 从响应头中获取cookie
String cookie = response.getHeaders().getFirst(HttpHeaders.SET_COOKIE); // 获取SET_COOKIE头的第一个值
// 3. 构建返回对象
JenkinsCrumbIssuerResponse result = new JenkinsCrumbIssuerResponse();
result.setCrumb(jsonNode.get("crumb").asText());
result.setCookie(cookie);
return result;
}
}

View File

@ -0,0 +1,58 @@
package com.qqchen.deploy.backend.deploy.integration.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* Jenkins构建信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class JenkinsBuildInfoResponse {
/**
* 构建号
*/
private Integer buildNumber;
/**
* 构建URL
*/
private String buildUrl;
/**
* 任务名称
*/
private String jobName;
/**
* 构建状态
*/
private String status;
/**
* 开始时间
*/
private LocalDateTime startTime;
/**
* 持续时间毫秒
*/
private Long duration;
/**
* 构建结果
*/
private String result;
/**
* 是否在队列中
*/
private Boolean inQueue;
}

View File

@ -0,0 +1,12 @@
package com.qqchen.deploy.backend.deploy.integration.response;
import lombok.Data;
@Data
public class JenkinsCrumbIssuerResponse {
private String crumb;
private String cookie;
}

View File

@ -0,0 +1,26 @@
package com.qqchen.deploy.backend.deploy.integration.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Jenkins构建信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class JenkinsQueueBuildInfoResponse {
/**
* 构建号
*/
private Integer buildNumber;
/**
* 构建URL
*/
private String buildUrl;
}

View File

@ -11,6 +11,8 @@ import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@ -29,6 +31,13 @@ public class EnvironmentServiceImpl extends BaseServiceImpl<Environment, Environ
@Resource
private EntityManager entityManager;
// @Override
// @Transactional
// public EnvironmentDTO create(EnvironmentDTO dto) {
// return this.create(dto);
// }
@Override
public List<EnvironmentDTO> getProjectEnvironments(Long projectGroupId) {
QEnvironment environment = QEnvironment.environment;
@ -43,8 +52,8 @@ public class EnvironmentServiceImpl extends BaseServiceImpl<Environment, Environ
.fetch();
return environments.stream()
.map(environmentConverter::toDto)
.collect(Collectors.toList());
.map(environmentConverter::toDto)
.collect(Collectors.toList());
}
@Override

View File

@ -101,7 +101,7 @@ public class ExternalSystemServiceImpl extends BaseServiceImpl<ExternalSystem, E
}
@Override
@Transactional(readOnly = true)
@Transactional
public boolean testConnection(Long id) {
ExternalSystem system = findEntityById(id);
if (!system.getEnabled()) {

View File

@ -0,0 +1,124 @@
package com.qqchen.deploy.backend.workflow.delegate;
import com.qqchen.deploy.backend.deploy.enums.JenkinsBuildStatus;
import com.qqchen.deploy.backend.deploy.integration.IJenkinsServiceIntegration;
import com.qqchen.deploy.backend.deploy.integration.response.JenkinsQueueBuildInfoResponse;
import com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants;
import com.qqchen.deploy.backend.workflow.dto.definition.node.localVariables.DeployNodeLocalVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables.DeployNodePanelVariables;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.BpmnError;
import org.flowable.engine.delegate.DelegateExecution;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Shell脚本任务的委派者实现
*/
@Slf4j
@Component
public class DeployNodeDelegate extends BaseNodeDelegate<DeployNodePanelVariables, DeployNodeLocalVariables> {
@Resource
private ApplicationEventPublisher eventPublisher;
@Resource
private IJenkinsServiceIntegration jenkinsServiceIntegration;
private static final int QUEUE_POLL_INTERVAL = 10; // 10秒
private static final int MAX_QUEUE_POLLS = 30; // 最多等待5分钟
// 轮询间隔
private static final int BUILD_POLL_INTERVAL = 10;
// 最大轮询次数
private static final int MAX_BUILD_POLLS = 180; // 30分钟超时
// 用于存储实时输出的Map
private static final Map<String, StringBuilder> outputMap = new ConcurrentHashMap<>();
private static final Map<String, StringBuilder> errorMap = new ConcurrentHashMap<>();
@Override
protected Class<DeployNodePanelVariables> getPanelVariablesClass() {
return DeployNodePanelVariables.class;
}
@Override
protected Class<DeployNodeLocalVariables> getLocalVariablesClass() {
return DeployNodeLocalVariables.class;
}
@Override
protected void executeInternal(DelegateExecution execution, DeployNodePanelVariables panelVariables, DeployNodeLocalVariables localVariables) {
String queueId = jenkinsServiceIntegration.buildWithParameters();
JenkinsQueueBuildInfoResponse buildInfo = waitForBuildToStart(queueId);
// 3. 轮询构建状态
pollBuildStatus("scp-meta", buildInfo.getBuildNumber());
}
private JenkinsQueueBuildInfoResponse waitForBuildToStart(String queueId) {
int attempts = 0;
while (attempts < MAX_QUEUE_POLLS) {
try {
JenkinsQueueBuildInfoResponse buildInfo = jenkinsServiceIntegration.getQueuedBuildInfo(queueId);
if (buildInfo != null) {
return buildInfo;
}
log.debug("Build still in queue, waiting... attempts: {}/{}", attempts + 1, MAX_QUEUE_POLLS);
Thread.sleep(QUEUE_POLL_INTERVAL * 1000L);
attempts++;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BpmnError("POLLING_INTERRUPTED", "Interrupted while waiting for build to start");
}
}
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, String.format("Build did not start within %d seconds", MAX_QUEUE_POLLS * QUEUE_POLL_INTERVAL));
}
private void pollBuildStatus(String jobName, Integer buildNumber) {
int attempts = 0;
while (attempts < MAX_BUILD_POLLS) {
try {
// 等待一定时间后再检查
Thread.sleep(BUILD_POLL_INTERVAL * 1000L);
// 获取构建状态
JenkinsBuildStatus status = jenkinsServiceIntegration.getBuildStatus(jobName, buildNumber);
log.info("Jenkins build status: job={}, buildNumber={}, status={}", jobName, buildNumber, status);
switch (status) {
case SUCCESS:
// 构建成功返回继续流程
return;
case FAILURE:
// 构建失败抛出错误
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, String.format("Jenkins build failed: job=%s, buildNumber=%d", jobName, buildNumber));
case ABORTED:
// 构建被取消抛出错误
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, String.format("Jenkins build was aborted: job=%s, buildNumber=%d", jobName, buildNumber));
case IN_PROGRESS:
// 继续轮询
attempts++;
break;
case NOT_FOUND:
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, String.format("Jenkins build not found: job=%s, buildNumber=%d", jobName, buildNumber));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, "Build status polling was interrupted");
}
}
// 超过最大轮询次数视为超时
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, String.format("Jenkins build timed out after %d minutes: job=%s, buildNumber=%d", MAX_BUILD_POLLS * BUILD_POLL_INTERVAL / 60, jobName, buildNumber));
}
}

View File

@ -1,176 +0,0 @@
package com.qqchen.deploy.backend.workflow.delegate;
import com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants;
import com.qqchen.deploy.backend.workflow.dto.definition.node.localVariables.ScriptNodeLocalVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables.ScriptNodePanelVariables;
import com.qqchen.deploy.backend.workflow.enums.NodeLogTypeEnums;
import com.qqchen.deploy.backend.workflow.event.ShellLogEvent;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.BpmnError;
import org.flowable.engine.delegate.DelegateExecution;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Shell脚本任务的委派者实现
*/
@Slf4j
@Component
public class JenkinsJobTriggerDelegate extends BaseNodeDelegate<ScriptNodePanelVariables, ScriptNodeLocalVariables> {
@Resource
private ApplicationEventPublisher eventPublisher;
// 用于存储实时输出的Map
private static final Map<String, StringBuilder> outputMap = new ConcurrentHashMap<>();
private static final Map<String, StringBuilder> errorMap = new ConcurrentHashMap<>();
@Override
protected Class<ScriptNodePanelVariables> getPanelVariablesClass() {
return ScriptNodePanelVariables.class;
}
@Override
protected Class<ScriptNodeLocalVariables> getLocalVariablesClass() {
return ScriptNodeLocalVariables.class;
}
@Override
protected void executeInternal(DelegateExecution execution,
ScriptNodePanelVariables panelVariables,
ScriptNodeLocalVariables localVariables) {
if (panelVariables == null || panelVariables.getScript() == null) {
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, "Script is required but not provided");
}
// try {
// log.info("准备执行脚本: {}", panelVariables.getScript());
// // 使用processInstanceId而不是executionId
// String processInstanceId = execution.getProcessInstanceId();
// outputMap.put(processInstanceId, new StringBuilder());
// errorMap.put(processInstanceId, new StringBuilder());
//
// // 创建进程构建器
// ProcessBuilder processBuilder = new ProcessBuilder();
//
// // 根据操作系统选择合适的shell
// String os = System.getProperty("os.name").toLowerCase();
// if (os.contains("win")) {
// // Windows系统使用cmd
// processBuilder.command("cmd", "/c", panelVariables.getScript());
// } else {
// // Unix-like系统使用bash
// processBuilder.command("bash", "-c", panelVariables.getScript());
// }
//
// // 设置工作目录
// if (StringUtils.hasText(localVariables.getWorkDir())) {
// // Windows系统路径处理
// String workDirValue = localVariables.getWorkDir();
// if (os.contains("win")) {
// // 确保使用Windows风格的路径分隔符
// workDirValue = workDirValue.replace("/", "\\");
// // 如果路径以\开头去掉第一个\
// if (workDirValue.startsWith("\\")) {
// workDirValue = workDirValue.substring(1);
// }
// }
// File workDirFile = new File(workDirValue);
// if (!workDirFile.exists()) {
// workDirFile.mkdirs();
// }
// processBuilder.directory(workDirFile);
// }
//
// // 设置环境变量
// if (localVariables.getEnv() != null) {
// processBuilder.environment().putAll(localVariables.getEnv());
// }
//
// // 执行命令
// log.info("执行shell脚本: {}", panelVariables.getScript());
// Process process = processBuilder.start();
//
// // 创建线程池处理输出
// ExecutorService executorService = Executors.newFixedThreadPool(2);
//
// // 处理标准输出
// Future<?> outputFuture = executorService.submit(() ->
// processInputStream(process.getInputStream(), processInstanceId, NodeLogTypeEnums.STDOUT));
//
// // 处理错误输出
// Future<?> errorFuture = executorService.submit(() ->
// processInputStream(process.getErrorStream(), processInstanceId, NodeLogTypeEnums.STDERR));
//
// // 等待进程完成
// int exitCode = process.waitFor();
//
// // 等待输出处理完成
// outputFuture.get(5, TimeUnit.SECONDS);
// errorFuture.get(5, TimeUnit.SECONDS);
//
// // 关闭线程池
// executorService.shutdown();
//
// // 设置最终结果
// StringBuilder finalOutput = outputMap.get(processInstanceId);
// StringBuilder finalError = errorMap.get(processInstanceId);
//
// execution.setVariable("shellOutput", finalOutput.toString());
// execution.setVariable("shellError", finalError.toString());
// execution.setVariable("shellExitCode", exitCode);
//
// // 清理缓存
// outputMap.remove(processInstanceId);
// errorMap.remove(processInstanceId);
//
// if (exitCode != 0) {
// log.error("Shell脚本执行失败退出码: {}", exitCode);
// execution.setVariable("errorDetail", "Shell脚本执行失败退出码: " + exitCode);
// throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, "Shell脚本执行失败退出码: " + exitCode);
// }
// log.info("Shell脚本执行成功");
// log.debug("脚本输出: {}", finalOutput);
//
// } catch (Exception e) {
// log.error("Shell脚本执行失败", e);
// throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, e.getMessage());
// }
}
private void processInputStream(InputStream inputStream, String processInstanceId, NodeLogTypeEnums logType) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = reader.readLine()) != null) {
// 发布日志事件
eventPublisher.publishEvent(new ShellLogEvent(processInstanceId, line, logType));
// 同时保存到StringBuilder中
if (logType == NodeLogTypeEnums.STDOUT) {
StringBuilder output = outputMap.get(processInstanceId);
synchronized (output) {
output.append(line).append("\n");
}
// log.info("Shell output: {}", line);
} else {
StringBuilder error = errorMap.get(processInstanceId);
synchronized (error) {
error.append(line).append("\n");
}
// log.error("Shell error: {}", line);
}
}
} catch (IOException e) {
log.error("Error reading process output", e);
}
}
}

View File

@ -8,7 +8,7 @@ import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class JenkinsJobTriggerNodeFormVariables extends BaseNodeFormVariables {
public class DeployNodeFormVariables extends BaseNodeFormVariables {
@SchemaProperty(
title = "项目",

View File

@ -6,12 +6,12 @@ import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class JenkinsJobTriggerNodeLocalVariables extends BaseNodeLocalVariables {
public class DeployNodeLocalVariables extends BaseNodeLocalVariables {
@SchemaProperty(
title = "委派者",
description = "委派者",
defaultValue = "${jenkinsJobTriggerDelegate}",
defaultValue = "${deployNodeDelegate}",
required = true
)
private String delegate;

View File

@ -0,0 +1,14 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 脚本执行器配置
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DeployNodePanelVariables extends BaseNodePanelVariables {
}

View File

@ -1,35 +0,0 @@
package com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables;
import com.qqchen.deploy.backend.workflow.annotation.SchemaProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 脚本执行器配置
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class JenkinsJobTriggerNodePanelVariables extends BaseNodePanelVariables {
// @SchemaProperty(
// title = "环境",
// description = "环境",
// required = true
// )
// private String environment;
//
// /**
// * 脚本语言
// */
// @SchemaProperty(
// title = "任务名",
// description = "任务名",
// required = true,
// enumNames = {"Shell脚本 (已支持)", "Python脚本 (开发中)", "JavaScript脚本 (开发中)", "Groovy脚本 (开发中)"},
// defaultValue = "shell"
// )
// private String job;
}

View File

@ -1,12 +1,12 @@
package com.qqchen.deploy.backend.workflow.enums;
import com.fasterxml.jackson.annotation.JsonValue;
import com.qqchen.deploy.backend.workflow.dto.definition.node.fromVariables.JenkinsJobTriggerNodeFormVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.fromVariables.DeployNodeFormVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.fromVariables.ScriptNodeFormVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.localVariables.JenkinsJobTriggerNodeLocalVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.localVariables.DeployNodeLocalVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.localVariables.ScriptNodeLocalVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables.DeployNodePanelVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables.EndNodePanelVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables.JenkinsJobTriggerNodePanelVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables.ScriptNodePanelVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.panelVariables.StartNodePanelVariables;
import com.qqchen.deploy.backend.workflow.dto.definition.node.uiVariables.NodeUiVariables;
@ -38,7 +38,7 @@ public enum NodeTypeEnums {
NodeUiVariables.class,
BpmnNodeTypeEnums.START_EVENT,
NodeCategoryEnums.EVENT,
"工作流的起点", // 节点简要描述
"工作流开始", // 节点简要描述
new WorkflowNodeGraph() // UI配置
.setShape("circle")
.setSize(40, 40)
@ -62,7 +62,7 @@ public enum NodeTypeEnums {
NodeUiVariables.class,
BpmnNodeTypeEnums.END_EVENT,
NodeCategoryEnums.EVENT,
"工作流的终点",
"工作流结束",
new WorkflowNodeGraph()
.setShape("circle")
.setSize(40, 40)
@ -85,16 +85,16 @@ public enum NodeTypeEnums {
.setStyle("#ffffff", "#1890ff", "code")
.configPorts(Arrays.asList("in", "out"))
),
JENKINS_NODE(
"JENKINS_JOB_TRIGGER",
"JENKINS任务触发",
JenkinsJobTriggerNodeLocalVariables.class,
JenkinsJobTriggerNodePanelVariables.class,
JenkinsJobTriggerNodeFormVariables.class,
DEPLOY_NODE(
"DEPLOY_NODE",
"构建任务",
DeployNodeLocalVariables.class,
DeployNodePanelVariables.class,
DeployNodeFormVariables.class,
NodeUiVariables.class,
BpmnNodeTypeEnums.SERVICE_TASK,
NodeCategoryEnums.TASK,
"JENKINS任务触发",
"构建任务",
null
);
//

View File

@ -542,31 +542,34 @@ CREATE TABLE workflow_log
-- 项目组表
CREATE TABLE deploy_project_group
(
-- 基础字段
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(100) NULL COMMENT '创建人',
create_time DATETIME(6) NULL COMMENT '创建时间',
update_by VARCHAR(100) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 1 COMMENT '版本号',
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0-未删除1-已删除',
-- 基础字段
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
create_by VARCHAR(100) NULL COMMENT '创建人',
create_time DATETIME(6) NULL COMMENT '创建时间',
update_by VARCHAR(100) NULL COMMENT '更新人',
update_time DATETIME(6) NULL COMMENT '更新时间',
version INT NOT NULL DEFAULT 1 COMMENT '版本号',
deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除0-未删除1-已删除',
-- 业务字段
tenant_code BIGINT DEFAULT NULL COMMENT '租户CODE',
project_group_code VARCHAR(50) NOT NULL COMMENT '项目组编码',
project_group_name VARCHAR(100) NOT NULL COMMENT '项目组名称',
project_group_desc VARCHAR(255) NULL COMMENT '项目组描述',
project_group_status VARCHAR(50) NOT NULL DEFAULT 'ENABLED' COMMENT '项目组状态ENABLED-启用DISABLED-禁用',
sort INT NOT NULL DEFAULT 0 COMMENT '排序号',
-- 业务字段
tenant_code BIGINT DEFAULT NULL COMMENT '租户CODE',
type VARCHAR(50) NULL COMMENT '项目组类型',
project_group_code VARCHAR(50) NOT NULL COMMENT '项目组编码',
project_group_name VARCHAR(100) NOT NULL COMMENT '项目组名称',
project_group_desc VARCHAR(255) NULL COMMENT '项目组描述',
project_group_status VARCHAR(50) NOT NULL DEFAULT 'ENABLED' COMMENT '项目组状态ENABLED-启用DISABLED-禁用',
sort INT NOT NULL DEFAULT 0 COMMENT '排序号',
-- 索引
INDEX idx_tenant_id (tenant_code) COMMENT '租户ID索引',
UNIQUE INDEX uk_project_group_code (tenant_code, project_group_code) COMMENT '租户下项目组编码唯一',
-- 索引
INDEX idx_tenant_id (tenant_code) COMMENT '租户ID索引',
UNIQUE INDEX uk_project_group_code (tenant_code, project_group_code) COMMENT '租户下项目组编码唯一',
-- 外键约束
CONSTRAINT fk_project_tenant FOREIGN KEY (tenant_code)
REFERENCES sys_tenant (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目组表';
-- 外键约束
CONSTRAINT fk_project_tenant FOREIGN KEY (tenant_code)
REFERENCES sys_tenant (id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT ='项目组表';
-- 应用表
CREATE TABLE deploy_application
@ -608,11 +611,11 @@ CREATE TABLE deploy_environment
(
-- 业务字段
tenant_code VARCHAR(50) DEFAULT NULL COMMENT '租户编码',
build_type VARCHAR(50) NULL COMMENT '构建方式JENKINS-Jenkins构建,GITLAB_RUNNER-GitLab Runner构建,GITHUB_ACTION-GitHub Action构建',
deploy_type VARCHAR(50) NULL COMMENT '部署方式K8S-Kubernetes集群部署,DOCKER-Docker容器部署,VM-虚拟机部署',
-- build_type VARCHAR(50) NULL COMMENT '构建方式JENKINS-Jenkins构建,GITLAB_RUNNER-GitLab Runner构建,GITHUB_ACTION-GitHub Action构建',
env_code VARCHAR(50) NOT NULL COMMENT '环境编码',
env_name VARCHAR(100) NOT NULL COMMENT '环境名称',
env_desc VARCHAR(255) NULL COMMENT '环境描述',
sort INT NOT NULL DEFAULT 0 COMMENT '排序号',
-- 基础字段

View File

@ -167,8 +167,8 @@ INSERT INTO sys_external_system (
sync_status, last_sync_time, last_connect_time, config
) VALUES (
1, 'admin', '2023-12-01 00:00:00', 0, 'admin', '2023-12-01 00:00:00', 0,
'Jenkins测试环境', 'JENKINS', 'http://jenkins.test.com', '测试环境Jenkins服务器', 1, 1,
'BASIC', 'admin', 'password123', NULL,
'链宇JENKINS', 'JENKINS', 'https://ly-jenkins.iscmtech.com', '链宇JENKINS', 1, 1,
'BASIC', 'admin', 'Lianyu!@#~123456', NULL,
'SUCCESS', '2023-12-01 00:00:00', '2023-12-01 00:00:00', '{}'
), (
2, 'admin', '2023-12-01 00:00:00', 0, 'admin', '2023-12-01 00:00:00', 0,
@ -854,35 +854,35 @@ INSERT INTO workflow_definition (
-- --------------------------------------------------------------------------------------
-- 初始化项目组数据
INSERT INTO deploy_project_group (id, create_by, create_time, tenant_code, project_group_code, project_group_name, project_group_desc, project_group_status, sort)
VALUES
(1, 'admin', NOW(), 1, 'DEMO', '示例项目组', '用于演示的项目组', 'ENABLED', 1),
(2, 'admin', NOW(), 1, 'PLATFORM', '平台项目组', '平台相关的项目组', 'ENABLED', 2);
# INSERT INTO deploy_project_group (id, create_by, create_time, tenant_code, type, project_group_code, project_group_name, project_group_desc, project_group_status, sort)
# VALUES
# (1, 'admin', NOW(), 1, 'PRODUCT' 'DEMO', '示例项目组', '用于演示的项目组', 'ENABLED', 1),
# (2, 'admin', NOW(), 1, 'PRODUCT', 'PLATFORM', '平台项目组', '平台相关的项目组', 'ENABLED', 2);
-- 初始化应用数据
INSERT INTO deploy_application (
id, create_by, create_time,
project_group_id, app_code, app_name, app_desc, app_status,
repo_url, repo_branch, repo_type, build_type, dev_language, dev_framework, sort
)
VALUES
(
1, 'admin', NOW(),
1, 'DEMO-APP', '示例应用', '用于演示的应用', 'ENABLED',
'https://github.com/demo/demo-app.git', 'main', 'GIT', 'MAVEN', 'JAVA', 'SPRING_BOOT', 1
),
(
2, 'admin', NOW(),
1, 'DEMO-WEB', '示例前端', '用于演示的前端应用', 'ENABLED',
'https://github.com/demo/demo-web.git', 'main', 'GIT', 'NPM', 'NODEJS', 'VUE', 2
),
(
3, 'admin', NOW(),
2, 'PLATFORM-API', '平台API', '平台后端服务', 'ENABLED',
'https://github.com/platform/platform-api.git', 'main', 'GIT', 'MAVEN', 'JAVA', 'SPRING_BOOT', 1
),
(
4, 'admin', NOW(),
2, 'PLATFORM-WEB', '平台前端', '平台前端应用', 'ENABLED',
'https://github.com/platform/platform-web.git', 'main', 'GIT', 'NPM', 'NODEJS', 'VUE', 2
);
# INSERT INTO deploy_application (
# id, create_by, create_time,
# project_group_id, app_code, app_name, app_desc, app_status,
# repo_url, repo_branch, repo_type, build_type, dev_language, dev_framework, sort
# )
# VALUES
# (
# 1, 'admin', NOW(),
# 1, 'DEMO-APP', '示例应用', '用于演示的应用', 'ENABLED',
# 'https://github.com/demo/demo-app.git', 'main', 'GIT', 'MAVEN', 'JAVA', 'SPRING_BOOT', 1
# ),
# (
# 2, 'admin', NOW(),
# 1, 'DEMO-WEB', '示例前端', '用于演示的前端应用', 'ENABLED',
# 'https://github.com/demo/demo-web.git', 'main', 'GIT', 'NPM', 'NODEJS', 'VUE', 2
# ),
# (
# 3, 'admin', NOW(),
# 2, 'PLATFORM-API', '平台API', '平台后端服务', 'ENABLED',
# 'https://github.com/platform/platform-api.git', 'main', 'GIT', 'MAVEN', 'JAVA', 'SPRING_BOOT', 1
# ),
# (
# 4, 'admin', NOW(),
# 2, 'PLATFORM-WEB', '平台前端', '平台前端应用', 'ENABLED',
# 'https://github.com/platform/platform-web.git', 'main', 'GIT', 'NPM', 'NODEJS', 'VUE', 2
# );