forked from wrenn/wrenn
Add disk_size_mb, auto-expand base images, admin templates endpoint
Disk sizing: - Add disk_size_mb column to sandboxes table (default 20480 = 20GB) - Add disk_size_mb to CreateSandboxRequest proto, passed through the full chain: service → RPC → host agent → sandbox manager → devicemapper - devicemapper.CreateSnapshot takes separate cowSizeBytes param so the sparse CoW file can be sized independently from the origin - EnsureImageSizes() runs at host agent startup: expands any base image smaller than 20GB via truncate + resize2fs (sparse, no extra physical disk). Sandboxes then get the full 20GB via fast dm-snapshot path - FlattenRootfs shrinks output images with resize2fs -M so stored templates are compact; EnsureImageSizes re-expands on next startup Admin templates visibility: - Add GET /v1/admin/templates endpoint listing all templates across teams - Frontend admin templates page uses listAdminTemplates() instead of team-scoped listSnapshots() - Platform templates (team_id = all-zeros UUID) now visible to all teams: GetTemplateByTeam, ListTemplatesByTeam, ListTemplatesByTeamAndType queries include platform team_id in WHERE clause
This commit is contained in:
@ -59,6 +59,14 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expand base images to the standard disk size (sparse, no extra physical
|
||||||
|
// disk). This ensures dm-snapshot sandboxes see the full size from boot.
|
||||||
|
imagesDir := filepath.Join(rootDir, "images")
|
||||||
|
if err := sandbox.EnsureImageSizes(imagesDir, sandbox.DefaultDiskSizeMB); err != nil {
|
||||||
|
slog.Error("failed to expand base images", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
cfg := sandbox.Config{
|
cfg := sandbox.Config{
|
||||||
KernelPath: filepath.Join(rootDir, "kernels", "vmlinux"),
|
KernelPath: filepath.Join(rootDir, "kernels", "vmlinux"),
|
||||||
ImagesDir: filepath.Join(rootDir, "images"),
|
ImagesDir: filepath.Join(rootDir, "images"),
|
||||||
|
|||||||
@ -144,6 +144,7 @@ CREATE TABLE sandboxes (
|
|||||||
vcpus INTEGER NOT NULL DEFAULT 1,
|
vcpus INTEGER NOT NULL DEFAULT 1,
|
||||||
memory_mb INTEGER NOT NULL DEFAULT 512,
|
memory_mb INTEGER NOT NULL DEFAULT 512,
|
||||||
timeout_sec INTEGER NOT NULL DEFAULT 300,
|
timeout_sec INTEGER NOT NULL DEFAULT 300,
|
||||||
|
disk_size_mb INTEGER NOT NULL DEFAULT 20480,
|
||||||
guest_ip TEXT NOT NULL DEFAULT '',
|
guest_ip TEXT NOT NULL DEFAULT '',
|
||||||
host_ip TEXT NOT NULL DEFAULT '',
|
host_ip TEXT NOT NULL DEFAULT '',
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
-- name: InsertSandbox :one
|
-- name: InsertSandbox :one
|
||||||
INSERT INTO sandboxes (id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec)
|
INSERT INTO sandboxes (id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: GetSandbox :one
|
-- name: GetSandbox :one
|
||||||
|
|||||||
@ -7,7 +7,8 @@ RETURNING *;
|
|||||||
SELECT * FROM templates WHERE name = $1;
|
SELECT * FROM templates WHERE name = $1;
|
||||||
|
|
||||||
-- name: GetTemplateByTeam :one
|
-- name: GetTemplateByTeam :one
|
||||||
SELECT * FROM templates WHERE name = $1 AND team_id = $2;
|
-- Platform templates (team_id = 00000000-...) are visible to all teams.
|
||||||
|
SELECT * FROM templates WHERE name = $1 AND (team_id = $2 OR team_id = '00000000-0000-0000-0000-000000000000');
|
||||||
|
|
||||||
-- name: ListTemplates :many
|
-- name: ListTemplates :many
|
||||||
SELECT * FROM templates ORDER BY created_at DESC;
|
SELECT * FROM templates ORDER BY created_at DESC;
|
||||||
@ -16,10 +17,12 @@ SELECT * FROM templates ORDER BY created_at DESC;
|
|||||||
SELECT * FROM templates WHERE type = $1 ORDER BY created_at DESC;
|
SELECT * FROM templates WHERE type = $1 ORDER BY created_at DESC;
|
||||||
|
|
||||||
-- name: ListTemplatesByTeam :many
|
-- name: ListTemplatesByTeam :many
|
||||||
SELECT * FROM templates WHERE team_id = $1 ORDER BY created_at DESC;
|
-- Platform templates are visible to all teams.
|
||||||
|
SELECT * FROM templates WHERE (team_id = $1 OR team_id = '00000000-0000-0000-0000-000000000000') ORDER BY created_at DESC;
|
||||||
|
|
||||||
-- name: ListTemplatesByTeamAndType :many
|
-- name: ListTemplatesByTeamAndType :many
|
||||||
SELECT * FROM templates WHERE team_id = $1 AND type = $2 ORDER BY created_at DESC;
|
-- Platform templates are visible to all teams.
|
||||||
|
SELECT * FROM templates WHERE (team_id = $1 OR team_id = '00000000-0000-0000-0000-000000000000') AND type = $2 ORDER BY created_at DESC;
|
||||||
|
|
||||||
-- name: DeleteTemplate :exec
|
-- name: DeleteTemplate :exec
|
||||||
DELETE FROM templates WHERE name = $1;
|
DELETE FROM templates WHERE name = $1;
|
||||||
|
|||||||
@ -50,3 +50,17 @@ export async function listBuilds(): Promise<ApiResult<Build[]>> {
|
|||||||
export async function getBuild(id: string): Promise<ApiResult<Build>> {
|
export async function getBuild(id: string): Promise<ApiResult<Build>> {
|
||||||
return apiFetch('GET', `/api/v1/admin/builds/${id}`);
|
return apiFetch('GET', `/api/v1/admin/builds/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AdminTemplate = {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
vcpus: number;
|
||||||
|
memory_mb: number;
|
||||||
|
size_bytes: number;
|
||||||
|
team_id: string;
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function listAdminTemplates(): Promise<ApiResult<AdminTemplate[]>> {
|
||||||
|
return apiFetch('GET', '/api/v1/admin/templates');
|
||||||
|
}
|
||||||
|
|||||||
@ -3,12 +3,14 @@
|
|||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { toast } from '$lib/toast.svelte';
|
import { toast } from '$lib/toast.svelte';
|
||||||
import { formatDate, timeAgo } from '$lib/utils/format';
|
import { formatDate, timeAgo } from '$lib/utils/format';
|
||||||
import { listSnapshots, deleteSnapshot, type Snapshot } from '$lib/api/capsules';
|
import { deleteSnapshot } from '$lib/api/capsules';
|
||||||
import {
|
import {
|
||||||
listBuilds,
|
listBuilds,
|
||||||
createBuild,
|
createBuild,
|
||||||
|
listAdminTemplates,
|
||||||
type Build,
|
type Build,
|
||||||
type BuildLogEntry
|
type BuildLogEntry,
|
||||||
|
type AdminTemplate
|
||||||
} from '$lib/api/builds';
|
} from '$lib/api/builds';
|
||||||
|
|
||||||
let collapsed = $state(
|
let collapsed = $state(
|
||||||
@ -20,7 +22,7 @@
|
|||||||
let activeTab = $state<'templates' | 'builds'>('templates');
|
let activeTab = $state<'templates' | 'builds'>('templates');
|
||||||
|
|
||||||
// Templates state
|
// Templates state
|
||||||
let templates = $state<Snapshot[]>([]);
|
let templates = $state<AdminTemplate[]>([]);
|
||||||
let templatesLoading = $state(true);
|
let templatesLoading = $state(true);
|
||||||
let templatesError = $state<string | null>(null);
|
let templatesError = $state<string | null>(null);
|
||||||
|
|
||||||
@ -38,7 +40,7 @@
|
|||||||
let expandedSteps = $state<Set<number>>(new Set());
|
let expandedSteps = $state<Set<number>>(new Set());
|
||||||
|
|
||||||
// Delete template state
|
// Delete template state
|
||||||
let deleteTarget = $state<Snapshot | null>(null);
|
let deleteTarget = $state<AdminTemplate | null>(null);
|
||||||
let deleting = $state(false);
|
let deleting = $state(false);
|
||||||
let deleteError = $state<string | null>(null);
|
let deleteError = $state<string | null>(null);
|
||||||
|
|
||||||
@ -64,7 +66,7 @@
|
|||||||
async function fetchTemplates() {
|
async function fetchTemplates() {
|
||||||
templatesLoading = true;
|
templatesLoading = true;
|
||||||
templatesError = null;
|
templatesError = null;
|
||||||
const result = await listSnapshots();
|
const result = await listAdminTemplates();
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
templates = result.data;
|
templates = result.data;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -17,10 +17,11 @@ import (
|
|||||||
|
|
||||||
type buildHandler struct {
|
type buildHandler struct {
|
||||||
svc *service.BuildService
|
svc *service.BuildService
|
||||||
|
db *db.Queries
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBuildHandler(svc *service.BuildService) *buildHandler {
|
func newBuildHandler(svc *service.BuildService, db *db.Queries) *buildHandler {
|
||||||
return &buildHandler{svc: svc}
|
return &buildHandler{svc: svc, db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
type createBuildRequest struct {
|
type createBuildRequest struct {
|
||||||
@ -165,3 +166,39 @@ func (h *buildHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
writeJSON(w, http.StatusOK, buildToResponse(build))
|
writeJSON(w, http.StatusOK, buildToResponse(build))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListTemplates handles GET /v1/admin/templates — returns all templates across all teams.
|
||||||
|
func (h *buildHandler) ListTemplates(w http.ResponseWriter, r *http.Request) {
|
||||||
|
templates, err := h.db.ListTemplates(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "db_error", "failed to list templates")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type templateResponse struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
VCPUs int32 `json:"vcpus"`
|
||||||
|
MemoryMB int32 `json:"memory_mb"`
|
||||||
|
SizeBytes int64 `json:"size_bytes"`
|
||||||
|
TeamID string `json:"team_id"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := make([]templateResponse, len(templates))
|
||||||
|
for i, t := range templates {
|
||||||
|
resp[i] = templateResponse{
|
||||||
|
Name: t.Name,
|
||||||
|
Type: t.Type,
|
||||||
|
VCPUs: t.Vcpus,
|
||||||
|
MemoryMB: t.MemoryMb,
|
||||||
|
SizeBytes: t.SizeBytes,
|
||||||
|
TeamID: id.FormatTeamID(t.TeamID),
|
||||||
|
}
|
||||||
|
if t.CreatedAt.Valid {
|
||||||
|
resp[i].CreatedAt = t.CreatedAt.Time.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|||||||
@ -67,7 +67,7 @@ func New(
|
|||||||
auditH := newAuditHandler(auditSvc)
|
auditH := newAuditHandler(auditSvc)
|
||||||
statsH := newStatsHandler(statsSvc)
|
statsH := newStatsHandler(statsSvc)
|
||||||
metricsH := newSandboxMetricsHandler(queries, pool)
|
metricsH := newSandboxMetricsHandler(queries, pool)
|
||||||
buildH := newBuildHandler(buildSvc)
|
buildH := newBuildHandler(buildSvc, queries)
|
||||||
|
|
||||||
// OpenAPI spec and docs.
|
// OpenAPI spec and docs.
|
||||||
r.Get("/openapi.yaml", serveOpenAPI)
|
r.Get("/openapi.yaml", serveOpenAPI)
|
||||||
@ -177,6 +177,7 @@ func New(
|
|||||||
r.Use(requireJWT(jwtSecret))
|
r.Use(requireJWT(jwtSecret))
|
||||||
r.Use(requireAdmin(queries))
|
r.Use(requireAdmin(queries))
|
||||||
r.Put("/teams/{id}/byoc", teamH.SetBYOC)
|
r.Put("/teams/{id}/byoc", teamH.SetBYOC)
|
||||||
|
r.Get("/templates", buildH.ListTemplates)
|
||||||
r.Post("/builds", buildH.Create)
|
r.Post("/builds", buildH.Create)
|
||||||
r.Get("/builds", buildH.List)
|
r.Get("/builds", buildH.List)
|
||||||
r.Get("/builds/{id}", buildH.Get)
|
r.Get("/builds/{id}", buildH.Get)
|
||||||
|
|||||||
@ -91,6 +91,7 @@ type Sandbox struct {
|
|||||||
Vcpus int32 `json:"vcpus"`
|
Vcpus int32 `json:"vcpus"`
|
||||||
MemoryMb int32 `json:"memory_mb"`
|
MemoryMb int32 `json:"memory_mb"`
|
||||||
TimeoutSec int32 `json:"timeout_sec"`
|
TimeoutSec int32 `json:"timeout_sec"`
|
||||||
|
DiskSizeMb int32 `json:"disk_size_mb"`
|
||||||
GuestIp string `json:"guest_ip"`
|
GuestIp string `json:"guest_ip"`
|
||||||
HostIp string `json:"host_ip"`
|
HostIp string `json:"host_ip"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
|||||||
@ -43,7 +43,7 @@ func (q *Queries) BulkUpdateStatusByIDs(ctx context.Context, arg BulkUpdateStatu
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getSandbox = `-- name: GetSandbox :one
|
const getSandbox = `-- name: GetSandbox :one
|
||||||
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated FROM sandboxes WHERE id = $1
|
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated FROM sandboxes WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetSandbox(ctx context.Context, id pgtype.UUID) (Sandbox, error) {
|
func (q *Queries) GetSandbox(ctx context.Context, id pgtype.UUID) (Sandbox, error) {
|
||||||
@ -58,6 +58,7 @@ func (q *Queries) GetSandbox(ctx context.Context, id pgtype.UUID) (Sandbox, erro
|
|||||||
&i.Vcpus,
|
&i.Vcpus,
|
||||||
&i.MemoryMb,
|
&i.MemoryMb,
|
||||||
&i.TimeoutSec,
|
&i.TimeoutSec,
|
||||||
|
&i.DiskSizeMb,
|
||||||
&i.GuestIp,
|
&i.GuestIp,
|
||||||
&i.HostIp,
|
&i.HostIp,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
@ -69,7 +70,7 @@ func (q *Queries) GetSandbox(ctx context.Context, id pgtype.UUID) (Sandbox, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getSandboxByTeam = `-- name: GetSandboxByTeam :one
|
const getSandboxByTeam = `-- name: GetSandboxByTeam :one
|
||||||
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated FROM sandboxes WHERE id = $1 AND team_id = $2
|
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated FROM sandboxes WHERE id = $1 AND team_id = $2
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetSandboxByTeamParams struct {
|
type GetSandboxByTeamParams struct {
|
||||||
@ -89,6 +90,7 @@ func (q *Queries) GetSandboxByTeam(ctx context.Context, arg GetSandboxByTeamPara
|
|||||||
&i.Vcpus,
|
&i.Vcpus,
|
||||||
&i.MemoryMb,
|
&i.MemoryMb,
|
||||||
&i.TimeoutSec,
|
&i.TimeoutSec,
|
||||||
|
&i.DiskSizeMb,
|
||||||
&i.GuestIp,
|
&i.GuestIp,
|
||||||
&i.HostIp,
|
&i.HostIp,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
@ -100,9 +102,9 @@ func (q *Queries) GetSandboxByTeam(ctx context.Context, arg GetSandboxByTeamPara
|
|||||||
}
|
}
|
||||||
|
|
||||||
const insertSandbox = `-- name: InsertSandbox :one
|
const insertSandbox = `-- name: InsertSandbox :one
|
||||||
INSERT INTO sandboxes (id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec)
|
INSERT INTO sandboxes (id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
RETURNING id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated
|
RETURNING id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertSandboxParams struct {
|
type InsertSandboxParams struct {
|
||||||
@ -114,6 +116,7 @@ type InsertSandboxParams struct {
|
|||||||
Vcpus int32 `json:"vcpus"`
|
Vcpus int32 `json:"vcpus"`
|
||||||
MemoryMb int32 `json:"memory_mb"`
|
MemoryMb int32 `json:"memory_mb"`
|
||||||
TimeoutSec int32 `json:"timeout_sec"`
|
TimeoutSec int32 `json:"timeout_sec"`
|
||||||
|
DiskSizeMb int32 `json:"disk_size_mb"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) InsertSandbox(ctx context.Context, arg InsertSandboxParams) (Sandbox, error) {
|
func (q *Queries) InsertSandbox(ctx context.Context, arg InsertSandboxParams) (Sandbox, error) {
|
||||||
@ -126,6 +129,7 @@ func (q *Queries) InsertSandbox(ctx context.Context, arg InsertSandboxParams) (S
|
|||||||
arg.Vcpus,
|
arg.Vcpus,
|
||||||
arg.MemoryMb,
|
arg.MemoryMb,
|
||||||
arg.TimeoutSec,
|
arg.TimeoutSec,
|
||||||
|
arg.DiskSizeMb,
|
||||||
)
|
)
|
||||||
var i Sandbox
|
var i Sandbox
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@ -137,6 +141,7 @@ func (q *Queries) InsertSandbox(ctx context.Context, arg InsertSandboxParams) (S
|
|||||||
&i.Vcpus,
|
&i.Vcpus,
|
||||||
&i.MemoryMb,
|
&i.MemoryMb,
|
||||||
&i.TimeoutSec,
|
&i.TimeoutSec,
|
||||||
|
&i.DiskSizeMb,
|
||||||
&i.GuestIp,
|
&i.GuestIp,
|
||||||
&i.HostIp,
|
&i.HostIp,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
@ -148,7 +153,7 @@ func (q *Queries) InsertSandbox(ctx context.Context, arg InsertSandboxParams) (S
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listActiveSandboxesByTeam = `-- name: ListActiveSandboxesByTeam :many
|
const listActiveSandboxesByTeam = `-- name: ListActiveSandboxesByTeam :many
|
||||||
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated FROM sandboxes
|
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated FROM sandboxes
|
||||||
WHERE team_id = $1 AND status IN ('running', 'paused', 'starting')
|
WHERE team_id = $1 AND status IN ('running', 'paused', 'starting')
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
@ -171,6 +176,7 @@ func (q *Queries) ListActiveSandboxesByTeam(ctx context.Context, teamID pgtype.U
|
|||||||
&i.Vcpus,
|
&i.Vcpus,
|
||||||
&i.MemoryMb,
|
&i.MemoryMb,
|
||||||
&i.TimeoutSec,
|
&i.TimeoutSec,
|
||||||
|
&i.DiskSizeMb,
|
||||||
&i.GuestIp,
|
&i.GuestIp,
|
||||||
&i.HostIp,
|
&i.HostIp,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
@ -189,7 +195,7 @@ func (q *Queries) ListActiveSandboxesByTeam(ctx context.Context, teamID pgtype.U
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listSandboxes = `-- name: ListSandboxes :many
|
const listSandboxes = `-- name: ListSandboxes :many
|
||||||
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated FROM sandboxes ORDER BY created_at DESC
|
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated FROM sandboxes ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) ListSandboxes(ctx context.Context) ([]Sandbox, error) {
|
func (q *Queries) ListSandboxes(ctx context.Context) ([]Sandbox, error) {
|
||||||
@ -210,6 +216,7 @@ func (q *Queries) ListSandboxes(ctx context.Context) ([]Sandbox, error) {
|
|||||||
&i.Vcpus,
|
&i.Vcpus,
|
||||||
&i.MemoryMb,
|
&i.MemoryMb,
|
||||||
&i.TimeoutSec,
|
&i.TimeoutSec,
|
||||||
|
&i.DiskSizeMb,
|
||||||
&i.GuestIp,
|
&i.GuestIp,
|
||||||
&i.HostIp,
|
&i.HostIp,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
@ -228,7 +235,7 @@ func (q *Queries) ListSandboxes(ctx context.Context) ([]Sandbox, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listSandboxesByHostAndStatus = `-- name: ListSandboxesByHostAndStatus :many
|
const listSandboxesByHostAndStatus = `-- name: ListSandboxesByHostAndStatus :many
|
||||||
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated FROM sandboxes
|
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated FROM sandboxes
|
||||||
WHERE host_id = $1 AND status = ANY($2::text[])
|
WHERE host_id = $1 AND status = ANY($2::text[])
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
@ -256,6 +263,7 @@ func (q *Queries) ListSandboxesByHostAndStatus(ctx context.Context, arg ListSand
|
|||||||
&i.Vcpus,
|
&i.Vcpus,
|
||||||
&i.MemoryMb,
|
&i.MemoryMb,
|
||||||
&i.TimeoutSec,
|
&i.TimeoutSec,
|
||||||
|
&i.DiskSizeMb,
|
||||||
&i.GuestIp,
|
&i.GuestIp,
|
||||||
&i.HostIp,
|
&i.HostIp,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
@ -274,7 +282,7 @@ func (q *Queries) ListSandboxesByHostAndStatus(ctx context.Context, arg ListSand
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listSandboxesByTeam = `-- name: ListSandboxesByTeam :many
|
const listSandboxesByTeam = `-- name: ListSandboxesByTeam :many
|
||||||
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated FROM sandboxes
|
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated FROM sandboxes
|
||||||
WHERE team_id = $1 AND status NOT IN ('stopped', 'error')
|
WHERE team_id = $1 AND status NOT IN ('stopped', 'error')
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
@ -297,6 +305,7 @@ func (q *Queries) ListSandboxesByTeam(ctx context.Context, teamID pgtype.UUID) (
|
|||||||
&i.Vcpus,
|
&i.Vcpus,
|
||||||
&i.MemoryMb,
|
&i.MemoryMb,
|
||||||
&i.TimeoutSec,
|
&i.TimeoutSec,
|
||||||
|
&i.DiskSizeMb,
|
||||||
&i.GuestIp,
|
&i.GuestIp,
|
||||||
&i.HostIp,
|
&i.HostIp,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
@ -355,7 +364,7 @@ SET status = 'running',
|
|||||||
last_active_at = $4,
|
last_active_at = $4,
|
||||||
last_updated = NOW()
|
last_updated = NOW()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated
|
RETURNING id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateSandboxRunningParams struct {
|
type UpdateSandboxRunningParams struct {
|
||||||
@ -382,6 +391,7 @@ func (q *Queries) UpdateSandboxRunning(ctx context.Context, arg UpdateSandboxRun
|
|||||||
&i.Vcpus,
|
&i.Vcpus,
|
||||||
&i.MemoryMb,
|
&i.MemoryMb,
|
||||||
&i.TimeoutSec,
|
&i.TimeoutSec,
|
||||||
|
&i.DiskSizeMb,
|
||||||
&i.GuestIp,
|
&i.GuestIp,
|
||||||
&i.HostIp,
|
&i.HostIp,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
@ -397,7 +407,7 @@ UPDATE sandboxes
|
|||||||
SET status = $2,
|
SET status = $2,
|
||||||
last_updated = NOW()
|
last_updated = NOW()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated
|
RETURNING id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateSandboxStatusParams struct {
|
type UpdateSandboxStatusParams struct {
|
||||||
@ -417,6 +427,7 @@ func (q *Queries) UpdateSandboxStatus(ctx context.Context, arg UpdateSandboxStat
|
|||||||
&i.Vcpus,
|
&i.Vcpus,
|
||||||
&i.MemoryMb,
|
&i.MemoryMb,
|
||||||
&i.TimeoutSec,
|
&i.TimeoutSec,
|
||||||
|
&i.DiskSizeMb,
|
||||||
&i.GuestIp,
|
&i.GuestIp,
|
||||||
&i.HostIp,
|
&i.HostIp,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
|
|||||||
@ -54,7 +54,7 @@ func (q *Queries) GetTemplate(ctx context.Context, name string) (Template, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getTemplateByTeam = `-- name: GetTemplateByTeam :one
|
const getTemplateByTeam = `-- name: GetTemplateByTeam :one
|
||||||
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id FROM templates WHERE name = $1 AND team_id = $2
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id FROM templates WHERE name = $1 AND (team_id = $2 OR team_id = '00000000-0000-0000-0000-000000000000')
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetTemplateByTeamParams struct {
|
type GetTemplateByTeamParams struct {
|
||||||
@ -62,6 +62,7 @@ type GetTemplateByTeamParams struct {
|
|||||||
TeamID pgtype.UUID `json:"team_id"`
|
TeamID pgtype.UUID `json:"team_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Platform templates (team_id = 00000000-...) are visible to all teams.
|
||||||
func (q *Queries) GetTemplateByTeam(ctx context.Context, arg GetTemplateByTeamParams) (Template, error) {
|
func (q *Queries) GetTemplateByTeam(ctx context.Context, arg GetTemplateByTeamParams) (Template, error) {
|
||||||
row := q.db.QueryRow(ctx, getTemplateByTeam, arg.Name, arg.TeamID)
|
row := q.db.QueryRow(ctx, getTemplateByTeam, arg.Name, arg.TeamID)
|
||||||
var i Template
|
var i Template
|
||||||
@ -147,9 +148,10 @@ func (q *Queries) ListTemplates(ctx context.Context) ([]Template, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listTemplatesByTeam = `-- name: ListTemplatesByTeam :many
|
const listTemplatesByTeam = `-- name: ListTemplatesByTeam :many
|
||||||
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id FROM templates WHERE team_id = $1 ORDER BY created_at DESC
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id FROM templates WHERE (team_id = $1 OR team_id = '00000000-0000-0000-0000-000000000000') ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// Platform templates are visible to all teams.
|
||||||
func (q *Queries) ListTemplatesByTeam(ctx context.Context, teamID pgtype.UUID) ([]Template, error) {
|
func (q *Queries) ListTemplatesByTeam(ctx context.Context, teamID pgtype.UUID) ([]Template, error) {
|
||||||
rows, err := q.db.Query(ctx, listTemplatesByTeam, teamID)
|
rows, err := q.db.Query(ctx, listTemplatesByTeam, teamID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -179,7 +181,7 @@ func (q *Queries) ListTemplatesByTeam(ctx context.Context, teamID pgtype.UUID) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listTemplatesByTeamAndType = `-- name: ListTemplatesByTeamAndType :many
|
const listTemplatesByTeamAndType = `-- name: ListTemplatesByTeamAndType :many
|
||||||
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id FROM templates WHERE team_id = $1 AND type = $2 ORDER BY created_at DESC
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id FROM templates WHERE (team_id = $1 OR team_id = '00000000-0000-0000-0000-000000000000') AND type = $2 ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
type ListTemplatesByTeamAndTypeParams struct {
|
type ListTemplatesByTeamAndTypeParams struct {
|
||||||
@ -187,6 +189,7 @@ type ListTemplatesByTeamAndTypeParams struct {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Platform templates are visible to all teams.
|
||||||
func (q *Queries) ListTemplatesByTeamAndType(ctx context.Context, arg ListTemplatesByTeamAndTypeParams) ([]Template, error) {
|
func (q *Queries) ListTemplatesByTeamAndType(ctx context.Context, arg ListTemplatesByTeamAndTypeParams) ([]Template, error) {
|
||||||
rows, err := q.db.Query(ctx, listTemplatesByTeamAndType, arg.TeamID, arg.Type)
|
rows, err := q.db.Query(ctx, listTemplatesByTeamAndType, arg.TeamID, arg.Type)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -116,9 +116,10 @@ type SnapshotDevice struct {
|
|||||||
// writable CoW layer.
|
// writable CoW layer.
|
||||||
//
|
//
|
||||||
// The origin loop device must already exist (from LoopRegistry.Acquire).
|
// The origin loop device must already exist (from LoopRegistry.Acquire).
|
||||||
func CreateSnapshot(name, originLoopDev, cowPath string, originSizeBytes int64) (*SnapshotDevice, error) {
|
func CreateSnapshot(name, originLoopDev, cowPath string, originSizeBytes, cowSizeBytes int64) (*SnapshotDevice, error) {
|
||||||
// Create sparse CoW file sized to match the origin.
|
// Create sparse CoW file. The logical size limits how many blocks can be
|
||||||
if err := createSparseFile(cowPath, originSizeBytes); err != nil {
|
// modified; because the file is sparse, only written blocks use real disk.
|
||||||
|
if err := createSparseFile(cowPath, cowSizeBytes); err != nil {
|
||||||
return nil, fmt.Errorf("create cow file: %w", err)
|
return nil, fmt.Errorf("create cow file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,6 +129,9 @@ func CreateSnapshot(name, originLoopDev, cowPath string, originSizeBytes int64)
|
|||||||
return nil, fmt.Errorf("losetup cow: %w", err)
|
return nil, fmt.Errorf("losetup cow: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The dm-snapshot virtual device size must match the origin — the snapshot
|
||||||
|
// target maps 1:1 onto origin sectors. The CoW file just needs enough
|
||||||
|
// space to store all modified blocks (it's sparse, so 20GB costs nothing).
|
||||||
sectors := originSizeBytes / 512
|
sectors := originSizeBytes / 512
|
||||||
if err := dmsetupCreate(name, originLoopDev, cowLoopDev, sectors); err != nil {
|
if err := dmsetupCreate(name, originLoopDev, cowLoopDev, sectors); err != nil {
|
||||||
if detachErr := losetupDetach(cowLoopDev); detachErr != nil {
|
if detachErr := losetupDetach(cowLoopDev); detachErr != nil {
|
||||||
|
|||||||
@ -39,7 +39,7 @@ func (s *Server) CreateSandbox(
|
|||||||
) (*connect.Response[pb.CreateSandboxResponse], error) {
|
) (*connect.Response[pb.CreateSandboxResponse], error) {
|
||||||
msg := req.Msg
|
msg := req.Msg
|
||||||
|
|
||||||
sb, err := s.mgr.Create(ctx, msg.SandboxId, msg.Template, int(msg.Vcpus), int(msg.MemoryMb), int(msg.TimeoutSec))
|
sb, err := s.mgr.Create(ctx, msg.SandboxId, msg.Template, int(msg.Vcpus), int(msg.MemoryMb), int(msg.TimeoutSec), int(msg.DiskSizeMb))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("create sandbox: %w", err))
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("create sandbox: %w", err))
|
||||||
}
|
}
|
||||||
|
|||||||
74
internal/sandbox/images.go
Normal file
74
internal/sandbox/images.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
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 = 20480 // 20 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
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -94,7 +95,7 @@ func New(cfg Config) *Manager {
|
|||||||
|
|
||||||
// Create boots a new sandbox: clone rootfs, set up network, start VM, wait for envd.
|
// Create boots a new sandbox: clone rootfs, set up network, start VM, wait for envd.
|
||||||
// If sandboxID is empty, a new ID is generated.
|
// If sandboxID is empty, a new ID is generated.
|
||||||
func (m *Manager) Create(ctx context.Context, sandboxID, template string, vcpus, memoryMB, timeoutSec int) (*models.Sandbox, error) {
|
func (m *Manager) Create(ctx context.Context, sandboxID, template string, vcpus, memoryMB, timeoutSec, diskSizeMB int) (*models.Sandbox, error) {
|
||||||
if sandboxID == "" {
|
if sandboxID == "" {
|
||||||
sandboxID = id.FormatSandboxID(id.NewSandboxID())
|
sandboxID = id.FormatSandboxID(id.NewSandboxID())
|
||||||
}
|
}
|
||||||
@ -105,6 +106,9 @@ func (m *Manager) Create(ctx context.Context, sandboxID, template string, vcpus,
|
|||||||
if memoryMB <= 0 {
|
if memoryMB <= 0 {
|
||||||
memoryMB = 512
|
memoryMB = 512
|
||||||
}
|
}
|
||||||
|
if diskSizeMB <= 0 {
|
||||||
|
diskSizeMB = 20480 // 20 GB default
|
||||||
|
}
|
||||||
|
|
||||||
if template == "" {
|
if template == "" {
|
||||||
template = "minimal"
|
template = "minimal"
|
||||||
@ -115,7 +119,7 @@ func (m *Manager) Create(ctx context.Context, sandboxID, template string, vcpus,
|
|||||||
|
|
||||||
// Check if template refers to a snapshot (has snapfile + memfile + header + rootfs).
|
// Check if template refers to a snapshot (has snapfile + memfile + header + rootfs).
|
||||||
if snapshot.IsSnapshot(m.cfg.ImagesDir, template) {
|
if snapshot.IsSnapshot(m.cfg.ImagesDir, template) {
|
||||||
return m.createFromSnapshot(ctx, sandboxID, template, vcpus, memoryMB, timeoutSec)
|
return m.createFromSnapshot(ctx, sandboxID, template, vcpus, memoryMB, timeoutSec, diskSizeMB)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve base rootfs image: /var/lib/wrenn/images/{template}/rootfs.ext4
|
// Resolve base rootfs image: /var/lib/wrenn/images/{template}/rootfs.ext4
|
||||||
@ -139,7 +143,8 @@ func (m *Manager) Create(ctx context.Context, sandboxID, template string, vcpus,
|
|||||||
// Create dm-snapshot with per-sandbox CoW file.
|
// Create dm-snapshot with per-sandbox CoW file.
|
||||||
dmName := "wrenn-" + sandboxID
|
dmName := "wrenn-" + sandboxID
|
||||||
cowPath := filepath.Join(m.cfg.SandboxesDir, fmt.Sprintf("%s.cow", sandboxID))
|
cowPath := filepath.Join(m.cfg.SandboxesDir, fmt.Sprintf("%s.cow", sandboxID))
|
||||||
dmDev, err := devicemapper.CreateSnapshot(dmName, originLoop, cowPath, originSize)
|
cowSize := int64(diskSizeMB) * 1024 * 1024
|
||||||
|
dmDev, err := devicemapper.CreateSnapshot(dmName, originLoop, cowPath, originSize, cowSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.loops.Release(baseRootfs)
|
m.loops.Release(baseRootfs)
|
||||||
return nil, fmt.Errorf("create dm-snapshot: %w", err)
|
return nil, fmt.Errorf("create dm-snapshot: %w", err)
|
||||||
@ -853,6 +858,17 @@ func (m *Manager) FlattenRootfs(ctx context.Context, sandboxID, name string) (in
|
|||||||
// Clean up dm device and loop device now that flatten is complete.
|
// Clean up dm device and loop device now that flatten is complete.
|
||||||
m.cleanupDM(sb)
|
m.cleanupDM(sb)
|
||||||
|
|
||||||
|
// Shrink the flattened image to its minimum size so stored templates are
|
||||||
|
// compact. EnsureImageSizes will re-expand them on the next agent startup.
|
||||||
|
if out, err := exec.Command("e2fsck", "-fy", outputPath).CombinedOutput(); err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() > 1 {
|
||||||
|
slog.Warn("e2fsck before shrink failed (non-fatal)", "output", string(out), "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if out, err := exec.Command("resize2fs", "-M", outputPath).CombinedOutput(); err != nil {
|
||||||
|
slog.Warn("resize2fs -M failed (non-fatal)", "output", string(out), "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
sizeBytes, err := snapshot.DirSize(m.cfg.ImagesDir, name)
|
sizeBytes, err := snapshot.DirSize(m.cfg.ImagesDir, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("failed to calculate template size", "error", err)
|
slog.Warn("failed to calculate template size", "error", err)
|
||||||
@ -891,7 +907,7 @@ func (m *Manager) DeleteSnapshot(name string) error {
|
|||||||
// in ImagesDir/{snapshotName}/. Uses UFFD for lazy memory loading.
|
// in ImagesDir/{snapshotName}/. Uses UFFD for lazy memory loading.
|
||||||
// The template's rootfs.ext4 is a flattened standalone image — we create a
|
// The template's rootfs.ext4 is a flattened standalone image — we create a
|
||||||
// dm-snapshot on top of it just like a normal Create.
|
// dm-snapshot on top of it just like a normal Create.
|
||||||
func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID, snapshotName string, vcpus, _, timeoutSec int) (*models.Sandbox, error) {
|
func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID, snapshotName string, vcpus, _, timeoutSec, diskSizeMB int) (*models.Sandbox, error) {
|
||||||
imagesDir := m.cfg.ImagesDir
|
imagesDir := m.cfg.ImagesDir
|
||||||
|
|
||||||
// Read the header.
|
// Read the header.
|
||||||
@ -936,7 +952,8 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID, snapshotNam
|
|||||||
|
|
||||||
dmName := "wrenn-" + sandboxID
|
dmName := "wrenn-" + sandboxID
|
||||||
cowPath := filepath.Join(m.cfg.SandboxesDir, fmt.Sprintf("%s.cow", sandboxID))
|
cowPath := filepath.Join(m.cfg.SandboxesDir, fmt.Sprintf("%s.cow", sandboxID))
|
||||||
dmDev, err := devicemapper.CreateSnapshot(dmName, originLoop, cowPath, originSize)
|
cowSize := int64(diskSizeMB) * 1024 * 1024
|
||||||
|
dmDev, err := devicemapper.CreateSnapshot(dmName, originLoop, cowPath, originSize, cowSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
source.Close()
|
source.Close()
|
||||||
m.loops.Release(baseRootfs)
|
m.loops.Release(baseRootfs)
|
||||||
|
|||||||
@ -200,6 +200,7 @@ func (s *BuildService) executeBuild(ctx context.Context, buildIDStr string) {
|
|||||||
Vcpus: build.Vcpus,
|
Vcpus: build.Vcpus,
|
||||||
MemoryMb: build.MemoryMb,
|
MemoryMb: build.MemoryMb,
|
||||||
TimeoutSec: 0, // no auto-pause for builds
|
TimeoutSec: 0, // no auto-pause for builds
|
||||||
|
DiskSizeMb: 20480, // 20 GB for template builds
|
||||||
}))
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.failBuild(ctx, buildID, fmt.Sprintf("create sandbox failed: %v", err))
|
s.failBuild(ctx, buildID, fmt.Sprintf("create sandbox failed: %v", err))
|
||||||
|
|||||||
@ -32,6 +32,7 @@ type SandboxCreateParams struct {
|
|||||||
VCPUs int32
|
VCPUs int32
|
||||||
MemoryMB int32
|
MemoryMB int32
|
||||||
TimeoutSec int32
|
TimeoutSec int32
|
||||||
|
DiskSizeMB int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// agentForSandbox looks up the host for the given sandbox and returns a client.
|
// agentForSandbox looks up the host for the given sandbox and returns a client.
|
||||||
@ -77,6 +78,9 @@ func (s *SandboxService) Create(ctx context.Context, p SandboxCreateParams) (db.
|
|||||||
if p.MemoryMB <= 0 {
|
if p.MemoryMB <= 0 {
|
||||||
p.MemoryMB = 512
|
p.MemoryMB = 512
|
||||||
}
|
}
|
||||||
|
if p.DiskSizeMB <= 0 {
|
||||||
|
p.DiskSizeMB = 20480 // 20 GB default
|
||||||
|
}
|
||||||
|
|
||||||
// If the template is a snapshot, use its baked-in vcpus/memory.
|
// If the template is a snapshot, use its baked-in vcpus/memory.
|
||||||
if tmpl, err := s.DB.GetTemplateByTeam(ctx, db.GetTemplateByTeamParams{Name: p.Template, TeamID: p.TeamID}); err == nil && tmpl.Type == "snapshot" {
|
if tmpl, err := s.DB.GetTemplateByTeam(ctx, db.GetTemplateByTeamParams{Name: p.Template, TeamID: p.TeamID}); err == nil && tmpl.Type == "snapshot" {
|
||||||
@ -117,6 +121,7 @@ func (s *SandboxService) Create(ctx context.Context, p SandboxCreateParams) (db.
|
|||||||
Vcpus: p.VCPUs,
|
Vcpus: p.VCPUs,
|
||||||
MemoryMb: p.MemoryMB,
|
MemoryMb: p.MemoryMB,
|
||||||
TimeoutSec: p.TimeoutSec,
|
TimeoutSec: p.TimeoutSec,
|
||||||
|
DiskSizeMb: p.DiskSizeMB,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return db.Sandbox{}, fmt.Errorf("insert sandbox: %w", err)
|
return db.Sandbox{}, fmt.Errorf("insert sandbox: %w", err)
|
||||||
}
|
}
|
||||||
@ -127,6 +132,7 @@ func (s *SandboxService) Create(ctx context.Context, p SandboxCreateParams) (db.
|
|||||||
Vcpus: p.VCPUs,
|
Vcpus: p.VCPUs,
|
||||||
MemoryMb: p.MemoryMB,
|
MemoryMb: p.MemoryMB,
|
||||||
TimeoutSec: p.TimeoutSec,
|
TimeoutSec: p.TimeoutSec,
|
||||||
|
DiskSizeMb: p.DiskSizeMB,
|
||||||
}))
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, dbErr := s.DB.UpdateSandboxStatus(ctx, db.UpdateSandboxStatusParams{
|
if _, dbErr := s.DB.UpdateSandboxStatus(ctx, db.UpdateSandboxStatusParams{
|
||||||
|
|||||||
@ -34,6 +34,9 @@ type CreateSandboxRequest struct {
|
|||||||
// TTL in seconds. Sandbox is auto-paused after this duration of
|
// TTL in seconds. Sandbox is auto-paused after this duration of
|
||||||
// inactivity. 0 means no auto-pause.
|
// inactivity. 0 means no auto-pause.
|
||||||
TimeoutSec int32 `protobuf:"varint,4,opt,name=timeout_sec,json=timeoutSec,proto3" json:"timeout_sec,omitempty"`
|
TimeoutSec int32 `protobuf:"varint,4,opt,name=timeout_sec,json=timeoutSec,proto3" json:"timeout_sec,omitempty"`
|
||||||
|
// Disk size in MB for the sparse CoW file. Limits how much data the
|
||||||
|
// sandbox can write beyond the base image. Default: 20480 (20 GB).
|
||||||
|
DiskSizeMb int32 `protobuf:"varint,6,opt,name=disk_size_mb,json=diskSizeMb,proto3" json:"disk_size_mb,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -103,6 +106,13 @@ func (x *CreateSandboxRequest) GetTimeoutSec() int32 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *CreateSandboxRequest) GetDiskSizeMb() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.DiskSizeMb
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
type CreateSandboxResponse struct {
|
type CreateSandboxResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"`
|
SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"`
|
||||||
@ -2271,7 +2281,7 @@ var File_hostagent_proto protoreflect.FileDescriptor
|
|||||||
|
|
||||||
const file_hostagent_proto_rawDesc = "" +
|
const file_hostagent_proto_rawDesc = "" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\x0fhostagent.proto\x12\fhostagent.v1\"\xa5\x01\n" +
|
"\x0fhostagent.proto\x12\fhostagent.v1\"\xc7\x01\n" +
|
||||||
"\x14CreateSandboxRequest\x12\x1d\n" +
|
"\x14CreateSandboxRequest\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"sandbox_id\x18\x05 \x01(\tR\tsandboxId\x12\x1a\n" +
|
"sandbox_id\x18\x05 \x01(\tR\tsandboxId\x12\x1a\n" +
|
||||||
@ -2279,7 +2289,9 @@ const file_hostagent_proto_rawDesc = "" +
|
|||||||
"\x05vcpus\x18\x02 \x01(\x05R\x05vcpus\x12\x1b\n" +
|
"\x05vcpus\x18\x02 \x01(\x05R\x05vcpus\x12\x1b\n" +
|
||||||
"\tmemory_mb\x18\x03 \x01(\x05R\bmemoryMb\x12\x1f\n" +
|
"\tmemory_mb\x18\x03 \x01(\x05R\bmemoryMb\x12\x1f\n" +
|
||||||
"\vtimeout_sec\x18\x04 \x01(\x05R\n" +
|
"\vtimeout_sec\x18\x04 \x01(\x05R\n" +
|
||||||
"timeoutSec\"g\n" +
|
"timeoutSec\x12 \n" +
|
||||||
|
"\fdisk_size_mb\x18\x06 \x01(\x05R\n" +
|
||||||
|
"diskSizeMb\"g\n" +
|
||||||
"\x15CreateSandboxResponse\x12\x1d\n" +
|
"\x15CreateSandboxResponse\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x16\n" +
|
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x16\n" +
|
||||||
|
|||||||
@ -85,6 +85,10 @@ message CreateSandboxRequest {
|
|||||||
// TTL in seconds. Sandbox is auto-paused after this duration of
|
// TTL in seconds. Sandbox is auto-paused after this duration of
|
||||||
// inactivity. 0 means no auto-pause.
|
// inactivity. 0 means no auto-pause.
|
||||||
int32 timeout_sec = 4;
|
int32 timeout_sec = 4;
|
||||||
|
|
||||||
|
// Disk size in MB for the sparse CoW file. Limits how much data the
|
||||||
|
// sandbox can write beyond the base image. Default: 20480 (20 GB).
|
||||||
|
int32 disk_size_mb = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreateSandboxResponse {
|
message CreateSandboxResponse {
|
||||||
|
|||||||
Reference in New Issue
Block a user