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
+115 -2
View File
@@ -15,9 +15,20 @@ func TestBuiltinIsValid(t *testing.T) {
func TestBuiltinIDsStable(t *testing.T) {
reg := Builtin()
want := []string{"docker.clearsafe", "disk.usage", "net.ports", "pkg.refreshindex"}
want := []string{
"docker.ps", "docker.psall", "docker.images", "docker.stats", "docker.df",
"docker.info", "docker.version", "docker.logs", "docker.inspect",
"docker.networks", "docker.volumes",
"docker.start", "docker.stop", "docker.restart", "docker.rm", "docker.rmi",
"docker.exec", "docker.pull", "docker.tag", "docker.save", "docker.load", "docker.cp",
"docker.clearsafe", "docker.pruneimages", "docker.prunecontainers", "docker.prunenets",
"docker.prunebuilder", "docker.prunevolumes", "docker.clearall",
"docker.compose.up", "docker.compose.down", "docker.compose.ps", "docker.compose.logs",
"diagnose",
"disk.usage", "net.ports", "pkg.refreshindex",
}
if got := reg.IDs(); !reflect.DeepEqual(got, want) {
t.Fatalf("IDs() = %v, want %v", got, want)
t.Fatalf("IDs() =\n%v\nwant\n%v", got, want)
}
}
@@ -171,6 +182,108 @@ func TestDiskUsageIsBuiltinEverywhere(t *testing.T) {
}
}
func TestDockerCommandsWellFormed(t *testing.T) {
reg := Builtin()
destructive := map[string]bool{
"docker.clearall": true,
"docker.prunevolumes": true,
}
needsArgs := map[string]int{
"docker.logs": 1,
"docker.inspect": 1,
"docker.start": 1,
"docker.stop": 1,
"docker.restart": 1,
"docker.rm": 1,
"docker.rmi": 1,
"docker.exec": 1,
"docker.pull": 1,
"docker.tag": 2,
"docker.save": 1,
"docker.cp": 2,
}
for i := range reg.Commands {
cmd := &reg.Commands[i]
if cmd.Group != "docker" {
continue
}
if destructive[cmd.ID] != cmd.RequiresYes {
t.Errorf("%s: RequiresYes = %v, want %v", cmd.ID, cmd.RequiresYes, destructive[cmd.ID])
}
if want, ok := needsArgs[cmd.ID]; ok && cmd.MinArgs != want {
t.Errorf("%s: MinArgs = %d, want %d", cmd.ID, cmd.MinArgs, want)
}
if cmd.MinArgs > 0 && strings.TrimSpace(cmd.Usage) == "" {
t.Errorf("%s: missing Usage for MinArgs=%d", cmd.ID, cmd.MinArgs)
}
var podman, docker int
for _, v := range cmd.Variants {
if len(v.Argv) == 0 {
t.Errorf("%s: docker command must have an argv variant", cmd.ID)
continue
}
switch v.Argv[0] {
case "podman":
podman++
case "docker":
docker++
}
if v.Match.Runtime != v.Argv[0] {
t.Errorf("%s: runtime %q does not match program %q", cmd.ID, v.Match.Runtime, v.Argv[0])
}
}
if podman != 1 || docker != 3 {
t.Errorf("%s: variants = %d podman / %d docker, want 1/3", cmd.ID, podman, docker)
}
}
}
func TestValidateArgs(t *testing.T) {
logs := &Command{ID: "docker.logs", Usage: "<容器>", MinArgs: 1}
tag := &Command{ID: "docker.tag", Usage: "<源> <目标>", MinArgs: 2}
cases := []struct {
name string
cmd *Command
args []string
wantErr bool
}{
{"no args", logs, nil, true},
{"only option", logs, []string{"-f"}, true},
{"positional", logs, []string{"web"}, false},
{"option then positional", logs, []string{"-f", "web"}, false},
{"option value counted loosely", logs, []string{"--tail", "100"}, false},
{"interactive option then args", logs, []string{"-it", "web", "sh"}, false},
{"path flag", &Command{ID: "disk.usage", Usage: "<路径>", MinArgs: 1}, []string{"--path", "/var"}, false},
{"after double dash", logs, []string{"--", "-weird-name"}, false},
{"two required", tag, []string{"old:1", "new:1"}, false},
{"two required short", tag, []string{"old:1"}, true},
{"option does not satisfy requirement", tag, []string{"-f", "old:1"}, true},
{"no requirement", &Command{ID: "docker.ps"}, nil, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := tc.cmd.ValidateArgs(tc.args)
if tc.wantErr && err == nil {
t.Fatalf("expected an error for %v", tc.args)
}
if !tc.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err != nil {
msg := err.Error()
if !strings.Contains(msg, "kopsc "+strings.ReplaceAll(tc.cmd.ID, ".", " ")) {
t.Errorf("error lacks usage hint: %q", msg)
}
}
})
}
}
func TestMatchIsComparable(t *testing.T) {
a := Match{OS: "linux", Distro: "centos", DistroLike: []string{"rhel"}, Runtime: "docker"}
b := Match{OS: "linux", Distro: "centos", DistroLike: []string{"rhel"}, Runtime: "docker"}