feat: 将更多 Docker 命令改为内置实现,统一按名称/ID 解析目标
- exec/rm/rmi/stop/restart 支持名称、ID 或唯一 ID 前缀,目标不存在或 前缀有歧义时在调用 docker 前报错;exec 默认 -it 并以 -- 分隔命令 - logs 新增 --since/--grep/--save;df 改为按占用排序的可读排行; stats 默认单次快照,无参数时统计全部运行中容器 - 破坏性确认:Command.ConfirmBuiltin 提供将删除对象的清单, rm/rmi 无 --yes 时列清单并退出码 2,拒绝 -f,运行中容器提示先 stop - 抽出 executor/dockertargets.go 统一镜像/容器解析与 docker 调用
This commit is contained in:
+17
-6
@@ -152,9 +152,7 @@ func (a *App) newLeaf(cmd *registry.Command, name string) *cobra.Command {
|
||||
plan := executor.Plan{Result: result, ExtraArgs: extra}
|
||||
|
||||
if cmd.RequiresYes && !yes && !dryRun {
|
||||
a.printConfirmation(cmd, plan)
|
||||
a.exitCode = 2
|
||||
return nil
|
||||
return a.printConfirmation(cmd, plan)
|
||||
}
|
||||
|
||||
return plan.Run(context.Background(), executor.Options{
|
||||
@@ -168,14 +166,27 @@ func (a *App) newLeaf(cmd *registry.Command, name string) *cobra.Command {
|
||||
return leaf
|
||||
}
|
||||
|
||||
// printConfirmation explains a destructive command and how to approve it.
|
||||
func (a *App) printConfirmation(cmd *registry.Command, plan executor.Plan) {
|
||||
// printConfirmation explains a destructive command and how to approve it. If
|
||||
// the command has a preview builtin, the objects it would touch are listed
|
||||
// first (and an unknown target is reported before asking for --yes).
|
||||
func (a *App) printConfirmation(cmd *registry.Command, plan executor.Plan) error {
|
||||
if err := executor.Preview(context.Background(), plan, a.Stdout); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := a.Stdout
|
||||
fmt.Fprintln(w, "这是破坏性命令,未执行。")
|
||||
fmt.Fprintf(w, "命令: %s\n", cmd.Invocation())
|
||||
fmt.Fprintf(w, "将执行: %s\n", plan.Display())
|
||||
fmt.Fprintf(w, "影响: %s\n\n", cmd.Action)
|
||||
fmt.Fprintf(w, "确认执行请追加 --yes:\n %s --yes\n", cmd.Invocation())
|
||||
// Repeat the caller's own arguments so the suggestion is copy-pasteable.
|
||||
suggested := cmd.Invocation() + " --yes"
|
||||
if len(plan.ExtraArgs) > 0 {
|
||||
suggested += " " + strings.Join(plan.ExtraArgs, " ")
|
||||
}
|
||||
fmt.Fprintf(w, "确认执行请追加 --yes:\n %s\n", suggested)
|
||||
a.exitCode = 2
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseLeafArgs splits leaf arguments: kopsc-level flags before the first `--`
|
||||
|
||||
@@ -631,3 +631,105 @@ func TestDockerLoadThroughCLIRequiresArchive(t *testing.T) {
|
||||
t.Errorf("stderr = %q", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
// cliRuntime installs a fake docker answering both `images` and `ps`, and
|
||||
// returns the path of the invocation log.
|
||||
func cliRuntime(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
logPath := filepath.Join(dir, "calls.log")
|
||||
script := "#!/bin/sh\n" +
|
||||
"echo \"$*\" >> " + logPath + "\n" +
|
||||
"case \"$1\" in\n" +
|
||||
" images) cat <<'EOT'\nIMAGE ID\tREPOSITORY\tTAG\tDIGEST\n9c6f07244728\talpine\t3.20\t\nEOT\n" +
|
||||
" ;;\n" +
|
||||
" ps) cat <<'EOT'\n3f2a1b0c9d8e\tweb\tnginx:1.25\tUp 2 hours\nEOT\n" +
|
||||
" ;;\n" +
|
||||
"esac\n"
|
||||
if err := os.WriteFile(filepath.Join(dir, "docker"), []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
return logPath
|
||||
}
|
||||
|
||||
func TestDockerExecThroughCLIDefaultsToInteractive(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("fake runtime is a shell script")
|
||||
}
|
||||
logPath := cliRuntime(t)
|
||||
|
||||
app, _, stderr := testApp(centos("docker"))
|
||||
if code := app.Execute([]string{"docker", "exec", "web", "sh", "-c", "echo hi"}); code != 0 {
|
||||
t.Fatalf("exit code = %d, stderr=%s", code, stderr.String())
|
||||
}
|
||||
data, _ := os.ReadFile(logPath)
|
||||
if !strings.Contains(string(data), "exec -it web -- sh -c echo hi") {
|
||||
t.Errorf("docker calls = %q, want the -it/-- form", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerExecRejectsDashItThroughCLI(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("fake runtime is a shell script")
|
||||
}
|
||||
cliRuntime(t)
|
||||
|
||||
app, _, stderr := testApp(centos("docker"))
|
||||
if code := app.Execute([]string{"docker", "exec", "-it", "web", "sh"}); code == 0 {
|
||||
t.Fatal("expected a non-zero exit code for an explicit -it")
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "已默认以交互方式") {
|
||||
t.Errorf("stderr = %q", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerStopThroughCLIResolvesContainerName(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("fake runtime is a shell script")
|
||||
}
|
||||
logPath := cliRuntime(t)
|
||||
|
||||
app, _, stderr := testApp(centos("docker"))
|
||||
if code := app.Execute([]string{"docker", "stop", "3f2a1b"}); code != 0 {
|
||||
t.Fatalf("exit code = %d, stderr=%s", code, stderr.String())
|
||||
}
|
||||
data, _ := os.ReadFile(logPath)
|
||||
if !strings.Contains(string(data), "stop web") {
|
||||
t.Errorf("docker calls = %q, want `stop web`", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerRMWithoutYesListsTargetsAndExitsTwo(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("fake runtime is a shell script")
|
||||
}
|
||||
logPath := cliRuntime(t)
|
||||
|
||||
app, stdout, stderr := testApp(centos("docker"))
|
||||
if code := app.Execute([]string{"docker", "rm", "web"}); code != 2 {
|
||||
t.Fatalf("exit code = %d, want 2; stderr=%s", code, stderr.String())
|
||||
}
|
||||
out := stdout.String()
|
||||
for _, want := range []string{"将影响:", "3f2a1b0c9d8e", "web", "nginx:1.25", "Up 2 hours", "这是破坏性命令", "--yes"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("confirmation missing %q:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
for _, call := range readCalls(logPath) {
|
||||
if strings.HasPrefix(call, "rm ") {
|
||||
t.Fatalf("docker rm must not run without --yes, calls = %v", readCalls(logPath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readCalls(path string) []string {
|
||||
data, _ := os.ReadFile(path)
|
||||
var out []string
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
if line = strings.TrimSpace(line); line != "" {
|
||||
out = append(out, line)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user