forked from wrenn/wrenn
Add UUID-based template IDs and team-scoped template directory layout
Introduces internal/layout package for centralized path construction,
migrates templates from name-based TEXT primary keys to UUID PKs with
team-scoped directories (WRENN_DIR/images/teams/{team_id}/{template_id}).
The built-in minimal template uses sentinel zero UUIDs. Proto messages
carry team_id + template_id alongside deprecated template name field.
Team deletion now cleans up template files across all hosts.
This commit is contained in:
64
db/migrations/20260328162803_template_uuid_pk.sql
Normal file
64
db/migrations/20260328162803_template_uuid_pk.sql
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
-- +goose Up
|
||||||
|
|
||||||
|
-- 1. Add UUID id column to templates and make it the primary key.
|
||||||
|
ALTER TABLE templates ADD COLUMN id UUID DEFAULT gen_random_uuid();
|
||||||
|
UPDATE templates SET id = gen_random_uuid() WHERE id IS NULL;
|
||||||
|
ALTER TABLE templates ALTER COLUMN id SET NOT NULL;
|
||||||
|
ALTER TABLE templates DROP CONSTRAINT templates_pkey;
|
||||||
|
ALTER TABLE templates ADD PRIMARY KEY (id);
|
||||||
|
|
||||||
|
-- 2. Name becomes a display field with team-scoped uniqueness.
|
||||||
|
ALTER TABLE templates ADD CONSTRAINT uq_templates_team_name UNIQUE (team_id, name);
|
||||||
|
|
||||||
|
-- 3. Prevent team templates from using names that belong to global (platform) templates.
|
||||||
|
-- A team template insert/update with a name matching any platform template is rejected.
|
||||||
|
CREATE OR REPLACE FUNCTION check_global_template_name_collision()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.team_id != '00000000-0000-0000-0000-000000000000' THEN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM templates
|
||||||
|
WHERE name = NEW.name
|
||||||
|
AND team_id = '00000000-0000-0000-0000-000000000000'
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION 'template name "%" is reserved by a global template', NEW.name
|
||||||
|
USING ERRCODE = 'unique_violation';
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_check_global_template_name
|
||||||
|
BEFORE INSERT OR UPDATE ON templates
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION check_global_template_name_collision();
|
||||||
|
|
||||||
|
-- 4. Add template UUID references to template_builds.
|
||||||
|
ALTER TABLE template_builds
|
||||||
|
ADD COLUMN template_id UUID,
|
||||||
|
ADD COLUMN team_id UUID;
|
||||||
|
|
||||||
|
-- 5. Add template UUID references to sandboxes.
|
||||||
|
ALTER TABLE sandboxes
|
||||||
|
ADD COLUMN template_id UUID,
|
||||||
|
ADD COLUMN template_team_id UUID;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
|
||||||
|
ALTER TABLE sandboxes
|
||||||
|
DROP COLUMN IF EXISTS template_team_id,
|
||||||
|
DROP COLUMN IF EXISTS template_id;
|
||||||
|
|
||||||
|
ALTER TABLE template_builds
|
||||||
|
DROP COLUMN IF EXISTS team_id,
|
||||||
|
DROP COLUMN IF EXISTS template_id;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_check_global_template_name ON templates;
|
||||||
|
DROP FUNCTION IF EXISTS check_global_template_name_collision();
|
||||||
|
|
||||||
|
ALTER TABLE templates DROP CONSTRAINT IF EXISTS uq_templates_team_name;
|
||||||
|
|
||||||
|
ALTER TABLE templates DROP CONSTRAINT IF EXISTS templates_pkey;
|
||||||
|
ALTER TABLE templates ADD PRIMARY KEY (name);
|
||||||
|
ALTER TABLE templates DROP COLUMN IF EXISTS id;
|
||||||
@ -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, disk_size_mb)
|
INSERT INTO sandboxes (id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, template_id, template_team_id)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: GetSandbox :one
|
-- name: GetSandbox :one
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
-- name: InsertTemplateBuild :one
|
-- name: InsertTemplateBuild :one
|
||||||
INSERT INTO template_builds (id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, total_steps)
|
INSERT INTO template_builds (id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, total_steps, template_id, team_id)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending', $8)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending', $8, $9, $10)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: GetTemplateBuild :one
|
-- name: GetTemplateBuild :one
|
||||||
|
|||||||
@ -1,15 +1,23 @@
|
|||||||
-- name: InsertTemplate :one
|
-- name: InsertTemplate :one
|
||||||
INSERT INTO templates (name, type, vcpus, memory_mb, size_bytes, team_id)
|
INSERT INTO templates (id, name, type, vcpus, memory_mb, size_bytes, team_id)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: GetTemplate :one
|
-- name: GetTemplate :one
|
||||||
SELECT * FROM templates WHERE name = $1;
|
SELECT * FROM templates WHERE id = $1;
|
||||||
|
|
||||||
-- name: GetTemplateByTeam :one
|
-- name: GetTemplateByTeam :one
|
||||||
-- Platform templates (team_id = 00000000-...) are visible to all teams.
|
-- 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');
|
SELECT * FROM templates WHERE name = $1 AND (team_id = $2 OR team_id = '00000000-0000-0000-0000-000000000000');
|
||||||
|
|
||||||
|
-- name: GetTemplateByName :one
|
||||||
|
-- Look up a template by team_id and name (exact team match, no global fallback).
|
||||||
|
SELECT * FROM templates WHERE team_id = $1 AND name = $2;
|
||||||
|
|
||||||
|
-- name: GetPlatformTemplateByName :one
|
||||||
|
-- Check if a global (platform) template exists with the given name.
|
||||||
|
SELECT * FROM templates WHERE team_id = '00000000-0000-0000-0000-000000000000' AND name = $1;
|
||||||
|
|
||||||
-- name: ListTemplates :many
|
-- name: ListTemplates :many
|
||||||
SELECT * FROM templates ORDER BY created_at DESC;
|
SELECT * FROM templates ORDER BY created_at DESC;
|
||||||
|
|
||||||
@ -25,7 +33,15 @@ SELECT * FROM templates WHERE (team_id = $1 OR team_id = '00000000-0000-0000-000
|
|||||||
SELECT * FROM templates WHERE (team_id = $1 OR team_id = '00000000-0000-0000-0000-000000000000') AND type = $2 ORDER BY created_at DESC;
|
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 id = $1;
|
||||||
|
|
||||||
-- name: DeleteTemplateByTeam :exec
|
-- name: DeleteTemplateByTeam :exec
|
||||||
DELETE FROM templates WHERE name = $1 AND team_id = $2;
|
DELETE FROM templates WHERE name = $1 AND team_id = $2;
|
||||||
|
|
||||||
|
-- name: DeleteTemplatesByTeam :exec
|
||||||
|
-- Bulk delete all templates owned by a team (for team soft-delete cleanup).
|
||||||
|
DELETE FROM templates WHERE team_id = $1;
|
||||||
|
|
||||||
|
-- name: ListTemplatesByTeamOnly :many
|
||||||
|
-- List templates owned by a specific team (NOT including platform templates).
|
||||||
|
SELECT * FROM templates WHERE team_id = $1 ORDER BY created_at DESC;
|
||||||
|
|||||||
@ -180,13 +180,13 @@ func (h *buildHandler) ListTemplates(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type templateResponse struct {
|
type templateResponse struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
VCPUs int32 `json:"vcpus"`
|
VCPUs int32 `json:"vcpus"`
|
||||||
MemoryMB int32 `json:"memory_mb"`
|
MemoryMB int32 `json:"memory_mb"`
|
||||||
SizeBytes int64 `json:"size_bytes"`
|
SizeBytes int64 `json:"size_bytes"`
|
||||||
TeamID string `json:"team_id"`
|
TeamID string `json:"team_id"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := make([]templateResponse, len(templates))
|
resp := make([]templateResponse, len(templates))
|
||||||
@ -216,7 +216,8 @@ func (h *buildHandler) DeleteTemplate(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
if _, err := h.db.GetTemplate(ctx, name); err != nil {
|
tmpl, err := h.db.GetPlatformTemplateByName(ctx, name)
|
||||||
|
if err != nil {
|
||||||
writeError(w, http.StatusNotFound, "not_found", "template not found")
|
writeError(w, http.StatusNotFound, "not_found", "template not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -231,14 +232,17 @@ func (h *buildHandler) DeleteTemplate(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, err := agent.DeleteSnapshot(ctx, connect.NewRequest(&pb.DeleteSnapshotRequest{Name: name})); err != nil {
|
if _, err := agent.DeleteSnapshot(ctx, connect.NewRequest(&pb.DeleteSnapshotRequest{
|
||||||
|
TeamId: formatUUIDForRPC(tmpl.TeamID),
|
||||||
|
TemplateId: formatUUIDForRPC(tmpl.ID),
|
||||||
|
})); err != nil {
|
||||||
if connect.CodeOf(err) != connect.CodeNotFound {
|
if connect.CodeOf(err) != connect.CodeNotFound {
|
||||||
slog.Warn("admin: failed to delete template on host", "host_id", id.FormatHostID(host.ID), "name", name, "error", err)
|
slog.Warn("admin: failed to delete template on host", "host_id", id.FormatHostID(host.ID), "name", name, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.db.DeleteTemplate(ctx, name); err != nil {
|
if err := h.db.DeleteTemplate(ctx, tmpl.ID); err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, "db_error", "failed to delete template record")
|
writeError(w, http.StatusInternalServerError, "db_error", "failed to delete template record")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import (
|
|||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/sandbox/internal/audit"
|
"git.omukk.dev/wrenn/sandbox/internal/audit"
|
||||||
"git.omukk.dev/wrenn/sandbox/internal/auth"
|
"git.omukk.dev/wrenn/sandbox/internal/auth"
|
||||||
"git.omukk.dev/wrenn/sandbox/internal/db"
|
"git.omukk.dev/wrenn/sandbox/internal/db"
|
||||||
@ -34,8 +36,8 @@ func newSnapshotHandler(svc *service.TemplateService, db *db.Queries, pool *life
|
|||||||
|
|
||||||
// deleteSnapshotBroadcast attempts to delete snapshot files on all online hosts.
|
// deleteSnapshotBroadcast attempts to delete snapshot files on all online hosts.
|
||||||
// Snapshots aren't currently host-tracked in the DB, so we broadcast to all hosts
|
// Snapshots aren't currently host-tracked in the DB, so we broadcast to all hosts
|
||||||
// and ignore NotFound errors. TODO: add host_id to templates table.
|
// and ignore NotFound errors.
|
||||||
func (h *snapshotHandler) deleteSnapshotBroadcast(ctx context.Context, name string) error {
|
func (h *snapshotHandler) deleteSnapshotBroadcast(ctx context.Context, teamID, templateID pgtype.UUID) error {
|
||||||
hosts, err := h.db.ListActiveHosts(ctx)
|
hosts, err := h.db.ListActiveHosts(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list hosts: %w", err)
|
return fmt.Errorf("list hosts: %w", err)
|
||||||
@ -48,9 +50,12 @@ func (h *snapshotHandler) deleteSnapshotBroadcast(ctx context.Context, name stri
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, err := agent.DeleteSnapshot(ctx, connect.NewRequest(&pb.DeleteSnapshotRequest{Name: name})); err != nil {
|
if _, err := agent.DeleteSnapshot(ctx, connect.NewRequest(&pb.DeleteSnapshotRequest{
|
||||||
|
TeamId: formatUUIDForRPC(teamID),
|
||||||
|
TemplateId: formatUUIDForRPC(templateID),
|
||||||
|
})); err != nil {
|
||||||
if connect.CodeOf(err) != connect.CodeNotFound {
|
if connect.CodeOf(err) != connect.CodeNotFound {
|
||||||
slog.Warn("snapshot: failed to delete on host", "host_id", id.FormatHostID(host.ID), "name", name, "error", err)
|
slog.Warn("snapshot: failed to delete on host", "host_id", id.FormatHostID(host.ID), "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -122,14 +127,20 @@ func (h *snapshotHandler) Create(w http.ResponseWriter, r *http.Request) {
|
|||||||
ac := auth.MustFromContext(ctx)
|
ac := auth.MustFromContext(ctx)
|
||||||
overwrite := r.URL.Query().Get("overwrite") == "true"
|
overwrite := r.URL.Query().Get("overwrite") == "true"
|
||||||
|
|
||||||
|
// Check for global name collision.
|
||||||
|
if _, err := h.db.GetPlatformTemplateByName(ctx, req.Name); err == nil {
|
||||||
|
writeError(w, http.StatusConflict, "name_reserved", "template name is reserved by a global template")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check if name already exists for this team.
|
// Check if name already exists for this team.
|
||||||
if _, err := h.db.GetTemplateByTeam(ctx, db.GetTemplateByTeamParams{Name: req.Name, TeamID: ac.TeamID}); err == nil {
|
if existing, err := h.db.GetTemplateByTeam(ctx, db.GetTemplateByTeamParams{Name: req.Name, TeamID: ac.TeamID}); err == nil {
|
||||||
if !overwrite {
|
if !overwrite {
|
||||||
writeError(w, http.StatusConflict, "already_exists", "snapshot name already exists; use ?overwrite=true to replace")
|
writeError(w, http.StatusConflict, "already_exists", "snapshot name already exists; use ?overwrite=true to replace")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Delete old snapshot files from all hosts before removing the DB record.
|
// Delete old snapshot files from all hosts before removing the DB record.
|
||||||
if err := h.deleteSnapshotBroadcast(ctx, req.Name); err != nil {
|
if err := h.deleteSnapshotBroadcast(ctx, existing.TeamID, existing.ID); err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, "agent_error", "failed to delete existing snapshot files")
|
writeError(w, http.StatusInternalServerError, "agent_error", "failed to delete existing snapshot files")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -174,9 +185,14 @@ func (h *snapshotHandler) Create(w http.ResponseWriter, r *http.Request) {
|
|||||||
snapCtx, snapCancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
snapCtx, snapCancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
defer snapCancel()
|
defer snapCancel()
|
||||||
|
|
||||||
|
// Generate the new template ID upfront so the host agent knows where to store files.
|
||||||
|
newTemplateID := id.NewTemplateID()
|
||||||
|
|
||||||
resp, err := agent.CreateSnapshot(snapCtx, connect.NewRequest(&pb.CreateSnapshotRequest{
|
resp, err := agent.CreateSnapshot(snapCtx, connect.NewRequest(&pb.CreateSnapshotRequest{
|
||||||
SandboxId: req.SandboxID,
|
SandboxId: req.SandboxID,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
|
TeamId: formatUUIDForRPC(ac.TeamID),
|
||||||
|
TemplateId: formatUUIDForRPC(newTemplateID),
|
||||||
}))
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Snapshot failed — revert status back to what it was.
|
// Snapshot failed — revert status back to what it was.
|
||||||
@ -193,6 +209,7 @@ func (h *snapshotHandler) Create(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tmpl, err := h.db.InsertTemplate(snapCtx, db.InsertTemplateParams{
|
tmpl, err := h.db.InsertTemplate(snapCtx, db.InsertTemplateParams{
|
||||||
|
ID: newTemplateID,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Type: "snapshot",
|
Type: "snapshot",
|
||||||
Vcpus: sb.Vcpus,
|
Vcpus: sb.Vcpus,
|
||||||
@ -255,7 +272,7 @@ func (h *snapshotHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.deleteSnapshotBroadcast(ctx, name); err != nil {
|
if err := h.deleteSnapshotBroadcast(ctx, tmpl.TeamID, tmpl.ID); err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, "agent_error", "failed to delete snapshot files")
|
writeError(w, http.StatusInternalServerError, "agent_error", "failed to delete snapshot files")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
|
"git.omukk.dev/wrenn/sandbox/internal/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
type errorResponse struct {
|
type errorResponse struct {
|
||||||
@ -35,6 +38,11 @@ func writeError(w http.ResponseWriter, status int, code, message string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formatUUIDForRPC converts a pgtype.UUID to a hex string for RPC messages.
|
||||||
|
func formatUUIDForRPC(u pgtype.UUID) string {
|
||||||
|
return id.UUIDString(u)
|
||||||
|
}
|
||||||
|
|
||||||
// agentErrToHTTP maps a Connect RPC error to an HTTP status, error code, and message.
|
// agentErrToHTTP maps a Connect RPC error to an HTTP status, error code, and message.
|
||||||
func agentErrToHTTP(err error) (int, string, string) {
|
func agentErrToHTTP(err error) (int, string, string) {
|
||||||
switch connect.CodeOf(err) {
|
switch connect.CodeOf(err) {
|
||||||
|
|||||||
@ -83,21 +83,23 @@ type OauthProvider struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Sandbox struct {
|
type Sandbox struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
TeamID pgtype.UUID `json:"team_id"`
|
TeamID pgtype.UUID `json:"team_id"`
|
||||||
HostID pgtype.UUID `json:"host_id"`
|
HostID pgtype.UUID `json:"host_id"`
|
||||||
Template string `json:"template"`
|
Template string `json:"template"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
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"`
|
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"`
|
||||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||||||
LastActiveAt pgtype.Timestamptz `json:"last_active_at"`
|
LastActiveAt pgtype.Timestamptz `json:"last_active_at"`
|
||||||
LastUpdated pgtype.Timestamptz `json:"last_updated"`
|
LastUpdated pgtype.Timestamptz `json:"last_updated"`
|
||||||
|
TemplateID pgtype.UUID `json:"template_id"`
|
||||||
|
TemplateTeamID pgtype.UUID `json:"template_team_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SandboxMetricPoint struct {
|
type SandboxMetricPoint struct {
|
||||||
@ -146,6 +148,7 @@ type Template struct {
|
|||||||
SizeBytes int64 `json:"size_bytes"`
|
SizeBytes int64 `json:"size_bytes"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
TeamID pgtype.UUID `json:"team_id"`
|
TeamID pgtype.UUID `json:"team_id"`
|
||||||
|
ID pgtype.UUID `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TemplateBuild struct {
|
type TemplateBuild struct {
|
||||||
@ -166,6 +169,8 @@ type TemplateBuild struct {
|
|||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||||||
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||||
|
TemplateID pgtype.UUID `json:"template_id"`
|
||||||
|
TeamID pgtype.UUID `json:"team_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
|
|||||||
@ -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, disk_size_mb, 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, template_id, template_team_id 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) {
|
||||||
@ -65,12 +65,14 @@ func (q *Queries) GetSandbox(ctx context.Context, id pgtype.UUID) (Sandbox, erro
|
|||||||
&i.StartedAt,
|
&i.StartedAt,
|
||||||
&i.LastActiveAt,
|
&i.LastActiveAt,
|
||||||
&i.LastUpdated,
|
&i.LastUpdated,
|
||||||
|
&i.TemplateID,
|
||||||
|
&i.TemplateTeamID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSandboxByTeam = `-- name: GetSandboxByTeam :one
|
const getSandboxByTeam = `-- name: GetSandboxByTeam :one
|
||||||
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
|
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, template_id, template_team_id FROM sandboxes WHERE id = $1 AND team_id = $2
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetSandboxByTeamParams struct {
|
type GetSandboxByTeamParams struct {
|
||||||
@ -97,26 +99,30 @@ func (q *Queries) GetSandboxByTeam(ctx context.Context, arg GetSandboxByTeamPara
|
|||||||
&i.StartedAt,
|
&i.StartedAt,
|
||||||
&i.LastActiveAt,
|
&i.LastActiveAt,
|
||||||
&i.LastUpdated,
|
&i.LastUpdated,
|
||||||
|
&i.TemplateID,
|
||||||
|
&i.TemplateTeamID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
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, disk_size_mb)
|
INSERT INTO sandboxes (id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, template_id, template_team_id)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||||
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
|
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, template_id, template_team_id
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertSandboxParams struct {
|
type InsertSandboxParams struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
TeamID pgtype.UUID `json:"team_id"`
|
TeamID pgtype.UUID `json:"team_id"`
|
||||||
HostID pgtype.UUID `json:"host_id"`
|
HostID pgtype.UUID `json:"host_id"`
|
||||||
Template string `json:"template"`
|
Template string `json:"template"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
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"`
|
DiskSizeMb int32 `json:"disk_size_mb"`
|
||||||
|
TemplateID pgtype.UUID `json:"template_id"`
|
||||||
|
TemplateTeamID pgtype.UUID `json:"template_team_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) InsertSandbox(ctx context.Context, arg InsertSandboxParams) (Sandbox, error) {
|
func (q *Queries) InsertSandbox(ctx context.Context, arg InsertSandboxParams) (Sandbox, error) {
|
||||||
@ -130,6 +136,8 @@ func (q *Queries) InsertSandbox(ctx context.Context, arg InsertSandboxParams) (S
|
|||||||
arg.MemoryMb,
|
arg.MemoryMb,
|
||||||
arg.TimeoutSec,
|
arg.TimeoutSec,
|
||||||
arg.DiskSizeMb,
|
arg.DiskSizeMb,
|
||||||
|
arg.TemplateID,
|
||||||
|
arg.TemplateTeamID,
|
||||||
)
|
)
|
||||||
var i Sandbox
|
var i Sandbox
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@ -148,12 +156,14 @@ func (q *Queries) InsertSandbox(ctx context.Context, arg InsertSandboxParams) (S
|
|||||||
&i.StartedAt,
|
&i.StartedAt,
|
||||||
&i.LastActiveAt,
|
&i.LastActiveAt,
|
||||||
&i.LastUpdated,
|
&i.LastUpdated,
|
||||||
|
&i.TemplateID,
|
||||||
|
&i.TemplateTeamID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const listActiveSandboxesByTeam = `-- name: ListActiveSandboxesByTeam :many
|
const listActiveSandboxesByTeam = `-- name: ListActiveSandboxesByTeam :many
|
||||||
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
|
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, template_id, template_team_id 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
|
||||||
`
|
`
|
||||||
@ -183,6 +193,8 @@ func (q *Queries) ListActiveSandboxesByTeam(ctx context.Context, teamID pgtype.U
|
|||||||
&i.StartedAt,
|
&i.StartedAt,
|
||||||
&i.LastActiveAt,
|
&i.LastActiveAt,
|
||||||
&i.LastUpdated,
|
&i.LastUpdated,
|
||||||
|
&i.TemplateID,
|
||||||
|
&i.TemplateTeamID,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -195,7 +207,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, disk_size_mb, 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, template_id, template_team_id 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) {
|
||||||
@ -223,6 +235,8 @@ func (q *Queries) ListSandboxes(ctx context.Context) ([]Sandbox, error) {
|
|||||||
&i.StartedAt,
|
&i.StartedAt,
|
||||||
&i.LastActiveAt,
|
&i.LastActiveAt,
|
||||||
&i.LastUpdated,
|
&i.LastUpdated,
|
||||||
|
&i.TemplateID,
|
||||||
|
&i.TemplateTeamID,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -235,7 +249,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, disk_size_mb, 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, template_id, template_team_id 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
|
||||||
`
|
`
|
||||||
@ -270,6 +284,8 @@ func (q *Queries) ListSandboxesByHostAndStatus(ctx context.Context, arg ListSand
|
|||||||
&i.StartedAt,
|
&i.StartedAt,
|
||||||
&i.LastActiveAt,
|
&i.LastActiveAt,
|
||||||
&i.LastUpdated,
|
&i.LastUpdated,
|
||||||
|
&i.TemplateID,
|
||||||
|
&i.TemplateTeamID,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -282,7 +298,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, disk_size_mb, 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, template_id, template_team_id 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
|
||||||
`
|
`
|
||||||
@ -312,6 +328,8 @@ func (q *Queries) ListSandboxesByTeam(ctx context.Context, teamID pgtype.UUID) (
|
|||||||
&i.StartedAt,
|
&i.StartedAt,
|
||||||
&i.LastActiveAt,
|
&i.LastActiveAt,
|
||||||
&i.LastUpdated,
|
&i.LastUpdated,
|
||||||
|
&i.TemplateID,
|
||||||
|
&i.TemplateTeamID,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -364,7 +382,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, disk_size_mb, 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, template_id, template_team_id
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateSandboxRunningParams struct {
|
type UpdateSandboxRunningParams struct {
|
||||||
@ -398,6 +416,8 @@ func (q *Queries) UpdateSandboxRunning(ctx context.Context, arg UpdateSandboxRun
|
|||||||
&i.StartedAt,
|
&i.StartedAt,
|
||||||
&i.LastActiveAt,
|
&i.LastActiveAt,
|
||||||
&i.LastUpdated,
|
&i.LastUpdated,
|
||||||
|
&i.TemplateID,
|
||||||
|
&i.TemplateTeamID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@ -407,7 +427,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, disk_size_mb, 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, template_id, template_team_id
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateSandboxStatusParams struct {
|
type UpdateSandboxStatusParams struct {
|
||||||
@ -434,6 +454,8 @@ func (q *Queries) UpdateSandboxStatus(ctx context.Context, arg UpdateSandboxStat
|
|||||||
&i.StartedAt,
|
&i.StartedAt,
|
||||||
&i.LastActiveAt,
|
&i.LastActiveAt,
|
||||||
&i.LastUpdated,
|
&i.LastUpdated,
|
||||||
|
&i.TemplateID,
|
||||||
|
&i.TemplateTeamID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const getTemplateBuild = `-- name: GetTemplateBuild :one
|
const getTemplateBuild = `-- name: GetTemplateBuild :one
|
||||||
SELECT id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at FROM template_builds WHERE id = $1
|
SELECT id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id FROM template_builds WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetTemplateBuild(ctx context.Context, id pgtype.UUID) (TemplateBuild, error) {
|
func (q *Queries) GetTemplateBuild(ctx context.Context, id pgtype.UUID) (TemplateBuild, error) {
|
||||||
@ -36,14 +36,16 @@ func (q *Queries) GetTemplateBuild(ctx context.Context, id pgtype.UUID) (Templat
|
|||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.StartedAt,
|
&i.StartedAt,
|
||||||
&i.CompletedAt,
|
&i.CompletedAt,
|
||||||
|
&i.TemplateID,
|
||||||
|
&i.TeamID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertTemplateBuild = `-- name: InsertTemplateBuild :one
|
const insertTemplateBuild = `-- name: InsertTemplateBuild :one
|
||||||
INSERT INTO template_builds (id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, total_steps)
|
INSERT INTO template_builds (id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, total_steps, template_id, team_id)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending', $8)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending', $8, $9, $10)
|
||||||
RETURNING id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at
|
RETURNING id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertTemplateBuildParams struct {
|
type InsertTemplateBuildParams struct {
|
||||||
@ -55,6 +57,8 @@ type InsertTemplateBuildParams struct {
|
|||||||
Vcpus int32 `json:"vcpus"`
|
Vcpus int32 `json:"vcpus"`
|
||||||
MemoryMb int32 `json:"memory_mb"`
|
MemoryMb int32 `json:"memory_mb"`
|
||||||
TotalSteps int32 `json:"total_steps"`
|
TotalSteps int32 `json:"total_steps"`
|
||||||
|
TemplateID pgtype.UUID `json:"template_id"`
|
||||||
|
TeamID pgtype.UUID `json:"team_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) InsertTemplateBuild(ctx context.Context, arg InsertTemplateBuildParams) (TemplateBuild, error) {
|
func (q *Queries) InsertTemplateBuild(ctx context.Context, arg InsertTemplateBuildParams) (TemplateBuild, error) {
|
||||||
@ -67,6 +71,8 @@ func (q *Queries) InsertTemplateBuild(ctx context.Context, arg InsertTemplateBui
|
|||||||
arg.Vcpus,
|
arg.Vcpus,
|
||||||
arg.MemoryMb,
|
arg.MemoryMb,
|
||||||
arg.TotalSteps,
|
arg.TotalSteps,
|
||||||
|
arg.TemplateID,
|
||||||
|
arg.TeamID,
|
||||||
)
|
)
|
||||||
var i TemplateBuild
|
var i TemplateBuild
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@ -87,12 +93,14 @@ func (q *Queries) InsertTemplateBuild(ctx context.Context, arg InsertTemplateBui
|
|||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.StartedAt,
|
&i.StartedAt,
|
||||||
&i.CompletedAt,
|
&i.CompletedAt,
|
||||||
|
&i.TemplateID,
|
||||||
|
&i.TeamID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const listTemplateBuilds = `-- name: ListTemplateBuilds :many
|
const listTemplateBuilds = `-- name: ListTemplateBuilds :many
|
||||||
SELECT id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at FROM template_builds ORDER BY created_at DESC
|
SELECT id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id FROM template_builds ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) ListTemplateBuilds(ctx context.Context) ([]TemplateBuild, error) {
|
func (q *Queries) ListTemplateBuilds(ctx context.Context) ([]TemplateBuild, error) {
|
||||||
@ -122,6 +130,8 @@ func (q *Queries) ListTemplateBuilds(ctx context.Context) ([]TemplateBuild, erro
|
|||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.StartedAt,
|
&i.StartedAt,
|
||||||
&i.CompletedAt,
|
&i.CompletedAt,
|
||||||
|
&i.TemplateID,
|
||||||
|
&i.TeamID,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -189,7 +199,7 @@ SET status = $2,
|
|||||||
started_at = CASE WHEN $2 = 'running' AND started_at IS NULL THEN NOW() ELSE started_at END,
|
started_at = CASE WHEN $2 = 'running' AND started_at IS NULL THEN NOW() ELSE started_at END,
|
||||||
completed_at = CASE WHEN $2 IN ('success', 'failed') THEN NOW() ELSE completed_at END
|
completed_at = CASE WHEN $2 IN ('success', 'failed') THEN NOW() ELSE completed_at END
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at
|
RETURNING id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateBuildStatusParams struct {
|
type UpdateBuildStatusParams struct {
|
||||||
@ -218,6 +228,8 @@ func (q *Queries) UpdateBuildStatus(ctx context.Context, arg UpdateBuildStatusPa
|
|||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.StartedAt,
|
&i.StartedAt,
|
||||||
&i.CompletedAt,
|
&i.CompletedAt,
|
||||||
|
&i.TemplateID,
|
||||||
|
&i.TeamID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,11 +12,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const deleteTemplate = `-- name: DeleteTemplate :exec
|
const deleteTemplate = `-- name: DeleteTemplate :exec
|
||||||
DELETE FROM templates WHERE name = $1
|
DELETE FROM templates WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) DeleteTemplate(ctx context.Context, name string) error {
|
func (q *Queries) DeleteTemplate(ctx context.Context, id pgtype.UUID) error {
|
||||||
_, err := q.db.Exec(ctx, deleteTemplate, name)
|
_, err := q.db.Exec(ctx, deleteTemplate, id)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,12 +34,23 @@ func (q *Queries) DeleteTemplateByTeam(ctx context.Context, arg DeleteTemplateBy
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTemplate = `-- name: GetTemplate :one
|
const deleteTemplatesByTeam = `-- name: DeleteTemplatesByTeam :exec
|
||||||
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id FROM templates WHERE name = $1
|
DELETE FROM templates WHERE team_id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetTemplate(ctx context.Context, name string) (Template, error) {
|
// Bulk delete all templates owned by a team (for team soft-delete cleanup).
|
||||||
row := q.db.QueryRow(ctx, getTemplate, name)
|
func (q *Queries) DeleteTemplatesByTeam(ctx context.Context, teamID pgtype.UUID) error {
|
||||||
|
_, err := q.db.Exec(ctx, deleteTemplatesByTeam, teamID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPlatformTemplateByName = `-- name: GetPlatformTemplateByName :one
|
||||||
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id FROM templates WHERE team_id = '00000000-0000-0000-0000-000000000000' AND name = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
// Check if a global (platform) template exists with the given name.
|
||||||
|
func (q *Queries) GetPlatformTemplateByName(ctx context.Context, name string) (Template, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getPlatformTemplateByName, name)
|
||||||
var i Template
|
var i Template
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.Name,
|
&i.Name,
|
||||||
@ -49,12 +60,59 @@ func (q *Queries) GetTemplate(ctx context.Context, name string) (Template, error
|
|||||||
&i.SizeBytes,
|
&i.SizeBytes,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.TeamID,
|
&i.TeamID,
|
||||||
|
&i.ID,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTemplate = `-- name: GetTemplate :one
|
||||||
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id FROM templates WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetTemplate(ctx context.Context, id pgtype.UUID) (Template, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getTemplate, id)
|
||||||
|
var i Template
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Name,
|
||||||
|
&i.Type,
|
||||||
|
&i.Vcpus,
|
||||||
|
&i.MemoryMb,
|
||||||
|
&i.SizeBytes,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.TeamID,
|
||||||
|
&i.ID,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTemplateByName = `-- name: GetTemplateByName :one
|
||||||
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id FROM templates WHERE team_id = $1 AND name = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetTemplateByNameParams struct {
|
||||||
|
TeamID pgtype.UUID `json:"team_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up a template by team_id and name (exact team match, no global fallback).
|
||||||
|
func (q *Queries) GetTemplateByName(ctx context.Context, arg GetTemplateByNameParams) (Template, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getTemplateByName, arg.TeamID, arg.Name)
|
||||||
|
var i Template
|
||||||
|
err := row.Scan(
|
||||||
|
&i.Name,
|
||||||
|
&i.Type,
|
||||||
|
&i.Vcpus,
|
||||||
|
&i.MemoryMb,
|
||||||
|
&i.SizeBytes,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.TeamID,
|
||||||
|
&i.ID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
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 OR team_id = '00000000-0000-0000-0000-000000000000')
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id FROM templates WHERE name = $1 AND (team_id = $2 OR team_id = '00000000-0000-0000-0000-000000000000')
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetTemplateByTeamParams struct {
|
type GetTemplateByTeamParams struct {
|
||||||
@ -74,17 +132,19 @@ func (q *Queries) GetTemplateByTeam(ctx context.Context, arg GetTemplateByTeamPa
|
|||||||
&i.SizeBytes,
|
&i.SizeBytes,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.TeamID,
|
&i.TeamID,
|
||||||
|
&i.ID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertTemplate = `-- name: InsertTemplate :one
|
const insertTemplate = `-- name: InsertTemplate :one
|
||||||
INSERT INTO templates (name, type, vcpus, memory_mb, size_bytes, team_id)
|
INSERT INTO templates (id, name, type, vcpus, memory_mb, size_bytes, team_id)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
RETURNING name, type, vcpus, memory_mb, size_bytes, created_at, team_id
|
RETURNING name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertTemplateParams struct {
|
type InsertTemplateParams struct {
|
||||||
|
ID pgtype.UUID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Vcpus int32 `json:"vcpus"`
|
Vcpus int32 `json:"vcpus"`
|
||||||
@ -95,6 +155,7 @@ type InsertTemplateParams struct {
|
|||||||
|
|
||||||
func (q *Queries) InsertTemplate(ctx context.Context, arg InsertTemplateParams) (Template, error) {
|
func (q *Queries) InsertTemplate(ctx context.Context, arg InsertTemplateParams) (Template, error) {
|
||||||
row := q.db.QueryRow(ctx, insertTemplate,
|
row := q.db.QueryRow(ctx, insertTemplate,
|
||||||
|
arg.ID,
|
||||||
arg.Name,
|
arg.Name,
|
||||||
arg.Type,
|
arg.Type,
|
||||||
arg.Vcpus,
|
arg.Vcpus,
|
||||||
@ -111,12 +172,13 @@ func (q *Queries) InsertTemplate(ctx context.Context, arg InsertTemplateParams)
|
|||||||
&i.SizeBytes,
|
&i.SizeBytes,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.TeamID,
|
&i.TeamID,
|
||||||
|
&i.ID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const listTemplates = `-- name: ListTemplates :many
|
const listTemplates = `-- name: ListTemplates :many
|
||||||
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id FROM templates ORDER BY created_at DESC
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id FROM templates ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) ListTemplates(ctx context.Context) ([]Template, error) {
|
func (q *Queries) ListTemplates(ctx context.Context) ([]Template, error) {
|
||||||
@ -136,6 +198,7 @@ func (q *Queries) ListTemplates(ctx context.Context) ([]Template, error) {
|
|||||||
&i.SizeBytes,
|
&i.SizeBytes,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.TeamID,
|
&i.TeamID,
|
||||||
|
&i.ID,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -148,7 +211,7 @@ 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 OR team_id = '00000000-0000-0000-0000-000000000000') ORDER BY created_at DESC
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, 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.
|
// Platform templates are visible to all teams.
|
||||||
@ -169,6 +232,7 @@ func (q *Queries) ListTemplatesByTeam(ctx context.Context, teamID pgtype.UUID) (
|
|||||||
&i.SizeBytes,
|
&i.SizeBytes,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.TeamID,
|
&i.TeamID,
|
||||||
|
&i.ID,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -181,7 +245,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 OR team_id = '00000000-0000-0000-0000-000000000000') AND type = $2 ORDER BY created_at DESC
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, 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 {
|
||||||
@ -207,6 +271,41 @@ func (q *Queries) ListTemplatesByTeamAndType(ctx context.Context, arg ListTempla
|
|||||||
&i.SizeBytes,
|
&i.SizeBytes,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.TeamID,
|
&i.TeamID,
|
||||||
|
&i.ID,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listTemplatesByTeamOnly = `-- name: ListTemplatesByTeamOnly :many
|
||||||
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id FROM templates WHERE team_id = $1 ORDER BY created_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
// List templates owned by a specific team (NOT including platform templates).
|
||||||
|
func (q *Queries) ListTemplatesByTeamOnly(ctx context.Context, teamID pgtype.UUID) ([]Template, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listTemplatesByTeamOnly, teamID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Template
|
||||||
|
for rows.Next() {
|
||||||
|
var i Template
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.Name,
|
||||||
|
&i.Type,
|
||||||
|
&i.Vcpus,
|
||||||
|
&i.MemoryMb,
|
||||||
|
&i.SizeBytes,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.TeamID,
|
||||||
|
&i.ID,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -219,7 +318,7 @@ func (q *Queries) ListTemplatesByTeamAndType(ctx context.Context, arg ListTempla
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listTemplatesByType = `-- name: ListTemplatesByType :many
|
const listTemplatesByType = `-- name: ListTemplatesByType :many
|
||||||
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id FROM templates WHERE type = $1 ORDER BY created_at DESC
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id FROM templates WHERE type = $1 ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) ListTemplatesByType(ctx context.Context, type_ string) ([]Template, error) {
|
func (q *Queries) ListTemplatesByType(ctx context.Context, type_ string) ([]Template, error) {
|
||||||
@ -239,6 +338,7 @@ func (q *Queries) ListTemplatesByType(ctx context.Context, type_ string) ([]Temp
|
|||||||
&i.SizeBytes,
|
&i.SizeBytes,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.TeamID,
|
&i.TeamID,
|
||||||
|
&i.ID,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
pb "git.omukk.dev/wrenn/sandbox/proto/hostagent/gen"
|
pb "git.omukk.dev/wrenn/sandbox/proto/hostagent/gen"
|
||||||
"git.omukk.dev/wrenn/sandbox/proto/hostagent/gen/hostagentv1connect"
|
"git.omukk.dev/wrenn/sandbox/proto/hostagent/gen/hostagentv1connect"
|
||||||
@ -33,13 +35,35 @@ func NewServer(mgr *sandbox.Manager, terminate func()) *Server {
|
|||||||
return &Server{mgr: mgr, terminate: terminate}
|
return &Server{mgr: mgr, terminate: terminate}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseUUIDString parses a UUID hex string into a pgtype.UUID.
|
||||||
|
// An empty string yields an all-zeros UUID (valid).
|
||||||
|
func parseUUIDString(s string) (pgtype.UUID, error) {
|
||||||
|
if s == "" {
|
||||||
|
return pgtype.UUID{Bytes: [16]byte{}, Valid: true}, nil
|
||||||
|
}
|
||||||
|
parsed, err := uuid.Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
return pgtype.UUID{}, fmt.Errorf("invalid UUID %q: %w", s, err)
|
||||||
|
}
|
||||||
|
return pgtype.UUID{Bytes: parsed, Valid: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) CreateSandbox(
|
func (s *Server) CreateSandbox(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req *connect.Request[pb.CreateSandboxRequest],
|
req *connect.Request[pb.CreateSandboxRequest],
|
||||||
) (*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), int(msg.DiskSizeMb))
|
teamID, err := parseUUIDString(msg.TeamId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, connect.NewError(connect.CodeInvalidArgument, err)
|
||||||
|
}
|
||||||
|
templateID, err := parseUUIDString(msg.TemplateId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, connect.NewError(connect.CodeInvalidArgument, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sb, err := s.mgr.Create(ctx, msg.SandboxId, teamID, templateID, 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))
|
||||||
}
|
}
|
||||||
@ -90,12 +114,22 @@ func (s *Server) CreateSnapshot(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req *connect.Request[pb.CreateSnapshotRequest],
|
req *connect.Request[pb.CreateSnapshotRequest],
|
||||||
) (*connect.Response[pb.CreateSnapshotResponse], error) {
|
) (*connect.Response[pb.CreateSnapshotResponse], error) {
|
||||||
sizeBytes, err := s.mgr.CreateSnapshot(ctx, req.Msg.SandboxId, req.Msg.Name)
|
msg := req.Msg
|
||||||
|
teamID, err := parseUUIDString(msg.TeamId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, connect.NewError(connect.CodeInvalidArgument, err)
|
||||||
|
}
|
||||||
|
templateID, err := parseUUIDString(msg.TemplateId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, connect.NewError(connect.CodeInvalidArgument, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sizeBytes, err := s.mgr.CreateSnapshot(ctx, msg.SandboxId, teamID, templateID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("create snapshot: %w", err))
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("create snapshot: %w", err))
|
||||||
}
|
}
|
||||||
return connect.NewResponse(&pb.CreateSnapshotResponse{
|
return connect.NewResponse(&pb.CreateSnapshotResponse{
|
||||||
Name: req.Msg.Name,
|
Name: msg.Name,
|
||||||
SizeBytes: sizeBytes,
|
SizeBytes: sizeBytes,
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
@ -104,7 +138,17 @@ func (s *Server) DeleteSnapshot(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req *connect.Request[pb.DeleteSnapshotRequest],
|
req *connect.Request[pb.DeleteSnapshotRequest],
|
||||||
) (*connect.Response[pb.DeleteSnapshotResponse], error) {
|
) (*connect.Response[pb.DeleteSnapshotResponse], error) {
|
||||||
if err := s.mgr.DeleteSnapshot(req.Msg.Name); err != nil {
|
msg := req.Msg
|
||||||
|
teamID, err := parseUUIDString(msg.TeamId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, connect.NewError(connect.CodeInvalidArgument, err)
|
||||||
|
}
|
||||||
|
templateID, err := parseUUIDString(msg.TemplateId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, connect.NewError(connect.CodeInvalidArgument, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.mgr.DeleteSnapshot(teamID, templateID); err != nil {
|
||||||
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("delete snapshot: %w", err))
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("delete snapshot: %w", err))
|
||||||
}
|
}
|
||||||
return connect.NewResponse(&pb.DeleteSnapshotResponse{}), nil
|
return connect.NewResponse(&pb.DeleteSnapshotResponse{}), nil
|
||||||
@ -114,7 +158,17 @@ func (s *Server) FlattenRootfs(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req *connect.Request[pb.FlattenRootfsRequest],
|
req *connect.Request[pb.FlattenRootfsRequest],
|
||||||
) (*connect.Response[pb.FlattenRootfsResponse], error) {
|
) (*connect.Response[pb.FlattenRootfsResponse], error) {
|
||||||
sizeBytes, err := s.mgr.FlattenRootfs(ctx, req.Msg.SandboxId, req.Msg.Name)
|
msg := req.Msg
|
||||||
|
teamID, err := parseUUIDString(msg.TeamId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, connect.NewError(connect.CodeInvalidArgument, err)
|
||||||
|
}
|
||||||
|
templateID, err := parseUUIDString(msg.TemplateId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, connect.NewError(connect.CodeInvalidArgument, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sizeBytes, err := s.mgr.FlattenRootfs(ctx, msg.SandboxId, teamID, templateID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("flatten rootfs: %w", err))
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("flatten rootfs: %w", err))
|
||||||
}
|
}
|
||||||
@ -413,7 +467,8 @@ func (s *Server) ListSandboxes(
|
|||||||
infos[i] = &pb.SandboxInfo{
|
infos[i] = &pb.SandboxInfo{
|
||||||
SandboxId: sb.ID,
|
SandboxId: sb.ID,
|
||||||
Status: string(sb.Status),
|
Status: string(sb.Status),
|
||||||
Template: sb.Template,
|
TeamId: uuid.UUID(sb.TemplateTeamID).String(),
|
||||||
|
TemplateId: uuid.UUID(sb.TemplateID).String(),
|
||||||
Vcpus: int32(sb.VCPUs),
|
Vcpus: int32(sb.VCPUs),
|
||||||
MemoryMb: int32(sb.MemoryMB),
|
MemoryMb: int32(sb.MemoryMB),
|
||||||
HostIp: sb.HostIP.String(),
|
HostIp: sb.HostIP.String(),
|
||||||
|
|||||||
@ -36,8 +36,9 @@ func NewAuditLogID() pgtype.UUID { return newUUID() }
|
|||||||
func NewBuildID() pgtype.UUID { return newUUID() }
|
func NewBuildID() pgtype.UUID { return newUUID() }
|
||||||
func NewAdminPermissionID() pgtype.UUID { return newUUID() }
|
func NewAdminPermissionID() pgtype.UUID { return newUUID() }
|
||||||
|
|
||||||
|
func NewTemplateID() pgtype.UUID { return newUUID() }
|
||||||
|
|
||||||
// NewSnapshotName generates a snapshot name: "template-" + 8 hex chars.
|
// NewSnapshotName generates a snapshot name: "template-" + 8 hex chars.
|
||||||
// Templates use TEXT primary keys (not UUID), so this stays as a string.
|
|
||||||
func NewSnapshotName() string {
|
func NewSnapshotName() string {
|
||||||
return "template-" + hex8()
|
return "template-" + hex8()
|
||||||
}
|
}
|
||||||
@ -76,8 +77,8 @@ const (
|
|||||||
PrefixAdminPermission = "perm-"
|
PrefixAdminPermission = "perm-"
|
||||||
)
|
)
|
||||||
|
|
||||||
// uuidToBase36 encodes 16 UUID bytes as a 25-char base36 string (0-9a-z).
|
// UUIDToBase36 encodes 16 UUID bytes as a 25-char base36 string (0-9a-z).
|
||||||
func uuidToBase36(b [16]byte) string {
|
func UUIDToBase36(b [16]byte) string {
|
||||||
n := new(big.Int).SetBytes(b[:])
|
n := new(big.Int).SetBytes(b[:])
|
||||||
buf := make([]byte, base36IDLen)
|
buf := make([]byte, base36IDLen)
|
||||||
mod := new(big.Int)
|
mod := new(big.Int)
|
||||||
@ -110,7 +111,7 @@ func base36ToUUID(s string) ([16]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func formatUUID(prefix string, id pgtype.UUID) string {
|
func formatUUID(prefix string, id pgtype.UUID) string {
|
||||||
return prefix + uuidToBase36(id.Bytes)
|
return prefix + UUIDToBase36(id.Bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FormatSandboxID(id pgtype.UUID) string { return formatUUID(PrefixSandbox, id) }
|
func FormatSandboxID(id pgtype.UUID) string { return formatUUID(PrefixSandbox, id) }
|
||||||
@ -151,6 +152,17 @@ func ParseBuildID(s string) (pgtype.UUID, error) { return parseUUID(PrefixBu
|
|||||||
// (e.g. base templates, shared infrastructure).
|
// (e.g. base templates, shared infrastructure).
|
||||||
var PlatformTeamID = pgtype.UUID{Bytes: [16]byte{}, Valid: true}
|
var PlatformTeamID = pgtype.UUID{Bytes: [16]byte{}, Valid: true}
|
||||||
|
|
||||||
|
// MinimalTemplateID is the all-zeros UUID sentinel for the built-in "minimal"
|
||||||
|
// template. When both team_id and template_id are zero, the host agent uses
|
||||||
|
// the minimal rootfs at WRENN_DIR/images/minimal/.
|
||||||
|
var MinimalTemplateID = pgtype.UUID{Bytes: [16]byte{}, Valid: true}
|
||||||
|
|
||||||
|
// UUIDString converts a pgtype.UUID to a standard hyphenated UUID string
|
||||||
|
// (e.g., "6ba7b810-9dad-11d1-80b4-00c04fd430c8"). Used for RPC wire format.
|
||||||
|
func UUIDString(id pgtype.UUID) string {
|
||||||
|
return uuid.UUID(id.Bytes).String()
|
||||||
|
}
|
||||||
|
|
||||||
// --- Helpers ---
|
// --- Helpers ---
|
||||||
|
|
||||||
func hex8() string {
|
func hex8() string {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import (
|
|||||||
func TestBase36RoundTrip(t *testing.T) {
|
func TestBase36RoundTrip(t *testing.T) {
|
||||||
for i := 0; i < 1000; i++ {
|
for i := 0; i < 1000; i++ {
|
||||||
orig := uuid.New()
|
orig := uuid.New()
|
||||||
encoded := uuidToBase36(orig)
|
encoded := UUIDToBase36(orig)
|
||||||
|
|
||||||
if len(encoded) != base36IDLen {
|
if len(encoded) != base36IDLen {
|
||||||
t.Fatalf("expected %d chars, got %d: %s", base36IDLen, len(encoded), encoded)
|
t.Fatalf("expected %d chars, got %d: %s", base36IDLen, len(encoded), encoded)
|
||||||
@ -29,7 +29,7 @@ func TestBase36RoundTrip(t *testing.T) {
|
|||||||
|
|
||||||
func TestBase36ZeroUUID(t *testing.T) {
|
func TestBase36ZeroUUID(t *testing.T) {
|
||||||
var zero [16]byte
|
var zero [16]byte
|
||||||
encoded := uuidToBase36(zero)
|
encoded := UUIDToBase36(zero)
|
||||||
if encoded != "0000000000000000000000000" {
|
if encoded != "0000000000000000000000000" {
|
||||||
t.Fatalf("zero UUID should encode to all zeros, got %s", encoded)
|
t.Fatalf("zero UUID should encode to all zeros, got %s", encoded)
|
||||||
}
|
}
|
||||||
@ -87,7 +87,7 @@ func TestPlatformTeamIDFormats(t *testing.T) {
|
|||||||
func TestMaxUUID(t *testing.T) {
|
func TestMaxUUID(t *testing.T) {
|
||||||
max := [16]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
max := [16]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
|
||||||
encoded := uuidToBase36(max)
|
encoded := UUIDToBase36(max)
|
||||||
if len(encoded) != base36IDLen {
|
if len(encoded) != base36IDLen {
|
||||||
t.Fatalf("max UUID encoding wrong length: %d", len(encoded))
|
t.Fatalf("max UUID encoding wrong length: %d", len(encoded))
|
||||||
}
|
}
|
||||||
|
|||||||
58
internal/layout/layout.go
Normal file
58
internal/layout/layout.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package layout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
|
"git.omukk.dev/wrenn/sandbox/internal/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsMinimal reports whether the given team and template IDs represent the
|
||||||
|
// built-in "minimal" template (both all-zeros).
|
||||||
|
func IsMinimal(teamID, templateID pgtype.UUID) bool {
|
||||||
|
return teamID.Bytes == id.PlatformTeamID.Bytes && templateID.Bytes == id.MinimalTemplateID.Bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateDir returns the on-disk directory for a template.
|
||||||
|
//
|
||||||
|
// minimal (zeros, zeros): {wrennDir}/images/minimal
|
||||||
|
// all others: {wrennDir}/images/teams/{base36(teamID)}/{base36(templateID)}
|
||||||
|
func TemplateDir(wrennDir string, teamID, templateID pgtype.UUID) string {
|
||||||
|
if IsMinimal(teamID, templateID) {
|
||||||
|
return filepath.Join(wrennDir, "images", "minimal")
|
||||||
|
}
|
||||||
|
return filepath.Join(wrennDir, "images", "teams",
|
||||||
|
id.UUIDToBase36(teamID.Bytes),
|
||||||
|
id.UUIDToBase36(templateID.Bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateRootfs returns the path to a template's rootfs.ext4.
|
||||||
|
func TemplateRootfs(wrennDir string, teamID, templateID pgtype.UUID) string {
|
||||||
|
return filepath.Join(TemplateDir(wrennDir, teamID, templateID), "rootfs.ext4")
|
||||||
|
}
|
||||||
|
|
||||||
|
// PauseSnapshotDir returns the directory for a paused sandbox's snapshot files.
|
||||||
|
func PauseSnapshotDir(wrennDir, sandboxID string) string {
|
||||||
|
return filepath.Join(wrennDir, "snapshots", sandboxID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SandboxesDir returns the directory for running sandbox CoW files.
|
||||||
|
func SandboxesDir(wrennDir string) string {
|
||||||
|
return filepath.Join(wrennDir, "sandboxes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// KernelPath returns the path to the Firecracker kernel.
|
||||||
|
func KernelPath(wrennDir string) string {
|
||||||
|
return filepath.Join(wrennDir, "kernels", "vmlinux")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImagesRoot returns the root images directory.
|
||||||
|
func ImagesRoot(wrennDir string) string {
|
||||||
|
return filepath.Join(wrennDir, "images")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamsDir returns the directory containing all team template subdirectories.
|
||||||
|
func TeamsDir(wrennDir string) string {
|
||||||
|
return filepath.Join(wrennDir, "images", "teams")
|
||||||
|
}
|
||||||
120
internal/layout/layout_test.go
Normal file
120
internal/layout/layout_test.go
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package layout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
|
"git.omukk.dev/wrenn/sandbox/internal/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsMinimal(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
teamID pgtype.UUID
|
||||||
|
templateID pgtype.UUID
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "both zeros",
|
||||||
|
teamID: id.PlatformTeamID,
|
||||||
|
templateID: id.MinimalTemplateID,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-zero team",
|
||||||
|
teamID: pgtype.UUID{Bytes: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Valid: true},
|
||||||
|
templateID: id.MinimalTemplateID,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-zero template",
|
||||||
|
teamID: id.PlatformTeamID,
|
||||||
|
templateID: pgtype.UUID{Bytes: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Valid: true},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both non-zero",
|
||||||
|
teamID: pgtype.UUID{Bytes: [16]byte{1}, Valid: true},
|
||||||
|
templateID: pgtype.UUID{Bytes: [16]byte{2}, Valid: true},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := IsMinimal(tt.teamID, tt.templateID); got != tt.want {
|
||||||
|
t.Errorf("IsMinimal() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateDir(t *testing.T) {
|
||||||
|
wrennDir := "/var/lib/wrenn"
|
||||||
|
|
||||||
|
t.Run("minimal", func(t *testing.T) {
|
||||||
|
got := TemplateDir(wrennDir, id.PlatformTeamID, id.MinimalTemplateID)
|
||||||
|
want := filepath.Join(wrennDir, "images", "minimal")
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("TemplateDir() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("team template", func(t *testing.T) {
|
||||||
|
teamID := pgtype.UUID{Bytes: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Valid: true}
|
||||||
|
tmplID := pgtype.UUID{Bytes: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}, Valid: true}
|
||||||
|
got := TemplateDir(wrennDir, teamID, tmplID)
|
||||||
|
want := filepath.Join(wrennDir, "images", "teams",
|
||||||
|
id.UUIDToBase36(teamID.Bytes),
|
||||||
|
id.UUIDToBase36(tmplID.Bytes))
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("TemplateDir() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("global template (platform team, non-zero template)", func(t *testing.T) {
|
||||||
|
tmplID := pgtype.UUID{Bytes: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5}, Valid: true}
|
||||||
|
got := TemplateDir(wrennDir, id.PlatformTeamID, tmplID)
|
||||||
|
want := filepath.Join(wrennDir, "images", "teams",
|
||||||
|
id.UUIDToBase36(id.PlatformTeamID.Bytes),
|
||||||
|
id.UUIDToBase36(tmplID.Bytes))
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("TemplateDir() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateRootfs(t *testing.T) {
|
||||||
|
wrennDir := "/var/lib/wrenn"
|
||||||
|
got := TemplateRootfs(wrennDir, id.PlatformTeamID, id.MinimalTemplateID)
|
||||||
|
want := filepath.Join(wrennDir, "images", "minimal", "rootfs.ext4")
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("TemplateRootfs() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPauseSnapshotDir(t *testing.T) {
|
||||||
|
got := PauseSnapshotDir("/var/lib/wrenn", "sb-abc123")
|
||||||
|
want := "/var/lib/wrenn/snapshots/sb-abc123"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("PauseSnapshotDir() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSandboxesDir(t *testing.T) {
|
||||||
|
got := SandboxesDir("/var/lib/wrenn")
|
||||||
|
want := "/var/lib/wrenn/sandboxes"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("SandboxesDir() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKernelPath(t *testing.T) {
|
||||||
|
got := KernelPath("/var/lib/wrenn")
|
||||||
|
want := "/var/lib/wrenn/kernels/vmlinux"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("KernelPath() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,15 +18,16 @@ const (
|
|||||||
|
|
||||||
// Sandbox holds all state for a running sandbox on this host.
|
// Sandbox holds all state for a running sandbox on this host.
|
||||||
type Sandbox struct {
|
type Sandbox struct {
|
||||||
ID string
|
ID string
|
||||||
Status SandboxStatus
|
Status SandboxStatus
|
||||||
Template string
|
TemplateTeamID [16]byte
|
||||||
VCPUs int
|
TemplateID [16]byte
|
||||||
MemoryMB int
|
VCPUs int
|
||||||
TimeoutSec int
|
MemoryMB int
|
||||||
SlotIndex int
|
TimeoutSec int
|
||||||
HostIP net.IP
|
SlotIndex int
|
||||||
RootfsPath string
|
HostIP net.IP
|
||||||
CreatedAt time.Time
|
RootfsPath string
|
||||||
LastActiveAt time.Time
|
CreatedAt time.Time
|
||||||
|
LastActiveAt time.Time
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.omukk.dev/wrenn/sandbox/internal/id"
|
||||||
|
"git.omukk.dev/wrenn/sandbox/internal/layout"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultDiskSizeMB is the standard disk size for base images. Images smaller
|
// DefaultDiskSizeMB is the standard disk size for base images. Images smaller
|
||||||
@ -14,61 +17,90 @@ import (
|
|||||||
// changes; no physical disk is consumed beyond the original content.
|
// changes; no physical disk is consumed beyond the original content.
|
||||||
const DefaultDiskSizeMB = 5120 // 5 GB
|
const DefaultDiskSizeMB = 5120 // 5 GB
|
||||||
|
|
||||||
// EnsureImageSizes walks the images directory and expands any rootfs.ext4 that
|
// EnsureImageSizes walks template directories and expands any rootfs.ext4 that
|
||||||
// is smaller than the target size. This is idempotent: images already at or
|
// 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
|
// above the target size are left untouched. Should be called once at host agent
|
||||||
// startup before any sandboxes are created.
|
// startup before any sandboxes are created.
|
||||||
func EnsureImageSizes(imagesDir string, targetMB int) error {
|
func EnsureImageSizes(wrennDir string, targetMB int) error {
|
||||||
if targetMB <= 0 {
|
if targetMB <= 0 {
|
||||||
targetMB = DefaultDiskSizeMB
|
targetMB = DefaultDiskSizeMB
|
||||||
}
|
}
|
||||||
targetBytes := int64(targetMB) * 1024 * 1024
|
targetBytes := int64(targetMB) * 1024 * 1024
|
||||||
|
|
||||||
entries, err := os.ReadDir(imagesDir)
|
// Expand the built-in minimal image.
|
||||||
if err != nil {
|
minimalRootfs := layout.TemplateRootfs(wrennDir, id.PlatformTeamID, id.MinimalTemplateID)
|
||||||
return fmt.Errorf("read images dir: %w", err)
|
if err := expandImage(minimalRootfs, targetBytes, targetMB); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, entry := range entries {
|
// Walk teams/{teamDir}/{templateDir}/rootfs.ext4 two levels deep.
|
||||||
if !entry.IsDir() {
|
teamsDir := layout.TeamsDir(wrennDir)
|
||||||
|
teamEntries, err := os.ReadDir(teamsDir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil // teams dir doesn't exist yet — nothing to expand
|
||||||
|
}
|
||||||
|
return fmt.Errorf("read teams dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, teamEntry := range teamEntries {
|
||||||
|
if !teamEntry.IsDir() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rootfs := filepath.Join(imagesDir, entry.Name(), "rootfs.ext4")
|
teamPath := filepath.Join(teamsDir, teamEntry.Name())
|
||||||
info, err := os.Stat(rootfs)
|
templateEntries, err := os.ReadDir(teamPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue // not every template dir has a rootfs.ext4
|
continue
|
||||||
}
|
}
|
||||||
|
for _, tmplEntry := range templateEntries {
|
||||||
if info.Size() >= targetBytes {
|
if !tmplEntry.IsDir() {
|
||||||
continue // already large enough
|
continue
|
||||||
}
|
}
|
||||||
|
rootfs := filepath.Join(teamPath, tmplEntry.Name(), "rootfs.ext4")
|
||||||
slog.Info("expanding base image",
|
if err := expandImage(rootfs, targetBytes, targetMB); err != nil {
|
||||||
"template", entry.Name(),
|
return err
|
||||||
"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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// expandImage expands a single rootfs image if it is smaller than targetBytes.
|
||||||
|
func expandImage(rootfs string, targetBytes int64, targetMB int) error {
|
||||||
|
info, err := os.Stat(rootfs)
|
||||||
|
if err != nil {
|
||||||
|
return nil // not every template dir has a rootfs.ext4
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Size() >= targetBytes {
|
||||||
|
return nil // already large enough
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("expanding base image",
|
||||||
|
"path", rootfs,
|
||||||
|
"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", "path", rootfs, "size_mb", targetMB)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -11,25 +11,23 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/sandbox/internal/devicemapper"
|
"git.omukk.dev/wrenn/sandbox/internal/devicemapper"
|
||||||
"git.omukk.dev/wrenn/sandbox/internal/envdclient"
|
"git.omukk.dev/wrenn/sandbox/internal/envdclient"
|
||||||
"git.omukk.dev/wrenn/sandbox/internal/id"
|
"git.omukk.dev/wrenn/sandbox/internal/id"
|
||||||
|
"git.omukk.dev/wrenn/sandbox/internal/layout"
|
||||||
"git.omukk.dev/wrenn/sandbox/internal/models"
|
"git.omukk.dev/wrenn/sandbox/internal/models"
|
||||||
"git.omukk.dev/wrenn/sandbox/internal/network"
|
"git.omukk.dev/wrenn/sandbox/internal/network"
|
||||||
"git.omukk.dev/wrenn/sandbox/internal/snapshot"
|
"git.omukk.dev/wrenn/sandbox/internal/snapshot"
|
||||||
"git.omukk.dev/wrenn/sandbox/internal/uffd"
|
"git.omukk.dev/wrenn/sandbox/internal/uffd"
|
||||||
"git.omukk.dev/wrenn/sandbox/internal/validate"
|
|
||||||
"git.omukk.dev/wrenn/sandbox/internal/vm"
|
"git.omukk.dev/wrenn/sandbox/internal/vm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config holds the paths and defaults for the sandbox manager.
|
// Config holds the paths and defaults for the sandbox manager.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
KernelPath string
|
WrennDir string // root directory (e.g. /var/lib/wrenn); all sub-paths derived via layout package
|
||||||
ImagesDir string // directory containing template images (e.g., /var/lib/wrenn/images/{name}/rootfs.ext4)
|
EnvdTimeout time.Duration
|
||||||
SandboxesDir string // directory for per-sandbox rootfs clones (e.g., /var/lib/wrenn/sandboxes)
|
|
||||||
SnapshotsDir string // directory for pause snapshots (e.g., /var/lib/wrenn/snapshots/{sandbox-id}/)
|
|
||||||
EnvdTimeout time.Duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manager orchestrates sandbox lifecycle: VM, network, filesystem, envd.
|
// Manager orchestrates sandbox lifecycle: VM, network, filesystem, envd.
|
||||||
@ -52,8 +50,8 @@ type sandboxState struct {
|
|||||||
slot *network.Slot
|
slot *network.Slot
|
||||||
client *envdclient.Client
|
client *envdclient.Client
|
||||||
uffdSocketPath string // non-empty for sandboxes restored from snapshot
|
uffdSocketPath string // non-empty for sandboxes restored from snapshot
|
||||||
dmDevice *devicemapper.SnapshotDevice
|
dmDevice *devicemapper.SnapshotDevice
|
||||||
baseImagePath string // path to the base template rootfs (for loop registry release)
|
baseImagePath string // path to the base template rootfs (for loop registry release)
|
||||||
|
|
||||||
// parent holds the snapshot header and diff file paths from which this
|
// parent holds the snapshot header and diff file paths from which this
|
||||||
// sandbox was restored. Non-nil means re-pause should use "Diff" snapshot
|
// sandbox was restored. Non-nil means re-pause should use "Diff" snapshot
|
||||||
@ -95,7 +93,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, diskSizeMB int) (*models.Sandbox, error) {
|
func (m *Manager) Create(ctx context.Context, sandboxID string, teamID, templateID pgtype.UUID, vcpus, memoryMB, timeoutSec, diskSizeMB int) (*models.Sandbox, error) {
|
||||||
if sandboxID == "" {
|
if sandboxID == "" {
|
||||||
sandboxID = id.FormatSandboxID(id.NewSandboxID())
|
sandboxID = id.FormatSandboxID(id.NewSandboxID())
|
||||||
}
|
}
|
||||||
@ -110,20 +108,14 @@ func (m *Manager) Create(ctx context.Context, sandboxID, template string, vcpus,
|
|||||||
diskSizeMB = 5120 // 5 GB default
|
diskSizeMB = 5120 // 5 GB default
|
||||||
}
|
}
|
||||||
|
|
||||||
if template == "" {
|
|
||||||
template = "minimal"
|
|
||||||
}
|
|
||||||
if err := validate.SafeName(template); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid template name: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
tmplDir := layout.TemplateDir(m.cfg.WrennDir, teamID, templateID)
|
||||||
return m.createFromSnapshot(ctx, sandboxID, template, vcpus, memoryMB, timeoutSec, diskSizeMB)
|
if _, err := os.Stat(filepath.Join(tmplDir, snapshot.SnapFileName)); err == nil {
|
||||||
|
return m.createFromSnapshot(ctx, sandboxID, teamID, templateID, vcpus, memoryMB, timeoutSec, diskSizeMB)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve base rootfs image: /var/lib/wrenn/images/{template}/rootfs.ext4
|
// Resolve base rootfs image.
|
||||||
baseRootfs := filepath.Join(m.cfg.ImagesDir, template, "rootfs.ext4")
|
baseRootfs := layout.TemplateRootfs(m.cfg.WrennDir, teamID, templateID)
|
||||||
if _, err := os.Stat(baseRootfs); err != nil {
|
if _, err := os.Stat(baseRootfs); err != nil {
|
||||||
return nil, fmt.Errorf("base rootfs not found at %s: %w", baseRootfs, err)
|
return nil, fmt.Errorf("base rootfs not found at %s: %w", baseRootfs, err)
|
||||||
}
|
}
|
||||||
@ -142,7 +134,7 @@ 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(layout.SandboxesDir(m.cfg.WrennDir), fmt.Sprintf("%s.cow", sandboxID))
|
||||||
cowSize := int64(diskSizeMB) * 1024 * 1024
|
cowSize := int64(diskSizeMB) * 1024 * 1024
|
||||||
dmDev, err := devicemapper.CreateSnapshot(dmName, originLoop, cowPath, originSize, cowSize)
|
dmDev, err := devicemapper.CreateSnapshot(dmName, originLoop, cowPath, originSize, cowSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -172,7 +164,7 @@ func (m *Manager) Create(ctx context.Context, sandboxID, template string, vcpus,
|
|||||||
// Boot VM — Firecracker gets the dm device path.
|
// Boot VM — Firecracker gets the dm device path.
|
||||||
vmCfg := vm.VMConfig{
|
vmCfg := vm.VMConfig{
|
||||||
SandboxID: sandboxID,
|
SandboxID: sandboxID,
|
||||||
KernelPath: m.cfg.KernelPath,
|
KernelPath: layout.KernelPath(m.cfg.WrennDir),
|
||||||
RootfsPath: dmDev.DevicePath,
|
RootfsPath: dmDev.DevicePath,
|
||||||
VCPUs: vcpus,
|
VCPUs: vcpus,
|
||||||
MemoryMB: memoryMB,
|
MemoryMB: memoryMB,
|
||||||
@ -211,17 +203,18 @@ func (m *Manager) Create(ctx context.Context, sandboxID, template string, vcpus,
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
sb := &sandboxState{
|
sb := &sandboxState{
|
||||||
Sandbox: models.Sandbox{
|
Sandbox: models.Sandbox{
|
||||||
ID: sandboxID,
|
ID: sandboxID,
|
||||||
Status: models.StatusRunning,
|
Status: models.StatusRunning,
|
||||||
Template: template,
|
TemplateTeamID: teamID.Bytes,
|
||||||
VCPUs: vcpus,
|
TemplateID: templateID.Bytes,
|
||||||
MemoryMB: memoryMB,
|
VCPUs: vcpus,
|
||||||
TimeoutSec: timeoutSec,
|
MemoryMB: memoryMB,
|
||||||
SlotIndex: slotIdx,
|
TimeoutSec: timeoutSec,
|
||||||
HostIP: slot.HostIP,
|
SlotIndex: slotIdx,
|
||||||
RootfsPath: dmDev.DevicePath,
|
HostIP: slot.HostIP,
|
||||||
CreatedAt: now,
|
RootfsPath: dmDev.DevicePath,
|
||||||
LastActiveAt: now,
|
CreatedAt: now,
|
||||||
|
LastActiveAt: now,
|
||||||
},
|
},
|
||||||
slot: slot,
|
slot: slot,
|
||||||
client: client,
|
client: client,
|
||||||
@ -237,7 +230,8 @@ func (m *Manager) Create(ctx context.Context, sandboxID, template string, vcpus,
|
|||||||
|
|
||||||
slog.Info("sandbox created",
|
slog.Info("sandbox created",
|
||||||
"id", sandboxID,
|
"id", sandboxID,
|
||||||
"template", template,
|
"team_id", teamID,
|
||||||
|
"template_id", templateID,
|
||||||
"host_ip", slot.HostIP.String(),
|
"host_ip", slot.HostIP.String(),
|
||||||
"dm_device", dmDev.DevicePath,
|
"dm_device", dmDev.DevicePath,
|
||||||
)
|
)
|
||||||
@ -260,7 +254,9 @@ func (m *Manager) Destroy(ctx context.Context, sandboxID string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Always clean up pause snapshot files (may exist if sandbox was paused).
|
// Always clean up pause snapshot files (may exist if sandbox was paused).
|
||||||
warnErr("snapshot cleanup error", sandboxID, snapshot.Remove(m.cfg.SnapshotsDir, sandboxID))
|
if err := os.RemoveAll(layout.PauseSnapshotDir(m.cfg.WrennDir, sandboxID)); err != nil {
|
||||||
|
slog.Warn("snapshot cleanup error", "id", sandboxID, "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
slog.Info("sandbox destroyed", "id", sandboxID)
|
slog.Info("sandbox destroyed", "id", sandboxID)
|
||||||
return nil
|
return nil
|
||||||
@ -331,18 +327,18 @@ func (m *Manager) Pause(ctx context.Context, sandboxID string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Take VM state snapshot (snapfile + memfile).
|
// Step 2: Take VM state snapshot (snapfile + memfile).
|
||||||
if err := snapshot.EnsureDir(m.cfg.SnapshotsDir, sandboxID); err != nil {
|
pauseDir := layout.PauseSnapshotDir(m.cfg.WrennDir, sandboxID)
|
||||||
|
if err := os.MkdirAll(pauseDir, 0755); err != nil {
|
||||||
resumeOnError()
|
resumeOnError()
|
||||||
return fmt.Errorf("create snapshot dir: %w", err)
|
return fmt.Errorf("create snapshot dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
snapDir := snapshot.DirPath(m.cfg.SnapshotsDir, sandboxID)
|
rawMemPath := filepath.Join(pauseDir, "memfile.raw")
|
||||||
rawMemPath := filepath.Join(snapDir, "memfile.raw")
|
snapPath := filepath.Join(pauseDir, snapshot.SnapFileName)
|
||||||
snapPath := snapshot.SnapPath(m.cfg.SnapshotsDir, sandboxID)
|
|
||||||
|
|
||||||
snapshotStart := time.Now()
|
snapshotStart := time.Now()
|
||||||
if err := m.vm.Snapshot(ctx, sandboxID, snapPath, rawMemPath, snapshotType); err != nil {
|
if err := m.vm.Snapshot(ctx, sandboxID, snapPath, rawMemPath, snapshotType); err != nil {
|
||||||
warnErr("snapshot dir cleanup error", sandboxID, snapshot.Remove(m.cfg.SnapshotsDir, sandboxID))
|
warnErr("snapshot dir cleanup error", sandboxID, os.RemoveAll(pauseDir))
|
||||||
resumeOnError()
|
resumeOnError()
|
||||||
return fmt.Errorf("create VM snapshot: %w", err)
|
return fmt.Errorf("create VM snapshot: %w", err)
|
||||||
}
|
}
|
||||||
@ -350,24 +346,24 @@ func (m *Manager) Pause(ctx context.Context, sandboxID string) error {
|
|||||||
|
|
||||||
// Step 3: Process the raw memfile into a compact diff + header.
|
// Step 3: Process the raw memfile into a compact diff + header.
|
||||||
buildID := uuid.New()
|
buildID := uuid.New()
|
||||||
headerPath := snapshot.MemHeaderPath(m.cfg.SnapshotsDir, sandboxID)
|
headerPath := filepath.Join(pauseDir, snapshot.MemHeaderName)
|
||||||
|
|
||||||
processStart := time.Now()
|
processStart := time.Now()
|
||||||
if sb.parent != nil && snapshotType == "Diff" {
|
if sb.parent != nil && snapshotType == "Diff" {
|
||||||
// Diff: process against parent header, producing only changed blocks.
|
// Diff: process against parent header, producing only changed blocks.
|
||||||
diffPath := snapshot.MemDiffPathForBuild(m.cfg.SnapshotsDir, sandboxID, buildID)
|
diffPath := snapshot.MemDiffPathForBuild(pauseDir, "", buildID)
|
||||||
if _, err := snapshot.ProcessMemfileWithParent(rawMemPath, diffPath, headerPath, sb.parent.header, buildID); err != nil {
|
if _, err := snapshot.ProcessMemfileWithParent(rawMemPath, diffPath, headerPath, sb.parent.header, buildID); err != nil {
|
||||||
warnErr("snapshot dir cleanup error", sandboxID, snapshot.Remove(m.cfg.SnapshotsDir, sandboxID))
|
warnErr("snapshot dir cleanup error", sandboxID, os.RemoveAll(pauseDir))
|
||||||
resumeOnError()
|
resumeOnError()
|
||||||
return fmt.Errorf("process memfile with parent: %w", err)
|
return fmt.Errorf("process memfile with parent: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy previous generation diff files into the snapshot directory.
|
// Copy previous generation diff files into the snapshot directory.
|
||||||
for prevBuildID, prevPath := range sb.parent.diffPaths {
|
for prevBuildID, prevPath := range sb.parent.diffPaths {
|
||||||
dstPath := snapshot.MemDiffPathForBuild(m.cfg.SnapshotsDir, sandboxID, uuid.MustParse(prevBuildID))
|
dstPath := snapshot.MemDiffPathForBuild(pauseDir, "", uuid.MustParse(prevBuildID))
|
||||||
if prevPath != dstPath {
|
if prevPath != dstPath {
|
||||||
if err := copyFile(prevPath, dstPath); err != nil {
|
if err := copyFile(prevPath, dstPath); err != nil {
|
||||||
warnErr("snapshot dir cleanup error", sandboxID, snapshot.Remove(m.cfg.SnapshotsDir, sandboxID))
|
warnErr("snapshot dir cleanup error", sandboxID, os.RemoveAll(pauseDir))
|
||||||
resumeOnError()
|
resumeOnError()
|
||||||
return fmt.Errorf("copy parent diff file: %w", err)
|
return fmt.Errorf("copy parent diff file: %w", err)
|
||||||
}
|
}
|
||||||
@ -375,9 +371,9 @@ func (m *Manager) Pause(ctx context.Context, sandboxID string) error {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Full: first generation or generation cap reached — single diff file.
|
// Full: first generation or generation cap reached — single diff file.
|
||||||
diffPath := snapshot.MemDiffPath(m.cfg.SnapshotsDir, sandboxID)
|
diffPath := snapshot.MemDiffPath(pauseDir, "")
|
||||||
if _, err := snapshot.ProcessMemfile(rawMemPath, diffPath, headerPath, buildID); err != nil {
|
if _, err := snapshot.ProcessMemfile(rawMemPath, diffPath, headerPath, buildID); err != nil {
|
||||||
warnErr("snapshot dir cleanup error", sandboxID, snapshot.Remove(m.cfg.SnapshotsDir, sandboxID))
|
warnErr("snapshot dir cleanup error", sandboxID, os.RemoveAll(pauseDir))
|
||||||
resumeOnError()
|
resumeOnError()
|
||||||
return fmt.Errorf("process memfile: %w", err)
|
return fmt.Errorf("process memfile: %w", err)
|
||||||
}
|
}
|
||||||
@ -407,7 +403,7 @@ func (m *Manager) Pause(ctx context.Context, sandboxID string) error {
|
|||||||
if sb.uffdSocketPath != "" {
|
if sb.uffdSocketPath != "" {
|
||||||
os.Remove(sb.uffdSocketPath)
|
os.Remove(sb.uffdSocketPath)
|
||||||
}
|
}
|
||||||
warnErr("snapshot dir cleanup error", sandboxID, snapshot.Remove(m.cfg.SnapshotsDir, sandboxID))
|
warnErr("snapshot dir cleanup error", sandboxID, os.RemoveAll(pauseDir))
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
delete(m.boxes, sandboxID)
|
delete(m.boxes, sandboxID)
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
@ -415,9 +411,9 @@ func (m *Manager) Pause(ctx context.Context, sandboxID string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Move (not copy) the CoW file into the snapshot directory.
|
// Move (not copy) the CoW file into the snapshot directory.
|
||||||
snapshotCow := snapshot.CowPath(m.cfg.SnapshotsDir, sandboxID)
|
snapshotCow := snapshot.CowPath(pauseDir, "")
|
||||||
if err := os.Rename(sb.dmDevice.CowPath, snapshotCow); err != nil {
|
if err := os.Rename(sb.dmDevice.CowPath, snapshotCow); err != nil {
|
||||||
warnErr("snapshot dir cleanup error", sandboxID, snapshot.Remove(m.cfg.SnapshotsDir, sandboxID))
|
warnErr("snapshot dir cleanup error", sandboxID, os.RemoveAll(pauseDir))
|
||||||
// VM and dm-snapshot are already gone — clean up remaining resources.
|
// VM and dm-snapshot are already gone — clean up remaining resources.
|
||||||
warnErr("network cleanup error during pause", sandboxID, network.RemoveNetwork(sb.slot))
|
warnErr("network cleanup error during pause", sandboxID, network.RemoveNetwork(sb.slot))
|
||||||
m.slots.Release(sb.SlotIndex)
|
m.slots.Release(sb.SlotIndex)
|
||||||
@ -434,10 +430,10 @@ func (m *Manager) Pause(ctx context.Context, sandboxID string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Record which base template this CoW was built against.
|
// Record which base template this CoW was built against.
|
||||||
if err := snapshot.WriteMeta(m.cfg.SnapshotsDir, sandboxID, &snapshot.RootfsMeta{
|
if err := snapshot.WriteMeta(pauseDir, "", &snapshot.RootfsMeta{
|
||||||
BaseTemplate: sb.baseImagePath,
|
BaseTemplate: sb.baseImagePath,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
warnErr("snapshot dir cleanup error", sandboxID, snapshot.Remove(m.cfg.SnapshotsDir, sandboxID))
|
warnErr("snapshot dir cleanup error", sandboxID, os.RemoveAll(pauseDir))
|
||||||
// VM and dm-snapshot are already gone — clean up remaining resources.
|
// VM and dm-snapshot are already gone — clean up remaining resources.
|
||||||
warnErr("network cleanup error during pause", sandboxID, network.RemoveNetwork(sb.slot))
|
warnErr("network cleanup error during pause", sandboxID, network.RemoveNetwork(sb.slot))
|
||||||
m.slots.Release(sb.SlotIndex)
|
m.slots.Release(sb.SlotIndex)
|
||||||
@ -477,13 +473,13 @@ func (m *Manager) Pause(ctx context.Context, sandboxID string) error {
|
|||||||
// Resume restores a paused sandbox from its snapshot using UFFD for
|
// Resume restores a paused sandbox from its snapshot using UFFD for
|
||||||
// lazy memory loading. The sandbox gets a new network slot.
|
// lazy memory loading. The sandbox gets a new network slot.
|
||||||
func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int) (*models.Sandbox, error) {
|
func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int) (*models.Sandbox, error) {
|
||||||
snapDir := m.cfg.SnapshotsDir
|
pauseDir := layout.PauseSnapshotDir(m.cfg.WrennDir, sandboxID)
|
||||||
if !snapshot.Exists(snapDir, sandboxID) {
|
if _, err := os.Stat(pauseDir); err != nil {
|
||||||
return nil, fmt.Errorf("no snapshot found for sandbox %s", sandboxID)
|
return nil, fmt.Errorf("no snapshot found for sandbox %s", sandboxID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the header to set up the UFFD memory source.
|
// Read the header to set up the UFFD memory source.
|
||||||
headerData, err := os.ReadFile(snapshot.MemHeaderPath(snapDir, sandboxID))
|
headerData, err := os.ReadFile(filepath.Join(pauseDir, snapshot.MemHeaderName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("read header: %w", err)
|
return nil, fmt.Errorf("read header: %w", err)
|
||||||
}
|
}
|
||||||
@ -494,7 +490,7 @@ func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build diff file map — supports both single-generation and multi-generation.
|
// Build diff file map — supports both single-generation and multi-generation.
|
||||||
diffPaths, err := snapshot.ListDiffFiles(snapDir, sandboxID, header)
|
diffPaths, err := snapshot.ListDiffFiles(pauseDir, "", header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("list diff files: %w", err)
|
return nil, fmt.Errorf("list diff files: %w", err)
|
||||||
}
|
}
|
||||||
@ -505,7 +501,7 @@ func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read rootfs metadata to find the base template image.
|
// Read rootfs metadata to find the base template image.
|
||||||
meta, err := snapshot.ReadMeta(snapDir, sandboxID)
|
meta, err := snapshot.ReadMeta(pauseDir, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
source.Close()
|
source.Close()
|
||||||
return nil, fmt.Errorf("read rootfs meta: %w", err)
|
return nil, fmt.Errorf("read rootfs meta: %w", err)
|
||||||
@ -527,8 +523,8 @@ func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Move CoW file from snapshot dir to sandboxes dir for the running sandbox.
|
// Move CoW file from snapshot dir to sandboxes dir for the running sandbox.
|
||||||
savedCow := snapshot.CowPath(snapDir, sandboxID)
|
savedCow := snapshot.CowPath(pauseDir, "")
|
||||||
cowPath := filepath.Join(m.cfg.SandboxesDir, fmt.Sprintf("%s.cow", sandboxID))
|
cowPath := filepath.Join(layout.SandboxesDir(m.cfg.WrennDir), fmt.Sprintf("%s.cow", sandboxID))
|
||||||
if err := os.Rename(savedCow, cowPath); err != nil {
|
if err := os.Rename(savedCow, cowPath); err != nil {
|
||||||
source.Close()
|
source.Close()
|
||||||
m.loops.Release(baseImagePath)
|
m.loops.Release(baseImagePath)
|
||||||
@ -574,7 +570,7 @@ func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start UFFD server.
|
// Start UFFD server.
|
||||||
uffdSocketPath := filepath.Join(m.cfg.SandboxesDir, fmt.Sprintf("%s-uffd.sock", sandboxID))
|
uffdSocketPath := filepath.Join(layout.SandboxesDir(m.cfg.WrennDir), fmt.Sprintf("%s-uffd.sock", sandboxID))
|
||||||
os.Remove(uffdSocketPath) // Clean stale socket.
|
os.Remove(uffdSocketPath) // Clean stale socket.
|
||||||
uffdServer := uffd.NewServer(uffdSocketPath, source)
|
uffdServer := uffd.NewServer(uffdSocketPath, source)
|
||||||
if err := uffdServer.Start(ctx); err != nil {
|
if err := uffdServer.Start(ctx); err != nil {
|
||||||
@ -590,7 +586,7 @@ func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int)
|
|||||||
// Restore VM from snapshot.
|
// Restore VM from snapshot.
|
||||||
vmCfg := vm.VMConfig{
|
vmCfg := vm.VMConfig{
|
||||||
SandboxID: sandboxID,
|
SandboxID: sandboxID,
|
||||||
KernelPath: m.cfg.KernelPath,
|
KernelPath: layout.KernelPath(m.cfg.WrennDir),
|
||||||
RootfsPath: dmDev.DevicePath,
|
RootfsPath: dmDev.DevicePath,
|
||||||
VCPUs: 1, // Placeholder; overridden by snapshot.
|
VCPUs: 1, // Placeholder; overridden by snapshot.
|
||||||
MemoryMB: int(header.Metadata.Size / (1024 * 1024)), // Placeholder; overridden by snapshot.
|
MemoryMB: int(header.Metadata.Size / (1024 * 1024)), // Placeholder; overridden by snapshot.
|
||||||
@ -602,8 +598,8 @@ func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int)
|
|||||||
NetMask: slot.GuestNetMask,
|
NetMask: slot.GuestNetMask,
|
||||||
}
|
}
|
||||||
|
|
||||||
snapPath := snapshot.SnapPath(snapDir, sandboxID)
|
resumeSnapPath := filepath.Join(pauseDir, snapshot.SnapFileName)
|
||||||
if _, err := m.vm.CreateFromSnapshot(ctx, vmCfg, snapPath, uffdSocketPath); err != nil {
|
if _, err := m.vm.CreateFromSnapshot(ctx, vmCfg, resumeSnapPath, uffdSocketPath); err != nil {
|
||||||
warnErr("uffd server stop error", sandboxID, uffdServer.Stop())
|
warnErr("uffd server stop error", sandboxID, uffdServer.Stop())
|
||||||
source.Close()
|
source.Close()
|
||||||
warnErr("network cleanup error", sandboxID, network.RemoveNetwork(slot))
|
warnErr("network cleanup error", sandboxID, network.RemoveNetwork(slot))
|
||||||
@ -636,7 +632,6 @@ func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int)
|
|||||||
Sandbox: models.Sandbox{
|
Sandbox: models.Sandbox{
|
||||||
ID: sandboxID,
|
ID: sandboxID,
|
||||||
Status: models.StatusRunning,
|
Status: models.StatusRunning,
|
||||||
Template: "",
|
|
||||||
VCPUs: vmCfg.VCPUs,
|
VCPUs: vmCfg.VCPUs,
|
||||||
MemoryMB: vmCfg.MemoryMB,
|
MemoryMB: vmCfg.MemoryMB,
|
||||||
TimeoutSec: timeoutSec,
|
TimeoutSec: timeoutSec,
|
||||||
@ -685,11 +680,7 @@ func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int)
|
|||||||
// The rootfs is flattened (base + CoW merged) into a new standalone rootfs.ext4
|
// The rootfs is flattened (base + CoW merged) into a new standalone rootfs.ext4
|
||||||
// so the template has no dependency on the original base image. Memory state
|
// so the template has no dependency on the original base image. Memory state
|
||||||
// and VM snapshot files are copied as-is.
|
// and VM snapshot files are copied as-is.
|
||||||
func (m *Manager) CreateSnapshot(ctx context.Context, sandboxID, name string) (int64, error) {
|
func (m *Manager) CreateSnapshot(ctx context.Context, sandboxID string, teamID, templateID pgtype.UUID) (int64, error) {
|
||||||
if err := validate.SafeName(name); err != nil {
|
|
||||||
return 0, fmt.Errorf("invalid snapshot name: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the sandbox is running, pause it first.
|
// If the sandbox is running, pause it first.
|
||||||
if _, err := m.get(sandboxID); err == nil {
|
if _, err := m.get(sandboxID); err == nil {
|
||||||
if err := m.Pause(ctx, sandboxID); err != nil {
|
if err := m.Pause(ctx, sandboxID); err != nil {
|
||||||
@ -697,25 +688,26 @@ func (m *Manager) CreateSnapshot(ctx context.Context, sandboxID, name string) (i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point, pause snapshot files must exist in SnapshotsDir/{sandboxID}/.
|
// At this point, pause snapshot files must exist.
|
||||||
if !snapshot.Exists(m.cfg.SnapshotsDir, sandboxID) {
|
pauseDir := layout.PauseSnapshotDir(m.cfg.WrennDir, sandboxID)
|
||||||
|
if _, err := os.Stat(pauseDir); err != nil {
|
||||||
return 0, fmt.Errorf("no snapshot found for sandbox %s", sandboxID)
|
return 0, fmt.Errorf("no snapshot found for sandbox %s", sandboxID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create template directory.
|
// Create template directory.
|
||||||
if err := snapshot.EnsureDir(m.cfg.ImagesDir, name); err != nil {
|
dstDir := layout.TemplateDir(m.cfg.WrennDir, teamID, templateID)
|
||||||
|
if err := os.MkdirAll(dstDir, 0755); err != nil {
|
||||||
return 0, fmt.Errorf("create template dir: %w", err)
|
return 0, fmt.Errorf("create template dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy VM snapshot file and memory header.
|
// Copy VM snapshot file and memory header.
|
||||||
srcDir := snapshot.DirPath(m.cfg.SnapshotsDir, sandboxID)
|
srcDir := pauseDir
|
||||||
dstDir := snapshot.DirPath(m.cfg.ImagesDir, name)
|
|
||||||
|
|
||||||
for _, fname := range []string{snapshot.SnapFileName, snapshot.MemHeaderName} {
|
for _, fname := range []string{snapshot.SnapFileName, snapshot.MemHeaderName} {
|
||||||
src := filepath.Join(srcDir, fname)
|
src := filepath.Join(srcDir, fname)
|
||||||
dst := filepath.Join(dstDir, fname)
|
dst := filepath.Join(dstDir, fname)
|
||||||
if err := copyFile(src, dst); err != nil {
|
if err := copyFile(src, dst); err != nil {
|
||||||
warnErr("template dir cleanup error", name, snapshot.Remove(m.cfg.ImagesDir, name))
|
warnErr("template dir cleanup error", dstDir, os.RemoveAll(dstDir))
|
||||||
return 0, fmt.Errorf("copy %s: %w", fname, err)
|
return 0, fmt.Errorf("copy %s: %w", fname, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -723,59 +715,59 @@ func (m *Manager) CreateSnapshot(ctx context.Context, sandboxID, name string) (i
|
|||||||
// Copy all memory diff files referenced by the header (supports multi-generation).
|
// Copy all memory diff files referenced by the header (supports multi-generation).
|
||||||
headerData, err := os.ReadFile(filepath.Join(srcDir, snapshot.MemHeaderName))
|
headerData, err := os.ReadFile(filepath.Join(srcDir, snapshot.MemHeaderName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
warnErr("template dir cleanup error", name, snapshot.Remove(m.cfg.ImagesDir, name))
|
warnErr("template dir cleanup error", dstDir, os.RemoveAll(dstDir))
|
||||||
return 0, fmt.Errorf("read header for template: %w", err)
|
return 0, fmt.Errorf("read header for template: %w", err)
|
||||||
}
|
}
|
||||||
srcHeader, err := snapshot.Deserialize(headerData)
|
srcHeader, err := snapshot.Deserialize(headerData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
warnErr("template dir cleanup error", name, snapshot.Remove(m.cfg.ImagesDir, name))
|
warnErr("template dir cleanup error", dstDir, os.RemoveAll(dstDir))
|
||||||
return 0, fmt.Errorf("deserialize header for template: %w", err)
|
return 0, fmt.Errorf("deserialize header for template: %w", err)
|
||||||
}
|
}
|
||||||
srcDiffPaths, err := snapshot.ListDiffFiles(m.cfg.SnapshotsDir, sandboxID, srcHeader)
|
srcDiffPaths, err := snapshot.ListDiffFiles(pauseDir, "", srcHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
warnErr("template dir cleanup error", name, snapshot.Remove(m.cfg.ImagesDir, name))
|
warnErr("template dir cleanup error", dstDir, os.RemoveAll(dstDir))
|
||||||
return 0, fmt.Errorf("list diff files for template: %w", err)
|
return 0, fmt.Errorf("list diff files for template: %w", err)
|
||||||
}
|
}
|
||||||
for _, srcPath := range srcDiffPaths {
|
for _, srcPath := range srcDiffPaths {
|
||||||
dstPath := filepath.Join(dstDir, filepath.Base(srcPath))
|
dstPath := filepath.Join(dstDir, filepath.Base(srcPath))
|
||||||
if err := copyFile(srcPath, dstPath); err != nil {
|
if err := copyFile(srcPath, dstPath); err != nil {
|
||||||
warnErr("template dir cleanup error", name, snapshot.Remove(m.cfg.ImagesDir, name))
|
warnErr("template dir cleanup error", dstDir, os.RemoveAll(dstDir))
|
||||||
return 0, fmt.Errorf("copy diff file %s: %w", filepath.Base(srcPath), err)
|
return 0, fmt.Errorf("copy diff file %s: %w", filepath.Base(srcPath), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flatten rootfs: temporarily set up dm device from base + CoW, dd to new image.
|
// Flatten rootfs: temporarily set up dm device from base + CoW, dd to new image.
|
||||||
meta, err := snapshot.ReadMeta(m.cfg.SnapshotsDir, sandboxID)
|
meta, err := snapshot.ReadMeta(pauseDir, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
warnErr("template dir cleanup error", name, snapshot.Remove(m.cfg.ImagesDir, name))
|
warnErr("template dir cleanup error", dstDir, os.RemoveAll(dstDir))
|
||||||
return 0, fmt.Errorf("read rootfs meta: %w", err)
|
return 0, fmt.Errorf("read rootfs meta: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
originLoop, err := m.loops.Acquire(meta.BaseTemplate)
|
originLoop, err := m.loops.Acquire(meta.BaseTemplate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
warnErr("template dir cleanup error", name, snapshot.Remove(m.cfg.ImagesDir, name))
|
warnErr("template dir cleanup error", dstDir, os.RemoveAll(dstDir))
|
||||||
return 0, fmt.Errorf("acquire loop device for flatten: %w", err)
|
return 0, fmt.Errorf("acquire loop device for flatten: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
originSize, err := devicemapper.OriginSizeBytes(originLoop)
|
originSize, err := devicemapper.OriginSizeBytes(originLoop)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.loops.Release(meta.BaseTemplate)
|
m.loops.Release(meta.BaseTemplate)
|
||||||
warnErr("template dir cleanup error", name, snapshot.Remove(m.cfg.ImagesDir, name))
|
warnErr("template dir cleanup error", dstDir, os.RemoveAll(dstDir))
|
||||||
return 0, fmt.Errorf("get origin size: %w", err)
|
return 0, fmt.Errorf("get origin size: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporarily restore the dm-snapshot to read the merged view.
|
// Temporarily restore the dm-snapshot to read the merged view.
|
||||||
cowPath := snapshot.CowPath(m.cfg.SnapshotsDir, sandboxID)
|
cowPath := snapshot.CowPath(pauseDir, "")
|
||||||
tmpDmName := "wrenn-flatten-" + sandboxID
|
tmpDmName := "wrenn-flatten-" + sandboxID
|
||||||
tmpDev, err := devicemapper.RestoreSnapshot(ctx, tmpDmName, originLoop, cowPath, originSize)
|
tmpDev, err := devicemapper.RestoreSnapshot(ctx, tmpDmName, originLoop, cowPath, originSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.loops.Release(meta.BaseTemplate)
|
m.loops.Release(meta.BaseTemplate)
|
||||||
warnErr("template dir cleanup error", name, snapshot.Remove(m.cfg.ImagesDir, name))
|
warnErr("template dir cleanup error", dstDir, os.RemoveAll(dstDir))
|
||||||
return 0, fmt.Errorf("restore dm-snapshot for flatten: %w", err)
|
return 0, fmt.Errorf("restore dm-snapshot for flatten: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flatten to new standalone rootfs.
|
// Flatten to new standalone rootfs.
|
||||||
flattenedPath := snapshot.RootfsPath(m.cfg.ImagesDir, name)
|
flattenedPath := filepath.Join(dstDir, snapshot.RootfsFileName)
|
||||||
flattenErr := devicemapper.FlattenSnapshot(tmpDev.DevicePath, flattenedPath)
|
flattenErr := devicemapper.FlattenSnapshot(tmpDev.DevicePath, flattenedPath)
|
||||||
|
|
||||||
// Always clean up the temporary dm device.
|
// Always clean up the temporary dm device.
|
||||||
@ -783,18 +775,19 @@ func (m *Manager) CreateSnapshot(ctx context.Context, sandboxID, name string) (i
|
|||||||
m.loops.Release(meta.BaseTemplate)
|
m.loops.Release(meta.BaseTemplate)
|
||||||
|
|
||||||
if flattenErr != nil {
|
if flattenErr != nil {
|
||||||
warnErr("template dir cleanup error", name, snapshot.Remove(m.cfg.ImagesDir, name))
|
warnErr("template dir cleanup error", dstDir, os.RemoveAll(dstDir))
|
||||||
return 0, fmt.Errorf("flatten rootfs: %w", flattenErr)
|
return 0, fmt.Errorf("flatten rootfs: %w", flattenErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
sizeBytes, err := snapshot.DirSize(m.cfg.ImagesDir, name)
|
sizeBytes, err := snapshot.DirSize(dstDir, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("failed to calculate snapshot size", "error", err)
|
slog.Warn("failed to calculate snapshot size", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("template snapshot created (rootfs flattened)",
|
slog.Info("template snapshot created (rootfs flattened)",
|
||||||
"sandbox", sandboxID,
|
"sandbox", sandboxID,
|
||||||
"name", name,
|
"team_id", teamID,
|
||||||
|
"template_id", templateID,
|
||||||
"size_bytes", sizeBytes,
|
"size_bytes", sizeBytes,
|
||||||
)
|
)
|
||||||
return sizeBytes, nil
|
return sizeBytes, nil
|
||||||
@ -804,11 +797,7 @@ func (m *Manager) CreateSnapshot(ctx context.Context, sandboxID, name string) (i
|
|||||||
// rootfs into a standalone rootfs.ext4, and cleans up all resources.
|
// rootfs into a standalone rootfs.ext4, and cleans up all resources.
|
||||||
// The result is an image-only template (no VM memory/CPU state) stored in
|
// The result is an image-only template (no VM memory/CPU state) stored in
|
||||||
// ImagesDir/{name}/rootfs.ext4.
|
// ImagesDir/{name}/rootfs.ext4.
|
||||||
func (m *Manager) FlattenRootfs(ctx context.Context, sandboxID, name string) (int64, error) {
|
func (m *Manager) FlattenRootfs(ctx context.Context, sandboxID string, teamID, templateID pgtype.UUID) (int64, error) {
|
||||||
if err := validate.SafeName(name); err != nil {
|
|
||||||
return 0, fmt.Errorf("invalid template name: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
sb, ok := m.boxes[sandboxID]
|
sb, ok := m.boxes[sandboxID]
|
||||||
if ok {
|
if ok {
|
||||||
@ -837,21 +826,22 @@ func (m *Manager) FlattenRootfs(ctx context.Context, sandboxID, name string) (in
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create template directory and flatten the dm-snapshot.
|
// Create template directory and flatten the dm-snapshot.
|
||||||
if err := snapshot.EnsureDir(m.cfg.ImagesDir, name); err != nil {
|
flattenDstDir := layout.TemplateDir(m.cfg.WrennDir, teamID, templateID)
|
||||||
|
if err := os.MkdirAll(flattenDstDir, 0755); err != nil {
|
||||||
m.cleanupDM(sb)
|
m.cleanupDM(sb)
|
||||||
return 0, fmt.Errorf("create template dir: %w", err)
|
return 0, fmt.Errorf("create template dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
outputPath := snapshot.RootfsPath(m.cfg.ImagesDir, name)
|
outputPath := filepath.Join(flattenDstDir, snapshot.RootfsFileName)
|
||||||
if sb.dmDevice == nil {
|
if sb.dmDevice == nil {
|
||||||
m.cleanupDM(sb)
|
m.cleanupDM(sb)
|
||||||
warnErr("template dir cleanup error", name, snapshot.Remove(m.cfg.ImagesDir, name))
|
warnErr("template dir cleanup error", flattenDstDir, os.RemoveAll(flattenDstDir))
|
||||||
return 0, fmt.Errorf("sandbox %s has no dm device", sandboxID)
|
return 0, fmt.Errorf("sandbox %s has no dm device", sandboxID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := devicemapper.FlattenSnapshot(sb.dmDevice.DevicePath, outputPath); err != nil {
|
if err := devicemapper.FlattenSnapshot(sb.dmDevice.DevicePath, outputPath); err != nil {
|
||||||
m.cleanupDM(sb)
|
m.cleanupDM(sb)
|
||||||
warnErr("template dir cleanup error", name, snapshot.Remove(m.cfg.ImagesDir, name))
|
warnErr("template dir cleanup error", flattenDstDir, os.RemoveAll(flattenDstDir))
|
||||||
return 0, fmt.Errorf("flatten rootfs: %w", err)
|
return 0, fmt.Errorf("flatten rootfs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -869,14 +859,15 @@ func (m *Manager) FlattenRootfs(ctx context.Context, sandboxID, name string) (in
|
|||||||
slog.Warn("resize2fs -M failed (non-fatal)", "output", string(out), "error", err)
|
slog.Warn("resize2fs -M failed (non-fatal)", "output", string(out), "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sizeBytes, err := snapshot.DirSize(m.cfg.ImagesDir, name)
|
sizeBytes, err := snapshot.DirSize(flattenDstDir, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("failed to calculate template size", "error", err)
|
slog.Warn("failed to calculate template size", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("rootfs flattened to image-only template",
|
slog.Info("rootfs flattened to image-only template",
|
||||||
"sandbox", sandboxID,
|
"sandbox", sandboxID,
|
||||||
"name", name,
|
"team_id", teamID,
|
||||||
|
"template_id", templateID,
|
||||||
"size_bytes", sizeBytes,
|
"size_bytes", sizeBytes,
|
||||||
)
|
)
|
||||||
return sizeBytes, nil
|
return sizeBytes, nil
|
||||||
@ -896,22 +887,19 @@ func (m *Manager) cleanupDM(sb *sandboxState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteSnapshot removes a snapshot template from disk.
|
// DeleteSnapshot removes a snapshot template from disk.
|
||||||
func (m *Manager) DeleteSnapshot(name string) error {
|
func (m *Manager) DeleteSnapshot(teamID, templateID pgtype.UUID) error {
|
||||||
if err := validate.SafeName(name); err != nil {
|
return os.RemoveAll(layout.TemplateDir(m.cfg.WrennDir, teamID, templateID))
|
||||||
return fmt.Errorf("invalid snapshot name: %w", err)
|
|
||||||
}
|
|
||||||
return snapshot.Remove(m.cfg.ImagesDir, name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// createFromSnapshot creates a new sandbox by restoring from a snapshot template
|
// createFromSnapshot creates a new sandbox by restoring from a snapshot template
|
||||||
// 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, diskSizeMB int) (*models.Sandbox, error) {
|
func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID string, teamID, templateID pgtype.UUID, vcpus, _, timeoutSec, diskSizeMB int) (*models.Sandbox, error) {
|
||||||
imagesDir := m.cfg.ImagesDir
|
tmplDir := layout.TemplateDir(m.cfg.WrennDir, teamID, templateID)
|
||||||
|
|
||||||
// Read the header.
|
// Read the header.
|
||||||
headerData, err := os.ReadFile(snapshot.MemHeaderPath(imagesDir, snapshotName))
|
headerData, err := os.ReadFile(filepath.Join(tmplDir, snapshot.MemHeaderName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("read snapshot header: %w", err)
|
return nil, fmt.Errorf("read snapshot header: %w", err)
|
||||||
}
|
}
|
||||||
@ -925,7 +913,7 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID, snapshotNam
|
|||||||
memoryMB := int(header.Metadata.Size / (1024 * 1024))
|
memoryMB := int(header.Metadata.Size / (1024 * 1024))
|
||||||
|
|
||||||
// Build diff file map — supports multi-generation templates.
|
// Build diff file map — supports multi-generation templates.
|
||||||
diffPaths, err := snapshot.ListDiffFiles(imagesDir, snapshotName, header)
|
diffPaths, err := snapshot.ListDiffFiles(tmplDir, "", header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("list diff files: %w", err)
|
return nil, fmt.Errorf("list diff files: %w", err)
|
||||||
}
|
}
|
||||||
@ -936,7 +924,7 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID, snapshotNam
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set up dm-snapshot on the template's flattened rootfs.
|
// Set up dm-snapshot on the template's flattened rootfs.
|
||||||
baseRootfs := snapshot.RootfsPath(imagesDir, snapshotName)
|
baseRootfs := filepath.Join(tmplDir, snapshot.RootfsFileName)
|
||||||
originLoop, err := m.loops.Acquire(baseRootfs)
|
originLoop, err := m.loops.Acquire(baseRootfs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
source.Close()
|
source.Close()
|
||||||
@ -951,7 +939,7 @@ 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(layout.SandboxesDir(m.cfg.WrennDir), fmt.Sprintf("%s.cow", sandboxID))
|
||||||
cowSize := int64(diskSizeMB) * 1024 * 1024
|
cowSize := int64(diskSizeMB) * 1024 * 1024
|
||||||
dmDev, err := devicemapper.CreateSnapshot(dmName, originLoop, cowPath, originSize, cowSize)
|
dmDev, err := devicemapper.CreateSnapshot(dmName, originLoop, cowPath, originSize, cowSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -981,7 +969,7 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID, snapshotNam
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start UFFD server.
|
// Start UFFD server.
|
||||||
uffdSocketPath := filepath.Join(m.cfg.SandboxesDir, fmt.Sprintf("%s-uffd.sock", sandboxID))
|
uffdSocketPath := filepath.Join(layout.SandboxesDir(m.cfg.WrennDir), fmt.Sprintf("%s-uffd.sock", sandboxID))
|
||||||
os.Remove(uffdSocketPath)
|
os.Remove(uffdSocketPath)
|
||||||
uffdServer := uffd.NewServer(uffdSocketPath, source)
|
uffdServer := uffd.NewServer(uffdSocketPath, source)
|
||||||
if err := uffdServer.Start(ctx); err != nil {
|
if err := uffdServer.Start(ctx); err != nil {
|
||||||
@ -997,7 +985,7 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID, snapshotNam
|
|||||||
// Restore VM.
|
// Restore VM.
|
||||||
vmCfg := vm.VMConfig{
|
vmCfg := vm.VMConfig{
|
||||||
SandboxID: sandboxID,
|
SandboxID: sandboxID,
|
||||||
KernelPath: m.cfg.KernelPath,
|
KernelPath: layout.KernelPath(m.cfg.WrennDir),
|
||||||
RootfsPath: dmDev.DevicePath,
|
RootfsPath: dmDev.DevicePath,
|
||||||
VCPUs: vcpus,
|
VCPUs: vcpus,
|
||||||
MemoryMB: memoryMB,
|
MemoryMB: memoryMB,
|
||||||
@ -1009,7 +997,7 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID, snapshotNam
|
|||||||
NetMask: slot.GuestNetMask,
|
NetMask: slot.GuestNetMask,
|
||||||
}
|
}
|
||||||
|
|
||||||
snapPath := snapshot.SnapPath(imagesDir, snapshotName)
|
snapPath := filepath.Join(tmplDir, snapshot.SnapFileName)
|
||||||
if _, err := m.vm.CreateFromSnapshot(ctx, vmCfg, snapPath, uffdSocketPath); err != nil {
|
if _, err := m.vm.CreateFromSnapshot(ctx, vmCfg, snapPath, uffdSocketPath); err != nil {
|
||||||
warnErr("uffd server stop error", sandboxID, uffdServer.Stop())
|
warnErr("uffd server stop error", sandboxID, uffdServer.Stop())
|
||||||
source.Close()
|
source.Close()
|
||||||
@ -1041,17 +1029,18 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID, snapshotNam
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
sb := &sandboxState{
|
sb := &sandboxState{
|
||||||
Sandbox: models.Sandbox{
|
Sandbox: models.Sandbox{
|
||||||
ID: sandboxID,
|
ID: sandboxID,
|
||||||
Status: models.StatusRunning,
|
Status: models.StatusRunning,
|
||||||
Template: snapshotName,
|
TemplateTeamID: teamID.Bytes,
|
||||||
VCPUs: vcpus,
|
TemplateID: templateID.Bytes,
|
||||||
MemoryMB: memoryMB,
|
VCPUs: vcpus,
|
||||||
TimeoutSec: timeoutSec,
|
MemoryMB: memoryMB,
|
||||||
SlotIndex: slotIdx,
|
TimeoutSec: timeoutSec,
|
||||||
HostIP: slot.HostIP,
|
SlotIndex: slotIdx,
|
||||||
RootfsPath: dmDev.DevicePath,
|
HostIP: slot.HostIP,
|
||||||
CreatedAt: now,
|
RootfsPath: dmDev.DevicePath,
|
||||||
LastActiveAt: now,
|
CreatedAt: now,
|
||||||
|
LastActiveAt: now,
|
||||||
},
|
},
|
||||||
slot: slot,
|
slot: slot,
|
||||||
client: client,
|
client: client,
|
||||||
@ -1073,7 +1062,8 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID, snapshotNam
|
|||||||
|
|
||||||
slog.Info("sandbox created from snapshot",
|
slog.Info("sandbox created from snapshot",
|
||||||
"id", sandboxID,
|
"id", sandboxID,
|
||||||
"snapshot", snapshotName,
|
"team_id", teamID,
|
||||||
|
"template_id", templateID,
|
||||||
"host_ip", slot.HostIP.String(),
|
"host_ip", slot.HostIP.String(),
|
||||||
"dm_device", dmDev.DevicePath,
|
"dm_device", dmDev.DevicePath,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -95,6 +95,7 @@ func (s *BuildService) Create(ctx context.Context, p BuildCreateParams) (db.Temp
|
|||||||
|
|
||||||
buildID := id.NewBuildID()
|
buildID := id.NewBuildID()
|
||||||
buildIDStr := id.FormatBuildID(buildID)
|
buildIDStr := id.FormatBuildID(buildID)
|
||||||
|
newTemplateID := id.NewTemplateID()
|
||||||
|
|
||||||
build, err := s.DB.InsertTemplateBuild(ctx, db.InsertTemplateBuildParams{
|
build, err := s.DB.InsertTemplateBuild(ctx, db.InsertTemplateBuildParams{
|
||||||
ID: buildID,
|
ID: buildID,
|
||||||
@ -105,6 +106,8 @@ func (s *BuildService) Create(ctx context.Context, p BuildCreateParams) (db.Temp
|
|||||||
Vcpus: p.VCPUs,
|
Vcpus: p.VCPUs,
|
||||||
MemoryMb: p.MemoryMB,
|
MemoryMb: p.MemoryMB,
|
||||||
TotalSteps: int32(len(p.Recipe) + len(preBuildCmds) + len(postBuildCmds)),
|
TotalSteps: int32(len(p.Recipe) + len(preBuildCmds) + len(postBuildCmds)),
|
||||||
|
TemplateID: newTemplateID,
|
||||||
|
TeamID: id.PlatformTeamID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return db.TemplateBuild{}, fmt.Errorf("insert build: %w", err)
|
return db.TemplateBuild{}, fmt.Errorf("insert build: %w", err)
|
||||||
@ -207,12 +210,27 @@ func (s *BuildService) executeBuild(ctx context.Context, buildIDStr string) {
|
|||||||
sandboxIDStr := id.FormatSandboxID(sandboxID)
|
sandboxIDStr := id.FormatSandboxID(sandboxID)
|
||||||
log = log.With("sandbox_id", sandboxIDStr, "host_id", id.FormatHostID(host.ID))
|
log = log.With("sandbox_id", sandboxIDStr, "host_id", id.FormatHostID(host.ID))
|
||||||
|
|
||||||
|
// Resolve the base template to UUIDs. "minimal" is the zero sentinel.
|
||||||
|
baseTeamID := id.PlatformTeamID
|
||||||
|
baseTemplateID := id.MinimalTemplateID
|
||||||
|
if build.BaseTemplate != "minimal" {
|
||||||
|
baseTmpl, err := s.DB.GetPlatformTemplateByName(ctx, build.BaseTemplate)
|
||||||
|
if err != nil {
|
||||||
|
s.failBuild(ctx, buildID, fmt.Sprintf("base template %q not found: %v", build.BaseTemplate, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
baseTeamID = baseTmpl.TeamID
|
||||||
|
baseTemplateID = baseTmpl.ID
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := agent.CreateSandbox(ctx, connect.NewRequest(&pb.CreateSandboxRequest{
|
resp, err := agent.CreateSandbox(ctx, connect.NewRequest(&pb.CreateSandboxRequest{
|
||||||
SandboxId: sandboxIDStr,
|
SandboxId: sandboxIDStr,
|
||||||
Template: build.BaseTemplate,
|
Template: build.BaseTemplate,
|
||||||
|
TeamId: id.UUIDString(baseTeamID),
|
||||||
|
TemplateId: id.UUIDString(baseTemplateID),
|
||||||
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: 5120, // 5 GB for template builds
|
DiskSizeMb: 5120, // 5 GB for template builds
|
||||||
}))
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -316,8 +334,10 @@ func (s *BuildService) executeBuild(ctx context.Context, buildIDStr string) {
|
|||||||
// Healthcheck passed → full snapshot (with memory/CPU state).
|
// Healthcheck passed → full snapshot (with memory/CPU state).
|
||||||
log.Info("healthcheck passed, creating snapshot")
|
log.Info("healthcheck passed, creating snapshot")
|
||||||
snapResp, err := agent.CreateSnapshot(ctx, connect.NewRequest(&pb.CreateSnapshotRequest{
|
snapResp, err := agent.CreateSnapshot(ctx, connect.NewRequest(&pb.CreateSnapshotRequest{
|
||||||
SandboxId: sandboxIDStr,
|
SandboxId: sandboxIDStr,
|
||||||
Name: build.Name,
|
Name: build.Name,
|
||||||
|
TeamId: id.UUIDString(build.TeamID),
|
||||||
|
TemplateId: id.UUIDString(build.TemplateID),
|
||||||
}))
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.destroySandbox(ctx, agent, sandboxIDStr)
|
s.destroySandbox(ctx, agent, sandboxIDStr)
|
||||||
@ -329,8 +349,10 @@ func (s *BuildService) executeBuild(ctx context.Context, buildIDStr string) {
|
|||||||
// No healthcheck → image-only template (rootfs only).
|
// No healthcheck → image-only template (rootfs only).
|
||||||
log.Info("no healthcheck, flattening rootfs")
|
log.Info("no healthcheck, flattening rootfs")
|
||||||
flatResp, err := agent.FlattenRootfs(ctx, connect.NewRequest(&pb.FlattenRootfsRequest{
|
flatResp, err := agent.FlattenRootfs(ctx, connect.NewRequest(&pb.FlattenRootfsRequest{
|
||||||
SandboxId: sandboxIDStr,
|
SandboxId: sandboxIDStr,
|
||||||
Name: build.Name,
|
Name: build.Name,
|
||||||
|
TeamId: id.UUIDString(build.TeamID),
|
||||||
|
TemplateId: id.UUIDString(build.TemplateID),
|
||||||
}))
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.destroySandbox(ctx, agent, sandboxIDStr)
|
s.destroySandbox(ctx, agent, sandboxIDStr)
|
||||||
@ -347,6 +369,7 @@ func (s *BuildService) executeBuild(ctx context.Context, buildIDStr string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := s.DB.InsertTemplate(ctx, db.InsertTemplateParams{
|
if _, err := s.DB.InsertTemplate(ctx, db.InsertTemplateParams{
|
||||||
|
ID: build.TemplateID,
|
||||||
Name: build.Name,
|
Name: build.Name,
|
||||||
Type: templateType,
|
Type: templateType,
|
||||||
Vcpus: build.Vcpus,
|
Vcpus: build.Vcpus,
|
||||||
|
|||||||
@ -82,10 +82,21 @@ func (s *SandboxService) Create(ctx context.Context, p SandboxCreateParams) (db.
|
|||||||
p.DiskSizeMB = 5120 // 5 GB default
|
p.DiskSizeMB = 5120 // 5 GB default
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the template is a snapshot, use its baked-in vcpus/memory.
|
// Resolve template name → (teamID, templateID).
|
||||||
if tmpl, err := s.DB.GetTemplateByTeam(ctx, db.GetTemplateByTeamParams{Name: p.Template, TeamID: p.TeamID}); err == nil && tmpl.Type == "snapshot" {
|
templateTeamID := id.PlatformTeamID
|
||||||
p.VCPUs = tmpl.Vcpus
|
templateID := id.MinimalTemplateID
|
||||||
p.MemoryMB = tmpl.MemoryMb
|
if p.Template != "minimal" {
|
||||||
|
tmpl, err := s.DB.GetTemplateByTeam(ctx, db.GetTemplateByTeamParams{Name: p.Template, TeamID: p.TeamID})
|
||||||
|
if err != nil {
|
||||||
|
return db.Sandbox{}, fmt.Errorf("template %q not found: %w", p.Template, err)
|
||||||
|
}
|
||||||
|
templateTeamID = tmpl.TeamID
|
||||||
|
templateID = tmpl.ID
|
||||||
|
// If the template is a snapshot, use its baked-in vcpus/memory.
|
||||||
|
if tmpl.Type == "snapshot" {
|
||||||
|
p.VCPUs = tmpl.Vcpus
|
||||||
|
p.MemoryMB = tmpl.MemoryMb
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !p.TeamID.Valid {
|
if !p.TeamID.Valid {
|
||||||
@ -113,15 +124,17 @@ func (s *SandboxService) Create(ctx context.Context, p SandboxCreateParams) (db.
|
|||||||
sandboxIDStr := id.FormatSandboxID(sandboxID)
|
sandboxIDStr := id.FormatSandboxID(sandboxID)
|
||||||
|
|
||||||
if _, err := s.DB.InsertSandbox(ctx, db.InsertSandboxParams{
|
if _, err := s.DB.InsertSandbox(ctx, db.InsertSandboxParams{
|
||||||
ID: sandboxID,
|
ID: sandboxID,
|
||||||
TeamID: p.TeamID,
|
TeamID: p.TeamID,
|
||||||
HostID: host.ID,
|
HostID: host.ID,
|
||||||
Template: p.Template,
|
Template: p.Template,
|
||||||
Status: "pending",
|
Status: "pending",
|
||||||
Vcpus: p.VCPUs,
|
Vcpus: p.VCPUs,
|
||||||
MemoryMb: p.MemoryMB,
|
MemoryMb: p.MemoryMB,
|
||||||
TimeoutSec: p.TimeoutSec,
|
TimeoutSec: p.TimeoutSec,
|
||||||
DiskSizeMb: p.DiskSizeMB,
|
DiskSizeMb: p.DiskSizeMB,
|
||||||
|
TemplateID: templateID,
|
||||||
|
TemplateTeamID: templateTeamID,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return db.Sandbox{}, fmt.Errorf("insert sandbox: %w", err)
|
return db.Sandbox{}, fmt.Errorf("insert sandbox: %w", err)
|
||||||
}
|
}
|
||||||
@ -129,6 +142,8 @@ func (s *SandboxService) Create(ctx context.Context, p SandboxCreateParams) (db.
|
|||||||
resp, err := agent.CreateSandbox(ctx, connect.NewRequest(&pb.CreateSandboxRequest{
|
resp, err := agent.CreateSandbox(ctx, connect.NewRequest(&pb.CreateSandboxRequest{
|
||||||
SandboxId: sandboxIDStr,
|
SandboxId: sandboxIDStr,
|
||||||
Template: p.Template,
|
Template: p.Template,
|
||||||
|
TeamId: id.UUIDString(templateTeamID),
|
||||||
|
TemplateId: id.UUIDString(templateID),
|
||||||
Vcpus: p.VCPUs,
|
Vcpus: p.VCPUs,
|
||||||
MemoryMb: p.MemoryMB,
|
MemoryMb: p.MemoryMB,
|
||||||
TimeoutSec: p.TimeoutSec,
|
TimeoutSec: p.TimeoutSec,
|
||||||
|
|||||||
@ -202,12 +202,61 @@ func (s *TeamService) DeleteTeam(ctx context.Context, teamID, callerUserID pgtyp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up team-owned templates from all hosts in the background.
|
||||||
|
go s.cleanupTeamTemplates(context.Background(), teamID)
|
||||||
|
|
||||||
if err := s.DB.SoftDeleteTeam(ctx, teamID); err != nil {
|
if err := s.DB.SoftDeleteTeam(ctx, teamID); err != nil {
|
||||||
return fmt.Errorf("soft delete team: %w", err)
|
return fmt.Errorf("soft delete team: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanupTeamTemplates deletes all template files for a team from all online hosts,
|
||||||
|
// then removes the DB records. Called asynchronously during team deletion.
|
||||||
|
func (s *TeamService) cleanupTeamTemplates(ctx context.Context, teamID pgtype.UUID) {
|
||||||
|
templates, err := s.DB.ListTemplatesByTeamOnly(ctx, teamID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("team delete: failed to list templates for cleanup", "team_id", id.FormatTeamID(teamID), "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(templates) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hosts, err := s.DB.ListActiveHosts(ctx)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("team delete: failed to list hosts for template cleanup", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tmpl := range templates {
|
||||||
|
for _, host := range hosts {
|
||||||
|
if host.Status != "online" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
agent, err := s.HostPool.GetForHost(host)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := agent.DeleteSnapshot(ctx, connect.NewRequest(&pb.DeleteSnapshotRequest{
|
||||||
|
TeamId: id.UUIDString(tmpl.TeamID),
|
||||||
|
TemplateId: id.UUIDString(tmpl.ID),
|
||||||
|
})); err != nil && connect.CodeOf(err) != connect.CodeNotFound {
|
||||||
|
slog.Warn("team delete: failed to delete template on host",
|
||||||
|
"host_id", id.FormatHostID(host.ID),
|
||||||
|
"template", tmpl.Name,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove DB records.
|
||||||
|
if err := s.DB.DeleteTemplatesByTeam(ctx, teamID); err != nil {
|
||||||
|
slog.Warn("team delete: failed to delete template records", "team_id", id.FormatTeamID(teamID), "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetMembers returns all members of the team with their emails and roles.
|
// GetMembers returns all members of the team with their emails and roles.
|
||||||
func (s *TeamService) GetMembers(ctx context.Context, teamID pgtype.UUID) ([]MemberInfo, error) {
|
func (s *TeamService) GetMembers(ctx context.Context, teamID pgtype.UUID) ([]MemberInfo, error) {
|
||||||
rows, err := s.DB.GetTeamMembers(ctx, teamID)
|
rows, err := s.DB.GetTeamMembers(ctx, teamID)
|
||||||
|
|||||||
@ -25,7 +25,7 @@ type CreateSandboxRequest struct {
|
|||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
// Sandbox ID assigned by the control plane. If empty, the host agent generates one.
|
// Sandbox ID assigned by the control plane. If empty, the host agent generates one.
|
||||||
SandboxId string `protobuf:"bytes,5,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"`
|
SandboxId string `protobuf:"bytes,5,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"`
|
||||||
// Template name (e.g., "minimal", "python311"). Determines base rootfs.
|
// Deprecated: use team_id + template_id instead.
|
||||||
Template string `protobuf:"bytes,1,opt,name=template,proto3" json:"template,omitempty"`
|
Template string `protobuf:"bytes,1,opt,name=template,proto3" json:"template,omitempty"`
|
||||||
// Number of virtual CPUs (default: 1).
|
// Number of virtual CPUs (default: 1).
|
||||||
Vcpus int32 `protobuf:"varint,2,opt,name=vcpus,proto3" json:"vcpus,omitempty"`
|
Vcpus int32 `protobuf:"varint,2,opt,name=vcpus,proto3" json:"vcpus,omitempty"`
|
||||||
@ -36,7 +36,11 @@ type CreateSandboxRequest struct {
|
|||||||
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 rootfs. Base images are expanded to this size
|
// Disk size in MB for the rootfs. Base images are expanded to this size
|
||||||
// at host agent startup. Default: 5120 (5 GB).
|
// at host agent startup. Default: 5120 (5 GB).
|
||||||
DiskSizeMb int32 `protobuf:"varint,6,opt,name=disk_size_mb,json=diskSizeMb,proto3" json:"disk_size_mb,omitempty"`
|
DiskSizeMb int32 `protobuf:"varint,6,opt,name=disk_size_mb,json=diskSizeMb,proto3" json:"disk_size_mb,omitempty"`
|
||||||
|
// Team UUID that owns the template (hex string). All-zeros = platform.
|
||||||
|
TeamId string `protobuf:"bytes,7,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"`
|
||||||
|
// Template UUID (hex string). Both zeros + team zeros = "minimal" sentinel.
|
||||||
|
TemplateId string `protobuf:"bytes,8,opt,name=template_id,json=templateId,proto3" json:"template_id,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -113,6 +117,20 @@ func (x *CreateSandboxRequest) GetDiskSizeMb() int32 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *CreateSandboxRequest) GetTeamId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.TeamId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateSandboxRequest) GetTemplateId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.TemplateId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
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"`
|
||||||
@ -448,9 +466,14 @@ func (x *ResumeSandboxResponse) GetHostIp() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateSnapshotRequest struct {
|
type CreateSnapshotRequest 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"`
|
||||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
// Deprecated: use team_id + template_id instead.
|
||||||
|
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
// Team UUID that will own the new template.
|
||||||
|
TeamId string `protobuf:"bytes,3,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"`
|
||||||
|
// Template UUID for the new snapshot template.
|
||||||
|
TemplateId string `protobuf:"bytes,4,opt,name=template_id,json=templateId,proto3" json:"template_id,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -499,6 +522,20 @@ func (x *CreateSnapshotRequest) GetName() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *CreateSnapshotRequest) GetTeamId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.TeamId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateSnapshotRequest) GetTemplateId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.TemplateId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type CreateSnapshotResponse struct {
|
type CreateSnapshotResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
@ -552,8 +589,13 @@ func (x *CreateSnapshotResponse) GetSizeBytes() int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DeleteSnapshotRequest struct {
|
type DeleteSnapshotRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
// Deprecated: use team_id + template_id instead.
|
||||||
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
// Team UUID that owns the template.
|
||||||
|
TeamId string `protobuf:"bytes,2,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"`
|
||||||
|
// Template UUID to delete.
|
||||||
|
TemplateId string `protobuf:"bytes,3,opt,name=template_id,json=templateId,proto3" json:"template_id,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -595,6 +637,20 @@ func (x *DeleteSnapshotRequest) GetName() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *DeleteSnapshotRequest) GetTeamId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.TeamId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DeleteSnapshotRequest) GetTemplateId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.TemplateId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type DeleteSnapshotResponse struct {
|
type DeleteSnapshotResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
@ -851,16 +907,19 @@ func (x *ListSandboxesResponse) GetAutoPausedSandboxIds() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SandboxInfo struct {
|
type SandboxInfo 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"`
|
||||||
Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"`
|
Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"`
|
||||||
Template string `protobuf:"bytes,3,opt,name=template,proto3" json:"template,omitempty"`
|
// Deprecated: use team_id + template_id instead.
|
||||||
Vcpus int32 `protobuf:"varint,4,opt,name=vcpus,proto3" json:"vcpus,omitempty"`
|
Template string `protobuf:"bytes,3,opt,name=template,proto3" json:"template,omitempty"`
|
||||||
MemoryMb int32 `protobuf:"varint,5,opt,name=memory_mb,json=memoryMb,proto3" json:"memory_mb,omitempty"`
|
Vcpus int32 `protobuf:"varint,4,opt,name=vcpus,proto3" json:"vcpus,omitempty"`
|
||||||
HostIp string `protobuf:"bytes,6,opt,name=host_ip,json=hostIp,proto3" json:"host_ip,omitempty"`
|
MemoryMb int32 `protobuf:"varint,5,opt,name=memory_mb,json=memoryMb,proto3" json:"memory_mb,omitempty"`
|
||||||
CreatedAtUnix int64 `protobuf:"varint,7,opt,name=created_at_unix,json=createdAtUnix,proto3" json:"created_at_unix,omitempty"`
|
HostIp string `protobuf:"bytes,6,opt,name=host_ip,json=hostIp,proto3" json:"host_ip,omitempty"`
|
||||||
LastActiveAtUnix int64 `protobuf:"varint,8,opt,name=last_active_at_unix,json=lastActiveAtUnix,proto3" json:"last_active_at_unix,omitempty"`
|
CreatedAtUnix int64 `protobuf:"varint,7,opt,name=created_at_unix,json=createdAtUnix,proto3" json:"created_at_unix,omitempty"`
|
||||||
TimeoutSec int32 `protobuf:"varint,9,opt,name=timeout_sec,json=timeoutSec,proto3" json:"timeout_sec,omitempty"`
|
LastActiveAtUnix int64 `protobuf:"varint,8,opt,name=last_active_at_unix,json=lastActiveAtUnix,proto3" json:"last_active_at_unix,omitempty"`
|
||||||
|
TimeoutSec int32 `protobuf:"varint,9,opt,name=timeout_sec,json=timeoutSec,proto3" json:"timeout_sec,omitempty"`
|
||||||
|
TeamId string `protobuf:"bytes,10,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"`
|
||||||
|
TemplateId string `protobuf:"bytes,11,opt,name=template_id,json=templateId,proto3" json:"template_id,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -958,6 +1017,20 @@ func (x *SandboxInfo) GetTimeoutSec() int32 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *SandboxInfo) GetTeamId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.TeamId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SandboxInfo) GetTemplateId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.TemplateId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type WriteFileRequest struct {
|
type WriteFileRequest 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"`
|
||||||
@ -2182,9 +2255,14 @@ func (x *FlushSandboxMetricsResponse) GetPoints_24H() []*MetricPoint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FlattenRootfsRequest struct {
|
type FlattenRootfsRequest 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"`
|
||||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // template name — output written to images/{name}/rootfs.ext4
|
// Deprecated: use team_id + template_id instead.
|
||||||
|
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
// Team UUID that will own the resulting template.
|
||||||
|
TeamId string `protobuf:"bytes,3,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"`
|
||||||
|
// Template UUID for the output.
|
||||||
|
TemplateId string `protobuf:"bytes,4,opt,name=template_id,json=templateId,proto3" json:"template_id,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -2233,6 +2311,20 @@ func (x *FlattenRootfsRequest) GetName() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *FlattenRootfsRequest) GetTeamId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.TeamId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *FlattenRootfsRequest) GetTemplateId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.TemplateId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type FlattenRootfsResponse struct {
|
type FlattenRootfsResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
SizeBytes int64 `protobuf:"varint,1,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"`
|
SizeBytes int64 `protobuf:"varint,1,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"`
|
||||||
@ -2281,7 +2373,7 @@ var File_hostagent_proto protoreflect.FileDescriptor
|
|||||||
|
|
||||||
const file_hostagent_proto_rawDesc = "" +
|
const file_hostagent_proto_rawDesc = "" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\x0fhostagent.proto\x12\fhostagent.v1\"\xc7\x01\n" +
|
"\x0fhostagent.proto\x12\fhostagent.v1\"\x81\x02\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" +
|
||||||
@ -2291,7 +2383,10 @@ const file_hostagent_proto_rawDesc = "" +
|
|||||||
"\vtimeout_sec\x18\x04 \x01(\x05R\n" +
|
"\vtimeout_sec\x18\x04 \x01(\x05R\n" +
|
||||||
"timeoutSec\x12 \n" +
|
"timeoutSec\x12 \n" +
|
||||||
"\fdisk_size_mb\x18\x06 \x01(\x05R\n" +
|
"\fdisk_size_mb\x18\x06 \x01(\x05R\n" +
|
||||||
"diskSizeMb\"g\n" +
|
"diskSizeMb\x12\x17\n" +
|
||||||
|
"\ateam_id\x18\a \x01(\tR\x06teamId\x12\x1f\n" +
|
||||||
|
"\vtemplate_id\x18\b \x01(\tR\n" +
|
||||||
|
"templateId\"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" +
|
||||||
@ -2314,17 +2409,23 @@ const file_hostagent_proto_rawDesc = "" +
|
|||||||
"\n" +
|
"\n" +
|
||||||
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x16\n" +
|
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x16\n" +
|
||||||
"\x06status\x18\x02 \x01(\tR\x06status\x12\x17\n" +
|
"\x06status\x18\x02 \x01(\tR\x06status\x12\x17\n" +
|
||||||
"\ahost_ip\x18\x03 \x01(\tR\x06hostIp\"J\n" +
|
"\ahost_ip\x18\x03 \x01(\tR\x06hostIp\"\x84\x01\n" +
|
||||||
"\x15CreateSnapshotRequest\x12\x1d\n" +
|
"\x15CreateSnapshotRequest\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" +
|
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" +
|
||||||
"\x04name\x18\x02 \x01(\tR\x04name\"K\n" +
|
"\x04name\x18\x02 \x01(\tR\x04name\x12\x17\n" +
|
||||||
|
"\ateam_id\x18\x03 \x01(\tR\x06teamId\x12\x1f\n" +
|
||||||
|
"\vtemplate_id\x18\x04 \x01(\tR\n" +
|
||||||
|
"templateId\"K\n" +
|
||||||
"\x16CreateSnapshotResponse\x12\x12\n" +
|
"\x16CreateSnapshotResponse\x12\x12\n" +
|
||||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" +
|
"\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"size_bytes\x18\x02 \x01(\x03R\tsizeBytes\"+\n" +
|
"size_bytes\x18\x02 \x01(\x03R\tsizeBytes\"e\n" +
|
||||||
"\x15DeleteSnapshotRequest\x12\x12\n" +
|
"\x15DeleteSnapshotRequest\x12\x12\n" +
|
||||||
"\x04name\x18\x01 \x01(\tR\x04name\"\x18\n" +
|
"\x04name\x18\x01 \x01(\tR\x04name\x12\x17\n" +
|
||||||
|
"\ateam_id\x18\x02 \x01(\tR\x06teamId\x12\x1f\n" +
|
||||||
|
"\vtemplate_id\x18\x03 \x01(\tR\n" +
|
||||||
|
"templateId\"\x18\n" +
|
||||||
"\x16DeleteSnapshotResponse\"s\n" +
|
"\x16DeleteSnapshotResponse\"s\n" +
|
||||||
"\vExecRequest\x12\x1d\n" +
|
"\vExecRequest\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
@ -2340,7 +2441,7 @@ const file_hostagent_proto_rawDesc = "" +
|
|||||||
"\x14ListSandboxesRequest\"\x87\x01\n" +
|
"\x14ListSandboxesRequest\"\x87\x01\n" +
|
||||||
"\x15ListSandboxesResponse\x127\n" +
|
"\x15ListSandboxesResponse\x127\n" +
|
||||||
"\tsandboxes\x18\x01 \x03(\v2\x19.hostagent.v1.SandboxInfoR\tsandboxes\x125\n" +
|
"\tsandboxes\x18\x01 \x03(\v2\x19.hostagent.v1.SandboxInfoR\tsandboxes\x125\n" +
|
||||||
"\x17auto_paused_sandbox_ids\x18\x02 \x03(\tR\x14autoPausedSandboxIds\"\xa4\x02\n" +
|
"\x17auto_paused_sandbox_ids\x18\x02 \x03(\tR\x14autoPausedSandboxIds\"\xde\x02\n" +
|
||||||
"\vSandboxInfo\x12\x1d\n" +
|
"\vSandboxInfo\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" +
|
||||||
@ -2352,7 +2453,11 @@ const file_hostagent_proto_rawDesc = "" +
|
|||||||
"\x0fcreated_at_unix\x18\a \x01(\x03R\rcreatedAtUnix\x12-\n" +
|
"\x0fcreated_at_unix\x18\a \x01(\x03R\rcreatedAtUnix\x12-\n" +
|
||||||
"\x13last_active_at_unix\x18\b \x01(\x03R\x10lastActiveAtUnix\x12\x1f\n" +
|
"\x13last_active_at_unix\x18\b \x01(\x03R\x10lastActiveAtUnix\x12\x1f\n" +
|
||||||
"\vtimeout_sec\x18\t \x01(\x05R\n" +
|
"\vtimeout_sec\x18\t \x01(\x05R\n" +
|
||||||
"timeoutSec\"_\n" +
|
"timeoutSec\x12\x17\n" +
|
||||||
|
"\ateam_id\x18\n" +
|
||||||
|
" \x01(\tR\x06teamId\x12\x1f\n" +
|
||||||
|
"\vtemplate_id\x18\v \x01(\tR\n" +
|
||||||
|
"templateId\"_\n" +
|
||||||
"\x10WriteFileRequest\x12\x1d\n" +
|
"\x10WriteFileRequest\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" +
|
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" +
|
||||||
@ -2427,11 +2532,14 @@ const file_hostagent_proto_rawDesc = "" +
|
|||||||
"points_10m\x18\x01 \x03(\v2\x19.hostagent.v1.MetricPointR\tpoints10m\x126\n" +
|
"points_10m\x18\x01 \x03(\v2\x19.hostagent.v1.MetricPointR\tpoints10m\x126\n" +
|
||||||
"\tpoints_2h\x18\x02 \x03(\v2\x19.hostagent.v1.MetricPointR\bpoints2h\x128\n" +
|
"\tpoints_2h\x18\x02 \x03(\v2\x19.hostagent.v1.MetricPointR\bpoints2h\x128\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"points_24h\x18\x03 \x03(\v2\x19.hostagent.v1.MetricPointR\tpoints24h\"I\n" +
|
"points_24h\x18\x03 \x03(\v2\x19.hostagent.v1.MetricPointR\tpoints24h\"\x83\x01\n" +
|
||||||
"\x14FlattenRootfsRequest\x12\x1d\n" +
|
"\x14FlattenRootfsRequest\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" +
|
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x12\n" +
|
||||||
"\x04name\x18\x02 \x01(\tR\x04name\"6\n" +
|
"\x04name\x18\x02 \x01(\tR\x04name\x12\x17\n" +
|
||||||
|
"\ateam_id\x18\x03 \x01(\tR\x06teamId\x12\x1f\n" +
|
||||||
|
"\vtemplate_id\x18\x04 \x01(\tR\n" +
|
||||||
|
"templateId\"6\n" +
|
||||||
"\x15FlattenRootfsResponse\x12\x1d\n" +
|
"\x15FlattenRootfsResponse\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"size_bytes\x18\x01 \x01(\x03R\tsizeBytes2\xc8\f\n" +
|
"size_bytes\x18\x01 \x01(\x03R\tsizeBytes2\xc8\f\n" +
|
||||||
|
|||||||
@ -73,7 +73,7 @@ message CreateSandboxRequest {
|
|||||||
// Sandbox ID assigned by the control plane. If empty, the host agent generates one.
|
// Sandbox ID assigned by the control plane. If empty, the host agent generates one.
|
||||||
string sandbox_id = 5;
|
string sandbox_id = 5;
|
||||||
|
|
||||||
// Template name (e.g., "minimal", "python311"). Determines base rootfs.
|
// Deprecated: use team_id + template_id instead.
|
||||||
string template = 1;
|
string template = 1;
|
||||||
|
|
||||||
// Number of virtual CPUs (default: 1).
|
// Number of virtual CPUs (default: 1).
|
||||||
@ -89,6 +89,12 @@ message CreateSandboxRequest {
|
|||||||
// Disk size in MB for the rootfs. Base images are expanded to this size
|
// Disk size in MB for the rootfs. Base images are expanded to this size
|
||||||
// at host agent startup. Default: 5120 (5 GB).
|
// at host agent startup. Default: 5120 (5 GB).
|
||||||
int32 disk_size_mb = 6;
|
int32 disk_size_mb = 6;
|
||||||
|
|
||||||
|
// Team UUID that owns the template (hex string). All-zeros = platform.
|
||||||
|
string team_id = 7;
|
||||||
|
|
||||||
|
// Template UUID (hex string). Both zeros + team zeros = "minimal" sentinel.
|
||||||
|
string template_id = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreateSandboxResponse {
|
message CreateSandboxResponse {
|
||||||
@ -125,7 +131,12 @@ message ResumeSandboxResponse {
|
|||||||
|
|
||||||
message CreateSnapshotRequest {
|
message CreateSnapshotRequest {
|
||||||
string sandbox_id = 1;
|
string sandbox_id = 1;
|
||||||
|
// Deprecated: use team_id + template_id instead.
|
||||||
string name = 2;
|
string name = 2;
|
||||||
|
// Team UUID that will own the new template.
|
||||||
|
string team_id = 3;
|
||||||
|
// Template UUID for the new snapshot template.
|
||||||
|
string template_id = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreateSnapshotResponse {
|
message CreateSnapshotResponse {
|
||||||
@ -134,7 +145,12 @@ message CreateSnapshotResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message DeleteSnapshotRequest {
|
message DeleteSnapshotRequest {
|
||||||
|
// Deprecated: use team_id + template_id instead.
|
||||||
string name = 1;
|
string name = 1;
|
||||||
|
// Team UUID that owns the template.
|
||||||
|
string team_id = 2;
|
||||||
|
// Template UUID to delete.
|
||||||
|
string template_id = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DeleteSnapshotResponse {}
|
message DeleteSnapshotResponse {}
|
||||||
@ -166,6 +182,7 @@ message ListSandboxesResponse {
|
|||||||
message SandboxInfo {
|
message SandboxInfo {
|
||||||
string sandbox_id = 1;
|
string sandbox_id = 1;
|
||||||
string status = 2;
|
string status = 2;
|
||||||
|
// Deprecated: use team_id + template_id instead.
|
||||||
string template = 3;
|
string template = 3;
|
||||||
int32 vcpus = 4;
|
int32 vcpus = 4;
|
||||||
int32 memory_mb = 5;
|
int32 memory_mb = 5;
|
||||||
@ -173,6 +190,8 @@ message SandboxInfo {
|
|||||||
int64 created_at_unix = 7;
|
int64 created_at_unix = 7;
|
||||||
int64 last_active_at_unix = 8;
|
int64 last_active_at_unix = 8;
|
||||||
int32 timeout_sec = 9;
|
int32 timeout_sec = 9;
|
||||||
|
string team_id = 10;
|
||||||
|
string template_id = 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
message WriteFileRequest {
|
message WriteFileRequest {
|
||||||
@ -299,7 +318,12 @@ message FlushSandboxMetricsResponse {
|
|||||||
|
|
||||||
message FlattenRootfsRequest {
|
message FlattenRootfsRequest {
|
||||||
string sandbox_id = 1;
|
string sandbox_id = 1;
|
||||||
string name = 2; // template name — output written to images/{name}/rootfs.ext4
|
// Deprecated: use team_id + template_id instead.
|
||||||
|
string name = 2;
|
||||||
|
// Team UUID that will own the resulting template.
|
||||||
|
string team_id = 3;
|
||||||
|
// Template UUID for the output.
|
||||||
|
string template_id = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message FlattenRootfsResponse {
|
message FlattenRootfsResponse {
|
||||||
|
|||||||
Reference in New Issue
Block a user