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 index 8d2d2b16..1b3789c4 100644 --- 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 @@ -121,6 +121,37 @@ public class K8sDeploymentApiController extends BaseController getPodLogs( + @Parameter(description = "Deployment ID", required = true) @PathVariable Long deploymentId, + @Parameter(description = "Pod名称", required = true) @PathVariable String podName, + @Parameter(description = "容器名称(可选,默认第一个容器)") @RequestParam(required = false) String container, + @Parameter(description = "返回最后N行日志(可选)") @RequestParam(required = false) Integer tail, + @Parameter(description = "返回最近N秒的日志(可选)") @RequestParam(required = false) Integer sinceSeconds + ) { + return Response.success(k8sPodService.getPodLogs(deploymentId, podName, container, tail, sinceSeconds)); + } + + @Operation(summary = "重启Deployment", description = "通过更新annotation触发Deployment滚动重启") + @PostMapping("/{id}/restart") + public Response restartDeployment( + @Parameter(description = "Deployment ID", required = true) @PathVariable Long id + ) { + k8sDeploymentService.restartDeployment(id); + return Response.success(); + } + + @Operation(summary = "扩缩容Deployment", description = "修改Deployment的副本数") + @PostMapping("/{id}/scale") + public Response scaleDeployment( + @Parameter(description = "Deployment ID", required = true) @PathVariable Long id, + @Validated @RequestBody com.qqchen.deploy.backend.deploy.dto.ScaleDeploymentRequest request + ) { + k8sDeploymentService.scaleDeployment(id, request.getReplicas()); + return Response.success(); + } + @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/dto/K8sDeploymentDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/K8sDeploymentDTO.java index 04e5de77..65f8d6a3 100644 --- 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 @@ -31,6 +31,9 @@ public class K8sDeploymentDTO extends BaseDTO { @Schema(description = "就绪副本数") private Integer readyReplicas; + @Schema(description = "总重启次数(所有Pod的重启次数总和)") + private Integer totalRestartCount; + @Schema(description = "容器镜像") private String image; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ScaleDeploymentRequest.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ScaleDeploymentRequest.java new file mode 100644 index 00000000..cb866347 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/ScaleDeploymentRequest.java @@ -0,0 +1,19 @@ +package com.qqchen.deploy.backend.deploy.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * Deployment扩缩容请求 + */ +@Data +@Schema(description = "Deployment扩缩容请求") +public class ScaleDeploymentRequest { + + @NotNull(message = "副本数不能为空") + @Min(value = 0, message = "副本数不能小于0") + @Schema(description = "目标副本数", example = "3", required = true) + private Integer replicas; +} 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 index f4d75f62..3d232823 100644 --- 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 @@ -38,6 +38,9 @@ public class K8sDeployment extends Entity { @Column(name = "ready_replicas") private Integer readyReplicas; + @Column(name = "total_restart_count") + private Integer totalRestartCount; + @Column(name = "image") private String image; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/ContainerStateEnum.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/ContainerStateEnum.java new file mode 100644 index 00000000..cbaf7d9f --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/ContainerStateEnum.java @@ -0,0 +1,56 @@ +package com.qqchen.deploy.backend.deploy.enums; + +import lombok.Getter; + +/** + * 容器状态枚举 + */ +@Getter +public enum ContainerStateEnum { + + /** + * 运行中 + */ + RUNNING("running", "运行中"), + + /** + * 等待中 + */ + WAITING("waiting", "等待中"), + + /** + * 已终止 + */ + TERMINATED("terminated", "已终止"), + + /** + * 未知状态 + */ + UNKNOWN("unknown", "未知状态"); + + private final String code; + private final String description; + + ContainerStateEnum(String code, String description) { + this.code = code; + this.description = description; + } + + /** + * 根据code获取枚举 + * + * @param code 状态码 + * @return 容器状态枚举 + */ + public static ContainerStateEnum fromCode(String code) { + if (code == null) { + return UNKNOWN; + } + for (ContainerStateEnum state : values()) { + if (state.code.equalsIgnoreCase(code)) { + return state; + } + } + return UNKNOWN; + } +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/PodPhaseEnum.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/PodPhaseEnum.java new file mode 100644 index 00000000..4e7885e8 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/enums/PodPhaseEnum.java @@ -0,0 +1,61 @@ +package com.qqchen.deploy.backend.deploy.enums; + +import lombok.Getter; + +/** + * Pod阶段枚举 + */ +@Getter +public enum PodPhaseEnum { + + /** + * 等待中 + */ + PENDING("Pending", "等待中"), + + /** + * 运行中 + */ + RUNNING("Running", "运行中"), + + /** + * 成功 + */ + SUCCEEDED("Succeeded", "成功"), + + /** + * 失败 + */ + FAILED("Failed", "失败"), + + /** + * 未知状态 + */ + UNKNOWN("Unknown", "未知状态"); + + private final String code; + private final String description; + + PodPhaseEnum(String code, String description) { + this.code = code; + this.description = description; + } + + /** + * 根据code获取枚举 + * + * @param code 阶段码 + * @return Pod阶段枚举 + */ + public static PodPhaseEnum fromCode(String code) { + if (code == null) { + return UNKNOWN; + } + for (PodPhaseEnum phase : values()) { + if (phase.code.equalsIgnoreCase(code)) { + return phase; + } + } + return UNKNOWN; + } +} 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 index ac04c442..f18a0cb6 100644 --- 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 @@ -75,6 +75,50 @@ public interface IK8sServiceIntegration extends IExternalSystemIntegration { */ K8sPodResponse getPod(ExternalSystem externalSystem, String namespace, String podName); + /** + * 查询Pod日志 + * + * @param externalSystem K8S系统配置 + * @param namespace 命名空间名称 + * @param podName Pod名称 + * @param container 容器名称(可选,默认第一个容器) + * @param tail 返回最后N行日志(可选) + * @param sinceSeconds 返回最近N秒的日志(可选) + * @param follow 是否持续输出日志(可选,默认false) + * @return Pod日志内容 + */ + String getPodLogs(ExternalSystem externalSystem, String namespace, String podName, + String container, Integer tail, Integer sinceSeconds, Boolean follow); + + /** + * 重启Deployment(通过更新annotation触发滚动更新) + * + * @param externalSystem K8S系统配置 + * @param namespace 命名空间名称 + * @param deploymentName Deployment名称 + */ + void restartDeployment(ExternalSystem externalSystem, String namespace, String deploymentName); + + /** + * 扩缩容Deployment + * + * @param externalSystem K8S系统配置 + * @param namespace 命名空间名称 + * @param deploymentName Deployment名称 + * @param replicas 目标副本数 + */ + void scaleDeployment(ExternalSystem externalSystem, String namespace, String deploymentName, Integer replicas); + + /** + * 计算Deployment下所有Pod的总重启次数 + * + * @param externalSystem K8S系统配置 + * @param namespace 命名空间名称 + * @param deploymentName Deployment名称 + * @return 总重启次数 + */ + Integer calculateTotalRestartCount(ExternalSystem externalSystem, String namespace, String deploymentName); + /** * 获取系统类型 * 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 index 883265d2..0828d462 100644 --- 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 @@ -1,6 +1,8 @@ package com.qqchen.deploy.backend.deploy.integration.impl; import com.qqchen.deploy.backend.deploy.entity.ExternalSystem; +import com.qqchen.deploy.backend.deploy.enums.ContainerStateEnum; +import com.qqchen.deploy.backend.deploy.enums.PodPhaseEnum; 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; @@ -38,6 +40,7 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp // K8S ApiClient缓存 - 线程安全 private static final Map API_CLIENT_CACHE = new ConcurrentHashMap<>(); + private static final long CACHE_EXPIRE_TIME = 30 * 60 * 1000; // 30分钟过期 /** @@ -45,6 +48,7 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp */ private static class K8sApiClientCache { final ApiClient apiClient; + final long expireTime; K8sApiClientCache(ApiClient apiClient) { @@ -93,24 +97,24 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp @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; @@ -123,26 +127,26 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp @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( @@ -151,20 +155,20 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp ) ); } - + // 序列化为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); @@ -177,45 +181,45 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp @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 + if (deployment.getSpec() != null && deployment.getSpec().getTemplate() != null && deployment.getSpec().getTemplate().getSpec() != null - && deployment.getSpec().getTemplate().getSpec().getContainers() != 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( @@ -224,22 +228,22 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp ) ); } - + // 序列化为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失败,集群: {}, 命名空间: {}, 错误: {}", + log.error("查询K8S Deployment失败,集群: {}, 命名空间: {}, 错误: {}", externalSystem.getName(), namespace, e.getMessage(), e); throw new BusinessException(ResponseCode.K8S_DEPLOYMENT_SYNC_FAILED); } @@ -251,45 +255,45 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp @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 + if (deployment.getSpec() != null && deployment.getSpec().getTemplate() != null && deployment.getSpec().getTemplate().getSpec() != null - && deployment.getSpec().getTemplate().getSpec().getContainers() != 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( @@ -298,20 +302,20 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp ) ); } - + // 序列化为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); @@ -324,25 +328,25 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp @Override public List listPods(ExternalSystem externalSystem, String namespace) { log.info("查询K8S Pod,集群: {}, 命名空间: {}", externalSystem.getName(), namespace); - + try { K8sApiClientCache cache = getApiClientCache(externalSystem); CoreV1Api api = new CoreV1Api(cache.apiClient); - + V1PodList podList = api.listNamespacedPod( namespace, null, null, null, null, null, null, null, null, null, null ); - + List pods = new ArrayList<>(); for (V1Pod pod : podList.getItems()) { pods.add(convertPodToResponse(pod)); } - + log.info("查询到 {} 个Pod", pods.size()); return pods; - + } catch (Exception e) { - log.error("查询K8S Pod失败,集群: {}, 命名空间: {}, 错误: {}", + log.error("查询K8S Pod失败,集群: {}, 命名空间: {}, 错误: {}", externalSystem.getName(), namespace, e.getMessage(), e); throw new BusinessException(ResponseCode.K8S_OPERATION_FAILED); } @@ -353,53 +357,53 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp */ @Override public List listPodsByDeployment(ExternalSystem externalSystem, String namespace, String deploymentName) { - log.info("查询K8S Deployment的Pod,集群: {}, 命名空间: {}, Deployment: {}", + log.info("查询K8S Deployment的Pod,集群: {}, 命名空间: {}, Deployment: {}", externalSystem.getName(), namespace, deploymentName); - + try { K8sApiClientCache cache = getApiClientCache(externalSystem); - + // 1. 先查询Deployment获取selector AppsV1Api appsApi = new AppsV1Api(cache.apiClient); V1Deployment deployment = appsApi.readNamespacedDeployment(deploymentName, namespace, null); - - if (deployment.getSpec() == null || deployment.getSpec().getSelector() == null + + if (deployment.getSpec() == null || deployment.getSpec().getSelector() == null || deployment.getSpec().getSelector().getMatchLabels() == null) { log.warn("Deployment没有selector: {}/{}", namespace, deploymentName); return new ArrayList<>(); } - + // 2. 构建label selector Map matchLabels = deployment.getSpec().getSelector().getMatchLabels(); String labelSelector = matchLabels.entrySet().stream() .map(entry -> entry.getKey() + "=" + entry.getValue()) .reduce((a, b) -> a + "," + b) .orElse(""); - + // 3. 使用label selector查询Pod CoreV1Api coreApi = new CoreV1Api(cache.apiClient); V1PodList podList = coreApi.listNamespacedPod( namespace, null, null, null, null, labelSelector, null, null, null, null, null ); - + List pods = new ArrayList<>(); for (V1Pod pod : podList.getItems()) { pods.add(convertPodToResponse(pod)); } - + log.info("查询到 {} 个Pod", pods.size()); return pods; - + } catch (ApiException e) { if (e.getCode() == 404) { log.warn("Deployment不存在: {}/{}", namespace, deploymentName); throw new BusinessException(ResponseCode.K8S_RESOURCE_NOT_FOUND); } - log.error("查询K8S Deployment的Pod失败,集群: {}, 命名空间: {}, Deployment: {}, 错误: {}", + log.error("查询K8S Deployment的Pod失败,集群: {}, 命名空间: {}, Deployment: {}, 错误: {}", externalSystem.getName(), namespace, deploymentName, e.getMessage(), e); throw new BusinessException(ResponseCode.K8S_OPERATION_FAILED); } catch (Exception e) { - log.error("查询K8S Deployment的Pod失败,集群: {}, 命名空间: {}, Deployment: {}, 错误: {}", + log.error("查询K8S Deployment的Pod失败,集群: {}, 命名空间: {}, Deployment: {}, 错误: {}", externalSystem.getName(), namespace, deploymentName, e.getMessage(), e); throw new BusinessException(ResponseCode.K8S_OPERATION_FAILED); } @@ -410,27 +414,27 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp */ @Override public K8sPodResponse getPod(ExternalSystem externalSystem, String namespace, String podName) { - log.info("查询K8S Pod详情,集群: {}, 命名空间: {}, Pod: {}", + log.info("查询K8S Pod详情,集群: {}, 命名空间: {}, Pod: {}", externalSystem.getName(), namespace, podName); - + try { K8sApiClientCache cache = getApiClientCache(externalSystem); CoreV1Api api = new CoreV1Api(cache.apiClient); - + V1Pod pod = api.readNamespacedPod(podName, namespace, null); - + return convertPodToResponse(pod); - + } catch (ApiException e) { if (e.getCode() == 404) { log.warn("Pod不存在: {}/{}", namespace, podName); throw new BusinessException(ResponseCode.K8S_POD_NOT_FOUND); } - log.error("查询K8S Pod详情失败,集群: {}, 命名空间: {}, Pod: {}, 错误: {}", + log.error("查询K8S Pod详情失败,集群: {}, 命名空间: {}, Pod: {}, 错误: {}", externalSystem.getName(), namespace, podName, e.getMessage(), e); throw new BusinessException(ResponseCode.K8S_OPERATION_FAILED); } catch (Exception e) { - log.error("查询K8S Pod详情失败,集群: {}, 命名空间: {}, Pod: {}, 错误: {}", + log.error("查询K8S Pod详情失败,集群: {}, 命名空间: {}, Pod: {}, 错误: {}", externalSystem.getName(), namespace, podName, e.getMessage(), e); throw new BusinessException(ResponseCode.K8S_OPERATION_FAILED); } @@ -441,13 +445,13 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp */ private K8sPodResponse convertPodToResponse(V1Pod pod) { K8sPodResponse response = new K8sPodResponse(); - + // 基本信息 response.setName(pod.getMetadata().getName()); response.setNamespace(pod.getMetadata().getNamespace()); response.setLabels(pod.getMetadata().getLabels()); response.setAnnotations(pod.getMetadata().getAnnotations()); - + // 时间信息 if (pod.getMetadata().getCreationTimestamp() != null) { response.setCreationTimestamp( @@ -457,15 +461,15 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp ) ); } - + // 状态信息 if (pod.getStatus() != null) { - response.setPhase(pod.getStatus().getPhase()); + response.setPhase(PodPhaseEnum.fromCode(pod.getStatus().getPhase())); response.setReason(pod.getStatus().getReason()); response.setMessage(pod.getStatus().getMessage()); response.setPodIP(pod.getStatus().getPodIP()); response.setHostIP(pod.getStatus().getHostIP()); - + if (pod.getStatus().getStartTime() != null) { response.setStartTime( LocalDateTime.ofInstant( @@ -474,13 +478,13 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp ) ); } - + // 容器状态 if (pod.getStatus().getContainerStatuses() != null) { List containers = new ArrayList<>(); int totalRestartCount = 0; boolean allReady = true; - + for (V1ContainerStatus containerStatus : pod.getStatus().getContainerStatuses()) { K8sPodResponse.ContainerInfo containerInfo = new K8sPodResponse.ContainerInfo(); containerInfo.setName(containerStatus.getName()); @@ -489,17 +493,17 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp containerInfo.setReady(containerStatus.getReady()); containerInfo.setRestartCount(containerStatus.getRestartCount()); containerInfo.setContainerID(containerStatus.getContainerID()); - + totalRestartCount += containerStatus.getRestartCount(); if (!containerStatus.getReady()) { allReady = false; } - + // 容器状态 V1ContainerState state = containerStatus.getState(); if (state != null) { if (state.getRunning() != null) { - containerInfo.setState("running"); + containerInfo.setState(ContainerStateEnum.RUNNING); if (state.getRunning().getStartedAt() != null) { containerInfo.setStartedAt( LocalDateTime.ofInstant( @@ -509,57 +513,57 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp ); } } else if (state.getWaiting() != null) { - containerInfo.setState("waiting"); + containerInfo.setState(ContainerStateEnum.WAITING); containerInfo.setStateReason(state.getWaiting().getReason()); containerInfo.setStateMessage(state.getWaiting().getMessage()); } else if (state.getTerminated() != null) { - containerInfo.setState("terminated"); + containerInfo.setState(ContainerStateEnum.TERMINATED); containerInfo.setStateReason(state.getTerminated().getReason()); containerInfo.setStateMessage(state.getTerminated().getMessage()); } } - + containers.add(containerInfo); } - + response.setContainers(containers); response.setRestartCount(totalRestartCount); response.setReady(allReady); } } - + // Spec信息 if (pod.getSpec() != null) { response.setNodeName(pod.getSpec().getNodeName()); - + // 容器资源配置 if (pod.getSpec().getContainers() != null && response.getContainers() != null) { for (int i = 0; i < pod.getSpec().getContainers().size() && i < response.getContainers().size(); i++) { V1Container container = pod.getSpec().getContainers().get(i); K8sPodResponse.ContainerInfo containerInfo = response.getContainers().get(i); - + if (container.getResources() != null) { if (container.getResources().getRequests() != null) { containerInfo.setCpuRequest( - container.getResources().getRequests().get("cpu") != null - ? container.getResources().getRequests().get("cpu").toSuffixedString() + container.getResources().getRequests().get("cpu") != null + ? container.getResources().getRequests().get("cpu").toSuffixedString() : null ); containerInfo.setMemoryRequest( - container.getResources().getRequests().get("memory") != null - ? container.getResources().getRequests().get("memory").toSuffixedString() + container.getResources().getRequests().get("memory") != null + ? container.getResources().getRequests().get("memory").toSuffixedString() : null ); } if (container.getResources().getLimits() != null) { containerInfo.setCpuLimit( - container.getResources().getLimits().get("cpu") != null - ? container.getResources().getLimits().get("cpu").toSuffixedString() + container.getResources().getLimits().get("cpu") != null + ? container.getResources().getLimits().get("cpu").toSuffixedString() : null ); containerInfo.setMemoryLimit( - container.getResources().getLimits().get("memory") != null - ? container.getResources().getLimits().get("memory").toSuffixedString() + container.getResources().getLimits().get("memory") != null + ? container.getResources().getLimits().get("memory").toSuffixedString() : null ); } @@ -567,47 +571,202 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp } } } - + // Owner信息 if (pod.getMetadata().getOwnerReferences() != null && !pod.getMetadata().getOwnerReferences().isEmpty()) { V1OwnerReference owner = pod.getMetadata().getOwnerReferences().get(0); response.setOwnerKind(owner.getKind()); response.setOwnerName(owner.getName()); } - + // 序列化为YAML try { response.setYamlConfig(Yaml.dump(pod)); } catch (Exception e) { log.warn("序列化Pod为YAML失败: {}", pod.getMetadata().getName(), e); } - + return response; } /** - * 创建K8S ApiClient(对外接口,使用缓存) - * - * @param externalSystem K8S系统配置 - * @return ApiClient + * 查询Pod日志 */ - private ApiClient createApiClient(ExternalSystem externalSystem) { - return getApiClientCache(externalSystem).apiClient; + @Override + public String getPodLogs(ExternalSystem externalSystem, String namespace, String podName, + String container, Integer tail, Integer sinceSeconds, Boolean follow) { + log.info("查询K8S Pod日志,集群: {}, 命名空间: {}, Pod: {}, 容器: {}", + externalSystem.getName(), namespace, podName, container); + + try { + K8sApiClientCache cache = getApiClientCache(externalSystem); + CoreV1Api api = new CoreV1Api(cache.apiClient); + + // 查询Pod日志 + String logs = api.readNamespacedPodLog( + podName, // Pod名称 + namespace, // 命名空间 + container, // 容器名称(可选) + follow != null && follow, // 是否持续输出 + null, // insecureSkipTLSVerifyBackend + null, // limitBytes + "false", // pretty + false, // previous(是否查询上一个容器的日志) + sinceSeconds, // sinceSeconds + tail, // tail + false // timestamps + ); + + log.info("查询Pod日志成功,日志长度: {}", logs != null ? logs.length() : 0); + return logs != null ? logs : ""; + + } catch (ApiException e) { + if (e.getCode() == 404) { + log.warn("Pod不存在: {}/{}", namespace, podName); + throw new BusinessException(ResponseCode.K8S_POD_NOT_FOUND); + } + log.error("查询K8S Pod日志失败,集群: {}, 命名空间: {}, Pod: {}, 错误: {}", + externalSystem.getName(), namespace, podName, e.getMessage(), e); + throw new BusinessException(ResponseCode.K8S_OPERATION_FAILED); + } catch (Exception e) { + log.error("查询K8S Pod日志失败,集群: {}, 命名空间: {}, Pod: {}, 错误: {}", + externalSystem.getName(), namespace, podName, e.getMessage(), e); + throw new BusinessException(ResponseCode.K8S_OPERATION_FAILED); + } + } + + /** + * 重启Deployment(通过更新annotation触发滚动更新) + */ + @Override + public void restartDeployment(ExternalSystem externalSystem, String namespace, String deploymentName) { + log.info("重启K8S Deployment,集群: {}, 命名空间: {}, Deployment: {}", + externalSystem.getName(), namespace, deploymentName); + + try { + K8sApiClientCache cache = getApiClientCache(externalSystem); + AppsV1Api api = new AppsV1Api(cache.apiClient); + + // 构建patch内容:更新spec.template.metadata.annotations添加重启时间戳 + String patchBody = String.format( + "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"kubectl.kubernetes.io/restartedAt\":\"%s\"}}}}}", + LocalDateTime.now().toString() + ); + + // 使用strategic merge patch更新Deployment + api.patchNamespacedDeployment( + deploymentName, + namespace, + new io.kubernetes.client.custom.V1Patch(patchBody), + null, + null, + null, + null, + null + ); + + log.info("重启K8S Deployment成功"); + + } catch (ApiException e) { + if (e.getCode() == 404) { + log.warn("Deployment不存在: {}/{}", namespace, deploymentName); + throw new BusinessException(ResponseCode.K8S_RESOURCE_NOT_FOUND); + } + log.error("重启K8S Deployment失败,集群: {}, 命名空间: {}, Deployment: {}, 错误: {}", + externalSystem.getName(), namespace, deploymentName, e.getMessage(), e); + throw new BusinessException(ResponseCode.K8S_OPERATION_FAILED); + } catch (Exception e) { + log.error("重启K8S Deployment失败,集群: {}, 命名空间: {}, Deployment: {}, 错误: {}", + externalSystem.getName(), namespace, deploymentName, e.getMessage(), e); + throw new BusinessException(ResponseCode.K8S_OPERATION_FAILED); + } + } + + /** + * 扩缩容Deployment + */ + @Override + public void scaleDeployment(ExternalSystem externalSystem, String namespace, String deploymentName, Integer replicas) { + log.info("扩缩容K8S Deployment,集群: {}, 命名空间: {}, Deployment: {}, 目标副本数: {}", + externalSystem.getName(), namespace, deploymentName, replicas); + + try { + K8sApiClientCache cache = getApiClientCache(externalSystem); + AppsV1Api api = new AppsV1Api(cache.apiClient); + + // 构建patch内容:更新spec.replicas + String patchBody = String.format("{\"spec\":{\"replicas\":%d}}", replicas); + + // 使用strategic merge patch更新Deployment的scale + api.patchNamespacedDeploymentScale( + deploymentName, + namespace, + new io.kubernetes.client.custom.V1Patch(patchBody), + null, + null, + null, + null, + null + ); + + log.info("扩缩容K8S Deployment成功"); + + } catch (ApiException e) { + if (e.getCode() == 404) { + log.warn("Deployment不存在: {}/{}", namespace, deploymentName); + throw new BusinessException(ResponseCode.K8S_RESOURCE_NOT_FOUND); + } + log.error("扩缩容K8S Deployment失败,集群: {}, 命名空间: {}, Deployment: {}, 错误: {}", + externalSystem.getName(), namespace, deploymentName, e.getMessage(), e); + throw new BusinessException(ResponseCode.K8S_OPERATION_FAILED); + } catch (Exception e) { + log.error("扩缩容K8S Deployment失败,集群: {}, 命名空间: {}, Deployment: {}, 错误: {}", + externalSystem.getName(), namespace, deploymentName, e.getMessage(), e); + throw new BusinessException(ResponseCode.K8S_OPERATION_FAILED); + } + } + + /** + * 计算Deployment下所有Pod的总重启次数 + */ + @Override + public Integer calculateTotalRestartCount(ExternalSystem externalSystem, String namespace, String deploymentName) { + log.debug("计算Deployment的总重启次数,集群: {}, 命名空间: {}, Deployment: {}", + externalSystem.getName(), namespace, deploymentName); + + try { + // 查询Deployment下的所有Pod + List pods = listPodsByDeployment(externalSystem, namespace, deploymentName); + + // 累加所有Pod的重启次数 + int totalRestartCount = pods.stream() + .mapToInt(pod -> pod.getRestartCount() != null ? pod.getRestartCount() : 0) + .sum(); + + log.debug("Deployment总重启次数: {}", totalRestartCount); + return totalRestartCount; + + } catch (Exception e) { + log.warn("计算Deployment总重启次数失败,集群: {}, 命名空间: {}, Deployment: {}, 错误: {}", + externalSystem.getName(), namespace, deploymentName, e.getMessage()); + // 计算失败时返回null,不影响同步流程 + return null; + } } /** * 创建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秒连接超时 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 index cf6a437e..729be619 100644 --- 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 @@ -12,6 +12,7 @@ public class K8sDeploymentResponse { private Integer replicas; private Integer availableReplicas; private Integer readyReplicas; + private Integer totalRestartCount; private String image; private Map labels; private Map selector; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/response/K8sPodResponse.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/response/K8sPodResponse.java index 0cd95e1e..bc4568d8 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/response/K8sPodResponse.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/response/K8sPodResponse.java @@ -1,5 +1,7 @@ package com.qqchen.deploy.backend.deploy.integration.response; +import com.qqchen.deploy.backend.deploy.enums.ContainerStateEnum; +import com.qqchen.deploy.backend.deploy.enums.PodPhaseEnum; import lombok.Data; import java.time.LocalDateTime; @@ -23,9 +25,9 @@ public class K8sPodResponse { private String namespace; /** - * Pod状态阶段(Running, Pending, Succeeded, Failed, Unknown) + * Pod状态阶段 */ - private String phase; + private PodPhaseEnum phase; /** * Pod状态原因 @@ -123,9 +125,9 @@ public class K8sPodResponse { private String imageID; /** - * 容器状态(running, waiting, terminated) + * 容器状态 */ - private String state; + private ContainerStateEnum state; /** * 是否就绪 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 index 626f12f0..70b585c4 100644 --- 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 @@ -20,4 +20,19 @@ public interface IK8sDeploymentService extends IBaseService findByExternalSystemId(Long externalSystemId); List findByNamespaceId(Long namespaceId); + + /** + * 重启Deployment + * + * @param deploymentId Deployment ID + */ + void restartDeployment(Long deploymentId); + + /** + * 扩缩容Deployment + * + * @param deploymentId Deployment ID + * @param replicas 目标副本数 + */ + void scaleDeployment(Long deploymentId, Integer replicas); } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IK8sPodService.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IK8sPodService.java index 632357e8..61cc6038 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IK8sPodService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IK8sPodService.java @@ -53,4 +53,16 @@ public interface IK8sPodService { * @return Pod列表 */ List listPodsByNamespace(Long namespaceId); + + /** + * 查询Pod日志 + * + * @param deploymentId Deployment ID + * @param podName Pod名称 + * @param container 容器名称(可选) + * @param tail 返回最后N行日志(可选) + * @param sinceSeconds 返回最近N秒的日志(可选) + * @return Pod日志内容 + */ + String getPodLogs(Long deploymentId, String podName, String container, Integer tail, Integer sinceSeconds); } 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 index d10bff60..80b85f80 100644 --- 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 @@ -99,6 +99,13 @@ public class K8sDeploymentServiceImpl extends BaseServiceImpl deployments = k8sDeploymentRepository.findByNamespaceId(namespaceId); return k8sDeploymentConverter.toDtoList(deployments); } + + @Override + public void restartDeployment(Long deploymentId) { + log.info("重启Deployment,deploymentId: {}", deploymentId); + + // 1. 查询K8sDeployment + K8sDeployment deployment = k8sDeploymentRepository.findById(deploymentId) + .orElseThrow(() -> new BusinessException(ResponseCode.K8S_RESOURCE_NOT_FOUND)); + + // 2. 查询K8sNamespace + K8sNamespace namespace = k8sNamespaceRepository.findById(deployment.getNamespaceId()) + .orElseThrow(() -> new BusinessException(ResponseCode.K8S_RESOURCE_NOT_FOUND)); + + // 3. 查询ExternalSystem + ExternalSystem externalSystem = externalSystemRepository.findById(deployment.getExternalSystemId()) + .orElseThrow(() -> new BusinessException(ResponseCode.EXTERNAL_SYSTEM_NOT_FOUND)); + + // 4. 调用K8s API重启Deployment + k8sServiceIntegration.restartDeployment( + externalSystem, + namespace.getNamespaceName(), + deployment.getDeploymentName() + ); + + log.info("Deployment重启成功,deploymentId: {}", deploymentId); + } + + @Override + public void scaleDeployment(Long deploymentId, Integer replicas) { + log.info("扩缩容Deployment,deploymentId: {}, replicas: {}", deploymentId, replicas); + + // 参数校验 + if (replicas == null || replicas < 0) { + throw new BusinessException(ResponseCode.INVALID_PARAM); + } + + // 1. 查询K8sDeployment + K8sDeployment deployment = k8sDeploymentRepository.findById(deploymentId) + .orElseThrow(() -> new BusinessException(ResponseCode.K8S_RESOURCE_NOT_FOUND)); + + // 2. 查询K8sNamespace + K8sNamespace namespace = k8sNamespaceRepository.findById(deployment.getNamespaceId()) + .orElseThrow(() -> new BusinessException(ResponseCode.K8S_RESOURCE_NOT_FOUND)); + + // 3. 查询ExternalSystem + ExternalSystem externalSystem = externalSystemRepository.findById(deployment.getExternalSystemId()) + .orElseThrow(() -> new BusinessException(ResponseCode.EXTERNAL_SYSTEM_NOT_FOUND)); + + // 4. 调用K8s API扩缩容Deployment + k8sServiceIntegration.scaleDeployment( + externalSystem, + namespace.getNamespaceName(), + deployment.getDeploymentName(), + replicas + ); + + // 5. 更新本地数据库记录 + deployment.setReplicas(replicas); + k8sDeploymentRepository.save(deployment); + + log.info("Deployment扩缩容成功,deploymentId: {}, replicas: {}", deploymentId, replicas); + } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/K8sPodServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/K8sPodServiceImpl.java index 084d8757..c63ea45c 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/K8sPodServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/K8sPodServiceImpl.java @@ -129,4 +129,33 @@ public class K8sPodServiceImpl implements IK8sPodService { // 4. 调用K8s API查询Pod详情 return k8sServiceIntegration.getPod(externalSystem, namespace.getNamespaceName(), podName); } + + @Override + public String getPodLogs(Long deploymentId, String podName, String container, Integer tail, Integer sinceSeconds) { + log.info("查询Pod日志,deploymentId: {}, podName: {}, container: {}, tail: {}, sinceSeconds: {}", + deploymentId, podName, container, tail, sinceSeconds); + + // 1. 查询K8sDeployment + K8sDeployment deployment = k8sDeploymentRepository.findById(deploymentId) + .orElseThrow(() -> new BusinessException(ResponseCode.K8S_RESOURCE_NOT_FOUND)); + + // 2. 查询K8sNamespace + K8sNamespace namespace = k8sNamespaceRepository.findById(deployment.getNamespaceId()) + .orElseThrow(() -> new BusinessException(ResponseCode.K8S_RESOURCE_NOT_FOUND)); + + // 3. 查询ExternalSystem + ExternalSystem externalSystem = externalSystemRepository.findById(deployment.getExternalSystemId()) + .orElseThrow(() -> new BusinessException(ResponseCode.EXTERNAL_SYSTEM_NOT_FOUND)); + + // 4. 调用K8s API查询Pod日志 + return k8sServiceIntegration.getPodLogs( + externalSystem, + namespace.getNamespaceName(), + podName, + container, + tail, + sinceSeconds, + false // follow参数设为false,不持续输出 + ); + } }