registry/handlers/blob.go (167 lines of code) (raw):
package handlers
import (
"context"
"errors"
"net/http"
"github.com/docker/distribution"
"github.com/docker/distribution/configuration"
"github.com/docker/distribution/log"
"github.com/docker/distribution/registry/api/errcode"
v2 "github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/datastore"
"github.com/gorilla/handlers"
"github.com/opencontainers/go-digest"
)
// blobDispatcher uses the request context to build a blobHandler.
func blobDispatcher(ctx *Context, _ *http.Request) http.Handler {
dgst, err := getDigest(ctx)
if err != nil {
if err == errDigestNotAvailable {
return http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
ctx.Errors = append(ctx.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err))
})
}
return http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
ctx.Errors = append(ctx.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err))
})
}
blobHandler := &blobHandler{
Context: ctx,
Digest: dgst,
}
mhandler := handlers.MethodHandler{
http.MethodGet: http.HandlerFunc(blobHandler.HandleGetBlob),
http.MethodHead: http.HandlerFunc(blobHandler.HandleGetBlob),
}
if !ctx.readOnly {
mhandler[http.MethodDelete] = http.HandlerFunc(blobHandler.DeleteBlob)
}
return checkOngoingRename(mhandler, ctx)
}
// blobHandler serves http blob requests.
type blobHandler struct {
*Context
Digest digest.Digest
}
func dbBlobLinkExists(ctx context.Context, db datastore.Queryer, repoPath string, dgst digest.Digest, cache datastore.RepositoryCache) error {
l := log.GetLogger(log.WithContext(ctx)).WithFields(log.Fields{"repository": repoPath, "digest": dgst})
l.Debug("finding repository blob link in database")
var opts []datastore.RepositoryStoreOption
if cache != nil {
opts = append(opts, datastore.WithRepositoryCache(cache))
}
rStore := datastore.NewRepositoryStore(db, opts...)
r, err := rStore.FindByPath(ctx, repoPath)
if err != nil {
return err
}
if r == nil {
err := v2.ErrorCodeBlobUnknown.WithDetail(dgst)
l.WithError(err).Debug("no repository found in database checking for repository blob link")
return err
}
found, err := rStore.ExistsBlob(ctx, r, dgst)
if err != nil {
return err
}
if !found {
err := v2.ErrorCodeBlobUnknown.WithDetail(dgst)
l.WithError(err).Debug("repository blob link not found in database")
return err
}
return nil
}
// HandleGetBlob fetches the binary data from backend storage returns it in the
// response.
func (bh *blobHandler) HandleGetBlob(w http.ResponseWriter, r *http.Request) {
log.GetLogger(log.WithContext(bh)).Debug("HandleGetBlob")
var dgst digest.Digest
blobs := bh.Repository.Blobs(bh)
if bh.useDatabase {
if err := dbBlobLinkExists(bh.Context, bh.db.Primary(), bh.Repository.Named().Name(), bh.Digest, bh.GetRepoCache()); err != nil {
bh.Errors = append(bh.Errors, errcode.FromUnknownError(err))
return
}
dgst = bh.Digest
} else {
desc, err := blobs.Stat(bh, bh.Digest)
if err != nil {
if err == distribution.ErrBlobUnknown {
bh.Errors = append(bh.Errors, v2.ErrorCodeBlobUnknown.WithDetail(bh.Digest))
} else {
bh.Errors = append(bh.Errors, errcode.FromUnknownError(err))
}
return
}
dgst = desc.Digest
}
// TODO: The unused returned meta object (i.e "_" ) is returned in preparation for tackling
// https://gitlab.com/gitlab-org/container-registry/-/issues/824. In that issue a refactor will be implemented
// to allow notifications to be emitted directly from the handlers (hence requiring the meta object presence).
if _, err := blobs.ServeBlob(bh, w, r, dgst); err != nil {
log.GetLogger(log.WithContext(bh)).WithError(err).Debug("unexpected error getting blob HTTP handler")
if errors.Is(err, distribution.ErrBlobUnknown) {
bh.Errors = append(bh.Errors, v2.ErrorCodeBlobUnknown.WithDetail(bh.Digest))
} else {
bh.Errors = append(bh.Errors, errcode.FromUnknownError(err))
}
return
}
}
// dbDeleteBlob does not actually delete a blob from the database (that's GC's responsibility), it only unlinks it from
// a repository.
func dbDeleteBlob(ctx context.Context, config *configuration.Configuration, db datastore.Queryer, cache datastore.RepositoryCache, repoPath string, d digest.Digest) error {
l := log.GetLogger(log.WithContext(ctx)).WithFields(log.Fields{"repository": repoPath, "digest": d})
l.Debug("deleting blob from repository in database")
if !deleteEnabled(config) {
return distribution.ErrUnsupported
}
rStore := datastore.NewRepositoryStore(db, datastore.WithRepositoryCache(cache))
r, err := rStore.FindByPath(ctx, repoPath)
if err != nil {
return err
}
if r == nil {
return distribution.ErrRepositoryUnknown{Name: repoPath}
}
found, err := rStore.UnlinkBlob(ctx, r, d)
if err != nil {
return err
}
if !found {
return distribution.ErrBlobUnknown
}
return nil
}
func deleteEnabled(config *configuration.Configuration) bool {
if d, ok := config.Storage["delete"]; ok {
e, ok := d["enabled"]
if ok {
if deleteEnabled, ok := e.(bool); ok && deleteEnabled {
return true
}
}
}
return false
}
// DeleteBlob deletes a layer blob
func (bh *blobHandler) DeleteBlob(w http.ResponseWriter, _ *http.Request) {
log.GetLogger(log.WithContext(bh)).Debug("DeleteBlob")
err := bh.deleteBlobImpl()
if err != nil {
switch err {
case distribution.ErrUnsupported:
bh.Errors = append(bh.Errors, errcode.ErrorCodeUnsupported)
return
case distribution.ErrBlobUnknown:
bh.Errors = append(bh.Errors, v2.ErrorCodeBlobUnknown)
return
case distribution.ErrRepositoryUnknown{Name: bh.Repository.Named().Name()}:
bh.Errors = append(bh.Errors, v2.ErrorCodeNameUnknown)
return
default:
bh.Errors = append(bh.Errors, errcode.FromUnknownError(err))
log.GetLogger(log.WithContext(bh)).WithError(err).Error("failed to delete blob")
return
}
}
w.Header().Set("Content-Length", "0")
w.WriteHeader(http.StatusAccepted)
}
func (bh *blobHandler) deleteBlobImpl() error {
if !bh.useDatabase {
blobs := bh.Repository.Blobs(bh)
return blobs.Delete(bh, bh.Digest)
}
return dbDeleteBlob(bh.Context, bh.App.Config, bh.db.Primary(), bh.GetRepoCache(), bh.Repository.Named().Name(), bh.Digest)
}