Files
mcr/internal/oci/blob.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

106 lines
3.2 KiB
Go

package oci
import (
"errors"
"fmt"
"io"
"net/http"
"strconv"
"git.wntrmute.dev/kyle/mcr/internal/db"
"git.wntrmute.dev/kyle/mcr/internal/policy"
)
func (h *Handler) handleBlobGet(w http.ResponseWriter, r *http.Request, repo, digest string) {
if !h.checkPolicy(w, r, policy.ActionPull, 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 get: lookup repository", "error", err, "repo", repo)
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
return
}
exists, err := h.db.BlobExistsInRepo(repoID, digest)
if err != nil {
h.log.Error("blob get: check blob exists in repo", "error", err, "repo", repo, "digest", digest)
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
return
}
if !exists {
writeOCIError(w, "BLOB_UNKNOWN", http.StatusNotFound,
fmt.Sprintf("blob %q not found in repository", digest))
return
}
size, err := h.blobs.Stat(digest)
if err != nil {
h.log.Error("blob get: stat blob in storage", "error", err, "repo", repo, "digest", digest)
writeOCIError(w, "BLOB_UNKNOWN", http.StatusNotFound, "blob not found in storage")
return
}
rc, err := h.blobs.Open(digest)
if err != nil {
h.log.Error("blob get: open blob in storage", "error", err, "repo", repo, "digest", digest)
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
return
}
defer func() { _ = rc.Close() }()
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Docker-Content-Digest", digest)
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
w.WriteHeader(http.StatusOK)
_, _ = io.Copy(w, rc)
}
func (h *Handler) handleBlobHead(w http.ResponseWriter, r *http.Request, repo, digest string) {
if !h.checkPolicy(w, r, policy.ActionPull, 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 head: lookup repository", "error", err, "repo", repo)
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
return
}
exists, err := h.db.BlobExistsInRepo(repoID, digest)
if err != nil {
h.log.Error("blob head: check blob exists in repo", "error", err, "repo", repo, "digest", digest)
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
return
}
if !exists {
writeOCIError(w, "BLOB_UNKNOWN", http.StatusNotFound,
fmt.Sprintf("blob %q not found in repository", digest))
return
}
size, err := h.blobs.Stat(digest)
if err != nil {
h.log.Error("blob head: stat blob in storage", "error", err, "repo", repo, "digest", digest)
writeOCIError(w, "BLOB_UNKNOWN", http.StatusNotFound, "blob not found in storage")
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Docker-Content-Digest", digest)
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
w.WriteHeader(http.StatusOK)
}