Files
zot/pkg/storage/s3/driver.go
T
Ramkumar Chinchani 55b68228da feat(storage): redirect blob pulls to backend URLs (#4092)
* 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>
2026-06-15 14:36:07 -07:00

118 lines
3.0 KiB
Go

package s3
import (
"context"
"io"
"net/http"
// Add s3 support.
"github.com/distribution/distribution/v3/registry/storage/driver"
_ "github.com/distribution/distribution/v3/registry/storage/driver/s3-aws"
storageConstants "zotregistry.dev/zot/v2/pkg/storage/constants"
)
type Driver struct {
store driver.StorageDriver
}
func New(storeDriver driver.StorageDriver) *Driver {
return &Driver{store: storeDriver}
}
func (driver *Driver) Name() string {
return storageConstants.S3StorageDriverName
}
func (driver *Driver) EnsureDir(path string) error {
return nil
}
func (driver *Driver) DirExists(path string) bool {
if fi, err := driver.store.Stat(context.Background(), path); err == nil && fi.IsDir() {
return true
}
return false
}
func (driver *Driver) Reader(path string, offset int64) (io.ReadCloser, error) {
return driver.store.Reader(context.Background(), path, offset)
}
func (driver *Driver) ReadFile(path string) ([]byte, error) {
return driver.store.GetContent(context.Background(), path)
}
func (driver *Driver) Delete(path string) error {
return driver.store.Delete(context.Background(), path)
}
func (driver *Driver) Stat(path string) (driver.FileInfo, error) {
return driver.store.Stat(context.Background(), path)
}
func (driver *Driver) Writer(filepath string, append bool) (driver.FileWriter, error) { //nolint:predeclared
return driver.store.Writer(context.Background(), filepath, append)
}
func (driver *Driver) WriteFile(filepath string, content []byte) (int, error) {
var n int
if stwr, err := driver.store.Writer(context.Background(), filepath, false); err == nil {
defer stwr.Close()
if n, err = stwr.Write(content); err != nil {
return -1, err
}
if err := stwr.Commit(context.Background()); err != nil {
return -1, err
}
} else {
return -1, err
}
return n, nil
}
func (driver *Driver) Walk(path string, f driver.WalkFn) error {
return driver.store.Walk(context.Background(), path, f)
}
func (driver *Driver) List(fullpath string) ([]string, error) {
return driver.store.List(context.Background(), fullpath)
}
func (driver *Driver) Move(sourcePath string, destPath string) error {
return driver.store.Move(context.Background(), sourcePath, destPath)
}
func (driver *Driver) SameFile(path1, path2 string) bool {
fi1, _ := driver.store.Stat(context.Background(), path1)
fi2, _ := driver.store.Stat(context.Background(), path2)
if fi1 != nil && fi2 != nil {
if fi1.IsDir() == fi2.IsDir() &&
fi1.ModTime() == fi2.ModTime() &&
fi1.Path() == fi2.Path() &&
fi1.Size() == fi2.Size() {
return true
}
}
return false
}
// Link puts an empty file that will act like a link between the original file and deduped one.
// Because s3 doesn't support symlinks, wherever the storage will encounter an empty file, it will get the original one
// from cache.
func (driver *Driver) Link(src, dest string) error {
return driver.store.PutContent(context.Background(), dest, []byte{})
}
func (driver *Driver) RedirectURL(r *http.Request, path string) (string, error) {
return driver.store.RedirectURL(r, path)
}