diff --git a/frontend/src/lib/api/builds.ts b/frontend/src/lib/api/builds.ts index bfa69fa..900acf2 100644 --- a/frontend/src/lib/api/builds.ts +++ b/frontend/src/lib/api/builds.ts @@ -64,3 +64,7 @@ export type AdminTemplate = { export async function listAdminTemplates(): Promise> { return apiFetch('GET', '/api/v1/admin/templates'); } + +export async function deleteAdminTemplate(name: string): Promise> { + return apiFetch('DELETE', `/api/v1/admin/templates/${name}`); +} diff --git a/frontend/src/routes/admin/templates/+page.svelte b/frontend/src/routes/admin/templates/+page.svelte index c320ea8..0d719bd 100644 --- a/frontend/src/routes/admin/templates/+page.svelte +++ b/frontend/src/routes/admin/templates/+page.svelte @@ -3,11 +3,11 @@ import { onMount, onDestroy } from 'svelte'; import { toast } from '$lib/toast.svelte'; import { formatDate, timeAgo } from '$lib/utils/format'; - import { deleteSnapshot } from '$lib/api/capsules'; import { listBuilds, createBuild, listAdminTemplates, + deleteAdminTemplate, type Build, type BuildLogEntry, type AdminTemplate @@ -145,7 +145,7 @@ deleting = true; deleteError = null; const name = deleteTarget.name; - const result = await deleteSnapshot(name); + const result = await deleteAdminTemplate(name); if (result.ok) { templates = templates.filter((t) => t.name !== name); deleteTarget = null; @@ -413,14 +413,12 @@ - {#if tmpl.type === 'snapshot'} - - {/if} + {/each} diff --git a/internal/api/handlers_builds.go b/internal/api/handlers_builds.go index 61eebe7..8b8fd5c 100644 --- a/internal/api/handlers_builds.go +++ b/internal/api/handlers_builds.go @@ -7,21 +7,25 @@ import ( "net/http" "time" + "connectrpc.com/connect" "github.com/go-chi/chi/v5" "git.omukk.dev/wrenn/sandbox/internal/db" "git.omukk.dev/wrenn/sandbox/internal/id" + "git.omukk.dev/wrenn/sandbox/internal/lifecycle" "git.omukk.dev/wrenn/sandbox/internal/service" "git.omukk.dev/wrenn/sandbox/internal/validate" + pb "git.omukk.dev/wrenn/sandbox/proto/hostagent/gen" ) type buildHandler struct { - svc *service.BuildService - db *db.Queries + svc *service.BuildService + db *db.Queries + pool *lifecycle.HostClientPool } -func newBuildHandler(svc *service.BuildService, db *db.Queries) *buildHandler { - return &buildHandler{svc: svc, db: db} +func newBuildHandler(svc *service.BuildService, db *db.Queries, pool *lifecycle.HostClientPool) *buildHandler { + return &buildHandler{svc: svc, db: db, pool: pool} } type createBuildRequest struct { @@ -202,3 +206,42 @@ func (h *buildHandler) ListTemplates(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, resp) } + +// DeleteTemplate handles DELETE /v1/admin/templates/{name}. +func (h *buildHandler) DeleteTemplate(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + if err := validate.SafeName(name); err != nil { + writeError(w, http.StatusBadRequest, "invalid_request", fmt.Sprintf("invalid template name: %s", err)) + return + } + ctx := r.Context() + + if _, err := h.db.GetTemplate(ctx, name); err != nil { + writeError(w, http.StatusNotFound, "not_found", "template not found") + return + } + + // Broadcast delete to all online hosts. + hosts, _ := h.db.ListActiveHosts(ctx) + for _, host := range hosts { + if host.Status != "online" { + continue + } + agent, err := h.pool.GetForHost(host) + if err != nil { + continue + } + if _, err := agent.DeleteSnapshot(ctx, connect.NewRequest(&pb.DeleteSnapshotRequest{Name: name})); err != nil { + 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) + } + } + } + + if err := h.db.DeleteTemplate(ctx, name); err != nil { + writeError(w, http.StatusInternalServerError, "db_error", "failed to delete template record") + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/internal/api/server.go b/internal/api/server.go index 1be4473..d298b29 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -67,7 +67,7 @@ func New( auditH := newAuditHandler(auditSvc) statsH := newStatsHandler(statsSvc) metricsH := newSandboxMetricsHandler(queries, pool) - buildH := newBuildHandler(buildSvc, queries) + buildH := newBuildHandler(buildSvc, queries, pool) // OpenAPI spec and docs. r.Get("/openapi.yaml", serveOpenAPI) @@ -178,6 +178,7 @@ func New( r.Use(requireAdmin(queries)) r.Put("/teams/{id}/byoc", teamH.SetBYOC) r.Get("/templates", buildH.ListTemplates) + r.Delete("/templates/{name}", buildH.DeleteTemplate) r.Post("/builds", buildH.Create) r.Get("/builds", buildH.List) r.Get("/builds/{id}", buildH.Get)