system: build work-seed without sudo
BuildWorkSeedImage used to mount the source rootfs and the new seed
image — both via sudo. After the privilege split (59e48e8) the owner
daemon runs without sudo and those mounts fail silently inside the
image-pull pipeline (runBuildWorkSeed swallows errors), so every
freshly pulled image landed in the store with an empty WorkSeedPath
and 'banger doctor' kept warning that /root would be empty.
Rewrite the builder around the existing sudoless toolkit:
1. RdumpExt4Dir extracts /root from the source rootfs into a host
tempdir (debugfs, no mount).
2. truncate + mkfs.ext4 -F -E root_owner=0:0 produces an empty
user-owned ext4 file.
3. A Go walk over the staged tree calls MkdirExt4 /
WriteExt4FileOwned for every dir + regular file, forcing
root:root and preserving mode bits.
Symlinks and special files in /root are skipped — extremely rare on
a stock distro and not part of what makes a useful seed.
Fix won't retroactively populate already-pulled images: re-pull the
default image (e.g. 'banger image delete debian-bookworm && banger
image pull debian-bookworm') to get a seeded work-seed.ext4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3ec357090a
commit
408ad6756c
1 changed files with 59 additions and 11 deletions
|
|
@ -68,14 +68,33 @@ func WorkSeedPath(rootfsPath string) string {
|
||||||
return rootfsPath + ".work-seed"
|
return rootfsPath + ".work-seed"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildWorkSeedImage creates a sized ext4 image at outPath containing
|
||||||
|
// the /root subtree of rootfsPath. Uses only sudoless tooling: rdump
|
||||||
|
// to extract via debugfs, mkfs.ext4 to create the empty image (the
|
||||||
|
// output file is user-owned, so no elevation needed), and the ext4
|
||||||
|
// toolkit (MkdirExt4 / WriteExt4FileOwned) to ingest each entry as
|
||||||
|
// root:root. Symlinks and special files are skipped — /root in a
|
||||||
|
// stock distro contains regular files and dirs only.
|
||||||
func BuildWorkSeedImage(ctx context.Context, runner CommandRunner, rootfsPath, outPath string) error {
|
func BuildWorkSeedImage(ctx context.Context, runner CommandRunner, rootfsPath, outPath string) error {
|
||||||
rootMount, cleanupRoot, err := MountTempDir(ctx, runner, rootfsPath, true)
|
stage, err := os.MkdirTemp("", "banger-work-seed-stage-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer cleanupRoot()
|
defer os.RemoveAll(stage)
|
||||||
|
|
||||||
|
if err := RdumpExt4Dir(ctx, runner, rootfsPath, "/root", stage); err != nil {
|
||||||
|
return fmt.Errorf("extract /root from %s: %w", rootfsPath, err)
|
||||||
|
}
|
||||||
|
rootHome := filepath.Join(stage, "root")
|
||||||
|
if _, err := os.Stat(rootHome); err != nil {
|
||||||
|
// rootfs has no /root (unusual). Build an empty seed so the
|
||||||
|
// caller still gets a usable artifact — VMs cloning it will
|
||||||
|
// just see an empty fs root, same as the no-seed fallback.
|
||||||
|
if err := os.MkdirAll(rootHome, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rootHome := filepath.Join(rootMount, "root")
|
|
||||||
sizeBytes, err := estimateWorkSeedSize(ctx, runner, rootHome)
|
sizeBytes, err := estimateWorkSeedSize(ctx, runner, rootHome)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -93,17 +112,46 @@ func BuildWorkSeedImage(ctx context.Context, runner CommandRunner, rootfsPath, o
|
||||||
if err := os.Truncate(outPath, sizeBytes); err != nil {
|
if err := os.Truncate(outPath, sizeBytes); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := runner.Run(ctx, "mkfs.ext4", "-F", outPath); err != nil {
|
// `-E root_owner=0:0` stamps inode 2 (which becomes /root in the
|
||||||
|
// guest) as root:root. Per-entry owners are forced via the ext4
|
||||||
|
// toolkit walk below.
|
||||||
|
if _, err := runner.Run(ctx, "mkfs.ext4", "-F", "-E", "root_owner=0:0", outPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return ingestWorkSeedTree(ctx, runner, outPath, rootHome)
|
||||||
|
}
|
||||||
|
|
||||||
workMount, cleanupWork, err := MountTempDir(ctx, runner, outPath, false)
|
// ingestWorkSeedTree walks the staged host tree and writes every
|
||||||
if err != nil {
|
// directory and regular file into the work-seed ext4 as root:root,
|
||||||
return err
|
// preserving source mode bits. Symlinks and special files are
|
||||||
}
|
// skipped silently — they are vanishingly rare in distro /root and
|
||||||
defer cleanupWork()
|
// don't survive the work-seed → work-disk clone path either.
|
||||||
|
func ingestWorkSeedTree(ctx context.Context, runner CommandRunner, imagePath, srcRoot string) error {
|
||||||
return CopyDirContents(ctx, runner, rootHome, workMount, true)
|
srcRoot = filepath.Clean(srcRoot)
|
||||||
|
return filepath.Walk(srcRoot, func(hostPath string, info os.FileInfo, walkErr error) error {
|
||||||
|
if walkErr != nil {
|
||||||
|
return walkErr
|
||||||
|
}
|
||||||
|
if hostPath == srcRoot {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rel, err := filepath.Rel(srcRoot, hostPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
guestPath := "/" + filepath.ToSlash(rel)
|
||||||
|
switch {
|
||||||
|
case info.IsDir():
|
||||||
|
return MkdirExt4(ctx, runner, imagePath, guestPath, info.Mode().Perm(), 0, 0)
|
||||||
|
case info.Mode().IsRegular():
|
||||||
|
data, err := os.ReadFile(hostPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return WriteExt4FileOwned(ctx, runner, imagePath, guestPath, info.Mode().Perm(), 0, 0, data)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func estimateWorkSeedSize(ctx context.Context, runner CommandRunner, rootHome string) (int64, error) {
|
func estimateWorkSeedSize(ctx context.Context, runner CommandRunner, rootHome string) (int64, error) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue