Files
kenaito-kopsc/internal/diagnose/docker.go
T
odboy e02f3010cf feat: 扩充 Docker 命令并新增 diagnose 状态诊断
- Docker:新增只读、生命周期、清理、Compose 共 32 条命令,Linux 自动适配 podman
- 危险命令 clearall/prunevolumes 未带 --yes 时仅提示并以退出码 2 结束
- 需要容器/镜像名的命令做必填参数校验,-- 之后参数计入位置参数
- diagnose:自动识别 Docker/Podman 容器或 K8s Pod,输出状态、根因、依据、建议并附日志与事件
- diagnose 中 -n 表示 namespace,其余命令 -n 仍为 --dry-run
- 新增内置动作桥接与平台变体 helper,补充单元/集成/CLI 测试与 README
2026-10-31 14:09:38 +08:00

249 lines
7.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package diagnose
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
)
type dockerHealth struct {
Status string `json:"Status"`
}
type dockerContainer struct {
ID string `json:"Id"`
Name string `json:"Name"`
RestartCount int `json:"RestartCount"`
Config struct {
Image string `json:"Image"`
} `json:"Config"`
State struct {
Status string `json:"Status"`
Running bool `json:"Running"`
Paused bool `json:"Paused"`
Restarting bool `json:"Restarting"`
OOMKilled bool `json:"OOMKilled"`
Dead bool `json:"Dead"`
ExitCode int `json:"ExitCode"`
Error string `json:"Error"`
StartedAt string `json:"StartedAt"`
FinishedAt string `json:"FinishedAt"`
Health *dockerHealth `json:"Health"`
} `json:"State"`
}
func (s *Service) diagnoseDocker(ctx context.Context, src source, target string, opts Options) error {
res, err := s.run(ctx, src.program, "inspect", "--type", "container", target)
if err != nil {
return err
}
var containers []dockerContainer
if err := json.Unmarshal(res.Stdout, &containers); err != nil {
return fmt.Errorf("解析 %s inspect 输出失败: %w", src.program, err)
}
if len(containers) == 0 {
return fmt.Errorf("%s 中没有名为 %q 的容器", src.program, target)
}
c := containers[0]
name := strings.TrimPrefix(c.Name, "/")
if name == "" {
name = target
}
fmt.Fprintf(s.out, "目标: %s\n", name)
fmt.Fprintf(s.out, "来源: Docker (%s)\n", src.program)
fmt.Fprintf(s.out, "状态: %s ExitCode=%d OOMKilled=%t RestartCount=%d\n",
c.State.Status, c.State.ExitCode, c.State.OOMKilled, c.RestartCount)
if c.Config.Image != "" {
fmt.Fprintf(s.out, "镜像: %s\n", c.Config.Image)
}
if c.State.Health != nil && c.State.Health.Status != "" {
fmt.Fprintf(s.out, "健康: %s\n", c.State.Health.Status)
}
if c.State.StartedAt != "" && !strings.HasPrefix(c.State.StartedAt, "0001-") {
fmt.Fprintf(s.out, "时间: %s", c.State.StartedAt)
if c.State.FinishedAt != "" && !strings.HasPrefix(c.State.FinishedAt, "0001-") && c.State.FinishedAt != c.State.StartedAt {
fmt.Fprintf(s.out, " → %s", c.State.FinishedAt)
}
fmt.Fprintln(s.out)
}
s.renderFinding("", judgeDocker(c))
if !opts.NoLogs {
logRes, err := s.run(ctx, src.program, "logs", "--tail", strconv.Itoa(opts.Lines), "--timestamps", target)
if err != nil {
fmt.Fprintf(s.errOut, "警告: 获取日志失败: %v\n", err)
} else {
s.writeSection(fmt.Sprintf("最近日志 (%d 行)", opts.Lines), string(logRes.Stdout))
}
}
if !opts.NoEvents {
now := s.now()
since := now.Add(-time.Hour).UTC().Format(time.RFC3339)
until := now.UTC().Format(time.RFC3339)
eventRes, err := s.run(ctx, src.program, "events",
"--since", since, "--until", until, "--filter", "container="+name)
if err != nil {
fmt.Fprintf(s.errOut, "警告: 获取事件失败: %v\n", err)
} else {
s.writeSection("最近事件 (1h)", tailLines(string(eventRes.Stdout), 20))
}
}
return nil
}
// judgeDocker maps container state to a human-readable root cause.
func judgeDocker(c dockerContainer) Finding {
st := c.State
evidence := "State.Status=" + st.Status
if st.Status == "exited" || st.ExitCode != 0 {
evidence += fmt.Sprintf(", ExitCode=%d", st.ExitCode)
}
if st.OOMKilled {
evidence += ", OOMKilled=true"
}
if c.RestartCount > 0 {
evidence += fmt.Sprintf(", RestartCount=%d", c.RestartCount)
}
if st.Error != "" {
evidence += ", Error=" + truncate(st.Error, 200)
}
switch {
case st.Restarting || st.Status == "restarting":
return Finding{
Severity: "error",
Verdict: "容器处于重启循环,正在反复启动",
Evidence: evidence,
Advice: "查看最近日志与上一次退出码,确认应用是否启动即崩溃或依赖不可用",
}
case st.Status == "exited":
switch {
case st.OOMKilled:
return Finding{
Severity: "error",
Verdict: "容器因内存超限被 OOM kill",
Evidence: evidence,
Advice: "提高容器的内存限制,或降低应用峰值内存;可用 kopsc docker stats 观察",
}
case st.ExitCode == 137:
return Finding{
Severity: "error",
Verdict: "容器收到 SIGKILL(137),可能被手动 kill 或 stop 超时后强杀",
Evidence: evidence,
Advice: "若并非人为操作,检查宿主机 OOM 日志与 stop 超时设置",
}
case st.ExitCode == 143:
return Finding{
Severity: "warn",
Verdict: "容器收到 SIGTERM(143)后退出,通常是正常停止",
Evidence: evidence,
Advice: "若预期继续运行,检查是谁发送了停止信号或重启策略",
}
case st.ExitCode == 126:
return Finding{
Severity: "error",
Verdict: "容器内命令不可执行(126):权限不足或可执行格式错误",
Evidence: evidence,
Advice: "检查 entrypoint/cmd 是否可执行、是否缺少可执行权限",
}
case st.ExitCode == 127:
return Finding{
Severity: "error",
Verdict: "容器内命令不存在(127)",
Evidence: evidence,
Advice: "检查 entrypoint/cmd 与镜像内实际的二进制路径",
}
case st.ExitCode == 0:
if c.RestartCount > 0 {
return Finding{
Severity: "warn",
Verdict: "容器正常退出,但已多次重启,重启策略可能导致循环",
Evidence: evidence,
Advice: "若是任务型容器,确认重启策略应为 no/on-failure 而不是 always",
}
}
return Finding{
Severity: "ok",
Verdict: "容器已正常退出(ExitCode=0)",
Evidence: evidence,
}
default:
return Finding{
Severity: "error",
Verdict: "容器以非零状态退出,应用自身报错",
Evidence: evidence,
Advice: "查看下方日志的末尾,定位应用错误;确认命令与配置参数",
}
}
case st.Status == "running":
if st.Paused {
return Finding{
Severity: "warn",
Verdict: "容器处于暂停(paused)状态",
Evidence: evidence,
Advice: "确认是否有人执行了 docker pause;唤醒使用 docker unpause",
}
}
if st.Health != nil && strings.EqualFold(st.Health.Status, "unhealthy") {
return Finding{
Severity: "error",
Verdict: "容器健康检查失败(unhealthy)",
Evidence: evidence + ", State.Health.Status=unhealthy",
Advice: "查看日志与 healthcheck 命令;确认依赖服务可用、检查间隔与超时合理",
}
}
return Finding{
Severity: "ok",
Verdict: "容器正在运行",
Evidence: evidence,
}
case st.Status == "created":
return Finding{
Severity: "warn",
Verdict: "容器已创建但尚未启动",
Evidence: evidence,
Advice: "使用 kopsc docker start <容器> 启动;若启动失败查看 docker events",
}
case st.Status == "paused":
return Finding{
Severity: "warn",
Verdict: "容器已暂停",
Evidence: evidence,
}
case st.Status == "dead":
return Finding{
Severity: "error",
Verdict: "容器处于 dead 状态,运行时未能正常清理",
Evidence: evidence,
Advice: "通常需要删除后重新创建容器;检查存储驱动与宿主资源",
}
}
return Finding{
Severity: "warn",
Verdict: "容器状态未识别:" + st.Status,
Evidence: evidence,
}
}
func truncate(s string, max int) string {
s = strings.ReplaceAll(strings.TrimSpace(s), "\n", " ")
if len(s) <= max {
return s
}
return s[:max] + "…"
}