Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 15:49:16 -05:00
pkg-proxy/internal/handler/container_referrers.go
Simon Chrzanowski a2889d40a0 Log per-handler request lines at debug level
Each download and metadata handler logged its own request line at info,
next to the request line LoggerMiddleware already writes for every
request with path, status and duration. That doubled the info log volume
on the hot path. The handler lines only repeat values parsed from the
path, so they move to debug.

Kept at info: the npm audit line (it carries the request body size), the
julia package line (it carries the name resolved from the UUID) and the
gem and hex cooldown lines.
2026-10-25 17:21:11 +01:00

222 lines
8.1 KiB
Go

package handler
import (
"cmp"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"time"
)
const (
containerReferrersCacheEcosystem = "oci-referrers"
containerReferrersMediaType = "application/vnd.oci.image.index.v1+json"
referrersMatchCount = 3 // full match + name + digest
)
var (
// referrersPathPattern matches referrers paths: {name}/referrers/{digest}
referrersPathPattern = regexp.MustCompile(`^(.+)/referrers/([^/]+)$`)
referrersDigestPattern = regexp.MustCompile(`^(sha256:[a-f0-9]{64}|sha512:[a-f0-9]{128})$`)
)
// parseReferrersPath extracts repository name and subject digest from a
// referrers path.
func (h *ContainerHandler) parseReferrersPath(path string) (name, digest string) {
matches := referrersPathPattern.FindStringSubmatch(path)
if len(matches) != referrersMatchCount {
return "", ""
}
return matches[1], matches[2]
}
// handleReferrers serves the OCI 1.1 referrers API.
// Path format: {name}/referrers/{digest}
func (h *ContainerHandler) handleReferrers(w http.ResponseWriter, r *http.Request, path string) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
name, digest := h.parseReferrersPath(path)
if name == "" || !referrersDigestPattern.MatchString(digest) {
h.containerError(w, http.StatusBadRequest, "DIGEST_INVALID", "invalid referrers digest")
return
}
registryURL, upstreamName, _, ok := h.registryForRequest(r, name)
if !ok {
h.containerError(w, http.StatusNotFound, "NAME_UNKNOWN", "unknown upstream registry")
return
}
h.proxy.Logger.Debug("container referrers request", "name", upstreamName, "digest", digest)
h.serveReferrers(w, r, registryURL, upstreamName, digest)
}
func (h *ContainerHandler) serveReferrers(w http.ResponseWriter, r *http.Request, registryURL, name, digest string) {
// artifactType is never forwarded, so the upstream returns the full index
// and one row serves every filter. Without OCI-Filters-Applied in the
// response, clients filter the index themselves, as the spec requires.
// ns only picks the registry, so it is neither forwarded nor part of the
// cache identity.
query := r.URL.Query()
query.Del("artifactType")
query.Del(namespaceQueryParam)
// Same identity shape as manifests (registry, name, reference, variant);
// the oci-referrers ecosystem keeps the rows apart.
cacheKey := h.containerManifestCacheKey(registryURL, name, digest, query.Encode())
cached, err := h.loadContainerReferrers(r.Context(), cacheKey)
if err != nil {
h.proxy.Logger.Warn("failed to read cached container referrers", "error", err)
cached = nil
}
if cached != nil && h.containerTagsFresh(cached) {
h.writeContainerReferrers(w, r, registryURL, cached, false)
return
}
upstreamURL := fmt.Sprintf("%s/v2/%s/referrers/%s", registryURL, name, digest)
if encoded := query.Encode(); encoded != "" {
upstreamURL += "?" + encoded
}
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, upstreamURL, nil)
if err != nil {
h.containerError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to create request")
return
}
req.Header.Set("Accept", containerReferrersMediaType)
if cached != nil && cached.etag != "" {
req.Header.Set("If-None-Match", cached.etag)
}
resp, err := h.proxy.HTTPClient.Do(req)
if err != nil {
h.serveStaleReferrersOrError(w, r, registryURL, cached, err)
return
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode == http.StatusNotModified && cached != nil {
cached.fetchedAt = time.Now()
if err := h.storeContainerReferrers(r.Context(), cacheKey, cached); err != nil {
h.proxy.Logger.Warn("failed to refresh cached container referrers", "error", err)
}
h.writeContainerReferrers(w, r, registryURL, cached, false)
return
}
if resp.StatusCode != http.StatusOK {
if shouldServeStaleManifest(resp.StatusCode) && cached != nil {
h.serveStaleReferrersOrError(w, r, registryURL, cached,
fmt.Errorf("upstream returned status %d", resp.StatusCode))
return
}
// Upstream errors reach the client as sent. Only a 404 tells it the
// registry has no referrers API, so it falls back to the sha256-<hex>
// tag schema served as manifests.
h.proxy.relayResponse(w, r, resp, copyContainerReferrersHeaders)
return
}
body, err := h.proxy.ReadMetadata(resp.Body)
if err != nil {
h.serveStaleReferrersOrError(w, r, registryURL, cached, fmt.Errorf("reading referrers: %w", err))
return
}
if !json.Valid(body) {
h.serveStaleReferrersOrError(w, r, registryURL, cached, errors.New("upstream referrers response is not JSON"))
return
}
referrers := &cachedContainerTags{
body: body,
contentType: resp.Header.Get(headerContentType),
etag: resp.Header.Get(headerETag),
// Stored as sent upstream and rewritten when served, so a row shared
// by several client paths to the same registry links back correctly.
link: strings.Join(resp.Header.Values("Link"), ", "),
size: int64(len(body)),
fetchedAt: time.Now(),
}
if referrers.contentType == "" {
referrers.contentType = containerReferrersMediaType
}
if err := h.storeContainerReferrers(r.Context(), cacheKey, referrers); err != nil {
h.proxy.Logger.Warn("failed to cache container referrers", "error", err)
}
h.writeContainerReferrers(w, r, registryURL, referrers, false)
}
// serveStaleReferrersOrError serves a cached index when the upstream cannot
// answer. Without one it returns 502 rather than 404, so clients do not take
// a failed fetch as a registry without the referrers API.
func (h *ContainerHandler) serveStaleReferrersOrError(w http.ResponseWriter, r *http.Request, registryURL string, cached *cachedContainerTags, err error) {
if cached != nil {
h.proxy.Logger.Warn("upstream referrers fetch failed, serving stale cache", "error", err)
h.writeContainerReferrers(w, r, registryURL, cached, true)
return
}
if writeUpstreamHeld(w, err) {
return
}
h.proxy.Logger.Error("failed to fetch container referrers", "error", err)
h.containerError(w, http.StatusBadGateway, "INTERNAL_ERROR", "failed to fetch from upstream")
}
// copyContainerReferrersHeaders also keeps Retry-After, since uncached 429
// and 5xx answers are relayed here instead of turning into a 502.
func copyContainerReferrersHeaders(destination, source http.Header) {
copyContainerTagsHeaders(destination, source)
if value := source.Get("Retry-After"); value != "" {
destination.Set("Retry-After", value)
}
}
func (h *ContainerHandler) loadContainerReferrers(ctx context.Context, cacheKey string) (*cachedContainerTags, error) {
entry, body, err := h.loadContainerMetadata(ctx, containerReferrersCacheEcosystem, cacheKey)
if err != nil || entry == nil {
return nil, err
}
referrers := &cachedContainerTags{
body: body,
contentType: cmp.Or(entry.ContentType.String, containerReferrersMediaType),
etag: entry.ETag.String,
link: entry.Link.String,
size: int64(len(body)),
fetchedAt: entry.FetchedAt.Time,
}
if entry.Size.Valid {
referrers.size = entry.Size.Int64
}
return referrers, nil
}
func (h *ContainerHandler) storeContainerReferrers(ctx context.Context, cacheKey string, referrers *cachedContainerTags) error {
size, err := h.storeContainerMetadata(ctx, containerReferrersCacheEcosystem, cacheKey, referrers.body,
referrers.etag, referrers.link, referrers.contentType, "", time.Time{}, referrers.fetchedAt)
if err != nil {
return fmt.Errorf("storing referrers: %w", err)
}
referrers.size = size
return nil
}
func (h *ContainerHandler) writeContainerReferrers(w http.ResponseWriter, r *http.Request, registryURL string, referrers *cachedContainerTags, stale bool) {
w.Header().Set(headerContentType, referrers.contentType)
w.Header().Set(headerContentLength, strconv.FormatInt(referrers.size, 10))
if referrers.etag != "" {
w.Header().Set(headerETag, referrers.etag)
}
if link := h.rewriteContainerTagsLink(referrers.link, registryURL, r.URL.Path, r.URL.Query().Get(namespaceQueryParam)); link != "" {
w.Header().Set("Link", link)
}
if stale {
w.Header().Set("Warning", containerStaleWarning)
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write(referrers.body)
}