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 /
网友回复


