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>
93 lines
2.9 KiB
Go
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)
|
|
}
|