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
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
package diagnose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const crashLoopPodJSON = `{
|
||||
"metadata":{"name":"api-0","namespace":"prod"},
|
||||
"status":{
|
||||
"phase":"Running",
|
||||
"containerStatuses":[
|
||||
{"name":"app","ready":false,"restartCount":5,
|
||||
"state":{"waiting":{"reason":"CrashLoopBackOff","message":"back-off 5m0s"}},
|
||||
"lastState":{"terminated":{"reason":"Error","exitCode":1,"message":"boom"}}}
|
||||
]
|
||||
}
|
||||
}`
|
||||
|
||||
func waitingContainer(reason string, lastExit int) containerStatus {
|
||||
cs := containerStatus{Name: "app", RestartCount: 2}
|
||||
cs.State.Waiting = &podWaiting{Reason: reason, Message: "message"}
|
||||
if lastExit >= 0 {
|
||||
cs.LastState.Terminated = &podTerminated{Reason: "Error", ExitCode: lastExit}
|
||||
}
|
||||
return cs
|
||||
}
|
||||
|
||||
func TestJudgeContainer(t *testing.T) {
|
||||
oom := containerStatus{Name: "app"}
|
||||
oom.State.Terminated = &podTerminated{Reason: "OOMKilled", ExitCode: 137}
|
||||
|
||||
completed := containerStatus{Name: "app"}
|
||||
completed.State.Terminated = &podTerminated{Reason: "Completed", ExitCode: 0}
|
||||
|
||||
notReady := containerStatus{Name: "app"}
|
||||
notReady.State.Running = &podRunning{}
|
||||
|
||||
ready := containerStatus{Name: "app", Ready: true}
|
||||
ready.State.Running = &podRunning{}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
status containerStatus
|
||||
wantSeverity string
|
||||
wantContains string
|
||||
}{
|
||||
{"crash loop", waitingContainer("CrashLoopBackOff", 1), "error", "CrashLoopBackOff"},
|
||||
{"image pull", waitingContainer("ImagePullBackOff", -1), "error", "镜像拉取"},
|
||||
{"err image pull", waitingContainer("ErrImagePull", -1), "error", "镜像拉取"},
|
||||
{"config error", waitingContainer("CreateContainerConfigError", -1), "error", "配置"},
|
||||
{"creating", waitingContainer("ContainerCreating", -1), "warn", "创建"},
|
||||
{"oom killed", oom, "error", "OOMKilled"},
|
||||
{"completed", completed, "ok", "正常结束"},
|
||||
{"not ready", notReady, "warn", "未就绪"},
|
||||
{"ready", ready, "ok", "已就绪"},
|
||||
{"unknown", containerStatus{Name: "app"}, "warn", "未知"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
f := judgeContainer(tc.status)
|
||||
if f.Severity != tc.wantSeverity {
|
||||
t.Errorf("severity = %q, want %q (verdict %q)", f.Severity, tc.wantSeverity, f.Verdict)
|
||||
}
|
||||
if !strings.Contains(f.Verdict, tc.wantContains) {
|
||||
t.Errorf("verdict %q does not contain %q", f.Verdict, tc.wantContains)
|
||||
}
|
||||
if f.Evidence == "" {
|
||||
t.Error("evidence must not be empty")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJudgePod(t *testing.T) {
|
||||
cases := []struct {
|
||||
phase string
|
||||
wantSeverity string
|
||||
}{
|
||||
{"Pending", "warn"},
|
||||
{"Failed", "error"},
|
||||
{"Succeeded", "ok"},
|
||||
{"Running", "ok"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
var p pod
|
||||
p.Status.Phase = tc.phase
|
||||
if got := judgePod(p); got.Severity != tc.wantSeverity {
|
||||
t.Errorf("phase %s: severity = %q, want %q", tc.phase, got.Severity, tc.wantSeverity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagnoseK8sOutput(t *testing.T) {
|
||||
r := &fakeRunner{
|
||||
available: map[string]bool{"kubectl": true},
|
||||
respond: func(name string, args []string) (CommandResult, error) {
|
||||
joined := strings.Join(args, " ")
|
||||
switch {
|
||||
case strings.HasPrefix(joined, "get pod"):
|
||||
return CommandResult{Stdout: []byte(crashLoopPodJSON)}, nil
|
||||
case strings.HasPrefix(joined, "logs"):
|
||||
return CommandResult{Stdout: []byte("panic: boom\n")}, nil
|
||||
case strings.HasPrefix(joined, "get events"):
|
||||
return CommandResult{Stdout: []byte("0s Warning FailedScheduling insufficient cpu\n")}, nil
|
||||
case strings.HasPrefix(joined, "config view"):
|
||||
return CommandResult{}, nil
|
||||
}
|
||||
return CommandResult{}, fmt.Errorf("unexpected call %s", joined)
|
||||
},
|
||||
}
|
||||
|
||||
s, out, errOut := newTestService(r)
|
||||
err := s.Run(context.Background(), []string{"api-0", "--kind", "k8s", "--namespace", "prod"})
|
||||
if err != nil {
|
||||
t.Fatalf("Run error: %v (stderr=%s)", err, errOut.String())
|
||||
}
|
||||
|
||||
text := out.String()
|
||||
for _, want := range []string{
|
||||
"目标: api-0",
|
||||
"来源: Kubernetes",
|
||||
"namespace=prod",
|
||||
"phase=Running",
|
||||
"容器 app:",
|
||||
"CrashLoopBackOff",
|
||||
"lastState.exitCode=1",
|
||||
"日志 (app, 最近 50 行)",
|
||||
"panic: boom",
|
||||
"日志 (app, 上一次)",
|
||||
"FailedScheduling",
|
||||
"事件提示",
|
||||
} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Errorf("output missing %q:\n%s", want, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagnoseK8sFiltersContainer(t *testing.T) {
|
||||
podJSON := `{
|
||||
"metadata":{"name":"api-0","namespace":"prod"},
|
||||
"status":{"phase":"Running","containerStatuses":[
|
||||
{"name":"one","ready":true,"state":{"running":{}}},
|
||||
{"name":"two","ready":true,"state":{"running":{}}}
|
||||
]}
|
||||
}`
|
||||
|
||||
r := &fakeRunner{
|
||||
available: map[string]bool{"kubectl": true},
|
||||
respond: func(name string, args []string) (CommandResult, error) {
|
||||
joined := strings.Join(args, " ")
|
||||
if strings.HasPrefix(joined, "get pod") {
|
||||
return CommandResult{Stdout: []byte(podJSON)}, nil
|
||||
}
|
||||
return CommandResult{Stdout: []byte("log\n")}, nil
|
||||
},
|
||||
}
|
||||
|
||||
s, out, _ := newTestService(r)
|
||||
if err := s.Run(context.Background(), []string{"api-0", "--kind", "k8s", "-c", "one"}); err != nil {
|
||||
t.Fatalf("Run error: %v", err)
|
||||
}
|
||||
text := out.String()
|
||||
if !strings.Contains(text, "容器 one:") || strings.Contains(text, "容器 two:") {
|
||||
t.Errorf("container filter not applied:\n%s", text)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user