Phase 7: OCI delete path for manifests and blobs
Manifest delete (DELETE /v2/<name>/manifests/<digest>): rejects tag references with 405 UNSUPPORTED per OCI spec, cascades to tags and manifest_blobs via ON DELETE CASCADE, returns 202 Accepted. Blob delete (DELETE /v2/<name>/blobs/<digest>): removes manifest_blobs associations only — blob row and file are preserved for GC to handle, since other repos may reference the same content-addressed blob. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
84
internal/oci/delete.go
Normal file
84
internal/oci/delete.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.wntrmute.dev/kyle/mcr/internal/db"
|
||||
"git.wntrmute.dev/kyle/mcr/internal/policy"
|
||||
)
|
||||
|
||||
// handleManifestDelete handles DELETE /v2/<name>/manifests/<digest>.
|
||||
// Per OCI spec, deletion by tag is not supported — only by digest.
|
||||
func (h *Handler) handleManifestDelete(w http.ResponseWriter, r *http.Request, repo, reference string) {
|
||||
if !h.checkPolicy(w, r, policy.ActionDelete, repo) {
|
||||
return
|
||||
}
|
||||
|
||||
// Reference must be a digest, not a tag.
|
||||
if !isDigest(reference) {
|
||||
writeOCIError(w, "UNSUPPORTED", http.StatusMethodNotAllowed,
|
||||
"manifest deletion by tag is not supported; use digest")
|
||||
return
|
||||
}
|
||||
|
||||
repoID, err := h.db.GetRepositoryByName(repo)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrRepoNotFound) {
|
||||
writeOCIError(w, "NAME_UNKNOWN", http.StatusNotFound,
|
||||
fmt.Sprintf("repository %q not found", repo))
|
||||
return
|
||||
}
|
||||
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.DeleteManifest(repoID, reference); err != nil {
|
||||
if errors.Is(err, db.ErrManifestNotFound) {
|
||||
writeOCIError(w, "MANIFEST_UNKNOWN", http.StatusNotFound,
|
||||
fmt.Sprintf("manifest %q not found", reference))
|
||||
return
|
||||
}
|
||||
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
|
||||
return
|
||||
}
|
||||
|
||||
h.audit(r, "manifest_deleted", repo, reference)
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
// handleBlobDelete handles DELETE /v2/<name>/blobs/<digest>.
|
||||
// Removes manifest_blobs associations for this repo only. Does not delete
|
||||
// the blob row or file — that is GC's responsibility.
|
||||
func (h *Handler) handleBlobDelete(w http.ResponseWriter, r *http.Request, repo, digest string) {
|
||||
if !h.checkPolicy(w, r, policy.ActionDelete, repo) {
|
||||
return
|
||||
}
|
||||
|
||||
repoID, err := h.db.GetRepositoryByName(repo)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrRepoNotFound) {
|
||||
writeOCIError(w, "NAME_UNKNOWN", http.StatusNotFound,
|
||||
fmt.Sprintf("repository %q not found", repo))
|
||||
return
|
||||
}
|
||||
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.DeleteBlobFromRepo(repoID, digest); err != nil {
|
||||
if errors.Is(err, db.ErrBlobNotFound) {
|
||||
writeOCIError(w, "BLOB_UNKNOWN", http.StatusNotFound,
|
||||
fmt.Sprintf("blob %q not found in repository", digest))
|
||||
return
|
||||
}
|
||||
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
|
||||
return
|
||||
}
|
||||
|
||||
h.audit(r, "blob_deleted", repo, digest)
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
Reference in New Issue
Block a user