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

74
internal/system/files.go Normal file
View file

@ -0,0 +1,74 @@
package system
import (
"fmt"
"io"
"os"
"strings"
"golang.org/x/sys/unix"
)
func CopyFilePreferClone(sourcePath, targetPath string) error {
source, err := os.Open(sourcePath)
if err != nil {
return err
}
defer source.Close()
info, err := source.Stat()
if err != nil {
return err
}
target, err := os.OpenFile(targetPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, info.Mode().Perm())
if err != nil {
return err
}
defer target.Close()
if err := unix.IoctlFileClone(int(target.Fd()), int(source.Fd())); err == nil {
return nil
}
if _, err := source.Seek(0, io.SeekStart); err != nil {
return err
}
if _, err := target.Seek(0, io.SeekStart); err != nil {
return err
}
if _, err := io.Copy(target, source); err != nil {
return err
}
if err := target.Sync(); err != nil {
return err
}
if err := target.Chmod(info.Mode().Perm()); err != nil {
return err
}
return nil
}
func ReadNormalizedLines(path string) ([]string, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var out []string
for _, line := range strings.Split(string(data), "\n") {
if strings.HasSuffix(line, "\r") {
line = strings.TrimSuffix(line, "\r")
}
if idx := strings.Index(line, "#"); idx >= 0 {
line = line[:idx]
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
out = append(out, line)
}
if len(out) == 0 {
return nil, fmt.Errorf("file has no entries: %s", path)
}
return out, nil
}

View file

@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"io"
"math"
"os"
"os/exec"
"path/filepath"
@ -105,25 +106,48 @@ type ProcessStats struct {
}
func ReadProcessStats(ctx context.Context, pid int) (ProcessStats, error) {
_ = ctx
if pid <= 0 {
return ProcessStats{}, errors.New("pid is required")
}
runner := NewRunner()
out, err := runner.Run(ctx, "ps", "-p", strconv.Itoa(pid), "-o", "%cpu=,rss=,vsz=")
statData, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(pid), "stat"))
if err != nil {
return ProcessStats{}, err
}
fields := strings.Fields(string(out))
if len(fields) < 3 {
return ProcessStats{}, fmt.Errorf("unexpected ps output: %q", string(out))
statmData, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(pid), "statm"))
if err != nil {
return ProcessStats{}, err
}
cpu, _ := strconv.ParseFloat(fields[0], 64)
rssKB, _ := strconv.ParseInt(fields[1], 10, 64)
vszKB, _ := strconv.ParseInt(fields[2], 10, 64)
uptimeData, err := os.ReadFile("/proc/uptime")
if err != nil {
return ProcessStats{}, err
}
procStat, err := parseProcStat(string(statData))
if err != nil {
return ProcessStats{}, err
}
memStat, err := parseProcStatm(string(statmData))
if err != nil {
return ProcessStats{}, err
}
uptimeSeconds, err := parseProcUptime(string(uptimeData))
if err != nil {
return ProcessStats{}, err
}
const ticksPerSecond = 100.0
elapsedSeconds := uptimeSeconds - (float64(procStat.startTicks) / ticksPerSecond)
cpuPercent := 0.0
if elapsedSeconds > 0 {
totalCPUSeconds := float64(procStat.userTicks+procStat.systemTicks) / ticksPerSecond
cpuPercent = math.Max(0, (totalCPUSeconds/elapsedSeconds)*100)
}
pageSize := int64(os.Getpagesize())
return ProcessStats{
CPUPercent: cpu,
RSSBytes: rssKB * 1024,
VSZBytes: vszKB * 1024,
CPUPercent: cpuPercent,
RSSBytes: memStat.residentPages * pageSize,
VSZBytes: memStat.sizePages * pageSize,
}, nil
}
@ -179,7 +203,7 @@ func CopyDirContents(ctx context.Context, runner CommandRunner, sourceDir, targe
}
func ResizeExt4Image(ctx context.Context, runner CommandRunner, path string, bytes int64) error {
if _, err := runner.Run(ctx, "truncate", "-s", strconv.FormatInt(bytes, 10), path); err != nil {
if err := os.Truncate(path, bytes); err != nil {
return err
}
if _, err := runner.Run(ctx, "e2fsck", "-p", "-f", path); err != nil {
@ -324,3 +348,67 @@ func CopyStream(dst io.Writer, cmd *exec.Cmd) error {
cmd.Stdin = os.Stdin
return cmd.Run()
}
type procStat struct {
userTicks uint64
systemTicks uint64
startTicks uint64
}
type procStatm struct {
sizePages int64
residentPages int64
}
func parseProcStat(raw string) (procStat, error) {
raw = strings.TrimSpace(raw)
end := strings.LastIndex(raw, ")")
if end == -1 || end+2 >= len(raw) {
return procStat{}, fmt.Errorf("unexpected /proc stat format: %q", raw)
}
fields := strings.Fields(raw[end+2:])
if len(fields) < 20 {
return procStat{}, fmt.Errorf("unexpected /proc stat field count: %q", raw)
}
userTicks, err := strconv.ParseUint(fields[11], 10, 64)
if err != nil {
return procStat{}, err
}
systemTicks, err := strconv.ParseUint(fields[12], 10, 64)
if err != nil {
return procStat{}, err
}
startTicks, err := strconv.ParseUint(fields[19], 10, 64)
if err != nil {
return procStat{}, err
}
return procStat{
userTicks: userTicks,
systemTicks: systemTicks,
startTicks: startTicks,
}, nil
}
func parseProcStatm(raw string) (procStatm, error) {
fields := strings.Fields(strings.TrimSpace(raw))
if len(fields) < 2 {
return procStatm{}, fmt.Errorf("unexpected /proc statm format: %q", raw)
}
sizePages, err := strconv.ParseInt(fields[0], 10, 64)
if err != nil {
return procStatm{}, err
}
residentPages, err := strconv.ParseInt(fields[1], 10, 64)
if err != nil {
return procStatm{}, err
}
return procStatm{sizePages: sizePages, residentPages: residentPages}, nil
}
func parseProcUptime(raw string) (float64, error) {
fields := strings.Fields(strings.TrimSpace(raw))
if len(fields) == 0 {
return 0, fmt.Errorf("unexpected /proc uptime format: %q", raw)
}
return strconv.ParseFloat(fields[0], 64)
}

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()