+
16
-

回答

p-run-hostnet.go (完整代码)

package main

import (
    "archive/tar"
    "compress/gzip"
    "flag"
    "fmt"
    "io"
    "net/http"
    "os"
    "os/exec"
    "os/user"
    "path/filepath"
    "strings"
    "syscall"

    "github.comcom/google/uuid"
)

// --- 配置和常量 ---

const (
    alpineURL     = "https://dl-cdn.alpinelinux.org/alpine/v3.18/releases/x86_64/alpine-minirootfs-3.18.3-x86_64.tar.gz"
    alpineTarName = "alpine-minirootfs-3.18.3-x86_64.tar.gz"
)

// 定义终端颜色
const (
    ColorRed   = "\033[0;31m"
    ColorGreen = "\033[0;32m"
    ColorBlue  = "\033[0;34m"
    ColorNone  = "\033[0m"
)

// --- 辅助函数 (日志) ---

// Info 打印蓝色信息日志
func Info(format string, a ...interface{}) {
    fmt.Printf("%sINFO:%s %s\n", ColorBlue, ColorNone, fmt.Sprintf(format, a...))
}

// Error 打印红色错误日志并退出程序
func Error(format string, a ...interface{}) {
    fmt.Fprintf(os.Stderr, "%sERROR:%s %s\n", ColorRed, ColorNone, fmt.Sprintf(format, a...))
    os.Exit(1)
}

// --- 容器核心逻辑 ---

// prepareBaseImage 检查基础镜像是否存在,如果不存在则下载并解压
func prepareBaseImage(baseDir, imagesDir, imageName string) error {
    imagePath := filepath.Join(imagesDir, imageName)
    if _, err := os.Stat(filepath.Join(imagePath, "bin")); err == nil {
        return nil // 镜像已存在
    }

    Info("未找到镜像 '%s',正在准备...", imageName)
    if err := os.MkdirAll(imagePath, 0755); err != nil {
        return fmt.Errorf("创建镜像目录失败: %w", err)
    }

    tarPath := filepath.Join(baseDir, alpineTarName)
    if _, err := os.Stat(tarPath); os.IsNotExist(err) {
        Info("正在下载 Alpine Mini RootFS...")
        if err := downloadFile(tarPath, alpineURL); err != nil {
            return fmt.Errorf("下载 Alpine 失败: %w", err)
        }
    }

    Info("正在解压 rootfs 到 %s...", imagePath)
    if err := untar(tarPath, imagePath); err != nil {
        return fmt.Errorf("解压 rootfs 失败: %w", err)
    }

    return nil
}

// createContainerInstance 创建一个新的容器文件系统实例
func createContainerInstance(imagesDir, containersDir, imageName string) (string, string, error) {
    containerID := uuid.New().String()[:8]
    containerRootfs := filepath.Join(containersDir, containerID)
    Info("正在创建容器实例: %s", containerID)

    if err := os.MkdirAll(containerRootfs, 0755); err != nil {
        return "", "", fmt.Errorf("创建容器根目录失败: %w", err)
    }

    imagePath := filepath.Join(imagesDir, imageName)
    // 使用 cp -a 命令来保留所有文件属性,比 Go 原生实现更简单可靠
    cmd := exec.Command("cp", "-a", filepath.Join(imagePath, "/."), containerRootfs)
    if err := cmd.Run(); err != nil {
        return "", "", fmt.Errorf("复制镜像文件失败: %w", err)
    }

    return containerID, containerRootfs, nil
}

// cleanupContainer 在程序退出时清理容器资源
func cleanupContainer(containerRootfs string, autoRemove bool) {
    if !autoRemove {
        return
    }
    Info("正在移除容器文件系统: %s", containerRootfs)

    // 递归卸载挂载点
    exec.Command("umount", "-R", containerRootfs).Run()

    if err := os.RemoveAll(containerRootfs); err != nil {
        fmt.Fprintf(os.Stderr, "警告: 移除 %s 失败: %v\n", containerRootfs, err)
    }
}

