- 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 调用
256 lines
7.8 KiB
Go
256 lines
7.8 KiB
Go
//go:build !windows
|
|
|
|
package executor
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestArchiveName(t *testing.T) {
|
|
cases := map[string]string{
|
|
"alpine:3.20": "alpine_3.20.tar",
|
|
"alpine": "alpine.tar",
|
|
"nginx:1.25.3": "nginx_1.25.3.tar",
|
|
"registry.example.com:5000/app:1.0": "registry.example.com_5000_app_1.0.tar",
|
|
"team/app:latest": "team_app_latest.tar",
|
|
"alpine@sha256:abc": "alpine_sha256_abc.tar",
|
|
}
|
|
for in, want := range cases {
|
|
if got := archiveName(in); got != want {
|
|
t.Errorf("archiveName(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLooksLikeImageID(t *testing.T) {
|
|
yes := []string{
|
|
"sha256:9c6f07244728", "9c6f07244728", "9C6F07244728",
|
|
"sha256:9c6f0724472892f3061e",
|
|
}
|
|
for _, in := range yes {
|
|
if !looksLikeImageID(in) {
|
|
t.Errorf("looksLikeImageID(%q) = false, want true", in)
|
|
}
|
|
}
|
|
no := []string{"alpine", "alpine:3.20", "nginx:1.25", "", "abc"}
|
|
for _, in := range no {
|
|
if looksLikeImageID(in) {
|
|
t.Errorf("looksLikeImageID(%q) = true, want false", in)
|
|
}
|
|
}
|
|
}
|
|
|
|
// fakeRuntime installs a script called docker on PATH. images is the
|
|
// `images --format` payload; every invocation is appended to the returned log
|
|
// file as one line.
|
|
func fakeRuntime(t *testing.T, images string) (logPath string) {
|
|
t.Helper()
|
|
return fakeRuntimeScript(t, images, "", "")
|
|
}
|
|
|
|
// fakeRuntimeScript installs a fake `docker` on PATH. images and ps are the
|
|
// payloads of `docker images --format ...` and `docker ps -a --format ...`;
|
|
// every invocation is appended to the returned log file as one line. Empty
|
|
// payloads print nothing.
|
|
func fakeRuntimeScript(t *testing.T, images, ps, extraCase string) (logPath string) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
logPath = filepath.Join(dir, "calls.log")
|
|
|
|
script := "#!/bin/sh\n" +
|
|
"printf '%s' \"$0\" >> " + logPath + ".name\n" +
|
|
"echo \"$*\" >> " + logPath + "\n" +
|
|
"case \"$1\" in\n" +
|
|
" images) cat <<'EOF'\n" + images + "\nEOF\n" +
|
|
" ;;\n" +
|
|
" ps) cat <<'EOF'\n" + ps + "\nEOF\n" +
|
|
" ;;\n" +
|
|
extraCase +
|
|
"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 imageList() string {
|
|
return strings.Join([]string{
|
|
"IMAGE ID\tREPOSITORY\tTAG\tDIGEST",
|
|
"9c6f07244728\talpine\t3.20\t",
|
|
"9c6f07244728\talpine\tlatest\t",
|
|
"5ca7b1d5e2fc\tnginx\t1.25.3\t",
|
|
"1b9a2c3d4e5f\tregistry.example.com:5000/team/app\t1.0\t",
|
|
"7d3e5f7a9b1c\t<none>\t<none>\t",
|
|
}, "\n")
|
|
}
|
|
|
|
func calls(t *testing.T, logPath string) []string {
|
|
t.Helper()
|
|
data, err := os.ReadFile(logPath)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var out []string
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
if line = strings.TrimSpace(line); line != "" {
|
|
out = append(out, line)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestDockerSaveNamesArchiveAfterTag(t *testing.T) {
|
|
logPath := fakeRuntime(t, imageList())
|
|
|
|
var out bytes.Buffer
|
|
if err := runDockerSave(context.Background(), BuiltinRequest{
|
|
Args: []string{"alpine:3.20"},
|
|
Stdout: &out,
|
|
}); err != nil {
|
|
t.Fatalf("save: %v", err)
|
|
}
|
|
|
|
if !strings.Contains(out.String(), "alpine:3.20") || !strings.Contains(out.String(), "alpine_3.20.tar") {
|
|
t.Errorf("summary = %q", out.String())
|
|
}
|
|
got := calls(t, logPath)
|
|
want := "save -o alpine_3.20.tar alpine:3.20"
|
|
if len(got) < 2 || got[1] != want {
|
|
t.Fatalf("calls = %q, want a %q invocation", got, want)
|
|
}
|
|
}
|
|
|
|
func TestDockerSaveResolvesImageIDToName(t *testing.T) {
|
|
for _, id := range []string{"5ca7b1d5e2fc", "sha256:5ca7b1d5e2fc", "5ca7"} {
|
|
logPath := fakeRuntime(t, imageList())
|
|
var out bytes.Buffer
|
|
if err := runDockerSave(context.Background(), BuiltinRequest{Args: []string{id}, Stdout: &out}); err != nil {
|
|
t.Fatalf("save %q: %v", id, err)
|
|
}
|
|
got := calls(t, logPath)
|
|
want := "save -o nginx_1.25.3.tar " + id
|
|
if len(got) < 2 || got[1] != want {
|
|
t.Errorf("image %q: calls = %q, want %q", id, got, want)
|
|
}
|
|
if !strings.Contains(out.String(), "nginx_1.25.3.tar") {
|
|
t.Errorf("image %q: summary = %q", id, out.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDockerSaveRegistryWithPortKeepsName(t *testing.T) {
|
|
fakeRuntime(t, imageList())
|
|
var out bytes.Buffer
|
|
if err := runDockerSave(context.Background(), BuiltinRequest{
|
|
Args: []string{"registry.example.com:5000/team/app:1.0"},
|
|
Stdout: &out,
|
|
}); err != nil {
|
|
t.Fatalf("save: %v", err)
|
|
}
|
|
if !strings.Contains(out.String(), "registry.example.com_5000_team_app_1.0.tar") {
|
|
t.Fatalf("summary = %q", out.String())
|
|
}
|
|
}
|
|
|
|
func TestDockerSaveRejectsUntaggedImage(t *testing.T) {
|
|
fakeRuntime(t, imageList())
|
|
err := runDockerSave(context.Background(), BuiltinRequest{Args: []string{"7d3e5f7a9b1c"}})
|
|
if err == nil || !strings.Contains(err.Error(), "没有名称") {
|
|
t.Fatalf("err = %v, want untagged-image error", err)
|
|
}
|
|
}
|
|
|
|
func TestDockerSaveRejectsUnknownAndExtraImages(t *testing.T) {
|
|
fakeRuntime(t, imageList())
|
|
err := runDockerSave(context.Background(), BuiltinRequest{Args: []string{"busybox:1"}})
|
|
if err == nil || !strings.Contains(err.Error(), "本地没有镜像") {
|
|
t.Fatalf("err = %v, want unknown-image error", err)
|
|
}
|
|
|
|
err = runDockerSave(context.Background(), BuiltinRequest{Args: []string{"alpine:3.20", "nginx:1.25.3"}})
|
|
if err == nil || !strings.Contains(err.Error(), "一次只能保存一个镜像") {
|
|
t.Fatalf("err = %v, want single-image error", err)
|
|
}
|
|
|
|
err = runDockerSave(context.Background(), BuiltinRequest{})
|
|
if err == nil || !strings.Contains(err.Error(), "缺少镜像") {
|
|
t.Fatalf("err = %v, want missing-image error", err)
|
|
}
|
|
}
|
|
|
|
func TestDockerSaveForwardsPlatformFlag(t *testing.T) {
|
|
logPath := fakeRuntime(t, imageList())
|
|
var out bytes.Buffer
|
|
if err := runDockerSave(context.Background(), BuiltinRequest{
|
|
Args: []string{"--platform", "linux/arm64", "alpine:3.20"},
|
|
Stdout: &out,
|
|
}); err != nil {
|
|
t.Fatalf("save: %v", err)
|
|
}
|
|
got := calls(t, logPath)
|
|
want := "save -o alpine_3.20.tar alpine:3.20 --platform linux/arm64"
|
|
if len(got) < 2 || got[1] != want {
|
|
t.Fatalf("calls = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestDockerLoadTakesArchiveAsPositional(t *testing.T) {
|
|
dir := t.TempDir()
|
|
archive := filepath.Join(dir, "alpine_3.20.tar")
|
|
if err := os.WriteFile(archive, []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, args := range [][]string{
|
|
{archive},
|
|
{"-i", archive},
|
|
{"--input=" + archive},
|
|
{"--", archive},
|
|
{archive, "-q"},
|
|
} {
|
|
logPath := fakeRuntime(t, "")
|
|
if err := runDockerLoad(context.Background(), BuiltinRequest{Args: args}); err != nil {
|
|
t.Fatalf("load %v: %v", args, err)
|
|
}
|
|
got := calls(t, logPath)
|
|
joined := strings.Join(got, "|")
|
|
if !strings.Contains(joined, "load") || !strings.Contains(joined, "-i "+archive) {
|
|
t.Errorf("load %v produced calls %q", args, got)
|
|
}
|
|
if strings.Contains(joined, "<") {
|
|
t.Errorf("load %v must not rely on a shell redirect: %q", args, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDockerLoadRejectsMissingArchive(t *testing.T) {
|
|
fakeRuntime(t, "")
|
|
for _, args := range [][]string{{}, {"-i"}, {filepath.Join(t.TempDir(), "nope.tar")}, {t.TempDir()}} {
|
|
err := runDockerLoad(context.Background(), BuiltinRequest{Args: args})
|
|
if err == nil {
|
|
t.Errorf("load %v: expected an error", args)
|
|
}
|
|
}
|
|
if err := runDockerLoad(context.Background(), BuiltinRequest{Args: []string{"missing.tar"}}); err == nil ||
|
|
!strings.Contains(err.Error(), "无法读取归档文件") {
|
|
t.Errorf("missing archive err = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDockerLoadFromStdin(t *testing.T) {
|
|
logPath := fakeRuntime(t, "")
|
|
if err := runDockerLoad(context.Background(), BuiltinRequest{Args: []string{"-"}}); err != nil {
|
|
t.Fatalf("load -: %v", err)
|
|
}
|
|
got := calls(t, logPath)
|
|
if len(got) != 1 || got[0] != "load" {
|
|
t.Fatalf("calls = %q, want a single bare `load`", got)
|
|
}
|
|
}
|