Files
mcr/internal/oci/manifest.go
Kyle Isom d5580f01f2 Migrate module path from kyle/ to mc/ org
All import paths updated to git.wntrmute.dev/mc/. Bumps mcdsl to v1.2.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:05:59 -07:00

229 lines
6.8 KiB
Go

package oci
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"git.wntrmute.dev/mc/mcr/internal/auth"
"git.wntrmute.dev/mc/mcr/internal/db"
"git.wntrmute.dev/mc/mcr/internal/policy"
)
func (h *Handler) handleManifestGet(w http.ResponseWriter, r *http.Request, repo, reference string) {
if !h.checkPolicy(w, r, policy.ActionPull, repo) {
return
}
m, ok := h.resolveManifest(w, repo, reference)
if !ok {
return
}
h.audit(r, "manifest_pulled", repo, m.Digest)
w.Header().Set("Content-Type", m.MediaType)
w.Header().Set("Docker-Content-Digest", m.Digest)
w.Header().Set("Content-Length", strconv.FormatInt(m.Size, 10))
w.WriteHeader(http.StatusOK)
_, _ = w.Write(m.Content)
}
func (h *Handler) handleManifestHead(w http.ResponseWriter, r *http.Request, repo, reference string) {
if !h.checkPolicy(w, r, policy.ActionPull, repo) {
return
}
m, ok := h.resolveManifest(w, repo, reference)
if !ok {
return
}
w.Header().Set("Content-Type", m.MediaType)
w.Header().Set("Docker-Content-Digest", m.Digest)
w.Header().Set("Content-Length", strconv.FormatInt(m.Size, 10))
w.WriteHeader(http.StatusOK)
}
// resolveManifest looks up a manifest by tag or digest, writing OCI errors on failure.
func (h *Handler) resolveManifest(w http.ResponseWriter, repo, reference string) (*db.ManifestRow, bool) {
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 nil, false
}
h.log.Error("manifest resolve: lookup repository", "error", err, "repo", repo)
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
return nil, false
}
var m *db.ManifestRow
if isDigest(reference) {
m, err = h.db.GetManifestByDigest(repoID, reference)
} else {
m, err = h.db.GetManifestByTag(repoID, reference)
}
if err != nil {
if errors.Is(err, db.ErrManifestNotFound) {
writeOCIError(w, "MANIFEST_UNKNOWN", http.StatusNotFound,
fmt.Sprintf("manifest %q not found", reference))
return nil, false
}
h.log.Error("manifest resolve: lookup manifest", "error", err, "repo", repo, "reference", reference)
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
return nil, false
}
return m, true
}
// ociManifest is the minimal structure for parsing an OCI image manifest.
type ociManifest struct {
SchemaVersion int `json:"schemaVersion"`
MediaType string `json:"mediaType,omitempty"`
Config ociDescriptor `json:"config"`
Layers []ociDescriptor `json:"layers"`
}
// ociDescriptor is a content-addressed reference within a manifest.
type ociDescriptor struct {
MediaType string `json:"mediaType"`
Digest string `json:"digest"`
Size int64 `json:"size"`
}
// supportedManifestMediaTypes lists the manifest media types MCR accepts.
var supportedManifestMediaTypes = map[string]bool{
"application/vnd.oci.image.manifest.v1+json": true,
"application/vnd.docker.distribution.manifest.v2+json": true,
}
// handleManifestPut handles PUT /v2/<name>/manifests/<reference>
func (h *Handler) handleManifestPut(w http.ResponseWriter, r *http.Request, repo, reference string) {
if !h.checkPolicy(w, r, policy.ActionPush, repo) {
return
}
// Step 1: Read and parse manifest JSON.
body, err := io.ReadAll(r.Body)
if err != nil {
writeOCIError(w, "MANIFEST_INVALID", http.StatusBadRequest, "failed to read request body")
return
}
if len(body) == 0 {
writeOCIError(w, "MANIFEST_INVALID", http.StatusBadRequest, "empty manifest")
return
}
var manifest ociManifest
if err := json.Unmarshal(body, &manifest); err != nil {
writeOCIError(w, "MANIFEST_INVALID", http.StatusBadRequest, "malformed manifest JSON")
return
}
if manifest.SchemaVersion != 2 {
writeOCIError(w, "MANIFEST_INVALID", http.StatusBadRequest, "unsupported schema version")
return
}
// Determine media type from Content-Type header, falling back to manifest body.
mediaType := r.Header.Get("Content-Type")
if mediaType == "" {
mediaType = manifest.MediaType
}
if mediaType == "" {
mediaType = "application/vnd.oci.image.manifest.v1+json"
}
if !supportedManifestMediaTypes[mediaType] {
writeOCIError(w, "MANIFEST_INVALID", http.StatusBadRequest,
fmt.Sprintf("unsupported media type: %s", mediaType))
return
}
// Step 2: Compute SHA-256 digest.
sum := sha256.Sum256(body)
computedDigest := "sha256:" + hex.EncodeToString(sum[:])
// Step 3: If reference is a digest, verify it matches.
tag := ""
if isDigest(reference) {
if reference != computedDigest {
writeOCIError(w, "DIGEST_INVALID", http.StatusBadRequest,
fmt.Sprintf("digest mismatch: computed %s, got %s", computedDigest, reference))
return
}
} else {
tag = reference
}
// Step 4: Collect all referenced blob digests.
var blobDigests []string
if manifest.Config.Digest != "" {
blobDigests = append(blobDigests, manifest.Config.Digest)
}
for _, layer := range manifest.Layers {
if layer.Digest != "" {
blobDigests = append(blobDigests, layer.Digest)
}
}
// Step 5: Verify all referenced blobs exist.
for _, bd := range blobDigests {
exists, err := h.db.BlobExists(bd)
if err != nil {
h.log.Error("manifest push: check blob exists", "error", err, "repo", repo, "blob_digest", bd)
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
return
}
if !exists {
writeOCIError(w, "MANIFEST_BLOB_UNKNOWN", http.StatusBadRequest,
fmt.Sprintf("blob %s not found", bd))
return
}
}
// Step 6: Single transaction — create repo, insert manifest, populate
// manifest_blobs, upsert tag.
params := db.PushManifestParams{
RepoName: repo,
Digest: computedDigest,
MediaType: mediaType,
Content: body,
Size: int64(len(body)),
Tag: tag,
BlobDigests: blobDigests,
}
if err := h.db.PushManifest(params); err != nil {
h.log.Error("manifest push: write to database", "error", err, "repo", repo, "digest", computedDigest, "tag", tag)
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
return
}
// Step 7: Audit and respond.
details := map[string]string{}
if tag != "" {
details["tag"] = tag
}
if h.auditFn != nil {
claims := auth.ClaimsFromContext(r.Context())
actorID := ""
if claims != nil {
actorID = claims.Subject
}
h.auditFn("manifest_pushed", actorID, repo, computedDigest, r.RemoteAddr, details)
}
h.log.Info("manifest pushed", "repo", repo, "digest", computedDigest, "tag", tag, "size", len(body))
w.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", repo, computedDigest))
w.Header().Set("Docker-Content-Digest", computedDigest)
w.Header().Set("Content-Type", mediaType)
w.WriteHeader(http.StatusCreated)
}