feat(submit): give the upload lane a lifecycle — withdraw + admin delete (#76)
Nothing ever removed a submission: users could not retract a pending row, no
route deleted blobs or rows, and the reaper never touches uploads — so every
upload accumulated on the 5 GiB PVC forever and the only cleanup was SQL or
kubectl against the store.
- Blobs.Delete on both transports (local: RemoveAll of the id-namespaced dir,
id re-validated at the boundary; S3: idempotent object DELETE).
- Store: DeleteSubmission (admin, any status) and DeletePendingSubmission
(owner+pending CAS — a reviewed row can never be withdrawn out from under
its build).
- Manager.Delete / Manager.Withdraw delete the ROW first (under the CAS for
withdraw) and the blob after, so a live row can never point at a reaped
blob; a cleanup failure names the orphan explicitly instead of failing mute.
- API: DELETE /me/submissions/{id} (withdraw, app tier) and
DELETE /api/v1/submissions/{id} (admin) both return the row as it was;
audit events submission.withdraw / submission.delete; openapi documents both
paths; admin route pinned in the admin-only table.
- Panel: two-step withdraw on a pending row (frees the pending slot and the
storage budget); two-step delete on every admin row; zh/en copy; wire tests.
Unit: submit (withdraw happy path / wrong owner / reviewed row / no transport /
blob-cleanup failure), local+S3 delete idempotence, api handlers (200/404/409/
503 + route tier); pgint: withdraw CAS + admin delete exactly-once.
go vet/go test/gofmt clean; panel vitest 120 + typecheck green.
This commit is contained in:
20 files changed
+828
-45
No files matched your search
@@ -518,6 +518,11 @@ func (a *API) externalAPIRoutes() []apiRoute {
|
||||
// App-tier and owner-scoped (the id must belong to the principal), exactly
|
||||
// like the create/list routes above.
|
||||
{Method: "POST", Pattern: "/api/v1/me/submissions/{id}/context", h: a.handleUploadSubmissionContext},
|
||||
// Withdraw the caller's OWN pending submission: the row and its uploaded
|
||||
// context are deleted, freeing the pending slot and storage budget. Same
|
||||
// owner-scoping as the upload route — a reviewed submission is frozen (409)
|
||||
// and another user's id is invisible (404).
|
||||
{Method: "DELETE", Pattern: "/api/v1/me/submissions/{id}", h: a.handleWithdrawSubmission},
|
||||
// Admin (Zero-Trust) tier: create / mutate spec / image admission. These gate
|
||||
// on Principal.IsAdmin() inside the handler via the adminOnly wrapper, so the
|
||||
// boundary is exercised even where the body is a later-phase stub.
|
||||
@@ -547,6 +552,10 @@ func (a *API) externalAPIRoutes() []apiRoute {
|
||||
{Method: "GET", Pattern: "/api/v1/submissions", Admin: true, h: a.handleListSubmissions},
|
||||
{Method: "POST", Pattern: "/api/v1/submissions/{id}/approve", Admin: true, h: a.handleApproveSubmission},
|
||||
{Method: "POST", Pattern: "/api/v1/submissions/{id}/reject", Admin: true, h: a.handleRejectSubmission},
|
||||
// Retire a submission outright (row + uploaded context), any status. The
|
||||
// lane's lifecycle valve: without it, rejected/consumed uploads accumulated
|
||||
// on the uploads PVC forever — there is no other delete path.
|
||||
{Method: "DELETE", Pattern: "/api/v1/submissions/{id}", Admin: true, h: a.handleDeleteSubmission},
|
||||
// The reviewer's read path to the uploaded blob: the executed Dockerfile
|
||||
// lives inside it, so approval would otherwise be blind.
|
||||
{Method: "GET", Pattern: "/api/v1/submissions/{id}/context", Admin: true, h: a.handleAdminSubmissionContext},
|
||||
|
||||
Reference in new issue
Block a user