Files

206 lines
8.3 KiB
Go

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"
)
const inactiveSlotContainerMessage = "非活动槽位容器 "
// 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: inactiveSlotContainerMessage + 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: inactiveSlotContainerMessage + inactiveSlot.ContainerName + " 意外处于运行状态",
})
default:
findings = append(findings, finding{
level: daemonapi.DiagnosisLevelFixable,
code: codeInactiveContainerResidual,
message: inactiveSlotContainerMessage + 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
}