feat: 新增 proc nohup 后台运行、docker save/load 内置实现,移除 Podman 支持
- proc.nohup:后台运行命令并将 stdout/stderr 追加写入日志, Unix 下新会话、Windows 下 DETACHED_PROCESS 脱离终端 - docker.save:等价 docker save <镜像> -o <镜像名>.tar,文件名按 仓库名+标签生成(alpine:3.20 → alpine_3.20.tar),传镜像 ID 时 先解析出对应名称;docker.load 改为归档文件作位置参数 - 移除 Podman 支持,所有容器命令只走 docker
This commit is contained in:
@@ -161,20 +161,26 @@ func Builtin() *Registry {
|
||||
Variants: runtimeVariants("tag"),
|
||||
},
|
||||
{
|
||||
ID: "docker.save",
|
||||
Group: "docker",
|
||||
Summary: "把镜像导出为归档文件",
|
||||
Action: "等价 docker save;通常配合 -o 透传输出文件",
|
||||
Usage: "<镜像>",
|
||||
ID: "docker.save",
|
||||
Group: "docker",
|
||||
Summary: "把镜像导出为 <镜像名>.tar",
|
||||
Action: "等价 docker save <镜像> -o <镜像名>.tar:归档文件按镜像的仓库名与标签命名," +
|
||||
"标签的冒号改写为下划线(alpine:3.20 → alpine_3.20.tar,registry:5000/team/app:1.0 → registry_5000_team_app_1.0.tar);" +
|
||||
"传入镜像 ID 时先解析出它对应的镜像名称,再据此命名;未打标签的镜像直接报错。" +
|
||||
"--platform 等选项原样透传给底层运行时",
|
||||
Usage: "<镜像名|镜像ID>",
|
||||
MinArgs: 1,
|
||||
Variants: runtimeVariants("save"),
|
||||
Variants: runtimeBuiltinVariants("save"),
|
||||
},
|
||||
{
|
||||
ID: "docker.load",
|
||||
Group: "docker",
|
||||
Summary: "从归档文件导入镜像",
|
||||
Action: "等价 docker load;通常配合 -i 透传输入文件",
|
||||
Variants: runtimeVariants("load"),
|
||||
ID: "docker.load",
|
||||
Group: "docker",
|
||||
Summary: "从 <归档.tar> 导入镜像",
|
||||
Action: "等价 docker load -i <归档.tar>:归档文件作为位置参数(对应原本的 docker load < xxx.tar)," +
|
||||
"可一次给出多个;仍可用 -i/--input 指定,或传 - 表示从标准输入读取",
|
||||
Usage: "<归档.tar>...",
|
||||
MinArgs: 1,
|
||||
Variants: runtimeBuiltinVariants("load"),
|
||||
},
|
||||
{
|
||||
ID: "docker.cp",
|
||||
@@ -275,7 +281,7 @@ func Builtin() *Registry {
|
||||
ID: "diagnose",
|
||||
Group: "diagnose",
|
||||
Summary: "诊断容器或 Pod 当前状态的根因",
|
||||
Action: "自动识别 Docker/Podman 容器或 Kubernetes Pod,采集状态、最近日志与事件,给出原因、依据与建议;只读,不修改任何对象。--kind 可强制来源,K8s 下 -n/--namespace 指定命名空间",
|
||||
Action: "自动识别 Docker 容器或 Kubernetes Pod,采集状态、最近日志与事件,给出原因、依据与建议;只读,不修改任何对象。--kind 可强制来源,K8s 下 -n/--namespace 指定命名空间",
|
||||
Usage: "<容器名或 Pod 名>",
|
||||
MinArgs: 1,
|
||||
Namespaced: true,
|
||||
@@ -283,7 +289,7 @@ func Builtin() *Registry {
|
||||
{
|
||||
Match: Match{OS: "linux"},
|
||||
Builtin: "diagnose",
|
||||
Note: "Linux 内置诊断(docker/podman 与 kubectl 自动识别)",
|
||||
Note: "Linux 内置诊断(docker 与 kubectl 自动识别)",
|
||||
},
|
||||
{
|
||||
Match: Match{OS: "darwin"},
|
||||
@@ -372,6 +378,35 @@ func Builtin() *Registry {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "proc.nohup",
|
||||
Group: "proc",
|
||||
Summary: "在后台运行命令,输出写入日志(等价 nohup <命令> > output.log 2>&1 &)",
|
||||
Action: "启动一个与当前终端脱离的命令并立即返回:标准输入接到空设备," +
|
||||
"标准输出与标准错误都追加写入日志文件(默认 <程序名>.out,可用 -o 指定)," +
|
||||
"Unix 下进入新会话、无控制终端,Windows 下以 DETACHED_PROCESS 启动,关闭终端不会终止它。" +
|
||||
"选项在命令之前解析,命令之后的参数(含 -i 之类)原样透传,不加 shell;" +
|
||||
"命令自身的 -n/--dry-run 不会被 kopsc 截获;kopsc 自身的预览等选项写在命令之前或用 -- 分隔",
|
||||
Usage: "<命令> [参数...]",
|
||||
MinArgs: 1,
|
||||
Variants: []Variant{
|
||||
{
|
||||
Match: Match{OS: "linux"},
|
||||
Builtin: "proc.nohup",
|
||||
Note: "Linux 内置实现(setsid 新会话 + 日志追加)",
|
||||
},
|
||||
{
|
||||
Match: Match{OS: "darwin"},
|
||||
Builtin: "proc.nohup",
|
||||
Note: "macOS 内置实现(setsid 新会话 + 日志追加)",
|
||||
},
|
||||
{
|
||||
Match: Match{OS: "windows"},
|
||||
Builtin: "proc.nohup",
|
||||
Note: "Windows 内置实现(DETACHED_PROCESS + 日志追加)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "pkg.refreshindex",
|
||||
Group: "pkg",
|
||||
@@ -413,32 +448,42 @@ func Builtin() *Registry {
|
||||
}}
|
||||
}
|
||||
|
||||
// runtimeVariants builds the platform matrix for a container command. Linux
|
||||
// gets both podman and docker variants (resolution prefers podman when both
|
||||
// exist); macOS and Windows only have Docker Desktop.
|
||||
func runtimeVariants(args ...string) []Variant {
|
||||
program := func(runtime string) string {
|
||||
if runtime == "podman" {
|
||||
return "podman"
|
||||
// runtimeBuiltinVariants builds the platform matrix for a container command
|
||||
// that kopsc implements itself. Builtin variants carry no argv; the builtin
|
||||
// learns the runtime's program name from Variant.Runtime instead.
|
||||
func runtimeBuiltinVariants(name string) []Variant {
|
||||
makeVariant := func(osName, note string) Variant {
|
||||
return Variant{
|
||||
Match: Match{OS: osName, Runtime: "docker"},
|
||||
Builtin: "docker." + name,
|
||||
Runtime: "docker",
|
||||
Note: note,
|
||||
}
|
||||
return "docker"
|
||||
}
|
||||
return []Variant{
|
||||
makeVariant("linux", "Linux 上的 Docker 运行时,由 kopsc 内置实现"),
|
||||
makeVariant("darwin", "macOS 上的 Docker Desktop 运行时,由 kopsc 内置实现"),
|
||||
makeVariant("windows", "Windows 上的 Docker Desktop 运行时,由 kopsc 内置实现"),
|
||||
}
|
||||
}
|
||||
|
||||
makeVariant := func(osName, runtime, note string) Variant {
|
||||
// runtimeVariants builds the platform matrix for a container command: docker
|
||||
// on every platform (Docker Desktop on macOS and Windows).
|
||||
func runtimeVariants(args ...string) []Variant {
|
||||
makeVariant := func(osName, note string) Variant {
|
||||
argv := make([]string, 0, 1+len(args))
|
||||
argv = append(argv, program(runtime))
|
||||
argv = append(argv, "docker")
|
||||
argv = append(argv, args...)
|
||||
return Variant{
|
||||
Match: Match{OS: osName, Runtime: runtime},
|
||||
Match: Match{OS: osName, Runtime: "docker"},
|
||||
Argv: argv,
|
||||
Note: note,
|
||||
}
|
||||
}
|
||||
|
||||
return []Variant{
|
||||
makeVariant("linux", "podman", "Linux 上的 Podman 运行时"),
|
||||
makeVariant("linux", "docker", "Linux 上的 Docker 运行时"),
|
||||
makeVariant("darwin", "docker", "macOS 上的 Docker Desktop 运行时"),
|
||||
makeVariant("windows", "docker", "Windows 上的 Docker Desktop 运行时"),
|
||||
makeVariant("linux", "Linux 上的 Docker 运行时"),
|
||||
makeVariant("darwin", "macOS 上的 Docker Desktop 运行时"),
|
||||
makeVariant("windows", "Windows 上的 Docker Desktop 运行时"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ type Match struct {
|
||||
Arch string // runtime.GOARCH
|
||||
Distro string // exact os-release ID, e.g. "centos"
|
||||
DistroLike []string // os-release ID_LIKE ancestry, e.g. "rhel"
|
||||
Runtime string // container runtime required on PATH, e.g. "podman"
|
||||
Runtime string // container runtime required on PATH, e.g. "docker"
|
||||
}
|
||||
|
||||
// Variant is one concrete implementation of a command.
|
||||
@@ -27,6 +27,10 @@ type Variant struct {
|
||||
// have no equivalent system utility (for example on Windows).
|
||||
Builtin string
|
||||
Note string // human-readable rationale shown by `kopsc explain`
|
||||
// Runtime records which container runtime a builtin should drive when
|
||||
// Argv cannot carry it (builtin variants have no argv). Resolution uses
|
||||
// Match.Runtime; this field is only informational context for the builtin.
|
||||
Runtime string
|
||||
}
|
||||
|
||||
// Command is a platform-neutral intent, e.g. "docker.clearsafe".
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestBuiltinIDsStable(t *testing.T) {
|
||||
"docker.prunebuilder", "docker.prunevolumes", "docker.clearall",
|
||||
"docker.compose.up", "docker.compose.down", "docker.compose.ps", "docker.compose.logs",
|
||||
"diagnose",
|
||||
"disk.usage", "net.pids", "net.ports", "pkg.refreshindex",
|
||||
"disk.usage", "net.pids", "net.ports", "proc.nohup", "pkg.refreshindex",
|
||||
}
|
||||
if got := reg.IDs(); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("IDs() =\n%v\nwant\n%v", got, want)
|
||||
@@ -38,12 +38,12 @@ func TestBuiltinClearsafeIsSafe(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatal("docker.clearsafe not found")
|
||||
}
|
||||
if len(cmd.Variants) < 4 {
|
||||
if len(cmd.Variants) < 3 {
|
||||
t.Fatalf("expected a variant per platform, got %d", len(cmd.Variants))
|
||||
}
|
||||
|
||||
for _, variant := range cmd.Variants {
|
||||
if len(variant.Argv) == 0 || (variant.Argv[0] != "docker" && variant.Argv[0] != "podman") {
|
||||
if len(variant.Argv) == 0 || variant.Argv[0] != "docker" {
|
||||
t.Errorf("unexpected program %v", variant.Argv)
|
||||
}
|
||||
if strings.Join(variant.Argv, " ") != variant.Argv[0]+" system prune -f" {
|
||||
@@ -182,6 +182,32 @@ func TestDiskUsageIsBuiltinEverywhere(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcNohupIsBuiltinEverywhere(t *testing.T) {
|
||||
reg := Builtin()
|
||||
cmd, ok := reg.Lookup("proc.nohup")
|
||||
if !ok {
|
||||
t.Fatal("proc.nohup not found")
|
||||
}
|
||||
if cmd.RequiresYes {
|
||||
t.Error("proc.nohup is not destructive and must not require --yes")
|
||||
}
|
||||
if cmd.MinArgs != 1 || cmd.Usage == "" {
|
||||
t.Errorf("proc.nohup must require the command to run, got MinArgs=%d Usage=%q", cmd.MinArgs, cmd.Usage)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, variant := range cmd.Variants {
|
||||
if variant.Builtin != "proc.nohup" || len(variant.Argv) != 0 || variant.Shell {
|
||||
t.Errorf("variant for %s must be the proc.nohup builtin, got %+v", variant.Match.OS, variant)
|
||||
}
|
||||
seen[variant.Match.OS] = true
|
||||
}
|
||||
for _, osName := range []string{"linux", "darwin", "windows"} {
|
||||
if !seen[osName] {
|
||||
t.Errorf("proc.nohup lacks a %s variant", osName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerCommandsWellFormed(t *testing.T) {
|
||||
reg := Builtin()
|
||||
destructive := map[string]bool{
|
||||
@@ -200,8 +226,14 @@ func TestDockerCommandsWellFormed(t *testing.T) {
|
||||
"docker.pull": 1,
|
||||
"docker.tag": 2,
|
||||
"docker.save": 1,
|
||||
"docker.load": 1,
|
||||
"docker.cp": 2,
|
||||
}
|
||||
// Built from kopsc's own implementation rather than a fixed argv.
|
||||
builtin := map[string]string{
|
||||
"docker.save": "docker.save",
|
||||
"docker.load": "docker.load",
|
||||
}
|
||||
|
||||
for i := range reg.Commands {
|
||||
cmd := ®.Commands[i]
|
||||
@@ -219,24 +251,36 @@ func TestDockerCommandsWellFormed(t *testing.T) {
|
||||
t.Errorf("%s: missing Usage for MinArgs=%d", cmd.ID, cmd.MinArgs)
|
||||
}
|
||||
|
||||
var podman, docker int
|
||||
platforms := map[string]bool{}
|
||||
for _, v := range cmd.Variants {
|
||||
if want, ok := builtin[cmd.ID]; ok {
|
||||
if v.Builtin != want || len(v.Argv) != 0 {
|
||||
t.Errorf("%s: builtin variant = %q with argv %v, want builtin %q",
|
||||
cmd.ID, v.Builtin, v.Argv, want)
|
||||
continue
|
||||
}
|
||||
if v.Runtime != v.Match.Runtime || v.Runtime != "docker" {
|
||||
t.Errorf("%s: variant runtime %q / match %q, want docker", cmd.ID, v.Runtime, v.Match.Runtime)
|
||||
}
|
||||
platforms[v.Match.OS] = true
|
||||
continue
|
||||
}
|
||||
if len(v.Argv) == 0 {
|
||||
t.Errorf("%s: docker command must have an argv variant", cmd.ID)
|
||||
continue
|
||||
}
|
||||
switch v.Argv[0] {
|
||||
case "podman":
|
||||
podman++
|
||||
case "docker":
|
||||
docker++
|
||||
if v.Argv[0] != "docker" {
|
||||
t.Errorf("%s: unexpected program %q", cmd.ID, v.Argv[0])
|
||||
}
|
||||
if v.Match.Runtime != v.Argv[0] {
|
||||
t.Errorf("%s: runtime %q does not match program %q", cmd.ID, v.Match.Runtime, v.Argv[0])
|
||||
if v.Match.Runtime != "docker" {
|
||||
t.Errorf("%s: runtime %q, want docker", cmd.ID, v.Match.Runtime)
|
||||
}
|
||||
platforms[v.Match.OS] = true
|
||||
}
|
||||
if podman != 1 || docker != 3 {
|
||||
t.Errorf("%s: variants = %d podman / %d docker, want 1/3", cmd.ID, podman, docker)
|
||||
for _, osName := range []string{"linux", "darwin", "windows"} {
|
||||
if !platforms[osName] {
|
||||
t.Errorf("%s: missing %s variant", cmd.ID, osName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user