- 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 调用
178 lines
4.7 KiB
Go
178 lines
4.7 KiB
Go
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
|
|
}
|