// buildMountCommands 为数据卷构建挂载命令字符串
func buildMountCommands(volumes []string, containerRootfs string) (string, error) {
    var mountCommands []string
    for _, vol := range volumes {
        parts := strings.SplitN(vol, ":", 2)
        if len(parts) != 2 {
            return "", fmt.Errorf("无效的卷格式: '%s',请使用 'host:container'", vol)
        }
        hostPath, containerPath := parts[0], parts[1]

        absHostPath, err := filepath.Abs(hostPath)
        if err != nil {
            return "", fmt.Errorf("获取绝对路径失败 '%s': %w", hostPath, err)
        }

        if _, err := os.Stat(absHostPath); os.IsNotExist(err) {
            return "", fmt.Errorf("主机路径不存在: %s", absHostPath)
        }

        fullContainerPath := filepath.Join(containerRootfs, containerPath)
        cmd := fmt.Sprintf("mkdir -p %q && mount --bind %q %q", fullContainerPath, absHostPath, fullContainerPath)
        mountCommands = append(mountCommands, cmd)
    }
    return strings.Join(mountCommands, " && "), nil
}

// runInNewNamespace 使用 unshare 和 chroot 运行命令
func runInNewNamespace(containerRootfs string, interactive, tty bool, mountCmds string, command []string) error {
    // 将 shell 脚本嵌入 Go 字符串中
    shellScript := `
        set -e
        ROOTFS_PATH="$1"; IS_INTERACTIVE="$2"; MOUNT_COMMANDS_STR="$3"; shift 3

        # 挂载必要的文件系统
        mkdir -p "$ROOTFS_PATH/dev"; mount -t tmpfs none "$ROOTFS_PATH/dev"
        mkdir -p "$ROOTFS_PATH/dev/pts"; mount -t devpts none "$ROOTFS_PATH/dev/pts"; ln -sf pts/ptmx "$ROOTFS_PATH/dev/ptmx"
        mkdir -p "$ROOTFS_PATH/proc"; mkdir -p "$ROOTFS_PATH/sys"; mkdir -p "$ROOTFS_PATH/tmp"
        mount --rbind /sys "$ROOTFS_PATH/sys"; mount --make-rslave "$ROOTFS_PATH/sys"
        mount -t tmpfs none "$ROOTFS_PATH/tmp"

        # 复制 resolv.conf 并执行数据卷挂载
        if [ -r /etc/resolv.conf ]; then cp /etc/resolv.conf "$ROOTFS_PATH/etc/"; fi
        if [ -n "$MOUNT_COMMANDS_STR" ]; then eval "$MOUNT_COMMANDS_STR"; fi

        # 根据是否为交互模式执行不同的命令
        if [ "$IS_INTERACTIVE" = "true" ]; then
            exec chroot "$ROOTFS_PATH" /bin/sh -c ". /etc/profile; exec /bin/sh -i"
        else
            exec chroot "$ROOTFS_PATH" /bin/sh -c ". /etc/profile; exec \"\$@\"" sh "$@"
        fi
    `
    // 构建 unshare 命令的参数
    args := []string{
        "-U", "-r", "-m", "-p", "--fork", "--mount-proc",
        "sh", "-c", shellScript,
        "sh", // $0 for the script
        containerRootfs,
        fmt.Sprintf("%t", interactive),
        mountCmds,
    }
    args = append(args, command...)

    cmd := exec.Command("unshare", args...)
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNET,
    }
    if tty {
        cmd.SysProcAttr.Setctty = true
        cmd.SysProcAttr.Sessid = true
    }

    return cmd.Run()
}

// --- 文件下载和解压辅助函数 ---

func downloadFile(filepath string, url string) error {
    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    out, err := os.Create(filepath)
    if err != nil {
        return err
    }
    defer out.Close()

    _, err = io.Copy(out, resp.Body)
    return err
}

func untar(tarball, target string) error {
    reader, err := os.Open(tarball)
    if err != nil {
        return err
    }
    defer reader.Close()

    gzr, err := gzip.NewReader(reader)
    if err != nil {
        return err
    }
    defer gzr.Close()

    tr := tar.NewReader(gzr)

    for {
        header, err := tr.Next()
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }

        targetPath := filepath.Join(target, header.Name)

        switch header.Typeflag {
        case tar.TypeDir:
            if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
                return err
            }
        case tar.TypeReg:
            outFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
            if err != nil {
                return err
            }
            if _, err := io.Copy(outFile, tr); err != nil {
                outFile.Close()
                return err
            }
            outFile.Close()
        case tar.TypeSymlink:
            if err := os.Symlink(header.Linkname, targetPath); err != nil {
                return err
            }
        default:
            return fmt.Errorf("unsupported file type for %s: %c", header.Name, header.Typeflag)
        }
    }
    return nil
}

