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'sContainerBlobLinker), keeping raw SQL in the datastore layer per ADR-023. Thecontainer_blobsDELETE is the serialization point: a concurrent DELETE of the same digest that removes no row maps to 404BLOB_UNKNOWN, so concurrent DELETE is idempotent (exactly one 202, one 404). - The attachment delete carries the
sha256partition key (blob_storage_attachmentsisHASH(sha256), notHASH(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=integrationsuite pass locally.- Conformance: the blob-delete spec turns green; the delta is recorded from CI's
conformance:ociJUnit 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_blobsobject. 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 nosoft_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)