Move avoidable daemon shell-outs into Go

Reduce the control plane's dependency on helper scripts while keeping the hard Linux integration points in the approved shell-out layer.

Replace the bash-driven image build path with a native Go builder that clones and optionally resizes the rootfs, boots a temporary Firecracker VM, provisions the guest over SSH, installs packages and modules, and preserves the package-manifest sidecar.

Also replace a few small convenience shell-outs with Go helpers: read process stats from /proc, use os.Truncate for ext4 image growth, add file-clone and normalized-line helpers, drop the sh -c work-disk flattening path, and launch Firecracker via a direct sudo command.

Add tests for the new SSH/archive and system helpers, plus a policy test that keeps os/exec imports confined to cli/firecracker/system. Update the docs to describe customize.sh as a manual helper rather than the daemon's image-build backend.

Validated with go mod tidy, go test ./..., and make build.
This commit is contained in:
Thales Maciel 2026-03-17 17:13:07 -03:00
parent 0a0b0b617b
commit 942d242c03
No known key found for this signature in database
GPG key ID: 33112E6833C34679
17 changed files with 936 additions and 145 deletions

View file

@ -4,12 +4,12 @@ import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"banger/internal/api"
"banger/internal/model"
"banger/internal/paths"
"banger/internal/system"
)
func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (image model.Image, err error) {
@ -60,56 +60,40 @@ func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (i
}
defer logFile.Close()
rootfsPath := filepath.Join(artifactDir, "rootfs.ext4")
script := d.config.CustomizeScript
if script == "" {
return model.Image{}, fmt.Errorf("customize script not configured; %s", paths.RuntimeBundleHint())
}
if _, err := os.Stat(script); err != nil {
return model.Image{}, fmt.Errorf("customize.sh not found at %s; %s", script, paths.RuntimeBundleHint())
}
args := []string{script, baseRootfs, "--out", rootfsPath}
if params.Size != "" {
args = append(args, "--size", params.Size)
}
kernelPath := params.KernelPath
if kernelPath == "" {
kernelPath = d.config.DefaultKernel
}
if kernelPath != "" {
args = append(args, "--kernel", kernelPath)
}
initrdPath := params.InitrdPath
if initrdPath == "" {
initrdPath = d.config.DefaultInitrd
}
if initrdPath != "" {
args = append(args, "--initrd", initrdPath)
}
modulesDir := params.ModulesDir
if modulesDir == "" {
modulesDir = d.config.DefaultModulesDir
}
if modulesDir != "" {
args = append(args, "--modules", modulesDir)
}
if params.Docker {
args = append(args, "--docker")
}
if err := d.validateImageBuildPrereqs(ctx, baseRootfs, kernelPath, initrdPath, modulesDir); err != nil {
if err := d.validateImageBuildPrereqs(ctx, baseRootfs, kernelPath, initrdPath, modulesDir, params.Size); err != nil {
return model.Image{}, err
}
op.stage("launch_helper", "script", script, "build_log_path", buildLogPath, "artifact_dir", artifactDir)
cmd := exec.CommandContext(ctx, "bash", args...)
cmd.Stdout = logFile
cmd.Stderr = logFile
cmd.Stdin = nil
cmd.Dir = d.layout.StateDir
cmd.Env = append(
os.Environ(),
"BANGER_RUNTIME_DIR="+d.config.RuntimeDir,
"BANGER_STATE_DIR="+filepath.Join(d.layout.StateDir, "image-build"),
)
if err := cmd.Run(); err != nil {
spec := imageBuildSpec{
ID: id,
Name: name,
BaseRootfs: baseRootfs,
RootfsPath: rootfsPath,
BuildLog: logFile,
KernelPath: kernelPath,
InitrdPath: initrdPath,
ModulesDir: modulesDir,
PackagesPath: d.config.DefaultPackagesFile,
InstallDocker: params.Docker,
Size: params.Size,
}
op.stage("launch_builder", "build_log_path", buildLogPath, "artifact_dir", artifactDir)
if err := d.runImageBuild(ctx, spec); err != nil {
_ = os.RemoveAll(artifactDir)
return model.Image{}, err
}
if err := writePackagesMetadata(rootfsPath, d.config.DefaultPackagesFile); err != nil {
_ = os.RemoveAll(artifactDir)
return model.Image{}, err
}
@ -138,6 +122,18 @@ func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (i
return image, nil
}
func writePackagesMetadata(rootfsPath, packagesPath string) error {
if rootfsPath == "" || packagesPath == "" {
return nil
}
lines, err := system.ReadNormalizedLines(packagesPath)
if err != nil {
return err
}
metadataPath := rootfsPath + ".packages.sha256"
return os.WriteFile(metadataPath, []byte(packagesHash(lines)+"\n"), 0o644)
}
func (d *Daemon) DeleteImage(ctx context.Context, idOrName string) (model.Image, error) {
d.mu.Lock()
defer d.mu.Unlock()