diff --git a/errors/errors.go b/errors/errors.go index b49c0f87a..47f737ba0 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -60,6 +60,7 @@ var ( ErrBadBlob = errors.New("bad blob") ErrBadBlobDigest = errors.New("bad blob digest") ErrBlobReferenced = errors.New("blob referenced by manifest") + ErrBlobRedirectURLNotSupported = errors.New("blob redirect url not supported") ErrManifestReferenced = errors.New("manifest referenced by index image") ErrUnknownCode = errors.New("unknown error code") ErrBadCACert = errors.New("invalid tls ca cert") diff --git a/pkg/api/config/config.go b/pkg/api/config/config.go index 1fe1b3101..a44bfb60d 100644 --- a/pkg/api/config/config.go +++ b/pkg/api/config/config.go @@ -34,6 +34,7 @@ type StorageConfig struct { Retention ImageRetention StorageDriver map[string]interface{} `mapstructure:",omitempty"` CacheDriver map[string]interface{} `mapstructure:",omitempty"` + Redirect bool } type ImageRetention struct { diff --git a/pkg/api/routes.go b/pkg/api/routes.go index b92c42d56..a61e2e969 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -1080,6 +1080,8 @@ func (rh *RouteHandler) GetBlob(response http.ResponseWriter, request *http.Requ mediaType := request.Header.Get("Accept") + redirect := rh.c.Config.Storage.StorageConfig.Redirect + /* content range is supported for resumbale pulls */ partial := false @@ -1108,11 +1110,16 @@ func (rh *RouteHandler) GetBlob(response http.ResponseWriter, request *http.Requ } var repo io.ReadCloser - + var redirectURL string var blen, bsize int64 if partial { repo, blen, bsize, err = imgStore.GetBlobPartial(name, digest, mediaType, from, to) + } else if redirect { + redirectURL, err = imgStore.GetBlobURL(request, name, digest, mediaType) + if errors.Is(err, zerr.ErrBlobRedirectURLNotSupported) { + repo, blen, err = imgStore.GetBlob(name, digest, mediaType) + } } else { repo, blen, err = imgStore.GetBlob(name, digest, mediaType) } @@ -1139,6 +1146,11 @@ func (rh *RouteHandler) GetBlob(response http.ResponseWriter, request *http.Requ return } + if redirectURL != "" { + http.Redirect(response, request, redirectURL, http.StatusTemporaryRedirect) + return + } + defer repo.Close() response.Header().Set("Content-Length", strconv.FormatInt(blen, 10)) diff --git a/pkg/storage/imagestore/imagestore.go b/pkg/storage/imagestore/imagestore.go index ab753f184..fac4097cf 100644 --- a/pkg/storage/imagestore/imagestore.go +++ b/pkg/storage/imagestore/imagestore.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "net/http" "path" "path/filepath" "strings" @@ -1535,6 +1536,23 @@ func (is *ImageStore) GetBlob(repo string, digest godigest.Digest, mediaType str return blobReadCloser, binfo.Size(), nil } +func (is *ImageStore) GetBlobURL(request *http.Request, repo string, digest godigest.Digest, mediaType string) (string, error) { + var lockLatency time.Time + + if err := digest.Validate(); err != nil { + return "", err + } + + is.RLock(&lockLatency) + defer is.RUnlock(&lockLatency) + + binfo, err := is.originalBlobInfo(repo, digest) + if err != nil { + return "", err + } + return is.storeDriver.URLFor(request, binfo.Path()) +} + // GetBlobContent returns blob contents, the caller function MUST lock from outside. // Should be used for small files(manifests/config blobs). func (is *ImageStore) GetBlobContent(repo string, digest godigest.Digest) ([]byte, error) { diff --git a/pkg/storage/local/driver.go b/pkg/storage/local/driver.go index 177e5d021..c597b7a41 100644 --- a/pkg/storage/local/driver.go +++ b/pkg/storage/local/driver.go @@ -6,6 +6,7 @@ import ( "context" "errors" "io" + "net/http" "os" "path" "sort" @@ -58,6 +59,10 @@ func (driver *Driver) DirExists(path string) bool { return true } +func (driver *Driver) URLFor(_ *http.Request, _ string) (string, error) { + return "", zerr.ErrBlobRedirectURLNotSupported +} + func (driver *Driver) Reader(path string, offset int64) (io.ReadCloser, error) { file, err := os.OpenFile(path, os.O_RDONLY, storageConstants.DefaultFilePerms) if err != nil { diff --git a/pkg/storage/s3/driver.go b/pkg/storage/s3/driver.go index e1a94fdf9..c2e8769ea 100644 --- a/pkg/storage/s3/driver.go +++ b/pkg/storage/s3/driver.go @@ -3,6 +3,7 @@ package s3 import ( "context" "io" + "net/http" // Add s3 support. "github.com/distribution/distribution/v3/registry/storage/driver" @@ -35,6 +36,10 @@ func (driver *Driver) DirExists(path string) bool { return false } +func (driver *Driver) URLFor(r *http.Request, path string) (string, error) { + return driver.store.RedirectURL(r, path) +} + func (driver *Driver) Reader(path string, offset int64) (io.ReadCloser, error) { return driver.store.Reader(context.Background(), path, offset) } diff --git a/pkg/storage/types/types.go b/pkg/storage/types/types.go index edc067073..549e1b1ec 100644 --- a/pkg/storage/types/types.go +++ b/pkg/storage/types/types.go @@ -3,6 +3,7 @@ package types import ( "context" "io" + "net/http" "time" storagedriver "github.com/distribution/distribution/v3/registry/storage/driver" @@ -53,6 +54,7 @@ type ImageStore interface { //nolint:interfacebloat CheckBlob(repo string, digest godigest.Digest) (bool, int64, error) StatBlob(repo string, digest godigest.Digest) (bool, int64, time.Time, error) GetBlob(repo string, digest godigest.Digest, mediaType string) (io.ReadCloser, int64, error) + GetBlobURL(r *http.Request, repo string, digest godigest.Digest, mediaType string) (string, error) GetBlobPartial(repo string, digest godigest.Digest, mediaType string, from, to int64, ) (io.ReadCloser, int64, int64, error) DeleteBlob(repo string, digest godigest.Digest) error @@ -75,6 +77,7 @@ type Driver interface { //nolint:interfacebloat Name() string EnsureDir(path string) error DirExists(path string) bool + URLFor(request *http.Request, path string) (string, error) Reader(path string, offset int64) (io.ReadCloser, error) ReadFile(path string) ([]byte, error) Delete(path string) error diff --git a/pkg/test/mocks/image_store_mock.go b/pkg/test/mocks/image_store_mock.go index 5d125a93d..0f1286d8b 100644 --- a/pkg/test/mocks/image_store_mock.go +++ b/pkg/test/mocks/image_store_mock.go @@ -3,6 +3,7 @@ package mocks import ( "context" "io" + "net/http" "time" godigest "github.com/opencontainers/go-digest" @@ -44,6 +45,7 @@ type MockedImageStore struct { GetBlobPartialFn func(repo string, digest godigest.Digest, mediaType string, from, to int64, ) (io.ReadCloser, int64, int64, error) GetBlobFn func(repo string, digest godigest.Digest, mediaType string) (io.ReadCloser, int64, error) + GetBlobURLFn func(request *http.Request, repo string, digest godigest.Digest, mediaType string) (string, error) DeleteBlobFn func(repo string, digest godigest.Digest) error GetIndexContentFn func(repo string) ([]byte, error) GetBlobContentFn func(repo string, digest godigest.Digest) ([]byte, error) @@ -339,6 +341,15 @@ func (is MockedImageStore) GetBlob(repo string, digest godigest.Digest, mediaTyp return io.NopCloser(&io.LimitedReader{}), 0, nil } +func (is MockedImageStore) GetBlobURL(request *http.Request, repo string, digest godigest.Digest, mediaType string, +) (string, error) { + if is.GetBlobURLFn != nil { + return is.GetBlobURLFn(request, repo, digest, mediaType) + } + + return "", nil +} + func (is MockedImageStore) DeleteBlobUpload(repo string, uuid string) error { if is.DeleteBlobUploadFn != nil { return is.DeleteBlobUploadFn(repo, uuid)