diff --git a/backend/pom.xml b/backend/pom.xml index bd79e7b1..fe0f3fd9 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -281,6 +281,13 @@ HikariCP 5.0.1 + + + + io.kubernetes + client-java + 18.0.1 + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/K8sDeploymentApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/K8sDeploymentApiController.java new file mode 100644 index 00000000..f4dbef67 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/K8sDeploymentApiController.java @@ -0,0 +1,106 @@ +package com.qqchen.deploy.backend.deploy.api; + +import com.qqchen.deploy.backend.deploy.dto.K8sDeploymentDTO; +import com.qqchen.deploy.backend.deploy.entity.K8sDeployment; +import com.qqchen.deploy.backend.deploy.query.K8sDeploymentQuery; +import com.qqchen.deploy.backend.deploy.service.IK8sDeploymentService; +import com.qqchen.deploy.backend.framework.api.Response; +import com.qqchen.deploy.backend.framework.controller.BaseController; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@Slf4j +@RestController +@RequestMapping("/api/v1/k8s-deployment") +@Tag(name = "K8S Deployment管理", description = "K8S Deployment管理相关接口") +public class K8sDeploymentApiController extends BaseController { + + @Resource + private IK8sDeploymentService k8sDeploymentService; + + @Override + public Response create(@Validated @RequestBody K8sDeploymentDTO dto) { + return super.create(dto); + } + + @Override + public Response update(@PathVariable Long id, @Validated @RequestBody K8sDeploymentDTO dto) { + return super.update(id, dto); + } + + @Override + public Response delete(@PathVariable Long id) { + return super.delete(id); + } + + @Override + public Response findById(@PathVariable Long id) { + return super.findById(id); + } + + @Override + public Response> findAll() { + return super.findAll(); + } + + @Override + public Response> page(K8sDeploymentQuery query) { + return super.page(query); + } + + @Override + public Response> findAll(K8sDeploymentQuery query) { + return super.findAll(query); + } + + @Override + public CompletableFuture> batchProcess(List dtos) { + return super.batchProcess(dtos); + } + + @Operation(summary = "同步K8S Deployment", description = "异步同步,支持两种模式:1)只传externalSystemId-全量同步 2)传externalSystemId+namespaceId-同步指定命名空间") + @PostMapping("/sync") + public Response sync( + @Parameter(description = "K8S集群ID(外部系统ID)", required = true) @RequestParam Long externalSystemId, + @Parameter(description = "命名空间ID(可选)", required = false) @RequestParam(required = false) Long namespaceId + ) { + if (namespaceId != null) { + k8sDeploymentService.syncDeployments(externalSystemId, namespaceId); + } else { + k8sDeploymentService.syncDeployments(externalSystemId); + } + return Response.success(); + } + + @Operation(summary = "根据集群ID查询Deployment", description = "查询指定K8S集群的所有Deployment") + @GetMapping("/by-system/{externalSystemId}") + public Response> findByExternalSystemId( + @Parameter(description = "K8S集群ID", required = true) @PathVariable Long externalSystemId + ) { + return Response.success(k8sDeploymentService.findByExternalSystemId(externalSystemId)); + } + + @Operation(summary = "根据命名空间ID查询Deployment", description = "查询指定命名空间的所有Deployment") + @GetMapping("/by-namespace/{namespaceId}") + public Response> findByNamespaceId( + @Parameter(description = "命名空间ID", required = true) @PathVariable Long namespaceId + ) { + return Response.success(k8sDeploymentService.findByNamespaceId(namespaceId)); + } + + @Override + protected void exportData(HttpServletResponse response, List data) { + log.info("导出K8S Deployment数据,数据量:{}", data.size()); + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/K8sNamespaceApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/K8sNamespaceApiController.java new file mode 100644 index 00000000..ea6332a2 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/K8sNamespaceApiController.java @@ -0,0 +1,85 @@ +package com.qqchen.deploy.backend.deploy.api; + +import com.qqchen.deploy.backend.deploy.dto.K8sNamespaceDTO; +import com.qqchen.deploy.backend.deploy.entity.K8sNamespace; +import com.qqchen.deploy.backend.deploy.query.K8sNamespaceQuery; +import com.qqchen.deploy.backend.deploy.service.IK8sNamespaceService; +import com.qqchen.deploy.backend.framework.api.Response; +import com.qqchen.deploy.backend.framework.controller.BaseController; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@Slf4j +@RestController +@RequestMapping("/api/v1/k8s-namespace") +@Tag(name = "K8S命名空间管理", description = "K8S命名空间管理相关接口") +public class K8sNamespaceApiController extends BaseController { + + @Resource + private IK8sNamespaceService k8sNamespaceService; + + @Override + public Response create(@Validated @RequestBody K8sNamespaceDTO dto) { + return super.create(dto); + } + + @Override + public Response update(@PathVariable Long id, @Validated @RequestBody K8sNamespaceDTO dto) { + return super.update(id, dto); + } + + @Override + public Response delete(@PathVariable Long id) { + return super.delete(id); + } + + @Override + public Response findById(@PathVariable Long id) { + return super.findById(id); + } + + @Override + public Response> findAll() { + return super.findAll(); + } + + @Override + public Response> page(K8sNamespaceQuery query) { + return super.page(query); + } + + @Override + public Response> findAll(K8sNamespaceQuery query) { + return super.findAll(query); + } + + @Override + public CompletableFuture> batchProcess(List dtos) { + return super.batchProcess(dtos); + } + + @Operation(summary = "同步K8S命名空间", description = "异步同步指定K8S集群的命名空间") + @PostMapping("/sync") + public Response sync( + @Parameter(description = "K8S集群ID(外部系统ID)", required = true) @RequestParam Long externalSystemId + ) { + k8sNamespaceService.syncNamespaces(externalSystemId); + return Response.success(); + } + + @Override + protected void exportData(HttpServletResponse response, List data) { + log.info("导出K8S命名空间数据,数据量:{}", data.size()); + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/K8sSyncHistoryApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/K8sSyncHistoryApiController.java new file mode 100644 index 00000000..ca4a09ef --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/K8sSyncHistoryApiController.java @@ -0,0 +1,84 @@ +package com.qqchen.deploy.backend.deploy.api; + +import com.qqchen.deploy.backend.deploy.dto.K8sSyncHistoryDTO; +import com.qqchen.deploy.backend.deploy.entity.K8sSyncHistory; +import com.qqchen.deploy.backend.deploy.query.K8sSyncHistoryQuery; +import com.qqchen.deploy.backend.deploy.service.IK8sSyncHistoryService; +import com.qqchen.deploy.backend.framework.api.Response; +import com.qqchen.deploy.backend.framework.controller.BaseController; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@Slf4j +@RestController +@RequestMapping("/api/v1/k8s-sync-history") +@Tag(name = "K8S同步历史管理", description = "K8S同步历史管理相关接口") +public class K8sSyncHistoryApiController extends BaseController { + + @Resource + private IK8sSyncHistoryService k8sSyncHistoryService; + + @Override + public Response create(@Validated @RequestBody K8sSyncHistoryDTO dto) { + return super.create(dto); + } + + @Override + public Response update(@PathVariable Long id, @Validated @RequestBody K8sSyncHistoryDTO dto) { + return super.update(id, dto); + } + + @Override + public Response delete(@PathVariable Long id) { + return super.delete(id); + } + + @Override + public Response findById(@PathVariable Long id) { + return super.findById(id); + } + + @Override + public Response> findAll() { + return super.findAll(); + } + + @Override + public Response> page(K8sSyncHistoryQuery query) { + return super.page(query); + } + + @Override + public Response> findAll(K8sSyncHistoryQuery query) { + return super.findAll(query); + } + + @Override + public CompletableFuture> batchProcess(List dtos) { + return super.batchProcess(dtos); + } + + @Operation(summary = "根据集群ID查询同步历史", description = "查询指定K8S集群的同步历史记录") + @GetMapping("/by-system/{externalSystemId}") + public Response> findByExternalSystemId( + @Parameter(description = "K8S集群ID", required = true) @PathVariable Long externalSystemId + ) { + return Response.success(k8sSyncHistoryService.findByExternalSystemId(externalSystemId)); + } + + @Override + protected void exportData(HttpServletResponse response, List data) { + log.info("导出K8S同步历史数据,数据量:{}", data.size()); + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/config/ThreadPoolConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/config/ThreadPoolConfig.java index 77b4c56f..6b0c59de 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/config/ThreadPoolConfig.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/config/ThreadPoolConfig.java @@ -84,6 +84,29 @@ public class ThreadPoolConfig { return executor; } + /** + * K8S资源同步线程池 - 使用虚拟线程(Java 21+) + * + * ⚠️ 为什么使用虚拟线程? + * 1. K8S API调用是典型的**网络I/O密集型**任务 + * 2. 等待K8S API响应时线程会长时间阻塞 + * 3. 虚拟线程在阻塞时不占用OS线程,资源消耗极低 + * 4. 支持数百个并发K8S资源同步(Namespace、Deployment、Pod等) + * + * 💡 场景: + * - 定时同步K8S命名空间 + * - 定时同步K8S Deployment + * - 实时查询Pod状态 + * - 多集群并发同步 + */ + @Bean("k8sTaskExecutor") + public SimpleAsyncTaskExecutor k8sTaskExecutor() { + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor("k8s-virtual-"); + executor.setVirtualThreads(true); + executor.setConcurrencyLimit(-1); // 无限制,支持多集群并发 + return executor; + } + /** * 通用应用任务线程池 - 保留平台线程(不使用虚拟线程) * diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/K8sDeploymentConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/K8sDeploymentConverter.java new file mode 100644 index 00000000..dd34c759 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/K8sDeploymentConverter.java @@ -0,0 +1,10 @@ +package com.qqchen.deploy.backend.deploy.converter; + +import com.qqchen.deploy.backend.deploy.dto.K8sDeploymentDTO; +import com.qqchen.deploy.backend.deploy.entity.K8sDeployment; +import com.qqchen.deploy.backend.framework.converter.BaseConverter; +import org.mapstruct.Mapper; + +@Mapper(config = BaseConverter.class) +public interface K8sDeploymentConverter extends BaseConverter { +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/K8sNamespaceConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/K8sNamespaceConverter.java new file mode 100644 index 00000000..bb55c049 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/K8sNamespaceConverter.java @@ -0,0 +1,10 @@ +package com.qqchen.deploy.backend.deploy.converter; + +import com.qqchen.deploy.backend.deploy.dto.K8sNamespaceDTO; +import com.qqchen.deploy.backend.deploy.entity.K8sNamespace; +import com.qqchen.deploy.backend.framework.converter.BaseConverter; +import org.mapstruct.Mapper; + +@Mapper(config = BaseConverter.class) +public interface K8sNamespaceConverter extends BaseConverter { +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/K8sSyncHistoryConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/K8sSyncHistoryConverter.java new file mode 100644 index 00000000..380ac7ed --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/converter/K8sSyncHistoryConverter.java @@ -0,0 +1,10 @@ +package com.qqchen.deploy.backend.deploy.converter; + +import com.qqchen.deploy.backend.deploy.dto.K8sSyncHistoryDTO; +import com.qqchen.deploy.backend.deploy.entity.K8sSyncHistory; +import com.qqchen.deploy.backend.framework.converter.BaseConverter; +import org.mapstruct.Mapper; + +@Mapper(config = BaseConverter.class) +public interface K8sSyncHistoryConverter extends BaseConverter { +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/K8sDeploymentDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/K8sDeploymentDTO.java new file mode 100644 index 00000000..04e5de77 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/K8sDeploymentDTO.java @@ -0,0 +1,51 @@ +package com.qqchen.deploy.backend.deploy.dto; + +import com.qqchen.deploy.backend.framework.dto.BaseDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; +import java.util.Map; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "K8S Deployment DTO") +public class K8sDeploymentDTO extends BaseDTO { + + @Schema(description = "K8S集群ID") + private Long externalSystemId; + + @Schema(description = "命名空间ID") + private Long namespaceId; + + @Schema(description = "Deployment名称") + private String deploymentName; + + @Schema(description = "期望副本数") + private Integer replicas; + + @Schema(description = "可用副本数") + private Integer availableReplicas; + + @Schema(description = "就绪副本数") + private Integer readyReplicas; + + @Schema(description = "容器镜像") + private String image; + + @Schema(description = "标签") + private Map labels; + + @Schema(description = "选择器") + private Map selector; + + @Schema(description = "K8S中的创建时间") + private LocalDateTime k8sCreateTime; + + @Schema(description = "K8S中的更新时间") + private LocalDateTime k8sUpdateTime; + + @Schema(description = "YAML配置") + private String yamlConfig; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/K8sNamespaceDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/K8sNamespaceDTO.java new file mode 100644 index 00000000..ac957a26 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/K8sNamespaceDTO.java @@ -0,0 +1,32 @@ +package com.qqchen.deploy.backend.deploy.dto; + +import com.qqchen.deploy.backend.framework.dto.BaseDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Map; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "K8S命名空间DTO") +public class K8sNamespaceDTO extends BaseDTO { + + @Schema(description = "K8S集群ID") + private Long externalSystemId; + + @Schema(description = "命名空间名称") + private String namespaceName; + + @Schema(description = "状态") + private String status; + + @Schema(description = "标签") + private Map labels; + + @Schema(description = "YAML配置") + private String yamlConfig; + + @Schema(description = "Deployment数量(仅在列表和分页查询时填充)") + private Long deploymentCount; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/K8sSyncHistoryDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/K8sSyncHistoryDTO.java new file mode 100644 index 00000000..9cc33832 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/K8sSyncHistoryDTO.java @@ -0,0 +1,37 @@ +package com.qqchen.deploy.backend.deploy.dto; + +import com.qqchen.deploy.backend.deploy.enums.ExternalSystemSyncStatus; +import com.qqchen.deploy.backend.deploy.enums.K8sSyncType; +import com.qqchen.deploy.backend.framework.dto.BaseDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "K8S同步历史DTO") +public class K8sSyncHistoryDTO extends BaseDTO { + + @Schema(description = "同步编号") + private String number; + + @Schema(description = "同步类型") + private K8sSyncType syncType; + + @Schema(description = "同步状态") + private ExternalSystemSyncStatus status; + + @Schema(description = "开始时间") + private LocalDateTime startTime; + + @Schema(description = "结束时间") + private LocalDateTime endTime; + + @Schema(description = "错误信息") + private String errorMessage; + + @Schema(description = "K8S集群ID") + private Long externalSystemId; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ExternalSystem.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ExternalSystem.java index 9085a52d..ca0cb1f2 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ExternalSystem.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ExternalSystem.java @@ -82,10 +82,10 @@ public class ExternalSystem extends Entity { private LocalDateTime lastConnectTime; /** - * 系统特有配置,JSON格式 + * 系统特有配置(如kubeconfig等) + * 使用TEXT类型存储,支持任意格式的配置内容 */ - @Column(columnDefinition = "JSON") + @Column(columnDefinition = "TEXT") private String config; - } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/K8sDeployment.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/K8sDeployment.java new file mode 100644 index 00000000..f4d75f62 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/K8sDeployment.java @@ -0,0 +1,60 @@ +package com.qqchen.deploy.backend.deploy.entity; + +import com.qqchen.deploy.backend.framework.domain.Entity; +import com.vladmihalcea.hibernate.type.json.JsonType; +import jakarta.persistence.Column; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * K8S Deployment实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@jakarta.persistence.Entity +@Table(name = "deploy_k8s_deployment") +public class K8sDeployment extends Entity { + + @Column(name = "external_system_id", nullable = false) + private Long externalSystemId; + + @Column(name = "namespace_id", nullable = false) + private Long namespaceId; + + @Column(name = "deployment_name", nullable = false) + private String deploymentName; + + @Column(name = "replicas") + private Integer replicas; + + @Column(name = "available_replicas") + private Integer availableReplicas; + + @Column(name = "ready_replicas") + private Integer readyReplicas; + + @Column(name = "image") + private String image; + + @Type(JsonType.class) + @Column(name = "labels", columnDefinition = "JSON") + private Map labels; + + @Type(JsonType.class) + @Column(name = "selector", columnDefinition = "JSON") + private Map selector; + + @Column(name = "k8s_create_time") + private LocalDateTime k8sCreateTime; + + @Column(name = "k8s_update_time") + private LocalDateTime k8sUpdateTime; + + @Column(name = "yaml_config", columnDefinition = "TEXT") + private String yamlConfig; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/K8sNamespace.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/K8sNamespace.java new file mode 100644 index 00000000..6c33b461 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/K8sNamespace.java @@ -0,0 +1,37 @@ +package com.qqchen.deploy.backend.deploy.entity; + +import com.qqchen.deploy.backend.framework.domain.Entity; +import com.vladmihalcea.hibernate.type.json.JsonType; +import jakarta.persistence.Column; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; + +import java.util.Map; + +/** + * K8S命名空间实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@jakarta.persistence.Entity +@Table(name = "deploy_k8s_namespace") +public class K8sNamespace extends Entity { + + @Column(name = "external_system_id", nullable = false) + private Long externalSystemId; + + @Column(name = "namespace_name", nullable = false) + private String namespaceName; + + @Column(name = "status") + private String status; + + @Type(JsonType.class) + @Column(name = "labels", columnDefinition = "JSON") + private Map labels; + + @Column(name = "yaml_config", columnDefinition = "TEXT") + private String yamlConfig; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/K8sSyncHistory.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/K8sSyncHistory.java new file mode 100644 index 00000000..ee061818 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/K8sSyncHistory.java @@ -0,0 +1,48 @@ +package com.qqchen.deploy.backend.deploy.entity; + +import com.qqchen.deploy.backend.deploy.enums.ExternalSystemSyncStatus; +import com.qqchen.deploy.backend.deploy.enums.K8sSyncType; +import com.qqchen.deploy.backend.framework.annotation.LogicDelete; +import com.qqchen.deploy.backend.framework.domain.Entity; +import jakarta.persistence.Column; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * K8S同步历史实体 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@jakarta.persistence.Entity +@Table(name = "deploy_k8s_sync_history") +@LogicDelete +public class K8sSyncHistory extends Entity { + + @Column(name = "sync_history_number", nullable = false) + private String number; + + @Column(name = "sync_type", nullable = false) + @Enumerated(EnumType.STRING) + private K8sSyncType syncType; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private ExternalSystemSyncStatus status; + + @Column(name = "start_time", nullable = false) + private LocalDateTime startTime; + + @Column(name = "end_time") + private LocalDateTime endTime; + + @Column(name = "error_message", columnDefinition = "TEXT") + private String errorMessage; + + @Column(name = "external_system_id", nullable = false) + private Long externalSystemId; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/K8sSyncType.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/K8sSyncType.java new file mode 100644 index 00000000..b6dba361 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/K8sSyncType.java @@ -0,0 +1,6 @@ +package com.qqchen.deploy.backend.deploy.enums; + +public enum K8sSyncType { + NAMESPACE, // 同步命名空间 + DEPLOYMENT // 同步Deployment +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/IK8sServiceIntegration.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/IK8sServiceIntegration.java new file mode 100644 index 00000000..a686cb56 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/IK8sServiceIntegration.java @@ -0,0 +1,57 @@ +package com.qqchen.deploy.backend.deploy.integration; + +import com.qqchen.deploy.backend.deploy.entity.ExternalSystem; +import com.qqchen.deploy.backend.deploy.integration.response.K8sDeploymentResponse; +import com.qqchen.deploy.backend.deploy.integration.response.K8sNamespaceResponse; +import com.qqchen.deploy.backend.system.enums.ExternalSystemTypeEnum; + +import java.util.List; + +/** + * K8S集成服务接口 + */ +public interface IK8sServiceIntegration extends IExternalSystemIntegration { + + /** + * 测试K8S连接 + * + * @param system K8S系统配置 + * @return 连接是否成功 + */ + boolean testConnection(ExternalSystem system); + + /** + * 查询所有命名空间 + * + * @param externalSystem K8S系统配置 + * @return 命名空间列表 + */ + List listNamespaces(ExternalSystem externalSystem); + + /** + * 查询指定命名空间下的所有Deployment + * + * @param externalSystem K8S系统配置 + * @param namespace 命名空间名称 + * @return Deployment列表 + */ + List listDeployments(ExternalSystem externalSystem, String namespace); + + /** + * 查询所有Deployment(跨命名空间) + * + * @param externalSystem K8S系统配置 + * @return Deployment列表 + */ + List listAllDeployments(ExternalSystem externalSystem); + + /** + * 获取系统类型 + * + * @return K8S系统类型 + */ + @Override + default ExternalSystemTypeEnum getSystemType() { + return ExternalSystemTypeEnum.K8S; + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/impl/K8sServiceIntegrationImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/impl/K8sServiceIntegrationImpl.java new file mode 100644 index 00000000..f02b67e3 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/impl/K8sServiceIntegrationImpl.java @@ -0,0 +1,349 @@ +package com.qqchen.deploy.backend.deploy.integration.impl; + +import com.qqchen.deploy.backend.deploy.entity.ExternalSystem; +import com.qqchen.deploy.backend.framework.utils.JsonUtils; +import com.qqchen.deploy.backend.deploy.integration.IK8sServiceIntegration; +import com.qqchen.deploy.backend.deploy.integration.response.K8sDeploymentResponse; +import com.qqchen.deploy.backend.deploy.integration.response.K8sNamespaceResponse; +import com.qqchen.deploy.backend.framework.enums.ResponseCode; +import com.qqchen.deploy.backend.framework.exception.BusinessException; +import com.qqchen.deploy.backend.system.enums.ExternalSystemTypeEnum; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.AppsV1Api; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.apis.VersionApi; +import io.kubernetes.client.openapi.models.*; +import io.kubernetes.client.util.Config; +import io.kubernetes.client.util.Yaml; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.io.StringReader; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * K8S集成服务实现 + */ +@Slf4j +@Service +public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration implements IK8sServiceIntegration { + + // K8S ApiClient缓存 - 线程安全 + private static final Map API_CLIENT_CACHE = new ConcurrentHashMap<>(); + private static final long CACHE_EXPIRE_TIME = 30 * 60 * 1000; // 30分钟过期 + + /** + * K8S ApiClient缓存内部类 + */ + private static class K8sApiClientCache { + final ApiClient apiClient; + final long expireTime; + + K8sApiClientCache(ApiClient apiClient) { + this.apiClient = apiClient; + this.expireTime = System.currentTimeMillis() + CACHE_EXPIRE_TIME; + } + + boolean isExpired() { + return System.currentTimeMillis() > expireTime; + } + } + + /** + * 线程安全地获取K8S ApiClient缓存 + * 如果缓存不存在或已过期,会重新创建 + */ + private synchronized K8sApiClientCache getApiClientCache(ExternalSystem system) { + Long systemId = system.getId(); + K8sApiClientCache cache = API_CLIENT_CACHE.get(systemId); + + if (cache == null || cache.isExpired()) { + log.debug("K8S ApiClient缓存失效,重新创建: systemId={}", systemId); + + try { + ApiClient apiClient = createApiClientInternal(system); + cache = new K8sApiClientCache(apiClient); + API_CLIENT_CACHE.put(systemId, cache); + log.debug("K8S ApiClient缓存已更新: systemId={}, expireTime={}", systemId, cache.expireTime); + } catch (Exception e) { + log.error("创建K8S ApiClient失败: systemId={}", systemId, e); + throw new BusinessException(ResponseCode.K8S_CONNECTION_FAILED); + } + } + + return cache; + } + + @Override + public ExternalSystemTypeEnum getSystemType() { + return ExternalSystemTypeEnum.K8S; + } + + /** + * 测试K8S连接 + */ + @Override + public boolean testConnection(ExternalSystem system) { + log.info("测试K8S连接,集群: {}", system.getName()); + + try { + String config = system.getConfig(); + if (config == null || config.trim().isEmpty()) { + throw new BusinessException(ResponseCode.K8S_CONFIG_EMPTY); + } + + // 创建K8S ApiClient并测试连接(直接使用config作为kubeconfig) + ApiClient client = Config.fromConfig(new StringReader(config)); + client.setConnectTimeout(15000); // 15秒连接超时 + client.setReadTimeout(30000); // 30秒读取超时 + + VersionApi versionApi = new VersionApi(client); + VersionInfo version = versionApi.getCode(); + log.info("K8S集群连接成功,版本: {}", version.getGitVersion()); + + return true; + + } catch (Exception e) { + log.error("K8S连接测试失败,集群: {}, 错误: {}", system.getName(), e.getMessage(), e); + return false; + } + } + + /** + * 查询所有命名空间 + */ + @Override + public List listNamespaces(ExternalSystem externalSystem) { + log.info("查询K8S命名空间,集群: {}", externalSystem.getName()); + + try { + K8sApiClientCache cache = getApiClientCache(externalSystem); + CoreV1Api api = new CoreV1Api(cache.apiClient); + + V1NamespaceList namespaceList = api.listNamespace( + null, null, null, null, null, null, null, null, null, null + ); + + List namespaces = new ArrayList<>(); + for (V1Namespace ns : namespaceList.getItems()) { + K8sNamespaceResponse response = new K8sNamespaceResponse(); + response.setName(ns.getMetadata().getName()); + + if (ns.getStatus() != null && ns.getStatus().getPhase() != null) { + response.setStatus(ns.getStatus().getPhase()); + } + + response.setLabels(ns.getMetadata().getLabels()); + + if (ns.getMetadata().getCreationTimestamp() != null) { + response.setCreationTimestamp( + LocalDateTime.ofInstant( + ns.getMetadata().getCreationTimestamp().toInstant(), + ZoneId.systemDefault() + ) + ); + } + + // 序列化为YAML配置 + try { + response.setYamlConfig(Yaml.dump(ns)); + } catch (Exception e) { + log.warn("序列化Namespace为YAML失败: {}", ns.getMetadata().getName(), e); + } + + namespaces.add(response); + } + + log.info("查询到 {} 个命名空间", namespaces.size()); + return namespaces; + + } catch (Exception e) { + log.error("查询K8S命名空间失败,集群: {}, 错误: {}", externalSystem.getName(), e.getMessage(), e); + throw new BusinessException(ResponseCode.K8S_NAMESPACE_SYNC_FAILED); + } + } + + /** + * 查询指定命名空间下的所有Deployment + */ + @Override + public List listDeployments(ExternalSystem externalSystem, String namespace) { + log.info("查询K8S Deployment,集群: {}, 命名空间: {}", externalSystem.getName(), namespace); + + try { + K8sApiClientCache cache = getApiClientCache(externalSystem); + AppsV1Api api = new AppsV1Api(cache.apiClient); + + V1DeploymentList deploymentList = api.listNamespacedDeployment( + namespace, null, null, null, null, null, null, null, null, null, null + ); + + List deployments = new ArrayList<>(); + for (V1Deployment deployment : deploymentList.getItems()) { + K8sDeploymentResponse response = new K8sDeploymentResponse(); + response.setName(deployment.getMetadata().getName()); + response.setNamespace(deployment.getMetadata().getNamespace()); + + if (deployment.getSpec() != null) { + response.setReplicas(deployment.getSpec().getReplicas()); + } + + if (deployment.getStatus() != null) { + response.setAvailableReplicas(deployment.getStatus().getAvailableReplicas()); + response.setReadyReplicas(deployment.getStatus().getReadyReplicas()); + } + + response.setLabels(deployment.getMetadata().getLabels()); + + if (deployment.getSpec() != null && deployment.getSpec().getSelector() != null) { + response.setSelector(deployment.getSpec().getSelector().getMatchLabels()); + } + + // 获取第一个容器的镜像 + if (deployment.getSpec() != null + && deployment.getSpec().getTemplate() != null + && deployment.getSpec().getTemplate().getSpec() != null + && deployment.getSpec().getTemplate().getSpec().getContainers() != null + && !deployment.getSpec().getTemplate().getSpec().getContainers().isEmpty()) { + response.setImage(deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); + } + + if (deployment.getMetadata().getCreationTimestamp() != null) { + response.setCreationTimestamp( + LocalDateTime.ofInstant( + deployment.getMetadata().getCreationTimestamp().toInstant(), + ZoneId.systemDefault() + ) + ); + } + + // 序列化为YAML配置 + try { + response.setYamlConfig(Yaml.dump(deployment)); + } catch (Exception e) { + log.warn("序列化Deployment为YAML失败: {}", deployment.getMetadata().getName(), e); + } + + deployments.add(response); + } + + log.info("查询到 {} 个Deployment", deployments.size()); + return deployments; + + } catch (Exception e) { + log.error("查询K8S Deployment失败,集群: {}, 命名空间: {}, 错误: {}", + externalSystem.getName(), namespace, e.getMessage(), e); + throw new BusinessException(ResponseCode.K8S_DEPLOYMENT_SYNC_FAILED); + } + } + + /** + * 查询所有Deployment(跨命名空间) + */ + @Override + public List listAllDeployments(ExternalSystem externalSystem) { + log.info("查询所有K8S Deployment,集群: {}", externalSystem.getName()); + + try { + K8sApiClientCache cache = getApiClientCache(externalSystem); + AppsV1Api api = new AppsV1Api(cache.apiClient); + + V1DeploymentList deploymentList = api.listDeploymentForAllNamespaces( + null, null, null, null, null, null, null, null, null, null + ); + + List deployments = new ArrayList<>(); + for (V1Deployment deployment : deploymentList.getItems()) { + K8sDeploymentResponse response = new K8sDeploymentResponse(); + response.setName(deployment.getMetadata().getName()); + response.setNamespace(deployment.getMetadata().getNamespace()); + + if (deployment.getSpec() != null) { + response.setReplicas(deployment.getSpec().getReplicas()); + } + + if (deployment.getStatus() != null) { + response.setAvailableReplicas(deployment.getStatus().getAvailableReplicas()); + response.setReadyReplicas(deployment.getStatus().getReadyReplicas()); + } + + response.setLabels(deployment.getMetadata().getLabels()); + + if (deployment.getSpec() != null && deployment.getSpec().getSelector() != null) { + response.setSelector(deployment.getSpec().getSelector().getMatchLabels()); + } + + // 获取第一个容器的镜像 + if (deployment.getSpec() != null + && deployment.getSpec().getTemplate() != null + && deployment.getSpec().getTemplate().getSpec() != null + && deployment.getSpec().getTemplate().getSpec().getContainers() != null + && !deployment.getSpec().getTemplate().getSpec().getContainers().isEmpty()) { + response.setImage(deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); + } + + if (deployment.getMetadata().getCreationTimestamp() != null) { + response.setCreationTimestamp( + LocalDateTime.ofInstant( + deployment.getMetadata().getCreationTimestamp().toInstant(), + ZoneId.systemDefault() + ) + ); + } + + // 序列化为YAML配置 + try { + response.setYamlConfig(Yaml.dump(deployment)); + } catch (Exception e) { + log.warn("序列化Deployment为YAML失败: {}", deployment.getMetadata().getName(), e); + } + + deployments.add(response); + } + + log.info("查询到 {} 个Deployment", deployments.size()); + return deployments; + + } catch (Exception e) { + log.error("查询所有K8S Deployment失败,集群: {}, 错误: {}", externalSystem.getName(), e.getMessage(), e); + throw new BusinessException(ResponseCode.K8S_DEPLOYMENT_SYNC_FAILED); + } + } + + /** + * 创建K8S ApiClient(对外接口,使用缓存) + * + * @param externalSystem K8S系统配置 + * @return ApiClient + */ + private ApiClient createApiClient(ExternalSystem externalSystem) { + return getApiClientCache(externalSystem).apiClient; + } + + /** + * 创建K8S ApiClient(内部实现,不使用缓存) + * + * @param externalSystem K8S系统配置 + * @return ApiClient + */ + private ApiClient createApiClientInternal(ExternalSystem externalSystem) throws Exception { + String config = externalSystem.getConfig(); + + if (config == null || config.trim().isEmpty()) { + throw new BusinessException(ResponseCode.K8S_CONFIG_EMPTY); + } + + // 直接使用config作为kubeconfig内容 + ApiClient client = Config.fromConfig(new StringReader(config)); + client.setConnectTimeout(15000); // 15秒连接超时 + client.setReadTimeout(30000); // 30秒读取超时 + return client; + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/response/K8sDeploymentResponse.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/response/K8sDeploymentResponse.java new file mode 100644 index 00000000..cf6a437e --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/response/K8sDeploymentResponse.java @@ -0,0 +1,21 @@ +package com.qqchen.deploy.backend.deploy.integration.response; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Map; + +@Data +public class K8sDeploymentResponse { + private String name; + private String namespace; + private Integer replicas; + private Integer availableReplicas; + private Integer readyReplicas; + private String image; + private Map labels; + private Map selector; + private LocalDateTime creationTimestamp; + private LocalDateTime lastUpdateTime; + private String yamlConfig; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/response/K8sNamespaceResponse.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/response/K8sNamespaceResponse.java new file mode 100644 index 00000000..7ab9b375 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/response/K8sNamespaceResponse.java @@ -0,0 +1,15 @@ +package com.qqchen.deploy.backend.deploy.integration.response; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Map; + +@Data +public class K8sNamespaceResponse { + private String name; + private String status; + private Map labels; + private LocalDateTime creationTimestamp; + private String yamlConfig; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/K8sDeploymentQuery.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/K8sDeploymentQuery.java new file mode 100644 index 00000000..3ef8a6c2 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/K8sDeploymentQuery.java @@ -0,0 +1,24 @@ +package com.qqchen.deploy.backend.deploy.query; + +import com.qqchen.deploy.backend.framework.annotation.QueryField; +import com.qqchen.deploy.backend.framework.enums.QueryType; +import com.qqchen.deploy.backend.framework.query.BaseQuery; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class K8sDeploymentQuery extends BaseQuery { + + @QueryField(field = "externalSystemId") + private Long externalSystemId; + + @QueryField(field = "namespaceId") + private Long namespaceId; + + @QueryField(field = "deploymentName", type = QueryType.LIKE) + private String deploymentName; + + @QueryField(field = "image", type = QueryType.LIKE) + private String image; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/K8sNamespaceQuery.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/K8sNamespaceQuery.java new file mode 100644 index 00000000..7516096b --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/K8sNamespaceQuery.java @@ -0,0 +1,21 @@ +package com.qqchen.deploy.backend.deploy.query; + +import com.qqchen.deploy.backend.framework.annotation.QueryField; +import com.qqchen.deploy.backend.framework.enums.QueryType; +import com.qqchen.deploy.backend.framework.query.BaseQuery; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class K8sNamespaceQuery extends BaseQuery { + + @QueryField(field = "externalSystemId") + private Long externalSystemId; + + @QueryField(field = "namespaceName", type = QueryType.LIKE) + private String namespaceName; + + @QueryField(field = "status") + private String status; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/K8sSyncHistoryQuery.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/K8sSyncHistoryQuery.java new file mode 100644 index 00000000..48752e78 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/K8sSyncHistoryQuery.java @@ -0,0 +1,22 @@ +package com.qqchen.deploy.backend.deploy.query; + +import com.qqchen.deploy.backend.deploy.enums.ExternalSystemSyncStatus; +import com.qqchen.deploy.backend.deploy.enums.K8sSyncType; +import com.qqchen.deploy.backend.framework.annotation.QueryField; +import com.qqchen.deploy.backend.framework.query.BaseQuery; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class K8sSyncHistoryQuery extends BaseQuery { + + @QueryField(field = "externalSystemId") + private Long externalSystemId; + + @QueryField(field = "syncType") + private K8sSyncType syncType; + + @QueryField(field = "status") + private ExternalSystemSyncStatus status; +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IJenkinsJobRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IJenkinsJobRepository.java index 8e39969b..ad68d4c5 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IJenkinsJobRepository.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IJenkinsJobRepository.java @@ -26,6 +26,14 @@ public interface IJenkinsJobRepository extends IBaseRepository */ Optional findByExternalSystemIdAndViewIdAndJobName(Long externalSystemId, Long viewId, String jobName); + /** + * 根据外部系统ID查询所有任务 + * + * @param externalSystemId 外部系统ID + * @return 任务列表 + */ + List findByExternalSystemId(Long externalSystemId); + /** * 根据外部系统ID和视图ID查询所有任务 * diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IK8sDeploymentRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IK8sDeploymentRepository.java new file mode 100644 index 00000000..5d7f6057 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IK8sDeploymentRepository.java @@ -0,0 +1,29 @@ +package com.qqchen.deploy.backend.deploy.repository; + +import com.qqchen.deploy.backend.deploy.entity.K8sDeployment; +import com.qqchen.deploy.backend.framework.repository.IBaseRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +@Repository +public interface IK8sDeploymentRepository extends IBaseRepository { + + Optional findByNamespaceIdAndDeploymentName(Long namespaceId, String deploymentName); + + List findByExternalSystemId(Long externalSystemId); + + List findByNamespaceId(Long namespaceId); + + Long countByNamespaceIdAndDeletedFalse(Long namespaceId); + + @Query("SELECT d.namespaceId as namespaceId, COUNT(d.id) as count " + + "FROM K8sDeployment d " + + "WHERE d.namespaceId IN :namespaceIds AND d.deleted = false " + + "GROUP BY d.namespaceId") + List countByNamespaceIds(@Param("namespaceIds") Collection namespaceIds); +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IK8sNamespaceRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IK8sNamespaceRepository.java new file mode 100644 index 00000000..72b432cf --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IK8sNamespaceRepository.java @@ -0,0 +1,18 @@ +package com.qqchen.deploy.backend.deploy.repository; + +import com.qqchen.deploy.backend.deploy.entity.K8sNamespace; +import com.qqchen.deploy.backend.framework.repository.IBaseRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface IK8sNamespaceRepository extends IBaseRepository { + + Optional findByExternalSystemIdAndNamespaceName(Long externalSystemId, String namespaceName); + + List findByExternalSystemId(Long externalSystemId); + + Long countByExternalSystemIdAndDeletedFalse(Long externalSystemId); +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IK8sSyncHistoryRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IK8sSyncHistoryRepository.java new file mode 100644 index 00000000..98f9484d --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/repository/IK8sSyncHistoryRepository.java @@ -0,0 +1,13 @@ +package com.qqchen.deploy.backend.deploy.repository; + +import com.qqchen.deploy.backend.deploy.entity.K8sSyncHistory; +import com.qqchen.deploy.backend.framework.repository.IBaseRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface IK8sSyncHistoryRepository extends IBaseRepository { + + List findByExternalSystemIdOrderByCreateTimeDesc(Long externalSystemId); +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IK8sDeploymentService.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IK8sDeploymentService.java new file mode 100644 index 00000000..626f12f0 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IK8sDeploymentService.java @@ -0,0 +1,23 @@ +package com.qqchen.deploy.backend.deploy.service; + +import com.qqchen.deploy.backend.deploy.dto.K8sDeploymentDTO; +import com.qqchen.deploy.backend.deploy.entity.ExternalSystem; +import com.qqchen.deploy.backend.deploy.entity.K8sDeployment; +import com.qqchen.deploy.backend.deploy.entity.K8sNamespace; +import com.qqchen.deploy.backend.deploy.query.K8sDeploymentQuery; +import com.qqchen.deploy.backend.framework.service.IBaseService; + +import java.util.List; + +public interface IK8sDeploymentService extends IBaseService { + + Integer syncDeployments(ExternalSystem externalSystem, K8sNamespace namespace); + + void syncDeployments(Long externalSystemId); + + void syncDeployments(Long externalSystemId, Long namespaceId); + + List findByExternalSystemId(Long externalSystemId); + + List findByNamespaceId(Long namespaceId); +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IK8sNamespaceService.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IK8sNamespaceService.java new file mode 100644 index 00000000..b7853b09 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IK8sNamespaceService.java @@ -0,0 +1,18 @@ +package com.qqchen.deploy.backend.deploy.service; + +import com.qqchen.deploy.backend.deploy.dto.K8sNamespaceDTO; +import com.qqchen.deploy.backend.deploy.entity.ExternalSystem; +import com.qqchen.deploy.backend.deploy.entity.K8sNamespace; +import com.qqchen.deploy.backend.deploy.query.K8sNamespaceQuery; +import com.qqchen.deploy.backend.framework.service.IBaseService; + +import java.util.List; + +public interface IK8sNamespaceService extends IBaseService { + + Integer syncNamespaces(ExternalSystem externalSystem); + + void syncNamespaces(Long externalSystemId); + + List findByExternalSystemId(Long externalSystemId); +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IK8sSyncHistoryService.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IK8sSyncHistoryService.java new file mode 100644 index 00000000..8d2bb726 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IK8sSyncHistoryService.java @@ -0,0 +1,13 @@ +package com.qqchen.deploy.backend.deploy.service; + +import com.qqchen.deploy.backend.deploy.dto.K8sSyncHistoryDTO; +import com.qqchen.deploy.backend.deploy.entity.K8sSyncHistory; +import com.qqchen.deploy.backend.deploy.query.K8sSyncHistoryQuery; +import com.qqchen.deploy.backend.framework.service.IBaseService; + +import java.util.List; + +public interface IK8sSyncHistoryService extends IBaseService { + + List findByExternalSystemId(Long externalSystemId); +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/JenkinsJobServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/JenkinsJobServiceImpl.java index ba4dda54..56ec9a9b 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/JenkinsJobServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/JenkinsJobServiceImpl.java @@ -29,11 +29,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; -import java.util.ArrayList; -import java.util.List; -import java.util.OptionalDouble; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -150,21 +146,32 @@ public class JenkinsJobServiceImpl extends BaseServiceImpl allJobResponses = new LinkedHashMap<>(); + Map> jobViewMapping = new HashMap<>(); StringBuilder errorMessages = new StringBuilder(); + + // 阶段1:收集所有View的Job信息 for (JenkinsView view : views) { try { - Integer syncedJobs = syncJobsByView(externalSystem, view); - totalSyncedJobs += syncedJobs; - log.info("Successfully synchronized {} jobs for view: {}", syncedJobs, view.getViewName()); + List jobResponses = jenkinsServiceIntegration.listJobs(externalSystem, view.getViewName()); + for (JenkinsJobResponse jobResponse : jobResponses) { + String jobName = jobResponse.getName(); + // 保存Job信息(同一Job只保留一份) + allJobResponses.putIfAbsent(jobName, jobResponse); + // 记录Job-View关系 + jobViewMapping.computeIfAbsent(jobName, k -> new HashSet<>()).add(view.getId()); + } + log.debug("Collected {} jobs from view: {}", jobResponses.size(), view.getViewName()); } catch (Exception e) { - // 记录错误但继续同步其他视图 - String errorMessage = String.format("Failed to sync jobs for view %s: %s", view.getViewName(), e.getMessage()); + String errorMessage = String.format("Failed to fetch jobs for view %s: %s", view.getViewName(), e.getMessage()); log.error(errorMessage, e); errorMessages.append(errorMessage).append("\n"); } } + + // 阶段2:批量更新Job(每个Job只更新一次,避免并发冲突) + int totalSyncedJobs = syncJobsInBatch(externalSystem, allJobResponses, jobViewMapping); // 5. 更新同步历史状态 if (errorMessages.length() > 0) { @@ -185,6 +192,62 @@ public class JenkinsJobServiceImpl extends BaseServiceImpl allJobResponses, + Map> jobViewMapping) { + if (allJobResponses.isEmpty()) { + log.debug("No jobs to sync for external system: {}", externalSystem.getId()); + return 0; + } + + // 1. 查询所有现有Job(按externalSystemId,不按viewId) + List existingJobs = jenkinsJobRepository.findByExternalSystemId(externalSystem.getId()); + Map existingJobMap = existingJobs.stream() + .collect(Collectors.toMap(JenkinsJob::getJobName, Function.identity(), (old, newVal) -> old)); + + // 2. 批量更新/新增Job + List jobsToSave = new ArrayList<>(); + for (Map.Entry entry : allJobResponses.entrySet()) { + String jobName = entry.getKey(); + JenkinsJobResponse jobResponse = entry.getValue(); + Set viewIds = jobViewMapping.get(jobName); + + JenkinsJob jenkinsJob = existingJobMap.get(jobName); + if (jenkinsJob == null) { + // 新Job:使用第一个View作为主View + jenkinsJob = new JenkinsJob(); + jenkinsJob.setExternalSystemId(externalSystem.getId()); + jenkinsJob.setJobName(jobName); + jenkinsJob.setViewId(viewIds.iterator().next()); // 设置第一个View + log.debug("Creating new Jenkins job: {}", jobName); + } else { + // 已存在的Job:保持原viewId或更新为第一个View + if (!viewIds.contains(jenkinsJob.getViewId())) { + jenkinsJob.setViewId(viewIds.iterator().next()); + } + log.debug("Updating existing Jenkins job: {}", jobName); + } + + // 更新Job信息 + updateJobFromResponse(jenkinsJob, jobResponse); + jobsToSave.add(jenkinsJob); + } + + // 3. 批量保存 + jenkinsJobRepository.saveAll(jobsToSave); + + log.info("Successfully synchronized {} unique jobs (avoiding duplicates)", jobsToSave.size()); + return jobsToSave.size(); + } + /** * 同步指定视图下的Jenkins任务 * diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/K8sDeploymentServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/K8sDeploymentServiceImpl.java new file mode 100644 index 00000000..d10bff60 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/K8sDeploymentServiceImpl.java @@ -0,0 +1,226 @@ +package com.qqchen.deploy.backend.deploy.service.impl; + +import com.qqchen.deploy.backend.deploy.converter.K8sDeploymentConverter; +import com.qqchen.deploy.backend.deploy.dto.K8sDeploymentDTO; +import com.qqchen.deploy.backend.deploy.dto.K8sSyncHistoryDTO; +import com.qqchen.deploy.backend.deploy.entity.ExternalSystem; +import com.qqchen.deploy.backend.deploy.entity.K8sDeployment; +import com.qqchen.deploy.backend.deploy.entity.K8sNamespace; +import com.qqchen.deploy.backend.deploy.enums.ExternalSystemSyncStatus; +import com.qqchen.deploy.backend.deploy.enums.K8sSyncType; +import com.qqchen.deploy.backend.deploy.integration.IK8sServiceIntegration; +import com.qqchen.deploy.backend.deploy.integration.response.K8sDeploymentResponse; +import com.qqchen.deploy.backend.deploy.query.K8sDeploymentQuery; +import com.qqchen.deploy.backend.deploy.repository.IExternalSystemRepository; +import com.qqchen.deploy.backend.deploy.repository.IK8sDeploymentRepository; +import com.qqchen.deploy.backend.deploy.repository.IK8sNamespaceRepository; +import com.qqchen.deploy.backend.deploy.service.IK8sDeploymentService; +import com.qqchen.deploy.backend.deploy.service.IK8sSyncHistoryService; +import com.qqchen.deploy.backend.framework.enums.ResponseCode; +import com.qqchen.deploy.backend.framework.exception.BusinessException; +import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +@Slf4j +@Service +public class K8sDeploymentServiceImpl extends BaseServiceImpl + implements IK8sDeploymentService { + + @Resource + private IK8sDeploymentRepository k8sDeploymentRepository; + + @Resource + private IK8sNamespaceRepository k8sNamespaceRepository; + + @Resource + private IExternalSystemRepository externalSystemRepository; + + @Resource + private K8sDeploymentConverter k8sDeploymentConverter; + + @Resource + private IK8sServiceIntegration k8sServiceIntegration; + + @Resource + private IK8sSyncHistoryService k8sSyncHistoryService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Integer syncDeployments(ExternalSystem externalSystem, K8sNamespace namespace) { + log.info("开始同步K8S Deployment,集群: {}, Namespace: {}", externalSystem.getName(), namespace.getNamespaceName()); + + try { + List deploymentResponses = k8sServiceIntegration.listDeployments( + externalSystem, namespace.getNamespaceName() + ); + + // 性能优化:批量查询现有数据,避免N+1问题 + List existingDeployments = k8sDeploymentRepository.findByNamespaceId(namespace.getId()); + Map existingMap = existingDeployments.stream() + .collect(java.util.stream.Collectors.toMap(K8sDeployment::getDeploymentName, d -> d)); + + // 收集K8S中存在的Deployment名称 + Set k8sDeploymentNames = deploymentResponses.stream() + .map(K8sDeploymentResponse::getName) + .collect(java.util.stream.Collectors.toSet()); + + List deploymentsToSave = new ArrayList<>(); + + // 1. 更新/新增K8S中存在的资源 + for (K8sDeploymentResponse response : deploymentResponses) { + K8sDeployment deployment = existingMap.get(response.getName()); + if (deployment == null) { + deployment = new K8sDeployment(); + deployment.setExternalSystemId(externalSystem.getId()); + deployment.setNamespaceId(namespace.getId()); + deployment.setDeploymentName(response.getName()); + } else { + // 如果之前被软删除,恢复它 + deployment.setDeleted(false); + } + + deployment.setReplicas(response.getReplicas()); + deployment.setAvailableReplicas(response.getAvailableReplicas()); + deployment.setReadyReplicas(response.getReadyReplicas()); + deployment.setImage(response.getImage()); + deployment.setLabels(response.getLabels()); + deployment.setSelector(response.getSelector()); + deployment.setK8sCreateTime(response.getCreationTimestamp()); + deployment.setK8sUpdateTime(response.getLastUpdateTime()); + deployment.setYamlConfig(response.getYamlConfig()); + deploymentsToSave.add(deployment); + } + + // 2. 软删除K8S中不存在但数据库中存在的资源 + for (K8sDeployment existing : existingDeployments) { + if (!k8sDeploymentNames.contains(existing.getDeploymentName()) && !existing.getDeleted()) { + existing.setDeleted(true); + deploymentsToSave.add(existing); + log.info("标记删除Deployment: {}", existing.getDeploymentName()); + } + } + + // 性能优化:批量保存 + k8sDeploymentRepository.saveAll(deploymentsToSave); + int syncCount = (int) deploymentResponses.size(); + + log.info("K8S Deployment同步完成,集群: {}, Namespace: {}, 同步数量: {}", + externalSystem.getName(), namespace.getNamespaceName(), syncCount); + return syncCount; + + } catch (Exception e) { + log.error("K8S Deployment同步失败,集群: {}, Namespace: {}, 错误: {}", + externalSystem.getName(), namespace.getNamespaceName(), e.getMessage(), e); + throw new BusinessException(ResponseCode.K8S_DEPLOYMENT_SYNC_FAILED); + } + } + + @Override + @Async("k8sTaskExecutor") + @Transactional(rollbackFor = Exception.class) + public void syncDeployments(Long externalSystemId) { + log.info("异步同步K8S Deployment(所有命名空间),集群ID: {}", externalSystemId); + + K8sSyncHistoryDTO syncHistory = new K8sSyncHistoryDTO(); + syncHistory.setExternalSystemId(externalSystemId); + syncHistory.setSyncType(K8sSyncType.DEPLOYMENT); + syncHistory.setStatus(ExternalSystemSyncStatus.RUNNING); + syncHistory.setStartTime(LocalDateTime.now()); + syncHistory.setNumber("K8S-DEPLOY-SYNC-" + System.currentTimeMillis()); + + syncHistory = k8sSyncHistoryService.create(syncHistory); + + try { + ExternalSystem externalSystem = externalSystemRepository.findById(externalSystemId) + .orElseThrow(() -> new BusinessException(ResponseCode.K8S_CLUSTER_NOT_FOUND)); + + List namespaces = k8sNamespaceRepository.findByExternalSystemId(externalSystemId); + + int totalSyncCount = 0; + for (K8sNamespace namespace : namespaces) { + int count = syncDeployments(externalSystem, namespace); + totalSyncCount += count; + } + + syncHistory.setStatus(ExternalSystemSyncStatus.SUCCESS); + syncHistory.setEndTime(LocalDateTime.now()); + k8sSyncHistoryService.update(syncHistory.getId(), syncHistory); + + log.info("K8S Deployment异步同步成功,集群ID: {}, 总同步数量: {}", externalSystemId, totalSyncCount); + + } catch (Exception e) { + log.error("K8S Deployment异步同步失败,集群ID: {}, 错误: {}", externalSystemId, e.getMessage(), e); + + syncHistory.setStatus(ExternalSystemSyncStatus.FAILED); + syncHistory.setEndTime(LocalDateTime.now()); + syncHistory.setErrorMessage(e.getMessage()); + k8sSyncHistoryService.update(syncHistory.getId(), syncHistory); + } + } + + @Override + @Async("k8sTaskExecutor") + @Transactional(rollbackFor = Exception.class) + public void syncDeployments(Long externalSystemId, Long namespaceId) { + log.info("异步同步K8S Deployment,集群ID: {}, 命名空间ID: {}", externalSystemId, namespaceId); + + K8sSyncHistoryDTO syncHistory = new K8sSyncHistoryDTO(); + syncHistory.setExternalSystemId(externalSystemId); + syncHistory.setSyncType(K8sSyncType.DEPLOYMENT); + syncHistory.setStatus(ExternalSystemSyncStatus.RUNNING); + syncHistory.setStartTime(LocalDateTime.now()); + syncHistory.setNumber("K8S-DEPLOY-SYNC-" + System.currentTimeMillis()); + + syncHistory = k8sSyncHistoryService.create(syncHistory); + + try { + ExternalSystem externalSystem = externalSystemRepository.findById(externalSystemId) + .orElseThrow(() -> new BusinessException(ResponseCode.K8S_CLUSTER_NOT_FOUND)); + + K8sNamespace namespace = k8sNamespaceRepository.findById(namespaceId) + .orElseThrow(() -> new BusinessException(ResponseCode.K8S_NAMESPACE_NOT_FOUND)); + + int syncCount = syncDeployments(externalSystem, namespace); + + syncHistory.setStatus(ExternalSystemSyncStatus.SUCCESS); + syncHistory.setEndTime(LocalDateTime.now()); + k8sSyncHistoryService.update(syncHistory.getId(), syncHistory); + + log.info("K8S Deployment异步同步成功,集群ID: {}, 命名空间ID: {}, 同步数量: {}", + externalSystemId, namespaceId, syncCount); + + } catch (Exception e) { + log.error("K8S Deployment异步同步失败,集群ID: {}, 命名空间ID: {}, 错误: {}", + externalSystemId, namespaceId, e.getMessage(), e); + + syncHistory.setStatus(ExternalSystemSyncStatus.FAILED); + syncHistory.setEndTime(LocalDateTime.now()); + syncHistory.setErrorMessage(e.getMessage()); + k8sSyncHistoryService.update(syncHistory.getId(), syncHistory); + } + } + + @Override + public List findByExternalSystemId(Long externalSystemId) { + List deployments = k8sDeploymentRepository.findByExternalSystemId(externalSystemId); + return k8sDeploymentConverter.toDtoList(deployments); + } + + @Override + public List findByNamespaceId(Long namespaceId) { + List deployments = k8sDeploymentRepository.findByNamespaceId(namespaceId); + return k8sDeploymentConverter.toDtoList(deployments); + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/K8sNamespaceServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/K8sNamespaceServiceImpl.java new file mode 100644 index 00000000..d6aff740 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/K8sNamespaceServiceImpl.java @@ -0,0 +1,194 @@ +package com.qqchen.deploy.backend.deploy.service.impl; + +import com.qqchen.deploy.backend.deploy.converter.K8sNamespaceConverter; +import com.qqchen.deploy.backend.deploy.dto.K8sNamespaceDTO; +import com.qqchen.deploy.backend.deploy.dto.K8sSyncHistoryDTO; +import com.qqchen.deploy.backend.deploy.entity.ExternalSystem; +import com.qqchen.deploy.backend.deploy.entity.K8sNamespace; +import com.qqchen.deploy.backend.deploy.enums.ExternalSystemSyncStatus; +import com.qqchen.deploy.backend.deploy.enums.K8sSyncType; +import com.qqchen.deploy.backend.deploy.integration.IK8sServiceIntegration; +import com.qqchen.deploy.backend.deploy.integration.response.K8sNamespaceResponse; +import com.qqchen.deploy.backend.deploy.query.K8sNamespaceQuery; +import com.qqchen.deploy.backend.deploy.repository.IExternalSystemRepository; +import com.qqchen.deploy.backend.deploy.repository.IK8sNamespaceRepository; +import com.qqchen.deploy.backend.deploy.service.IK8sNamespaceService; +import com.qqchen.deploy.backend.deploy.service.IK8sSyncHistoryService; +import com.qqchen.deploy.backend.framework.enums.ResponseCode; +import com.qqchen.deploy.backend.framework.exception.BusinessException; +import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +@Slf4j +@Service +public class K8sNamespaceServiceImpl extends BaseServiceImpl + implements IK8sNamespaceService { + + @Resource + private IK8sNamespaceRepository k8sNamespaceRepository; + + @Resource + private IExternalSystemRepository externalSystemRepository; + + @Resource + private K8sNamespaceConverter k8sNamespaceConverter; + + @Resource + private IK8sServiceIntegration k8sServiceIntegration; + + @Resource + private IK8sSyncHistoryService k8sSyncHistoryService; + + @Resource + private com.qqchen.deploy.backend.deploy.repository.IK8sDeploymentRepository k8sDeploymentRepository; + + @Override + @Transactional(rollbackFor = Exception.class) + public Integer syncNamespaces(ExternalSystem externalSystem) { + log.info("开始同步K8S命名空间,集群: {}", externalSystem.getName()); + + try { + List namespaceResponses = k8sServiceIntegration.listNamespaces(externalSystem); + + // 性能优化:批量查询现有数据,避免N+1问题 + List existingNamespaces = k8sNamespaceRepository.findByExternalSystemId(externalSystem.getId()); + Map existingMap = existingNamespaces.stream() + .collect(java.util.stream.Collectors.toMap(K8sNamespace::getNamespaceName, ns -> ns)); + + // 收集K8S中存在的命名空间名称 + Set k8sNamespaceNames = namespaceResponses.stream() + .map(K8sNamespaceResponse::getName) + .collect(java.util.stream.Collectors.toSet()); + + List namespacesToSave = new ArrayList<>(); + + // 1. 更新/新增K8S中存在的资源 + for (K8sNamespaceResponse response : namespaceResponses) { + K8sNamespace namespace = existingMap.get(response.getName()); + if (namespace == null) { + namespace = new K8sNamespace(); + namespace.setExternalSystemId(externalSystem.getId()); + namespace.setNamespaceName(response.getName()); + } else { + // 如果之前被软删除,恢复它 + namespace.setDeleted(false); + } + + namespace.setStatus(response.getStatus()); + namespace.setLabels(response.getLabels()); + namespace.setYamlConfig(response.getYamlConfig()); + namespacesToSave.add(namespace); + } + + // 2. 软删除K8S中不存在但数据库中存在的资源 + for (K8sNamespace existing : existingNamespaces) { + if (!k8sNamespaceNames.contains(existing.getNamespaceName()) && !existing.getDeleted()) { + existing.setDeleted(true); + namespacesToSave.add(existing); + log.info("标记删除命名空间: {}", existing.getNamespaceName()); + } + } + + // 性能优化:批量保存 + k8sNamespaceRepository.saveAll(namespacesToSave); + int syncCount = (int) namespaceResponses.size(); + + log.info("K8S命名空间同步完成,集群: {}, 同步数量: {}", externalSystem.getName(), syncCount); + return syncCount; + + } catch (Exception e) { + log.error("K8S命名空间同步失败,集群: {}, 错误: {}", externalSystem.getName(), e.getMessage(), e); + throw new BusinessException(ResponseCode.K8S_NAMESPACE_SYNC_FAILED); + } + } + + @Override + @Async("k8sTaskExecutor") + @Transactional(rollbackFor = Exception.class) + public void syncNamespaces(Long externalSystemId) { + log.info("异步同步K8S命名空间,集群ID: {}", externalSystemId); + + K8sSyncHistoryDTO syncHistory = new K8sSyncHistoryDTO(); + syncHistory.setExternalSystemId(externalSystemId); + syncHistory.setSyncType(K8sSyncType.NAMESPACE); + syncHistory.setStatus(ExternalSystemSyncStatus.RUNNING); + syncHistory.setStartTime(LocalDateTime.now()); + syncHistory.setNumber("K8S-NS-SYNC-" + System.currentTimeMillis()); + + syncHistory = k8sSyncHistoryService.create(syncHistory); + + try { + ExternalSystem externalSystem = externalSystemRepository.findById(externalSystemId) + .orElseThrow(() -> new BusinessException(ResponseCode.K8S_CLUSTER_NOT_FOUND)); + + int syncCount = syncNamespaces(externalSystem); + + syncHistory.setStatus(ExternalSystemSyncStatus.SUCCESS); + syncHistory.setEndTime(LocalDateTime.now()); + k8sSyncHistoryService.update(syncHistory.getId(), syncHistory); + + log.info("K8S命名空间异步同步成功,集群ID: {}, 同步数量: {}", externalSystemId, syncCount); + + } catch (Exception e) { + log.error("K8S命名空间异步同步失败,集群ID: {}, 错误: {}", externalSystemId, e.getMessage(), e); + + syncHistory.setStatus(ExternalSystemSyncStatus.FAILED); + syncHistory.setEndTime(LocalDateTime.now()); + syncHistory.setErrorMessage(e.getMessage()); + k8sSyncHistoryService.update(syncHistory.getId(), syncHistory); + } + } + + @Override + public List findByExternalSystemId(Long externalSystemId) { + List namespaces = k8sNamespaceRepository.findByExternalSystemId(externalSystemId); + return k8sNamespaceConverter.toDtoList(namespaces); + } + + @Override + public org.springframework.data.domain.Page page(K8sNamespaceQuery query) { + org.springframework.data.domain.Page page = super.page(query); + fillDeploymentCounts(page.getContent()); + return page; + } + + @Override + public List findAll(K8sNamespaceQuery query) { + List list = super.findAll(query); + fillDeploymentCounts(list); + return list; + } + + private void fillDeploymentCounts(List namespaces) { + if (namespaces.isEmpty()) { + return; + } + + List namespaceIds = namespaces.stream() + .map(K8sNamespaceDTO::getId) + .collect(java.util.stream.Collectors.toList()); + + List countResults = k8sDeploymentRepository.countByNamespaceIds(namespaceIds); + Map deploymentCountMap = countResults.stream() + .collect(java.util.stream.Collectors.toMap( + arr -> (Long) arr[0], + arr -> (Long) arr[1] + )); + + namespaces.forEach(namespace -> { + Long deploymentCount = deploymentCountMap.getOrDefault(namespace.getId(), 0L); + namespace.setDeploymentCount(deploymentCount); + }); + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/K8sSyncHistoryServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/K8sSyncHistoryServiceImpl.java new file mode 100644 index 00000000..e5efea1c --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/K8sSyncHistoryServiceImpl.java @@ -0,0 +1,32 @@ +package com.qqchen.deploy.backend.deploy.service.impl; + +import com.qqchen.deploy.backend.deploy.converter.K8sSyncHistoryConverter; +import com.qqchen.deploy.backend.deploy.dto.K8sSyncHistoryDTO; +import com.qqchen.deploy.backend.deploy.entity.K8sSyncHistory; +import com.qqchen.deploy.backend.deploy.query.K8sSyncHistoryQuery; +import com.qqchen.deploy.backend.deploy.repository.IK8sSyncHistoryRepository; +import com.qqchen.deploy.backend.deploy.service.IK8sSyncHistoryService; +import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +public class K8sSyncHistoryServiceImpl extends BaseServiceImpl + implements IK8sSyncHistoryService { + + @Resource + private IK8sSyncHistoryRepository k8sSyncHistoryRepository; + + @Resource + private K8sSyncHistoryConverter k8sSyncHistoryConverter; + + @Override + public List findByExternalSystemId(Long externalSystemId) { + List histories = k8sSyncHistoryRepository.findByExternalSystemIdOrderByCreateTimeDesc(externalSystemId); + return k8sSyncHistoryConverter.toDtoList(histories); + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java index fbfd9555..969515bb 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java @@ -270,7 +270,26 @@ public enum ResponseCode { JENKINS_QUEUE_TIMEOUT(3205, "jenkins.queue.timeout"), JENKINS_BUILD_TIMEOUT(3206, "jenkins.build.timeout"), JENKINS_API_ERROR(3207, "jenkins.api.error"), - JENKINS_RESPONSE_PARSE_ERROR(3208, "jenkins.response.parse.error"); + JENKINS_RESPONSE_PARSE_ERROR(3208, "jenkins.response.parse.error"), + + // K8S集成错误码 (3220-3239) + K8S_CLUSTER_NOT_FOUND(3220, "k8s.cluster.not.found"), + K8S_CONNECTION_FAILED(3221, "k8s.connection.failed"), + K8S_AUTH_FAILED(3222, "k8s.auth.failed"), + K8S_CONFIG_INVALID(3223, "k8s.config.invalid"), + K8S_CONFIG_EMPTY(3224, "k8s.config.empty"), + K8S_KUBECONFIG_INVALID(3225, "k8s.kubeconfig.invalid"), + K8S_KUBECONFIG_EMPTY(3226, "k8s.kubeconfig.empty"), + K8S_NAMESPACE_NOT_FOUND(3227, "k8s.namespace.not.found"), + K8S_DEPLOYMENT_NOT_FOUND(3228, "k8s.deployment.not.found"), + K8S_API_ERROR(3229, "k8s.api.error"), + K8S_SERVER_ERROR(3230, "k8s.server.error"), + K8S_SYNC_FAILED(3231, "k8s.sync.failed"), + K8S_NAMESPACE_SYNC_FAILED(3232, "k8s.namespace.sync.failed"), + K8S_DEPLOYMENT_SYNC_FAILED(3233, "k8s.deployment.sync.failed"), + K8S_POD_NOT_FOUND(3234, "k8s.pod.not.found"), + K8S_RESOURCE_NOT_FOUND(3235, "k8s.resource.not.found"), + K8S_OPERATION_FAILED(3236, "k8s.operation.failed"); private final int code; private final String messageKey; // 国际化消息key diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/enums/ExternalSystemAuthTypeEnum.java b/backend/src/main/java/com/qqchen/deploy/backend/system/enums/ExternalSystemAuthTypeEnum.java index aa000bba..4db0fc4c 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/system/enums/ExternalSystemAuthTypeEnum.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/enums/ExternalSystemAuthTypeEnum.java @@ -6,5 +6,6 @@ package com.qqchen.deploy.backend.system.enums; public enum ExternalSystemAuthTypeEnum { BASIC, TOKEN, - OAUTH + OAUTH, + KUBECONFIG } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/enums/ExternalSystemTypeEnum.java b/backend/src/main/java/com/qqchen/deploy/backend/system/enums/ExternalSystemTypeEnum.java index f58e6faa..a06f7931 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/system/enums/ExternalSystemTypeEnum.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/enums/ExternalSystemTypeEnum.java @@ -6,5 +6,6 @@ package com.qqchen.deploy.backend.system.enums; public enum ExternalSystemTypeEnum { JENKINS, GIT, - ZENTAO + ZENTAO, + K8S } \ No newline at end of file diff --git a/backend/src/main/resources/db/changelog/init/v1.0.0-data.sql b/backend/src/main/resources/db/changelog/init/v1.0.0-data.sql index a7ab8539..c506bb82 100644 --- a/backend/src/main/resources/db/changelog/init/v1.0.0-data.sql +++ b/backend/src/main/resources/db/changelog/init/v1.0.0-data.sql @@ -84,8 +84,10 @@ VALUES (302, 'Jenkins管理', '/resource/jenkins', 'Resource/Jenkins/List', 'BuildOutlined', 'resource:jenkins', 2, 300, 2, FALSE, TRUE, 'system', NOW(), 0, FALSE), -- Git管理 (303, 'Git管理', '/resource/git', 'Resource/Git/List', 'GithubOutlined', 'resource:git', 2, 300, 3, FALSE, TRUE, 'system', NOW(), 0, FALSE), +-- K8S管理 +(304, 'K8S管理', '/resource/k8s', 'Resource/K8s/List', 'CloudOutlined', 'resource:k8s', 2, 300, 4, FALSE, TRUE, 'system', NOW(), 0, FALSE), -- 三方系统管理 -(304, '三方系统管理', '/resource/external', 'Resource/External/List', 'ApiOutlined', 'resource:external', 2, 300, 4, FALSE, TRUE, 'system', NOW(), 0, FALSE), +(305, '三方系统管理', '/resource/external', 'Resource/External/List', 'ApiOutlined', 'resource:external', 2, 300, 5, FALSE, TRUE, 'system', NOW(), 0, FALSE), -- 系统管理 (1, '系统管理', '/system', NULL, 'SettingOutlined', NULL, 1, NULL, 99, FALSE, TRUE, 'system', NOW(), 0, FALSE), @@ -154,9 +156,9 @@ VALUES INSERT INTO sys_role_menu (role_id, menu_id) VALUES -- 管理员角色(拥有所有菜单) -(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (1, 99), (1, 100), (1, 101), (1, 102), (1, 104), (1, 200), (1, 201), (1, 202), (1, 203), (1, 204), (1, 205), (1, 206), (1, 300), (1, 301), (1, 302), (1, 303), (1, 304), (1, 1011), (1, 1041), (1, 1042), +(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (1, 99), (1, 100), (1, 101), (1, 102), (1, 104), (1, 200), (1, 201), (1, 202), (1, 203), (1, 204), (1, 205), (1, 206), (1, 300), (1, 301), (1, 302), (1, 303), (1, 304), (1, 305), (1, 1011), (1, 1041), (1, 1042), -- 运维角色 -(2, 99), (2, 200), (2, 201), (2, 202), (2, 203), (2, 204), (2, 205), (2, 300), (2, 301), (2, 302), (2, 303), (2, 304), +(2, 99), (2, 200), (2, 201), (2, 202), (2, 203), (2, 204), (2, 205), (2, 300), (2, 301), (2, 302), (2, 303), (2, 304), (2, 305), -- 开发角色(只有工作台) (3, 99); @@ -297,13 +299,23 @@ INSERT INTO sys_permission (id, create_time, menu_id, code, name, type, sort) VA (222, NOW(), 303, 'resource:repository-project:view', 'Git项目详情', 'FUNCTION', 2), (223, NOW(), 303, 'resource:repository-project:sync', '同步Git项目', 'FUNCTION', 3), --- 三方系统管理 (menu_id=304) -(231, NOW(), 304, 'resource:external:list', '三方系统查询', 'FUNCTION', 1), -(232, NOW(), 304, 'resource:external:view', '三方系统详情', 'FUNCTION', 2), -(233, NOW(), 304, 'resource:external:create', '三方系统创建', 'FUNCTION', 3), -(234, NOW(), 304, 'resource:external:update', '三方系统修改', 'FUNCTION', 4), -(235, NOW(), 304, 'resource:external:delete', '三方系统删除', 'FUNCTION', 5), -(236, NOW(), 304, 'resource:external:test-connection', '测试连接', 'FUNCTION', 6), +-- K8S管理 (menu_id=304) +(231, NOW(), 304, 'resource:k8s-namespace:list', 'K8S命名空间查询', 'FUNCTION', 1), +(232, NOW(), 304, 'resource:k8s-namespace:view', 'K8S命名空间详情', 'FUNCTION', 2), +(233, NOW(), 304, 'resource:k8s-namespace:sync', '同步K8S命名空间', 'FUNCTION', 3), +(234, NOW(), 304, 'resource:k8s-deployment:list', 'K8S Deployment查询', 'FUNCTION', 4), +(235, NOW(), 304, 'resource:k8s-deployment:view', 'K8S Deployment详情', 'FUNCTION', 5), +(236, NOW(), 304, 'resource:k8s-deployment:sync', '同步K8S Deployment', 'FUNCTION', 6), +(237, NOW(), 304, 'resource:k8s-sync-history:list', 'K8S同步历史查询', 'FUNCTION', 7), +(238, NOW(), 304, 'resource:k8s-sync-history:view', 'K8S同步历史详情', 'FUNCTION', 8), + +-- 三方系统管理 (menu_id=305) +(241, NOW(), 305, 'resource:external:list', '三方系统查询', 'FUNCTION', 1), +(242, NOW(), 305, 'resource:external:view', '三方系统详情', 'FUNCTION', 2), +(243, NOW(), 305, 'resource:external:create', '三方系统创建', 'FUNCTION', 3), +(244, NOW(), 305, 'resource:external:update', '三方系统修改', 'FUNCTION', 4), +(245, NOW(), 305, 'resource:external:delete', '三方系统删除', 'FUNCTION', 5), +(246, NOW(), 305, 'resource:external:test-connection', '测试连接', 'FUNCTION', 6), -- 工作流管理权限 -- 工作流设计 (menu_id=101) @@ -611,6 +623,8 @@ INSERT INTO sys_external_system ( 'TOKEN', 'admin', '1efa55a082da05c7b744ab09aa819f3950627215d0352e0f0197880ec2c9f75b', '9aa1357bf0823ee2027818ff07a850d9bd82fc07d62e03ad42250a6dce9c6417c5cb01caf65f5e77b85f2c4936fc1209', NULL, NULL, NOW(), NULL); +INSERT INTO `deploy-ease-platform`.`sys_external_system` (`id`, `create_by`, `create_time`, `deleted`, `update_by`, `update_time`, `version`, `name`, `type`, `url`, `remark`, `sort`, `enabled`, `auth_type`, `username`, `password`, `token`, `sync_status`, `last_sync_time`, `last_connect_time`, `config`) VALUES (7, 'qc-admin', NOW(), b'0', 'qc-admin', NOW(), 1, '链宇K8S', 'K8S', 'https://172.16.0.207:5443', NULL, 1, b'1', 'KUBECONFIG', 'admin', 'c1d87c01fa197d7d2f6d768510365cb67074a70b9263e236dfd2686f80b81b98', NULL, NULL, NULL, NULL, 'kind: Config\napiVersion: v1\npreferences: {}\nclusters:\n - name: internalCluster\n cluster:\n server: https://172.16.0.207:5443\n certificate-authority-data: >-\n LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUREekNDQWZlZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFwTVJrd0Z3WURWUVFLRXhCRFEwVWcKVkdWamFHNXZiRzluYVdWek1Rd3dDZ1lEVlFRREV3TkRRMFV3SGhjTk1qVXdOREUwTURjMU1EVXpXaGNOTkRVdwpOREUwTURjMU1EVXpXakFwTVJrd0Z3WURWUVFLRXhCRFEwVWdWR1ZqYUc1dmJHOW5hV1Z6TVF3d0NnWURWUVFECkV3TkRRMFV3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRREpQSU14M3NVUnhzeWQKcGhoYXdMWnBPOVE0WUxGVnJSQS9WWmhQUHFUdDhkVmZ3NU5mZ0ZNN3plcmdteEpMWUd0eGlHdXg0M3AwVFoxeApxZ1g0WFFqQUUwd24xTXZVQW13dlhFZ0ZFanlVUmRpSFZXNWF3OC95angxckI5b0dQMTdMUm14dHJNWmNUT1lxCnAyL1ZIQmZvWEZ5WUk2REk0U1pWOWR5SlIwNlZvdkF5cWphNm05ZmpmelVPc1pRd3p3aWluazh6OXBKNXJoOVgKOEV4UTRUc0FGSzhIcTFoOVJublRNNkIvbUJwRUJrSndnTGxqcjhXUjU0U0xkbGJpUFVmU1NobDVGck5rcUpsSgp4ekg2VUgwdW1iMklkL0JzQ2pBTmlhYUR3V0VZUUlJVUUxd2kySmEzRkFpTFhOR3FiQzBEL3dtNWd6WldZWlpQCnRkdVdQYUhOQWdNQkFBR2pRakJBTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQQmdOVkhSTUJBZjhFQlRBREFRSC8KTUIwR0ExVWREZ1FXQkJSMjE0c2ZidzFNNFRHWDBBZmF5YzlnZFhXK0NUQU5CZ2txaGtpRzl3MEJBUXNGQUFPQwpBUUVBc3FUVGJHN3VFWGNObThNODY1ZG4zemNnK1F1YWNkampSNXpLSWE1STA4R1RqTGRFMCs4RjVLU2Fmc1ZzCmI4cXFXVmNyaGU2Tk5Ea1ZmUVBuTEYvbmcxQjNZL2JNVEM5RzZjV3pid01DclpsM2p5ZHAzQzRhQzFWTUVYdnAKRzVqYlhHWmVsa1YydnlsWG1YYXN4OHkvWW1ySFBCY0dQVVBSOEpZWVdoWVpjVFVKOXduSys4R1VBTGU5YmRXeAp3RGFucWI4RG5Ib1NhWUtETkJLNUpMZlJOZTI3Yzd5YXdJMyt4eDdGZWFxTnh5MGdzNzlmdDlnRThmTThHcFE2CkNPWlNEUVNIdE4zbjdSdGlHZUF4RHBnUjc0RE1mRXZZaFkwUXVJOGhMZllWVngrWFlBaC9HdlhSSmhURXBXUkQKS0JrOTQwZ3JSZVJXaUZHOGRvRm5CTEdXeFE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==\nusers:\n - name: user\n user:\n client-certificate-data: >-\n LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURsVENDQW4yZ0F3SUJBZ0lJY3pFd0Q2QXhLOE13RFFZSktvWklodmNOQVFFTEJRQXdLVEVaTUJjR0ExVUUKQ2hNUVEwTkZJRlJsWTJodWIyeHZaMmxsY3pFTU1Bb0dBMVVFQXhNRFEwTkZNQjRYRFRJMU1EUXlOakF4TXpFeApNRm9YRFRNd01EUXlOakF4TXpFeE1Gb3dnYUV4ZERBUUJnTlZCQW9UQ1dOalpUcDFjMlZ5Y3pBbkJnTlZCQW9UCklEZGlOMkV3TVdabFlUQTBaRFJoWW1GaVkyTTNZVFpqTnpCa01tSTRNV1JsTURjR0ExVUVDaE13WldFM1pUbG0KT0RNME5tTTVORFZrTm1Jek16RmhaREExTURSaFkySmlaall0WTJWeWRDMHhOelEwTnpneE1ETXpNU2t3SndZRApWUVFERXlCbFlUZGxPV1k0TXpRMll6azBOV1EyWWpNek1XRmtNRFV3TkdGalltSm1OakNDQVNJd0RRWUpLb1pJCmh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTWs4Z3pIZXhSSEd6SjJtR0ZyQXRtazcxRGhnc1ZXdEVEOVYKbUU4K3BPM3gxVi9EazErQVV6dk42dUNiRWt0Z2EzR0lhN0hqZW5STm5YR3FCZmhkQ01BVFRDZlV5OVFDYkM5YwpTQVVTUEpSRjJJZFZibHJEei9LUEhXc0gyZ1kvWHN0R2JHMnN4bHhNNWlxbmI5VWNGK2hjWEpnam9NamhKbFgxCjNJbEhUcFdpOERLcU5ycWIxK04vTlE2eGxERFBDS0tlVHpQMmtubXVIMWZ3VEZEaE93QVVyd2VyV0gxR2VkTXoKb0grWUdrUUdRbkNBdVdPdnhaSG5oSXQyVnVJOVI5SktHWGtXczJTb21VbkhNZnBRZlM2WnZZaDM4R3dLTUEySgpwb1BCWVJoQWdoUVRYQ0xZbHJjVUNJdGMwYXBzTFFQL0NibURObFpobGsrMTI1WTlvYzBDQXdFQUFhTklNRVl3CkRnWURWUjBQQVFIL0JBUURBZ1dnTUJNR0ExVWRKUVFNTUFvR0NDc0dBUVVGQndNQ01COEdBMVVkSXdRWU1CYUEKRkhiWGl4OXZEVXpoTVpmUUI5ckp6MkIxZGI0Sk1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQ2ZMcG5QRWcrVQppMDc0ZzZBWmdkK203TzJEeWpXcHIxSVcxTURDcm1XSmQ0NW44U0RjQ2IySHVNSTAwR00wWXN0SnZURC9TWTZsCmQvUm1tSWxtaFNWQm1Da0c3Tk5oMnRxb241amFEVnVZenkrQ3Blek9TcHFVYVIyeENCWG90RjVicHlWN0FWRm8KdjNHVGpoZjN3bEtqV0hBYnYzUENVVjNSdnJVQXM5d2JsY29oVlRMelN3M3h3dGVITy9QSmdWNVdPNDVWaXpSMApWWjA3THhsZnVmaVByTWJoTEZFWmVYMElPRkVVQ056ai9xcVY1ZUhjQ2srMFVnMm9OSHZNRzFJamE4cmFIQjV6Ck51dFYrMDhpVjY3aWo5b0V3RGt4UzhJQ0NYbkJ1aytqUGVBV3owVnNVTWFoeWlzYk95K2I3bHkzaEgrOWpkVDcKWTNBVXFNcnVocGkxCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K\n client-key-data: >-\n LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBeVR5RE1kN0ZFY2JNbmFZWVdzQzJhVHZVT0dDeFZhMFFQMVdZVHo2azdmSFZYOE9UClg0QlRPODNxNEpzU1MyQnJjWWhyc2VONmRFMmRjYW9GK0YwSXdCTk1KOVRMMUFKc0wxeElCUkk4bEVYWWgxVnUKV3NQUDhvOGRhd2ZhQmo5ZXkwWnNiYXpHWEV6bUtxZHYxUndYNkZ4Y21DT2d5T0VtVmZYY2lVZE9sYUx3TXFvMgp1cHZYNDM4MURyR1VNTThJb3A1UE0vYVNlYTRmVi9CTVVPRTdBQlN2QjZ0WWZVWjUwek9nZjVnYVJBWkNjSUM1Clk2L0ZrZWVFaTNaVzRqMUgwa29aZVJhelpLaVpTY2N4K2xCOUxwbTlpSGZ3YkFvd0RZbW1nOEZoR0VDQ0ZCTmMKSXRpV3R4UUlpMXpScW13dEEvOEp1WU0yVm1HV1Q3WGJsajJoelFJREFRQUJBb0lCQUUwVDNuUmxqVG9IMlN1eApDTGNLQlZROVZFWGIwMUNybndPRE4zbHkxeDFFZWFQYWQwSW5GcnBiWHRGMDFBY0hBV0JWRGxydWRHTERyOEJ2CmpTWGFESlgxVGtBYlk0N3E5cUVWU1lpWHVaaFpRRnhsdm9VSlowYlN4a1BPbUJXNGhBaDhDdC9mUTRMcStXWHgKQ0FhcVlnWGdDcDlEVmp4YThLSVFMODVzLzQ2VVVTcHpJRTNzTU5PTlhsQ0lJZkw0Mm1TLzdLSGR6SUxZbzFXSQpuY2FCLzdNb3J5R0I3Z1ZhbEl0S2NvL2hPbnc4R1Y0cmtxaVgxZFc2UE04enZtTENjKzhSaUtFV3lvd1cxUVYwCmN5SGMwTjFxcFREeE1IMVl2N1ZWMGlYWlpTcGt3R05DQWZyek9Wa21UdnpyM1RtMlFxU1pFaEVSMEdkOVdIclkKYlk2K29NRUNnWUVBeVdieHBxN3VWdEYyYlV3WjJrdmJ5TmE5QjhaaXQveGFhMEhydjJGZ3BRVWJKelJlY0J0aQp5RWErYVcxYVcxdHUyL1FqTUVNdkp1ajlwZngzbzE5aGJKaVBPRytIU095S1ZEVjhybTlTWFVDd2xXODV4bVR0Cnk0NSs3elJoVlZnVXZvdUhhUU5FaXN5cDQ3U2Y2S21lbGtjeU5uZmpmT2Y5aHJjU1RlKzI4QVVDZ1lFQS84b1EKMmtkNTMwNWk2a0NKeWsxdlNkc25hTkhYL3NvTVJhZjFEbGQzakVlMEdOTDcwS0Nmc1JTeXdQTTlmMHZsdW1BQgpGc2ZxQnFXbU9tazI1Z2RqZDZ4M29rcGQ0RFg2VWNOY3JtOG16eEw2Q2ZvTllZQmwzeFIrYnFqN24waGhxaDV3ClIyOFNtd1FFSGlnU1ZKU0FYT3ljOG50OXBZMHNFbk1wSmV2QVBTa0NnWUVBbElrVnc4YlVCTGVxemVVSVZCVUsKWFU3eVR0K2pRdW9jaldvcXdoVEJRRE5KMlZvb0pDb3VhbUt5WC9MRVp3aEI5SHBUMFc1YlFpa25tTmxnS3Q5WApiTTMvSXJJdVpqdjlzU2xaY1JTcy9CV1Bwa1pIcCtnYjhMcUJKMDNNVXpNSTZaYmlJVExGeEZBNUk3UzlFc3kyCkowTU81MWo0TDlDeEREL01naW8vRXprQ2dZRUFvWHRSMUZ2WFp0QzN4YWRrMWVDNDUybUJvYjBJbllPMDU2eTMKR296Qm5rQU9STFc5MytIbnJ3V2dMQXZqd1IrTE1uUTFlOHBOeGxDQmR0TEJvOHI2VXEwQkFlWHRDZ1ZKdUtDYgpQRXhUdGRzSEc1RlBMVVRBQzJ1R3ZoblVjS1JqYytDdmhZbHJ0NDE3aEFaTVBEVmNMRTM4YjJEaTI4Y2FFYk8rClFJQnE1ckVDZ1lBYjJOZTJaTnpaSHk0Nm9xb1BJOXJ2bGRXbWpzNmNtajg5d2craTVqY3g1VGJDQWZWbElCMU0KeXMzUTZMY2hIcHdISDhNY2JlekF4eUR2QWlRUDhrMjd3YzVZb3c4eEFwaXgxbjBQU3FRc1p3SlFiaEh0Mm8zaQpIM2FseDJ6TkpjbGxieDJyRmhoWjlKUkM3ZTFBMEswdmkxMEVDeVk1Sk9jNFlMOEtTZHIwVmc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=\ncontexts:\n - name: internal\n context:\n cluster: internalCluster\n user: user\ncurrent-context: internal\n'); + -- -------------------------------------------------------------------------------------- -- 初始化工作流相关数据 -- -------------------------------------------------------------------------------------- @@ -903,6 +917,9 @@ INSERT INTO `deploy-ease-platform`.`schedule_job` (`id`, `create_by`, `create_ti INSERT INTO `deploy-ease-platform`.`schedule_job` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `version`, `deleted`, `job_name`, `job_description`, `category_id`, `bean_name`, `method_name`, `form_definition_id`, `method_params`, `cron_expression`, `status`, `concurrent`, `last_execute_time`, `next_execute_time`, `execute_count`, `success_count`, `fail_count`, `timeout_seconds`, `retry_count`, `alert_email`) VALUES (16, 'admin', NOW(), 'admin', NOW(), 5727, b'0', '隆基Git分支同步', '定期同步Git仓库分支信息,每5分钟执行一次', 2, 'repositoryBranchServiceImpl', 'syncBranches', NULL, '{\"externalSystemId\": 4}', '0 */5 * * * ?', 'DISABLED', b'0', NOW(), NOW(), 7, 7, 0, 3600, 2, ''); INSERT INTO `deploy-ease-platform`.`schedule_job` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `version`, `deleted`, `job_name`, `job_description`, `category_id`, `bean_name`, `method_name`, `form_definition_id`, `method_params`, `cron_expression`, `status`, `concurrent`, `last_execute_time`, `next_execute_time`, `execute_count`, `success_count`, `fail_count`, `timeout_seconds`, `retry_count`, `alert_email`) VALUES (17, 'admin', NOW(), 'dengqichen', NOW(), 56, b'0', '服务器预警', '', 4, 'serverMonitorScheduler', 'collectServerMetrics', NULL, '{\"notificationChannelId\": 5, \"resourceAlertTemplateId\": 11}', '0 */10 * * * ?', 'DISABLED', b'0', NOW(), NULL, 44, 43, 1, 300, 0, ''); +-- K8S定时同步任务(需要先配置K8S集群后启用,externalSystemId为K8S集群ID) +INSERT INTO `deploy-ease-platform`.`schedule_job` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `version`, `deleted`, `job_name`, `job_description`, `category_id`, `bean_name`, `method_name`, `form_definition_id`, `method_params`, `cron_expression`, `status`, `concurrent`, `last_execute_time`, `next_execute_time`, `execute_count`, `success_count`, `fail_count`, `timeout_seconds`, `retry_count`, `alert_email`) VALUES (18, 'admin', NOW(), 'admin', NOW(), 0, b'0', 'K8S命名空间同步', '定期同步K8S集群命名空间信息,每5分钟执行一次', 2, 'k8sNamespaceServiceImpl', 'syncNamespaces', NULL, '{\"externalSystemId\": 0}', '0 */5 * * * ?', 'DISABLED', b'0', NOW(), NOW(), 0, 0, 0, 600, 2, ''); +INSERT INTO `deploy-ease-platform`.`schedule_job` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `version`, `deleted`, `job_name`, `job_description`, `category_id`, `bean_name`, `method_name`, `form_definition_id`, `method_params`, `cron_expression`, `status`, `concurrent`, `last_execute_time`, `next_execute_time`, `execute_count`, `success_count`, `fail_count`, `timeout_seconds`, `retry_count`, `alert_email`) VALUES (19, 'admin', NOW(), 'admin', NOW(), 0, b'0', 'K8S Deployment同步', '定期同步K8S集群Deployment信息,每5分钟执行一次', 2, 'k8sDeploymentServiceImpl', 'syncDeployments', NULL, '{\"externalSystemId\": 0}', '0 */5 * * * ?', 'DISABLED', b'0', NOW(), NOW(), 0, 0, 0, 600, 2, ''); INSERT INTO `deploy-ease-platform`.`deploy_server_alert_rule` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `version`, `deleted`, `server_id`, `rule_name`, `alert_type`, `warning_threshold`, `critical_threshold`, `duration_minutes`, `enabled`, `description`) VALUES (1, 'admin', NOW(), 'admin', NOW(), 0, 0, NULL, '全局CPU告警规则', 'CPU', 75.00, 90.00, 5, 1, '全局规则:CPU使用率超过75%触发警告,超过90%触发严重告警'); diff --git a/backend/src/main/resources/db/changelog/init/v1.0.0-schema.sql b/backend/src/main/resources/db/changelog/init/v1.0.0-schema.sql index 7af4ea21..dac45ebf 100644 --- a/backend/src/main/resources/db/changelog/init/v1.0.0-schema.sql +++ b/backend/src/main/resources/db/changelog/init/v1.0.0-schema.sql @@ -283,7 +283,7 @@ CREATE TABLE sys_external_system sync_status VARCHAR(50) NULL COMMENT '同步状态(SUCCESS/FAILED/RUNNING)', last_sync_time DATETIME(6) NULL COMMENT '最后同步时间', last_connect_time DATETIME(6) NULL COMMENT '最近连接成功时间', - config JSON NULL COMMENT '系统特有配置', + config TEXT NULL COMMENT '系统特有配置(如kubeconfig等)', CONSTRAINT UK_external_system_name UNIQUE (name), CONSTRAINT UK_external_system_type_url UNIQUE (type, url) @@ -1449,3 +1449,86 @@ CREATE TABLE deploy_team_bookmark CONSTRAINT fk_bookmark_team FOREIGN KEY (team_id) REFERENCES deploy_team (id), CONSTRAINT fk_bookmark_category FOREIGN KEY (category_id) REFERENCES deploy_team_bookmark_category (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='团队导航书签表'; + +-- -------------------------------------------------------------------------------------- +-- K8S同步表 +-- -------------------------------------------------------------------------------------- + +-- K8S命名空间表 +CREATE TABLE deploy_k8s_namespace +( + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', + create_by VARCHAR(50) NULL COMMENT '创建人', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_by VARCHAR(50) NULL COMMENT '更新人', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + version INT DEFAULT 1 COMMENT '版本号', + deleted BOOLEAN DEFAULT FALSE COMMENT '是否删除', + + external_system_id BIGINT NOT NULL COMMENT 'K8S集群ID(外部系统ID)', + namespace_name VARCHAR(255) NOT NULL COMMENT '命名空间名称', + status VARCHAR(50) NULL COMMENT '状态', + labels JSON NULL COMMENT '标签', + yaml_config TEXT NULL COMMENT '完整的YAML配置', + + UNIQUE KEY uk_system_namespace (external_system_id, namespace_name), + INDEX idx_external_system (external_system_id), + INDEX idx_deleted (deleted) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='K8S命名空间表'; + +-- K8S Deployment表 +CREATE TABLE deploy_k8s_deployment +( + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', + create_by VARCHAR(50) NULL COMMENT '创建人', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_by VARCHAR(50) NULL COMMENT '更新人', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + version INT DEFAULT 1 COMMENT '版本号', + deleted BOOLEAN DEFAULT FALSE COMMENT '是否删除', + + external_system_id BIGINT NOT NULL COMMENT 'K8S集群ID(外部系统ID)', + namespace_id BIGINT NOT NULL COMMENT '命名空间ID', + deployment_name VARCHAR(255) NOT NULL COMMENT 'Deployment名称', + + replicas INT NULL COMMENT '期望副本数', + available_replicas INT NULL COMMENT '可用副本数', + ready_replicas INT NULL COMMENT '就绪副本数', + + image VARCHAR(500) NULL COMMENT '容器镜像', + labels JSON NULL COMMENT '标签', + selector JSON NULL COMMENT '选择器', + yaml_config TEXT NULL COMMENT '完整的YAML配置', + + k8s_create_time DATETIME NULL COMMENT 'K8S中的创建时间', + k8s_update_time DATETIME NULL COMMENT 'K8S中的更新时间', + + UNIQUE KEY uk_namespace_deployment (namespace_id, deployment_name), + INDEX idx_external_system (external_system_id), + INDEX idx_namespace (namespace_id), + INDEX idx_deleted (deleted) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='K8S Deployment表'; + +-- K8S同步历史表 +CREATE TABLE deploy_k8s_sync_history +( + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', + create_by VARCHAR(50) NULL COMMENT '创建人', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_by VARCHAR(50) NULL COMMENT '更新人', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + version INT DEFAULT 1 COMMENT '版本号', + deleted BOOLEAN DEFAULT FALSE COMMENT '是否删除', + + sync_history_number VARCHAR(100) NOT NULL COMMENT '同步编号', + sync_type VARCHAR(50) NOT NULL COMMENT '同步类型(NAMESPACE/DEPLOYMENT)', + status VARCHAR(50) NOT NULL COMMENT '同步状态(SUCCESS/FAILED/RUNNING)', + start_time DATETIME NOT NULL COMMENT '开始时间', + end_time DATETIME NULL COMMENT '结束时间', + error_message TEXT NULL COMMENT '错误信息', + external_system_id BIGINT NOT NULL COMMENT 'K8S集群ID(外部系统ID)', + + INDEX idx_external_system (external_system_id), + INDEX idx_sync_time (create_time), + INDEX idx_deleted (deleted) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='K8S同步历史表';