mirror of
https://github.com/project-zot/zot.git
synced 2026-06-18 13:37:57 +08:00
55b68228da
* feat(storage): redirect blob pulls to backend URLs * fix: rebase conflicts Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * refactor: rename redirect field Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * test: relax brittle TestPeriodicGC substore log assertion Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * feat(storage): improve blob redirect config handling and validation Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix(storage): address PR review feedback for blob redirect Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * feat(storage): apply latest PR review fixes for blob redirect Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * test: fix blob redirect and verify test regressions Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix(storage): enforce redirectBlobURL validation and add redirect tests Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix(storage): fix err113/noctx lint errors in storage driver tests - Replace httptest.NewRequest with httptest.NewRequestWithContext in s3, gcs, and imagestore driver tests (noctx) - Replace dynamic errors.New in s3 driver test with a package-level static sentinel error (err113) Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * test(storage): use temp dirs in imagestore redirect tests Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix: handle ranged blob redirects and add regression tests Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix: validate blob digest consistently in GetBlob Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * test: fix GetBlobPartialFn mock return values for range requests The test 'does not redirect ranged blob requests' was failing because the mock was returning incorrect length values. For a range request 'bytes=0-0' (1 byte), it was returning 4 bytes, which caused a length mismatch check in GetBlob to return HTTP 500. Fix the mock to dynamically calculate the correct length: to - from + 1 Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix(storage): preserve signed URL bytes in normalizeBlobRedirectURL Preserve the original URL bytes from backend storage drivers (important for signed/presigned URLs) while only lowercasing the scheme prefix. URL re-serialization via net/url can invalidate signatures through path escaping or canonicalization. Add regression tests covering signed URL query parameters and mixed-case scheme handling. Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix(storage): address PR review comments for blob redirect - Return signed redirect URLs unchanged; validate scheme/CRLF/host only, no URL normalization that would corrupt signed URL bytes - Add inline comments for all non-obvious decisions: range bypass, soft fallback on invalid URL, local driver empty return, subpath resolution, redirectBlobURL config constraint on local/empty driver - Expand TestNormalizeBlobRedirectURL to cover allowed schemes (http/https), parse failure, missing host, and CRLF injection cases - Add TestIsBlobRedirectEnabled covering subpath-only enablement with default store disabled Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * test(storage): address remaining blob redirect review comments Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> * fix: gofumpt formatting in routes_test.go Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> --------- Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com> Co-authored-by: Akash Kumar <meakash7902@gmail.com>
101 lines
3.3 KiB
Go
101 lines
3.3 KiB
Go
package imagestore_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/distribution/distribution/v3/registry/storage/driver"
|
|
godigest "github.com/opencontainers/go-digest"
|
|
. "github.com/smartystreets/goconvey/convey"
|
|
|
|
zerr "zotregistry.dev/zot/v2/errors"
|
|
"zotregistry.dev/zot/v2/pkg/extensions/monitoring"
|
|
zlog "zotregistry.dev/zot/v2/pkg/log"
|
|
"zotregistry.dev/zot/v2/pkg/storage/gcs"
|
|
"zotregistry.dev/zot/v2/pkg/storage/imagestore"
|
|
"zotregistry.dev/zot/v2/pkg/storage/local"
|
|
"zotregistry.dev/zot/v2/pkg/test/mocks"
|
|
)
|
|
|
|
func TestGetBlobRedirectURL(t *testing.T) {
|
|
Convey("GetBlobRedirectURL", t, func() {
|
|
log := zlog.NewTestLogger()
|
|
metrics := monitoring.NewMetricsServer(false, log)
|
|
|
|
Convey("returns bad digest for invalid digest", func() {
|
|
store := imagestore.NewImageStore(t.TempDir(), "", false, false, log, metrics, nil,
|
|
local.New(true), nil, nil, nil)
|
|
|
|
url, err := store.GetBlobRedirectURL(nil, "repo", godigest.Digest("not-a-digest"))
|
|
So(url, ShouldEqual, "")
|
|
So(errors.Is(err, zerr.ErrBadBlobDigest), ShouldBeTrue)
|
|
})
|
|
|
|
Convey("returns empty URL for local storage", func() {
|
|
store := imagestore.NewImageStore(t.TempDir(), "", false, false, log, metrics, nil,
|
|
local.New(true), nil, nil, nil)
|
|
|
|
digest := godigest.FromString("blob-content")
|
|
// Local driver has no external signed URL endpoint, so redirect is intentionally empty.
|
|
url, err := store.GetBlobRedirectURL(nil, "repo", digest)
|
|
So(err, ShouldBeNil)
|
|
So(url, ShouldEqual, "")
|
|
})
|
|
|
|
Convey("returns redirect URL for remote storage", func() {
|
|
rootDir := t.TempDir()
|
|
storeMock := &mocks.StorageDriverMock{}
|
|
remoteDriver := gcs.New(storeMock)
|
|
store := imagestore.NewImageStore(rootDir, "", false, false, log, metrics, nil,
|
|
remoteDriver, nil, nil, nil)
|
|
|
|
repo := "repo"
|
|
digest := godigest.FromString("blob-content")
|
|
expectedBlobPath := store.BlobPath(repo, digest)
|
|
expectedURL := "https://example.com/signed/blob"
|
|
|
|
storeMock.StatFn = func(_ context.Context, path string) (driver.FileInfo, error) {
|
|
So(path, ShouldEqual, expectedBlobPath)
|
|
|
|
return &mocks.FileInfoMock{
|
|
PathFn: func() string { return path },
|
|
SizeFn: func() int64 { return 42 },
|
|
}, nil
|
|
}
|
|
|
|
storeMock.RedirectURLFn = func(_ *http.Request, path string) (string, error) {
|
|
So(path, ShouldEqual, expectedBlobPath)
|
|
|
|
return expectedURL, nil
|
|
}
|
|
|
|
req := httptest.NewRequestWithContext(context.Background(), http.MethodGet,
|
|
"http://localhost/v2/repo/blobs/sha256:deadbeef", nil)
|
|
|
|
url, err := store.GetBlobRedirectURL(req, repo, digest)
|
|
So(err, ShouldBeNil)
|
|
So(url, ShouldEqual, expectedURL)
|
|
})
|
|
|
|
Convey("returns blob not found when blob path does not exist", func() {
|
|
rootDir := t.TempDir()
|
|
storeMock := &mocks.StorageDriverMock{}
|
|
remoteDriver := gcs.New(storeMock)
|
|
store := imagestore.NewImageStore(rootDir, "", false, false, log, metrics, nil,
|
|
remoteDriver, nil, nil, nil)
|
|
|
|
storeMock.StatFn = func(_ context.Context, path string) (driver.FileInfo, error) {
|
|
return nil, driver.PathNotFoundError{Path: path}
|
|
}
|
|
|
|
digest := godigest.FromString("blob-content")
|
|
url, err := store.GetBlobRedirectURL(nil, "repo", digest)
|
|
So(url, ShouldEqual, "")
|
|
So(errors.Is(err, zerr.ErrBlobNotFound), ShouldBeTrue)
|
|
})
|
|
})
|
|
}
|