banger/internal/system/system_test.go
Thales Maciel 942d242c03
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.
2026-03-17 17:13:07 -03:00

376 lines
10 KiB
Go

package system
import (
"context"
"errors"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
type systemCall struct {
sudo bool
name string
args []string
}
type systemStep struct {
call systemCall
out []byte
err error
}
type scriptedRunner struct {
t *testing.T
steps []systemStep
calls []systemCall
}
func (r *scriptedRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
return r.next(systemCall{name: name, args: append([]string(nil), args...)})
}
func (r *scriptedRunner) RunSudo(ctx context.Context, args ...string) ([]byte, error) {
return r.next(systemCall{sudo: true, args: append([]string(nil), args...)})
}
func (r *scriptedRunner) next(call systemCall) ([]byte, error) {
r.t.Helper()
r.calls = append(r.calls, call)
if len(r.steps) == 0 {
r.t.Fatalf("unexpected call: %+v", call)
}
step := r.steps[0]
r.steps = r.steps[1:]
if step.call.sudo != call.sudo || step.call.name != call.name || !reflect.DeepEqual(step.call.args, call.args) {
r.t.Fatalf("call mismatch:\n got: %+v\n want: %+v", call, step.call)
}
return step.out, step.err
}
func (r *scriptedRunner) assertExhausted() {
r.t.Helper()
if len(r.steps) != 0 {
r.t.Fatalf("unconsumed steps: %+v", r.steps)
}
}
type funcRunner struct {
run func(ctx context.Context, name string, args ...string) ([]byte, error)
runSudo func(ctx context.Context, args ...string) ([]byte, error)
}
func (r funcRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
if r.run == nil {
return nil, errors.New("unexpected Run call")
}
return r.run(ctx, name, args...)
}
func (r funcRunner) RunSudo(ctx context.Context, args ...string) ([]byte, error) {
if r.runSudo == nil {
return nil, errors.New("unexpected RunSudo call")
}
return r.runSudo(ctx, args...)
}
func TestResizeExt4ImageStopsAtFirstFailure(t *testing.T) {
t.Parallel()
tests := []struct {
name string
setup func(t *testing.T) string
steps func(path string) []systemStep
wantErr string
wantCalls int
}{
{
name: "truncate failure",
setup: func(t *testing.T) string {
return t.TempDir()
},
wantErr: "",
wantCalls: 0,
},
{
name: "e2fsck failure",
steps: func(path string) []systemStep {
return []systemStep{
{call: systemCall{name: "e2fsck", args: []string{"-p", "-f", path}}, err: errors.New("e2fsck failed")},
}
},
wantErr: "e2fsck failed",
wantCalls: 1,
},
{
name: "resize2fs failure",
steps: func(path string) []systemStep {
return []systemStep{
{call: systemCall{name: "e2fsck", args: []string{"-p", "-f", path}}},
{call: systemCall{name: "resize2fs", args: []string{path}}, err: errors.New("resize2fs failed")},
}
},
wantErr: "resize2fs failed",
wantCalls: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
path := "/tmp/root.ext4"
if tt.setup != nil {
path = tt.setup(t)
} else {
path = filepath.Join(t.TempDir(), "root.ext4")
if err := os.WriteFile(path, []byte("seed"), 0o644); err != nil {
t.Fatalf("WriteFile(%s): %v", path, err)
}
}
var steps []systemStep
if tt.steps != nil {
steps = tt.steps(path)
}
runner := &scriptedRunner{t: t, steps: steps}
err := ResizeExt4Image(context.Background(), runner, path, 4096)
if err == nil {
t.Fatal("ResizeExt4Image() succeeded, want error")
}
if tt.wantErr != "" && !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("ResizeExt4Image() error = %v, want %q", err, tt.wantErr)
}
if len(runner.calls) != tt.wantCalls {
t.Fatalf("call count = %d, want %d", len(runner.calls), tt.wantCalls)
}
runner.assertExhausted()
})
}
}
func TestReadNormalizedLines(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "packages.apt")
if err := os.WriteFile(path, []byte("\n# comment\n git \nless # trailing\n\r\ntmux\r\n"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
got, err := ReadNormalizedLines(path)
if err != nil {
t.Fatalf("ReadNormalizedLines: %v", err)
}
want := []string{"git", "less", "tmux"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("lines = %v, want %v", got, want)
}
}
func TestWriteExt4FileRemovesTempFileAndReturnsCopyError(t *testing.T) {
t.Parallel()
copyErr := errors.New("e2cp failed")
var tempPath string
runner := funcRunner{
runSudo: func(ctx context.Context, args ...string) ([]byte, error) {
switch args[0] {
case "e2rm":
return nil, errors.New("ignore remove")
case "e2cp":
tempPath = args[1]
if _, err := os.Stat(tempPath); err != nil {
t.Fatalf("temp file missing during e2cp: %v", err)
}
return nil, copyErr
default:
t.Fatalf("unexpected sudo call: %v", args)
return nil, nil
}
},
}
err := WriteExt4File(context.Background(), runner, "/tmp/root.ext4", "/etc/hostname", []byte("devbox\n"))
if !errors.Is(err, copyErr) {
t.Fatalf("WriteExt4File() error = %v, want %v", err, copyErr)
}
if tempPath == "" {
t.Fatal("expected e2cp temp path to be recorded")
}
if _, err := os.Stat(tempPath); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("temp file still exists after WriteExt4File: %v", err)
}
}
func TestMountTempDirUsesLoopForRegularFilesAndCleanupUsesBackgroundContext(t *testing.T) {
t.Parallel()
source := filepath.Join(t.TempDir(), "root.ext4")
if err := os.WriteFile(source, []byte("rootfs"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
var mountDir string
calls := 0
runner := funcRunner{
runSudo: func(callCtx context.Context, args ...string) ([]byte, error) {
calls++
switch calls {
case 1:
mountDir = args[len(args)-1]
want := []string{"mount", "-o", "ro,loop", source, mountDir}
if !reflect.DeepEqual(args, want) {
t.Fatalf("mount args = %v, want %v", args, want)
}
case 2:
if callCtx.Err() != nil {
t.Fatalf("cleanup context should not be canceled: %v", callCtx.Err())
}
want := []string{"umount", mountDir}
if !reflect.DeepEqual(args, want) {
t.Fatalf("cleanup args = %v, want %v", args, want)
}
default:
t.Fatalf("unexpected RunSudo call %d: %v", calls, args)
}
return nil, nil
},
}
gotMountDir, cleanup, err := MountTempDir(ctx, runner, source, true)
if err != nil {
t.Fatalf("MountTempDir: %v", err)
}
if gotMountDir != mountDir {
t.Fatalf("mount dir = %q, want %q", gotMountDir, mountDir)
}
cancel()
if err := cleanup(); err != nil {
t.Fatalf("cleanup: %v", err)
}
if _, err := os.Stat(mountDir); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("mount dir still exists after cleanup: %v", err)
}
}
func TestParseProcHelpers(t *testing.T) {
t.Parallel()
stat, err := parseProcStat("1234 (firecracker) S 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22")
if err != nil {
t.Fatalf("parseProcStat: %v", err)
}
if stat.userTicks != 11 || stat.systemTicks != 12 || stat.startTicks != 19 {
t.Fatalf("proc stat = %+v", stat)
}
statm, err := parseProcStatm("200 50 0 0 0 0 0")
if err != nil {
t.Fatalf("parseProcStatm: %v", err)
}
if statm.sizePages != 200 || statm.residentPages != 50 {
t.Fatalf("proc statm = %+v", statm)
}
uptime, err := parseProcUptime("321.50 42.10")
if err != nil {
t.Fatalf("parseProcUptime: %v", err)
}
if uptime != 321.50 {
t.Fatalf("uptime = %v, want 321.50", uptime)
}
}
func TestMountTempDirRemovesTempDirWhenMountFails(t *testing.T) {
t.Parallel()
source := t.TempDir()
var mountDir string
runner := funcRunner{
runSudo: func(ctx context.Context, args ...string) ([]byte, error) {
mountDir = args[len(args)-1]
return nil, errors.New("mount failed")
},
}
_, _, err := MountTempDir(context.Background(), runner, source, false)
if err == nil || !strings.Contains(err.Error(), "mount failed") {
t.Fatalf("MountTempDir() error = %v, want mount failure", err)
}
if mountDir == "" {
t.Fatal("expected mount path to be recorded")
}
if _, statErr := os.Stat(mountDir); !errors.Is(statErr, os.ErrNotExist) {
t.Fatalf("mount dir still exists after failed mount: %v", statErr)
}
}
func TestParseMetricsFileHandlesWholeAndLineDelimitedJSON(t *testing.T) {
t.Parallel()
full := filepath.Join(t.TempDir(), "metrics-full.json")
if err := os.WriteFile(full, []byte(`{"uptime":1,"signals":{"sigterm":0}}`), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
got := ParseMetricsFile(full)
if got["uptime"] != float64(1) {
t.Fatalf("ParseMetricsFile(full) = %v", got)
}
lines := filepath.Join(t.TempDir(), "metrics-lines.json")
if err := os.WriteFile(lines, []byte("junk\n{\"uptime\":2}\n"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
got = ParseMetricsFile(lines)
if got["uptime"] != float64(2) {
t.Fatalf("ParseMetricsFile(lines) = %v", got)
}
}
func TestUpdateFSTabStripsLegacyMountsAndAddsDefaultsOnce(t *testing.T) {
t.Parallel()
input := strings.Join([]string{
"/dev/vda / ext4 defaults 0 1",
"/dev/vdb /home ext4 defaults 0 2",
"/dev/vdc /var ext4 defaults 0 2",
"/dev/vdb /root ext4 defaults 0 2",
"tmpfs /run tmpfs defaults,nodev,nosuid,mode=0755 0 0",
"",
}, "\n")
got := UpdateFSTab(input)
if strings.Contains(got, "/home") || strings.Contains(got, "/var") {
t.Fatalf("UpdateFSTab() kept legacy mounts: %q", got)
}
if strings.Count(got, "/dev/vdb /root ext4 defaults 0 2") != 1 {
t.Fatalf("UpdateFSTab() duplicated /root mount: %q", got)
}
if strings.Count(got, "tmpfs /run tmpfs defaults,nodev,nosuid,mode=0755 0 0") != 1 {
t.Fatalf("UpdateFSTab() duplicated /run mount: %q", got)
}
if !strings.Contains(got, "tmpfs /tmp tmpfs defaults,nodev,nosuid,mode=1777 0 0") {
t.Fatalf("UpdateFSTab() missing /tmp mount: %q", got)
}
}
func TestUseLoopMount(t *testing.T) {
t.Parallel()
file := filepath.Join(t.TempDir(), "root.ext4")
if err := os.WriteFile(file, []byte("rootfs"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
dir := t.TempDir()
if !useLoopMount(file) {
t.Fatalf("useLoopMount(%s) = false, want true", file)
}
if useLoopMount(dir) {
t.Fatalf("useLoopMount(%s) = true, want false", dir)
}
if useLoopMount(filepath.Join(dir, "missing")) {
t.Fatalf("useLoopMount(missing) = true, want false")
}
}