Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/images/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ Content-addressable storage with tag symlinks (similar to Docker/Unikraft):
rootfs.erofs
latest -> abc123def456... # Tag symlink to digest
3.18 -> def456abc123... # Another tag
layers/ # Shared materialized layer artifacts
abc123def456.../
layer.erofs
artifact.erofs.json
system/
oci-cache/ # Shared OCI layout for all images
index.json # Manifest index with digest-based tags
Expand All @@ -82,6 +86,7 @@ Content-addressable storage with tag symlinks (similar to Docker/Unikraft):
- Natural hierarchy: All versions of an image grouped under repository
- Easy inspection: Clear which digest belongs to which image
- Layer caching: All images share the same blob storage, layers deduplicated automatically
- Materialized layer artifacts are reference-protected and reconciled by the layer lifecycle manager; stale temporary trees are age-gated before removal.

**Design:**
- Images stored by manifest digest (content hash)
Expand All @@ -92,6 +97,7 @@ Content-addressable storage with tag symlinks (similar to Docker/Unikraft):
- Shared blob storage enables automatic layer deduplication across all images
- Orphaned digests are automatically deleted when the last tag referencing them is removed
- Symlinks only created after successful build (status: ready)
- Disk accounting uses logical file sizes, matching image metadata and storage admission rather than filesystem block allocation.

## Reference Handling (reference.go)

Expand Down
80 changes: 68 additions & 12 deletions lib/images/disk.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package images

