refactor: split CLI diagnosis and status rendering

This commit is contained in:
2026-08-22 15:58:37 +08:00
parent 3f13130b8a
commit 076167d3ad
2 changed files with 125 additions and 102 deletions
+5 -3
View File
@@ -28,6 +28,8 @@ const (
codeDeploymentConfigMismatch = "deployment_record_config_mismatch" codeDeploymentConfigMismatch = "deployment_record_config_mismatch"
) )
const inactiveSlotContainerMessage = "非活动槽位容器 "
// diagnoseContainer 对 container 后端做只读诊断。 // diagnoseContainer 对 container 后端做只读诊断。
// 事实来源是 backend_container_deployment 单例记录;现场状态由容器引擎与宿主 Nginx 提供。 // 事实来源是 backend_container_deployment 单例记录;现场状态由容器引擎与宿主 Nginx 提供。
func (d *Diagnoser) diagnoseContainer(ctx context.Context) (daemonapi.Diagnosis, error) { func (d *Diagnoser) diagnoseContainer(ctx context.Context) (daemonapi.Diagnosis, error) {
@@ -143,7 +145,7 @@ func (d *Diagnoser) containerFindings(ctx context.Context, deployment transactio
findings = append(findings, finding{ findings = append(findings, finding{
level: daemonapi.DiagnosisLevelOK, level: daemonapi.DiagnosisLevelOK,
code: codeInactiveContainerOK, code: codeInactiveContainerOK,
message: "非活动槽位容器 " + inactiveSlot.ContainerName + " 不存在", message: inactiveSlotContainerMessage + inactiveSlot.ContainerName + " 不存在",
}) })
case inactiveErr != nil: case inactiveErr != nil:
return nil, fmt.Errorf("inspect inactive backend container %s: %w", inactiveSlot.ContainerName, inactiveErr) return nil, fmt.Errorf("inspect inactive backend container %s: %w", inactiveSlot.ContainerName, inactiveErr)
@@ -151,13 +153,13 @@ func (d *Diagnoser) containerFindings(ctx context.Context, deployment transactio
findings = append(findings, finding{ findings = append(findings, finding{
level: daemonapi.DiagnosisLevelDrift, level: daemonapi.DiagnosisLevelDrift,
code: codeInactiveContainerRunning, code: codeInactiveContainerRunning,
message: "非活动槽位容器 " + inactiveSlot.ContainerName + " 意外处于运行状态", message: inactiveSlotContainerMessage + inactiveSlot.ContainerName + " 意外处于运行状态",
}) })
default: default:
findings = append(findings, finding{ findings = append(findings, finding{
level: daemonapi.DiagnosisLevelFixable, level: daemonapi.DiagnosisLevelFixable,
code: codeInactiveContainerResidual, code: codeInactiveContainerResidual,
message: "非活动槽位容器 " + inactiveSlot.ContainerName + " 已停止,属于残留", message: inactiveSlotContainerMessage + inactiveSlot.ContainerName + " 已停止,属于残留",
action: "移除残留的已停止容器 " + inactiveSlot.ContainerName, action: "移除残留的已停止容器 " + inactiveSlot.ContainerName,
fix: &fixAction{kind: fixRemoveContainer, container: inactiveSlot.ContainerName}, fix: &fixAction{kind: fixRemoveContainer, container: inactiveSlot.ContainerName},
}) })
+120 -99
View File
@@ -67,87 +67,13 @@ func run(ctx context.Context, arguments []string, stdout io.Writer, stderr io.Wr
} }
switch arguments[0] { switch arguments[0] {
case "update": case "update":
request, err := parseUpdateArgs(arguments[1:], stderr) return runUpdate(ctx, arguments[1:], stdout, stderr)
if err != nil {
fmt.Fprintln(stderr, err)
return 2
}
var progress func(daemonapi.Response)
if !request.quite {
progress = func(event daemonapi.Response) {
writeUpdateProgress(stdout, event)
}
}
var response daemonapi.Response
if request.inputType == daemonapi.InputTypeContainerImage {
response, err = daemonclient.UpdateContainerImage(ctx, runtimepaths.Socket, request.service, request.imageReference, request.startLog, progress)
} else {
response, err = daemonclient.Update(ctx, runtimepaths.Socket, request.service, request.inputType, request.file, progress)
}
if err != nil {
if response.TransactionID != "" {
fmt.Fprintf(stderr, "transaction=%s state=%s error=%v\n", response.TransactionID, response.State, err)
} else {
fmt.Fprintln(stderr, "ymsctl update failed:", err)
}
return 1
}
if !request.quite {
fmt.Fprintf(stdout, "transaction=%s state=%s\n", response.TransactionID, response.State)
}
return 0
case "list": case "list":
if err := runList(ctx, arguments[1:], stdout, stderr); err != nil { return runListCommand(ctx, arguments[1:], stdout, stderr)
fmt.Fprintln(stderr, "ymsctl list failed:", err)
return 1
}
return 0
case "restart": case "restart":
request, err := parseRestartArgs(arguments[1:], stderr) return runRestart(ctx, arguments[1:], stdout, stderr)
if err != nil {
fmt.Fprintln(stderr, err)
return 2
}
var progress func(daemonapi.Response)
if !request.quite {
progress = func(event daemonapi.Response) {
writeUpdateProgress(stdout, event)
}
}
response, err := daemonclient.Restart(ctx, runtimepaths.Socket, request.service, progress)
if err != nil {
if response.TransactionID != "" {
fmt.Fprintf(stderr, "transaction=%s state=%s error=%v\n", response.TransactionID, response.State, err)
} else {
fmt.Fprintln(stderr, "ymsctl restart failed:", err)
}
return 1
}
if !request.quite {
fmt.Fprintf(stdout, "transaction=%s state=%s\n", response.TransactionID, response.State)
}
return 0
case "status", "doctor", "reconcile": case "status", "doctor", "reconcile":
request, err := parseDiagnosisArgs(arguments[0], arguments[1:], stderr) return runDiagnosis(ctx, arguments[0], arguments[1:], stdout, stderr)
if err != nil {
fmt.Fprintln(stderr, err)
return 2
}
var diagnosis daemonapi.Diagnosis
switch arguments[0] {
case "status":
diagnosis, err = daemonclient.Status(ctx, runtimepaths.Socket, request.service)
case "doctor":
diagnosis, err = daemonclient.Doctor(ctx, runtimepaths.Socket, request.service)
case "reconcile":
diagnosis, err = daemonclient.Reconcile(ctx, runtimepaths.Socket, request.service, request.apply)
}
if err != nil {
fmt.Fprintf(stderr, "ymsctl %s failed: %v\n", arguments[0], err)
return 1
}
writeDiagnosis(stdout, arguments[0], diagnosis)
return 0
case "help", "-h", "--help": case "help", "-h", "--help":
writeUsage(stdout) writeUsage(stdout)
return 0 return 0
@@ -158,6 +84,96 @@ func run(ctx context.Context, arguments []string, stdout io.Writer, stderr io.Wr
} }
} }
func runUpdate(ctx context.Context, arguments []string, stdout io.Writer, stderr io.Writer) int {
request, err := parseUpdateArgs(arguments, stderr)
if err != nil {
fmt.Fprintln(stderr, err)
return 2
}
var progress func(daemonapi.Response)
if !request.quite {
progress = func(event daemonapi.Response) {
writeUpdateProgress(stdout, event)
}
}
var response daemonapi.Response
if request.inputType == daemonapi.InputTypeContainerImage {
response, err = daemonclient.UpdateContainerImage(ctx, runtimepaths.Socket, request.service, request.imageReference, request.startLog, progress)
} else {
response, err = daemonclient.Update(ctx, runtimepaths.Socket, request.service, request.inputType, request.file, progress)
}
if err != nil {
if response.TransactionID != "" {
fmt.Fprintf(stderr, "transaction=%s state=%s error=%v\n", response.TransactionID, response.State, err)
} else {
fmt.Fprintln(stderr, "ymsctl update failed:", err)
}
return 1
}
if !request.quite {
fmt.Fprintf(stdout, "transaction=%s state=%s\n", response.TransactionID, response.State)
}
return 0
}
func runListCommand(ctx context.Context, arguments []string, stdout io.Writer, stderr io.Writer) int {
if err := runList(ctx, arguments, stdout, stderr); err != nil {
fmt.Fprintln(stderr, "ymsctl list failed:", err)
return 1
}
return 0
}
func runRestart(ctx context.Context, arguments []string, stdout io.Writer, stderr io.Writer) int {
request, err := parseRestartArgs(arguments, stderr)
if err != nil {
fmt.Fprintln(stderr, err)
return 2
}
var progress func(daemonapi.Response)
if !request.quite {
progress = func(event daemonapi.Response) {
writeUpdateProgress(stdout, event)
}
}
response, err := daemonclient.Restart(ctx, runtimepaths.Socket, request.service, progress)
if err != nil {
if response.TransactionID != "" {
fmt.Fprintf(stderr, "transaction=%s state=%s error=%v\n", response.TransactionID, response.State, err)
} else {
fmt.Fprintln(stderr, "ymsctl restart failed:", err)
}
return 1
}
if !request.quite {
fmt.Fprintf(stdout, "transaction=%s state=%s\n", response.TransactionID, response.State)
}
return 0
}
func runDiagnosis(ctx context.Context, command string, arguments []string, stdout io.Writer, stderr io.Writer) int {
request, err := parseDiagnosisArgs(command, arguments, stderr)
if err != nil {
fmt.Fprintln(stderr, err)
return 2
}
var diagnosis daemonapi.Diagnosis
switch command {
case "status":
diagnosis, err = daemonclient.Status(ctx, runtimepaths.Socket, request.service)
case "doctor":
diagnosis, err = daemonclient.Doctor(ctx, runtimepaths.Socket, request.service)
case "reconcile":
diagnosis, err = daemonclient.Reconcile(ctx, runtimepaths.Socket, request.service, request.apply)
}
if err != nil {
fmt.Fprintf(stderr, "ymsctl %s failed: %v\n", command, err)
return 1
}
writeDiagnosis(stdout, command, diagnosis)
return 0
}
// updateArguments 保存 update 子命令解析后的参数。 // updateArguments 保存 update 子命令解析后的参数。
type updateArguments struct { type updateArguments struct {
// service 目标服务名,当前仅接受 backend。 // service 目标服务名,当前仅接受 backend。
@@ -392,33 +408,15 @@ func writeUpdateProgress(output io.Writer, event daemonapi.Response) {
// status 只展示需要关注的漂移与可修复项,doctor 展示全部诊断项, // status 只展示需要关注的漂移与可修复项,doctor 展示全部诊断项,
// reconcile 只展示可自动修复项作为修复计划。 // reconcile 只展示可自动修复项作为修复计划。
func writeDiagnosis(output io.Writer, command string, diagnosis daemonapi.Diagnosis) { func writeDiagnosis(output io.Writer, command string, diagnosis daemonapi.Diagnosis) {
state := "HEALTHY" fmt.Fprintf(output, "service=%s type=%s status=%s\n", diagnosis.Service, diagnosis.Type, diagnosisState(diagnosis))
if !diagnosis.Healthy {
state = "DRIFT"
} else {
for _, item := range diagnosis.Items {
if item.Level == daemonapi.DiagnosisLevelFixable {
state = "REPAIRABLE"
break
}
}
}
fmt.Fprintf(output, "service=%s type=%s status=%s\n", diagnosis.Service, diagnosis.Type, state)
if diagnosis.RepairApplied { if diagnosis.RepairApplied {
fmt.Fprintf(output, "APPLIED 对账修复已执行 transaction=%s\n", diagnosis.RepairTransactionID) fmt.Fprintf(output, "APPLIED 对账修复已执行 transaction=%s\n", diagnosis.RepairTransactionID)
fmt.Fprintln(output, "RESULT 现场已重新核对") fmt.Fprintln(output, "RESULT 现场已重新核对")
} }
shown := 0 shown := 0
for _, item := range diagnosis.Items { for _, item := range diagnosis.Items {
switch command { if !showDiagnosisItem(command, item.Level) {
case "status": continue
if item.Level == daemonapi.DiagnosisLevelOK {
continue
}
case "reconcile":
if item.Level != daemonapi.DiagnosisLevelFixable {
continue
}
} }
shown++ shown++
fmt.Fprintf(output, "%-8s %s\n", strings.ToUpper(item.Level), item.Message) fmt.Fprintf(output, "%-8s %s\n", strings.ToUpper(item.Level), item.Message)
@@ -433,6 +431,29 @@ func writeDiagnosis(output io.Writer, command string, diagnosis daemonapi.Diagno
} }
} }
func diagnosisState(diagnosis daemonapi.Diagnosis) string {
if !diagnosis.Healthy {
return "DRIFT"
}
for _, item := range diagnosis.Items {
if item.Level == daemonapi.DiagnosisLevelFixable {
return "REPAIRABLE"
}
}
return "HEALTHY"
}
func showDiagnosisItem(command string, level string) bool {
switch command {
case "status":
return level != daemonapi.DiagnosisLevelOK
case "reconcile":
return level == daemonapi.DiagnosisLevelFixable
default:
return true
}
}
// runList 执行 list 子命令:解析过滤参数,打开事务存储读取最近事务, // runList 执行 list 子命令:解析过滤参数,打开事务存储读取最近事务,
// 按是否指定 --json 决定以稳定 JSON 或制表符分隔的表格形式输出到 stdout。 // 按是否指定 --json 决定以稳定 JSON 或制表符分隔的表格形式输出到 stdout。
// 它支持 --limit、--service、--state、--json 四个选项,返回查询或输出阶段的错误。 // 它支持 --limit、--service、--state、--json 四个选项,返回查询或输出阶段的错误。