Files
kenaito-kopsc/internal/diagnose/k8s_test.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

172 lines
5.1 KiB
Go

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)
}
}