1
0
forked from wrenn/wrenn
Files
wrenn-releases/internal/sandbox/images.go
pptx704 34af77e0d8 Fix snapshot race, delete auth, sparse dd, default disk to 5GB
Snapshot race fix:
- Pre-mark sandbox as "paused" in DB before issuing CreateSnapshot and
  PauseSandbox RPCs, preventing the reconciler from marking it "stopped"
  during the flatten window when the sandbox is gone from the host
  agent's in-memory map but DB still says "running"
- Revert status to "running" on RPC failure
- Check ctx.Err() before writing response to avoid writing to dead
  connections when client disconnects during long snapshot operations

Delete auth fix:
- Block non-admin deletion of platform templates (team_id = all-zeros)
  at DELETE /v1/snapshots/{name} with 403, preventing file deletion
  before the team ownership check fails

Sparse dd:
- Add conv=sparse to dd in FlattenSnapshot so flattened images preserve
  sparseness (~200MB actual vs 5GB logical)

Default disk size:
- Change default disk_size_mb from 20GB to 5GB across migration,
  manager, service, build, and EnsureImageSizes
- Disable split-button dropdown arrow for platform templates in
  dashboard snapshots page (teams cannot delete platform templates)
2026-03-28 14:30:18 +06:00

75 lines
2.2 KiB
Go

package sandbox
import (
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"
)
// DefaultDiskSizeMB is the standard disk size for base images. Images smaller
// than this are expanded at startup so that dm-snapshot sandboxes see the full
// size without per-sandbox copies. The expansion is sparse — only metadata
// changes; no physical disk is consumed beyond the original content.
const DefaultDiskSizeMB = 5120 // 5 GB
// EnsureImageSizes walks the images directory and expands any rootfs.ext4 that
// is smaller than the target size. This is idempotent: images already at or
// above the target size are left untouched. Should be called once at host agent
// startup before any sandboxes are created.
func EnsureImageSizes(imagesDir string, targetMB int) error {
if targetMB <= 0 {
targetMB = DefaultDiskSizeMB
}
targetBytes := int64(targetMB) * 1024 * 1024
entries, err := os.ReadDir(imagesDir)
if err != nil {
return fmt.Errorf("read images dir: %w", err)
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
rootfs := filepath.Join(imagesDir, entry.Name(), "rootfs.ext4")
info, err := os.Stat(rootfs)
if err != nil {
continue // not every template dir has a rootfs.ext4
}
if info.Size() >= targetBytes {
continue // already large enough
}
slog.Info("expanding base image",
"template", entry.Name(),
"from_mb", info.Size()/(1024*1024),
"to_mb", targetMB,
)
// Expand the file (sparse — instant, no physical disk used).
if err := os.Truncate(rootfs, targetBytes); err != nil {
return fmt.Errorf("truncate %s: %w", rootfs, err)
}
// Check filesystem before resize.
if out, err := exec.Command("e2fsck", "-fy", rootfs).CombinedOutput(); err != nil {
// e2fsck returns 1 if it fixed errors, which is fine.
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() > 1 {
return fmt.Errorf("e2fsck %s: %s: %w", rootfs, string(out), err)
}
}
// Grow the ext4 filesystem to fill the new file size.
if out, err := exec.Command("resize2fs", rootfs).CombinedOutput(); err != nil {
return fmt.Errorf("resize2fs %s: %s: %w", rootfs, string(out), err)
}
slog.Info("base image expanded", "template", entry.Name(), "size_mb", targetMB)
}
return nil
}