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:
@@ -0,0 +1,230 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
// runDockerRM implements `docker rm` over kopsc-resolved targets: containers
|
||||
// are named by name or ID (or a unique ID prefix), and are resolved before
|
||||
// docker is invoked so a typo fails with kopsc's own message instead of
|
||||
// docker's.
|
||||
//
|
||||
// The `${targets}` placeholder in the command's BaseArgs is replaced with the
|
||||
// resolved names; everything the caller typed after the targets is forwarded.
|
||||
func runDockerRM(ctx context.Context, req BuiltinRequest) error {
|
||||
targets, passthrough, err := splitDockerTargets(req.Args, rmValueOption)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return errors.New("缺少容器;用法: kopsc docker rm <容器>...")
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(targets))
|
||||
for _, target := range targets {
|
||||
ref, err := resolveContainerRef(ctx, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ref.Running() {
|
||||
return &ContainerRunningError{Name: ref.Names}
|
||||
}
|
||||
names = append(names, ref.Names)
|
||||
}
|
||||
|
||||
args := append([]string{"rm"}, names...)
|
||||
args = append(args, passthrough...)
|
||||
return dockerRun(ctx, args, req)
|
||||
}
|
||||
|
||||
// runDockerRMI implements `docker rmi` over kopsc-resolved targets: images are
|
||||
// named by name, ID, sha256:-prefixed ID or unique ID prefix. An image that is
|
||||
// only known by ID without a tag (a dangling image) is still removed — by its
|
||||
// ID, since it has no name.
|
||||
func runDockerRMI(ctx context.Context, req BuiltinRequest) error {
|
||||
targets, passthrough, err := splitDockerTargets(req.Args, rmiValueOption)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return errors.New("缺少镜像;用法: kopsc docker rmi <镜像名|镜像ID>...")
|
||||
}
|
||||
|
||||
refs, err := listImages(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
names := make([]string, 0, len(targets))
|
||||
for _, target := range targets {
|
||||
ref, err := lookupImageRef(refs, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ref.Name != "" {
|
||||
names = append(names, ref.Name)
|
||||
} else {
|
||||
names = append(names, ref.ID)
|
||||
}
|
||||
}
|
||||
|
||||
args := append([]string{"rmi"}, names...)
|
||||
args = append(args, passthrough...)
|
||||
return dockerRun(ctx, args, req)
|
||||
}
|
||||
|
||||
// ContainerRunningError reports a container that must be stopped before it
|
||||
// can be removed. docker rm refuses running containers anyway; reporting it
|
||||
// here gives the user the exact next command.
|
||||
type ContainerRunningError struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (e *ContainerRunningError) Error() string {
|
||||
return "容器 " + e.Name + " 正在运行,docker rm 不会强制删除;请先执行 kopsc docker stop " + e.Name
|
||||
}
|
||||
|
||||
// rmValueOption and rmiValueOption list options that take a separate value, so
|
||||
// the value is not mistaken for a target.
|
||||
var (
|
||||
rmValueOption = map[string]bool{}
|
||||
rmiValueOption = map[string]bool{"platform": true}
|
||||
)
|
||||
|
||||
// splitDockerTargets separates positional targets from options. Options are
|
||||
// collected in passthrough; an option that takes a value keeps its value with
|
||||
// it. A `--` ends option parsing, so a target may not start with "-".
|
||||
func splitDockerTargets(args []string, valueOptions map[string]bool) (targets, passthrough []string, err error) {
|
||||
if err := rejectForce(args, "不再接受 -f;需要强制删除请改用 --yes(kopsc 会在执行前列出将删除的对象)"); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
afterDashDash := false
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case afterDashDash:
|
||||
targets = append(targets, arg)
|
||||
case arg == "--":
|
||||
afterDashDash = true
|
||||
case !strings.HasPrefix(arg, "-") || arg == "-":
|
||||
targets = append(targets, arg)
|
||||
default:
|
||||
passthrough = append(passthrough, arg)
|
||||
if !strings.Contains(arg, "=") && valueOptions[strings.TrimLeft(arg, "-")] {
|
||||
if i+1 < len(args) {
|
||||
i++
|
||||
passthrough = append(passthrough, args[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return targets, passthrough, nil
|
||||
}
|
||||
|
||||
// rejectForce refuses docker's -f/--force up to the first `--`: kopsc shows
|
||||
// what will be deleted and takes --yes as the confirmation, so a force flag
|
||||
// would only duplicate (or, with -f, silently bypass) that contract.
|
||||
func rejectForce(args []string, message string) error {
|
||||
for _, arg := range args {
|
||||
if arg == "--" {
|
||||
return nil
|
||||
}
|
||||
// Bundled short flags such as -fv still carry the force flag.
|
||||
if arg == "--force" || arg == "--force=true" || arg == "-f" ||
|
||||
(strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") && strings.Contains(arg, "f")) {
|
||||
return errors.New(message)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runDockerPreviewRM lists the containers `docker rm` would delete: ID, name,
|
||||
// image and status, one per line. It never touches the containers, and it
|
||||
// reports an unknown target before the user is asked for --yes.
|
||||
func runDockerPreviewRM(ctx context.Context, req BuiltinRequest) error {
|
||||
targets, _, err := splitDockerTargets(req.Args, rmValueOption)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return errors.New("缺少容器;用法: kopsc docker rm <容器>...")
|
||||
}
|
||||
|
||||
refs, err := listContainers(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tw := tabwriter.NewWriter(req.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "ID\t名称\t镜像\t状态")
|
||||
for _, target := range targets {
|
||||
ref, err := matchContainerRef(refs, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", shortID(ref.ID), ref.Names, ref.Image, ref.Status)
|
||||
if ref.Running() {
|
||||
fmt.Fprintf(tw, "\t\t\t(运行中:需先 kopsc docker stop %s)\n", ref.Names)
|
||||
}
|
||||
}
|
||||
return tw.Flush()
|
||||
}
|
||||
|
||||
// runDockerPreviewRMI lists the images `docker rmi` would delete: name, ID,
|
||||
// creation time and size.
|
||||
func runDockerPreviewRMI(ctx context.Context, req BuiltinRequest) error {
|
||||
targets, _, err := splitDockerTargets(req.Args, rmiValueOption)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return errors.New("缺少镜像;用法: kopsc docker rmi <镜像名|镜像ID>...")
|
||||
}
|
||||
|
||||
refs, err := listImages(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tw := tabwriter.NewWriter(req.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "镜像\tID\t创建\t大小")
|
||||
for _, target := range targets {
|
||||
ref, err := lookupImageRef(refs, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := ref.Name
|
||||
if name == "" {
|
||||
name = "<none>(悬空镜像)"
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", name, shortID(ref.ID), formatCreated(ref.CreatedAt), ref.Size)
|
||||
}
|
||||
return tw.Flush()
|
||||
}
|
||||
|
||||
// shortID trims docker's 12-character ID for display, keeping shorter IDs.
|
||||
func shortID(id string) string {
|
||||
id = strings.TrimPrefix(id, "sha256:")
|
||||
if len(id) > 12 {
|
||||
return id[:12]
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// formatCreated renders docker's creation timestamp compactly.
|
||||
func formatCreated(created string) string {
|
||||
created = strings.TrimSpace(created)
|
||||
if i := strings.IndexByte(created, '.'); i > 0 && strings.Contains(created[:i], "T") == false {
|
||||
return created
|
||||
}
|
||||
// Docker prints "2024-10-01 12:34:56 +0800 CST"; drop the timezone suffix.
|
||||
if i := strings.IndexByte(created, ' '); i > 0 {
|
||||
rest := created[i+1:]
|
||||
if j := strings.IndexByte(rest, ' '); j > 0 && (strings.Contains(rest, "+") || strings.Contains(rest, "-")) {
|
||||
return created[:i+1+j]
|
||||
}
|
||||
}
|
||||
return created
|
||||
}
|
||||
Reference in New Issue
Block a user