204 lines
8.2 KiB
Go
204 lines
8.2 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"
|
||
|
|
)
|
||
|
|
|
||
|
|
// 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
|
||
|
|
}
|