daemon: rewrite git identity sync + file_sync on ext4 toolkit

ensureGitIdentityOnWorkDisk, writeGitIdentity, runFileSync, and
copyHostDir all dropped their mount + sudo install/mkdir/chmod/chown
scaffolding. Every write now goes through MkdirExt4,
WriteExt4FileOwned, ReadExt4File, and the new MkdirAllExt4 helper —
all sudoless against user-owned ext4 images.

Net effect with the prior two commits: ensureWorkDisk, authsync, image
seeding, git identity sync, and file_sync no longer mount the work
disk or spawn sudo mkdir/chmod/chown/cat/install. Only the
image-build path (which legitimately produces root-owned artifacts)
still touches MountTempDir.

The filesystemRunner test harness grew a small debugfs/e2cp/e2rm
emulator so the WorkspaceService tests keep exercising their real
code paths without a live ext4 image. The mock is deliberately
dumb — it only implements the subset runFileSync and writeGitIdentity
drive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-23 18:29:30 -03:00
parent f0685366ec
commit 6ab1a2b844
No known key found for this signature in database
GPG key ID: 33112E6833C34679
3 changed files with 253 additions and 74 deletions

View file

@ -5,7 +5,9 @@ import (
"errors"
"fmt"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"banger/internal/guest"
@ -115,17 +117,7 @@ func (s *WorkspaceService) ensureGitIdentityOnWorkDisk(ctx context.Context, vm *
}
vmCreateStage(ctx, "prepare_work_disk", "syncing git identity")
workMount, cleanupWork, err := system.MountTempDir(ctx, runner, vm.Runtime.WorkDiskPath, false)
if err != nil {
return err
}
defer cleanupWork()
if err := flattenNestedWorkHome(ctx, s.runner, workMount); err != nil {
return err
}
return writeGitIdentity(ctx, runner, filepath.Join(workMount, workDiskGitConfigRelativePath), identity)
return writeGitIdentity(ctx, runner, vm.Runtime.WorkDiskPath, "/"+workDiskGitConfigRelativePath, identity)
}
// runFileSync applies every [[file_sync]] entry from the daemon config
@ -133,10 +125,10 @@ func (s *WorkspaceService) ensureGitIdentityOnWorkDisk(ctx context.Context, vm *
// Other errors abort the VM create (since the user explicitly asked
// for the sync).
//
// File entries: `install -o 0 -g 0 -m <mode>` (mode defaults to 0600).
// Directory entries: walked in Go — each file is installed with its
// source permissions, each subdir is mkdir'd. The entry's `mode`
// field is only honoured for file entries.
// Operates directly on the ext4 image via the sudoless toolkit — no
// mount, no privileged install(1). Every write lands as root:root;
// file modes come from the [[file_sync]] entry (default 0600),
// directory modes from the source on the host.
func (s *WorkspaceService) runFileSync(ctx context.Context, vm *model.VMRecord) error {
if len(s.config.FileSync) == 0 {
return nil
@ -152,33 +144,12 @@ func (s *WorkspaceService) runFileSync(ctx context.Context, vm *model.VMRecord)
return fmt.Errorf("resolve host user home: %w", err)
}
// Mount the work disk once and reuse for every entry.
var workMount string
var cleanupWork func() error
ensureMount := func() (string, error) {
if workMount != "" {
return workMount, nil
}
m, c, err := system.MountTempDir(ctx, runner, vm.Runtime.WorkDiskPath, false)
if err != nil {
return "", err
}
workMount = m
cleanupWork = c
if err := flattenNestedWorkHome(ctx, s.runner, workMount); err != nil {
return "", err
}
return workMount, nil
}
defer func() {
if cleanupWork != nil {
cleanupWork()
}
}()
workDisk := vm.Runtime.WorkDiskPath
for _, entry := range s.config.FileSync {
hostPath := expandHostPath(entry.Host, hostHome)
guestRel := guestPathRelativeToRoot(entry.Guest)
guestImagePath := "/" + guestRel
info, err := os.Stat(hostPath)
if err != nil {
@ -189,48 +160,49 @@ func (s *WorkspaceService) runFileSync(ctx context.Context, vm *model.VMRecord)
return fmt.Errorf("file_sync: stat %s: %w", hostPath, err)
}
mount, err := ensureMount()
if err != nil {
return err
}
vmCreateStage(ctx, "prepare_work_disk", "file sync: "+entry.Host+" → "+entry.Guest)
target := filepath.Join(mount, guestRel)
if _, err := runner.RunSudo(ctx, "mkdir", "-p", filepath.Dir(target)); err != nil {
return fmt.Errorf("file_sync: mkdir %s: %w", filepath.Dir(target), err)
parent := path.Dir(guestImagePath)
if parent != "/" && parent != "." {
if err := system.MkdirAllExt4(ctx, runner, workDisk, parent, 0o755, 0, 0); err != nil {
return fmt.Errorf("file_sync: mkdir %s: %w", parent, err)
}
}
if info.IsDir() {
if err := s.copyHostDir(ctx, *vm, runner, hostPath, target); err != nil {
return fmt.Errorf("file_sync: copy directory %s → %s: %w", hostPath, target, err)
if err := s.copyHostDir(ctx, *vm, runner, workDisk, hostPath, guestImagePath); err != nil {
return fmt.Errorf("file_sync: copy directory %s → %s: %w", hostPath, guestImagePath, err)
}
continue
}
mode := entry.Mode
if mode == "" {
mode = "0600"
mode, err := parseFileSyncMode(entry.Mode)
if err != nil {
return fmt.Errorf("file_sync: %s: %w", entry.Host, err)
}
if _, err := runner.RunSudo(ctx, "install", "-o", "0", "-g", "0", "-m", mode, hostPath, target); err != nil {
return fmt.Errorf("file_sync: install %s → %s: %w", hostPath, target, err)
data, err := os.ReadFile(hostPath)
if err != nil {
return fmt.Errorf("file_sync: read %s: %w", hostPath, err)
}
if err := system.WriteExt4FileOwned(ctx, runner, workDisk, guestImagePath, mode, 0, 0, data); err != nil {
return fmt.Errorf("file_sync: write %s → %s: %w", hostPath, guestImagePath, err)
}
}
return nil
}
// copyHostDir recursively copies hostDir into guestTarget using only
// `mkdir` (for subdirs) and `install` (for files). Each file's source
// permissions are preserved; ownership is forced to root:root via
// `install -o 0 -g 0`. Symlinks encountered during recursion are
// SKIPPED with a warning — `os.Lstat` tells us the entry itself is a
// link without resolving it, so a symlink inside ~/.aws that points
// at ~/secrets can't leak out of the tree the user named. Other
// special types (devices, FIFOs) are skipped silently. Top-level
// host paths go through `os.Stat` back in runFileSync and still
// follow, since the user explicitly named that path.
func (s *WorkspaceService) copyHostDir(ctx context.Context, vm model.VMRecord, runner system.CommandRunner, hostDir, guestTarget string) error {
if _, err := runner.RunSudo(ctx, "mkdir", "-p", guestTarget); err != nil {
// copyHostDir recursively copies hostDir into guestTarget on the
// ext4 image via the sudoless toolkit. Each file's source permissions
// are preserved; directories get 0755; ownership is forced to
// root:root. Symlinks are SKIPPED with a warning — os.Lstat identifies
// the entry itself as a link without resolving it, so a symlink
// inside ~/.aws that points at ~/secrets can't leak out of the tree
// the user named. Other special types (devices, FIFOs) are skipped
// silently. Top-level host paths go through os.Stat back in
// runFileSync and still follow, since the user explicitly named that
// path.
func (s *WorkspaceService) copyHostDir(ctx context.Context, vm model.VMRecord, runner system.CommandRunner, imagePath, hostDir, guestTarget string) error {
if err := system.MkdirExt4(ctx, runner, imagePath, guestTarget, 0o755, 0, 0); err != nil {
return err
}
entries, err := os.ReadDir(hostDir)
@ -239,7 +211,7 @@ func (s *WorkspaceService) copyHostDir(ctx context.Context, vm model.VMRecord, r
}
for _, entry := range entries {
hostChild := filepath.Join(hostDir, entry.Name())
guestChild := filepath.Join(guestTarget, entry.Name())
guestChild := path.Join(guestTarget, entry.Name())
info, err := os.Lstat(hostChild)
if err != nil {
@ -249,12 +221,15 @@ func (s *WorkspaceService) copyHostDir(ctx context.Context, vm model.VMRecord, r
case info.Mode()&os.ModeSymlink != 0:
s.warnFileSyncSymlinkSkipped(vm, hostChild)
case info.IsDir():
if err := s.copyHostDir(ctx, vm, runner, hostChild, guestChild); err != nil {
if err := s.copyHostDir(ctx, vm, runner, imagePath, hostChild, guestChild); err != nil {
return err
}
case info.Mode().IsRegular():
mode := fmt.Sprintf("%04o", info.Mode().Perm())
if _, err := runner.RunSudo(ctx, "install", "-o", "0", "-g", "0", "-m", mode, hostChild, guestChild); err != nil {
data, err := os.ReadFile(hostChild)
if err != nil {
return err
}
if err := system.WriteExt4FileOwned(ctx, runner, imagePath, guestChild, info.Mode().Perm(), 0, 0, data); err != nil {
return err
}
}
@ -262,6 +237,21 @@ func (s *WorkspaceService) copyHostDir(ctx context.Context, vm model.VMRecord, r
return nil
}
// parseFileSyncMode parses the [[file_sync]] mode field (octal string,
// default "0600"). Returns the parsed FileMode with only the permission
// bits set; callers OR in S_IFREG via WriteExt4FileOwned.
func parseFileSyncMode(raw string) (os.FileMode, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
raw = "0600"
}
v, err := strconv.ParseUint(raw, 8, 32)
if err != nil {
return 0, fmt.Errorf("parse mode %q: %w", raw, err)
}
return os.FileMode(v) & os.ModePerm, nil
}
// expandHostPath expands a leading "~/" against the host user's
// home. Already-absolute paths pass through unchanged.
func expandHostPath(raw, home string) string {
@ -321,10 +311,23 @@ func gitConfigValue(ctx context.Context, runner system.CommandRunner, extraArgs
return strings.TrimSpace(string(out)), nil
}
func writeGitIdentity(ctx context.Context, runner system.CommandRunner, gitConfigPath string, identity gitIdentity) error {
existing, err := runner.RunSudo(ctx, "cat", gitConfigPath)
// writeGitIdentity merges user.name + user.email into the on-image
// gitconfig at guestPath. Reads the existing bytes via the ext4
// toolkit (no-op to empty if absent), edits via `git config --file`
// on a host tempfile so any pre-existing unrelated sections are
// preserved verbatim, then writes back through WriteExt4FileOwned
// at 0644 root:root.
func writeGitIdentity(ctx context.Context, runner system.CommandRunner, imagePath, guestPath string, identity gitIdentity) error {
var existing []byte
exists, err := system.Ext4PathExists(ctx, runner, imagePath, guestPath)
if err != nil {
existing = nil
return err
}
if exists {
existing, err = system.ReadExt4File(ctx, runner, imagePath, guestPath)
if err != nil {
return err
}
}
tmpFile, err := os.CreateTemp("", "banger-gitconfig-*")
@ -349,8 +352,11 @@ func writeGitIdentity(ctx context.Context, runner system.CommandRunner, gitConfi
if _, err := runner.Run(ctx, "git", "config", "--file", tmpPath, "user.email", identity.Email); err != nil {
return err
}
_, err = runner.RunSudo(ctx, "install", "-m", "644", tmpPath, gitConfigPath)
return err
merged, err := os.ReadFile(tmpPath)
if err != nil {
return err
}
return system.WriteExt4FileOwned(ctx, runner, imagePath, guestPath, 0o644, 0, 0, merged)
}
func (s *WorkspaceService) warnFileSyncSkipped(vm model.VMRecord, hostPath string, err error) {