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,177 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// runDockerLogs implements `kopsc docker logs <容器> [docker 选项...]`.
|
||||
//
|
||||
// `--since`, `--grep` and `--save` are kopsc's own options and must appear
|
||||
// before the container name; everything from the container name on is passed
|
||||
// to docker verbatim, so `-f`, `--tail` and friends keep working. When no
|
||||
// kopsc-only option is used the logs stream straight through; with --grep or
|
||||
// --save they are filtered and/or written to a file while still being shown.
|
||||
func runDockerLogs(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, target, rest, err := parseDockerLogs(req.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if target == "" {
|
||||
return errors.New("缺少容器;用法: kopsc docker logs <容器> [选项...]")
|
||||
}
|
||||
|
||||
args := append([]string{"logs", target}, rest...)
|
||||
if opts.grep == nil && opts.save == "" {
|
||||
return dockerRun(ctx, args, req)
|
||||
}
|
||||
return streamLogs(ctx, args, opts, req)
|
||||
}
|
||||
|
||||
// logsOptions are kopsc's private options for `docker logs`.
|
||||
type logsOptions struct {
|
||||
grep *regexp.Regexp // keep only lines matching this pattern
|
||||
save string // append the displayed log to this file
|
||||
}
|
||||
|
||||
// parseDockerLogs splits kopsc's options (before the container) from the
|
||||
// container name and the arguments forwarded to docker (after it).
|
||||
//
|
||||
// --since is accepted on either side of the container name; kopsc turns it
|
||||
// into docker's own --since, so it reaches docker exactly once.
|
||||
func parseDockerLogs(args []string) (opts logsOptions, target string, rest []string, err error) {
|
||||
i := 0
|
||||
for ; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
|
||||
if arg == "--" {
|
||||
i++
|
||||
break
|
||||
}
|
||||
if !strings.HasPrefix(arg, "-") || arg == "-" {
|
||||
break
|
||||
}
|
||||
|
||||
name, value := arg, ""
|
||||
if eq := strings.IndexByte(arg, '='); eq >= 0 && strings.HasPrefix(arg, "--") {
|
||||
name, value = arg[:eq], arg[eq+1:]
|
||||
}
|
||||
|
||||
takeValue := func() (string, error) {
|
||||
if value != "" {
|
||||
return value, nil
|
||||
}
|
||||
if i+1 >= len(args) {
|
||||
return "", fmt.Errorf("%s 缺少参数", name)
|
||||
}
|
||||
i++
|
||||
return args[i], nil
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "--grep":
|
||||
pattern, err := takeValue()
|
||||
if err != nil {
|
||||
return opts, "", nil, err
|
||||
}
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return opts, "", nil, fmt.Errorf("--grep 的正则表达式无效: %w", err)
|
||||
}
|
||||
opts.grep = re
|
||||
case "--save":
|
||||
save, err := takeValue()
|
||||
if err != nil {
|
||||
return opts, "", nil, err
|
||||
}
|
||||
opts.save = save
|
||||
case "--since":
|
||||
since, err := takeValue()
|
||||
if err != nil {
|
||||
return opts, "", nil, err
|
||||
}
|
||||
rest = append(rest, "--since", since)
|
||||
default:
|
||||
// A docker option typed before the container name: hand it to
|
||||
// docker, keeping any separate value attached to it.
|
||||
rest = append(rest, arg)
|
||||
if value == "" && dockerOptionTakesValue(name) {
|
||||
next, err := takeValue()
|
||||
if err != nil {
|
||||
return opts, "", nil, err
|
||||
}
|
||||
rest = append(rest, next)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if i >= len(args) {
|
||||
return opts, "", rest, nil
|
||||
}
|
||||
return opts, args[i], append(rest, args[i+1:]...), nil
|
||||
}
|
||||
|
||||
// dockerOptionTakesValue lists docker logs options that take a separate
|
||||
// value, so the value is not mistaken for the container name.
|
||||
func dockerOptionTakesValue(arg string) bool {
|
||||
switch arg {
|
||||
case "--tail", "-n", "--since", "-s", "--until", "--details", "--timestamp":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// streamLogs runs docker logs, relaying it line by line so --grep can filter
|
||||
// and --save can capture the log while it is still displayed.
|
||||
func streamLogs(ctx context.Context, args []string, opts logsOptions, req BuiltinRequest) error {
|
||||
cmd, out, err := startDockerPipe(ctx, args, req.Stderr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var file *os.File
|
||||
if opts.save != "" {
|
||||
if file, err = openLogSink(opts.save); err != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(out)
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if opts.grep != nil && !opts.grep.MatchString(line) {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintln(req.Stdout, line)
|
||||
if file != nil {
|
||||
if _, err := fmt.Fprintln(file, line); err != nil {
|
||||
return fmt.Errorf("写入 %q 失败: %w", opts.save, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return waitDockerPipe(cmd, scanner.Err())
|
||||
}
|
||||
|
||||
// openLogSink opens the --save file for appending, creating its directory.
|
||||
func openLogSink(path string) (*os.File, error) {
|
||||
if dir := filepath.Dir(path); dir != "" && dir != "." {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("创建目录 %q 失败: %w", dir, err)
|
||||
}
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开日志文件 %q 失败: %w", path, err)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
Reference in New Issue
Block a user