GCS storage support (#3798)

feat(storage): add a GCS driver

test(storage): add unit tests for GCS driver

test(storage): add missing unit tests for GCS driver & resolve lint issues

fix: configuration validation for GCS Storage

test(storage): resolve panic by test due to setupGCS ignoring returned error

test(storage): add dummy gcs credentials

test: add darwin support for macos to run tests

ci: update workflows to pin gcs emulator version

lint: resolve long line lengths & formatting issues

test: move error for gcs mock earlier with an error

test: stop test using local google credentials and use mock instead

test: add missing dummy creds

test(storage): use storage-testbench for GCS, isolate GCS tests, fix driver Delete

- Switch GCS emulator from fake-gcs-server to storage-testbench in CI.
  Run the GCS emulator only in the privileged-test job; remove it from
  minimal and extended test jobs.

- Consolidate GCS tests under pkg/storage/gcs (needprivileges,linux).
  Add TestMain with HTTPS proxy and /etc/hosts so tests talk to
  storage-testbench; move GCS-specific cases from storage_test.go and
  scrub_test.go into gcs_test.go. Run GCS tests via a second privileged-test
  invocation and collect coverage in coverage-needprivileges-gcs.txt.

- Make GCS driver Delete idempotent and normalize errors. Treat
  PathNotFoundError from Delete as success so that deleting an already-gone
  path (e.g. after GC under eventual consistency) does not fail. Add
  formatErr to map 404/not found to PathNotFoundError and use it for all
  driver methods so callers get consistent storage driver errors.

- Drop GCS branches and helpers from storage_test.go and scrub_test.go so
  non-privileged tests only use local/S3; GCS is tested only in
  pkg/storage/gcs with storage-testbench.

- Set GCSMOCK_ENDPOINT without /storage/v1/, as the rest of the URL is set in tests.

- Show errors in case of failure to create bucket.

- Consolidate StorageDriverMock structs inside the pkg/test/mocks package.

Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
Co-authored-by: Steven Marks <steve.marks@qomodo.io>
This commit is contained in:
Andrei Aaron
2026-02-19 09:41:21 +02:00
committed by GitHub
parent 5b2312d538
commit 5e57656bff
19 changed files with 4634 additions and 401 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build needprivileges
//go:build needprivileges && linux
package config_test
+57 -45
View File
@@ -299,6 +299,10 @@ func getStorageType(storageDriver map[string]any) string {
return storageConstants.S3StorageDriverName
}
if storeName == storageConstants.GCSStorageDriverName {
return storageConstants.GCSStorageDriverName
}
return storeName
}
@@ -559,6 +563,57 @@ func validateExtensionsConfig(cfg *config.Config, logger zlog.Logger) error {
return nil
}
func validateStorageConfigSection(
cfg *config.Config, logger zlog.Logger, storageConfig config.GlobalStorageConfig,
) error {
if len(storageConfig.StorageDriver) != 0 {
// enforce s3/gcs driver in case of using storage driver
if storageConfig.StorageDriver["name"] != storageConstants.S3StorageDriverName &&
storageConfig.StorageDriver["name"] != storageConstants.GCSStorageDriverName {
msg := "unsupported storage driver"
logger.Error().Err(zerr.ErrBadConfig).Interface("storageDriver", storageConfig.StorageDriver["name"]).Msg(msg)
return fmt.Errorf("%w: %s", zerr.ErrBadConfig, msg)
}
// enforce tmpDir in case sync + s3/gcs
extensionsConfig := cfg.CopyExtensionsConfig()
if extensionsConfig.IsSyncEnabled() && extensionsConfig.Sync.DownloadDir == "" {
msg := "using both sync and remote storage features needs config.Extensions.Sync.DownloadDir to be specified"
logger.Error().Err(zerr.ErrBadConfig).Msg(msg)
return fmt.Errorf("%w: %s", zerr.ErrBadConfig, msg)
}
}
// enforce s3/gcs driver on subpaths in case of using storage driver
if len(storageConfig.SubPaths) > 0 {
for route, subStorageConfig := range storageConfig.SubPaths {
if len(subStorageConfig.StorageDriver) != 0 {
if subStorageConfig.StorageDriver["name"] != storageConstants.S3StorageDriverName &&
subStorageConfig.StorageDriver["name"] != storageConstants.GCSStorageDriverName {
msg := "unsupported storage driver"
logger.Error().Err(zerr.ErrBadConfig).Str("subpath", route).Interface("storageDriver",
subStorageConfig.StorageDriver["name"]).Msg(msg)
return fmt.Errorf("%w: %s", zerr.ErrBadConfig, msg)
}
// enforce tmpDir in case sync + s3/gcs
extensionsConfig := cfg.CopyExtensionsConfig()
if extensionsConfig.IsSyncEnabled() && extensionsConfig.Sync.DownloadDir == "" {
msg := "using both sync and remote storage features needs config.Extensions.Sync.DownloadDir to be specified"
logger.Error().Err(zerr.ErrBadConfig).Msg(msg)
return fmt.Errorf("%w: %s", zerr.ErrBadConfig, msg)
}
}
}
}
return nil
}
func validateConfiguration(config *config.Config, logger zlog.Logger) error {
if err := validateHTTP(config, logger); err != nil {
return err
@@ -614,51 +669,8 @@ func validateConfiguration(config *config.Config, logger zlog.Logger) error {
}
storageConfig := config.CopyStorageConfig()
if len(storageConfig.StorageDriver) != 0 {
// enforce s3 driver in case of using storage driver
if storageConfig.StorageDriver["name"] != storageConstants.S3StorageDriverName {
msg := "unsupported storage driver"
logger.Error().Err(zerr.ErrBadConfig).Interface("cacheDriver", storageConfig.StorageDriver["name"]).Msg(msg)
return fmt.Errorf("%w: %s", zerr.ErrBadConfig, msg)
}
// enforce tmpDir in case sync + s3
extensionsConfig := config.CopyExtensionsConfig()
if extensionsConfig.IsSyncEnabled() && extensionsConfig.Sync.DownloadDir == "" {
msg := "using both sync and remote storage features needs config.Extensions.Sync.DownloadDir to be specified"
logger.Error().Err(zerr.ErrBadConfig).Msg(msg)
return fmt.Errorf("%w: %s", zerr.ErrBadConfig, msg)
}
}
// enforce s3 driver on subpaths in case of using storage driver
if storageConfig.SubPaths != nil {
if len(storageConfig.SubPaths) > 0 {
subPaths := storageConfig.SubPaths
for route, subStorageConfig := range subPaths {
if len(subStorageConfig.StorageDriver) != 0 {
if subStorageConfig.StorageDriver["name"] != storageConstants.S3StorageDriverName {
msg := "unsupported storage driver"
logger.Error().Err(zerr.ErrBadConfig).Str("subpath", route).Interface("storageDriver",
subStorageConfig.StorageDriver["name"]).Msg(msg)
return fmt.Errorf("%w: %s", zerr.ErrBadConfig, msg)
}
// enforce tmpDir in case sync + s3
extensionsConfig := config.CopyExtensionsConfig()
if extensionsConfig.IsSyncEnabled() && extensionsConfig.Sync.DownloadDir == "" {
msg := "using both sync and remote storage features needs config.Extensions.Sync.DownloadDir to be specified"
logger.Error().Err(zerr.ErrBadConfig).Msg(msg)
return fmt.Errorf("%w: %s", zerr.ErrBadConfig, msg)
}
}
}
}
if err := validateStorageConfigSection(config, logger, storageConfig); err != nil {
return err
}
// check glob patterns in authz config are compilable
+1
View File
@@ -24,5 +24,6 @@ const (
DefaultGCDelay = 1 * time.Hour
DefaultGCInterval = 1 * time.Hour
S3StorageDriverName = "s3"
GCSStorageDriverName = "gcs"
LocalStorageDriverName = "local"
)
+209
View File
@@ -0,0 +1,209 @@
package gcs
import (
"context"
"errors"
"io"
"strings"
// Add gcs support.
storagedriver "github.com/distribution/distribution/v3/registry/storage/driver"
_ "github.com/distribution/distribution/v3/registry/storage/driver/gcs"
storageConstants "zotregistry.dev/zot/v2/pkg/storage/constants"
)
type Driver struct {
store storagedriver.StorageDriver
}
func New(storeDriver storagedriver.StorageDriver) *Driver {
return &Driver{store: storeDriver}
}
func (driver *Driver) Name() string {
return storageConstants.GCSStorageDriverName
}
func (driver *Driver) EnsureDir(path string) error {
return nil
}
func (driver *Driver) DirExists(path string) bool {
if fi, err := driver.Stat(path); err == nil && fi.IsDir() {
return true
}
return false
}
func (driver *Driver) Reader(path string, offset int64) (io.ReadCloser, error) {
reader, err := driver.store.Reader(context.Background(), path, offset)
if err != nil {
return nil, driver.formatErr(err, path)
}
return reader, nil
}
func (driver *Driver) ReadFile(path string) ([]byte, error) {
content, err := driver.store.GetContent(context.Background(), path)
if err != nil {
return nil, driver.formatErr(err, path)
}
return content, nil
}
func (driver *Driver) Delete(path string) error {
err := driver.store.Delete(context.Background(), path)
if err == nil {
return nil
}
// Format the error first to convert GCS-specific 404 errors to PathNotFoundError
formattedErr := driver.formatErr(err, path)
// Check if the formatted error is PathNotFoundError
var pathNotFoundErr storagedriver.PathNotFoundError
if errors.As(formattedErr, &pathNotFoundErr) {
// For directory deletion, if the path doesn't exist, treat it as success (idempotent delete)
// In GCS, directories are just prefixes, so if all objects are deleted,
// the directory may already be gone (especially with eventual consistency in storage-testbench)
// This makes Delete idempotent: deleting a non-existent path is a no-op
return nil
}
return formattedErr
}
func (driver *Driver) Stat(path string) (storagedriver.FileInfo, error) {
fileInfo, err := driver.store.Stat(context.Background(), path)
if err != nil {
return nil, driver.formatErr(err, path)
}
return fileInfo, nil
}
func (driver *Driver) Writer(filepath string, append bool) (storagedriver.FileWriter, error) { //nolint:predeclared
writer, err := driver.store.Writer(context.Background(), filepath, append)
if err != nil {
return nil, driver.formatErr(err, filepath)
}
return writer, nil
}
func (driver *Driver) WriteFile(filepath string, content []byte) (int, error) {
var n int
stwr, err := driver.store.Writer(context.Background(), filepath, false)
if err != nil {
return -1, driver.formatErr(err, filepath)
}
defer stwr.Close()
if n, err = stwr.Write(content); err != nil {
return -1, driver.formatErr(err, filepath)
}
if err := stwr.Commit(context.Background()); err != nil {
return -1, driver.formatErr(err, filepath)
}
return n, nil
}
func (driver *Driver) Walk(path string, f storagedriver.WalkFn) error {
return driver.formatErr(driver.store.Walk(context.Background(), path, f), path)
}
func (driver *Driver) List(fullpath string) ([]string, error) {
list, err := driver.store.List(context.Background(), fullpath)
if err != nil {
return nil, driver.formatErr(err, fullpath)
}
return list, nil
}
func (driver *Driver) Move(sourcePath string, destPath string) error {
return driver.formatErr(driver.store.Move(context.Background(), sourcePath, destPath), sourcePath)
}
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 gcs 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.formatErr(driver.store.PutContent(context.Background(), dest, []byte{}), dest)
}
// formatErr converts GCS-specific 404/not found errors to PathNotFoundError.
func (driver *Driver) formatErr(err error, path string) error {
switch actual := err.(type) { //nolint: errorlint
case nil:
return nil
case storagedriver.PathNotFoundError:
actual.DriverName = driver.Name()
if actual.Path == "" && path != "" {
actual.Path = path
}
return actual
case storagedriver.InvalidPathError:
actual.DriverName = driver.Name()
return actual
case storagedriver.InvalidOffsetError:
actual.DriverName = driver.Name()
return actual
default:
// Check for GCS-specific 404/not found errors by unwrapping the error chain
errToCheck := err
for errToCheck != nil {
errStr := errToCheck.Error()
isNotFound := strings.Contains(errStr, "object doesn't exist") ||
strings.Contains(errStr, "Error 404") ||
strings.Contains(errStr, "does not exist")
if isNotFound {
return storagedriver.PathNotFoundError{
DriverName: driver.Name(),
Path: path,
}
}
if unwrappable, ok := errToCheck.(interface{ Unwrap() error }); ok {
errToCheck = unwrappable.Unwrap()
} else {
break
}
}
storageError := storagedriver.Error{
DriverName: driver.Name(),
Detail: err,
}
return storageError
}
}
+364
View File
@@ -0,0 +1,364 @@
package gcs_test
import (
"context"
"errors"
"fmt"
"io"
"strings"
"testing"
"time"
"github.com/distribution/distribution/v3/registry/storage/driver"
. "github.com/smartystreets/goconvey/convey"
"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/test/mocks"
)
var errTest = errors.New("error")
type fileInfoMock struct {
isDir bool
size int64
modTime time.Time
path string
}
func (f *fileInfoMock) Path() string { return f.path }
func (f *fileInfoMock) Size() int64 { return f.size }
func (f *fileInfoMock) ModTime() time.Time { return f.modTime }
func (f *fileInfoMock) IsDir() bool { return f.isDir }
func TestDriver(t *testing.T) {
Convey("GCS Driver", t, func() {
storeMock := &mocks.StorageDriverMock{}
gcsDriver := gcs.New(storeMock)
Convey("Name", func() {
So(gcsDriver.Name(), ShouldEqual, "gcs")
})
Convey("EnsureDir", func() {
err := gcsDriver.EnsureDir("/test")
So(err, ShouldBeNil)
})
Convey("DirExists", func() {
Convey("True", func() {
storeMock.StatFn = func(ctx context.Context, path string) (driver.FileInfo, error) {
return &mocks.FileInfoMock{
IsDirFn: func() bool { return true },
}, nil
}
So(gcsDriver.DirExists("/test"), ShouldBeTrue)
})
Convey("False - Not a dir", func() {
storeMock.StatFn = func(ctx context.Context, path string) (driver.FileInfo, error) {
return &mocks.FileInfoMock{
IsDirFn: func() bool { return false },
}, nil
}
So(gcsDriver.DirExists("/test"), ShouldBeFalse)
})
Convey("False - Error", func() {
storeMock.StatFn = func(ctx context.Context, path string) (driver.FileInfo, error) {
return nil, errTest
}
So(gcsDriver.DirExists("/test"), ShouldBeFalse)
})
})
Convey("Reader", func() {
Convey("Success", func() {
storeMock.ReaderFn = func(ctx context.Context, path string, offset int64) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("")), nil
}
r, err := gcsDriver.Reader("/test", 0)
So(err, ShouldBeNil)
So(r, ShouldNotBeNil)
})
Convey("InvalidOffsetError", func() {
storeMock.ReaderFn = func(ctx context.Context, path string, offset int64) (io.ReadCloser, error) {
return nil, driver.InvalidOffsetError{Path: path, Offset: offset}
}
_, err := gcsDriver.Reader("/test", 100)
So(err, ShouldNotBeNil)
var invalidOffset driver.InvalidOffsetError
So(errors.As(err, &invalidOffset), ShouldBeTrue)
So(invalidOffset.DriverName, ShouldEqual, "gcs")
})
})
Convey("ReadFile", func() {
Convey("Success", func() {
storeMock.GetContentFn = func(ctx context.Context, path string) ([]byte, error) {
return []byte("content"), nil
}
content, err := gcsDriver.ReadFile("/test")
So(err, ShouldBeNil)
So(string(content), ShouldEqual, "content")
})
Convey("PathNotFoundError with empty Path gets path set", func() {
storeMock.GetContentFn = func(ctx context.Context, path string) ([]byte, error) {
return nil, driver.PathNotFoundError{Path: ""} // Path empty so driver sets it
}
_, err := gcsDriver.ReadFile("/requested/path")
So(err, ShouldNotBeNil)
var pathErr driver.PathNotFoundError
So(errors.As(err, &pathErr), ShouldBeTrue)
So(pathErr.DriverName, ShouldEqual, "gcs")
So(pathErr.Path, ShouldEqual, "/requested/path")
})
Convey("GCS not-found string becomes PathNotFoundError", func() {
for _, msg := range []string{"object doesn't exist", "Error 404", "does not exist"} {
errMsg := msg
storeMock.GetContentFn = func(ctx context.Context, path string) ([]byte, error) {
//nolint:err113 // test needs variable not-found message
return nil, fmt.Errorf("%s", errMsg)
}
_, err := gcsDriver.ReadFile("/key")
So(err, ShouldNotBeNil)
var pathErr driver.PathNotFoundError
So(errors.As(err, &pathErr), ShouldBeTrue)
So(pathErr.Path, ShouldEqual, "/key")
}
})
Convey("Generic error becomes storagedriver.Error", func() {
storeMock.GetContentFn = func(ctx context.Context, path string) ([]byte, error) {
return nil, errTest
}
_, err := gcsDriver.ReadFile("/test")
So(err, ShouldNotBeNil)
var storageErr driver.Error
So(errors.As(err, &storageErr), ShouldBeTrue)
So(storageErr.DriverName, ShouldEqual, "gcs")
So(storageErr.Detail, ShouldEqual, errTest)
})
})
Convey("Delete", func() {
Convey("Success", func() {
storeMock.DeleteFn = func(ctx context.Context, path string) error {
return nil
}
err := gcsDriver.Delete("/test")
So(err, ShouldBeNil)
})
Convey("PathNotFoundError is idempotent (return nil)", func() {
storeMock.DeleteFn = func(ctx context.Context, path string) error {
return driver.PathNotFoundError{Path: path}
}
err := gcsDriver.Delete("/nonexistent")
So(err, ShouldBeNil)
})
Convey("Other error is returned", func() {
storeMock.DeleteFn = func(ctx context.Context, path string) error {
return errTest
}
err := gcsDriver.Delete("/test")
So(err, ShouldNotBeNil)
So(errors.Is(err, errTest), ShouldBeFalse) // wrapped in storagedriver.Error
})
})
Convey("Stat", func() {
Convey("Success", func() {
storeMock.StatFn = func(ctx context.Context, path string) (driver.FileInfo, error) {
return &mocks.FileInfoMock{}, nil
}
fi, err := gcsDriver.Stat("/test")
So(err, ShouldBeNil)
So(fi, ShouldNotBeNil)
})
Convey("InvalidPathError", func() {
storeMock.StatFn = func(ctx context.Context, path string) (driver.FileInfo, error) {
return nil, driver.InvalidPathError{Path: path}
}
_, err := gcsDriver.Stat("/bad")
So(err, ShouldNotBeNil)
var invalidPath driver.InvalidPathError
So(errors.As(err, &invalidPath), ShouldBeTrue)
So(invalidPath.DriverName, ShouldEqual, "gcs")
})
})
Convey("Writer", func() {
Convey("Success", func() {
storeMock.WriterFn = func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
return &mocks.FileWriterMock{}, nil
}
w, err := gcsDriver.Writer("/test", false)
So(err, ShouldBeNil)
So(w, ShouldNotBeNil)
})
Convey("Error", func() {
storeMock.WriterFn = func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
return nil, errTest
}
_, err := gcsDriver.Writer("/test", false)
So(err, ShouldNotBeNil)
})
})
Convey("WriteFile", func() {
Convey("Success", func() {
storeMock.WriterFn = func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
return &mocks.FileWriterMock{
WriteFn: func(p []byte) (int, error) {
return len(p), nil
},
CommitFn: func() error {
return nil
},
CloseFn: func() error {
return nil
},
}, nil
}
n, err := gcsDriver.WriteFile("/test", []byte("content"))
So(err, ShouldBeNil)
So(n, ShouldEqual, 7)
})
Convey("Writer Error", func() {
storeMock.WriterFn = func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
return nil, errTest
}
_, err := gcsDriver.WriteFile("/test", []byte("content"))
So(err, ShouldNotBeNil)
})
Convey("Write Error", func() {
storeMock.WriterFn = func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
return &mocks.FileWriterMock{
WriteFn: func(p []byte) (int, error) {
return 0, errTest
},
CloseFn: func() error { return nil },
}, nil
}
_, err := gcsDriver.WriteFile("/test", []byte("content"))
So(err, ShouldNotBeNil)
})
Convey("Commit Error", func() {
storeMock.WriterFn = func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
return &mocks.FileWriterMock{
WriteFn: func(p []byte) (int, error) {
return len(p), nil
},
CommitFn: func() error {
return errTest
},
CloseFn: func() error { return nil },
}, nil
}
_, err := gcsDriver.WriteFile("/test", []byte("content"))
So(err, ShouldNotBeNil)
})
})
Convey("Walk", func() {
storeMock.WalkFn = func(ctx context.Context, path string, f driver.WalkFn, _ ...func(*driver.WalkOptions)) error {
return nil
}
err := gcsDriver.Walk("/test", nil)
So(err, ShouldBeNil)
})
Convey("List", func() {
Convey("Success", func() {
storeMock.ListFn = func(ctx context.Context, path string) ([]string, error) {
return []string{"a"}, nil
}
l, err := gcsDriver.List("/test")
So(err, ShouldBeNil)
So(l, ShouldResemble, []string{"a"})
})
Convey("Error", func() {
storeMock.ListFn = func(ctx context.Context, path string) ([]string, error) {
return nil, errTest
}
_, err := gcsDriver.List("/test")
So(err, ShouldNotBeNil)
})
})
Convey("Move", func() {
storeMock.MoveFn = func(ctx context.Context, sourcePath, destPath string) error {
return nil
}
err := gcsDriver.Move("/src", "/dst")
So(err, ShouldBeNil)
})
Convey("SameFile", func() {
Convey("True", func() {
now := time.Now()
storeMock.StatFn = func(ctx context.Context, path string) (driver.FileInfo, error) {
return &fileInfoMock{
isDir: false,
size: 10,
modTime: now,
path: "/canonical/path",
}, nil
}
So(gcsDriver.SameFile("/path1", "/path2"), ShouldBeTrue)
})
Convey("False - Different ModTime", func() {
storeMock.StatFn = func(ctx context.Context, path string) (driver.FileInfo, error) {
modTime := time.Now()
if path == "/path2" {
modTime = modTime.Add(1 * time.Hour)
}
return &fileInfoMock{
isDir: false,
size: 10,
modTime: modTime,
path: path,
}, nil
}
So(gcsDriver.SameFile("/path1", "/path2"), ShouldBeFalse)
})
})
Convey("Link", func() {
storeMock.PutContentFn = func(ctx context.Context, path string, content []byte) error {
return nil
}
err := gcsDriver.Link("/src", "/dst")
So(err, ShouldBeNil)
})
})
}
func TestNewImageStore(t *testing.T) {
Convey("NewImageStore", t, func() {
storeMock := &mocks.StorageDriverMock{}
log := zlog.NewTestLogger()
metrics := monitoring.NewMetricsServer(false, log)
imgStore := gcs.NewImageStore("/tmp", "/tmp", true, true, log, metrics, nil, storeMock, nil, nil, nil)
So(imgStore, ShouldNotBeNil)
})
}
+38
View File
@@ -0,0 +1,38 @@
package gcs
import (
// Add gcs support.
"github.com/distribution/distribution/v3/registry/storage/driver"
// Load gcs driver.
_ "github.com/distribution/distribution/v3/registry/storage/driver/gcs"
"zotregistry.dev/zot/v2/pkg/compat"
"zotregistry.dev/zot/v2/pkg/extensions/events"
"zotregistry.dev/zot/v2/pkg/extensions/monitoring"
zlog "zotregistry.dev/zot/v2/pkg/log"
common "zotregistry.dev/zot/v2/pkg/storage/common"
"zotregistry.dev/zot/v2/pkg/storage/imagestore"
storageTypes "zotregistry.dev/zot/v2/pkg/storage/types"
)
// NewImageStore returns a new image store backed by cloud storages.
// see https://github.com/docker/docker.github.io/tree/master/registry/storage-drivers
// Use the last argument to properly set a cache database, or it will default to boltDB local storage.
func NewImageStore(rootDir string, cacheDir string, dedupe, commit bool, log zlog.Logger,
metrics monitoring.MetricServer, linter common.Lint, store driver.StorageDriver,
cacheDriver storageTypes.Cache, compat []compat.MediaCompatibility, recorder events.Recorder,
) storageTypes.ImageStore {
return imagestore.NewImageStore(
rootDir,
cacheDir,
dedupe,
commit,
log,
metrics,
linter,
New(store),
cacheDriver,
compat,
recorder,
)
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build needprivileges
//go:build needprivileges && linux
package local_test
+116 -304
View File
File diff suppressed because it is too large Load Diff
+21 -7
View File
@@ -18,6 +18,7 @@ import (
"zotregistry.dev/zot/v2/pkg/log"
common "zotregistry.dev/zot/v2/pkg/storage/common"
"zotregistry.dev/zot/v2/pkg/storage/constants"
"zotregistry.dev/zot/v2/pkg/storage/gcs"
"zotregistry.dev/zot/v2/pkg/storage/local"
"zotregistry.dev/zot/v2/pkg/storage/s3"
storageTypes "zotregistry.dev/zot/v2/pkg/storage/types"
@@ -63,7 +64,7 @@ func New(config *config.Config, linter common.Lint, metrics monitoring.MetricSer
)
} else {
storeName := fmt.Sprintf("%v", config.Storage.StorageDriver["name"])
if storeName != constants.S3StorageDriverName {
if storeName != constants.S3StorageDriverName && storeName != constants.GCSStorageDriverName {
log.Error().Err(zerr.ErrBadConfig).Str("storageDriver", storeName).
Msg("unsupported storage driver")
@@ -92,8 +93,15 @@ func New(config *config.Config, linter common.Lint, metrics monitoring.MetricSer
// false positive lint - linter does not implement Lint method
//nolint: typecheck,contextcheck
defaultStore = s3.NewImageStore(rootDir, config.Storage.RootDirectory,
config.Storage.Dedupe, config.Storage.Commit, log, metrics, linter, store, cacheDriver, config.HTTP.Compat, recorder)
if storeName == constants.S3StorageDriverName {
defaultStore = s3.NewImageStore(rootDir, config.Storage.RootDirectory,
config.Storage.Dedupe, config.Storage.Commit, log, metrics, linter, store, cacheDriver,
config.HTTP.Compat, recorder)
} else {
defaultStore = gcs.NewImageStore(rootDir, config.Storage.RootDirectory,
config.Storage.Dedupe, config.Storage.Commit, log, metrics, linter, store, cacheDriver,
config.HTTP.Compat, recorder)
}
}
storeController.DefaultStore = defaultStore
@@ -178,7 +186,7 @@ func getSubStore(cfg *config.Config, subPaths map[string]config.StorageConfig,
}
} else {
storeName := fmt.Sprintf("%v", storageConfig.StorageDriver["name"])
if storeName != constants.S3StorageDriverName {
if storeName != constants.S3StorageDriverName && storeName != constants.GCSStorageDriverName {
log.Error().Err(zerr.ErrBadConfig).Str("storageDriver", storeName).
Msg("unsupported storage driver")
@@ -210,9 +218,15 @@ func getSubStore(cfg *config.Config, subPaths map[string]config.StorageConfig,
// false positive lint - linter does not implement Lint method
//nolint: typecheck
subImageStore[route] = s3.NewImageStore(rootDir, storageConfig.RootDirectory,
storageConfig.Dedupe, storageConfig.Commit, log, metrics, linter, store, cacheDriver, cfg.HTTP.Compat, recorder,
)
if storeName == constants.S3StorageDriverName {
subImageStore[route] = s3.NewImageStore(rootDir, storageConfig.RootDirectory,
storageConfig.Dedupe, storageConfig.Commit, log, metrics, linter, store, cacheDriver, cfg.HTTP.Compat, recorder,
)
} else {
subImageStore[route] = gcs.NewImageStore(rootDir, storageConfig.RootDirectory,
storageConfig.Dedupe, storageConfig.Commit, log, metrics, linter, store, cacheDriver, cfg.HTTP.Compat, recorder,
)
}
}
}
+53 -27
View File
@@ -59,7 +59,9 @@ var DeleteReferrers = config.ImageRetention{ //nolint: gochecknoglobals
}
func cleanupStorage(store storageTypes.Driver, name string) {
_ = store.Delete(name)
if store != nil {
_ = store.Delete(name)
}
}
type createObjectStoreOpts struct {
@@ -207,7 +209,8 @@ func TestGetAllDedupeReposCandidates(t *testing.T) {
defer DumpKeys(t, opts.miniRedisAddr)
}
if testcase.storageType == storageConstants.S3StorageDriverName {
switch testcase.storageType {
case storageConstants.S3StorageDriverName:
tskip.SkipS3(t)
uuid, err := guuid.NewV4()
@@ -219,9 +222,10 @@ func TestGetAllDedupeReposCandidates(t *testing.T) {
opts.rootDir = testDir
var store storageTypes.Driver
store, imgStore, _, _ = createObjectsStore(opts)
defer cleanupStorage(store, testDir)
} else {
default:
_, imgStore, _, _ = createObjectsStore(opts)
}
@@ -279,7 +283,8 @@ func TestStorageAPIs(t *testing.T) {
defer DumpKeys(t, opts.miniRedisAddr)
}
if testcase.storageType == storageConstants.S3StorageDriverName {
switch testcase.storageType {
case storageConstants.S3StorageDriverName:
tskip.SkipS3(t)
uuid, err := guuid.NewV4()
@@ -291,9 +296,10 @@ func TestStorageAPIs(t *testing.T) {
opts.rootDir = testDir
var store storageTypes.Driver
store, imgStore, _, _ = createObjectsStore(opts)
defer cleanupStorage(store, testDir)
} else {
default:
_, imgStore, _, _ = createObjectsStore(opts)
}
@@ -1028,7 +1034,8 @@ func TestMandatoryAnnotations(t *testing.T) {
defer DumpKeys(t, opts.miniRedisAddr)
}
if testcase.storageType == storageConstants.S3StorageDriverName {
switch testcase.storageType {
case storageConstants.S3StorageDriverName:
tskip.SkipS3(t)
uuid, err := guuid.NewV4()
@@ -1040,6 +1047,7 @@ func TestMandatoryAnnotations(t *testing.T) {
opts.rootDir = testDir
var cacheDriver storageTypes.Cache
store, _, cacheDriver, _ = createObjectsStore(opts)
imgStore = imagestore.NewImageStore(testDir, cacheDir, false, false, log, metrics,
@@ -1050,8 +1058,9 @@ func TestMandatoryAnnotations(t *testing.T) {
}, store, cacheDriver, nil, nil)
defer cleanupStorage(store, testDir)
} else {
default:
var cacheDriver storageTypes.Cache
store, _, cacheDriver, _ = createObjectsStore(opts)
imgStore = imagestore.NewImageStore(cacheDir, cacheDir, true, true, log, metrics,
@@ -1115,8 +1124,10 @@ func TestMandatoryAnnotations(t *testing.T) {
}, store, nil, nil, nil)
} else {
var cacheDriver storageTypes.Cache
store, _, cacheDriver, _ = createObjectsStore(opts)
store, _, cacheDriver, err := createObjectsStore(opts)
if err != nil {
t.Fatal(err)
}
imgStore = imagestore.NewImageStore(cacheDir, cacheDir, true, true, log, metrics,
&mocks.MockedLint{
LintFn: func(repo string, manifestDigest godigest.Digest, imageStore storageTypes.ImageStore) (bool, error) {
@@ -1223,7 +1234,8 @@ func TestDeleteBlobsInUse(t *testing.T) {
defer DumpKeys(t, opts.miniRedisAddr)
}
if testcase.storageType == storageConstants.S3StorageDriverName {
switch testcase.storageType {
case storageConstants.S3StorageDriverName:
tskip.SkipS3(t)
uuid, err := guuid.NewV4()
@@ -1238,7 +1250,7 @@ func TestDeleteBlobsInUse(t *testing.T) {
store, imgStore, _, _ = createObjectsStore(opts)
defer cleanupStorage(store, testDir)
} else {
default:
_, imgStore, _, _ = createObjectsStore(opts)
}
@@ -1532,7 +1544,8 @@ func TestReuploadCorruptedBlob(t *testing.T) {
defer DumpKeys(t, opts.miniRedisAddr)
}
if testcase.storageType == storageConstants.S3StorageDriverName {
switch testcase.storageType {
case storageConstants.S3StorageDriverName:
tskip.SkipS3(t)
uuid, err := guuid.NewV4()
@@ -1545,7 +1558,7 @@ func TestReuploadCorruptedBlob(t *testing.T) {
driver, imgStore, _, _ = createObjectsStore(opts)
defer cleanupStorage(driver, testDir)
} else {
default:
driver, imgStore, _, _ = createObjectsStore(opts)
}
@@ -1666,7 +1679,8 @@ func TestStorageHandler(t *testing.T) {
defer DumpKeys(t, opts.miniRedisAddr)
}
if testcase.storageType == storageConstants.S3StorageDriverName {
switch testcase.storageType {
case storageConstants.S3StorageDriverName:
tskip.SkipS3(t)
var (
@@ -1695,7 +1709,7 @@ func TestStorageHandler(t *testing.T) {
thirdStorageDriver, thirdStore, _, _ = createObjectsStore(opts)
defer cleanupStorage(thirdStorageDriver, thirdRootDir)
} else {
default:
firstRootDir = t.TempDir()
opts.rootDir = firstRootDir
opts.cacheDir = firstRootDir
@@ -1784,7 +1798,8 @@ func TestGarbageCollectImageManifest(t *testing.T) {
Convey("Garbage collect with default/long delay", func() {
var imgStore storageTypes.ImageStore
if testcase.storageType == storageConstants.S3StorageDriverName {
switch testcase.storageType {
case storageConstants.S3StorageDriverName:
tskip.SkipS3(t)
uuid, err := guuid.NewV4()
@@ -1796,9 +1811,10 @@ func TestGarbageCollectImageManifest(t *testing.T) {
opts.rootDir = testDir
var store storageTypes.Driver
store, imgStore, _, _ = createObjectsStore(opts)
defer cleanupStorage(store, testDir)
} else {
default:
_, imgStore, _, _ = createObjectsStore(opts)
}
@@ -1945,7 +1961,8 @@ func TestGarbageCollectImageManifest(t *testing.T) {
gcDelay := 1 * time.Second
if testcase.storageType == storageConstants.S3StorageDriverName {
switch testcase.storageType {
case storageConstants.S3StorageDriverName:
tskip.SkipS3(t)
uuid, err := guuid.NewV4()
@@ -1957,9 +1974,10 @@ func TestGarbageCollectImageManifest(t *testing.T) {
opts.rootDir = testDir
var store storageTypes.Driver
store, imgStore, _, _ = createObjectsStore(opts)
defer cleanupStorage(store, testDir)
} else {
default:
_, imgStore, _, _ = createObjectsStore(opts)
}
@@ -2220,7 +2238,8 @@ func TestGarbageCollectImageManifest(t *testing.T) {
gcDelay := 3 * time.Second
if testcase.storageType == storageConstants.S3StorageDriverName {
switch testcase.storageType {
case storageConstants.S3StorageDriverName:
tskip.SkipS3(t)
uuid, err := guuid.NewV4()
@@ -2232,9 +2251,10 @@ func TestGarbageCollectImageManifest(t *testing.T) {
opts.rootDir = testDir
var store storageTypes.Driver
store, imgStore, _, _ = createObjectsStore(opts)
defer cleanupStorage(store, testDir)
} else {
default:
_, imgStore, _, _ = createObjectsStore(opts)
}
@@ -2467,7 +2487,8 @@ func TestGarbageCollectImageIndex(t *testing.T) {
Convey("Garbage collect with default/long delay", func() {
var imgStore storageTypes.ImageStore
if testcase.storageType == storageConstants.S3StorageDriverName {
switch testcase.storageType {
case storageConstants.S3StorageDriverName:
tskip.SkipS3(t)
uuid, err := guuid.NewV4()
@@ -2479,9 +2500,10 @@ func TestGarbageCollectImageIndex(t *testing.T) {
opts.rootDir = testDir
var store storageTypes.Driver
store, imgStore, _, _ = createObjectsStore(opts)
defer cleanupStorage(store, testDir)
} else {
default:
_, imgStore, _, _ = createObjectsStore(opts)
}
@@ -2586,7 +2608,8 @@ func TestGarbageCollectImageIndex(t *testing.T) {
gcDelay := 2 * time.Second
imageRetentionDelay := 2 * time.Second
if testcase.storageType == storageConstants.S3StorageDriverName {
switch testcase.storageType {
case storageConstants.S3StorageDriverName:
tskip.SkipS3(t)
uuid, err := guuid.NewV4()
@@ -2598,9 +2621,10 @@ func TestGarbageCollectImageIndex(t *testing.T) {
opts.rootDir = testDir
var store storageTypes.Driver
store, imgStore, _, _ = createObjectsStore(opts)
defer cleanupStorage(store, testDir)
} else {
default:
_, imgStore, _, _ = createObjectsStore(opts)
}
@@ -2876,7 +2900,8 @@ func TestGarbageCollectChainedImageIndexes(t *testing.T) {
defer DumpKeys(t, opts.miniRedisAddr)
}
if testcase.storageType == storageConstants.S3StorageDriverName {
switch testcase.storageType {
case storageConstants.S3StorageDriverName:
tskip.SkipS3(t)
uuid, err := guuid.NewV4()
@@ -2888,9 +2913,10 @@ func TestGarbageCollectChainedImageIndexes(t *testing.T) {
opts.rootDir = testDir
var store storageTypes.Driver
store, imgStore, _, _ = createObjectsStore(opts)
defer cleanupStorage(store, testDir)
} else {
default:
_, imgStore, _, _ = createObjectsStore(opts)
}
+3
View File
@@ -0,0 +1,3 @@
package common
type RlimT = uint64
+29 -12
View File
@@ -3,6 +3,7 @@ package mocks
import (
"context"
"io"
"net/http"
"strings"
"time"
@@ -10,16 +11,17 @@ import (
)
type StorageDriverMock struct {
NameFn func() string
GetContentFn func(ctx context.Context, path string) ([]byte, error)
PutContentFn func(ctx context.Context, path string, content []byte) error
ReaderFn func(ctx context.Context, path string, offset int64) (io.ReadCloser, error)
WriterFn func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error)
StatFn func(ctx context.Context, path string) (driver.FileInfo, error)
ListFn func(ctx context.Context, path string) ([]string, error)
MoveFn func(ctx context.Context, sourcePath, destPath string) error
DeleteFn func(ctx context.Context, path string) error
WalkFn func(ctx context.Context, path string, f driver.WalkFn) error
NameFn func() string
GetContentFn func(ctx context.Context, path string) ([]byte, error)
PutContentFn func(ctx context.Context, path string, content []byte) error
ReaderFn func(ctx context.Context, path string, offset int64) (io.ReadCloser, error)
WriterFn func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error)
StatFn func(ctx context.Context, path string) (driver.FileInfo, error)
ListFn func(ctx context.Context, path string) ([]string, error)
MoveFn func(ctx context.Context, sourcePath, destPath string) error
DeleteFn func(ctx context.Context, path string) error
WalkFn func(ctx context.Context, path string, f driver.WalkFn, options ...func(*driver.WalkOptions)) error
RedirectURLFn func(r *http.Request, path string) (string, error)
}
//nolint:gochecknoglobals
@@ -100,13 +102,23 @@ func (s *StorageDriverMock) Delete(ctx context.Context, path string) error {
return nil
}
func (s *StorageDriverMock) RedirectURL(r *http.Request, path string) (string, error) {
if s != nil && s.RedirectURLFn != nil {
return s.RedirectURLFn(r, path)
}
return "", nil
}
func (s *StorageDriverMock) URLFor(ctx context.Context, path string, options map[string]any) (string, error) {
return "", nil
}
func (s *StorageDriverMock) Walk(ctx context.Context, path string, f driver.WalkFn) error {
func (s *StorageDriverMock) Walk(ctx context.Context, path string, f driver.WalkFn,
options ...func(*driver.WalkOptions),
) error {
if s != nil && s.WalkFn != nil {
return s.WalkFn(ctx, path, f)
return s.WalkFn(ctx, path, f, options...)
}
return nil
@@ -115,9 +127,14 @@ func (s *StorageDriverMock) Walk(ctx context.Context, path string, f driver.Walk
type FileInfoMock struct {
IsDirFn func() bool
SizeFn func() int64
PathFn func() string
}
func (f *FileInfoMock) Path() string {
if f != nil && f.PathFn != nil {
return f.PathFn()
}
return ""
}
+8
View File
@@ -20,3 +20,11 @@ func SkipDynamo(t *testing.T) {
t.Skip("Skipping testing without AWS DynamoDB mock server")
}
}
func SkipGCS(t *testing.T) {
t.Helper()
if os.Getenv("GCSMOCK_ENDPOINT") == "" {
t.Skip("Skipping testing without GCS mock server")
}
}