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

@ -81,46 +81,64 @@ func TestResizeExt4ImageStopsAtFirstFailure(t *testing.T) {
tests := []struct {
name string
steps []systemStep
setup func(t *testing.T) string
steps func(path string) []systemStep
wantErr string
wantCalls int
}{
{
name: "truncate failure",
steps: []systemStep{
{call: systemCall{name: "truncate", args: []string{"-s", "4096", "/tmp/root.ext4"}}, err: errors.New("truncate failed")},
setup: func(t *testing.T) string {
return t.TempDir()
},
wantErr: "truncate failed",
wantCalls: 1,
wantErr: "",
wantCalls: 0,
},
{
name: "e2fsck failure",
steps: []systemStep{
{call: systemCall{name: "truncate", args: []string{"-s", "4096", "/tmp/root.ext4"}}},
{call: systemCall{name: "e2fsck", args: []string{"-p", "-f", "/tmp/root.ext4"}}, err: errors.New("e2fsck failed")},
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: 2,
wantCalls: 1,
},
{
name: "resize2fs failure",
steps: []systemStep{
{call: systemCall{name: "truncate", args: []string{"-s", "4096", "/tmp/root.ext4"}}},
{call: systemCall{name: "e2fsck", args: []string{"-p", "-f", "/tmp/root.ext4"}}},
{call: systemCall{name: "resize2fs", args: []string{"/tmp/root.ext4"}}, err: errors.New("resize2fs failed")},
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: 3,
wantCalls: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
runner := &scriptedRunner{t: t, steps: tt.steps}
err := ResizeExt4Image(context.Background(), runner, "/tmp/root.ext4", 4096)
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
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 {
@ -131,6 +149,24 @@ func TestResizeExt4ImageStopsAtFirstFailure(t *testing.T) {
}
}
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()
@ -218,6 +254,34 @@ func TestMountTempDirUsesLoopForRegularFilesAndCleanupUsesBackgroundContext(t *t
}
}
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()