// --- 主程序入口 ---

// volumeFlag 是一个自定义 flag 类型,用于接收多个 -v 参数
type volumeFlag []string

func (v *volumeFlag) String() string {
    return fmt.Sprintf("%v", *v)
}

func (v *volumeFlag) Set(value string) error {
    *v = append(*v, value)
    return nil
}

func main() {
    // --- 初始化配置路径 ---
    currentUser, err := user.Current()
    if err != nil {
        Error("无法获取当前用户信息: %v", err)
    }
    baseDir := filepath.Join(currentUser.HomeDir, ".local/share/p-run-hostnet")
    imagesDir := filepath.Join(baseDir, "images")
    containersDir := filepath.Join(baseDir, "containers")

    // --- 命令行参数解析 ---
    var (
        interactive bool
        tty         bool
        autoRemove  bool
        volumes     volumeFlag
    )

    fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
    fs.BoolVar(&interactive, "i", false, "交互式运行")
    fs.BoolVar(&tty, "t", false, "分配一个伪终端")
    fs.BoolVar(&autoRemove, "rm", false, "容器退出后自动删除")
    fs.Var(&volumes, "v", "挂载一个卷 (例如: /host/path:/container/path)")
    fs.Var(&volumes, "volume", "挂载一个卷 (例如: /host/path:/container/path)")

    // 手动处理组合标志 -it 和 -ti
    args := os.Args[1:]
    cleanedArgs := []string{}
    for _, arg := range args {
        if arg == "-it" || arg == "-ti" {
            interactive = true
            tty = true
        } else {
            cleanedArgs = append(cleanedArgs, arg)
        }
    }
    fs.Parse(cleanedArgs)

    nonFlagArgs := fs.Args()
    if len(nonFlagArgs) == 0 {
        fmt.Printf("使用方法: %s [-it] [--rm] [-v /host:/cont] <image_name> [command...]\n", os.Args[0])
        os.Exit(1)
    }
    imageName := nonFlagArgs[0]
    command := nonFlagArgs[1:]

    if len(command) == 0 {
        if interactive {
            command = []string{"/bin/sh"}
        } else {
            Error("请提供要执行的命令。")
        }
    }

    // --- 核心业务流程 ---
    if err := os.MkdirAll(imagesDir, 0755); err != nil {
        Error("创建 images 目录失败: %v", err)
    }
    if err := os.MkdirAll(containersDir, 0755); err != nil {
        Error("创建 containers 目录失败: %v", err)
    }

    if err := prepareBaseImage(baseDir, imagesDir, imageName); err != nil {
        Error("准备基础镜像失败: %v", err)
    }

    _, containerRootfs, err := createContainerInstance(imagesDir, containersDir, imageName)
    if err != nil {
        Error("创建容器实例失败: %v", err)
    }

    defer cleanupContainer(containerRootfs, autoRemove)

    mountCmds, err := buildMountCommands(volumes, containerRootfs)
    if err != nil {
        Error("处理卷挂载失败: %v", err)
    }

    Info("正在启动容器...")
    if err := runInNewNamespace(containerRootfs, interactive, tty, mountCmds, command); err != nil {
        if exitError, ok := err.(*exec.ExitError); ok {
            os.Exit(exitError.ExitCode())
        } else {
            os.Exit(1)
        }
    }
}

如何编译和运行

保存文件:将上面的代码保存为 p-run-hostnet.go。

初始化 Go 模块:在文件所在的目录打开终端,运行以下命令来创建一个 go.mod 文件。

go mod init p-run-hostnet

获取依赖:Go 会自动检测代码中所需的依赖。运行以下命令来下载并记录它们。

go mod tidy

这条命令会自动找到并添加 github.com/google/uuid 依赖。

编译

go build -o p-run-go p-run-hostnet.go

这将生成一个名为 p-run-go 的可执行文件。

运行:运行方式与原始 shell 脚本完全相同。

启动交互式 Shell:

./p-run-go -it --rm my-alpine

验证卷挂载:

echo "Hello from Go host" > test.txt
./p-run-go -it --rm -v $(pwd):/app my-alpine

进入容器后,你可以通过 ls /app 和 cat /app/test.txt 来验证文件是否成功挂载。

执行非交互式命令:

./p-run-go my-alpine ls -l /

网友回复

我知道答案,我要回答