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:
@@ -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 := ®.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"}
|
||||
|
||||
Reference in New Issue
Block a user