- 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 调用
174 lines
4.4 KiB
Go
174 lines
4.4 KiB
Go
package executor
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"text/tabwriter"
|
|
)
|
|
|
|
// dfRow is one row of `docker system df`: a resource type, how many exist,
|
|
// how much space they use and how much of that is reclaimable.
|
|
type dfRow struct {
|
|
Type string
|
|
Total int64
|
|
Active int64
|
|
Size int64
|
|
Reclaim int64
|
|
}
|
|
|
|
// runDockerDF implements `kopsc docker df`: `docker system df` rendered as a
|
|
// readable, sorted ranking with a total and a reclaim hint.
|
|
func runDockerDF(ctx context.Context, req BuiltinRequest) error {
|
|
rows, err := readDockerDF(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(rows) == 0 {
|
|
return errors.New("docker system df 没有返回任何数据")
|
|
}
|
|
renderDockerDF(rows, req.Stdout)
|
|
return nil
|
|
}
|
|
|
|
func readDockerDF(ctx context.Context) ([]dfRow, error) {
|
|
out, err := dockerOutput(ctx, "system", "df")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var rows []dfRow
|
|
for _, line := range splitLines(out) {
|
|
if strings.HasPrefix(strings.TrimSpace(line), "TYPE") {
|
|
continue
|
|
}
|
|
fields := strings.Split(line, "\t")
|
|
if len(fields) < 2 {
|
|
continue
|
|
}
|
|
row := dfRow{Type: strings.TrimSpace(fields[0])}
|
|
row.Total = parseCount(fieldAt(fields, 1))
|
|
row.Active = parseCount(fieldAt(fields, 2))
|
|
row.Size, _ = parseDockerSize(fieldAt(fields, 3))
|
|
reclaimable := fieldAt(fields, 4)
|
|
row.Reclaim, _ = parseDockerSize(reclaimable)
|
|
rows = append(rows, row)
|
|
}
|
|
|
|
// Rank by size, largest first; unknown types sort last but are kept.
|
|
sort.SliceStable(rows, func(i, j int) bool {
|
|
if rows[i].Size != rows[j].Size {
|
|
return rows[i].Size > rows[j].Size
|
|
}
|
|
return rows[i].Type < rows[j].Type
|
|
})
|
|
return rows, nil
|
|
}
|
|
|
|
func fieldAt(fields []string, i int) string {
|
|
if i < len(fields) {
|
|
return strings.TrimSpace(fields[i])
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// renderDockerDF writes the ranking. The trailing "可回收" column tells the
|
|
// user how much cleanup (kopsc docker clearsafe) would free.
|
|
func renderDockerDF(rows []dfRow, w io.Writer) {
|
|
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintln(tw, "类型\t项\t可清理\t大小\t可回收")
|
|
|
|
var totalSize, totalReclaim, totalItems, totalFree int64
|
|
for _, row := range rows {
|
|
free := row.Total - row.Active
|
|
if free < 0 {
|
|
free = 0
|
|
}
|
|
fmt.Fprintf(tw, "%s\t%d\t%d\t%s\t%s\n",
|
|
row.Type, row.Total, free, humanSize(row.Size), humanSize(row.Reclaim))
|
|
totalSize += row.Size
|
|
totalReclaim += row.Reclaim
|
|
totalItems += row.Total
|
|
totalFree += free
|
|
}
|
|
fmt.Fprintf(tw, "总计\t%d\t%d\t%s\t%s\n", totalItems, totalFree, humanSize(totalSize), humanSize(totalReclaim))
|
|
_ = tw.Flush()
|
|
|
|
if totalReclaim > 0 {
|
|
fmt.Fprintf(w, "\n可回收 %s;清理请用 kopsc docker clearsafe(不含全部未使用镜像与卷)\n", humanSize(totalReclaim))
|
|
} else {
|
|
fmt.Fprintln(w, "\n没有可回收的空间")
|
|
}
|
|
}
|
|
|
|
// parseCount reads an integer count such as "19" or "" (unknown).
|
|
func parseCount(s string) int64 {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return 0
|
|
}
|
|
n, err := strconv.ParseInt(s, 10, 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return n
|
|
}
|
|
|
|
// parseDockerSize reads docker's size text back into bytes. Docker prints
|
|
// values such as "0B", "1.234GB", "512MB", "1.5GB (25%)" or "1.234GB (2.1GB)"
|
|
// depending on the format and version, so only the leading number and unit are
|
|
// used. A "-" or empty value means zero.
|
|
func parseDockerSize(s string) (int64, error) {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" || s == "-" || s == "N/A" {
|
|
return 0, nil
|
|
}
|
|
// Docker appends a percentage or a parenthesised detail; drop it. A space
|
|
// only separates number and unit (docker prints "3 MiB" in some formats),
|
|
// so it is removed but not used to cut.
|
|
if i := strings.IndexByte(s, '('); i >= 0 {
|
|
s = strings.TrimSpace(s[:i])
|
|
}
|
|
s = strings.ReplaceAll(s, " ", "")
|
|
|
|
i := 0
|
|
for i < len(s) && (s[i] >= '0' && s[i] <= '9' || s[i] == '.') {
|
|
i++
|
|
}
|
|
number, unit := s[:i], strings.ToUpper(strings.TrimSpace(s[i:]))
|
|
value, err := strconv.ParseFloat(number, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("无法解析大小 %q", s)
|
|
}
|
|
|
|
multiplier := []struct {
|
|
unit string
|
|
base float64
|
|
}{
|
|
{"B", 1},
|
|
{"KB", 1e3},
|
|
{"MB", 1e6},
|
|
{"GB", 1e9},
|
|
{"TB", 1e12},
|
|
{"PB", 1e15},
|
|
{"KIB", 1 << 10},
|
|
{"MIB", 1 << 20},
|
|
{"GIB", 1 << 30},
|
|
{"TIB", 1 << 40},
|
|
}
|
|
if unit == "" {
|
|
// A bare number is bytes, as `--format {{.Size}}` may print.
|
|
unit = "B"
|
|
}
|
|
for _, m := range multiplier {
|
|
if unit == m.unit {
|
|
return int64(value * m.base), nil
|
|
}
|
|
}
|
|
return 0, fmt.Errorf("无法识别大小单位 %q", unit)
|
|
}
|