daemon: build a work-seed during image pull, refresh doctor check
Before this change `banger image pull` (both OCI-direct and bundle
paths) shipped images with an empty WorkSeedPath — the BuildWorkSeedImage
helper existed only behind the hidden `banger internal work-seed` CLI.
Every pulled image hit ensureWorkDisk's no-seed branch, and the guest
booted with a bare /root (no .bashrc, no .profile, none of the distro
defaults).
Pull now calls BuildWorkSeedImage after the rootfs is finalised (OCI)
or fetched (bundle). The builder is behind a new `workSeedBuilder` test
seam so existing pull tests don't accidentally demand sudo mount. The
build failure is non-fatal: any error logs a warning and leaves
WorkSeedPath empty — images stay publishable even if the pulled rootfs
has no /root to extract.
Verified end-to-end by wiping the cached smoke image and re-pulling:
work-seed.ext4 lands in the artifact dir next to rootfs.ext4, and all
21 smoke scenarios pass.
Also refreshes the "feature /root work disk" fallback tooling check —
the no-seed path no longer touches mount/umount/cp after commit
0e28504, so the doctor check now only requires truncate + mkfs.ext4.
The warn copy updates from "new VM creates will be slower" to "guest
/root will be empty", which matches the actual tradeoff post-refactor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
02773c1cf5
commit
3edd7c6de7
8 changed files with 74 additions and 19 deletions
|
|
@ -16,6 +16,7 @@ import (
|
|||
"banger/internal/imagepull"
|
||||
"banger/internal/model"
|
||||
"banger/internal/paths"
|
||||
"banger/internal/system"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
)
|
||||
|
|
@ -168,6 +169,7 @@ func (s *ImageService) pullFromOCI(ctx context.Context, params api.ImagePullPara
|
|||
if err := s.runFinalizePulledRootfs(ctx, rootfsExt4, meta); err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
workSeedExt4 := s.runBuildWorkSeed(ctx, rootfsExt4, stagingDir)
|
||||
|
||||
stagedKernel, stagedInitrd, stagedModules, err := imagemgr.StageBootArtifacts(ctx, s.runner, stagingDir, kernelPath, initrdPath, modulesDir)
|
||||
if err != nil {
|
||||
|
|
@ -187,6 +189,9 @@ func (s *ImageService) pullFromOCI(ctx context.Context, params api.ImagePullPara
|
|||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if workSeedExt4 != "" {
|
||||
image.WorkSeedPath = filepath.Join(finalDir, filepath.Base(workSeedExt4))
|
||||
}
|
||||
published, err := s.publishImage(ctx, image, stagingDir, finalDir)
|
||||
if err != nil {
|
||||
return model.Image{}, err
|
||||
|
|
@ -245,6 +250,7 @@ func (s *ImageService) pullFromBundle(ctx context.Context, params api.ImagePullP
|
|||
// so the final artifact dir contains only boot-relevant files.
|
||||
_ = os.Remove(filepath.Join(stagingDir, imagecat.ManifestFilename))
|
||||
rootfsExt4 := filepath.Join(stagingDir, imagecat.RootfsFilename)
|
||||
workSeedExt4 := s.runBuildWorkSeed(ctx, rootfsExt4, stagingDir)
|
||||
|
||||
stagedKernel, stagedInitrd, stagedModules, err := imagemgr.StageBootArtifacts(ctx, s.runner, stagingDir, kernelPath, initrdPath, modulesDir)
|
||||
if err != nil {
|
||||
|
|
@ -264,6 +270,9 @@ func (s *ImageService) pullFromBundle(ctx context.Context, params api.ImagePullP
|
|||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if workSeedExt4 != "" {
|
||||
image.WorkSeedPath = filepath.Join(finalDir, filepath.Base(workSeedExt4))
|
||||
}
|
||||
published, err := s.publishImage(ctx, image, stagingDir, finalDir)
|
||||
if err != nil {
|
||||
return model.Image{}, err
|
||||
|
|
@ -315,6 +324,30 @@ func (s *ImageService) runFinalizePulledRootfs(ctx context.Context, ext4File str
|
|||
return nil
|
||||
}
|
||||
|
||||
// runBuildWorkSeed extracts /root from the pulled rootfs into a
|
||||
// sibling work-seed ext4 image. Any failure is treated as non-fatal:
|
||||
// the image is still publishable without a seed, and VM create falls
|
||||
// back to the empty-work-disk path (losing distro dotfiles but keeping
|
||||
// every other guarantee). Returns the work-seed path on success, "" on
|
||||
// failure (with a warn logged). Tests substitute via s.workSeedBuilder.
|
||||
func (s *ImageService) runBuildWorkSeed(ctx context.Context, rootfsExt4, stagingDir string) string {
|
||||
outPath := filepath.Join(stagingDir, "work-seed.ext4")
|
||||
var err error
|
||||
if s.workSeedBuilder != nil {
|
||||
err = s.workSeedBuilder(ctx, rootfsExt4, outPath)
|
||||
} else {
|
||||
err = system.BuildWorkSeedImage(ctx, s.runner, rootfsExt4, outPath)
|
||||
}
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Warn("work-seed build failed; VMs using this image will start with an empty /root", "rootfs", rootfsExt4, "error", err.Error())
|
||||
}
|
||||
_ = os.Remove(outPath)
|
||||
return ""
|
||||
}
|
||||
return outPath
|
||||
}
|
||||
|
||||
// nameSanitize keeps lowercase alphanumerics + hyphens, collapses runs.
|
||||
var nameSanitizeRE = regexp.MustCompile(`[^a-z0-9]+`)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue