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 5280affd..ec82e15a 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 @@ -22,15 +22,24 @@ public class K8sDeploymentDTO extends BaseDTO { @Schema(description = "Deployment名称") private String deploymentName; - @Schema(description = "期望副本数") - private Integer replicas; + @Schema(description = "期望副本数(来自spec.replicas,用户声明的目标副本数)") + private Integer desiredReplicas; - @Schema(description = "可用副本数") + @Schema(description = "当前副本数(来自status.replicas,所有Pod总数,包括Running/Pending/Failed等所有状态)") + private Integer currentReplicas; + + @Schema(description = "可用副本数(来自status.availableReplicas,Ready且可接收流量的Pod数)") private Integer availableReplicas; - @Schema(description = "就绪副本数") + @Schema(description = "就绪副本数(来自status.readyReplicas,健康检查通过的Pod数)") private Integer readyReplicas; + @Schema(description = "已更新副本数(来自status.updatedReplicas,已更新到最新版本的Pod数)") + private Integer updatedReplicas; + + @Schema(description = "不可用副本数(来自status.unavailableReplicas,不可用的Pod数)") + private Integer unavailableReplicas; + @Schema(description = "总重启次数(所有Pod的重启次数总和)") private Integer totalRestartCount; 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 175e0b16..3c989168 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 @@ -29,15 +29,51 @@ public class K8sDeployment extends Entity { @Column(name = "deployment_name", nullable = false) private String deploymentName; - @Column(name = "replicas") - private Integer replicas; + /** + * 期望副本数(来自spec.replicas) + * 用户声明的目标副本数 + */ + @Column(name = "desired_replicas") + private Integer desiredReplicas; + /** + * 当前副本数(来自status.replicas) + * 所有Pod总数,包括Running/Pending/Failed等所有状态 + */ + @Column(name = "current_replicas") + private Integer currentReplicas; + + /** + * 可用副本数(来自status.availableReplicas) + * Ready且可接收流量的Pod数 + */ @Column(name = "available_replicas") private Integer availableReplicas; + /** + * 就绪副本数(来自status.readyReplicas) + * 健康检查通过的Pod数 + */ @Column(name = "ready_replicas") private Integer readyReplicas; + /** + * 已更新副本数(来自status.updatedReplicas) + * 已更新到最新版本的Pod数 + */ + @Column(name = "updated_replicas") + private Integer updatedReplicas; + + /** + * 不可用副本数(来自status.unavailableReplicas) + * 不可用的Pod数 + */ + @Column(name = "unavailable_replicas") + private Integer unavailableReplicas; + + /** + * 总重启次数(所有Pod的重启次数总和) + */ @Column(name = "total_restart_count") private Integer totalRestartCount; 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 ea567ea2..40e2b4d1 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 @@ -196,13 +196,18 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp response.setName(deployment.getMetadata().getName()); response.setNamespace(deployment.getMetadata().getNamespace()); + // 从spec读取期望副本数 if (deployment.getSpec() != null) { - response.setReplicas(deployment.getSpec().getReplicas()); + response.setDesiredReplicas(deployment.getSpec().getReplicas()); } + // 从status读取实际状态 if (deployment.getStatus() != null) { + response.setCurrentReplicas(deployment.getStatus().getReplicas()); response.setAvailableReplicas(deployment.getStatus().getAvailableReplicas()); response.setReadyReplicas(deployment.getStatus().getReadyReplicas()); + response.setUpdatedReplicas(deployment.getStatus().getUpdatedReplicas()); + response.setUnavailableReplicas(deployment.getStatus().getUnavailableReplicas()); } response.setLabels(deployment.getMetadata().getLabels()); @@ -212,12 +217,9 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp } // 获取第一个容器的镜像 - 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()); + String image = extractFirstContainerImage(deployment); + if (image != null) { + response.setImage(image); } if (deployment.getMetadata().getCreationTimestamp() != null) { @@ -273,13 +275,18 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp response.setName(deployment.getMetadata().getName()); response.setNamespace(deployment.getMetadata().getNamespace()); + // 从spec读取期望副本数 if (deployment.getSpec() != null) { - response.setReplicas(deployment.getSpec().getReplicas()); + response.setDesiredReplicas(deployment.getSpec().getReplicas()); } + // 从status读取实际状态 if (deployment.getStatus() != null) { + response.setCurrentReplicas(deployment.getStatus().getReplicas()); response.setAvailableReplicas(deployment.getStatus().getAvailableReplicas()); response.setReadyReplicas(deployment.getStatus().getReadyReplicas()); + response.setUpdatedReplicas(deployment.getStatus().getUpdatedReplicas()); + response.setUnavailableReplicas(deployment.getStatus().getUnavailableReplicas()); } response.setLabels(deployment.getMetadata().getLabels()); @@ -289,12 +296,9 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp } // 获取第一个容器的镜像 - 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()); + String image = extractFirstContainerImage(deployment); + if (image != null) { + response.setImage(image); } if (deployment.getMetadata().getCreationTimestamp() != null) { @@ -486,7 +490,7 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp } // 容器状态 - if (pod.getStatus().getContainerStatuses() != null) { + if (pod.getStatus().getContainerStatuses() != null && !pod.getStatus().getContainerStatuses().isEmpty()) { List containers = new ArrayList<>(); int totalRestartCount = 0; boolean allReady = true; @@ -535,6 +539,11 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp response.setContainers(containers); response.setRestartCount(totalRestartCount); response.setReady(allReady); + } else { + // Failed/Evicted状态的Pod可能没有containerStatuses + // 设置默认值,确保统计信息完整 + response.setRestartCount(0); + response.setReady(false); } } @@ -542,40 +551,8 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp 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() - : null - ); - containerInfo.setMemoryRequest( - 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() - : null - ); - containerInfo.setMemoryLimit( - container.getResources().getLimits().get("memory") != null - ? container.getResources().getLimits().get("memory").toSuffixedString() - : null - ); - } - } - } - } + // 填充容器资源配置 + fillContainerResources(pod.getSpec().getContainers(), response.getContainers()); } // Owner信息 @@ -886,4 +863,108 @@ public class K8sServiceIntegrationImpl extends BaseExternalSystemIntegration imp client.setReadTimeout(120000); // 120秒读取超时(优化日志查询等耗时操作) return client; } + + /** + * 提取Deployment中第一个容器的镜像 + * + * @param deployment K8s Deployment对象 + * @return 镜像名称,如果不存在则返回null + */ + private String extractFirstContainerImage(V1Deployment deployment) { + if (deployment.getSpec() == null) { + return null; + } + + V1PodTemplateSpec template = deployment.getSpec().getTemplate(); + if (template == null) { + return null; + } + + V1PodSpec podSpec = template.getSpec(); + if (podSpec == null) { + return null; + } + + List containers = podSpec.getContainers(); + if (containers == null || containers.isEmpty()) { + return null; + } + + return containers.get(0).getImage(); + } + + /** + * 填充容器资源配置信息 + * + * @param specContainers Pod Spec中的容器列表 + * @param containerInfos 响应对象中的容器信息列表 + */ + private void fillContainerResources(List specContainers, List containerInfos) { + if (specContainers == null || containerInfos == null) { + return; + } + + int size = Math.min(specContainers.size(), containerInfos.size()); + for (int i = 0; i < size; i++) { + V1Container container = specContainers.get(i); + K8sPodResponse.ContainerInfo containerInfo = containerInfos.get(i); + + V1ResourceRequirements resources = container.getResources(); + if (resources == null) { + continue; + } + + // 填充资源请求(requests) + fillResourceRequests(resources, containerInfo); + + // 填充资源限制(limits) + fillResourceLimits(resources, containerInfo); + } + } + + /** + * 填充容器资源请求配置 + * + * @param resources 资源配置对象 + * @param containerInfo 容器信息对象 + */ + private void fillResourceRequests(V1ResourceRequirements resources, K8sPodResponse.ContainerInfo containerInfo) { + Map requests = resources.getRequests(); + if (requests == null) { + return; + } + + io.kubernetes.client.custom.Quantity cpu = requests.get("cpu"); + if (cpu != null) { + containerInfo.setCpuRequest(cpu.toSuffixedString()); + } + + io.kubernetes.client.custom.Quantity memory = requests.get("memory"); + if (memory != null) { + containerInfo.setMemoryRequest(memory.toSuffixedString()); + } + } + + /** + * 填充容器资源限制配置 + * + * @param resources 资源配置对象 + * @param containerInfo 容器信息对象 + */ + private void fillResourceLimits(V1ResourceRequirements resources, K8sPodResponse.ContainerInfo containerInfo) { + Map limits = resources.getLimits(); + if (limits == null) { + return; + } + + io.kubernetes.client.custom.Quantity cpu = limits.get("cpu"); + if (cpu != null) { + containerInfo.setCpuLimit(cpu.toSuffixedString()); + } + + io.kubernetes.client.custom.Quantity memory = limits.get("memory"); + if (memory != null) { + containerInfo.setMemoryLimit(memory.toSuffixedString()); + } + } } 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 eefe1aa7..4850610c 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 @@ -5,14 +5,73 @@ import lombok.Data; import java.time.LocalDateTime; import java.util.Map; +/** + * K8s Deployment响应对象 + * + *

字段说明(参考kubectl get deployment输出): + *

+ * NAME    READY   UP-TO-DATE   AVAILABLE   AGE
+ * my-app  2/2     2            2           5d
+ *         ↑       ↑            ↑
+ *    readyReplicas/desiredReplicas  updatedReplicas  availableReplicas
+ * 
+ * + *

副本数字段完整映射K8s API: + *

    + *
  • desiredReplicas: 期望副本数(来自spec.replicas,用户声明的目标副本数)
  • + *
  • currentReplicas: 当前副本数(来自status.replicas,所有Pod总数,包括Running/Pending/Failed等所有状态)
  • + *
  • availableReplicas: 可用副本数(来自status.availableReplicas,Ready且可接收流量的Pod数)
  • + *
  • readyReplicas: 就绪副本数(来自status.readyReplicas,健康检查通过的Pod数)
  • + *
  • updatedReplicas: 已更新副本数(来自status.updatedReplicas,已更新到最新版本的Pod数)
  • + *
  • unavailableReplicas: 不可用副本数(来自status.unavailableReplicas,不可用的Pod数)
  • + *
+ */ @Data public class K8sDeploymentResponse { private String name; private String namespace; - private Integer replicas; + + /** + * 期望副本数(来自spec.replicas) + * 用户声明的目标副本数 + */ + private Integer desiredReplicas; + + /** + * 当前副本数(来自status.replicas) + * 所有Pod总数,包括Running/Pending/Failed等所有状态 + */ + private Integer currentReplicas; + + /** + * 可用副本数(来自status.availableReplicas) + * Ready且可接收流量的Pod数 + */ private Integer availableReplicas; + + /** + * 就绪副本数(来自status.readyReplicas) + * 健康检查通过的Pod数 + */ private Integer readyReplicas; + + /** + * 已更新副本数(来自status.updatedReplicas) + * 已更新到最新版本的Pod数 + */ + private Integer updatedReplicas; + + /** + * 不可用副本数(来自status.unavailableReplicas) + * 不可用的Pod数 + */ + private Integer unavailableReplicas; + + /** + * 总重启次数(所有Pod的重启次数总和) + */ private Integer totalRestartCount; + private String image; private Map labels; private Map selector; 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 fc0d0899..0fa0a3df 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 @@ -116,9 +116,12 @@ public class K8sDeploymentServiceImpl extends BaseServiceImpl = ({ pod, deploymentId, onViewLogs }) }; // 获取主容器信息 - const mainContainer = pod.containers[0]; + const mainContainer = pod.containers?.[0]; // 复制镜像名称 const handleCopyImage = () => {