banger/internal/imagepull/ext4.go
Thales Maciel ed4117d926
imagepull/BuildExt4: omit positional fs-size; rely on file truncation
mkfs.ext4's positional fs-size is documented in 1 KiB units (not the
filesystem's 4 KiB block size), so passing sizeBytes/4096 made
filesystems 1/4 the intended size. A 4 GiB request became a 1 GiB
ext4 in a 4 GiB file, packed to 0 free blocks — VM create then failed
with 'Could not allocate block' when patchRootOverlay tried to write
guest config.

The file is truncated to the target size before mkfs runs; without
the positional arg, mkfs uses the whole device.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:58:42 -03:00

73 lines
2.1 KiB
Go

package imagepull
import (
"context"
"errors"
"fmt"
"os"
"banger/internal/system"
)
// MinExt4Size is the smallest ext4 image we'll create. mkfs.ext4 needs a
// few megabytes for its bookkeeping; for a real rootfs the staging tree
// will dominate anyway.
const MinExt4Size int64 = 1 << 20 * 64 // 64 MiB
// BuildExt4 creates outFile as a sparse ext4 image of sizeBytes and
// populates it from srcDir using `mkfs.ext4 -F -d`. No mount, no sudo.
//
// sizeBytes must be at least MinExt4Size. Callers are expected to size
// the file with headroom over the staged tree (the daemon orchestrator
// does this; this function only enforces a sanity floor).
//
// The resulting image's file ownership reflects srcDir's on-disk
// ownership — see the package doc for the implications.
func BuildExt4(ctx context.Context, runner system.CommandRunner, srcDir, outFile string, sizeBytes int64) error {
if sizeBytes < MinExt4Size {
return fmt.Errorf("ext4 size %d below minimum %d", sizeBytes, MinExt4Size)
}
info, err := os.Stat(srcDir)
if err != nil {
return fmt.Errorf("stat source: %w", err)
}
if !info.IsDir() {
return fmt.Errorf("%s is not a directory", srcDir)
}
if err := os.Remove(outFile); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
f, err := os.OpenFile(outFile, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o644)
if err != nil {
return err
}
if err := f.Truncate(sizeBytes); err != nil {
_ = f.Close()
_ = os.Remove(outFile)
return err
}
if err := f.Close(); err != nil {
_ = os.Remove(outFile)
return err
}
// mkfs.ext4's positional `fs-size` is documented in 1 KiB units
// (NOT the filesystem's 4 KiB block size), so dividing by 4096
// produces a filesystem 1/4 the intended size. Omit the positional
// entirely — the file was truncated to sizeBytes above, and mkfs
// with no fs-size arg uses the whole device.
out, runErr := runner.Run(ctx, "mkfs.ext4",
"-F",
"-q",
"-d", srcDir,
"-L", "banger-rootfs",
"-E", "root_owner=0:0",
outFile,
)
if runErr != nil {
_ = os.Remove(outFile)
return fmt.Errorf("mkfs.ext4 -d: %w: %s", runErr, string(out))
}
return nil
}