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:
2026-10-31 14:09:38 +08:00
parent 84f7361396
commit e02f3010cf
19 changed files with 2465 additions and 69 deletions
+140
View File
@@ -0,0 +1,140 @@
package diagnose
import (
"context"
"fmt"
"strings"
"testing"
)
func TestResolveAutoDocker(t *testing.T) {
inspectJSON := `[{"Id":"abc","Name":"/web","State":{"Status":"running"}}]`
r := &fakeRunner{
available: map[string]bool{"docker": true, "kubectl": true},
respond: func(name string, args []string) (CommandResult, error) {
switch {
case len(args) > 0 && args[0] == "inspect":
return CommandResult{Stdout: []byte(inspectJSON)}, nil
case len(args) > 0 && args[0] == "logs":
return CommandResult{Stdout: []byte("ok\n")}, nil
case len(args) > 0 && args[0] == "events":
return CommandResult{}, nil
}
return CommandResult{}, fmt.Errorf("unexpected call %s %v", name, args)
},
}
s, out, _ := newTestService(r)
if err := s.Run(context.Background(), []string{"web"}); err != nil {
t.Fatalf("Run error: %v", err)
}
if !strings.Contains(out.String(), "来源: Docker (docker)") {
t.Errorf("expected docker source:\n%s", out.String())
}
if r.called("kubectl") {
t.Error("kubectl should not be consulted when docker hits")
}
}
func TestResolveAutoKubernetes(t *testing.T) {
r := &fakeRunner{
available: map[string]bool{"docker": true, "kubectl": true},
respond: func(name string, args []string) (CommandResult, error) {
joined := strings.Join(args, " ")
switch {
case len(args) > 0 && args[0] == "inspect":
return CommandResult{Stdout: []byte("[]")}, nil
case strings.HasPrefix(joined, "config view"):
return CommandResult{}, nil
case strings.HasPrefix(joined, "get pod"):
return CommandResult{Stdout: []byte("pod/api-0\n")}, nil
case strings.HasPrefix(joined, "logs"):
return CommandResult{Stdout: []byte("log\n")}, nil
case strings.HasPrefix(joined, "get events"):
return CommandResult{}, nil
}
return CommandResult{}, fmt.Errorf("unexpected call %s", joined)
},
}
s, out, _ := newTestService(r)
// The fake returns name output for the first pod probe but diagnoseK8s
// needs JSON; swap the responder after resolve would be brittle, so return
// JSON for every get pod call instead.
r.respond = func(name string, args []string) (CommandResult, error) {
joined := strings.Join(args, " ")
switch {
case len(args) > 0 && args[0] == "inspect":
return CommandResult{Stdout: []byte("[]")}, nil
case strings.HasPrefix(joined, "config view"):
return CommandResult{}, nil
case strings.HasPrefix(joined, "get pod"):
return CommandResult{Stdout: []byte(crashLoopPodJSON)}, nil
case strings.HasPrefix(joined, "logs"):
return CommandResult{Stdout: []byte("log\n")}, nil
case strings.HasPrefix(joined, "get events"):
return CommandResult{}, nil
}
return CommandResult{}, fmt.Errorf("unexpected call %s", joined)
}
if err := s.Run(context.Background(), []string{"api-0"}); err != nil {
t.Fatalf("Run error: %v", err)
}
if !strings.Contains(out.String(), "来源: Kubernetes") {
t.Errorf("expected kubernetes source:\n%s", out.String())
}
}
func TestResolveNotFound(t *testing.T) {
r := &fakeRunner{
available: map[string]bool{"docker": true, "kubectl": true},
respond: func(name string, args []string) (CommandResult, error) {
joined := strings.Join(args, " ")
switch {
case len(args) > 0 && args[0] == "inspect":
return CommandResult{Stdout: []byte("[]")}, nil
case strings.HasPrefix(joined, "config view"):
return CommandResult{}, nil
case strings.HasPrefix(joined, "get pod"):
return CommandResult{Stderr: []byte("not found")}, fmt.Errorf("exit status 1")
}
return CommandResult{}, nil
},
}
s, _, _ := newTestService(r)
err := s.Run(context.Background(), []string{"ghost"})
if err == nil || !strings.Contains(err.Error(), "未找到目标") {
t.Fatalf("err = %v, want not-found error", err)
}
}
func TestResolveNamespaceSkipsDocker(t *testing.T) {
r := &fakeRunner{
available: map[string]bool{"docker": true, "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{}, nil
case strings.HasPrefix(joined, "get events"):
return CommandResult{}, nil
}
return CommandResult{}, fmt.Errorf("unexpected call %s", joined)
},
}
s, out, _ := newTestService(r)
if err := s.Run(context.Background(), []string{"api-0", "--namespace", "prod"}); err != nil {
t.Fatalf("Run error: %v", err)
}
if r.called("inspect") {
t.Error("namespace should skip the docker probe")
}
if !strings.Contains(out.String(), "namespace=prod") {
t.Errorf("namespace missing from output:\n%s", out.String())
}
}