feat(oci): blob DELETE endpoint (S12 Step 11)

Why

S12 Step 11 implements the OCI blob DELETE endpoint (DELETE /v2/<slug>/container/<repo>/<image>/blobs/<digest>). Without it the dispatcher leaves DELETE on the interim 501 path and the blob-delete conformance spec fails.

Per the OCI-local plan Step 11 and S12 Blob Delete: hard-delete the container_blobs row and its blob_storage_attachments link in one transaction, return 202, and leave the shared CAS blob_storage_blobs row for S20 GC (other namespaces may still reference it).

Stacked on !415 (merged) (Step 10). This MR targets hswimelar/oci-local-step-10 so the diff is only Step 11's delta. Retarget to main after !415 (merged) merges (GitLab auto-retargets on merge).

Size. ~490 LOC of production code; the remainder is the unit + integration suite and the boot smoke test. Over the 500 reviewable-LOC target in development-model.md, but the overage is test code, which reviews faster than the production pace the ceiling is calibrated against.

What (non-obvious)

  • The row-pair hard-delete lives in datastore.ContainerBlobUnlinker (the inverse of Step 9's ContainerBlobLinker), keeping raw SQL in the datastore layer per ADR-023. The container_blobs DELETE is the serialization point: a concurrent DELETE of the same digest that removes no row maps to 404 BLOB_UNKNOWN, so concurrent DELETE is idempotent (exactly one 202, one 404).
  • The attachment delete carries the sha256 partition key (blob_storage_attachments is HASH(sha256), not HASH(namespace_id)) so it prunes to one partition, and the unlink reads its row on the transaction's connection so a DELETE holds a single pool connection.
  • A DB-backed boot smoke test exercises the composition-root wiring of the delete-capable handler (the development_stubs-gated path the unit tests cannot reach).

Test plan

  • go test -race -short ./internal/format/oci/... ./internal/datastore/... ./cmd/artifact-registry/... and the -tags=integration suite pass locally.
  • Conformance: the blob-delete spec turns green; the delta is recorded from CI's conformance:oci JUnit artifact.

Spec coverage

Spec: docs/specs/S12-container-oci-local.md

# Item Tests
AC-26 Blob delete TestBlobDelete_Success, TestBlobDeleteLeavesCASIntact, TestBlobDeleteThenGetReturnsBlobUnknown, TestConcurrentDeleteSameBlobIdempotent
P-5 Delete invariant TestBlobDeleteLeavesCASIntact, TestBlobDeleteThenGetReturnsBlobUnknown, TestConcurrentDeleteSameBlobIdempotent
E-1 BLOB_UNKNOWN (404) TestBlobDelete_NotLinkedReturns404BlobUnknown, TestBlobDelete_UnlinkNoopReturns404BlobUnknown, TestBlobDeleteThenGetReturnsBlobUnknown
E-10 NAME_UNKNOWN (404) TestBlobDelete_ImageMissingReturns404NameUnknown, TestBlobDelete_RepositoryMissingReturns404NameUnknown
E-18 INTERNAL (500, no leak) TestBlobDelete_UnlinkFailureReturns500
S-3 Digest format TestBlobDelete_MalformedDigestReturns400, TestBlobDelete_UnsupportedAlgorithmReturns400

E-17 UNAVAILABLE (503, GC-lock) is owned by S20 and out of scope per the plan.

Context for reviewers and agents

Non-goals (deferred, not omissions):

  • Physical reclamation of the CAS blob_storage_blobs object. Blob DELETE removes only the format row pair; cross-namespace reachability and GC land in S20.
  • Soft-delete. S12 is hard-delete-only per the plan's Decisions; the container_* tables have no soft_deleted_at.
  • The 503 GC-lock-contention path (E-17), which ships with S20's delete lock.

The final commit (fix(oci): prune the blob-DELETE attachment delete ...) applies /review-branch findings: it adds the sha256 partition key to the attachment delete (was scanning all 64 partitions), moves the unlink's pre-delete read onto the transaction connection (was pinning two pool connections per DELETE), and adds a regression guard that fails if the partition-key predicate is dropped.

Related to #19 (closed)

Edited by Hayley Swimelar

Merge request reports

Loading
Loading