diff --git a/cmd/revm/docker.go b/cmd/revm/docker.go index 59dca80..a804d7f 100644 --- a/cmd/revm/docker.go +++ b/cmd/revm/docker.go @@ -49,6 +49,15 @@ func dockerLifeCycle(_ context.Context, command *cli.Command) error { return err } + var containerDiskSpec *librevm.ContainerDiskSpec + if value := command.String(define.FlagContainerDisk); value != "" { + spec, err := librevm.ParseContainerDiskSpec(value) + if err != nil { + return err + } + containerDiskSpec = &spec + } + cfg := librevm.DefaultConfig(). WithMode(librevm.ModeContainer). WithName(command.String(define.FlagSessionID)). @@ -59,7 +68,7 @@ func dockerLifeCycle(_ context.Context, command *cli.Command) error { WithLogLevel(command.String(define.FlagLogLevel)). WithLogTo(command.String(define.FlagLogTo)). WithMount(command.StringSlice(define.FlagMount)...). - WithContainerDisk(command.String(define.FlagContainerDisk)). + WithContainerDiskSpec(containerDiskSpec). WithPodmanProxyAPIFile(command.String(define.FlagPodmanProxyAPIFile)). WithManageAPIFile(command.String(define.FlagManageAPIFile)). WithSSHKeyDir(command.String(define.FlagSSHKeyDir)). diff --git a/cmd/revm/flags.go b/cmd/revm/flags.go index 0ee4575..fa1440b 100644 --- a/cmd/revm/flags.go +++ b/cmd/revm/flags.go @@ -85,7 +85,7 @@ var ( containerDiskFlag = &cli.StringFlag{ Name: define.FlagContainerDisk, - Usage: "path to a persistent ext4 raw disk image for container storage; auto-created if the file does not exist; defaults to a workspace-local disk if unset", + Usage: "persistent ext4 raw disk image for container storage (format: [,version=]); auto-created if missing; if the stored version xattr is missing or mismatched, the disk is recreated; defaults to a workspace-local disk with the built-in container disk version when unset", } podmanProxyAPIFileFlag = &cli.StringFlag{ diff --git a/docs/docker-mode.md b/docs/docker-mode.md index 28f5c09..c140063 100644 --- a/docs/docker-mode.md +++ b/docs/docker-mode.md @@ -78,7 +78,7 @@ revm docker [flags] | `--raw-disk` | Attach an ext4 disk image (format: `[,uuid=][,version=][,mnt=]`); path-only works; new disks auto-create, default to a random UUID, and mount at `/mnt/` (repeatable) | — | | `--network` | Network stack: `gvisor` (full virtual NIC, supports port mapping) or `tsi` (transparent intercept) | `gvisor` | | `--system-proxy` | Read macOS system proxy and inject into containers; rewrites `127.0.0.1` to `host.containers.internal` | `false` | -| `--container-disk` | Path to a persistent ext4 raw disk image for container storage; auto-created if missing; defaults to a session-local disk | session-local | +| `--container-disk` | Container storage disk spec (format: `[,version=]`); path-only works; defaults to a session-local disk with the built-in container disk version; if the stored version xattr is missing or mismatched, the disk is recreated | session-local + built-in version | | `--podman-proxy-api-file` | Custom Unix socket path for the Podman API proxy; defaults to `/socks/podman-api.sock` | — | | `--manage-api-file` | Custom Unix socket path for the VM management API; defaults to `/socks/vmctl.sock` | — | | `--ssh-key-dir` | Directory to symlink the generated SSH key pair (`key` and `key.pub`) into; keys are always created inside the session directory | — | diff --git a/docs/docker-mode_zh.md b/docs/docker-mode_zh.md index 4d06913..f4b5f13 100644 --- a/docs/docker-mode_zh.md +++ b/docs/docker-mode_zh.md @@ -76,7 +76,7 @@ revm docker [flags] | `--raw-disk` | 挂载 ext4 裸盘镜像(格式:`[,uuid=][,version=][,mnt=]`);只传路径即可;新磁盘会自动创建,默认随机 UUID,并挂载到 `/mnt/`(可重复) | — | | `--network` | 网络栈:`gvisor`(完整虚拟网卡,支持端口映射)或 `tsi`(透明转发) | `gvisor` | | `--system-proxy` | 读取 macOS 系统代理并注入容器内,自动将 127.0.0.1 重写为 `host.containers.internal` | `false` | -| `--container-disk` | 持久化容器存储磁盘路径(ext4 裸盘镜像);不存在时自动创建;不指定则使用会话目录内的默认磁盘 | 会话目录内默认磁盘 | +| `--container-disk` | 容器存储磁盘规格(格式:`[,version=]`);只传路径即可;默认使用会话目录内的磁盘和内置 version;如果已有磁盘的 version xattr 缺失或不匹配,会直接重建 | 会话目录内默认磁盘 + 内置 version | | `--podman-proxy-api-file` | Podman API socket 的自定义 Unix socket 路径;默认为 `<会话目录>/socks/podman-api.sock` | — | | `--manage-api-file` | VM 管理 API socket 的自定义 Unix socket 路径;默认为 `<会话目录>/socks/vmctl.sock` | — | | `--ssh-key-dir` | SSH 密钥对(`key` 和 `key.pub`)的符号链接目录;密钥始终在会话目录内生成 | — | diff --git a/pkg/define/const.go b/pkg/define/const.go index f069bf2..82801a0 100644 --- a/pkg/define/const.go +++ b/pkg/define/const.go @@ -40,7 +40,8 @@ const ( VMConfigFilePathInGuest = "/vmconfig.json" HostDomainInGVPNet = "host.containers.internal" - ContainerStorageMountPoint = "/var/lib/containers" + ContainerStorageMountPoint = "/var/lib/containers" + DefaultContainerDiskVersion = "revm-container-storage-v1" DefaultGuestUser = "root" diff --git a/pkg/librevm/config.go b/pkg/librevm/config.go index 8688531..6b2a5e3 100644 --- a/pkg/librevm/config.go +++ b/pkg/librevm/config.go @@ -48,20 +48,19 @@ type Config struct { WorkDir string `toml:"workdir,omitempty" json:"workdir,omitempty"` Env []string `toml:"env,omitempty" json:"env,omitempty"` - Network string `toml:"network,omitempty" json:"network,omitempty"` // "gvisor" | "tsi" - Mounts []string `toml:"mounts,omitempty" json:"mounts,omitempty"` // "/host:/guest[,ro]" - Disks []RawDiskSpec `toml:"disks,omitempty" json:"disks,omitempty"` - ContainerDisk string `toml:"container_disk,omitempty" json:"containerDisk,omitempty"` - ContainerDiskVersion string `toml:"container_disk_version,omitempty" json:"containerDiskVersion,omitempty"` - PodmanProxyAPIFile string `toml:"podman_proxy_api_file,omitempty" json:"podmanProxyAPIFile,omitempty"` - ManageAPIFile string `toml:"manage_api_file,omitempty" json:"manageAPIFile,omitempty"` - SSHKeyDir string `toml:"ssh_key_dir,omitempty" json:"sshKeyDir,omitempty"` - ExportSSHKeyPrivateFile string `toml:"export_ssh_key_private_file,omitempty" json:"exportSSHKeyPrivateFile,omitempty"` - ExportSSHKeyPublicFile string `toml:"export_ssh_key_public_file,omitempty" json:"exportSSHKeyPublicFile,omitempty"` - Proxy bool `toml:"proxy,omitempty" json:"proxy,omitempty"` - LogLevel string `toml:"log_level,omitempty" json:"logLevel,omitempty"` // default "info" - LogTo string `toml:"log_to,omitempty" json:"logTo,omitempty"` - Reporters []EventReporter `toml:"-" json:"-"` + Network string `toml:"network,omitempty" json:"network,omitempty"` // "gvisor" | "tsi" + Mounts []string `toml:"mounts,omitempty" json:"mounts,omitempty"` // "/host:/guest[,ro]" + Disks []RawDiskSpec `toml:"disks,omitempty" json:"disks,omitempty"` + ContainerDisk *ContainerDiskSpec `toml:"container_disk,omitempty" json:"containerDisk,omitempty"` + PodmanProxyAPIFile string `toml:"podman_proxy_api_file,omitempty" json:"podmanProxyAPIFile,omitempty"` + ManageAPIFile string `toml:"manage_api_file,omitempty" json:"manageAPIFile,omitempty"` + SSHKeyDir string `toml:"ssh_key_dir,omitempty" json:"sshKeyDir,omitempty"` + ExportSSHKeyPrivateFile string `toml:"export_ssh_key_private_file,omitempty" json:"exportSSHKeyPrivateFile,omitempty"` + ExportSSHKeyPublicFile string `toml:"export_ssh_key_public_file,omitempty" json:"exportSSHKeyPublicFile,omitempty"` + Proxy bool `toml:"proxy,omitempty" json:"proxy,omitempty"` + LogLevel string `toml:"log_level,omitempty" json:"logLevel,omitempty"` // default "info" + LogTo string `toml:"log_to,omitempty" json:"logTo,omitempty"` + Reporters []EventReporter `toml:"-" json:"-"` } // DefaultConfig returns a Config with sensible defaults pre-filled. @@ -83,15 +82,10 @@ func (c *Config) WithMemory(mb uint64) *Config { c.MemoryMB = mb; return c } func (c *Config) WithRootfs(path string) *Config { c.Rootfs = path; return c } func (c *Config) WithWorkDir(dir string) *Config { c.WorkDir = dir; return c } func (c *Config) WithNetwork(mode string) *Config { c.Network = mode; return c } -func (c *Config) WithContainerDisk(path string) *Config { - if path != "" { - c.ContainerDisk = path - } - return c -} -func (c *Config) WithContainerDiskVersion(v string) *Config { - if v != "" { - c.ContainerDiskVersion = v +func (c *Config) WithContainerDiskSpec(spec *ContainerDiskSpec) *Config { + if spec != nil { + specCopy := *spec + c.ContainerDisk = &specCopy } return c } @@ -207,11 +201,9 @@ func (c *Config) MergeFrom(other *Config) { if len(other.Mounts) > 0 { c.Mounts = append(c.Mounts, other.Mounts...) } - if other.ContainerDisk != "" { - c.ContainerDisk = other.ContainerDisk - } - if other.ContainerDiskVersion != "" { - c.ContainerDiskVersion = other.ContainerDiskVersion + if other.ContainerDisk != nil { + specCopy := *other.ContainerDisk + c.ContainerDisk = &specCopy } if other.PodmanProxyAPIFile != "" { c.PodmanProxyAPIFile = other.PodmanProxyAPIFile diff --git a/pkg/librevm/machine.go b/pkg/librevm/machine.go index 71fb2ee..c841a85 100644 --- a/pkg/librevm/machine.go +++ b/pkg/librevm/machine.go @@ -504,13 +504,8 @@ func buildMachine(ctx context.Context, cfg Config, workspacePath string) (mc *de return nil, nil, fmt.Errorf("configure podman: %w", err) } - diskPath := mBuilder.pathMgr.GetBuiltInContainerStorageDiskFile() - if cfg.ContainerDisk != "" { - diskPath = cfg.ContainerDisk - } - logrus.Info("Preparing container storage disk...") - if err := mBuilder.configureContainerRAWDisk(ctx, diskPath, cfg.ContainerDiskVersion); err != nil { + if err := mBuilder.configureContainerRAWDisk(ctx, cfg.ContainerDisk, mBuilder.pathMgr.GetBuiltInContainerStorageDiskFile()); err != nil { return nil, nil, fmt.Errorf("setup container disk: %w", err) } } diff --git a/pkg/librevm/raw_disk.go b/pkg/librevm/raw_disk.go index aad0b39..798beab 100644 --- a/pkg/librevm/raw_disk.go +++ b/pkg/librevm/raw_disk.go @@ -15,6 +15,7 @@ import ( "strings" "github.com/google/uuid" + "github.com/sirupsen/logrus" ) type RawDiskSpec struct { @@ -24,6 +25,11 @@ type RawDiskSpec struct { MountTo string `toml:"mount_to,omitempty" json:"mountTo,omitempty"` } +type ContainerDiskSpec struct { + Path string `toml:"path,omitempty" json:"path,omitempty"` + Version string `toml:"version,omitempty" json:"version,omitempty"` +} + var ( newRawDiskManager = func() (disk.Manager, error) { return disk.NewBlkManager() } newRawDiskXattrManager = filesystem.NewXattrManager @@ -100,35 +106,125 @@ func ParseRawDiskSpec(input string) (RawDiskSpec, error) { return spec, nil } +func ParseContainerDiskSpec(input string) (ContainerDiskSpec, error) { + input = strings.TrimSpace(input) + if input == "" { + return ContainerDiskSpec{}, fmt.Errorf("container disk spec cannot be empty") + } + + parts := strings.Split(input, ",") + spec := ContainerDiskSpec{ + Path: strings.TrimSpace(parts[0]), + } + if spec.Path == "" { + return ContainerDiskSpec{}, fmt.Errorf("container disk path cannot be empty") + } + + seen := map[string]struct{}{} + for _, part := range parts[1:] { + part = strings.TrimSpace(part) + if part == "" { + return ContainerDiskSpec{}, fmt.Errorf("container disk spec %q contains an empty option", input) + } + + key, value, ok := strings.Cut(part, "=") + if !ok { + return ContainerDiskSpec{}, fmt.Errorf("container disk option %q must use key=value syntax", part) + } + + key = strings.ToLower(strings.TrimSpace(key)) + value = strings.TrimSpace(value) + if key == "" || value == "" { + return ContainerDiskSpec{}, fmt.Errorf("container disk option %q must have non-empty key and value", part) + } + if _, exists := seen[key]; exists { + return ContainerDiskSpec{}, fmt.Errorf("container disk option %q is duplicated", key) + } + seen[key] = struct{}{} + + switch key { + case "version": + spec.Version = value + default: + return ContainerDiskSpec{}, fmt.Errorf("unsupported container disk option %q", key) + } + } + + return spec, nil +} + func (v *machineBuilder) prepareRawDisk(ctx context.Context, spec RawDiskSpec) (define.BlkDev, error) { spec, err := normalizeRawDiskSpec(spec) if err != nil { return define.BlkDev{}, err } + logrus.Infof("preparing raw disk: path=%q requested_uuid=%q requested_version=%q requested_mount=%q", spec.Path, spec.UUID, spec.Version, spec.MountTo) + exists, err := rawDiskExists(spec.Path) if err != nil { return define.BlkDev{}, err } if exists { + logrus.Infof("raw disk already exists: path=%q", spec.Path) + recreate, err := shouldRecreateRAWDisk(ctx, spec) if err != nil { return define.BlkDev{}, err } if recreate { + logrus.Infof("recreating raw disk: path=%q", spec.Path) if err := os.Remove(spec.Path); err != nil && !os.IsNotExist(err) { return define.BlkDev{}, fmt.Errorf("remove stale raw disk %q: %w", spec.Path, err) } return createRAWDisk(ctx, spec) } + if spec.UUID != "" { + logrus.Infof("raw disk exists, requested uuid is ignored: path=%q requested_uuid=%q", spec.Path, spec.UUID) + } + return inspectRAWDisk(ctx, spec.Path, spec.MountTo) } return createRAWDisk(ctx, spec) } +func (v *machineBuilder) prepareContainerStorageDisk(ctx context.Context, spec *ContainerDiskSpec, defaultPath string) (define.BlkDev, error) { + rawDiskSpec, err := resolveContainerDiskSpec(spec, defaultPath) + if err != nil { + return define.BlkDev{}, err + } + + logrus.Infof("preparing container disk: path=%q requested_version=%q effective_version=%q mount=%q", rawDiskSpec.Path, containerDiskVersionValue(spec), rawDiskSpec.Version, rawDiskSpec.MountTo) + + exists, err := rawDiskExists(rawDiskSpec.Path) + if err != nil { + return define.BlkDev{}, err + } + + if exists { + logrus.Infof("container disk already exists: path=%q", rawDiskSpec.Path) + + recreate, err := shouldBumpContainerDisk(ctx, rawDiskSpec) + if err != nil { + return define.BlkDev{}, err + } + if recreate { + logrus.Infof("recreating container disk: path=%q", rawDiskSpec.Path) + if err := os.Remove(rawDiskSpec.Path); err != nil && !os.IsNotExist(err) { + return define.BlkDev{}, fmt.Errorf("remove stale container disk %q: %w", rawDiskSpec.Path, err) + } + return createRAWDisk(ctx, rawDiskSpec) + } + + return inspectRAWDisk(ctx, rawDiskSpec.Path, rawDiskSpec.MountTo) + } + + return createRAWDisk(ctx, rawDiskSpec) +} + func normalizeRawDiskSpec(spec RawDiskSpec) (RawDiskSpec, error) { spec.Path = strings.TrimSpace(spec.Path) if spec.Path == "" { @@ -160,6 +256,35 @@ func normalizeRawDiskSpec(spec RawDiskSpec) (RawDiskSpec, error) { return spec, nil } +func resolveContainerDiskSpec(spec *ContainerDiskSpec, defaultPath string) (RawDiskSpec, error) { + resolved := ContainerDiskSpec{ + Path: defaultPath, + Version: define.DefaultContainerDiskVersion, + } + if spec != nil { + if strings.TrimSpace(spec.Path) != "" { + resolved.Path = spec.Path + } + if strings.TrimSpace(spec.Version) != "" { + resolved.Version = spec.Version + } + } + + return normalizeRawDiskSpec(RawDiskSpec{ + Path: resolved.Path, + UUID: define.ContainerDiskUUID, + Version: resolved.Version, + MountTo: define.ContainerStorageMountPoint, + }) +} + +func containerDiskVersionValue(spec *ContainerDiskSpec) string { + if spec == nil { + return "" + } + return spec.Version +} + func rawDiskExists(rawDiskPath string) (bool, error) { _, err := os.Stat(rawDiskPath) if err == nil { @@ -173,18 +298,46 @@ func rawDiskExists(rawDiskPath string) (bool, error) { func shouldRecreateRAWDisk(ctx context.Context, spec RawDiskSpec) (bool, error) { if spec.Version == "" { + logrus.Infof("raw disk version not requested, skipping xattr comparison: path=%q", spec.Path) return false, nil } + logrus.Infof("checking raw disk version xattr: path=%q key=%q expected=%q", spec.Path, define.XattrDiskVersionKey, spec.Version) stored, ok, err := newRawDiskXattrManager().LookupXattr(ctx, spec.Path, define.XattrDiskVersionKey) if err != nil { return false, fmt.Errorf("read raw disk version xattr from %q: %w", spec.Path, err) } if !ok { + logrus.Infof("raw disk version xattr is missing, keeping existing disk: path=%q", spec.Path) return false, nil } - return stored != spec.Version, nil + if stored != spec.Version { + logrus.Infof("raw disk version mismatch: path=%q stored=%q expected=%q", spec.Path, stored, spec.Version) + return true, nil + } + + logrus.Infof("raw disk version matches: path=%q version=%q", spec.Path, stored) + return false, nil +} + +func shouldBumpContainerDisk(ctx context.Context, spec RawDiskSpec) (bool, error) { + logrus.Infof("checking container disk version xattr: path=%q key=%q expected=%q", spec.Path, define.XattrDiskVersionKey, spec.Version) + stored, ok, err := newRawDiskXattrManager().LookupXattr(ctx, spec.Path, define.XattrDiskVersionKey) + if err != nil { + return false, fmt.Errorf("read container disk version xattr from %q: %w", spec.Path, err) + } + if !ok { + logrus.Infof("container disk version xattr is missing, bumping disk: path=%q", spec.Path) + return true, nil + } + if stored != spec.Version { + logrus.Infof("container disk version mismatch: path=%q stored=%q expected=%q", spec.Path, stored, spec.Version) + return true, nil + } + + logrus.Infof("container disk version matches: path=%q version=%q", spec.Path, stored) + return false, nil } func createRAWDisk(ctx context.Context, spec RawDiskSpec) (define.BlkDev, error) { @@ -193,20 +346,25 @@ func createRAWDisk(ctx context.Context, spec RawDiskSpec) (define.BlkDev, error) diskUUID = uuid.NewString() } + logrus.Infof("creating raw disk: path=%q uuid=%q mount=%q", spec.Path, diskUUID, resolveRawDiskMount(diskUUID, spec.MountTo)) + diskMgr, err := newRawDiskManager() if err != nil { return define.BlkDev{}, err } + logrus.Infof("extracting embedded raw disk image: path=%q", spec.Path) if err := extractEmbeddedRAWDisk(ctx, spec.Path); err != nil { return define.BlkDev{}, fmt.Errorf("extract embedded raw disk to %q: %w", spec.Path, err) } + logrus.Infof("writing raw disk uuid: path=%q uuid=%q", spec.Path, diskUUID) if err := diskMgr.NewUUID(ctx, diskUUID, spec.Path); err != nil { return define.BlkDev{}, fmt.Errorf("write uuid %q to raw disk %q: %w", diskUUID, spec.Path, err) } if spec.Version != "" { + logrus.Infof("writing raw disk version xattr: path=%q key=%q value=%q", spec.Path, define.XattrDiskVersionKey, spec.Version) if err := newRawDiskXattrManager().SetXattr(ctx, spec.Path, define.XattrDiskVersionKey, spec.Version, true); err != nil { return define.BlkDev{}, fmt.Errorf("write raw disk version xattr on %q: %w", spec.Path, err) } @@ -227,6 +385,7 @@ func inspectRAWDisk(ctx context.Context, rawDiskPath string, mountOverride strin } info.MountTo = resolveRawDiskMount(info.UUID, mountOverride) + logrus.Infof("raw disk ready: path=%q uuid=%q mount=%q fstype=%q", info.Path, info.UUID, info.MountTo, info.FsType) return *info, nil } diff --git a/pkg/librevm/raw_disk_test.go b/pkg/librevm/raw_disk_test.go index 3fe22f2..9af27fc 100644 --- a/pkg/librevm/raw_disk_test.go +++ b/pkg/librevm/raw_disk_test.go @@ -129,6 +129,44 @@ func TestParseRawDiskSpec_RejectsNonKeyValueOptions(t *testing.T) { } } +func TestParseContainerDiskSpec_PathOnly(t *testing.T) { + spec, err := ParseContainerDiskSpec("/tmp/container-storage.ext4") + if err != nil { + t.Fatalf("ParseContainerDiskSpec returned error: %v", err) + } + + if spec.Path != "/tmp/container-storage.ext4" { + t.Fatalf("unexpected path: %q", spec.Path) + } + if spec.Version != "" { + t.Fatalf("expected version to be empty, got %q", spec.Version) + } +} + +func TestParseContainerDiskSpec_WithVersion(t *testing.T) { + spec, err := ParseContainerDiskSpec("/tmp/container-storage.ext4,version=v2") + if err != nil { + t.Fatalf("ParseContainerDiskSpec returned error: %v", err) + } + + if spec.Path != "/tmp/container-storage.ext4" { + t.Fatalf("unexpected path: %q", spec.Path) + } + if spec.Version != "v2" { + t.Fatalf("unexpected version: %q", spec.Version) + } +} + +func TestParseContainerDiskSpec_RejectsUnsupportedOption(t *testing.T) { + _, err := ParseContainerDiskSpec("/tmp/container-storage.ext4,uuid=" + uuid.NewString()) + if err == nil { + t.Fatal("expected ParseContainerDiskSpec to reject unsupported option") + } + if !strings.Contains(err.Error(), "unsupported container disk option") { + t.Fatalf("unexpected error: %v", err) + } +} + func TestEnsureRAWDisk_CreatesMissingDiskWithDefaults(t *testing.T) { diskMgr, xattrMgr, extracted := installRawDiskTestDoubles(t) rawDiskPath := filepath.Join(t.TempDir(), "new.ext4") @@ -260,6 +298,120 @@ func TestEnsureRAWDisk_RecreatesWhenVersionMismatches(t *testing.T) { } } +func TestPrepareContainerStorageDisk_DefaultsWhenUnset(t *testing.T) { + diskMgr, xattrMgr, extracted := installRawDiskTestDoubles(t) + defaultPath := filepath.Join(t.TempDir(), "container-storage.ext4") + + blkDev, err := (&machineBuilder{}).prepareContainerStorageDisk(context.Background(), nil, defaultPath) + if err != nil { + t.Fatalf("prepareContainerStorageDisk returned error: %v", err) + } + + defaultPath = mustAbsPath(defaultPath) + if len(*extracted) != 1 || (*extracted)[0] != defaultPath { + t.Fatalf("expected container disk to be extracted once, got %v", *extracted) + } + if len(diskMgr.newUUIDCalls) != 1 || diskMgr.newUUIDCalls[0].uuid != define.ContainerDiskUUID { + t.Fatalf("expected container disk UUID %q, got %v", define.ContainerDiskUUID, diskMgr.newUUIDCalls) + } + if got := xattrMgr.values[defaultPath][define.XattrDiskVersionKey]; got != define.DefaultContainerDiskVersion { + t.Fatalf("unexpected default container disk version xattr: %q", got) + } + if blkDev.MountTo != define.ContainerStorageMountPoint { + t.Fatalf("unexpected mount target: %q", blkDev.MountTo) + } +} + +func TestPrepareContainerStorageDisk_RecreatesWhenVersionXattrMissing(t *testing.T) { + diskMgr, xattrMgr, extracted := installRawDiskTestDoubles(t) + rawDiskPath := filepath.Join(t.TempDir(), "container-storage.ext4") + createTestFile(t, rawDiskPath) + + rawDiskPath = mustAbsPath(rawDiskPath) + diskMgr.uuids[rawDiskPath] = uuid.NewString() + + blkDev, err := (&machineBuilder{}).prepareContainerStorageDisk(context.Background(), &ContainerDiskSpec{ + Path: rawDiskPath, + Version: "v2", + }, filepath.Join(t.TempDir(), "unused.ext4")) + if err != nil { + t.Fatalf("prepareContainerStorageDisk returned error: %v", err) + } + + if len(*extracted) != 1 || (*extracted)[0] != rawDiskPath { + t.Fatalf("expected container disk recreation, got %v", *extracted) + } + if len(diskMgr.newUUIDCalls) != 1 || diskMgr.newUUIDCalls[0].uuid != define.ContainerDiskUUID { + t.Fatalf("expected recreated container disk UUID %q, got %v", define.ContainerDiskUUID, diskMgr.newUUIDCalls) + } + if got := xattrMgr.values[rawDiskPath][define.XattrDiskVersionKey]; got != "v2" { + t.Fatalf("unexpected container disk version xattr: %q", got) + } + if blkDev.MountTo != define.ContainerStorageMountPoint { + t.Fatalf("unexpected mount target: %q", blkDev.MountTo) + } +} + +func TestPrepareContainerStorageDisk_RecreatesWhenVersionMismatches(t *testing.T) { + diskMgr, xattrMgr, extracted := installRawDiskTestDoubles(t) + rawDiskPath := filepath.Join(t.TempDir(), "container-storage.ext4") + createTestFile(t, rawDiskPath) + + rawDiskPath = mustAbsPath(rawDiskPath) + diskMgr.uuids[rawDiskPath] = define.ContainerDiskUUID + xattrMgr.values[rawDiskPath] = map[string]string{ + define.XattrDiskVersionKey: "old-version", + } + + _, err := (&machineBuilder{}).prepareContainerStorageDisk(context.Background(), &ContainerDiskSpec{ + Path: rawDiskPath, + Version: "new-version", + }, filepath.Join(t.TempDir(), "unused.ext4")) + if err != nil { + t.Fatalf("prepareContainerStorageDisk returned error: %v", err) + } + + if len(*extracted) != 1 || (*extracted)[0] != rawDiskPath { + t.Fatalf("expected container disk recreation, got %v", *extracted) + } + if len(diskMgr.newUUIDCalls) != 1 || diskMgr.newUUIDCalls[0].uuid != define.ContainerDiskUUID { + t.Fatalf("expected recreated container disk UUID %q, got %v", define.ContainerDiskUUID, diskMgr.newUUIDCalls) + } + if got := xattrMgr.values[rawDiskPath][define.XattrDiskVersionKey]; got != "new-version" { + t.Fatalf("unexpected container disk version xattr: %q", got) + } +} + +func TestPrepareContainerStorageDisk_ReusesWhenVersionMatches(t *testing.T) { + diskMgr, xattrMgr, extracted := installRawDiskTestDoubles(t) + rawDiskPath := filepath.Join(t.TempDir(), "container-storage.ext4") + createTestFile(t, rawDiskPath) + + rawDiskPath = mustAbsPath(rawDiskPath) + diskMgr.uuids[rawDiskPath] = define.ContainerDiskUUID + xattrMgr.values[rawDiskPath] = map[string]string{ + define.XattrDiskVersionKey: "v3", + } + + blkDev, err := (&machineBuilder{}).prepareContainerStorageDisk(context.Background(), &ContainerDiskSpec{ + Path: rawDiskPath, + Version: "v3", + }, filepath.Join(t.TempDir(), "unused.ext4")) + if err != nil { + t.Fatalf("prepareContainerStorageDisk returned error: %v", err) + } + + if len(*extracted) != 0 { + t.Fatalf("expected existing container disk to be reused, got %v", *extracted) + } + if len(diskMgr.newUUIDCalls) != 0 { + t.Fatalf("expected no UUID rewrite, got %v", diskMgr.newUUIDCalls) + } + if blkDev.MountTo != define.ContainerStorageMountPoint { + t.Fatalf("unexpected mount target: %q", blkDev.MountTo) + } +} + func installRawDiskTestDoubles(t *testing.T) (*fakeRawDiskManager, *fakeXattrManager, *[]string) { t.Helper() diff --git a/pkg/librevm/storage.go b/pkg/librevm/storage.go index 47dce76..de7a62f 100644 --- a/pkg/librevm/storage.go +++ b/pkg/librevm/storage.go @@ -5,16 +5,10 @@ package librevm import ( "context" "fmt" - "linuxvm/pkg/define" ) -func (v *machineBuilder) configureContainerRAWDisk(ctx context.Context, diskPath string, version string) error { - blkDev, err := v.prepareRawDisk(ctx, RawDiskSpec{ - Path: diskPath, - UUID: define.ContainerDiskUUID, - Version: version, - MountTo: define.ContainerStorageMountPoint, - }) +func (v *machineBuilder) configureContainerRAWDisk(ctx context.Context, spec *ContainerDiskSpec, defaultPath string) error { + blkDev, err := v.prepareContainerStorageDisk(ctx, spec, defaultPath) if err != nil { return fmt.Errorf("prepare container storage raw disk: %w", err) }