Files
mcr/internal/oci/delete.go
Kyle Isom ef39152f4e Add structured error logging to OCI handlers
Every 500 response in the OCI package silently discarded the actual
error, making production debugging impossible. Add slog.Error before
each 500 response with the error and relevant context (repo, digest,
tag, uuid). Add slog.Info for state-mutating successes (manifest push,
blob upload complete, deletions).

Logger is injected into the OCI Handler via constructor, falling back
to slog.Default() if nil.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:47:44 -07:00

93 lines
2.9 KiB
Go

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
}
h.log.Error("manifest delete: lookup repository", "error", err, "repo", repo)
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
}
h.log.Error("manifest delete: delete from database", "error", err, "repo", repo, "digest", reference)
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
return
}
h.audit(r, "manifest_deleted", repo, reference)
h.log.Info("manifest deleted", "repo", repo, "digest", 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
}
h.log.Error("blob delete: lookup repository", "error", err, "repo", repo)
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
}
h.log.Error("blob delete: delete from database", "error", err, "repo", repo, "digest", digest)
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
return
}
h.audit(r, "blob_deleted", repo, digest)
h.log.Info("blob deleted", "repo", repo, "digest", digest)
w.WriteHeader(http.StatusAccepted)
}