import (
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -29,23 +31,28 @@ var DefaultImageFormat = func() ExportFormat {
return FormatErofs
}()

// ExportRootfs exports rootfs directory in specified format (public for system manager)
// ExportRootfs exports rootfs directory in specified format (public for system manager).
func ExportRootfs(rootfsDir, outputPath string, format ExportFormat) (int64, error) {
return ExportRootfsWithContext(context.Background(), rootfsDir, outputPath, format)
}

// ExportRootfsWithContext exports rootfs directory and cancels external formatters with ctx.
func ExportRootfsWithContext(ctx context.Context, rootfsDir, outputPath string, format ExportFormat) (int64, error) {
switch format {
case FormatExt4:
return convertToExt4(rootfsDir, outputPath)
return convertToExt4(ctx, rootfsDir, outputPath)
case FormatErofs:
return convertToErofs(rootfsDir, outputPath)
return convertToErofs(ctx, rootfsDir, outputPath)
case FormatCpio:
return convertToCpio(rootfsDir, outputPath)
return convertToCpio(ctx, rootfsDir, outputPath)
default:
return 0, fmt.Errorf("unsupported export format: %s", format)
}
}

// convertToCpio packages directory as uncompressed cpio archive (initramfs format)
// Uses uncompressed format for faster boot (kernel loads directly without decompression)
func convertToCpio(rootfsDir, outputPath string) (int64, error) {
func convertToCpio(ctx context.Context, rootfsDir, outputPath string) (int64, error) {
// Ensure parent directory exists
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
return 0, fmt.Errorf("create output dir: %w", err)
Expand All @@ -56,7 +63,13 @@ func convertToCpio(rootfsDir, outputPath string) (int64, error) {
if err != nil {
return 0, fmt.Errorf("create output file: %w", err)
}
defer outFile.Close()
keepOutput := false
defer func() {
_ = outFile.Close()
if !keepOutput {
_ = os.Remove(outputPath)
}
}()

// Create newc format cpio writer (kernel-compatible format)
cpioWriter := cpio.Newc.Writer(outFile)
Expand All @@ -66,6 +79,9 @@ func convertToCpio(rootfsDir, outputPath string) (int64, error) {

// Walk the rootfs directory and add all files
err = filepath.Walk(rootfsDir, func(path string, info os.FileInfo, err error) error {
if ctxErr := ctx.Err(); ctxErr != nil {
return ctxErr
}
if err != nil {
return err
}
Expand All @@ -89,6 +105,13 @@ func convertToCpio(rootfsDir, outputPath string) (int64, error) {

// Set the name to be relative to root
rec.Name = relPath
if rec.ReaderAt != nil {
rec.ReaderAt = &contextReaderAt{
ctx: ctx,
reader: rec.ReaderAt,
closer: readerCloser(rec.ReaderAt),
}
}

// Write the record to the archive
if err := cpioWriter.WriteRecord(rec); err != nil {
Expand All @@ -113,9 +136,35 @@ func convertToCpio(rootfsDir, outputPath string) (int64, error) {
return 0, fmt.Errorf("stat output: %w", err)
}

keepOutput = true
return stat.Size(), nil
}

type contextReaderAt struct {
ctx context.Context
reader io.ReaderAt
closer io.Closer
}

func (r *contextReaderAt) ReadAt(p []byte, off int64) (int, error) {
if err := r.ctx.Err(); err != nil {
return 0, err
}
return r.reader.ReadAt(p, off)
}

func (r *contextReaderAt) Close() error {
if r.closer == nil {
return nil
}
return r.closer.Close()
}

func readerCloser(reader io.ReaderAt) io.Closer {
closer, _ := reader.(io.Closer)
return closer
}

// sectorSize is the block size for disk images (required by Virtualization.framework)
const sectorSize = 4096

Expand All @@ -128,9 +177,9 @@ func alignToSector(size int64) int64 {
}

// convertToExt4 converts a rootfs directory to an ext4 disk image using mkfs.ext4
func convertToExt4(rootfsDir, diskPath string) (int64, error) {
func convertToExt4(ctx context.Context, rootfsDir, diskPath string) (int64, error) {
// Calculate size of rootfs directory
sizeBytes, err := dirSize(rootfsDir)
sizeBytes, err := dirSizeWithContext(ctx, rootfsDir)
if err != nil {
return 0, fmt.Errorf("calculate dir size: %w", err)
}
Expand Down Expand Up @@ -168,7 +217,7 @@ func convertToExt4(rootfsDir, diskPath string) (int64, error) {
// -O ^has_journal: Disable journal (not needed for read-only VM mounts)
// -d: Copy directory contents into filesystem
// -F: Force creation (file not block device)
cmd := exec.Command(mkfsExt4Binary(), "-b", "4096", "-O", "^has_journal", "-d", rootfsDir, "-F", diskPath)
cmd := exec.CommandContext(ctx, mkfsExt4Binary(), "-b", "4096", "-O", "^has_journal", "-d", rootfsDir, "-F", diskPath)
output, err := cmd.CombinedOutput()
if err != nil {
return 0, fmt.Errorf("mkfs.ext4 failed: %w, output: %s", err, output)
Expand All @@ -193,7 +242,7 @@ func convertToExt4(rootfsDir, diskPath string) (int64, error) {
}

// convertToErofs converts a rootfs directory to an erofs disk image using mkfs.erofs
func convertToErofs(rootfsDir, diskPath string) (int64, error) {
func convertToErofs(ctx context.Context, rootfsDir, diskPath string) (int64, error) {
// Ensure parent directory exists
if err := os.MkdirAll(filepath.Dir(diskPath), 0755); err != nil {
return 0, fmt.Errorf("create disk parent dir: %w", err)
Expand All @@ -202,7 +251,7 @@ func convertToErofs(rootfsDir, diskPath string) (int64, error) {
// Create erofs image with LZ4 fast compression
// -zlz4: LZ4 fast compression (~20-25% space savings, faster builds)
// erofs doesn't need pre-allocation, creates file directly
cmd := exec.Command("mkfs.erofs", "-zlz4", diskPath, rootfsDir)
cmd := exec.CommandContext(ctx, "mkfs.erofs", "-zlz4", diskPath, rootfsDir)
output, err := cmd.CombinedOutput()
if err != nil {
return 0, fmt.Errorf("mkfs.erofs failed: %w, output: %s", err, output)
Expand All @@ -226,13 +275,20 @@ func convertToErofs(rootfsDir, diskPath string) (int64, error) {
return stat.Size(), nil
}

// dirSize calculates the total size of a directory
// dirSize is used by layer-store reconciliation.
func dirSize(path string) (int64, error) {
return dirSizeWithContext(context.Background(), path)
}

func dirSizeWithContext(ctx context.Context, path string) (int64, error) {
var size int64
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if err := ctx.Err(); err != nil {
return err
}
if !info.IsDir() {
size += info.Size()
}
Expand Down
Loading
Loading