Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
4133967
Materialize per-layer artifacts with explicit whiteout handling
chruffins Aug 31, 2026
285a8c6
Harden layer artifact materialization
chruffins Sep 1, 2026
820d196
Address layer artifact review findings
chruffins Sep 1, 2026
480372f
Fix layer artifact composition edge cases
chruffins Sep 2, 2026
a0ea61a
Fix layer artifact composition and metadata handling edge cases
chruffins Sep 2, 2026
be91308
Harden layer artifact composition
chruffins Sep 2, 2026
122c28c
Harden layer tree portability and file copying
chruffins Sep 3, 2026
8416746
Harden layer materialization and metadata restoration
chruffins Sep 3, 2026
c2e927e
Share context-aware cached layer unpacking
chruffins Sep 3, 2026
8892900
Serialize layer store writes
chruffins Sep 3, 2026
2b7928e
Preserve explicit layer directory metadata
chruffins Sep 3, 2026
75f2d17
Use singleflight for layer materialization
chruffins Sep 3, 2026
deb563d
Test synthesized directory metadata preservation
chruffins Sep 3, 2026
53437a7
Collapse layer helper wrappers and dead branches
chruffins Sep 3, 2026
21b7c15
Drop access time preservation from layer metadata
chruffins Sep 3, 2026
f950306
Extract layers with umoci instead of a private tar walker
chruffins Sep 3, 2026
d3cb0cf
Detach shared layer builds from the initiating context
chruffins Sep 3, 2026
6681c41
Trim layer artifact plumbing
chruffins Sep 3, 2026
57d56ae
Harden layer artifact builds and drop unused plumbing
chruffins Sep 3, 2026
ae7ca83
Add layer artifact edge-case smoke tests
chruffins Sep 3, 2026
f18a718
Trim redundant smoke tests
chruffins Sep 3, 2026
cda3965
Harden shared layer materialization
chruffins Sep 8, 2026
bd68514
Harden layer cache validation
chruffins Sep 8, 2026
9e817ef
Retry disk usage scans after invalidation
chruffins Sep 8, 2026
1019753
Detect hosts that support layer artifacts
chruffins Sep 8, 2026
f99495a
Harden layer artifact accounting and cancellation
chruffins Sep 8, 2026
85afb94
Harden layer artifact capabilities and integrity
chruffins Sep 8, 2026
6247ad3
Use capability probes for whiteout tests
chruffins Sep 8, 2026
f9fdb37
Tighten layer build cleanup and verification
chruffins Sep 8, 2026
82ab651
Serialize image store ownership
chruffins Sep 8, 2026
2e8d6c8
Record layer blob encoding
chruffins Sep 8, 2026
3f5c8c1
Trim image store lock implementation
chruffins Sep 8, 2026
29fc86c
Reuse normalized layer encoding
chruffins Sep 8, 2026
eef8d3c
Simplify image store lock ownership
chruffins Sep 8, 2026
bac608d
Reject unstable disk snapshots and bad artifacts
chruffins Sep 8, 2026
e952cbc
Keep layer size helper for lifecycle reconciliation
chruffins Sep 8, 2026
abce7f3
Honor cancellation during disk accounting
chruffins Sep 9, 2026
1d761f7
Propagate disk accounting cancellation
chruffins Sep 9, 2026
c937320
Harden disk usage error handling
chruffins Sep 9, 2026
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
7 changes: 7 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,8 @@ 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)
- One Hypeman process owns a data directory at a time; the image manager holds `images/.lock` for its lifetime and rejects a second owner.
- Disk accounting uses logical file sizes, matching image metadata and storage admission rather than filesystem block allocation.

## Reference Handling (reference.go)

Expand Down
38 changes: 27 additions & 11 deletions lib/images/disk.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package images

import (
"context"
"fmt"
"os"
"os/exec"
Expand Down Expand Up @@ -29,23 +30,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 @@ -66,6 +72,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 Down Expand Up @@ -128,9 +137,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 +177,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 +202,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 +211,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 +235,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