feat(managementapi): serve the container bulk delete routes

Why

The three container bulk-delete operations are declared in the contract and answer the placeholder 501. The worker that applies container batches sits on the target branch, so serving the routes closes the container slice of S17 bulk delete: decode the selector, resolve the parent chain, enqueue one batch through the shared BulkEnqueuer seam, answer 202, and let the job apply it.

Stacked on chore(managementapi): apply container bulk mani... (!1583 - merged) • Hayley Swimelar • 19.4.

What (non-obvious parts)

  • Order per the spec's Bulk delete section: family guard (reads no row), then body decode, then parent resolve, then enqueue. A malformed body wins over a missing repository's 404, and an out-of-family {format} answers 404 whatever the body carries.
  • The shared enqueue seam (Deps.BulkEnqueuer) and the riverClientHolder threading land upstream with the npm bulk routes; this MR consumes them. An enqueuer built over a cell the jobs tier never publishes into answers 503 for the life of the pod with every unit test green, so the boot-level integration test drives a request through the composed app into river_job, and a mutation run (fresh holder at the call site) failed all three walks on exactly that miswire.
  • Tag-name subsets chunk through ChunkTagNameJobs: 1,000 max-length names encode past the 64 KiB payload cap. Image-id and digest subsets stay one job, digests deliberately (the worker applies manifests in dependency order, which only one job preserves).
  • Routes are enqueue-only: no usage-data events, no counter writes. The worker emits per entry applied.

Test plan

  • go test ./internal/managementapi/ ./cmd/artifact-registry/: route dispatch, job mapping (Validate per job, acceptance stamping, chunk round-trip), guard/decode/resolve ordering, selector rejections (all-zeros UUID, uppercase UUID, empty and over-long tag names, non-canonical digests, over-cap batches), 503/500 arms, and OpenAPI contract validation per status.
  • go test -tags=integration ./cmd/artifact-registry/ -run TestIntegration_ContainerBulkRoutes_ApplyThroughTheComposedApp: boots the composition root over live PostgreSQL and Redis and walks each route request → job → drained collection, including a tag literally named delete_all removed individually with the image's other tags surviving.
  • The contract sweep now serves the six container bulk arms (pending entries drained), and the write-route sweeps assert the rows as no-longer-placeholder.

The diff is about 1,910 added lines, 290 of them production (bulk_container.go plus the handler registrations and one wiring comment), and the rest is tests. Splitting would separate the routes from the suites that define their contract.

No e2e catalog edit: the delete-family catalog pass rides E2e lifecycle catalogs: one delete-family pass ... (#638) • Unassigned.

Context for LLM reviewers

Design rationale:

  • Decode-before-resolve follows the spec sentence "The body is validated before the route's own path segments and any artifact work". The family guard runs ahead of the decode, which reads against that sentence's letter for the {format} segment: the contract's containerFormat parameter resolves it ("a segment outside the list is a request this contract does not describe: the service answers 404 there too"), the guard reads no row, and the read routes and placeholders already answer that 404. TestHandler_ContainerWriteRoutes_AnswerByFormatFamily pins the order with an empty body and TestBulkContainerDeletes_FamilyGuardPrecedesTheDecode with a malformed one. The spec sentence deserves a scoping touch-up ("the artifact identifier segments") in a spec MR of its own.
  • AcceptedAt is stamped inside the batch builder, per the BulkDeleteBatchFunc contract: jobs are built at handler time, each stamped on a copy inside the enqueuer's transaction, and the builder performs no I/O there.
  • BulkDeleteContainerArgs.RepositoryID carries the repositories row id, not the container child id: the container bulk worker re-resolves the child row live at application time.
  • The compile-time BulkEnqueuer assertion stays beside the adapter in cmd/artifact-registry/bulk_enqueuer.go; the assertion anchor in wire_management.go points there instead of duplicating it.

Non-goals a reviewer might raise:

  • No OpenAPI or Bruno change: the operations, request schemas, and the 503 response component exist since the contract step. This MR drains contractPendingOperations and updates the sweeps only.
  • No route-side usage-data events or counters: the worker owns application and emits applied-gated events.
  • resolve.go's cancelled-request 500 classification gap is pre-existing and untouched here.
  • The ten --build-tags=integration golangci findings left in the package pair (noinlineerr, testifylint, wsl) predate this MR and live in the npm and oci boot-test files it does not touch; this MR's own files lint clean under --build-tags=integration --max-same-issues=0 --max-issues-per-linter=0.

Related to #313 (closed)

Edited by Hayley Swimelar

Merge request reports

Loading
Loading