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:
Thales Maciel 2026-04-16 18:08:56 -03:00
parent 43982a4ae3
commit 491c8e1ebb
No known key found for this signature in database
GPG key ID: 33112E6833C34679
5 changed files with 393 additions and 18 deletions

View file

@ -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]+`)