s3: added logic for deduping blobs

Because s3 doesn't support hard links we store duplicated blobs
as empty files. When the original blob is deleted its content is
moved to the the next duplicated blob and so on.

Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
This commit is contained in:
Petu Eusebiu
2022-04-12 13:01:04 +03:00
committed by Ramkumar Chinchani
parent ad08c08986
commit 5e22acbbc4
11 changed files with 1027 additions and 117 deletions
+6 -2
View File
@@ -308,13 +308,15 @@ zot also supports different storage drivers for each subpath.
### Specifying S3 credentials ### Specifying S3 credentials
There are multiple ways to specify S3 credentials:
- Config file: - Config file:
``` ```
"storage": {
"rootDirectory": "/tmp/zot", # local path used to store dedupe cache database
"dedupe": true,
"storageDriver": { "storageDriver": {
"name": "s3", "name": "s3",
"rootdirectory": "/zot", # this is a prefix that is applied to all S3 keys to allow you to segment data in your bucket if necessary.
"region": "us-east-2", "region": "us-east-2",
"bucket": "zot-storage", "bucket": "zot-storage",
"secure": true, "secure": true,
@@ -324,6 +326,8 @@ There are multiple ways to specify S3 credentials:
} }
``` ```
There are multiple ways to specify S3 credentials besides config file:
- Environment variables: - Environment variables:
SDK looks for credentials in the following environment variables: SDK looks for credentials in the following environment variables:
+12 -4
View File
@@ -1,9 +1,11 @@
{ {
"distSpecVersion": "1.0.1-dev", "distSpecVersion": "1.0.1-dev",
"storage": { "storage": {
"rootDirectory": "/zot", "rootDirectory": "/tmp/zot",
"dedupe": true,
"storageDriver": { "storageDriver": {
"name": "s3", "name": "s3",
"rootdirectory": "/zot",
"region": "us-east-2", "region": "us-east-2",
"bucket": "zot-storage", "bucket": "zot-storage",
"secure": true, "secure": true,
@@ -11,9 +13,11 @@
}, },
"subPaths": { "subPaths": {
"/a": { "/a": {
"rootDirectory": "/zot-a", "rootDirectory": "/tmp/zot1",
"dedupe": false,
"storageDriver": { "storageDriver": {
"name": "s3", "name": "s3",
"rootdirectory": "/zot-a",
"region": "us-east-2", "region": "us-east-2",
"bucket": "zot-storage", "bucket": "zot-storage",
"secure": true, "secure": true,
@@ -21,9 +25,11 @@
} }
}, },
"/b": { "/b": {
"rootDirectory": "/zot-b", "rootDirectory": "/tmp/zot2",
"dedupe": true,
"storageDriver": { "storageDriver": {
"name": "s3", "name": "s3",
"rootdirectory": "/zot-b",
"region": "us-east-2", "region": "us-east-2",
"bucket": "zot-storage", "bucket": "zot-storage",
"secure": true, "secure": true,
@@ -31,9 +37,11 @@
} }
}, },
"/c": { "/c": {
"rootDirectory": "/zot-c", "rootDirectory": "/tmp/zot3",
"dedupe": true,
"storageDriver": { "storageDriver": {
"name": "s3", "name": "s3",
"rootdirectory": "/zot-c",
"region": "us-east-2", "region": "us-east-2",
"bucket": "zot-storage", "bucket": "zot-storage",
"secure": false, "secure": false,
+22 -6
View File
@@ -218,7 +218,8 @@ func (c *Controller) InitImageStore(reloadCtx context.Context) error {
c.StoreController = storage.StoreController{} c.StoreController = storage.StoreController{}
if c.Config.Storage.RootDirectory != "" { if c.Config.Storage.RootDirectory != "" {
if c.Config.Storage.Dedupe { // no need to validate hard links work on s3
if c.Config.Storage.Dedupe && c.Config.Storage.StorageDriver == nil {
err := storage.ValidateHardLink(c.Config.Storage.RootDirectory) err := storage.ValidateHardLink(c.Config.Storage.RootDirectory)
if err != nil { if err != nil {
c.Log.Warn().Msg("input storage root directory filesystem does not supports hardlinking," + c.Log.Warn().Msg("input storage root directory filesystem does not supports hardlinking," +
@@ -229,7 +230,7 @@ func (c *Controller) InitImageStore(reloadCtx context.Context) error {
} }
var defaultStore storage.ImageStore var defaultStore storage.ImageStore
if len(c.Config.Storage.StorageDriver) == 0 { if c.Config.Storage.StorageDriver == nil {
defaultStore = storage.NewImageStore(c.Config.Storage.RootDirectory, defaultStore = storage.NewImageStore(c.Config.Storage.RootDirectory,
c.Config.Storage.GC, c.Config.Storage.GCDelay, c.Config.Storage.Dedupe, c.Config.Storage.Commit, c.Log, c.Metrics) c.Config.Storage.GC, c.Config.Storage.GCDelay, c.Config.Storage.Dedupe, c.Config.Storage.Commit, c.Log, c.Metrics)
} else { } else {
@@ -246,7 +247,14 @@ func (c *Controller) InitImageStore(reloadCtx context.Context) error {
return err return err
} }
defaultStore = s3.NewImageStore(c.Config.Storage.RootDirectory, /* in the case of s3 c.Config.Storage.RootDirectory is used for caching blobs locally and
c.Config.Storage.StorageDriver["rootdirectory"] is the actual rootDir in s3 */
rootDir := "/"
if c.Config.Storage.StorageDriver["rootdirectory"] != nil {
rootDir = fmt.Sprintf("%v", c.Config.Storage.StorageDriver["rootdirectory"])
}
defaultStore = s3.NewImageStore(rootDir, c.Config.Storage.RootDirectory,
c.Config.Storage.GC, c.Config.Storage.GCDelay, c.Config.Storage.Dedupe, c.Config.Storage.GC, c.Config.Storage.GCDelay, c.Config.Storage.Dedupe,
c.Config.Storage.Commit, c.Log, c.Metrics, store) c.Config.Storage.Commit, c.Log, c.Metrics, store)
} }
@@ -267,7 +275,8 @@ func (c *Controller) InitImageStore(reloadCtx context.Context) error {
// creating image store per subpaths // creating image store per subpaths
for route, storageConfig := range subPaths { for route, storageConfig := range subPaths {
if storageConfig.Dedupe { // no need to validate hard links work on s3
if storageConfig.Dedupe && storageConfig.StorageDriver == nil {
err := storage.ValidateHardLink(storageConfig.RootDirectory) err := storage.ValidateHardLink(storageConfig.RootDirectory)
if err != nil { if err != nil {
c.Log.Warn().Msg("input storage root directory filesystem does not supports hardlinking, " + c.Log.Warn().Msg("input storage root directory filesystem does not supports hardlinking, " +
@@ -277,7 +286,7 @@ func (c *Controller) InitImageStore(reloadCtx context.Context) error {
} }
} }
if len(storageConfig.StorageDriver) == 0 { if storageConfig.StorageDriver == nil {
subImageStore[route] = storage.NewImageStore(storageConfig.RootDirectory, subImageStore[route] = storage.NewImageStore(storageConfig.RootDirectory,
storageConfig.GC, storageConfig.GCDelay, storageConfig.Dedupe, storageConfig.Commit, c.Log, c.Metrics) storageConfig.GC, storageConfig.GCDelay, storageConfig.Dedupe, storageConfig.Commit, c.Log, c.Metrics)
} else { } else {
@@ -294,7 +303,14 @@ func (c *Controller) InitImageStore(reloadCtx context.Context) error {
return err return err
} }
subImageStore[route] = s3.NewImageStore(storageConfig.RootDirectory, /* in the case of s3 c.Config.Storage.RootDirectory is used for caching blobs locally and
c.Config.Storage.StorageDriver["rootdirectory"] is the actual rootDir in s3 */
rootDir := "/"
if c.Config.Storage.StorageDriver["rootdirectory"] != nil {
rootDir = fmt.Sprintf("%v", c.Config.Storage.StorageDriver["rootdirectory"])
}
subImageStore[route] = s3.NewImageStore(rootDir, storageConfig.RootDirectory,
storageConfig.GC, storageConfig.GCDelay, storageConfig.Dedupe, storageConfig.Commit, c.Log, c.Metrics, store) storageConfig.GC, storageConfig.GCDelay, storageConfig.Dedupe, storageConfig.Commit, c.Log, c.Metrics, store)
} }
} }
+4 -4
View File
@@ -151,8 +151,8 @@ func TestObjectStorageController(t *testing.T) {
conf := config.New() conf := config.New()
conf.HTTP.Port = port conf.HTTP.Port = port
storageDriverParams := map[string]interface{}{ storageDriverParams := map[string]interface{}{
"rootDir": "zot", "rootdirectory": "zot",
"name": storage.S3StorageDriverName, "name": storage.S3StorageDriverName,
} }
conf.Storage.StorageDriver = storageDriverParams conf.Storage.StorageDriver = storageDriverParams
ctlr := api.NewController(conf) ctlr := api.NewController(conf)
@@ -174,7 +174,7 @@ func TestObjectStorageController(t *testing.T) {
endpoint := os.Getenv("S3MOCK_ENDPOINT") endpoint := os.Getenv("S3MOCK_ENDPOINT")
storageDriverParams := map[string]interface{}{ storageDriverParams := map[string]interface{}{
"rootDir": "zot", "rootdirectory": "zot",
"name": storage.S3StorageDriverName, "name": storage.S3StorageDriverName,
"region": "us-east-2", "region": "us-east-2",
"bucket": bucket, "bucket": bucket,
@@ -206,7 +206,7 @@ func TestObjectStorageControllerSubPaths(t *testing.T) {
endpoint := os.Getenv("S3MOCK_ENDPOINT") endpoint := os.Getenv("S3MOCK_ENDPOINT")
storageDriverParams := map[string]interface{}{ storageDriverParams := map[string]interface{}{
"rootDir": "zot", "rootdirectory": "zot",
"name": storage.S3StorageDriverName, "name": storage.S3StorageDriverName,
"region": "us-east-2", "region": "us-east-2",
"bucket": bucket, "bucket": bucket,
+26 -18
View File
@@ -13,13 +13,15 @@ import (
const ( const (
BlobsCache = "blobs" BlobsCache = "blobs"
DBExtensionName = ".db"
dbCacheLockCheckTimeout = 10 * time.Second dbCacheLockCheckTimeout = 10 * time.Second
) )
type Cache struct { type Cache struct {
rootDir string rootDir string
db *bbolt.DB db *bbolt.DB
log zlog.Logger log zlog.Logger
useRelPaths bool // weather or not to use relative paths, should be true for filesystem and false for s3
} }
// Blob is a blob record. // Blob is a blob record.
@@ -27,8 +29,8 @@ type Blob struct {
Path string Path string
} }
func NewCache(rootDir, name string, log zlog.Logger) *Cache { func NewCache(rootDir string, name string, useRelPaths bool, log zlog.Logger) *Cache {
dbPath := path.Join(rootDir, name+".db") dbPath := path.Join(rootDir, name+DBExtensionName)
dbOpts := &bbolt.Options{ dbOpts := &bbolt.Options{
Timeout: dbCacheLockCheckTimeout, Timeout: dbCacheLockCheckTimeout,
FreelistType: bbolt.FreelistArrayType, FreelistType: bbolt.FreelistArrayType,
@@ -57,7 +59,7 @@ func NewCache(rootDir, name string, log zlog.Logger) *Cache {
return nil return nil
} }
return &Cache{rootDir: rootDir, db: cacheDB, log: log} return &Cache{rootDir: rootDir, db: cacheDB, useRelPaths: useRelPaths, log: log}
} }
func (c *Cache) PutBlob(digest, path string) error { func (c *Cache) PutBlob(digest, path string) error {
@@ -68,9 +70,12 @@ func (c *Cache) PutBlob(digest, path string) error {
} }
// use only relative (to rootDir) paths on blobs // use only relative (to rootDir) paths on blobs
relp, err := filepath.Rel(c.rootDir, path) var err error
if err != nil { if c.useRelPaths {
c.log.Error().Err(err).Str("path", path).Msg("unable to get relative path") path, err = filepath.Rel(c.rootDir, path)
if err != nil {
c.log.Error().Err(err).Str("path", path).Msg("unable to get relative path")
}
} }
if err := c.db.Update(func(tx *bbolt.Tx) error { if err := c.db.Update(func(tx *bbolt.Tx) error {
@@ -91,8 +96,8 @@ func (c *Cache) PutBlob(digest, path string) error {
return err return err
} }
if err := bucket.Put([]byte(relp), nil); err != nil { if err := bucket.Put([]byte(path), nil); err != nil {
c.log.Error().Err(err).Str("bucket", digest).Str("value", relp).Msg("unable to put record") c.log.Error().Err(err).Str("bucket", digest).Str("value", path).Msg("unable to put record")
return err return err
} }
@@ -166,9 +171,12 @@ func (c *Cache) HasBlob(digest, blob string) bool {
func (c *Cache) DeleteBlob(digest, path string) error { func (c *Cache) DeleteBlob(digest, path string) error {
// use only relative (to rootDir) paths on blobs // use only relative (to rootDir) paths on blobs
relp, err := filepath.Rel(c.rootDir, path) var err error
if err != nil { if c.useRelPaths {
c.log.Error().Err(err).Str("path", path).Msg("unable to get relative path") path, err = filepath.Rel(c.rootDir, path)
if err != nil {
c.log.Error().Err(err).Str("path", path).Msg("unable to get relative path")
}
} }
if err := c.db.Update(func(tx *bbolt.Tx) error { if err := c.db.Update(func(tx *bbolt.Tx) error {
@@ -186,8 +194,8 @@ func (c *Cache) DeleteBlob(digest, path string) error {
return errors.ErrCacheMiss return errors.ErrCacheMiss
} }
if err := bucket.Delete([]byte(relp)); err != nil { if err := bucket.Delete([]byte(path)); err != nil {
c.log.Error().Err(err).Str("digest", digest).Str("path", relp).Msg("unable to delete") c.log.Error().Err(err).Str("digest", digest).Str("path", path).Msg("unable to delete")
return err return err
} }
@@ -196,9 +204,9 @@ func (c *Cache) DeleteBlob(digest, path string) error {
k, _ := cur.First() k, _ := cur.First()
if k == nil { if k == nil {
c.log.Debug().Str("digest", digest).Str("path", relp).Msg("deleting empty bucket") c.log.Debug().Str("digest", digest).Str("path", path).Msg("deleting empty bucket")
if err := root.DeleteBucket([]byte(digest)); err != nil { if err := root.DeleteBucket([]byte(digest)); err != nil {
c.log.Error().Err(err).Str("digest", digest).Str("path", relp).Msg("unable to delete") c.log.Error().Err(err).Str("digest", digest).Str("path", path).Msg("unable to delete")
return err return err
} }
+2 -2
View File
@@ -17,9 +17,9 @@ func TestCache(t *testing.T) {
log := log.NewLogger("debug", "") log := log.NewLogger("debug", "")
So(log, ShouldNotBeNil) So(log, ShouldNotBeNil)
So(storage.NewCache("/deadBEEF", "cache_test", log), ShouldBeNil) So(storage.NewCache("/deadBEEF", "cache_test", true, log), ShouldBeNil)
cache := storage.NewCache(dir, "cache_test", log) cache := storage.NewCache(dir, "cache_test", true, log)
So(cache, ShouldNotBeNil) So(cache, ShouldNotBeNil)
val, err := cache.GetBlob("key") val, err := cache.GetBlob("key")
+681 -45
View File
@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
_ "crypto/sha256" _ "crypto/sha256"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@@ -19,6 +20,7 @@ import (
_ "github.com/docker/distribution/registry/storage/driver/s3-aws" _ "github.com/docker/distribution/registry/storage/driver/s3-aws"
guuid "github.com/gofrs/uuid" guuid "github.com/gofrs/uuid"
godigest "github.com/opencontainers/go-digest" godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/rs/zerolog" "github.com/rs/zerolog"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"gopkg.in/resty.v1" "gopkg.in/resty.v1"
@@ -27,6 +29,7 @@ import (
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage" "zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/storage/s3" "zotregistry.io/zot/pkg/storage/s3"
"zotregistry.io/zot/pkg/test"
) )
// nolint: gochecknoglobals // nolint: gochecknoglobals
@@ -50,15 +53,19 @@ func skipIt(t *testing.T) {
} }
} }
func createMockStorage(rootDir string, store driver.StorageDriver) storage.ImageStore { func createMockStorage(rootDir string, cacheDir string, dedupe bool, store driver.StorageDriver) storage.ImageStore {
log := log.Logger{Logger: zerolog.New(os.Stdout)} log := log.Logger{Logger: zerolog.New(os.Stdout)}
metrics := monitoring.NewMetricsServer(false, log) metrics := monitoring.NewMetricsServer(false, log)
il := s3.NewImageStore(rootDir, false, storage.DefaultGCDelay, false, false, log, metrics, store) il := s3.NewImageStore(rootDir, cacheDir, false, storage.DefaultGCDelay, dedupe, false, log, metrics, store)
return il return il
} }
func createObjectsStore(rootDir string) (driver.StorageDriver, storage.ImageStore, error) { func createObjectsStore(rootDir string, cacheDir string, dedupe bool) (
driver.StorageDriver,
storage.ImageStore,
error,
) {
bucket := "zot-storage-test" bucket := "zot-storage-test"
endpoint := os.Getenv("S3MOCK_ENDPOINT") endpoint := os.Getenv("S3MOCK_ENDPOINT")
storageDriverParams := map[string]interface{}{ storageDriverParams := map[string]interface{}{
@@ -67,6 +74,8 @@ func createObjectsStore(rootDir string) (driver.StorageDriver, storage.ImageStor
"region": "us-east-2", "region": "us-east-2",
"bucket": bucket, "bucket": bucket,
"regionendpoint": endpoint, "regionendpoint": endpoint,
"accesskey": "minioadmin",
"secretkey": "minioadmin",
"secure": false, "secure": false,
"skipverify": false, "skipverify": false,
} }
@@ -86,13 +95,14 @@ func createObjectsStore(rootDir string) (driver.StorageDriver, storage.ImageStor
log := log.Logger{Logger: zerolog.New(os.Stdout)} log := log.Logger{Logger: zerolog.New(os.Stdout)}
metrics := monitoring.NewMetricsServer(false, log) metrics := monitoring.NewMetricsServer(false, log)
il := s3.NewImageStore(rootDir, false, storage.DefaultGCDelay, false, false, log, metrics, store) il := s3.NewImageStore(rootDir, cacheDir, false, storage.DefaultGCDelay, dedupe, false, log, metrics, store)
return store, il, err return store, il, err
} }
type FileInfoMock struct { type FileInfoMock struct {
isDirFn func() bool isDirFn func() bool
sizeFn func() int64
} }
func (f *FileInfoMock) Path() string { func (f *FileInfoMock) Path() string {
@@ -100,6 +110,10 @@ func (f *FileInfoMock) Path() string {
} }
func (f *FileInfoMock) Size() int64 { func (f *FileInfoMock) Size() int64 {
if f != nil && f.sizeFn != nil {
return f.sizeFn()
}
return int64(fileInfoSize) return int64(fileInfoSize)
} }
@@ -265,7 +279,7 @@ func TestStorageDriverStatFunction(t *testing.T) {
testDir := path.Join("/oci-repo-test", uuid.String()) testDir := path.Join("/oci-repo-test", uuid.String())
storeDriver, imgStore, _ := createObjectsStore(testDir) storeDriver, imgStore, _ := createObjectsStore(testDir, t.TempDir(), true)
defer cleanupStorage(storeDriver, testDir) defer cleanupStorage(storeDriver, testDir)
/* There is an issue with storageDriver.Stat() that returns a storageDriver.FileInfo() /* There is an issue with storageDriver.Stat() that returns a storageDriver.FileInfo()
@@ -345,11 +359,12 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
testDir := path.Join("/oci-repo-test", uuid.String()) testDir := path.Join("/oci-repo-test", uuid.String())
storeDriver, imgStore, _ := createObjectsStore(testDir) tdir := t.TempDir()
storeDriver, imgStore, _ := createObjectsStore(testDir, tdir, true)
defer cleanupStorage(storeDriver, testDir) defer cleanupStorage(storeDriver, testDir)
Convey("Invalid validate repo", t, func(c C) { Convey("Invalid validate repo", t, func(c C) {
So(imgStore, ShouldNotBeNil)
So(imgStore.InitRepo(testImage), ShouldBeNil) So(imgStore.InitRepo(testImage), ShouldBeNil)
objects, err := storeDriver.List(context.Background(), path.Join(imgStore.RootDir(), testImage)) objects, err := storeDriver.List(context.Background(), path.Join(imgStore.RootDir(), testImage))
So(err, ShouldBeNil) So(err, ShouldBeNil)
@@ -365,9 +380,6 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Invalid get image tags", t, func(c C) { Convey("Invalid get image tags", t, func(c C) {
storeDriver, imgStore, err := createObjectsStore(testDir)
defer cleanupStorage(storeDriver, testDir)
So(err, ShouldBeNil)
So(imgStore.InitRepo(testImage), ShouldBeNil) So(imgStore.InitRepo(testImage), ShouldBeNil)
So(storeDriver.Move(context.Background(), path.Join(testDir, testImage, "index.json"), So(storeDriver.Move(context.Background(), path.Join(testDir, testImage, "index.json"),
@@ -386,10 +398,6 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Invalid get image manifest", t, func(c C) { Convey("Invalid get image manifest", t, func(c C) {
storeDriver, imgStore, err := createObjectsStore(testDir)
defer cleanupStorage(storeDriver, testDir)
So(err, ShouldBeNil)
So(imgStore, ShouldNotBeNil)
So(imgStore.InitRepo(testImage), ShouldBeNil) So(imgStore.InitRepo(testImage), ShouldBeNil)
So(storeDriver.Delete(context.Background(), path.Join(testDir, testImage, "index.json")), ShouldBeNil) So(storeDriver.Delete(context.Background(), path.Join(testDir, testImage, "index.json")), ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest(testImage, "") _, _, _, err = imgStore.GetImageManifest(testImage, "")
@@ -402,9 +410,6 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Invalid validate repo", t, func(c C) { Convey("Invalid validate repo", t, func(c C) {
storeDriver, imgStore, err := createObjectsStore(testDir)
defer cleanupStorage(storeDriver, testDir)
So(err, ShouldBeNil)
So(imgStore, ShouldNotBeNil) So(imgStore, ShouldNotBeNil)
So(imgStore.InitRepo(testImage), ShouldBeNil) So(imgStore.InitRepo(testImage), ShouldBeNil)
@@ -421,9 +426,6 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Invalid finish blob upload", t, func(c C) { Convey("Invalid finish blob upload", t, func(c C) {
storeDriver, imgStore, err := createObjectsStore(testDir)
defer cleanupStorage(storeDriver, testDir)
So(err, ShouldBeNil)
So(imgStore, ShouldNotBeNil) So(imgStore, ShouldNotBeNil)
So(imgStore.InitRepo(testImage), ShouldBeNil) So(imgStore.InitRepo(testImage), ShouldBeNil)
@@ -455,7 +457,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test storage driver errors", t, func(c C) { Convey("Test storage driver errors", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
listFn: func(ctx context.Context, path string) ([]string, error) { listFn: func(ctx context.Context, path string) ([]string, error) {
return []string{testImage}, errS3 return []string{testImage}, errS3
}, },
@@ -527,17 +529,32 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test ValidateRepo", t, func(c C) { Convey("Test ValidateRepo", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ tdir := t.TempDir()
imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
listFn: func(ctx context.Context, path string) ([]string, error) { listFn: func(ctx context.Context, path string) ([]string, error) {
return []string{testImage, testImage}, errS3 return []string{testImage, testImage}, errS3
}, },
}) })
_, err := imgStore.ValidateRepo(testImage) _, err := imgStore.ValidateRepo(testImage)
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
listFn: func(ctx context.Context, path string) ([]string, error) {
return []string{testImage, testImage}, nil
},
statFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
return nil, errS3
},
})
_, err = imgStore.ValidateRepo(testImage)
So(err, ShouldNotBeNil)
}) })
Convey("Test ValidateRepo2", t, func(c C) { Convey("Test ValidateRepo2", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
listFn: func(ctx context.Context, path string) ([]string, error) { listFn: func(ctx context.Context, path string) ([]string, error) {
return []string{"test/test/oci-layout", "test/test/index.json"}, nil return []string{"test/test/oci-layout", "test/test/index.json"}, nil
}, },
@@ -550,7 +567,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test ValidateRepo3", t, func(c C) { Convey("Test ValidateRepo3", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
listFn: func(ctx context.Context, path string) ([]string, error) { listFn: func(ctx context.Context, path string) ([]string, error) {
return []string{"test/test/oci-layout", "test/test/index.json"}, nil return []string{"test/test/oci-layout", "test/test/index.json"}, nil
}, },
@@ -567,7 +584,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
Convey("Test ValidateRepo4", t, func(c C) { Convey("Test ValidateRepo4", t, func(c C) {
ociLayout := []byte(`{"imageLayoutVersion": "9.9.9"}`) ociLayout := []byte(`{"imageLayoutVersion": "9.9.9"}`)
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
listFn: func(ctx context.Context, path string) ([]string, error) { listFn: func(ctx context.Context, path string) ([]string, error) {
return []string{"test/test/oci-layout", "test/test/index.json"}, nil return []string{"test/test/oci-layout", "test/test/index.json"}, nil
}, },
@@ -583,7 +600,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test GetRepositories", t, func(c C) { Convey("Test GetRepositories", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
walkFn: func(ctx context.Context, path string, f driver.WalkFn) error { walkFn: func(ctx context.Context, path string, f driver.WalkFn) error {
return f(new(FileInfoMock)) return f(new(FileInfoMock))
}, },
@@ -594,7 +611,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test DeleteImageManifest", t, func(c C) { Convey("Test DeleteImageManifest", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
getContentFn: func(ctx context.Context, path string) ([]byte, error) { getContentFn: func(ctx context.Context, path string) ([]byte, error) {
return []byte{}, errS3 return []byte{}, errS3
}, },
@@ -604,13 +621,13 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test DeleteImageManifest2", t, func(c C) { Convey("Test DeleteImageManifest2", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{}) imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{})
err := imgStore.DeleteImageManifest(testImage, "1.0") err := imgStore.DeleteImageManifest(testImage, "1.0")
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
}) })
Convey("Test NewBlobUpload", t, func(c C) { Convey("Test NewBlobUpload", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
putContentFn: func(ctx context.Context, path string, content []byte) error { putContentFn: func(ctx context.Context, path string, content []byte) error {
return errS3 return errS3
}, },
@@ -620,7 +637,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test GetBlobUpload", t, func(c C) { Convey("Test GetBlobUpload", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
statFn: func(ctx context.Context, path string) (driver.FileInfo, error) { statFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
return &FileInfoMock{}, errS3 return &FileInfoMock{}, errS3
}, },
@@ -630,7 +647,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test PutBlobChunkStreamed", t, func(c C) { Convey("Test PutBlobChunkStreamed", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
writerFn: func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) { writerFn: func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
return &FileWriterMock{}, errS3 return &FileWriterMock{}, errS3
}, },
@@ -640,7 +657,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test PutBlobChunkStreamed2", t, func(c C) { Convey("Test PutBlobChunkStreamed2", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
writerFn: func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) { writerFn: func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
return &FileWriterMock{writeFn: func(b []byte) (int, error) { return &FileWriterMock{writeFn: func(b []byte) (int, error) {
return 0, errS3 return 0, errS3
@@ -652,7 +669,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test PutBlobChunk", t, func(c C) { Convey("Test PutBlobChunk", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
writerFn: func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) { writerFn: func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
return &FileWriterMock{}, errS3 return &FileWriterMock{}, errS3
}, },
@@ -662,7 +679,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test PutBlobChunk2", t, func(c C) { Convey("Test PutBlobChunk2", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
writerFn: func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) { writerFn: func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
return &FileWriterMock{ return &FileWriterMock{
writeFn: func(b []byte) (int, error) { writeFn: func(b []byte) (int, error) {
@@ -679,7 +696,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test PutBlobChunk3", t, func(c C) { Convey("Test PutBlobChunk3", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
writerFn: func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) { writerFn: func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
return &FileWriterMock{ return &FileWriterMock{
writeFn: func(b []byte) (int, error) { writeFn: func(b []byte) (int, error) {
@@ -693,7 +710,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test FinishBlobUpload", t, func(c C) { Convey("Test FinishBlobUpload", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
writerFn: func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) { writerFn: func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
return &FileWriterMock{ return &FileWriterMock{
commitFn: func() error { commitFn: func() error {
@@ -708,7 +725,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test FinishBlobUpload2", t, func(c C) { Convey("Test FinishBlobUpload2", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
writerFn: func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) { writerFn: func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
return &FileWriterMock{ return &FileWriterMock{
closeFn: func() error { closeFn: func() error {
@@ -723,7 +740,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test FinishBlobUpload3", t, func(c C) { Convey("Test FinishBlobUpload3", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
readerFn: func(ctx context.Context, path string, offset int64) (io.ReadCloser, error) { readerFn: func(ctx context.Context, path string, offset int64) (io.ReadCloser, error) {
return nil, errS3 return nil, errS3
}, },
@@ -734,7 +751,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test FinishBlobUpload4", t, func(c C) { Convey("Test FinishBlobUpload4", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
moveFn: func(ctx context.Context, sourcePath, destPath string) error { moveFn: func(ctx context.Context, sourcePath, destPath string) error {
return errS3 return errS3
}, },
@@ -745,7 +762,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test FullBlobUpload", t, func(c C) { Convey("Test FullBlobUpload", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
writerFn: func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) { writerFn: func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
return &FileWriterMock{}, errS3 return &FileWriterMock{}, errS3
}, },
@@ -756,14 +773,14 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test FullBlobUpload2", t, func(c C) { Convey("Test FullBlobUpload2", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{}) imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{})
d := godigest.FromBytes([]byte(" ")) d := godigest.FromBytes([]byte(" "))
_, _, err := imgStore.FullBlobUpload(testImage, ioutil.NopCloser(strings.NewReader("")), d.String()) _, _, err := imgStore.FullBlobUpload(testImage, ioutil.NopCloser(strings.NewReader("")), d.String())
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
}) })
Convey("Test FullBlobUpload3", t, func(c C) { Convey("Test FullBlobUpload3", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
moveFn: func(ctx context.Context, sourcePath, destPath string) error { moveFn: func(ctx context.Context, sourcePath, destPath string) error {
return errS3 return errS3
}, },
@@ -774,7 +791,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test GetBlob", t, func(c C) { Convey("Test GetBlob", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
readerFn: func(ctx context.Context, path string, offset int64) (io.ReadCloser, error) { readerFn: func(ctx context.Context, path string, offset int64) (io.ReadCloser, error) {
return ioutil.NopCloser(strings.NewReader("")), errS3 return ioutil.NopCloser(strings.NewReader("")), errS3
}, },
@@ -785,7 +802,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test DeleteBlob", t, func(c C) { Convey("Test DeleteBlob", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
deleteFn: func(ctx context.Context, path string) error { deleteFn: func(ctx context.Context, path string) error {
return errS3 return errS3
}, },
@@ -796,7 +813,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
}) })
Convey("Test GetReferrers", t, func(c C) { Convey("Test GetReferrers", t, func(c C) {
imgStore = createMockStorage(testDir, &StorageDriverMock{ imgStore = createMockStorage(testDir, tdir, false, &StorageDriverMock{
deleteFn: func(ctx context.Context, path string) error { deleteFn: func(ctx context.Context, path string) error {
return errS3 return errS3
}, },
@@ -807,3 +824,622 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
So(err, ShouldEqual, zerr.ErrMethodNotSupported) So(err, ShouldEqual, zerr.ErrMethodNotSupported)
}) })
} }
func TestS3Dedupe(t *testing.T) {
skipIt(t)
Convey("Dedupe", t, func(c C) {
uuid, err := guuid.NewV4()
if err != nil {
panic(err)
}
testDir := path.Join("/oci-repo-test", uuid.String())
tdir := t.TempDir()
storeDriver, imgStore, _ := createObjectsStore(testDir, tdir, true)
defer cleanupStorage(storeDriver, testDir)
// manifest1
upload, err := imgStore.NewBlobUpload("dedupe1")
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
content := []byte("test-data3")
buf := bytes.NewBuffer(content)
buflen := buf.Len()
digest := godigest.FromBytes(content)
blob, err := imgStore.PutBlobChunkStreamed("dedupe1", upload, buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
blobDigest1 := strings.Split(digest.String(), ":")[1]
So(blobDigest1, ShouldNotBeEmpty)
err = imgStore.FinishBlobUpload("dedupe1", upload, buf, digest.String())
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
_, checkBlobSize1, err := imgStore.CheckBlob("dedupe1", digest.String())
So(checkBlobSize1, ShouldBeGreaterThan, 0)
So(err, ShouldBeNil)
_, getBlobSize1, err := imgStore.GetBlob("dedupe1", digest.String(), "application/vnd.oci.image.layer.v1.tar+gzip")
So(getBlobSize1, ShouldBeGreaterThan, 0)
So(err, ShouldBeNil)
cblob, cdigest := test.GetRandomImageConfig()
_, clen, err := imgStore.FullBlobUpload("dedupe1", bytes.NewReader(cblob), cdigest.String())
So(err, ShouldBeNil)
So(clen, ShouldEqual, len(cblob))
hasBlob, _, err := imgStore.CheckBlob("dedupe1", cdigest.String())
So(err, ShouldBeNil)
So(hasBlob, ShouldEqual, true)
manifest := ispec.Manifest{
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest,
Size: int64(buflen),
},
},
}
manifest.SchemaVersion = 2
manifestBuf, err := json.Marshal(manifest)
So(err, ShouldBeNil)
digest = godigest.FromBytes(manifestBuf)
_, err = imgStore.PutImageManifest("dedupe1", digest.String(), ispec.MediaTypeImageManifest, manifestBuf)
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest("dedupe1", digest.String())
So(err, ShouldBeNil)
// manifest2
upload, err = imgStore.NewBlobUpload("dedupe2")
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
content = []byte("test-data3")
buf = bytes.NewBuffer(content)
buflen = buf.Len()
digest = godigest.FromBytes(content)
blob, err = imgStore.PutBlobChunkStreamed("dedupe2", upload, buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
blobDigest2 := strings.Split(digest.String(), ":")[1]
So(blobDigest2, ShouldNotBeEmpty)
err = imgStore.FinishBlobUpload("dedupe2", upload, buf, digest.String())
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
_, checkBlobSize2, err := imgStore.CheckBlob("dedupe2", digest.String())
So(err, ShouldBeNil)
So(checkBlobSize2, ShouldBeGreaterThan, 0)
_, getBlobSize2, err := imgStore.GetBlob("dedupe2", digest.String(), "application/vnd.oci.image.layer.v1.tar+gzip")
So(err, ShouldBeNil)
So(getBlobSize2, ShouldBeGreaterThan, 0)
So(checkBlobSize1, ShouldEqual, checkBlobSize2)
So(getBlobSize1, ShouldEqual, getBlobSize2)
cblob, cdigest = test.GetRandomImageConfig()
_, clen, err = imgStore.FullBlobUpload("dedupe2", bytes.NewReader(cblob), cdigest.String())
So(err, ShouldBeNil)
So(clen, ShouldEqual, len(cblob))
hasBlob, _, err = imgStore.CheckBlob("dedupe2", cdigest.String())
So(err, ShouldBeNil)
So(hasBlob, ShouldEqual, true)
manifest = ispec.Manifest{
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest,
Size: int64(buflen),
},
},
}
manifest.SchemaVersion = 2
manifestBuf, err = json.Marshal(manifest)
So(err, ShouldBeNil)
digest = godigest.FromBytes(manifestBuf)
_, err = imgStore.PutImageManifest("dedupe2", "1.0", ispec.MediaTypeImageManifest, manifestBuf)
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest("dedupe2", digest.String())
So(err, ShouldBeNil)
fi1, err := storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe1", "blobs", "sha256", blobDigest1))
So(err, ShouldBeNil)
fi2, err := storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe2", "blobs", "sha256", blobDigest2))
So(err, ShouldBeNil)
// original blob should have the real content of blob
So(fi1.Size(), ShouldNotEqual, fi2.Size())
So(fi1.Size(), ShouldBeGreaterThan, 0)
// deduped blob should be of size 0
So(fi2.Size(), ShouldEqual, 0)
Convey("Check that delete blobs moves the real content to the next contenders", func() {
// if we delete blob1, the content should be moved to blob2
err = imgStore.DeleteBlob("dedupe1", "sha256:"+blobDigest1)
So(err, ShouldBeNil)
_, err = storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe1", "blobs", "sha256", blobDigest1))
So(err, ShouldNotBeNil)
fi2, err = storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe2", "blobs", "sha256", blobDigest2))
So(err, ShouldBeNil)
So(fi2.Size(), ShouldBeGreaterThan, 0)
// the second blob should now be equal to the deleted blob.
So(fi2.Size(), ShouldEqual, fi1.Size())
err = imgStore.DeleteBlob("dedupe2", "sha256:"+blobDigest2)
So(err, ShouldBeNil)
_, err = storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe2", "blobs", "sha256", blobDigest2))
So(err, ShouldNotBeNil)
})
Convey("Check backward compatibility - switch dedupe to false", func() {
/* copy cache to the new storage with dedupe false (doing this because we
already have a cache object holding the lock on cache db file) */
input, err := ioutil.ReadFile(path.Join(tdir, s3.CacheDBName+storage.DBExtensionName))
So(err, ShouldBeNil)
tdir = t.TempDir()
err = ioutil.WriteFile(path.Join(tdir, s3.CacheDBName+storage.DBExtensionName), input, 0o600)
So(err, ShouldBeNil)
storeDriver, imgStore, _ := createObjectsStore(testDir, tdir, false)
defer cleanupStorage(storeDriver, testDir)
// manifest3 without dedupe
upload, err = imgStore.NewBlobUpload("dedupe3")
So(err, ShouldBeNil)
So(upload, ShouldNotBeEmpty)
content = []byte("test-data3")
buf = bytes.NewBuffer(content)
buflen = buf.Len()
digest = godigest.FromBytes(content)
blob, err = imgStore.PutBlobChunkStreamed("dedupe3", upload, buf)
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
blobDigest2 := strings.Split(digest.String(), ":")[1]
So(blobDigest2, ShouldNotBeEmpty)
err = imgStore.FinishBlobUpload("dedupe3", upload, buf, digest.String())
So(err, ShouldBeNil)
So(blob, ShouldEqual, buflen)
_, _, err = imgStore.CheckBlob("dedupe3", digest.String())
So(err, ShouldBeNil)
// check that we retrieve the real dedupe2/blob (which is deduped earlier - 0 size) when switching to dedupe false
_, getBlobSize2, err = imgStore.GetBlob("dedupe2", digest.String(), "application/vnd.oci.image.layer.v1.tar+gzip")
So(err, ShouldBeNil)
So(getBlobSize1, ShouldEqual, getBlobSize2)
_, checkBlobSize2, err := imgStore.CheckBlob("dedupe2", digest.String())
So(err, ShouldBeNil)
So(checkBlobSize2, ShouldBeGreaterThan, 0)
So(checkBlobSize2, ShouldEqual, getBlobSize2)
_, getBlobSize3, err := imgStore.GetBlob("dedupe3", digest.String(), "application/vnd.oci.image.layer.v1.tar+gzip")
So(err, ShouldBeNil)
So(getBlobSize1, ShouldEqual, getBlobSize3)
_, checkBlobSize3, err := imgStore.CheckBlob("dedupe3", digest.String())
So(err, ShouldBeNil)
So(checkBlobSize3, ShouldBeGreaterThan, 0)
So(checkBlobSize3, ShouldEqual, getBlobSize3)
cblob, cdigest = test.GetRandomImageConfig()
_, clen, err = imgStore.FullBlobUpload("dedupe3", bytes.NewReader(cblob), cdigest.String())
So(err, ShouldBeNil)
So(clen, ShouldEqual, len(cblob))
hasBlob, _, err = imgStore.CheckBlob("dedupe3", cdigest.String())
So(err, ShouldBeNil)
So(hasBlob, ShouldEqual, true)
manifest = ispec.Manifest{
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest,
Size: int64(buflen),
},
},
}
manifest.SchemaVersion = 2
manifestBuf, err = json.Marshal(manifest)
So(err, ShouldBeNil)
digest = godigest.FromBytes(manifestBuf)
_, err = imgStore.PutImageManifest("dedupe3", "1.0", ispec.MediaTypeImageManifest, manifestBuf)
So(err, ShouldBeNil)
_, _, _, err = imgStore.GetImageManifest("dedupe3", digest.String())
So(err, ShouldBeNil)
fi1, err := storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe1", "blobs", "sha256", blobDigest1))
So(err, ShouldBeNil)
fi2, err := storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe2", "blobs", "sha256", blobDigest1))
So(err, ShouldBeNil)
So(fi2.Size(), ShouldEqual, 0)
fi3, err := storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe3", "blobs", "sha256", blobDigest2))
So(err, ShouldBeNil)
// the new blob with dedupe false should be equal with the origin blob from dedupe1
So(fi1.Size(), ShouldEqual, fi3.Size())
})
})
}
func TestS3DedupeErr(t *testing.T) {
skipIt(t)
uuid, err := guuid.NewV4()
if err != nil {
panic(err)
}
testDir := path.Join("/oci-repo-test", uuid.String())
tdir := t.TempDir()
storeDriver, imgStore, _ := createObjectsStore(testDir, tdir, true)
defer cleanupStorage(storeDriver, testDir)
Convey("Test DedupeBlob", t, func(c C) {
tdir := t.TempDir()
imgStore = createMockStorage(testDir, tdir, true, &StorageDriverMock{})
err = os.Remove(path.Join(tdir, s3.CacheDBName+storage.DBExtensionName))
digest := godigest.NewDigestFromEncoded(godigest.SHA256, "digest")
// trigger unable to insert blob record
err := imgStore.DedupeBlob("", digest, "")
So(err, ShouldNotBeNil)
imgStore = createMockStorage(testDir, tdir, true, &StorageDriverMock{
moveFn: func(ctx context.Context, sourcePath string, destPath string) error {
return errS3
},
statFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
return driver.FileInfoInternal{}, errS3
},
})
// trigger unable to rename blob
err = imgStore.DedupeBlob("", digest, "dst")
So(err, ShouldNotBeNil)
// trigger retry
err = imgStore.DedupeBlob("", digest, "dst")
So(err, ShouldNotBeNil)
})
Convey("Test DedupeBlob - error on second store.Stat()", t, func(c C) {
tdir := t.TempDir()
imgStore = createMockStorage(testDir, tdir, true, &StorageDriverMock{
statFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
if path == "dst2" {
return driver.FileInfoInternal{}, errS3
}
return driver.FileInfoInternal{}, nil
},
})
digest := godigest.NewDigestFromEncoded(godigest.SHA256, "digest")
err := imgStore.DedupeBlob("", digest, "dst")
So(err, ShouldBeNil)
err = imgStore.DedupeBlob("", digest, "dst2")
So(err, ShouldNotBeNil)
})
Convey("Test DedupeBlob - error on store.PutContent()", t, func(c C) {
tdir := t.TempDir()
imgStore = createMockStorage(testDir, tdir, true, &StorageDriverMock{
putContentFn: func(ctx context.Context, path string, content []byte) error {
return errS3
},
statFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
return nil, nil
},
})
digest := godigest.NewDigestFromEncoded(godigest.SHA256, "digest")
err := imgStore.DedupeBlob("", digest, "dst")
So(err, ShouldBeNil)
err = imgStore.DedupeBlob("", digest, "dst2")
So(err, ShouldNotBeNil)
})
Convey("Test DedupeBlob - error on store.Delete()", t, func(c C) {
tdir := t.TempDir()
imgStore = createMockStorage(testDir, tdir, true, &StorageDriverMock{
deleteFn: func(ctx context.Context, path string) error {
return errS3
},
statFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
return nil, nil
},
})
digest := godigest.NewDigestFromEncoded(godigest.SHA256, "digest")
err := imgStore.DedupeBlob("", digest, "dst")
So(err, ShouldBeNil)
err = imgStore.DedupeBlob("", digest, "dst")
So(err, ShouldNotBeNil)
})
Convey("Test copyBlob() - error on initRepo()", t, func(c C) {
tdir := t.TempDir()
imgStore = createMockStorage(testDir, tdir, true, &StorageDriverMock{
putContentFn: func(ctx context.Context, path string, content []byte) error {
return errS3
},
statFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
return driver.FileInfoInternal{}, errS3
},
writerFn: func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
return &FileWriterMock{}, errS3
},
})
digest := godigest.NewDigestFromEncoded(godigest.SHA256,
"7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc")
err := imgStore.DedupeBlob("repo", digest, "dst")
So(err, ShouldBeNil)
_, _, err = imgStore.CheckBlob("repo", digest.String())
So(err, ShouldNotBeNil)
})
Convey("Test copyBlob() - error on store.PutContent()", t, func(c C) {
tdir := t.TempDir()
imgStore = createMockStorage(testDir, tdir, true, &StorageDriverMock{
putContentFn: func(ctx context.Context, path string, content []byte) error {
return errS3
},
statFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
return driver.FileInfoInternal{}, errS3
},
})
digest := godigest.NewDigestFromEncoded(godigest.SHA256,
"7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc")
err := imgStore.DedupeBlob("repo", digest, "dst")
So(err, ShouldBeNil)
_, _, err = imgStore.CheckBlob("repo", digest.String())
So(err, ShouldNotBeNil)
})
Convey("Test copyBlob() - error on store.Stat()", t, func(c C) {
tdir := t.TempDir()
imgStore = createMockStorage(testDir, tdir, true, &StorageDriverMock{
statFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
return driver.FileInfoInternal{}, errS3
},
})
digest := godigest.NewDigestFromEncoded(godigest.SHA256,
"7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc")
err := imgStore.DedupeBlob("repo", digest, "dst")
So(err, ShouldBeNil)
_, _, err = imgStore.CheckBlob("repo", digest.String())
So(err, ShouldNotBeNil)
})
Convey("Test GetBlob() - error on second store.Stat()", t, func(c C) {
tdir := t.TempDir()
imgStore = createMockStorage(testDir, tdir, true, &StorageDriverMock{})
digest := godigest.NewDigestFromEncoded(godigest.SHA256,
"7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc")
err := imgStore.DedupeBlob("/src/dst", digest, "/repo1/dst1")
So(err, ShouldBeNil)
err = imgStore.DedupeBlob("/src/dst", digest, "/repo2/dst2")
So(err, ShouldBeNil)
// copy cache db to the new imagestore
input, err := ioutil.ReadFile(path.Join(tdir, s3.CacheDBName+storage.DBExtensionName))
So(err, ShouldBeNil)
tdir = t.TempDir()
err = ioutil.WriteFile(path.Join(tdir, s3.CacheDBName+storage.DBExtensionName), input, 0o600)
So(err, ShouldBeNil)
imgStore = createMockStorage(testDir, tdir, true, &StorageDriverMock{
statFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
if strings.Contains(path, "repo1/dst1") {
return driver.FileInfoInternal{}, driver.PathNotFoundError{}
}
return driver.FileInfoInternal{}, nil
},
})
_, _, err = imgStore.GetBlob("repo2", digest.String(), "application/vnd.oci.image.layer.v1.tar+gzip")
So(err, ShouldNotBeNil)
})
Convey("Test GetBlob() - error on store.Reader()", t, func(c C) {
tdir := t.TempDir()
imgStore = createMockStorage(testDir, tdir, true, &StorageDriverMock{})
digest := godigest.NewDigestFromEncoded(godigest.SHA256,
"7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc")
err := imgStore.DedupeBlob("/src/dst", digest, "/repo1/dst1")
So(err, ShouldBeNil)
err = imgStore.DedupeBlob("/src/dst", digest, "/repo2/dst2")
So(err, ShouldBeNil)
// copy cache db to the new imagestore
input, err := ioutil.ReadFile(path.Join(tdir, s3.CacheDBName+storage.DBExtensionName))
So(err, ShouldBeNil)
tdir = t.TempDir()
err = ioutil.WriteFile(path.Join(tdir, s3.CacheDBName+storage.DBExtensionName), input, 0o600)
So(err, ShouldBeNil)
imgStore = createMockStorage(testDir, tdir, true, &StorageDriverMock{
statFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
return &FileInfoMock{
sizeFn: func() int64 {
return 0
},
}, nil
},
readerFn: func(ctx context.Context, path string, offset int64) (io.ReadCloser, error) {
if strings.Contains(path, "repo1/dst1") {
return ioutil.NopCloser(strings.NewReader("")), errS3
}
return ioutil.NopCloser(strings.NewReader("")), nil
},
})
_, _, err = imgStore.GetBlob("repo2", digest.String(), "application/vnd.oci.image.layer.v1.tar+gzip")
So(err, ShouldNotBeNil)
})
Convey("Test DeleteBlob() - error on store.Move()", t, func(c C) {
tdir := t.TempDir()
hash := "7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc"
digest := godigest.NewDigestFromEncoded(godigest.SHA256, hash)
blobPath := path.Join(testDir, "repo/blobs/sha256", hash)
imgStore = createMockStorage(testDir, tdir, true, &StorageDriverMock{
moveFn: func(ctx context.Context, sourcePath, destPath string) error {
if destPath == blobPath {
return nil
}
return errS3
},
statFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
if path != blobPath {
return nil, errS3
}
return &FileInfoMock{}, nil
},
})
err := imgStore.DedupeBlob("repo", digest, blobPath)
So(err, ShouldBeNil)
_, _, err = imgStore.CheckBlob("repo2", digest.String())
So(err, ShouldBeNil)
err = imgStore.DeleteBlob("repo", digest.String())
So(err, ShouldNotBeNil)
})
Convey("Test FullBlobUpload", t, func(c C) {
tdir := t.TempDir()
imgStore = createMockStorage(testDir, tdir, true, &StorageDriverMock{
moveFn: func(ctx context.Context, sourcePath, destPath string) error {
return errS3
},
})
d := godigest.FromBytes([]byte(""))
_, _, err := imgStore.FullBlobUpload(testImage, ioutil.NopCloser(strings.NewReader("")), d.String())
So(err, ShouldNotBeNil)
})
Convey("Test FinishBlobUpload", t, func(c C) {
tdir := t.TempDir()
imgStore = createMockStorage(testDir, tdir, true, &StorageDriverMock{
moveFn: func(ctx context.Context, sourcePath, destPath string) error {
return errS3
},
})
d := godigest.FromBytes([]byte(""))
err := imgStore.FinishBlobUpload(testImage, "uuid", ioutil.NopCloser(strings.NewReader("")), d.String())
So(err, ShouldNotBeNil)
})
}
func TestInjectDedupe(t *testing.T) {
tdir := t.TempDir()
uuid, err := guuid.NewV4()
if err != nil {
panic(err)
}
testDir := path.Join("/oci-repo-test", uuid.String())
Convey("Inject errors in DedupeBlob function", t, func() {
imgStore := createMockStorage(testDir, tdir, true, &StorageDriverMock{
statFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
return &FileInfoMock{}, errS3
},
})
err := imgStore.DedupeBlob("blob", "digest", "newblob")
So(err, ShouldBeNil)
injected := test.InjectFailure(0)
err = imgStore.DedupeBlob("blob", "digest", "newblob")
if injected {
So(err, ShouldNotBeNil)
} else {
So(err, ShouldBeNil)
}
injected = test.InjectFailure(1)
err = imgStore.DedupeBlob("blob", "digest", "newblob")
if injected {
So(err, ShouldNotBeNil)
} else {
So(err, ShouldBeNil)
}
})
}
+261 -27
View File
@@ -8,9 +8,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"os"
"path" "path"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
"time" "time"
@@ -27,11 +27,13 @@ import (
"zotregistry.io/zot/pkg/extensions/monitoring" "zotregistry.io/zot/pkg/extensions/monitoring"
zlog "zotregistry.io/zot/pkg/log" zlog "zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage" "zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/test"
) )
const ( const (
RLOCK = "RLock" RLOCK = "RLock"
RWLOCK = "RWLock" RWLOCK = "RWLock"
CacheDBName = "s3_cache"
) )
// ObjectStorage provides the image storage operations. // ObjectStorage provides the image storage operations.
@@ -46,6 +48,8 @@ type ObjectStorage struct {
// see: https://github.com/distribution/distribution/blob/main/registry/storage/driver/s3-aws/s3.go#L545 // see: https://github.com/distribution/distribution/blob/main/registry/storage/driver/s3-aws/s3.go#L545
multiPartUploads sync.Map multiPartUploads sync.Map
metrics monitoring.MetricServer metrics monitoring.MetricServer
cache *storage.Cache
dedupe bool
} }
func (is *ObjectStorage) RootDir() string { func (is *ObjectStorage) RootDir() string {
@@ -62,7 +66,7 @@ func (is *ObjectStorage) DirExists(d string) bool {
// NewObjectStorage returns a new image store backed by cloud storages. // NewObjectStorage returns a new image store backed by cloud storages.
// see https://github.com/docker/docker.github.io/tree/master/registry/storage-drivers // see https://github.com/docker/docker.github.io/tree/master/registry/storage-drivers
func NewImageStore(rootDir string, gc bool, gcDelay time.Duration, dedupe, commit bool, func NewImageStore(rootDir string, cacheDir string, gc bool, gcDelay time.Duration, dedupe, commit bool,
log zlog.Logger, metrics monitoring.MetricServer, log zlog.Logger, metrics monitoring.MetricServer,
store driver.StorageDriver, store driver.StorageDriver,
) storage.ImageStore { ) storage.ImageStore {
@@ -74,6 +78,19 @@ func NewImageStore(rootDir string, gc bool, gcDelay time.Duration, dedupe, commi
log: log.With().Caller().Logger(), log: log.With().Caller().Logger(),
multiPartUploads: sync.Map{}, multiPartUploads: sync.Map{},
metrics: metrics, metrics: metrics,
dedupe: dedupe,
}
cachePath := path.Join(cacheDir, CacheDBName+storage.DBExtensionName)
if dedupe {
imgStore.cache = storage.NewCache(cacheDir, CacheDBName, false, log)
} else {
// if dedupe was used in previous runs use it to serve blobs correctly
if _, err := os.Stat(cachePath); err == nil {
log.Info().Str("cache path", cachePath).Msg("found cache database")
imgStore.cache = storage.NewCache(cacheDir, CacheDBName, false, log)
}
} }
return imgStore return imgStore
@@ -197,15 +214,11 @@ func (is *ObjectStorage) ValidateRepo(name string) (bool, error) {
} }
for _, file := range files { for _, file := range files {
f, err := is.store.Stat(context.Background(), file) _, err := is.store.Stat(context.Background(), file)
if err != nil { if err != nil {
return false, err return false, err
} }
if strings.HasSuffix(file, "blobs") && !f.IsDir() {
return false, nil
}
filename, err := filepath.Rel(dir, file) filename, err := filepath.Rel(dir, file)
if err != nil { if err != nil {
return false, err return false, err
@@ -923,11 +936,20 @@ func (is *ObjectStorage) FinishBlobUpload(repo, uuid string, body io.Reader, dig
is.Lock(&lockLatency) is.Lock(&lockLatency)
defer is.Unlock(&lockLatency) defer is.Unlock(&lockLatency)
if err := is.store.Move(context.Background(), src, dst); err != nil { if is.dedupe && is.cache != nil {
is.log.Error().Err(err).Str("src", src).Str("dstDigest", dstDigest.String()). if err := is.DedupeBlob(src, dstDigest, dst); err != nil {
Str("dst", dst).Msg("unable to finish blob") is.log.Error().Err(err).Str("src", src).Str("dstDigest", dstDigest.String()).
Str("dst", dst).Msg("unable to dedupe blob")
return err return err
}
} else {
if err := is.store.Move(context.Background(), src, dst); err != nil {
is.log.Error().Err(err).Str("src", src).Str("dstDigest", dstDigest.String()).
Str("dst", dst).Msg("unable to finish blob")
return err
}
} }
is.multiPartUploads.Delete(src) is.multiPartUploads.Delete(src)
@@ -994,17 +1016,104 @@ func (is *ObjectStorage) FullBlobUpload(repo string, body io.Reader, digest stri
dst := is.BlobPath(repo, dstDigest) dst := is.BlobPath(repo, dstDigest)
if err := is.store.Move(context.Background(), src, dst); err != nil { if is.dedupe && is.cache != nil {
is.log.Error().Err(err).Str("src", src).Str("dstDigest", dstDigest.String()). if err := is.DedupeBlob(src, dstDigest, dst); err != nil {
Str("dst", dst).Msg("unable to finish blob") is.log.Error().Err(err).Str("src", src).Str("dstDigest", dstDigest.String()).
Str("dst", dst).Msg("unable to dedupe blob")
return "", -1, err return "", -1, err
}
} else {
if err := is.store.Move(context.Background(), src, dst); err != nil {
is.log.Error().Err(err).Str("src", src).Str("dstDigest", dstDigest.String()).
Str("dst", dst).Msg("unable to finish blob")
return "", -1, err
}
} }
return uuid, int64(nbytes), nil return uuid, int64(nbytes), nil
} }
func (is *ObjectStorage) DedupeBlob(src string, dstDigest godigest.Digest, dst string) error { func (is *ObjectStorage) DedupeBlob(src string, dstDigest godigest.Digest, dst string) error {
retry:
is.log.Debug().Str("src", src).Str("dstDigest", dstDigest.String()).Str("dst", dst).Msg("dedupe: enter")
dstRecord, err := is.cache.GetBlob(dstDigest.String())
if err := test.Error(err); err != nil && !errors.Is(err, zerr.ErrCacheMiss) {
is.log.Error().Err(err).Str("blobPath", dst).Msg("dedupe: unable to lookup blob record")
return err
}
if dstRecord == "" {
// cache record doesn't exist, so first disk and cache entry for this digest
if err := is.cache.PutBlob(dstDigest.String(), dst); err != nil {
is.log.Error().Err(err).Str("blobPath", dst).Msg("dedupe: unable to insert blob record")
return err
}
// move the blob from uploads to final dest
if err := is.store.Move(context.Background(), src, dst); err != nil {
is.log.Error().Err(err).Str("src", src).Str("dst", dst).Msg("dedupe: unable to rename blob")
return err
}
is.log.Debug().Str("src", src).Str("dst", dst).Msg("dedupe: rename")
} else {
// cache record exists, but due to GC and upgrades from older versions,
// disk content and cache records may go out of sync
_, err := is.store.Stat(context.Background(), dstRecord)
if err != nil {
is.log.Error().Err(err).Str("blobPath", dstRecord).Msg("dedupe: unable to stat")
// the actual blob on disk may have been removed by GC, so sync the cache
err := is.cache.DeleteBlob(dstDigest.String(), dstRecord)
if err = test.Error(err); err != nil {
// nolint:lll
is.log.Error().Err(err).Str("dstDigest", dstDigest.String()).Str("dst", dst).Msg("dedupe: unable to delete blob record")
return err
}
goto retry
}
fileInfo, err := is.store.Stat(context.Background(), dst)
if err != nil && !errors.As(err, &driver.PathNotFoundError{}) {
is.log.Error().Err(err).Str("blobPath", dstRecord).Msg("dedupe: unable to stat")
return err
}
if dstRecord == dst {
is.log.Warn().Msg("FOUND equal dsts")
}
// prevent overwrite original blob
if fileInfo == nil && dstRecord != dst {
// put empty file so that we are compliant with oci layout, this will act as a deduped blob
err = is.store.PutContent(context.Background(), dst, []byte{})
if err != nil {
is.log.Error().Err(err).Str("blobPath", dstRecord).Msg("dedupe: unable to write empty file")
return err
}
} else {
is.log.Warn().Msg("prevent overwrite")
}
// remove temp blobupload
if err := is.store.Delete(context.Background(), src); err != nil {
is.log.Error().Err(err).Str("src", src).Msg("dedupe: unable to remove blob")
return err
}
is.log.Debug().Str("src", src).Msg("dedupe: remove")
}
return nil return nil
} }
@@ -1041,24 +1150,81 @@ func (is *ObjectStorage) CheckBlob(repo, digest string) (bool, int64, error) {
blobPath := is.BlobPath(repo, dgst) blobPath := is.BlobPath(repo, dgst)
is.RLock(&lockLatency) if is.dedupe && is.cache != nil {
defer is.RUnlock(&lockLatency) is.Lock(&lockLatency)
defer is.Unlock(&lockLatency)
} else {
is.RLock(&lockLatency)
defer is.RUnlock(&lockLatency)
}
binfo, err := is.store.Stat(context.Background(), blobPath) binfo, err := is.store.Stat(context.Background(), blobPath)
if err != nil { if err == nil && binfo.Size() > 0 {
var perr driver.PathNotFoundError is.log.Debug().Str("blob path", blobPath).Msg("blob path found")
if errors.As(err, &perr) {
return false, -1, zerr.ErrBlobNotFound
}
is.log.Error().Err(err).Str("blob", blobPath).Msg("failed to stat blob") return true, binfo.Size(), nil
}
// otherwise is a 'deduped' blob (empty file)
// Check blobs in cache
dstRecord, err := is.checkCacheBlob(digest)
if err != nil {
is.log.Error().Err(err).Str("digest", digest).Msg("cache: not found")
return false, -1, zerr.ErrBlobNotFound
}
// If found copy to location
blobSize, err := is.copyBlob(repo, blobPath, dstRecord)
if err != nil {
return false, -1, zerr.ErrBlobNotFound
}
// put deduped blob in cache
if err := is.cache.PutBlob(digest, blobPath); err != nil {
is.log.Error().Err(err).Str("blobPath", blobPath).Msg("dedupe: unable to insert blob record")
return false, -1, err return false, -1, err
} }
is.log.Debug().Str("blob path", blobPath).Msg("blob path found") return true, blobSize, nil
}
return true, binfo.Size(), nil func (is *ObjectStorage) checkCacheBlob(digest string) (string, error) {
if is.cache == nil {
return "", zerr.ErrBlobNotFound
}
dstRecord, err := is.cache.GetBlob(digest)
if err != nil {
return "", err
}
is.log.Debug().Str("digest", digest).Str("dstRecord", dstRecord).Msg("cache: found dedupe record")
return dstRecord, nil
}
func (is *ObjectStorage) copyBlob(repo string, blobPath string, dstRecord string) (int64, error) {
if err := is.initRepo(repo); err != nil {
is.log.Error().Err(err).Str("repo", repo).Msg("unable to initialize an empty repo")
return -1, err
}
if err := is.store.PutContent(context.Background(), blobPath, []byte{}); err != nil {
is.log.Error().Err(err).Str("blobPath", blobPath).Str("link", dstRecord).Msg("dedupe: unable to link")
return -1, zerr.ErrBlobNotFound
}
// return original blob with content instead of the deduped one (blobPath)
binfo, err := is.store.Stat(context.Background(), dstRecord)
if err == nil {
return binfo.Size(), nil
}
return -1, zerr.ErrBlobNotFound
} }
// GetBlob returns a stream to read the blob. // GetBlob returns a stream to read the blob.
@@ -1092,6 +1258,36 @@ func (is *ObjectStorage) GetBlob(repo, digest, mediaType string) (io.Reader, int
return nil, -1, err return nil, -1, err
} }
// is a 'deduped' blob
if binfo.Size() == 0 && is.cache != nil {
// Check blobs in cache
dstRecord, err := is.checkCacheBlob(digest)
if err == nil {
binfo, err := is.store.Stat(context.Background(), dstRecord)
if err != nil {
is.log.Error().Err(err).Str("blob", dstRecord).Msg("failed to stat blob")
// the actual blob on disk may have been removed by GC, so sync the cache
if err := is.cache.DeleteBlob(digest, dstRecord); err != nil {
is.log.Error().Err(err).Str("dstDigest", digest).Str("dst", dstRecord).Msg("dedupe: unable to delete blob record")
return nil, -1, err
}
return nil, -1, zerr.ErrBlobNotFound
}
blobReader, err := is.store.Reader(context.Background(), dstRecord, 0)
if err != nil {
is.log.Error().Err(err).Str("blob", dstRecord).Msg("failed to open blob")
return nil, -1, err
}
return blobReader, binfo.Size(), nil
}
}
return blobReader, binfo.Size(), nil return blobReader, binfo.Size(), nil
} }
@@ -1158,6 +1354,44 @@ func (is *ObjectStorage) DeleteBlob(repo, digest string) error {
return zerr.ErrBlobNotFound return zerr.ErrBlobNotFound
} }
if is.cache != nil {
dstRecord, err := is.cache.GetBlob(digest)
if err != nil && !errors.Is(err, zerr.ErrCacheMiss) {
is.log.Error().Err(err).Str("blobPath", dstRecord).Msg("dedupe: unable to lookup blob record")
return err
}
// remove cache entry and move blob contents to the next candidate if there is any
if err := is.cache.DeleteBlob(digest, blobPath); err != nil {
is.log.Error().Err(err).Str("digest", digest).Str("blobPath", blobPath).Msg("unable to remove blob path from cache")
return err
}
// if the deleted blob is one with content
if dstRecord == blobPath {
// get next candidate
dstRecord, err := is.cache.GetBlob(digest)
if err != nil && !errors.Is(err, zerr.ErrCacheMiss) {
is.log.Error().Err(err).Str("blobPath", dstRecord).Msg("dedupe: unable to lookup blob record")
return err
}
// if we have a new candidate move the blob content to it
if dstRecord != "" {
if err := is.store.Move(context.Background(), blobPath, dstRecord); err != nil {
is.log.Error().Err(err).Str("blobPath", blobPath).Msg("unable to remove blob path")
return err
}
return nil
}
}
}
if err := is.store.Delete(context.Background(), blobPath); err != nil { if err := is.store.Delete(context.Background(), blobPath); err != nil {
is.log.Error().Err(err).Str("blobPath", blobPath).Msg("unable to remove blob path") is.log.Error().Err(err).Str("blobPath", blobPath).Msg("unable to remove blob path")
+1 -1
View File
@@ -129,7 +129,7 @@ func NewImageStore(rootDir string, gc bool, gcDelay time.Duration, dedupe, commi
} }
if dedupe { if dedupe {
imgStore.cache = NewCache(rootDir, "cache", log) imgStore.cache = NewCache(rootDir, "cache", true, log)
} }
if gc { if gc {
+9 -6
View File
@@ -42,7 +42,7 @@ func skipIt(t *testing.T) {
} }
} }
func createObjectsStore(rootDir string) (driver.StorageDriver, storage.ImageStore, error) { func createObjectsStore(rootDir string, cacheDir string) (driver.StorageDriver, storage.ImageStore, error) {
bucket := "zot-storage-test" bucket := "zot-storage-test"
endpoint := os.Getenv("S3MOCK_ENDPOINT") endpoint := os.Getenv("S3MOCK_ENDPOINT")
storageDriverParams := map[string]interface{}{ storageDriverParams := map[string]interface{}{
@@ -51,6 +51,8 @@ func createObjectsStore(rootDir string) (driver.StorageDriver, storage.ImageStor
"region": "us-east-2", "region": "us-east-2",
"bucket": bucket, "bucket": bucket,
"regionendpoint": endpoint, "regionendpoint": endpoint,
"accesskey": "minioadmin",
"secretkey": "minioadmin",
"secure": false, "secure": false,
"skipverify": false, "skipverify": false,
} }
@@ -71,7 +73,7 @@ func createObjectsStore(rootDir string) (driver.StorageDriver, storage.ImageStor
log := log.Logger{Logger: zerolog.New(os.Stdout)} log := log.Logger{Logger: zerolog.New(os.Stdout)}
metrics := monitoring.NewMetricsServer(false, log) metrics := monitoring.NewMetricsServer(false, log)
il := s3.NewImageStore(rootDir, false, storage.DefaultGCDelay, false, false, log, metrics, store) il := s3.NewImageStore(rootDir, cacheDir, false, storage.DefaultGCDelay, true, false, log, metrics, store)
return store, il, err return store, il, err
} }
@@ -105,9 +107,10 @@ func TestStorageAPIs(t *testing.T) {
} }
testDir := path.Join("/oci-repo-test", uuid.String()) testDir := path.Join("/oci-repo-test", uuid.String())
tdir := t.TempDir()
var store driver.StorageDriver var store driver.StorageDriver
store, imgStore, _ = createObjectsStore(testDir) store, imgStore, _ = createObjectsStore(testDir, tdir)
defer cleanupStorage(store, testDir) defer cleanupStorage(store, testDir)
} else { } else {
dir := t.TempDir() dir := t.TempDir()
@@ -676,15 +679,15 @@ func TestStorageHandler(t *testing.T) {
var thirdStorageDriver driver.StorageDriver var thirdStorageDriver driver.StorageDriver
firstRootDir = "/util_test1" firstRootDir = "/util_test1"
firstStorageDriver, firstStore, _ = createObjectsStore(firstRootDir) firstStorageDriver, firstStore, _ = createObjectsStore(firstRootDir, t.TempDir())
defer cleanupStorage(firstStorageDriver, firstRootDir) defer cleanupStorage(firstStorageDriver, firstRootDir)
secondRootDir = "/util_test2" secondRootDir = "/util_test2"
secondStorageDriver, secondStore, _ = createObjectsStore(secondRootDir) secondStorageDriver, secondStore, _ = createObjectsStore(secondRootDir, t.TempDir())
defer cleanupStorage(secondStorageDriver, secondRootDir) defer cleanupStorage(secondStorageDriver, secondRootDir)
thirdRootDir = "/util_test3" thirdRootDir = "/util_test3"
thirdStorageDriver, thirdStore, _ = createObjectsStore(thirdRootDir) thirdStorageDriver, thirdStore, _ = createObjectsStore(thirdRootDir, t.TempDir())
defer cleanupStorage(thirdStorageDriver, thirdRootDir) defer cleanupStorage(thirdStorageDriver, thirdRootDir)
} else { } else {
// Create temporary directory // Create temporary directory
+3 -2
View File
@@ -1,11 +1,12 @@
{ {
"distSpecVersion": "1.0.0", "distSpecVersion": "1.0.1",
"storage": { "storage": {
"rootDirectory": "/zot", "rootDirectory": "/tmp/zot",
"gc": false, "gc": false,
"dedupe": false, "dedupe": false,
"storageDriver": { "storageDriver": {
"name": "s3", "name": "s3",
"rootdirectory": "/zot",
"region": "us-east-2", "region": "us-east-2",
"bucket": "zot-storage", "bucket": "zot-storage",
"regionendpoint": "http://localhost:9000", "regionendpoint": "http://localhost:9000",