158 lines
6.3 KiB
Go
158 lines
6.3 KiB
Go
// Package backendstatus 对 backend 组件的现场状态与 SQLite 事实来源做只读诊断,并按需执行自动修复。
|
||
//
|
||
// 诊断遵循“SQLite 是事实来源、外部系统是待校验状态”的原则:status 与 doctor 只读,
|
||
// 不修改任何现场;reconcile 默认只生成修复计划,仅在显式要求 apply 时执行经过明确授权的
|
||
// 自动修复动作,且每个修复动作都先写入事务再执行外部变更。
|
||
package backendstatus
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
|
||
"yms-daemon/internal/containerengine"
|
||
"yms-daemon/internal/daemonapi"
|
||
"yms-daemon/internal/deploymentconfig"
|
||
"yms-daemon/internal/hostnginx"
|
||
"yms-daemon/internal/systemd"
|
||
"yms-daemon/internal/transaction"
|
||
)
|
||
|
||
// gateway 抽象宿主 Nginx 配置的读取与切流,便于在测试中替换。
|
||
type gateway interface {
|
||
// Read 返回当前完整配置快照及其活动后端端口。
|
||
Read() (hostnginx.Snapshot, error)
|
||
// Switch 把活动后端端口切到指定端口并重载 Nginx。
|
||
Switch(context.Context, int) (hostnginx.Snapshot, error)
|
||
}
|
||
|
||
// Diagnoser 持有执行诊断与自动修复所需的全部依赖。
|
||
// 除 store、coordinator、gateway 外,其余依赖按组件运行类型选择性使用:container 使用
|
||
// engine,native 使用 units,未使用的依赖可以为 nil。
|
||
type Diagnoser struct {
|
||
// config 已校验的本地部署配置,决定组件运行类型与槽位信息。
|
||
config deploymentconfig.Config
|
||
// store 服务端 SQLite 事务存储,是诊断的事实来源。
|
||
store *transaction.Store
|
||
// coordinator 用于在 reconcile --apply 时以可恢复方式执行单个修复步骤。
|
||
coordinator *transaction.Coordinator
|
||
// engine 容器引擎,container 后端诊断时使用。
|
||
engine containerengine.Engine
|
||
// units systemd 管理器,native 后端诊断时使用。
|
||
units systemd.Manager
|
||
// gateway 宿主 Nginx 配置控制器,用于读取活动端口与执行切流修复。
|
||
gateway gateway
|
||
}
|
||
|
||
// New 构造一个 Diagnoser 并校验依赖。
|
||
// config 必须已经通过 deploymentconfig.Validate;store、coordinator、gateway 不能为空;
|
||
// 当 backend.type 为 container 时 engine 不能为空,为 native 时 units 不能为空。
|
||
func New(config deploymentconfig.Config, store *transaction.Store, coordinator *transaction.Coordinator, engine containerengine.Engine, units systemd.Manager, gateway gateway) (*Diagnoser, error) {
|
||
if err := config.Validate(); err != nil {
|
||
return nil, err
|
||
}
|
||
if store == nil || coordinator == nil || gateway == nil {
|
||
return nil, errors.New("backend status dependencies are required")
|
||
}
|
||
switch config.Backend.Type {
|
||
case deploymentconfig.BackendTypeContainer:
|
||
if engine == nil {
|
||
return nil, errors.New("container backend diagnosis requires a container engine")
|
||
}
|
||
case deploymentconfig.BackendTypeNative:
|
||
if units == nil {
|
||
return nil, errors.New("native backend diagnosis requires a systemd manager")
|
||
}
|
||
default:
|
||
return nil, fmt.Errorf("unsupported backend.type %q", config.Backend.Type)
|
||
}
|
||
return &Diagnoser{config: config, store: store, coordinator: coordinator, engine: engine, units: units, gateway: gateway}, nil
|
||
}
|
||
|
||
// Diagnose 执行只读诊断并返回结构化结果,不修改任何现场状态。
|
||
// 返回的 daemonapi.Diagnosis 中,Healthy 为 true 表示不存在需要人工处理的漂移项。
|
||
func (d *Diagnoser) Diagnose(ctx context.Context) (daemonapi.Diagnosis, error) {
|
||
switch d.config.Backend.Type {
|
||
case deploymentconfig.BackendTypeContainer:
|
||
return d.diagnoseContainer(ctx)
|
||
case deploymentconfig.BackendTypeNative:
|
||
return d.diagnoseNative(ctx)
|
||
default:
|
||
return daemonapi.Diagnosis{}, fmt.Errorf("unsupported backend.type %q", d.config.Backend.Type)
|
||
}
|
||
}
|
||
|
||
// Reconcile 生成修复计划(apply 为 false)或执行自动修复(apply 为 true)。
|
||
// apply 为 false 时行为与 Diagnose 一致,仅返回只读诊断;apply 为 true 时先诊断,
|
||
// 对每个可自动修复项执行修复动作,完成后重新诊断并返回修复后的结果。
|
||
func (d *Diagnoser) Reconcile(ctx context.Context, apply bool) (daemonapi.Diagnosis, error) {
|
||
if !apply {
|
||
return d.Diagnose(ctx)
|
||
}
|
||
switch d.config.Backend.Type {
|
||
case deploymentconfig.BackendTypeContainer:
|
||
return d.reconcileContainer(ctx)
|
||
case deploymentconfig.BackendTypeNative:
|
||
return d.reconcileNative(ctx)
|
||
default:
|
||
return daemonapi.Diagnosis{}, fmt.Errorf("unsupported backend.type %q", d.config.Backend.Type)
|
||
}
|
||
}
|
||
|
||
// otherPort 返回给定端口的另一个蓝绿槽位端口。
|
||
func otherPort(port int) int {
|
||
if port == deploymentconfig.BackendPort8080 {
|
||
return deploymentconfig.BackendPort8081
|
||
}
|
||
return deploymentconfig.BackendPort8080
|
||
}
|
||
|
||
// finding 内部诊断结论,比协议层的 DiagnosisItem 多携带一个可选的修复参数。
|
||
type finding struct {
|
||
// level 诊断级别,取值 daemonapi.DiagnosisLevelOK / DiagnosisLevelFixable / DiagnosisLevelDrift。
|
||
level string
|
||
// code 稳定标识,供测试与 reconcile 识别。
|
||
code string
|
||
// message 人类可读的诊断描述。
|
||
message string
|
||
// action 仅在 level 为 fixable 时给出建议动作描述。
|
||
action string
|
||
// fix 描述可自动修复动作,仅 fixable 项非 nil。
|
||
fix *fixAction
|
||
}
|
||
|
||
// fixAction 描述一个可自动修复动作所需的精确参数。
|
||
type fixAction struct {
|
||
// kind 修复动作种类,取值 fixSwitchPort 或 fixRemoveContainer。
|
||
kind string
|
||
// port 切流修复的目标端口。
|
||
port int
|
||
// container 容器移除修复的目标容器名。
|
||
container string
|
||
}
|
||
|
||
const (
|
||
// fixSwitchPort 表示把宿主 Nginx 切流到指定端口。
|
||
fixSwitchPort = "nginx-switch"
|
||
// fixRemoveContainer 表示移除残留的已停止容器。
|
||
fixRemoveContainer = "container-remove"
|
||
)
|
||
|
||
// toDiagnosis 把内部 finding 列表转换为协议层诊断结果,并据此计算 Healthy 标志。
|
||
func toDiagnosis(service string, runtimeType string, findings []finding) daemonapi.Diagnosis {
|
||
items := make([]daemonapi.DiagnosisItem, 0, len(findings))
|
||
healthy := true
|
||
for _, f := range findings {
|
||
if f.level == daemonapi.DiagnosisLevelDrift {
|
||
healthy = false
|
||
}
|
||
items = append(items, daemonapi.DiagnosisItem{
|
||
Level: f.level,
|
||
Code: f.code,
|
||
Message: f.message,
|
||
Action: f.action,
|
||
})
|
||
}
|
||
return daemonapi.Diagnosis{Service: service, Type: runtimeType, Healthy: healthy, Items: items}
|
||
}
|