package backendstatus import ( "context" "errors" "fmt" "yms-daemon/internal/containerengine" "yms-daemon/internal/daemonapi" "yms-daemon/internal/deploymentconfig" "yms-daemon/internal/transaction" ) // 诊断项稳定标识,供测试与 reconcile 识别,不参与展示。 const ( codeUndeployed = "container_backend_undeployed" codeUndeployedWithHistory = "container_backend_undeployed_with_history" codeContainerWithoutRecord = "container_present_without_deployment_record" codeCommittedPortOK = "nginx_active_port" codeNginxWrongPort = "nginx_wrong_port" codeActiveContainerOK = "active_container_healthy" codeActiveContainerMissing = "active_container_missing" codeActiveContainerIdentity = "active_container_identity_mismatch" codeActiveContainerNotRunning = "active_container_not_running" codeInactiveContainerOK = "inactive_container_absent" codeInactiveContainerResidual = "inactive_container_residual" codeInactiveContainerRunning = "inactive_container_running" codeDeploymentConfigMismatch = "deployment_record_config_mismatch" ) // diagnoseContainer 对 container 后端做只读诊断。 // 事实来源是 backend_container_deployment 单例记录;现场状态由容器引擎与宿主 Nginx 提供。 func (d *Diagnoser) diagnoseContainer(ctx context.Context) (daemonapi.Diagnosis, error) { findings, err := d.containerGather(ctx) if err != nil { return daemonapi.Diagnosis{}, err } return toDiagnosis("backend", deploymentconfig.BackendTypeContainer, findings), nil } // containerGather 读取 container 后端的部署记录、历史事务与宿主 Nginx 状态,生成全部诊断项。 func (d *Diagnoser) containerGather(ctx context.Context) ([]finding, error) { deployment, depErr := d.store.BackendContainerDeployment(ctx) hasDeployment := depErr == nil if depErr != nil && !errors.Is(depErr, transaction.ErrNotFound) { return nil, fmt.Errorf("read backend container deployment: %w", depErr) } hasHistory, err := d.store.HasCommittedBackendContainerTransactionHistory(ctx) if err != nil { return nil, err } before, err := d.gateway.Read() if err != nil { return nil, fmt.Errorf("read host Nginx configuration: %w", err) } return d.containerFindings(ctx, deployment, hasDeployment, hasHistory, before.ActivePort) } // containerFindings 生成 container 后端的全部诊断项。 // deployment 部署记录,hasDeployment 表示是否存在该记录,hasHistory 表示是否存在历史 // 已提交事务,activePort 是宿主 Nginx 当前指向的后端端口。 func (d *Diagnoser) containerFindings(ctx context.Context, deployment transaction.BackendContainerDeployment, hasDeployment bool, hasHistory bool, activePort int) ([]finding, error) { if !hasDeployment { return d.containerUndeployedFindings(ctx, hasHistory) } findings := make([]finding, 0, 6) committedSlot, err := d.config.Backend.SlotForPort(deployment.ActivePort) if err != nil { return nil, err } if deployment.ContainerName != committedSlot.ContainerName { findings = append(findings, finding{ level: daemonapi.DiagnosisLevelDrift, code: codeDeploymentConfigMismatch, message: fmt.Sprintf("部署记录容器 %s 与活动端口 %d 的槽位容器 %s 不一致", deployment.ContainerName, deployment.ActivePort, committedSlot.ContainerName), }) return findings, nil } committedContainer, committedErr := d.engine.InspectContainer(ctx, deployment.ContainerName) committedHealthy := false switch { case errors.Is(committedErr, containerengine.ErrNotFound): findings = append(findings, finding{ level: daemonapi.DiagnosisLevelDrift, code: codeActiveContainerMissing, message: "部署记录的活动容器 " + deployment.ContainerName + " 不存在", }) case committedErr != nil: return nil, fmt.Errorf("inspect committed backend container %s: %w", deployment.ContainerName, committedErr) case committedContainer.ID != deployment.ContainerID: findings = append(findings, finding{ level: daemonapi.DiagnosisLevelDrift, code: codeActiveContainerIdentity, message: "活动容器 " + deployment.ContainerName + " 身份与部署记录不一致", }) case !committedContainer.Running || committedContainer.Dead: findings = append(findings, finding{ level: daemonapi.DiagnosisLevelDrift, code: codeActiveContainerNotRunning, message: "活动容器 " + deployment.ContainerName + " 未运行", }) default: committedHealthy = true findings = append(findings, finding{ level: daemonapi.DiagnosisLevelOK, code: codeActiveContainerOK, message: "活动容器 " + deployment.ContainerName + " 健康", }) } if activePort == deployment.ActivePort { findings = append(findings, finding{ level: daemonapi.DiagnosisLevelOK, code: codeCommittedPortOK, message: fmt.Sprintf("宿主 Nginx 指向部署记录的活动端口 %d", deployment.ActivePort), }) } else if committedHealthy { findings = append(findings, finding{ level: daemonapi.DiagnosisLevelFixable, code: codeNginxWrongPort, message: fmt.Sprintf("宿主 Nginx 指向 %d,但部署记录的活动端口是 %d", activePort, deployment.ActivePort), action: fmt.Sprintf("将宿主 Nginx 切流到端口 %d", deployment.ActivePort), fix: &fixAction{kind: fixSwitchPort, port: deployment.ActivePort}, }) } else { findings = append(findings, finding{ level: daemonapi.DiagnosisLevelDrift, code: codeNginxWrongPort, message: fmt.Sprintf("宿主 Nginx 指向 %d,与部署记录活动端口 %d 不一致,且活动容器不健康", activePort, deployment.ActivePort), }) } inactiveSlot, err := d.config.Backend.SlotForPort(otherPort(deployment.ActivePort)) if err != nil { return nil, err } inactiveContainer, inactiveErr := d.engine.InspectContainer(ctx, inactiveSlot.ContainerName) switch { case errors.Is(inactiveErr, containerengine.ErrNotFound): findings = append(findings, finding{ level: daemonapi.DiagnosisLevelOK, code: codeInactiveContainerOK, message: "非活动槽位容器 " + inactiveSlot.ContainerName + " 不存在", }) case inactiveErr != nil: return nil, fmt.Errorf("inspect inactive backend container %s: %w", inactiveSlot.ContainerName, inactiveErr) case inactiveContainer.Running && !inactiveContainer.Dead: findings = append(findings, finding{ level: daemonapi.DiagnosisLevelDrift, code: codeInactiveContainerRunning, message: "非活动槽位容器 " + inactiveSlot.ContainerName + " 意外处于运行状态", }) default: findings = append(findings, finding{ level: daemonapi.DiagnosisLevelFixable, code: codeInactiveContainerResidual, message: "非活动槽位容器 " + inactiveSlot.ContainerName + " 已停止,属于残留", action: "移除残留的已停止容器 " + inactiveSlot.ContainerName, fix: &fixAction{kind: fixRemoveContainer, container: inactiveSlot.ContainerName}, }) } return findings, nil } // containerUndeployedFindings 处理不存在部署记录的情况,依据容器现场与历史记录判断是否为全新机器。 func (d *Diagnoser) containerUndeployedFindings(ctx context.Context, hasHistory bool) ([]finding, error) { var present []string for _, port := range []int{deploymentconfig.BackendPort8080, deploymentconfig.BackendPort8081} { slot, err := d.config.Backend.SlotForPort(port) if err != nil { return nil, err } if _, err := d.engine.InspectContainer(ctx, slot.ContainerName); err == nil { present = append(present, slot.ContainerName) } else if !errors.Is(err, containerengine.ErrNotFound) { return nil, fmt.Errorf("inspect backend container %s: %w", slot.ContainerName, err) } } if len(present) > 0 { return []finding{{ level: daemonapi.DiagnosisLevelDrift, code: codeContainerWithoutRecord, message: fmt.Sprintf("SQLite 没有部署记录,但现场存在容器 %v", present), }}, nil } if hasHistory { return []finding{{ level: daemonapi.DiagnosisLevelDrift, code: codeUndeployedWithHistory, message: "存在历史已提交事务,但缺少部署记录且两个槽位容器均不存在", }}, nil } return []finding{{ level: daemonapi.DiagnosisLevelOK, code: codeUndeployed, message: "全新机器,尚未部署 backend 容器", }}, nil }