Phase B-2: pre-inject banger guest agents into pulled rootfs
New imagepull.InjectGuestAgents writes banger's guest-side assets straight into the pulled ext4 so systemd will start them at first boot: /usr/local/bin/banger-vsock-agent (binary, 0755) /usr/local/libexec/banger-network-bootstrap (script, 0755) /etc/systemd/system/banger-network.service (unit, 0644) /etc/systemd/system/banger-vsock-agent.service (unit, 0644) /etc/modules-load.d/banger-vsock.conf (modules, 0644) plus enable-at-boot symlinks under /etc/systemd/system/multi-user.target.wants/ All writes + ownership + symlinks go through one `debugfs -w -f -` invocation. No sudo required because the caller owns the ext4 file. Script is deterministic: shallow-first mkdir, then write, then sif, then symlink. "File exists" errors from mkdir on already-present dirs are tolerated (debugfs keeps going past them with -f, and we filter them out of the output scan). Asset content reuses the existing guestnet.BootstrapScript / SystemdServiceUnit / ConfigPath and vsockagent.ServiceUnit / ModulesLoadConfig / GuestInstallPath — one source of truth, no duplicated systemd unit strings. Daemon wiring: new d.finalizePulledRootfs seam runs both ApplyOwnership (B-1) and InjectGuestAgents as one phase between BuildExt4 and StageBootArtifacts. The companion vsock-agent binary is resolved via paths.CompanionBinaryPath. Existing daemon tests stub the seam with a no-op to avoid needing a real companion binary + debugfs in the test harness. Tests: real-ext4 round-trip that builds a minimal ext4, runs InjectGuestAgents, then verifies every expected path is present via `debugfs stat`, plus uid=0 and mode 0755 on the vsock-agent binary. Also: missing-binary rejection, ancestor-collection order test. debugfs/mkfs.ext4 tests skip on hosts without the binaries. After B-1+B-2, any OCI image that already ships sshd boots with banger-network and banger-vsock-agent running; image pull is one step from "useful rootfs primitive". B-3 (first-boot sshd install) unlocks images that don't ship sshd. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
43982a4ae3
commit
491c8e1ebb
5 changed files with 393 additions and 18 deletions
|
|
@ -52,6 +52,7 @@ type Daemon struct {
|
|||
vmCaps []vmCapability
|
||||
imageBuild func(context.Context, imageBuildSpec) error
|
||||
pullAndFlatten func(ctx context.Context, ref, cacheDir, destDir string) (imagepull.Metadata, error)
|
||||
finalizePulledRootfs func(ctx context.Context, ext4File string, meta imagepull.Metadata) error
|
||||
requestHandler func(context.Context, rpc.Request) rpc.Response
|
||||
guestWaitForSSH func(context.Context, string, string, time.Duration) error
|
||||
guestDial func(context.Context, string, string) (guestSSHClient, error)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"banger/internal/daemon/imagemgr"
|
||||
"banger/internal/imagepull"
|
||||
"banger/internal/model"
|
||||
"banger/internal/paths"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
)
|
||||
|
|
@ -107,8 +108,8 @@ func (d *Daemon) PullImage(ctx context.Context, params api.ImagePullParams) (ima
|
|||
if err := imagepull.BuildExt4(ctx, d.runner, rootfsTree, rootfsExt4, sizeBytes); err != nil {
|
||||
return model.Image{}, fmt.Errorf("build rootfs ext4: %w", err)
|
||||
}
|
||||
if err := imagepull.ApplyOwnership(ctx, d.runner, rootfsExt4, meta); err != nil {
|
||||
return model.Image{}, fmt.Errorf("apply ownership: %w", err)
|
||||
if err := d.runFinalizePulledRootfs(ctx, rootfsExt4, meta); err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
|
||||
stagedKernel, stagedInitrd, stagedModules, err := imagemgr.StageBootArtifacts(ctx, d.runner, stagingDir, kernelPath, initrdPath, modulesDir)
|
||||
|
|
@ -153,6 +154,29 @@ func (d *Daemon) runPullAndFlatten(ctx context.Context, ref, cacheDir, destDir s
|
|||
return imagepull.Flatten(ctx, pulled, destDir)
|
||||
}
|
||||
|
||||
// runFinalizePulledRootfs applies ownership fixup and injects banger's
|
||||
// guest agents. Tests substitute via d.finalizePulledRootfs; nil →
|
||||
// real implementation using debugfs + the companion vsock-agent
|
||||
// binary resolved via paths.CompanionBinaryPath.
|
||||
func (d *Daemon) runFinalizePulledRootfs(ctx context.Context, ext4File string, meta imagepull.Metadata) error {
|
||||
if d.finalizePulledRootfs != nil {
|
||||
return d.finalizePulledRootfs(ctx, ext4File, meta)
|
||||
}
|
||||
if err := imagepull.ApplyOwnership(ctx, d.runner, ext4File, meta); err != nil {
|
||||
return fmt.Errorf("apply ownership: %w", err)
|
||||
}
|
||||
vsockBin, err := paths.CompanionBinaryPath("banger-vsock-agent")
|
||||
if err != nil {
|
||||
return fmt.Errorf("locate vsock agent binary: %w", err)
|
||||
}
|
||||
if err := imagepull.InjectGuestAgents(ctx, d.runner, ext4File, imagepull.GuestAgentAssets{
|
||||
VsockAgentBin: vsockBin,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("inject guest agents: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// nameSanitize keeps lowercase alphanumerics + hyphens, collapses runs.
|
||||
var nameSanitizeRE = regexp.MustCompile(`[^a-z0-9]+`)
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,12 @@ func writeFakeKernelTriple(t *testing.T) (kernelPath, initrdPath, modulesDir str
|
|||
return
|
||||
}
|
||||
|
||||
// stubFinalizePulledRootfs is a no-op seam substitute that skips the real
|
||||
// debugfs + vsock-agent-binary injection machinery during daemon tests.
|
||||
func stubFinalizePulledRootfs(_ context.Context, _ string, _ imagepull.Metadata) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// stubPullAndFlatten writes a fixed file tree into destDir, simulating a
|
||||
// successful OCI pull without the network or tarball machinery.
|
||||
func stubPullAndFlatten(_ context.Context, _ string, _ string, destDir string) (imagepull.Metadata, error) {
|
||||
|
|
@ -65,10 +71,11 @@ func TestPullImageHappyPath(t *testing.T) {
|
|||
kernel, initrd, modules := writeFakeKernelTriple(t)
|
||||
|
||||
d := &Daemon{
|
||||
layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: cacheDir},
|
||||
store: openDaemonStore(t),
|
||||
runner: system.NewRunner(),
|
||||
pullAndFlatten: stubPullAndFlatten,
|
||||
layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: cacheDir},
|
||||
store: openDaemonStore(t),
|
||||
runner: system.NewRunner(),
|
||||
pullAndFlatten: stubPullAndFlatten,
|
||||
finalizePulledRootfs: stubFinalizePulledRootfs,
|
||||
}
|
||||
|
||||
image, err := d.PullImage(context.Background(), api.ImagePullParams{
|
||||
|
|
@ -109,10 +116,11 @@ func TestPullImageRejectsExistingName(t *testing.T) {
|
|||
kernel, _, _ := writeFakeKernelTriple(t)
|
||||
|
||||
d := &Daemon{
|
||||
layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: t.TempDir()},
|
||||
store: openDaemonStore(t),
|
||||
runner: system.NewRunner(),
|
||||
pullAndFlatten: stubPullAndFlatten,
|
||||
layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: t.TempDir()},
|
||||
store: openDaemonStore(t),
|
||||
runner: system.NewRunner(),
|
||||
pullAndFlatten: stubPullAndFlatten,
|
||||
finalizePulledRootfs: stubFinalizePulledRootfs,
|
||||
}
|
||||
// Seed a preexisting image with the would-be derived name.
|
||||
id, _ := model.NewID()
|
||||
|
|
@ -136,10 +144,11 @@ func TestPullImageRejectsExistingName(t *testing.T) {
|
|||
|
||||
func TestPullImageRequiresKernel(t *testing.T) {
|
||||
d := &Daemon{
|
||||
layout: paths.Layout{ImagesDir: t.TempDir(), OCICacheDir: t.TempDir()},
|
||||
store: openDaemonStore(t),
|
||||
runner: system.NewRunner(),
|
||||
pullAndFlatten: stubPullAndFlatten,
|
||||
layout: paths.Layout{ImagesDir: t.TempDir(), OCICacheDir: t.TempDir()},
|
||||
store: openDaemonStore(t),
|
||||
runner: system.NewRunner(),
|
||||
pullAndFlatten: stubPullAndFlatten,
|
||||
finalizePulledRootfs: stubFinalizePulledRootfs,
|
||||
}
|
||||
_, err := d.PullImage(context.Background(), api.ImagePullParams{
|
||||
Ref: "docker.io/library/debian:bookworm",
|
||||
|
|
@ -157,10 +166,11 @@ func TestPullImageCleansStagingOnFailure(t *testing.T) {
|
|||
}
|
||||
|
||||
d := &Daemon{
|
||||
layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: t.TempDir()},
|
||||
store: openDaemonStore(t),
|
||||
runner: system.NewRunner(),
|
||||
pullAndFlatten: failureSeam,
|
||||
layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: t.TempDir()},
|
||||
store: openDaemonStore(t),
|
||||
runner: system.NewRunner(),
|
||||
pullAndFlatten: failureSeam,
|
||||
finalizePulledRootfs: stubFinalizePulledRootfs,
|
||||
}
|
||||
_, err := d.PullImage(context.Background(), api.ImagePullParams{
|
||||
Ref: "docker.io/library/debian:bookworm",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue