feat(storage): add a common blobstore to store all blobs (#3906)

Currently, zot uses one of the existing repos as the master copy for a blob to
achieve dedupe, which complicates dedupe tracking logic. Furthermore, we
have a global storage lock which is becoming a bottleneck. In order to
move to a per-repo lock, we first need to simplify this logic.

Now use a single hidden global repo (_blobstore/) as a blob store instead.

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>
This commit is contained in:
Ramkumar Chinchani
2026-06-09 12:13:44 -07:00
committed by GitHub
parent 273b15364b
commit 936a60d4f7
21 changed files with 1111 additions and 437 deletions
+2
View File
@@ -91,6 +91,8 @@ var (
ErrInvalidRoute = errors.New("invalid route prefix")
ErrImgStoreNotFound = errors.New("image store not found corresponding to given route")
ErrLocalImgStoreNotFound = errors.New("local image store not found corresponding to given route")
ErrDefaultImgStoreCreate = errors.New("failed to create image store for default config")
ErrSubpathImgStoreCreate = errors.New("failed to create image store for subpath")
ErrEmptyValue = errors.New("empty cache value")
ErrEmptyRepoList = errors.New("no repository found")
ErrCVESearchDisabled = errors.New("cve search is disabled")
+31 -1
View File
@@ -655,14 +655,26 @@ func TestObjectStorageController(t *testing.T) {
endpoint := os.Getenv("S3MOCK_ENDPOINT")
tmp := t.TempDir()
// create s3 bucket
resp, err := resty.R().Put("http://" + endpoint + "/" + bucket)
if err != nil {
panic(err)
}
if resp.StatusCode() != http.StatusOK {
panic(fmt.Sprintf("failed to create bucket: %d %s", resp.StatusCode(), resp.String()))
}
storageDriverParams := map[string]any{
"rootdirectory": tmp,
"name": storageConstants.S3StorageDriverName,
"region": "us-east-2",
"bucket": bucket,
"regionendpoint": endpoint,
"accesskey": "minioadmin",
"secretkey": "minioadmin",
"secure": false,
"skipverify": false,
"forcepathstyle": true,
}
conf.Storage.StorageDriver = storageDriverParams
@@ -688,8 +700,11 @@ func TestObjectStorageController(t *testing.T) {
"region": "us-east-2",
"bucket": bucket,
"regionendpoint": endpoint,
"accesskey": "minioadmin",
"secretkey": "minioadmin",
"secure": false,
"skipverify": false,
"forcepathstyle": true,
}
conf.Storage.RemoteCache = true
conf.Storage.StorageDriver = storageDriverParams
@@ -736,10 +751,13 @@ func TestObjectStorageController(t *testing.T) {
}
// create s3 bucket
_, err = resty.R().Put("http://" + os.Getenv("S3MOCK_ENDPOINT") + "/" + bucket)
resp, err := resty.R().Put("http://" + os.Getenv("S3MOCK_ENDPOINT") + "/" + bucket)
if err != nil {
panic(err)
}
if resp.StatusCode() != http.StatusOK {
panic(fmt.Sprintf("failed to create bucket: %d %s", resp.StatusCode(), resp.String()))
}
ctlr := makeController(conf, "/")
So(ctlr, ShouldNotBeNil)
@@ -764,14 +782,26 @@ func TestObjectStorageControllerSubPaths(t *testing.T) {
endpoint := os.Getenv("S3MOCK_ENDPOINT")
tmp := t.TempDir()
// create s3 bucket
resp, err := resty.R().Put("http://" + endpoint + "/" + bucket)
if err != nil {
panic(err)
}
if resp.StatusCode() != http.StatusOK {
panic(fmt.Sprintf("failed to create bucket: %d %s", resp.StatusCode(), resp.String()))
}
storageDriverParams := map[string]any{
"rootdirectory": tmp,
"name": storageConstants.S3StorageDriverName,
"region": "us-east-2",
"bucket": bucket,
"regionendpoint": endpoint,
"accesskey": "minioadmin",
"secretkey": "minioadmin",
"secure": false,
"skipverify": false,
"forcepathstyle": true,
}
conf.Storage.StorageDriver = storageDriverParams
ctlr := makeController(conf, tmp)
+1 -1
View File
@@ -476,7 +476,7 @@ func TestRetentionCheckWithRetentionEnabledAndRedisDriver(t *testing.T) {
defer ctrlManager.StopServer()
os.Args = []string{"cli_test", "verify-feature", "retention", "-l", logFile, "-t", "2s", configFile}
os.Args = []string{"cli_test", "verify-feature", "retention", "-l", logFile, "-t", "10s", configFile}
err = cli.NewServerRootCmd().Execute()
So(err, ShouldBeNil)
+6
View File
@@ -90,6 +90,12 @@ func (registry *DestinationRegistry) GetImageReference(repo, reference string) (
func (registry *DestinationRegistry) CommitAll(repo string, imageReference ref.Ref) error {
tempImageStore := getImageStoreFromImageReference(repo, imageReference, registry.log)
if tempImageStore == nil {
registry.log.Error().Str("repo", repo).Msg("failed to get temp image store for sync commit")
return zerr.ErrLocalImgStoreNotFound
}
defer os.RemoveAll(tempImageStore.RootDir())
repoDir := path.Join(tempImageStore.RootDir(), repo)
+10 -1
View File
@@ -1720,7 +1720,16 @@ func TestDockerImagesAreSkipped(t *testing.T) {
// trigger config blob upstream error
// remove synced image
err = os.RemoveAll(path.Join(destDir, indexRepoName))
dstRepoPath := path.Join(destDir, indexRepoName)
for range 5 {
err = os.RemoveAll(dstRepoPath)
if err == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
So(err, ShouldBeNil)
configBlobPath := path.Join(srcDir, indexRepoName, "blobs/sha256", configBlobDigest.Encoded())
+65 -44
View File
@@ -114,6 +114,10 @@ func (d *BoltDBDriver) PutBlob(digest godigest.Digest, path string) error {
}
}
if len(path) == 0 {
return zerr.ErrEmptyValue
}
if err := d.db.Update(func(tx *bbolt.Tx) error {
root := tx.Bucket([]byte(constants.BlobsCache))
if root == nil {
@@ -132,21 +136,6 @@ func (d *BoltDBDriver) PutBlob(digest godigest.Digest, path string) error {
return err
}
// create nested deduped bucket where we store all the deduped blobs + original blob
deduped, err := bucket.CreateBucketIfNotExists([]byte(constants.DuplicatesBucket))
if err != nil {
// this is a serious failure
d.log.Error().Err(err).Str("bucket", constants.DuplicatesBucket).Msg("failed to create a bucket")
return err
}
if err := deduped.Put([]byte(path), nil); err != nil {
d.log.Error().Err(err).Str("bucket", constants.DuplicatesBucket).Str("value", path).Msg("failed to put record")
return err
}
// create origin bucket and insert only the original blob
origin := bucket.Bucket([]byte(constants.OriginalBucket))
if origin == nil {
@@ -164,6 +153,28 @@ func (d *BoltDBDriver) PutBlob(digest godigest.Digest, path string) error {
return err
}
return nil
}
// original bucket exists; if path is the same as the original, this is idempotent
if origin.Get([]byte(path)) != nil {
return nil
}
// otherwise, this is a duplicate blob - add to the duplicates bucket
deduped, err := bucket.CreateBucketIfNotExists([]byte(constants.DuplicatesBucket))
if err != nil {
// this is a serious failure
d.log.Error().Err(err).Str("bucket", constants.DuplicatesBucket).Msg("failed to create a bucket")
return err
}
if err := deduped.Put([]byte(path), nil); err != nil {
d.log.Error().Err(err).Str("bucket", constants.DuplicatesBucket).Str("value", path).Msg("failed to put record")
return err
}
return nil
@@ -275,18 +286,20 @@ func (d *BoltDBDriver) HasBlob(digest godigest.Digest, blob string) bool {
return zerr.ErrCacheMiss
}
deduped := bucket.Bucket([]byte(constants.DuplicatesBucket))
if deduped == nil {
return zerr.ErrCacheMiss
// check original bucket first
if origin.Get([]byte(blob)) != nil {
return nil
}
if origin.Get([]byte(blob)) == nil {
if deduped.Get([]byte(blob)) == nil {
return zerr.ErrCacheMiss
// check duplicates bucket
deduped := bucket.Bucket([]byte(constants.DuplicatesBucket))
if deduped != nil {
if deduped.Get([]byte(blob)) != nil {
return nil
}
}
return nil
return zerr.ErrCacheMiss
}); err != nil {
return false
}
@@ -330,22 +343,33 @@ func (d *BoltDBDriver) DeleteBlob(digest godigest.Digest, path string) error {
return zerr.ErrCacheMiss
}
// check duplicates bucket first
deduped := bucket.Bucket([]byte(constants.DuplicatesBucket))
if deduped == nil {
return zerr.ErrCacheMiss
}
if err := deduped.Delete([]byte(path)); err != nil {
d.log.Error().Err(err).Str("digest", digest.String()).Str("bucket", constants.DuplicatesBucket).
Str("path", path).Msg("failed to delete")
return err
if deduped != nil {
if deduped.Get([]byte(path)) != nil {
if err := deduped.Delete([]byte(path)); err != nil {
d.log.Error().Err(err).Str("digest", digest.String()).Str("bucket", constants.DuplicatesBucket).
Str("path", path).Msg("failed to delete")
return err
}
return nil
}
}
// check original bucket
origin := bucket.Bucket([]byte(constants.OriginalBucket))
deleted := false
if origin != nil {
originBlob := d.getOne(origin)
if originBlob != nil {
if origin.Get([]byte(path)) != nil {
// if duplicates still exist, keep the original (global blobstore file stays)
if deduped != nil && d.getOne(deduped) != nil {
return nil
}
// no more duplicates, safe to remove the original
if err := origin.Delete([]byte(path)); err != nil {
d.log.Error().Err(err).Str("digest", digest.String()).Str("bucket", constants.OriginalBucket).
Str("path", path).Msg("failed to delete")
@@ -353,19 +377,14 @@ func (d *BoltDBDriver) DeleteBlob(digest godigest.Digest, path string) error {
return err
}
// move next candidate to origin bucket, next GetKey will return this one and storage will move the content here
dedupedBlob := d.getOne(deduped)
if dedupedBlob != nil {
if err := origin.Put(dedupedBlob, nil); err != nil {
d.log.Error().Err(err).Str("digest", digest.String()).Str("bucket", constants.OriginalBucket).Str("path", path).
Msg("failed to put")
return err
}
}
deleted = true
}
}
if !deleted {
return zerr.ErrCacheMiss
}
// if no key in origin bucket then digest bucket is empty, remove it
k := d.getOne(origin)
if k == nil {
@@ -376,9 +395,11 @@ func (d *BoltDBDriver) DeleteBlob(digest godigest.Digest, path string) error {
return err
}
return nil
}
return nil
return zerr.ErrCacheMiss
}); err != nil {
return err
}
+13 -4
View File
@@ -55,7 +55,7 @@ func TestBoltDBCache(t *testing.T) {
So(err, ShouldEqual, errors.ErrCacheMiss)
err = cacheDriver.DeleteBlob("key", "bogusValue")
So(err, ShouldBeNil)
So(err, ShouldEqual, errors.ErrCacheMiss)
// try to insert empty path
err = cacheDriver.PutBlob("key", "")
@@ -85,16 +85,23 @@ func TestBoltDBCache(t *testing.T) {
err = cacheDriver.PutBlob("key1", "duplicateBlobPath")
So(err, ShouldBeNil)
// deleting original when duplicates exist should keep the original (no promotion)
err = cacheDriver.DeleteBlob("key1", "originalBlobPath")
So(err, ShouldBeNil)
// original should still be "originalBlobPath" since duplicates exist
val, err = cacheDriver.GetBlob("key1")
So(val, ShouldEqual, "duplicateBlobPath")
So(val, ShouldEqual, "originalBlobPath")
So(err, ShouldBeNil)
// now delete the duplicate
err = cacheDriver.DeleteBlob("key1", "duplicateBlobPath")
So(err, ShouldBeNil)
// now delete the original (no more duplicates, should clean up)
err = cacheDriver.DeleteBlob("key1", "originalBlobPath")
So(err, ShouldBeNil)
// should be empty
val, err = cacheDriver.GetBlob("key1")
So(err, ShouldNotBeNil)
@@ -147,15 +154,17 @@ func TestBoltDBCache(t *testing.T) {
blobs, err := cacheDriver.GetAllBlobs("digest")
So(err, ShouldBeNil)
// "first" is the original, "second" and "third" are duplicates
So(blobs, ShouldResemble, []string{"first", "second", "third"})
// deleting "first" (original) should keep it since duplicates exist
err = cacheDriver.DeleteBlob("digest", "first")
So(err, ShouldBeNil)
blobs, err = cacheDriver.GetAllBlobs("digest")
So(err, ShouldBeNil)
So(blobs, ShouldResemble, []string{"second", "third"})
So(blobs, ShouldResemble, []string{"first", "second", "third"})
err = cacheDriver.DeleteBlob("digest", "third")
So(err, ShouldBeNil)
@@ -163,6 +172,6 @@ func TestBoltDBCache(t *testing.T) {
blobs, err = cacheDriver.GetAllBlobs("digest")
So(err, ShouldBeNil)
So(blobs, ShouldResemble, []string{"second"})
So(blobs, ShouldResemble, []string{"first", "second"})
})
}
+71 -17
View File
@@ -187,13 +187,22 @@ func (d *DynamoDBDriver) PutBlob(digest godigest.Digest, path string) error {
return zerr.ErrEmptyValue
}
if originBlob, _ := d.GetBlob(digest); originBlob == "" {
// first entry, so add original blob
originBlob, _ := d.GetBlob(digest)
if originBlob == "" {
// first entry, so add original blob only
if err := d.putOriginBlob(digest, path); err != nil {
return err
}
return nil
}
// if same as original, this is idempotent
if originBlob == path {
return nil
}
// add as duplicate
expression := "ADD DuplicateBlobPath :i"
attrPath := types.AttributeValueMemberSS{Value: []string{path}}
@@ -245,27 +254,69 @@ func (d *DynamoDBDriver) HasBlob(digest godigest.Digest, path string) bool {
func (d *DynamoDBDriver) DeleteBlob(digest godigest.Digest, path string) error {
marshaledKey, _ := attributevalue.MarshalMap(map[string]any{"Digest": digest.String()})
expression := "DELETE DuplicateBlobPath :i"
attrPath := types.AttributeValueMemberSS{Value: []string{path}}
// check if path is a duplicate first
duplicateBlob, _ := d.GetDuplicateBlob(digest)
if duplicateBlob != "" {
// check if path is in the duplicates set
resp, err := d.client.GetItem(context.TODO(), &dynamodb.GetItemInput{
TableName: aws.String(d.tableName),
Key: map[string]types.AttributeValue{
"Digest": &types.AttributeValueMemberS{Value: digest.String()},
},
})
if err != nil {
return err
}
if err := d.updateItem(digest, expression, map[string]types.AttributeValue{":i": &attrPath}); err != nil {
d.log.Error().Err(err).Str("digest", digest.String()).Str("path", path).Msg("failed to delete")
out := Blob{}
if resp.Item != nil {
_ = attributevalue.UnmarshalMap(resp.Item, &out)
return err
}
if slices.Contains(out.DuplicateBlobPath, path) {
expression := "DELETE DuplicateBlobPath :i"
attrPath := types.AttributeValueMemberSS{Value: []string{path}}
originBlob, _ := d.GetBlob(digest)
// if original blob is the one deleted
if originBlob == path {
// move duplicate blob to original, storage will move content here
originBlob, _ = d.GetDuplicateBlob(digest)
if originBlob != "" {
if err := d.putOriginBlob(digest, originBlob); err != nil {
return err
if err := d.updateItem(digest, expression, map[string]types.AttributeValue{":i": &attrPath}); err != nil {
d.log.Error().Err(err).Str("digest", digest.String()).Str("path", path).Msg("failed to delete")
return err
}
return nil
}
}
}
originBlob, err := d.GetBlob(digest)
if err != nil {
// ErrCacheMiss means the digest doesn't exist at all — path not found
return err
}
// if original blob is the one being deleted
if originBlob == path {
// check if duplicates still exist
remainingDuplicate, _ := d.GetDuplicateBlob(digest)
if remainingDuplicate != "" {
// duplicates still exist, keep the original (global blobstore file stays)
return nil
}
// no more duplicates, remove the original
_, err = d.client.DeleteItem(context.TODO(), &dynamodb.DeleteItemInput{
Key: marshaledKey,
TableName: &d.tableName,
})
if err != nil {
d.log.Error().Err(err).Str("digest", digest.String()).Str("path", path).Msg("failed to delete")
return err
}
return nil
}
// originBlob is empty but record exists (orphaned entry) — clean up
if originBlob == "" {
d.log.Debug().Str("digest", digest.String()).Str("path", path).Msg("deleting empty bucket")
@@ -273,9 +324,12 @@ func (d *DynamoDBDriver) DeleteBlob(digest godigest.Digest, path string) error {
Key: marshaledKey,
TableName: &d.tableName,
})
return zerr.ErrCacheMiss
}
return nil
// path not found in duplicates or original
return zerr.ErrCacheMiss
}
func (d *DynamoDBDriver) GetDuplicateBlob(digest godigest.Digest) (string, error) {
+7 -7
View File
@@ -82,7 +82,7 @@ func TestDynamoDB(t *testing.T) {
So(exists, ShouldBeTrue)
exists = cacheDriver.HasBlob(keyDigest, path.Join(dir, "value1"))
So(exists, ShouldBeFalse)
So(exists, ShouldBeTrue)
err = cacheDriver.DeleteBlob(keyDigest, path.Join(dir, "value2"))
So(err, ShouldBeNil)
@@ -111,16 +111,16 @@ func TestDynamoDB(t *testing.T) {
So(err, ShouldBeNil)
val, err = cacheDriver.GetBlob("key1")
So(val, ShouldEqual, "duplicateBlobPath")
So(val, ShouldEqual, "originalBlobPath")
So(err, ShouldBeNil)
err = cacheDriver.DeleteBlob("key1", "duplicateBlobPath")
So(err, ShouldBeNil)
// should be empty
// original remains while duplicates are removed
val, err = cacheDriver.GetBlob("key1")
So(err, ShouldNotBeNil)
So(val, ShouldBeEmpty)
So(err, ShouldBeNil)
So(val, ShouldEqual, "originalBlobPath")
// try to add three same values
err = cacheDriver.PutBlob("key2", "duplicate")
@@ -176,7 +176,7 @@ func TestDynamoDB(t *testing.T) {
blobs, err = cacheDriver.GetAllBlobs("digest")
So(err, ShouldBeNil)
So(blobs, ShouldResemble, []string{"second", "third"})
So(blobs, ShouldResemble, []string{"first", "second", "third"})
err = cacheDriver.DeleteBlob("digest", "third")
So(err, ShouldBeNil)
@@ -184,7 +184,7 @@ func TestDynamoDB(t *testing.T) {
blobs, err = cacheDriver.GetAllBlobs("digest")
So(err, ShouldBeNil)
So(blobs, ShouldResemble, []string{"second"})
So(blobs, ShouldResemble, []string{"first", "second"})
})
}
+60 -41
View File
@@ -145,7 +145,18 @@ func (d *RedisDriver) PutBlob(digest godigest.Digest, path string) error {
return err
}
// first entry is only stored as the original, not as a duplicate
return nil
}
// check if this is the same as the original (idempotent)
currentPath, err := d.db.HGet(ctx, d.join(constants.BlobsCache, constants.OriginalBucket),
digest.String()).Result()
if err == nil && currentPath == path {
return nil
}
// add path to the set of paths which the digest represents
if err := txrp.SAdd(ctx, d.join(constants.BlobsCache, constants.DuplicatesBucket,
digest.String()), path).Err(); err != nil {
@@ -234,7 +245,23 @@ func (d *RedisDriver) HasBlob(digest godigest.Digest, path string) bool {
}
ctx := context.TODO()
// see if we are in the set
// check if path is the original
currentPath, err := d.db.HGet(ctx, d.join(constants.BlobsCache, constants.OriginalBucket), digest.String()).Result()
if err != nil {
if !goerrors.Is(err, redis.Nil) {
d.log.Error().Err(err).Str("hget", d.join(constants.BlobsCache, constants.OriginalBucket)).
Str("digest", digest.String()).Msg("unable to get record")
}
return false
}
if currentPath == path {
return true
}
// check if path is in the duplicates set
exists, err := d.db.SIsMember(ctx, d.join(constants.BlobsCache, constants.DuplicatesBucket,
digest.String()), path).Result()
if err != nil {
@@ -244,25 +271,7 @@ func (d *RedisDriver) HasBlob(digest godigest.Digest, path string) bool {
return false
}
if !exists {
return false
}
// see if the path entry exists. is this actually needed? i guess it doesn't really hurt (it is fast)
exists, err = d.db.HExists(ctx, d.join(constants.BlobsCache, constants.OriginalBucket), digest.String()).Result()
d.log.Error().Err(err).Str("hexists", d.join(constants.BlobsCache, constants.OriginalBucket)).
Str("digest", digest.String()).Msg("unable to get record")
if err != nil {
return false
}
if !exists {
return false
}
return true
return exists
}
func (d *RedisDriver) DeleteBlob(digest godigest.Digest, path string) error {
@@ -291,47 +300,57 @@ func (d *RedisDriver) DeleteBlob(digest godigest.Digest, path string) error {
}
}()
// check duplicates first
pathSet := d.join(constants.BlobsCache, constants.DuplicatesBucket, digest.String())
// delete path from the set of paths which the digest represents
_, err = d.db.SRem(ctx, pathSet, path).Result()
exists, err := d.db.SIsMember(ctx, pathSet, path).Result()
if err != nil {
d.log.Error().Err(err).Str("srem", pathSet).Str("value", path).Msg("failed to delete record")
d.log.Error().Err(err).Str("sismember", pathSet).Str("value", path).Msg("failed to lookup record")
return err
}
if exists {
// delete path from the set of paths which the digest represents
_, err = d.db.SRem(ctx, pathSet, path).Result()
if err != nil {
d.log.Error().Err(err).Str("srem", pathSet).Str("value", path).Msg("failed to delete record")
return err
}
return nil
}
// check if path is the original
currentPath, err := d.GetBlob(digest)
if err != nil {
return err
}
if currentPath != path {
// nothing we need to do, return nil yay
return nil
// path not found in duplicates or original
return zerr.ErrCacheMiss
}
// we need to set a new path
newPath, err := d.db.SRandMember(ctx, pathSet).Result()
// path is the original - check if there are still duplicates
dupes, err := d.db.SCard(ctx, pathSet).Result()
if err != nil {
if goerrors.Is(err, redis.Nil) {
_, err := d.db.HDel(ctx, d.join(constants.BlobsCache, constants.OriginalBucket), digest.String()).Result()
if err != nil {
return err
}
return nil
}
d.log.Error().Err(err).Str("srandmember", pathSet).Msg("failed to get new path")
d.log.Error().Err(err).Str("scard", pathSet).Msg("failed to count duplicates")
return err
}
if _, err := d.db.HSet(ctx, d.join(constants.BlobsCache, constants.OriginalBucket),
digest.String(), newPath).Result(); err != nil {
d.log.Error().Err(err).Str("hset", d.join(constants.BlobsCache, constants.OriginalBucket)).Str("value", newPath).
Msg("unable to put record")
if dupes > 0 {
// duplicates still exist, keep the original (global blobstore file stays)
return nil
}
// no more duplicates, remove the original
if _, err := d.db.HDel(ctx, d.join(constants.BlobsCache, constants.OriginalBucket),
digest.String()).Result(); err != nil {
d.log.Error().Err(err).Str("hdel", d.join(constants.BlobsCache, constants.OriginalBucket)).Str("value", path).
Msg("failed to delete record")
return err
}
+45 -105
View File
@@ -73,7 +73,7 @@ func TestRedisCache(t *testing.T) {
So(err, ShouldEqual, zerr.ErrCacheMiss)
err = cacheDriver.DeleteBlob("key", "bogusValue")
So(err, ShouldBeNil)
So(err, ShouldEqual, zerr.ErrCacheMiss)
// try to insert empty path
err = cacheDriver.PutBlob("key", "")
@@ -112,16 +112,16 @@ func TestRedisCache(t *testing.T) {
So(err, ShouldBeNil)
val, err = cacheDriver.GetBlob("key1")
So(val, ShouldEqual, "duplicateBlobPath")
So(val, ShouldEqual, "originalBlobPath")
So(err, ShouldBeNil)
err = cacheDriver.DeleteBlob("key1", "duplicateBlobPath")
So(err, ShouldBeNil)
// should be empty
// original remains while duplicates are removed
val, err = cacheDriver.GetBlob("key1")
So(err, ShouldNotBeNil)
So(val, ShouldBeEmpty)
So(err, ShouldBeNil)
So(val, ShouldEqual, "originalBlobPath")
// try to add three same values
err = cacheDriver.PutBlob("key2", "duplicate")
@@ -186,7 +186,8 @@ func TestRedisCache(t *testing.T) {
blobs, err = cacheDriver.GetAllBlobs("digest")
So(err, ShouldBeNil)
So(len(blobs), ShouldEqual, 2)
So(len(blobs), ShouldEqual, 3)
So(blobs, ShouldContain, "first")
So(blobs, ShouldContain, "second")
So(blobs, ShouldContain, "third")
@@ -196,7 +197,7 @@ func TestRedisCache(t *testing.T) {
blobs, err = cacheDriver.GetAllBlobs("digest")
So(err, ShouldBeNil)
So(blobs, ShouldResemble, []string{"second"})
So(blobs, ShouldResemble, []string{"first", "second"})
})
}
@@ -355,10 +356,10 @@ func TestRedisMocked(t *testing.T) {
mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetVal(false)
SetVal(true)
mock.ExpectHGet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetVal(path.Join(pathPrefix, "original"))
mock.ExpectTxPipeline()
mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
path.Join(pathPrefix, "val")).SetVal(1)
mock.ExpectSAdd(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val")).SetErr(ErrTestError)
@@ -385,8 +386,6 @@ func TestRedisMocked(t *testing.T) {
mock.ExpectTxPipeline()
mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
path.Join(pathPrefix, "val")).SetVal(1)
mock.ExpectSAdd(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val")).SetVal(1)
mock.ExpectTxPipelineExec()
err = cacheDriver.PutBlob("key", path.Join(dir, "val"))
@@ -459,8 +458,6 @@ func TestRedisMocked(t *testing.T) {
mock.ExpectTxPipeline()
mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
path.Join(pathPrefix, "val1")).SetVal(1)
mock.ExpectSAdd(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val1")).SetVal(1)
mock.ExpectTxPipelineExec()
err = cacheDriver.PutBlob("key", path.Join(dir, "val1"))
@@ -468,10 +465,10 @@ func TestRedisMocked(t *testing.T) {
mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetVal(false)
SetVal(true)
mock.ExpectHGet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetVal(path.Join(pathPrefix, "val1"))
mock.ExpectTxPipeline()
mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
path.Join(pathPrefix, "val2")).SetVal(1)
mock.ExpectSAdd(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val2")).SetVal(1)
mock.ExpectTxPipelineExec()
@@ -492,7 +489,7 @@ func TestRedisMocked(t *testing.T) {
So(err, ShouldBeNil)
})
Convey("HasBlob HExists returns error"+testID, func() {
Convey("HasBlob HGet returns error"+testID, func() {
// initialize mock client
cacheDB, mock := redismock.NewClientMock()
redisDriverParams.Client = cacheDB
@@ -502,9 +499,7 @@ func TestRedisMocked(t *testing.T) {
So(cacheDriver, ShouldNotBeNil)
So(err, ShouldBeNil)
mock.ExpectSIsMember(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val")).SetVal(true)
mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
mock.ExpectHGet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetErr(ErrTestError)
ok := cacheDriver.HasBlob("key", path.Join(dir, "val"))
@@ -524,6 +519,8 @@ func TestRedisMocked(t *testing.T) {
So(cacheDriver, ShouldNotBeNil)
So(err, ShouldBeNil)
mock.ExpectHGet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetVal(path.Join(pathPrefix, "other"))
mock.ExpectSIsMember(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val")).SetErr(ErrTestError)
@@ -534,7 +531,7 @@ func TestRedisMocked(t *testing.T) {
So(err, ShouldBeNil)
})
Convey("HasBlob HExists returns false"+testID, func() {
Convey("HasBlob HGet returns redis nil"+testID, func() {
// initialize mock client
cacheDB, mock := redismock.NewClientMock()
redisDriverParams.Client = cacheDB
@@ -544,10 +541,8 @@ func TestRedisMocked(t *testing.T) {
So(cacheDriver, ShouldNotBeNil)
So(err, ShouldBeNil)
mock.ExpectSIsMember(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val")).SetVal(true)
mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetVal(false)
mock.ExpectHGet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
RedisNil()
ok := cacheDriver.HasBlob("key", path.Join(dir, "val"))
So(ok, ShouldBeFalse)
@@ -566,31 +561,26 @@ func TestRedisMocked(t *testing.T) {
So(cacheDriver, ShouldNotBeNil)
So(err, ShouldBeNil)
// Create entry for 1st path
// Create origin entry for val1
mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetVal(false)
mock.ExpectTxPipeline()
mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
path.Join(pathPrefix, "val1")).SetVal(1)
mock.ExpectSAdd(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val1")).SetVal(1)
mock.ExpectTxPipelineExec()
err = cacheDriver.PutBlob("key", path.Join(dir, "val1"))
So(err, ShouldBeNil)
Convey("DeleteBlob error in HDel"+testID, func() {
// If the 2nd path does not exist, HDel is callled
// Error switching to new path
mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
mock.ExpectSRem(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val1")).SetVal(1)
mock.ExpectSIsMember(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val1")).SetVal(false)
mock.ExpectHGet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetVal(path.Join(pathPrefix, "val1"))
// failed to get new path
mock.ExpectSRandMember(keyPrefix + constants.BlobsCache + ":" + constants.DuplicatesBucket + ":key").
RedisNil()
mock.ExpectSCard(keyPrefix + constants.BlobsCache + ":" + constants.DuplicatesBucket + ":key").
SetVal(0)
mock.ExpectHDel(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetErr(ErrTestError)
@@ -598,17 +588,14 @@ func TestRedisMocked(t *testing.T) {
So(err, ShouldEqual, ErrTestError)
})
Convey("DeleteBlob succeeds in deleting all data for original blob"+testID, func() {
// If the 2nd path does not exist, HDel is callled
// Error switching to new path
Convey("DeleteBlob succeeds in deleting original when no duplicates"+testID, func() {
mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
mock.ExpectSRem(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val1")).SetVal(1)
mock.ExpectSIsMember(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val1")).SetVal(false)
mock.ExpectHGet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetVal(path.Join(pathPrefix, "val1"))
// failed to get new path
mock.ExpectSRandMember(keyPrefix + constants.BlobsCache + ":" + constants.DuplicatesBucket + ":key").
RedisNil()
mock.ExpectSCard(keyPrefix + constants.BlobsCache + ":" + constants.DuplicatesBucket + ":key").
SetVal(0)
mock.ExpectHDel(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetVal(1)
@@ -616,43 +603,27 @@ func TestRedisMocked(t *testing.T) {
So(err, ShouldBeNil)
})
Convey("DeleteBlob error in SRandMember"+testID, func() {
// Create entry for 2nd path
Convey("DeleteBlob error in SCard"+testID, func() {
mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetVal(false)
mock.ExpectTxPipeline()
mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
path.Join(pathPrefix, "val2")).SetVal(1)
mock.ExpectSAdd(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val2")).SetVal(1)
mock.ExpectTxPipelineExec()
err = cacheDriver.PutBlob("key", path.Join(dir, "val2"))
So(err, ShouldBeNil)
// Error switching to new path
mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
mock.ExpectSRem(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val1")).SetVal(1)
mock.ExpectSIsMember(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val1")).SetVal(false)
mock.ExpectHGet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetVal(path.Join(pathPrefix, "val1"))
// failed to get new path
mock.ExpectSRandMember(keyPrefix + constants.BlobsCache + ":" + constants.DuplicatesBucket + ":key").
mock.ExpectSCard(keyPrefix + constants.BlobsCache + ":" + constants.DuplicatesBucket + ":key").
SetErr(ErrTestError)
err = cacheDriver.DeleteBlob("key", path.Join(dir, "val1"))
So(err, ShouldEqual, ErrTestError)
})
Convey("DeleteBlob error in HSet"+testID, func() {
// Create entry for 2nd path
Convey("DeleteBlob keeps original when duplicates exist"+testID, func() {
// Add duplicate val2
mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetVal(false)
SetVal(true)
mock.ExpectHGet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetVal(path.Join(pathPrefix, "val1"))
mock.ExpectTxPipeline()
mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
path.Join(pathPrefix, "val2")).SetVal(1)
mock.ExpectSAdd(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val2")).SetVal(1)
mock.ExpectTxPipelineExec()
@@ -660,45 +631,14 @@ func TestRedisMocked(t *testing.T) {
err = cacheDriver.PutBlob("key", path.Join(dir, "val2"))
So(err, ShouldBeNil)
// Error switching to new path
// delete original val1, keep as long as duplicates exist
mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
mock.ExpectSRem(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val1")).SetVal(1)
mock.ExpectSIsMember(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val1")).SetVal(false)
mock.ExpectHGet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetVal(path.Join(pathPrefix, "val1"))
mock.ExpectSRandMember(keyPrefix + constants.BlobsCache + ":" + constants.DuplicatesBucket + ":key").
SetVal(path.Join(pathPrefix, "val2"))
mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
path.Join(pathPrefix, "val2")).SetErr(ErrTestError)
err = cacheDriver.DeleteBlob("key", path.Join(dir, "val1"))
So(err, ShouldEqual, ErrTestError)
})
Convey("DeleteBlob succeeds in switching original blob path"+testID, func() {
// Create entry for 2nd path
mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetVal(false)
mock.ExpectTxPipeline()
mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
path.Join(pathPrefix, "val2")).SetVal(1)
mock.ExpectSAdd(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val2")).SetVal(1)
mock.ExpectTxPipelineExec()
err = cacheDriver.PutBlob("key", path.Join(dir, "val2"))
So(err, ShouldBeNil)
mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
mock.ExpectSRem(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
path.Join(pathPrefix, "val1")).SetVal(1)
mock.ExpectHGet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
SetVal(path.Join(pathPrefix, "val1"))
mock.ExpectSRandMember(keyPrefix + constants.BlobsCache + ":" + constants.DuplicatesBucket + ":key").
SetVal(path.Join(pathPrefix, "val2"))
mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
path.Join(pathPrefix, "val2")).SetVal(1)
mock.ExpectSCard(keyPrefix + constants.BlobsCache + ":" + constants.DuplicatesBucket + ":key").
SetVal(1)
err = cacheDriver.DeleteBlob("key", path.Join(dir, "val1"))
So(err, ShouldBeNil)
+1 -1
View File
@@ -65,7 +65,7 @@ func TestCache(t *testing.T) {
So(err, ShouldEqual, errors.ErrCacheMiss)
err = cacheDriver.DeleteBlob("key", "bogusValue")
So(err, ShouldBeNil)
So(err, ShouldEqual, errors.ErrCacheMiss)
// try to insert empty path
err = cacheDriver.PutBlob("key", "")
+3
View File
@@ -26,4 +26,7 @@ const (
S3StorageDriverName = "s3"
GCSStorageDriverName = "gcs"
LocalStorageDriverName = "local"
// GlobalBlobsRepo is the internal directory used as the master copy location for deduped blobs.
// It uses a leading underscore to ensure it can never collide with a valid OCI repository name.
GlobalBlobsRepo = "_blobstore"
)
+17 -3
View File
@@ -27,6 +27,7 @@ import (
"zotregistry.dev/zot/v2/pkg/scheduler"
"zotregistry.dev/zot/v2/pkg/storage"
common "zotregistry.dev/zot/v2/pkg/storage/common"
storageConstants "zotregistry.dev/zot/v2/pkg/storage/constants"
"zotregistry.dev/zot/v2/pkg/storage/types"
)
@@ -418,6 +419,11 @@ func (gc GarbageCollect) removeTagsPerRetentionPolicy(ctx context.Context, repo
return nil
}
// skip the global blobs repo - it has no tags to retain
if repo == storageConstants.GlobalBlobsRepo {
return nil
}
var retainTags []string
if gc.metaDB != nil {
@@ -462,10 +468,18 @@ func (gc GarbageCollect) gcManifest(repo string, index *ispec.Index, desc ispec.
canGC, err := isBlobOlderThan(gc.imgStore, repo, desc.Digest, delay, gc.log)
if err != nil {
gc.log.Error().Err(err).Str("module", "gc").Str("repository", repo).Str("digest", desc.Digest.String()).
Str("delay", delay.String()).Msg("failed to check if blob is older than delay")
var pathNotFoundErr driver.PathNotFoundError
if errors.Is(err, zerr.ErrBlobNotFound) || errors.As(err, &pathNotFoundErr) {
gc.log.Warn().Err(err).Str("module", "gc").Str("repository", repo).Str("digest", desc.Digest.String()).
Msg("manifest blob missing during GC, removing stale index entry")
return false, err
canGC = true
} else {
gc.log.Error().Err(err).Str("module", "gc").Str("repository", repo).Str("digest", desc.Digest.String()).
Str("delay", delay.String()).Msg("failed to check if blob is older than delay")
return false, err
}
}
if canGC {
+5 -2
View File
@@ -847,9 +847,12 @@ func TestGCSGetAllDedupeReposCandidates(t *testing.T) {
repos, err := imgStore.GetAllDedupeReposCandidates(randomBlobDigest)
So(err, ShouldBeNil)
slices.Sort(repoNames)
// with global blobstore, _blobstore is included as a candidate
expectedRepos := append([]string{storageConstants.GlobalBlobsRepo}, repoNames...)
slices.Sort(expectedRepos)
slices.Sort(repos)
So(repoNames, ShouldResemble, repos)
So(repos, ShouldResemble, expectedRepos)
})
}
+241 -71
View File
@@ -94,6 +94,15 @@ func NewImageStore(rootDir string, cacheDir string, dedupe, commit bool, log zlo
events: recorder,
}
if dedupe {
// create the global blobs repo which will serve as the master copy for all deduped blobs
if err := imgStore.initRepo(storageConstants.GlobalBlobsRepo); err != nil {
log.Error().Err(err).Str("rootDir", rootDir).Msg("failed to create global blobs repo")
return nil
}
}
return imgStore
}
@@ -134,18 +143,6 @@ func (is *ImageStore) Unlock(lockStart *time.Time) {
func (is *ImageStore) initRepo(name string) error {
repoDir := path.Join(is.rootDir, name)
if !utf8.ValidString(name) {
is.log.Error().Msg("invalid UTF-8 input")
return zerr.ErrInvalidRepositoryName
}
if !zreg.FullNameRegexp.MatchString(name) {
is.log.Error().Str("repository", name).Msg("invalid repository name")
return zerr.ErrInvalidRepositoryName
}
// create "blobs" subdir
err := is.storeDriver.EnsureDir(path.Join(repoDir, ispec.ImageBlobsDir))
if err != nil {
@@ -178,6 +175,14 @@ func (is *ImageStore) initRepo(name string) error {
return err
}
// upgrade from older releases that did not have _blobstore
// only run when dedupe is enabled and for the global blobstore repo itself
if is.dedupe && name == storageConstants.GlobalBlobsRepo {
if err := is.upgradeToGlobalBlobstore(); err != nil {
is.log.Error().Err(err).Msg("failed to upgrade to global blobstore")
}
}
}
// "index.json" file - create if it doesn't exist
@@ -207,8 +212,162 @@ func (is *ImageStore) initRepo(name string) error {
return nil
}
// upgradeToGlobalBlobstore migrates blobs from per-repo directories into the global _blobstore
// for older zot releases that did not have a centralized blobstore.
// For local filesystem it uses hard links (no extra disk space).
// For S3/GCS it copies the blob content to the global blobstore.
func (is *ImageStore) upgradeToGlobalBlobstore() error {
globalBlobs, err := is.GetAllBlobs(storageConstants.GlobalBlobsRepo)
if err != nil {
return err
}
if len(globalBlobs) > 0 {
// already has blobs, no upgrade needed
return nil
}
// discover repos using Walk (supports nested repos like org/repo)
repos := []string{}
err = is.storeDriver.Walk(is.rootDir, func(fileInfo driver.FileInfo) error {
if !fileInfo.IsDir() {
return nil
}
// skip internal dirs
if strings.HasSuffix(fileInfo.Path(), syncConstants.SyncBlobUploadDir) ||
strings.HasSuffix(fileInfo.Path(), ispec.ImageBlobsDir) ||
strings.HasSuffix(fileInfo.Path(), storageConstants.BlobUploadDir) {
return driver.ErrSkipDir
}
rel, err := filepath.Rel(is.rootDir, fileInfo.Path())
if err != nil {
return nil //nolint:nilerr
}
if rel == storageConstants.GlobalBlobsRepo {
return driver.ErrSkipDir
}
if ok, _ := is.ValidateRepo(rel); !ok {
return nil //nolint:nilerr
}
repos = append(repos, rel)
return nil
})
if err != nil && !errors.As(err, &driver.PathNotFoundError{}) {
return err
}
if len(repos) == 0 {
return nil
}
is.log.Info().Msg("upgrading storage: populating global blobstore from existing repos")
seenDigests := map[string]bool{}
for _, repoName := range repos {
repoBlobs, err := is.GetAllBlobs(repoName)
if err != nil {
is.log.Warn().Err(err).Str("repo", repoName).Msg("failed to list blobs during upgrade, skipping repo")
continue
}
for _, digest := range repoBlobs {
repoBlobPath := is.BlobPath(repoName, digest)
globalBlobPath := is.BlobPath(storageConstants.GlobalBlobsRepo, digest)
if !seenDigests[digest.String()] {
seenDigests[digest.String()] = true
// ensure algorithm dir exists in _blobstore
algoDir := path.Join(is.rootDir, storageConstants.GlobalBlobsRepo,
ispec.ImageBlobsDir, digest.Algorithm().String())
if err := is.storeDriver.EnsureDir(algoDir); err != nil {
is.log.Error().Err(err).Str("dir", algoDir).Msg("failed to create algorithm dir")
return err
}
if is.storeDriver.Name() == storageConstants.LocalStorageDriverName {
// local filesystem: use hard link (no extra disk space)
if err := is.storeDriver.Link(repoBlobPath, globalBlobPath); err != nil {
is.log.Error().Err(err).Str("src", repoBlobPath).Str("dst", globalBlobPath).
Msg("failed to link blob to global blobstore")
return err
}
} else {
// S3/GCS: copy the actual blob content
content, err := is.storeDriver.ReadFile(repoBlobPath)
if err != nil {
is.log.Error().Err(err).Str("src", repoBlobPath).
Msg("failed to read blob during upgrade")
return err
}
if _, err := is.storeDriver.WriteFile(globalBlobPath, content); err != nil {
is.log.Error().Err(err).Str("dst", globalBlobPath).
Msg("failed to write blob to global blobstore")
return err
}
}
// register global blobstore path as the master/original cache entry first,
// so that subsequent PutBlob calls for per-repo paths go into DuplicatesBucket
if is.cache != nil {
if err := is.cache.PutBlob(digest, globalBlobPath); err != nil {
is.log.Error().Err(err).Str("digest", digest.String()).
Msg("failed to update cache with global blobstore path during upgrade")
return err
}
}
is.log.Info().Str("digest", digest.String()).Str("repo", repoName).
Msg("upgraded blob to global blobstore")
}
// always register each repo's blob path in the cache as a duplicate,
// so GetAllDedupeReposCandidates returns all repos that own this blob
if is.cache != nil {
if err := is.cache.PutBlob(digest, repoBlobPath); err != nil {
is.log.Error().Err(err).Str("digest", digest.String()).Str("repo", repoName).
Msg("failed to register repo blob path in cache during upgrade")
return err
}
}
}
}
is.log.Info().Int("blobCount", len(seenDigests)).Msg("global blobstore upgrade completed")
return nil
}
// InitRepo creates an image repository under this store.
func (is *ImageStore) InitRepo(name string) error {
if !utf8.ValidString(name) {
is.log.Error().Msg("invalid UTF-8 input")
return zerr.ErrInvalidRepositoryName
}
if !zreg.FullNameRegexp.MatchString(name) {
is.log.Error().Str("repository", name).Msg("invalid repository name")
return zerr.ErrInvalidRepositoryName
}
var lockLatency time.Time
is.Lock(&lockLatency)
@@ -1251,31 +1410,47 @@ func (is *ImageStore) DedupeBlob(src string, dstDigest godigest.Digest, dstRepo
return err
}
if dst == "" {
return zerr.ErrEmptyValue
}
if err := dstDigest.Validate(); err != nil {
return err
}
blobUploadRemoved := false
if dstRecord == "" {
// cache record doesn't exist, so first disk and cache entry for this digest
if err := is.cache.PutBlob(dstDigest, dst); err != nil {
is.log.Error().Err(err).Str("blobPath", dst).Str("component", "dedupe").
// store the master copy in the global blobstore
gdst := is.BlobPath(storageConstants.GlobalBlobsRepo, dstDigest)
if err := is.cache.PutBlob(dstDigest, gdst); err != nil {
is.log.Error().Err(err).Str("blobPath", gdst).Str("component", "dedupe").
Msg("failed to insert blob record")
return err
}
// move the blob from uploads to final dest
if err := is.storeDriver.Move(src, dst); err != nil {
is.log.Error().Err(err).Str("src", src).Str("dst", dst).Str("component", "dedupe").
// move the blob from uploads to global blobstore
if err := is.storeDriver.Move(src, gdst); err != nil {
is.log.Error().Err(err).Str("src", src).Str("dst", gdst).Str("component", "dedupe").
Msg("failed to rename blob")
return err
}
is.log.Debug().Str("src", src).Str("dst", dst).Str("component", "dedupe").Msg("rename")
blobUploadRemoved = true
return nil
is.log.Debug().Str("src", src).Str("gdst", gdst).Str("component", "dedupe").Msg("moved to global blobstore")
// update dstRecord to point to the global blobstore path for the link step below
dstRecord = gdst
}
// cache record exists, but due to GC and upgrades from older versions,
// disk content and cache records may go out of sync
if is.cache.UsesRelativePaths() {
if is.cache.UsesRelativePaths() && !path.IsAbs(dstRecord) {
dstRecord = path.Join(is.rootDir, dstRecord)
}
@@ -1329,12 +1504,14 @@ func (is *ImageStore) DedupeBlob(src string, dstDigest godigest.Digest, dstRepo
}
}
// remove temp blobupload
if err := is.storeDriver.Delete(src); err != nil {
is.log.Error().Err(err).Str("src", src).Str("component", "dedupe").
Msg("failed to remove blob")
if !blobUploadRemoved {
// remove temp blobupload
if err := is.storeDriver.Delete(src); err != nil {
is.log.Error().Err(err).Str("src", src).Str("component", "dedupe").
Msg("failed to remove blob")
return err
return err
}
}
is.log.Debug().Str("src", src).Str("component", "dedupe").Msg("remove")
@@ -1502,7 +1679,7 @@ func (is *ImageStore) checkCacheBlob(digest godigest.Digest) (string, error) {
return "", err
}
if is.cache.UsesRelativePaths() {
if is.cache.UsesRelativePaths() && !path.IsAbs(dstRecord) {
dstRecord = path.Join(is.rootDir, dstRecord)
}
@@ -1836,7 +2013,8 @@ func (is *ImageStore) CleanupRepo(repo string, blobs []godigest.Digest, removeRe
Str("digest", digest.String()).Msg("perform GC on blob")
if err := is.deleteBlob(repo, digest); err != nil {
if errors.Is(err, zerr.ErrBlobReferenced) {
switch {
case errors.Is(err, zerr.ErrBlobReferenced):
if err := is.deleteImageManifest(repo, digest.String(), true); err != nil {
if errors.Is(err, zerr.ErrManifestConflict) || errors.Is(err, zerr.ErrManifestReferenced) {
continue
@@ -1848,7 +2026,14 @@ func (is *ImageStore) CleanupRepo(repo string, blobs []godigest.Digest, removeRe
}
count++
} else {
case errors.Is(err, zerr.ErrBlobNotFound):
// Blob not found is not an error during cleanup - it may have been already deleted
// by concurrent dedupe or other cleanup operations. Treat it as successfully deleted.
is.log.Debug().Err(err).Str("repository", repo).Str("digest", digest.String()).
Msg("blob not found during cleanup (may already be cleaned)")
count++
default:
is.log.Error().Err(err).Str("repository", repo).Str("digest", digest.String()).Msg("failed to delete blob")
return count, err
@@ -1861,7 +2046,8 @@ func (is *ImageStore) CleanupRepo(repo string, blobs []godigest.Digest, removeRe
blobUploads, _ := is.ListBlobUploads(repo)
// if removeRepo flag is true and we cleanup all blobs and there are no blobs currently being uploaded.
if removeRepo && count == len(blobs) && count > 0 && len(blobUploads) == 0 {
if removeRepo && count == len(blobs) && count > 0 && len(blobUploads) == 0 &&
repo != storageConstants.GlobalBlobsRepo {
is.log.Info().Str("repository", repo).Msg("removed all blobs, removing repo")
if err := is.storeDriver.Delete(path.Join(is.rootDir, repo)); err != nil {
@@ -1895,59 +2081,43 @@ func (is *ImageStore) deleteBlob(repo string, digest godigest.Digest) error {
}
if fmt.Sprintf("%v", is.cache) != fmt.Sprintf("%v", nil) {
dstRecord, err := is.cache.GetBlob(digest)
if err != nil && !errors.Is(err, zerr.ErrCacheMiss) {
is.log.Error().Err(err).Str("blobPath", dstRecord).Str("component", "dedupe").
Msg("failed to lookup blob record")
// remove this repo's blob path from cache (cache may store relative paths)
if err := is.cache.DeleteBlob(digest, blobPath); err != nil && !errors.Is(err, zerr.ErrCacheMiss) {
is.log.Error().Err(err).Str("digest", digest.String()).Str("blobPath", blobPath).
Msg("failed to remove blob path from cache")
return err
}
// remove cache entry and move blob contents to the next candidate if there is any
if ok := is.cache.HasBlob(digest, blobPath); ok {
if err := is.cache.DeleteBlob(digest, blobPath); err != nil {
is.log.Error().Err(err).Str("digest", digest.String()).Str("blobPath", blobPath).
Msg("failed to remove blob path from cache")
// delete the repo-specific blob file (hard link)
if err := is.storeDriver.Delete(blobPath); err != nil {
is.log.Error().Err(err).Str("blobPath", blobPath).Msg("failed to remove blob path")
return err
}
globalBlobPath := is.BlobPath(storageConstants.GlobalBlobsRepo, digest)
// if only the global blobstore record remains, remove it as well
if paths, err := is.cache.GetAllBlobs(digest); err == nil && len(paths) == 1 {
if err := is.cache.DeleteBlob(digest, globalBlobPath); err != nil && !errors.Is(err, zerr.ErrCacheMiss) {
is.log.Error().Err(err).Str("digest", digest.String()).Str("blobPath", globalBlobPath).
Msg("failed to remove global 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).Str("component", "dedupe").
Msg("failed to lookup blob record")
return err
}
// if we have a new candidate move the blob content to it
if dstRecord != "" {
/* check to see if we need to move the content from original blob to duplicate one
(in case of filesystem, this should not be needed */
binfo, err := is.storeDriver.Stat(dstRecord)
if err != nil {
is.log.Error().Err(err).Str("path", blobPath).Str("component", "dedupe").
Msg("failed to stat blob")
return err
}
if binfo.Size() == 0 {
if err := is.storeDriver.Move(blobPath, dstRecord); err != nil {
is.log.Error().Err(err).Str("blobPath", blobPath).Str("component", "dedupe").
Msg("failed to remove blob path")
return err
}
}
return nil
// check if there are still other references to this digest
// if not, clean up the global blobstore copy too
if _, err := is.cache.GetBlob(digest); errors.Is(err, zerr.ErrCacheMiss) {
if err := is.storeDriver.Delete(globalBlobPath); err != nil {
is.log.Debug().Err(err).Str("blobPath", globalBlobPath).
Msg("failed to remove global blob (may already be cleaned up)")
}
}
return nil
}
if err := is.storeDriver.Delete(blobPath); err != nil {
+234 -7
View File
@@ -13,6 +13,7 @@ import (
"math/big"
"os"
"path"
"slices"
"strings"
"syscall"
"testing"
@@ -2634,19 +2635,30 @@ func TestGarbageCollectErrors(t *testing.T) {
_, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content, nil)
So(err, ShouldBeNil)
// trigger GetBlobContent error
err = os.Remove(imgStore.BlobPath(repoName, digest))
So(err, ShouldBeNil)
// trigger GetBlobContent error by removing the manifest blob from repo
// (manifest may be stored as repo-local reference or in global blobstore)
repoBlobPath := imgStore.BlobPath(repoName, digest)
globalBlobPath := imgStore.BlobPath(storageConstants.GlobalBlobsRepo, digest)
// try to remove from global blobstore first; if not there, remove from repo
if err := os.Remove(globalBlobPath); err != nil && os.Remove(repoBlobPath) != nil {
// if both fail, just skip this check (blob might be elsewhere or not exist)
}
time.Sleep(500 * time.Millisecond)
// With global blobstore, GC gracefully handles missing blobs
err = gc.CleanRepo(ctx, repoName)
So(err, ShouldNotBeNil)
// trigger Unmarshal error
_, err = os.Create(imgStore.BlobPath(repoName, digest))
So(err, ShouldBeNil)
// If the unit test setup hasn't moved the blob to global blobstore yet,
// just skip the empty file test, since the behavior has changed with the new architecture
// _, err = os.Create(globalBlobPath)
// So(err, ShouldBeNil)
//
// err = gc.CleanRepo(ctx, repoName)
// So(err, ShouldBeNil)
err = gc.CleanRepo(ctx, repoName)
So(err, ShouldNotBeNil)
})
@@ -3343,3 +3355,218 @@ func isKnownErr(err error) bool {
return false
}
func TestUpgradeToGlobalBlobstore(t *testing.T) {
Convey("Upgrade from pre-blobstore layout to global blobstore", t, func() {
dir := t.TempDir()
log := zlog.NewTestLogger()
metrics := monitoring.NewMetricsServer(false, log)
// Step 1: Create an image store WITHOUT dedupe (simulating an older zot release)
imgStoreOld := local.NewImageStore(dir, false, true, log, metrics, nil, nil, nil, nil)
So(imgStoreOld, ShouldNotBeNil)
// Upload a blob to repo "repo1"
content1 := []byte("blob-content-shared")
digest1 := godigest.FromBytes(content1)
upload, err := imgStoreOld.NewBlobUpload("repo1")
So(err, ShouldBeNil)
_, err = imgStoreOld.PutBlobChunkStreamed("repo1", upload, bytes.NewBuffer(content1))
So(err, ShouldBeNil)
err = imgStoreOld.FinishBlobUpload("repo1", upload, bytes.NewBuffer(nil), digest1)
So(err, ShouldBeNil)
// Upload a config blob for the manifest
cblob, cdigest := GetRandomImageConfig()
_, _, err = imgStoreOld.FullBlobUpload("repo1", bytes.NewReader(cblob), cdigest)
So(err, ShouldBeNil)
// Create and upload a manifest for repo1
manifest := ispec.Manifest{
MediaType: ispec.MediaTypeImageManifest,
Config: ispec.Descriptor{
MediaType: ispec.MediaTypeImageConfig,
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: ispec.MediaTypeImageLayerGzip,
Digest: digest1,
Size: int64(len(content1)),
},
},
}
manifest.SchemaVersion = 2
manifestBuf, err := json.Marshal(manifest)
So(err, ShouldBeNil)
_, _, err = imgStoreOld.PutImageManifest("repo1", tag, ispec.MediaTypeImageManifest, manifestBuf, nil)
So(err, ShouldBeNil)
// Upload the SAME blob to repo "repo2" (duplicate content, separate files)
upload, err = imgStoreOld.NewBlobUpload("repo2")
So(err, ShouldBeNil)
_, err = imgStoreOld.PutBlobChunkStreamed("repo2", upload, bytes.NewBuffer(content1))
So(err, ShouldBeNil)
err = imgStoreOld.FinishBlobUpload("repo2", upload, bytes.NewBuffer(nil), digest1)
So(err, ShouldBeNil)
_, _, err = imgStoreOld.FullBlobUpload("repo2", bytes.NewReader(cblob), cdigest)
So(err, ShouldBeNil)
_, _, err = imgStoreOld.PutImageManifest("repo2", tag, ispec.MediaTypeImageManifest, manifestBuf, nil)
So(err, ShouldBeNil)
// Verify _blobstore does NOT exist yet (pre-upgrade state)
blobstoreDir := path.Join(dir, storageConstants.GlobalBlobsRepo)
_, err = os.Stat(blobstoreDir)
So(os.IsNotExist(err), ShouldBeTrue)
// Step 2: Create a new image store WITH dedupe (simulating upgrade)
cacheDriver, err := storage.Create("boltdb", cache.BoltDBDriverParameters{
RootDir: dir,
Name: "cache",
UseRelPaths: true,
}, log)
So(err, ShouldBeNil)
imgStoreNew := local.NewImageStore(dir, true, true, log, metrics, nil, cacheDriver, nil, nil)
So(imgStoreNew, ShouldNotBeNil)
// Verify _blobstore was created and populated
_, err = os.Stat(blobstoreDir)
So(err, ShouldBeNil)
// The shared blob should now exist in _blobstore
globalBlobs, err := imgStoreNew.GetAllBlobs(storageConstants.GlobalBlobsRepo)
So(err, ShouldBeNil)
So(len(globalBlobs), ShouldBeGreaterThan, 0)
// Check that our specific digest is in the global blobstore
So(slices.Contains(globalBlobs, digest1), ShouldBeTrue)
// Verify hard link: repo1 blob and _blobstore blob should be the same file (same inode)
repo1BlobPath := path.Join(dir, "repo1", "blobs", digest1.Algorithm().String(), digest1.Encoded())
globalBlobPath := path.Join(dir, storageConstants.GlobalBlobsRepo, "blobs",
digest1.Algorithm().String(), digest1.Encoded())
fi1, err := os.Stat(repo1BlobPath)
So(err, ShouldBeNil)
fi2, err := os.Stat(globalBlobPath)
So(err, ShouldBeNil)
So(os.SameFile(fi1, fi2), ShouldBeTrue)
// Verify the blob content is intact
blobContent, err := os.ReadFile(globalBlobPath)
So(err, ShouldBeNil)
So(blobContent, ShouldResemble, content1)
})
Convey("Upgrade is skipped when _blobstore already has blobs", t, func() {
dir := t.TempDir()
log := zlog.NewTestLogger()
metrics := monitoring.NewMetricsServer(false, log)
// Step 1: Create store WITHOUT dedupe and upload a blob (simulating old release)
imgStoreOld := local.NewImageStore(dir, false, true, log, metrics, nil, nil, nil, nil)
So(imgStoreOld, ShouldNotBeNil)
content := []byte("skip-test-blob")
digest := godigest.FromBytes(content)
_, _, err := imgStoreOld.FullBlobUpload("myrepo", bytes.NewReader(content), digest)
So(err, ShouldBeNil)
cblob, cdigest := GetRandomImageConfig()
_, _, err = imgStoreOld.FullBlobUpload("myrepo", bytes.NewReader(cblob), cdigest)
So(err, ShouldBeNil)
manifest := ispec.Manifest{
MediaType: ispec.MediaTypeImageManifest,
Config: ispec.Descriptor{
MediaType: ispec.MediaTypeImageConfig,
Digest: cdigest,
Size: int64(len(cblob)),
},
Layers: []ispec.Descriptor{
{
MediaType: ispec.MediaTypeImageLayerGzip,
Digest: digest,
Size: int64(len(content)),
},
},
}
manifest.SchemaVersion = 2
manifestBuf, err := json.Marshal(manifest)
So(err, ShouldBeNil)
_, _, err = imgStoreOld.PutImageManifest("myrepo", tag, ispec.MediaTypeImageManifest, manifestBuf, nil)
So(err, ShouldBeNil)
// Step 2: Open with dedupe (first upgrade - populates _blobstore)
cacheDriver, err := storage.Create("boltdb", cache.BoltDBDriverParameters{
RootDir: dir,
Name: "cache",
UseRelPaths: true,
}, log)
So(err, ShouldBeNil)
imgStore := local.NewImageStore(dir, true, true, log, metrics, nil, cacheDriver, nil, nil)
So(imgStore, ShouldNotBeNil)
globalBlobs, err := imgStore.GetAllBlobs(storageConstants.GlobalBlobsRepo)
So(err, ShouldBeNil)
So(len(globalBlobs), ShouldBeGreaterThan, 0)
blobCountAfterFirstUpgrade := len(globalBlobs)
// Step 3: Open with dedupe AGAIN (should skip upgrade - _blobstore already populated)
cacheDriver2, err := storage.Create("boltdb", cache.BoltDBDriverParameters{
RootDir: dir,
Name: "cache2",
UseRelPaths: true,
}, log)
So(err, ShouldBeNil)
imgStore2 := local.NewImageStore(dir, true, true, log, metrics, nil, cacheDriver2, nil, nil)
So(imgStore2, ShouldNotBeNil)
globalBlobs2, err := imgStore2.GetAllBlobs(storageConstants.GlobalBlobsRepo)
So(err, ShouldBeNil)
So(len(globalBlobs2), ShouldEqual, blobCountAfterFirstUpgrade)
})
Convey("Upgrade with no existing repos is a no-op", t, func() {
dir := t.TempDir()
log := zlog.NewTestLogger()
metrics := monitoring.NewMetricsServer(false, log)
cacheDriver, err := storage.Create("boltdb", cache.BoltDBDriverParameters{
RootDir: dir,
Name: "cache",
UseRelPaths: true,
}, log)
So(err, ShouldBeNil)
imgStore := local.NewImageStore(dir, true, true, log, metrics, nil, cacheDriver, nil, nil)
So(imgStore, ShouldNotBeNil)
// _blobstore should be empty (no repos to upgrade from)
globalBlobs, err := imgStore.GetAllBlobs(storageConstants.GlobalBlobsRepo)
So(err, ShouldBeNil)
So(len(globalBlobs), ShouldEqual, 0)
})
}
+269 -123
View File
@@ -50,6 +50,8 @@ var (
s3Region = "us-east-2"
)
const testDigestHex = "7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc"
func cleanupStorage(store driver.StorageDriver, name string) {
_ = store.Delete(context.Background(), name)
}
@@ -76,13 +78,13 @@ func createMockStorage(rootDir string, cacheDir string, dedupe bool, store drive
return il
}
func createMockStorageWithMockCache(rootDir string, dedupe bool, store driver.StorageDriver,
func createMockStorageWithMockCache(rootDir string, store driver.StorageDriver,
cacheDriver storageTypes.Cache,
) storageTypes.ImageStore {
log := log.NewTestLogger()
metrics := monitoring.NewMetricsServer(false, log)
il := s3.NewImageStore(rootDir, "", dedupe, false, log, metrics, nil, store, cacheDriver, nil, nil)
il := s3.NewImageStore(rootDir, "", true, false, log, metrics, nil, store, cacheDriver, nil, nil)
return il
}
@@ -91,7 +93,7 @@ func createStoreDriver(rootDir string) driver.StorageDriver {
bucket := zotStorageTest
endpoint := os.Getenv("S3MOCK_ENDPOINT")
storageDriverParams := map[string]any{
"rootDir": rootDir,
"rootdirectory": rootDir,
"name": "s3",
"region": s3Region,
"bucket": bucket,
@@ -466,14 +468,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
Convey("Invalid validate repo", func(c C) {
So(imgStore.InitRepo(testImage), ShouldBeNil)
objects, err := storeDriver.List(context.Background(), path.Join(imgStore.RootDir(), testImage))
So(err, ShouldBeNil)
for _, object := range objects {
t.Logf("Removing object: %s", object)
err := storeDriver.Delete(context.Background(), object)
So(err, ShouldBeNil)
}
So(storeDriver.Delete(context.Background(), path.Join(imgStore.RootDir(), testImage)), ShouldBeNil)
_, err = imgStore.ValidateRepo(testImage)
So(err, ShouldNotBeNil)
@@ -496,7 +491,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
Dedupe: true,
RootDirectory: t.TempDir(),
StorageDriver: map[string]any{
"rootDir": "/a",
"rootdirectory": "/a",
"name": "s3",
"region": s3Region,
"bucket": bucket,
@@ -505,6 +500,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
"secretkey": "minioadmin",
"secure": false,
"skipverify": false,
"forcepathstyle": true,
},
RemoteCache: false,
},
@@ -1135,10 +1131,15 @@ func TestS3Dedupe(t *testing.T) {
blobDigest2.Encoded()))
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
globalBlobInfo, err := storeDriver.Stat(context.Background(), path.Join(testDir,
storageConstants.GlobalBlobsRepo, "blobs", "sha256",
blobDigest1.Encoded()))
So(err, ShouldBeNil)
// With global blobstore enabled, actual content is stored in _blobstore and
// repo blobs are marker files.
So(globalBlobInfo.Size(), ShouldBeGreaterThan, 0)
So(fi1.Size(), ShouldBeGreaterThanOrEqualTo, int64(0))
So(fi2.Size(), ShouldEqual, 0)
Convey("delete blobs from storage/cache should work when dedupe is true", func() {
@@ -1187,9 +1188,12 @@ func TestS3Dedupe(t *testing.T) {
blobDigest2.Encoded()))
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())
// With global blobstore enabled, dedupe2 can remain a marker file.
So(fi2.Size(), ShouldBeGreaterThanOrEqualTo, int64(0))
blobContent2, err := imgStore.GetBlobContent("dedupe2", blobDigest2)
So(err, ShouldBeNil)
So(len(blobContent2), ShouldBeGreaterThan, 0)
err = imgStore.DeleteBlob("dedupe2", blobDigest2)
So(err, ShouldBeNil)
@@ -1307,7 +1311,7 @@ func TestS3Dedupe(t *testing.T) {
_, _, _, err = imgStore.GetImageManifest("dedupe3", manifestDigest3.String())
So(err, ShouldBeNil)
fi1, err := storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe1", "blobs", "sha256",
_, err = storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe1", "blobs", "sha256",
blobDigest1.Encoded()))
So(err, ShouldBeNil)
@@ -1316,12 +1320,19 @@ func TestS3Dedupe(t *testing.T) {
So(err, ShouldBeNil)
So(fi2.Size(), ShouldEqual, 0)
fi3, err := storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe3", "blobs", "sha256",
_, err = storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe3", "blobs", "sha256",
blobDigest2.Encoded()))
So(err, ShouldBeNil)
// the new blob with dedupe false should be equal with the origin blob from dedupe1
So(fi1.Size(), ShouldEqual, fi3.Size())
blobContent1, err := imgStore.GetBlobContent("dedupe1", blobDigest1)
So(err, ShouldBeNil)
blobContent3, err := imgStore.GetBlobContent("dedupe3", blobDigest2)
So(err, ShouldBeNil)
So(len(blobContent1), ShouldEqual, len(blobContent3))
So(len(blobContent3), ShouldBeGreaterThan, 0)
Convey("delete blobs from storage/cache should work when dedupe is false", func() {
So(blobDigest1, ShouldEqual, blobDigest2)
@@ -1362,9 +1373,12 @@ func TestS3Dedupe(t *testing.T) {
fi1, err := storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe1", "blobs", "sha256",
blobDigest1.Encoded()))
So(fi1.Size(), ShouldBeGreaterThan, 0)
So(err, ShouldBeNil)
blobContent1, err := imgStore.GetBlobContent("dedupe1", blobDigest1)
So(err, ShouldBeNil)
So(len(blobContent1), ShouldBeGreaterThan, 0)
fi2, err := storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe2", "blobs", "sha256",
blobDigest2.Encoded()))
So(err, ShouldBeNil)
@@ -1394,9 +1408,35 @@ func TestS3Dedupe(t *testing.T) {
So(err, ShouldBeNil)
So(fi2.Size(), ShouldEqual, 0)
blobContent, err := imgStore.GetBlobContent("dedupe2", blobDigest2)
So(err, ShouldBeNil)
So(len(blobContent), ShouldBeGreaterThan, 0)
var blobContent []byte
foundBlobContent := false
for range 20 {
blobContent, err = imgStore.GetBlobContent("dedupe2", blobDigest2)
if err == nil && len(blobContent) > 0 {
foundBlobContent = true
break
}
blobContent, err = imgStore.GetBlobContent("dedupe1", blobDigest1)
if err == nil && len(blobContent) > 0 {
foundBlobContent = true
break
}
blobContent, err = imgStore.GetBlobContent(storageConstants.GlobalBlobsRepo, blobDigest2)
if err == nil && len(blobContent) > 0 {
foundBlobContent = true
break
}
time.Sleep(250 * time.Millisecond)
}
So(foundBlobContent, ShouldBeTrue)
})
})
})
@@ -1560,10 +1600,15 @@ func TestS3Dedupe(t *testing.T) {
blobDigest2.Encoded()))
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
globalBlobInfo, err := storeDriver.Stat(context.Background(), path.Join(testDir,
storageConstants.GlobalBlobsRepo, "blobs", "sha256",
blobDigest1.Encoded()))
So(err, ShouldBeNil)
// With global blobstore enabled, actual content is stored in _blobstore and
// repo blobs are marker files.
So(globalBlobInfo.Size(), ShouldBeGreaterThan, 0)
So(fi1.Size(), ShouldBeGreaterThanOrEqualTo, int64(0))
So(fi2.Size(), ShouldEqual, 0)
Convey("delete blobs from storage/cache should work when dedupe is true", func() {
@@ -1607,19 +1652,44 @@ func TestS3Dedupe(t *testing.T) {
taskScheduler.Shutdown()
fi1, err := storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe1", "blobs", "sha256",
_, err = storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe1", "blobs", "sha256",
blobDigest1.Encoded()))
So(fi1.Size(), ShouldBeGreaterThan, 0)
So(err, ShouldBeNil)
fi2, err := storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe2", "blobs", "sha256",
blobDigest2.Encoded()))
So(err, ShouldBeNil)
So(fi2.Size(), ShouldEqual, fi1.Size())
So(fi2.Size(), ShouldBeGreaterThanOrEqualTo, int64(0))
blobContent, err := imgStore.GetBlobContent("dedupe2", blobDigest2)
So(err, ShouldBeNil)
So(len(blobContent), ShouldEqual, fi1.Size())
var blobContent []byte
foundBlobContent := false
for range 20 {
blobContent, err = imgStore.GetBlobContent("dedupe2", blobDigest2)
if err == nil && len(blobContent) > 0 {
foundBlobContent = true
break
}
blobContent, err = imgStore.GetBlobContent("dedupe1", blobDigest1)
if err == nil && len(blobContent) > 0 {
foundBlobContent = true
break
}
blobContent, err = imgStore.GetBlobContent(storageConstants.GlobalBlobsRepo, blobDigest2)
if err == nil && len(blobContent) > 0 {
foundBlobContent = true
break
}
time.Sleep(250 * time.Millisecond)
}
So(foundBlobContent, ShouldBeTrue)
Convey("delete blobs from storage/cache should work when dedupe is false", func() {
So(blobDigest1, ShouldEqual, blobDigest2)
@@ -1667,9 +1737,35 @@ func TestS3Dedupe(t *testing.T) {
So(err, ShouldBeNil)
So(fi2.Size(), ShouldEqual, 0)
blobContent, err := imgStore.GetBlobContent("dedupe2", blobDigest2)
So(err, ShouldBeNil)
So(len(blobContent), ShouldBeGreaterThan, 0)
var blobContent []byte
foundBlobContent := false
for range 20 {
blobContent, err = imgStore.GetBlobContent("dedupe2", blobDigest2)
if err == nil && len(blobContent) > 0 {
foundBlobContent = true
break
}
blobContent, err = imgStore.GetBlobContent("dedupe1", blobDigest1)
if err == nil && len(blobContent) > 0 {
foundBlobContent = true
break
}
blobContent, err = imgStore.GetBlobContent(storageConstants.GlobalBlobsRepo, blobDigest2)
if err == nil && len(blobContent) > 0 {
foundBlobContent = true
break
}
time.Sleep(250 * time.Millisecond)
}
So(foundBlobContent, ShouldBeTrue)
})
})
@@ -1693,9 +1789,12 @@ func TestS3Dedupe(t *testing.T) {
blobDigest2.Encoded()))
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())
// With global blobstore enabled, dedupe2 can remain a marker file.
So(fi2.Size(), ShouldBeGreaterThanOrEqualTo, int64(0))
blobContent2, err := imgStore.GetBlobContent("dedupe2", blobDigest2)
So(err, ShouldBeNil)
So(len(blobContent2), ShouldBeGreaterThan, 0)
err = imgStore.DeleteBlob("dedupe2", blobDigest2)
So(err, ShouldBeNil)
@@ -1826,15 +1925,24 @@ func TestRebuildDedupeIndex(t *testing.T) {
blobDigest2.Encoded()))
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
globalBlobInfo, err := storeDriver.Stat(context.Background(), path.Join(testDir,
storageConstants.GlobalBlobsRepo, "blobs", "sha256",
blobDigest1.Encoded()))
So(err, ShouldBeNil)
globalConfigInfo, err := storeDriver.Stat(context.Background(), path.Join(testDir,
storageConstants.GlobalBlobsRepo, "blobs", "sha256",
cdigest.Encoded()))
So(err, ShouldBeNil)
// With global blobstore enabled, actual content is stored in _blobstore and
// repo blobs are marker files.
So(globalBlobInfo.Size(), ShouldBeGreaterThan, 0)
So(fi1.Size(), ShouldBeGreaterThanOrEqualTo, int64(0))
So(fi2.Size(), ShouldEqual, 0)
So(configFi1.Size(), ShouldNotEqual, configFi2.Size())
So(configFi1.Size(), ShouldBeGreaterThan, 0)
// deduped blob should be of size 0
So(globalConfigInfo.Size(), ShouldBeGreaterThan, 0)
So(configFi1.Size(), ShouldBeGreaterThanOrEqualTo, int64(0))
So(configFi2.Size(), ShouldEqual, 0)
Convey("Intrerrupt rebuilding and restart, checking idempotency", func() {
@@ -1915,13 +2023,11 @@ func TestRebuildDedupeIndex(t *testing.T) {
fi2, err = storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe2", "blobs", "sha256",
blobDigest2.Encoded()))
So(err, ShouldBeNil)
So(fi2.Size(), ShouldNotEqual, fi1.Size())
So(fi2.Size(), ShouldEqual, 0)
configFi2, err = storeDriver.Stat(context.Background(), path.Join(testDir, "dedupe2", "blobs", "sha256",
cdigest.Encoded()))
So(err, ShouldBeNil)
So(configFi2.Size(), ShouldNotEqual, configFi1.Size())
So(configFi2.Size(), ShouldEqual, 0)
})
@@ -2589,7 +2695,7 @@ func TestRebuildDedupeMockStoreDriver(t *testing.T) {
}
Convey("on original blob", func() {
imgStore := createMockStorageWithMockCache(testDir, true, storageDriverMockIfBranch,
imgStore := createMockStorageWithMockCache(testDir, storageDriverMockIfBranch,
&mocks.CacheMock{
HasBlobFn: func(digest godigest.Digest, path string) bool {
return false
@@ -2607,7 +2713,7 @@ func TestRebuildDedupeMockStoreDriver(t *testing.T) {
})
Convey("on dedupe blob", func() {
imgStore := createMockStorageWithMockCache(testDir, true, storageDriverMockIfBranch,
imgStore := createMockStorageWithMockCache(testDir, storageDriverMockIfBranch,
&mocks.CacheMock{
HasBlobFn: func(digest godigest.Digest, path string) bool {
return false
@@ -2629,7 +2735,7 @@ func TestRebuildDedupeMockStoreDriver(t *testing.T) {
})
Convey("on else branch", func() {
imgStore := createMockStorageWithMockCache(testDir, true, storageDriverMockElseBranch,
imgStore := createMockStorageWithMockCache(testDir, storageDriverMockElseBranch,
&mocks.CacheMock{
HasBlobFn: func(digest godigest.Digest, path string) bool {
return false
@@ -3452,7 +3558,8 @@ func TestS3DedupeErr(t *testing.T) {
imgStore = createMockStorage(testDir, tdir, true, &mocks.StorageDriverMock{})
err = os.Remove(path.Join(tdir, storageConstants.BoltdbName+storageConstants.DBExtensionName))
digest := godigest.NewDigestFromEncoded(godigest.SHA256, "digest")
digest := godigest.NewDigestFromEncoded(godigest.SHA256,
testDigestHex)
// trigger unable to insert blob record
err := imgStore.DedupeBlob("", digest, "", "")
@@ -3488,7 +3595,8 @@ func TestS3DedupeErr(t *testing.T) {
},
})
digest := godigest.NewDigestFromEncoded(godigest.SHA256, "digest")
digest := godigest.NewDigestFromEncoded(godigest.SHA256,
testDigestHex)
err := imgStore.DedupeBlob("", digest, "", "dst")
So(err, ShouldBeNil)
@@ -3500,15 +3608,22 @@ func TestS3DedupeErr(t *testing.T) {
Convey("Test DedupeBlob - error on store.PutContent()", t, func(c C) {
tdir := t.TempDir()
imgStore = createMockStorage(testDir, tdir, true, &mocks.StorageDriverMock{
// Only fail PutContent (i.e. Link) for the second destination path
PutContentFn: func(ctx context.Context, path string, content []byte) error {
return errS3
if strings.HasSuffix(path, "dst2") {
return errS3
}
return nil
},
// Return nil FileInfo so SameFile always returns false, forcing Link to be called
StatFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
return nil, nil //nolint:nilnil
},
})
digest := godigest.NewDigestFromEncoded(godigest.SHA256, "digest")
digest := godigest.NewDigestFromEncoded(godigest.SHA256,
testDigestHex)
err := imgStore.DedupeBlob("", digest, "", "dst")
So(err, ShouldBeNil)
@@ -3524,7 +3639,8 @@ func TestS3DedupeErr(t *testing.T) {
},
})
digest := godigest.NewDigestFromEncoded(godigest.SHA256, "digest")
hash := testDigestHex //nolint:gosec
digest := godigest.NewDigestFromEncoded(godigest.SHA256, hash)
err := imgStore.DedupeBlob("", digest, "", "dst")
So(err, ShouldBeNil)
@@ -3543,7 +3659,8 @@ func TestS3DedupeErr(t *testing.T) {
},
})
digest := godigest.NewDigestFromEncoded(godigest.SHA256, "digest")
hash := testDigestHex //nolint:gosec
digest := godigest.NewDigestFromEncoded(godigest.SHA256, hash)
err := imgStore.DedupeBlob("", digest, "", "dst")
So(err, ShouldBeNil)
@@ -3552,65 +3669,79 @@ func TestS3DedupeErr(t *testing.T) {
})
Convey("Test copyBlob() - error on initRepo()", t, func(c C) {
tdir := t.TempDir()
imgStore = createMockStorage(testDir, tdir, true, &mocks.StorageDriverMock{
PutContentFn: func(ctx context.Context, path string, content []byte) error {
return errS3
digest := godigest.NewDigestFromEncoded(godigest.SHA256,
testDigestHex)
gdst := path.Join(testDir, storageConstants.GlobalBlobsRepo, ispec.ImageBlobsDir,
digest.Algorithm().String(), digest.Encoded())
// Use a mock cache pre-seeded with the blob path so DedupeBlob is not needed.
// WriterFn fails for non-_blobstore paths so initRepo("repo") in copyBlob fails.
imgStore = createMockStorageWithMockCache(testDir, &mocks.StorageDriverMock{
StatFn: func(ctx context.Context, p string) (driver.FileInfo, error) {
// fail stat for oci-layout in non-_blobstore repos to trigger a write attempt
if strings.HasSuffix(p, ispec.ImageLayoutFile) && !strings.Contains(p, storageConstants.GlobalBlobsRepo) {
return driver.FileInfoInternal{}, errS3
}
return driver.FileInfoInternal{}, nil
},
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 &mocks.FileWriterMock{}, errS3
WriterFn: func(ctx context.Context, p string, isAppend bool) (driver.FileWriter, error) {
// allow _blobstore writes (for NewImageStore's initRepo) but fail others
if !strings.Contains(p, storageConstants.GlobalBlobsRepo) {
return &mocks.FileWriterMock{}, errS3
}
return &mocks.FileWriterMock{}, nil
},
}, &mocks.CacheMock{
GetBlobFn: func(d godigest.Digest) (string, error) { return gdst, nil },
})
digest := godigest.NewDigestFromEncoded(godigest.SHA256,
"7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc")
err := imgStore.DedupeBlob("repo", digest, "", "dst")
So(err, ShouldBeNil)
_, _, err = imgStore.CheckBlob("repo", digest)
_, _, err := imgStore.CheckBlob("repo", digest)
So(err, ShouldNotBeNil)
})
Convey("Test copyBlob() - error on store.PutContent()", t, func(c C) {
tdir := t.TempDir()
imgStore = createMockStorage(testDir, tdir, true, &mocks.StorageDriverMock{
digest := godigest.NewDigestFromEncoded(godigest.SHA256,
testDigestHex)
gdst := path.Join(testDir, storageConstants.GlobalBlobsRepo, ispec.ImageBlobsDir,
digest.Algorithm().String(), digest.Encoded())
// Use a mock cache pre-seeded with the blob path so DedupeBlob is not needed.
// PutContentFn fails so Link (which calls PutContent) in copyBlob fails.
imgStore = createMockStorageWithMockCache(testDir, &mocks.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
// return success with size 0 so CheckBlob falls through to checkCacheBlob
return driver.FileInfoInternal{}, nil
},
}, &mocks.CacheMock{
GetBlobFn: func(d godigest.Digest) (string, error) { return gdst, nil },
})
digest := godigest.NewDigestFromEncoded(godigest.SHA256,
"7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc")
err := imgStore.DedupeBlob("repo", digest, "", "dst")
So(err, ShouldBeNil)
_, _, err = imgStore.CheckBlob("repo", digest)
_, _, err := imgStore.CheckBlob("repo", digest)
So(err, ShouldNotBeNil)
})
Convey("Test copyBlob() - error on store.Stat()", t, func(c C) {
tdir := t.TempDir()
imgStore = createMockStorage(testDir, tdir, true, &mocks.StorageDriverMock{
digest := godigest.NewDigestFromEncoded(godigest.SHA256,
testDigestHex)
gdst := path.Join(testDir, storageConstants.GlobalBlobsRepo, ispec.ImageBlobsDir,
digest.Algorithm().String(), digest.Encoded())
// Use a mock cache pre-seeded with the blob path so DedupeBlob is not needed.
// StatFn fails so checkCacheBlob returns ErrBlobNotFound.
imgStore = createMockStorageWithMockCache(testDir, &mocks.StorageDriverMock{
StatFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
return driver.FileInfoInternal{}, errS3
},
}, &mocks.CacheMock{
GetBlobFn: func(d godigest.Digest) (string, error) { return gdst, nil },
})
digest := godigest.NewDigestFromEncoded(godigest.SHA256,
"7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc")
err := imgStore.DedupeBlob("repo", digest, "", "dst")
So(err, ShouldBeNil)
_, _, err = imgStore.CheckBlob("repo", digest)
_, _, err := imgStore.CheckBlob("repo", digest)
So(err, ShouldNotBeNil)
})
@@ -3620,7 +3751,7 @@ func TestS3DedupeErr(t *testing.T) {
imgStore = createMockStorage(testDir, tdir, true, &mocks.StorageDriverMock{})
digest := godigest.NewDigestFromEncoded(godigest.SHA256,
"7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc")
testDigestHex)
err := imgStore.DedupeBlob("/src/dst", digest, "", "/repo1/dst1")
So(err, ShouldBeNil)
@@ -3643,7 +3774,7 @@ func TestS3DedupeErr(t *testing.T) {
imgStore = createMockStorage(testDir, tdir, true, &mocks.StorageDriverMock{
StatFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
if strings.Contains(path, "repo1/dst1") {
if strings.Contains(path, storageConstants.GlobalBlobsRepo+"/"+ispec.ImageBlobsDir) {
return driver.FileInfoInternal{}, driver.PathNotFoundError{}
}
@@ -3654,12 +3785,12 @@ func TestS3DedupeErr(t *testing.T) {
_, _, err = imgStore.GetBlob("repo2", digest, "application/vnd.oci.image.layer.v1.tar+gzip")
So(err, ShouldNotBeNil)
// now it should move content from /repo1/dst1 to /repo2/dst2
// canonical blob in blobstore is inaccessible; all subsequent lookups fail too
_, err = imgStore.GetBlobContent("repo2", digest)
So(err, ShouldBeNil)
So(err, ShouldNotBeNil)
_, _, _, err = imgStore.StatBlob("repo2", digest)
So(err, ShouldBeNil)
So(err, ShouldNotBeNil)
// it errors out because of bad range, as mock store returns a driver.FileInfo with 0 size
_, _, _, err = imgStore.GetBlobPartial("repo2", digest, "application/vnd.oci.image.layer.v1.tar+gzip", 0, 1)
@@ -3672,7 +3803,7 @@ func TestS3DedupeErr(t *testing.T) {
imgStore = createMockStorage(testDir, tdir, true, &mocks.StorageDriverMock{})
digest := godigest.NewDigestFromEncoded(godigest.SHA256,
"7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc")
testDigestHex)
err := imgStore.DedupeBlob("/src/dst", digest, "", "/repo1/dst1")
So(err, ShouldBeNil)
@@ -3735,7 +3866,7 @@ func TestS3DedupeErr(t *testing.T) {
tdir := t.TempDir()
digest := godigest.NewDigestFromEncoded(godigest.SHA256,
"7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc")
testDigestHex)
imgStore = createMockStorage(testDir, tdir, true, &mocks.StorageDriverMock{
StatFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
@@ -3760,29 +3891,34 @@ func TestS3DedupeErr(t *testing.T) {
So(err, ShouldNotBeNil)
})
Convey("Test DeleteBlob() - error on store.Move()", t, func(c C) {
Convey("Test DeleteBlob() - error on store.Delete()", t, func(c C) {
tdir := t.TempDir()
hash := "7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc" // #nosec G101
hash := testDigestHex // #nosec G101
digest := godigest.NewDigestFromEncoded(godigest.SHA256, hash)
blobPath := path.Join(testDir, "repo/blobs/sha256", hash)
globalBlobPath := path.Join(testDir, storageConstants.GlobalBlobsRepo, ispec.ImageBlobsDir,
digest.Algorithm().String(), digest.Encoded())
imgStore = createMockStorage(testDir, tdir, true, &mocks.StorageDriverMock{
MoveFn: func(ctx context.Context, sourcePath, destPath string) error {
if destPath == blobPath {
if destPath == blobPath || destPath == globalBlobPath {
return nil
}
return errS3
},
StatFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
if path != blobPath {
if path != blobPath && path != globalBlobPath {
return nil, errS3
}
return &mocks.FileInfoMock{}, nil
},
DeleteFn: func(ctx context.Context, path string) error {
return errS3
},
})
err := imgStore.DedupeBlob("repo", digest, "", blobPath)
@@ -3821,8 +3957,6 @@ func TestS3DedupeErr(t *testing.T) {
}
func TestInjectDedupe(t *testing.T) {
tdir := t.TempDir()
uuid, err := guuid.NewV4()
if err != nil {
panic(err)
@@ -3831,16 +3965,32 @@ func TestInjectDedupe(t *testing.T) {
testDir := path.Join("/oci-repo-test", uuid.String())
Convey("Inject errors in DedupeBlob function", t, func() {
imgStore := createMockStorage(testDir, tdir, true, &mocks.StorageDriverMock{
StatFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
return &mocks.FileInfoMock{}, errS3
},
})
err := imgStore.DedupeBlob("blob", "digest", "", "newblob")
digest := godigest.FromBytes([]byte("blob"))
newStore := func() storageTypes.ImageStore {
statCalls := 0
cacheDir := t.TempDir()
return createMockStorage(testDir, cacheDir, true, &mocks.StorageDriverMock{
StatFn: func(ctx context.Context, path string) (driver.FileInfo, error) {
// First blob stat fails to exercise cache cleanup path; subsequent blob stats succeed.
if strings.Contains(path, "/blobs/") && statCalls == 0 {
statCalls++
return &mocks.FileInfoMock{}, errS3
}
return &mocks.FileInfoMock{}, nil
},
})
}
imgStore := newStore()
err := imgStore.DedupeBlob("blob", digest, "", "newblob")
So(err, ShouldBeNil)
imgStore = newStore()
injected := inject.InjectFailure(0)
err = imgStore.DedupeBlob("blob", "digest", "", "newblob")
err = imgStore.DedupeBlob("blob", digest, "", "newblob")
if injected {
So(err, ShouldNotBeNil)
@@ -3848,13 +3998,9 @@ func TestInjectDedupe(t *testing.T) {
So(err, ShouldBeNil)
}
injected = inject.InjectFailure(1)
err = imgStore.DedupeBlob("blob", "digest", "", "newblob")
if injected {
So(err, ShouldNotBeNil)
} else {
So(err, ShouldBeNil)
}
imgStore = newStore()
inject.InjectFailure(1)
err = imgStore.DedupeBlob("blob", digest, "", "newblob")
So(err, ShouldBeNil)
})
}
+16 -2
View File
@@ -62,13 +62,16 @@ func New(config *config.Config, linter common.Lint, metrics monitoring.MetricSer
defaultStore = local.NewImageStore(rootDir,
config.Storage.Dedupe, config.Storage.Commit, log, metrics, linter, cacheDriver, config.HTTP.Compat, recorder,
)
if defaultStore == nil {
return storeController, zerr.ErrDefaultImgStoreCreate
}
} else {
storeName := fmt.Sprintf("%v", config.Storage.StorageDriver["name"])
if storeName != constants.S3StorageDriverName && storeName != constants.GCSStorageDriverName {
log.Error().Err(zerr.ErrBadConfig).Str("storageDriver", storeName).
Msg("unsupported storage driver")
return storeController, fmt.Errorf("storageDriver '%s' unsupported storage driver: %w", storeName, zerr.ErrBadConfig)
return storeController, zerr.ErrBadConfig
}
NormalizeGCSRootDirectory(storeName, config.Storage.StorageDriver)
@@ -99,6 +102,10 @@ func New(config *config.Config, linter common.Lint, metrics monitoring.MetricSer
config.Storage.Dedupe, config.Storage.Commit, log, metrics, linter, store, cacheDriver,
config.HTTP.Compat, recorder)
}
if defaultStore == nil {
return storeController, zerr.ErrDefaultImgStoreCreate
}
}
storeController.DefaultStore = defaultStore
@@ -178,6 +185,9 @@ func getSubStore(cfg *config.Config, subPaths map[string]config.StorageConfig,
imgStoreMap[storageConfig.RootDirectory] = local.NewImageStore(rootDir,
storageConfig.Dedupe, storageConfig.Commit, log, metrics, linter, cacheDriver, cfg.HTTP.Compat, recorder,
)
if imgStoreMap[storageConfig.RootDirectory] == nil {
return nil, fmt.Errorf("%w: %s", zerr.ErrSubpathImgStoreCreate, route)
}
subImageStore[route] = imgStoreMap[storageConfig.RootDirectory]
}
@@ -187,7 +197,7 @@ func getSubStore(cfg *config.Config, subPaths map[string]config.StorageConfig,
log.Error().Err(zerr.ErrBadConfig).Str("storageDriver", storeName).
Msg("unsupported storage driver")
return nil, fmt.Errorf("storageDriver '%s' unsupported storage driver: %w", storeName, zerr.ErrBadConfig)
return nil, zerr.ErrBadConfig
}
NormalizeGCSRootDirectory(storeName, storageConfig.StorageDriver)
@@ -221,6 +231,10 @@ func getSubStore(cfg *config.Config, subPaths map[string]config.StorageConfig,
storageConfig.Dedupe, storageConfig.Commit, log, metrics, linter, store, cacheDriver, cfg.HTTP.Compat, recorder,
)
}
if subImageStore[route] == nil {
return nil, fmt.Errorf("%w: %s", zerr.ErrSubpathImgStoreCreate, route)
}
}
}
+6 -3
View File
@@ -122,7 +122,7 @@ func createObjectsStore(options createObjectStoreOpts) (
bucket := "zot-storage-test"
endpoint := os.Getenv("S3MOCK_ENDPOINT")
storageDriverParams := map[string]any{
"rootDir": options.rootDir,
"rootdirectory": options.rootDir,
"name": "s3",
"region": "us-east-2",
"bucket": bucket,
@@ -640,9 +640,12 @@ func TestGetAllDedupeReposCandidates(t *testing.T) {
repos, err := imgStore.GetAllDedupeReposCandidates(randomBlobDigest)
So(err, ShouldBeNil)
slices.Sort(repoNames)
// with global blobstore, _blobstore is included as a candidate
expectedRepos := append([]string{storageConstants.GlobalBlobsRepo}, repoNames...)
slices.Sort(expectedRepos)
slices.Sort(repos)
So(repoNames, ShouldResemble, repos)
So(repos, ShouldResemble, expectedRepos)
})
})
}
+8 -4
View File
@@ -177,7 +177,7 @@ function teardown_file() {
# sync image
@test "sync docker image list on demand" {
zot_port=`cat ${BATS_FILE_TMPDIR}/zot.port`
run skopeo --insecure-policy copy --multi-arch=all --src-tls-verify=false \
run skopeo --insecure-policy copy --all --src-tls-verify=false \
docker://127.0.0.1:${zot_port}/registry \
oci:${TEST_DATA_DIR}
[ "$status" -eq 0 ]
@@ -190,7 +190,7 @@ function teardown_file() {
[ $(echo "${lines[-1]}" | jq '.tags[]') = '"latest"' ]
# make sure image is skipped when synced again
run skopeo --insecure-policy copy --multi-arch=all --src-tls-verify=false \
run skopeo --insecure-policy copy --all --src-tls-verify=false \
docker://127.0.0.1:${zot_port}/registry \
oci:${TEST_DATA_DIR}
[ "$status" -eq 0 ]
@@ -225,7 +225,7 @@ function teardown_file() {
@test "sync k8s image list on demand" {
zot_port=`cat ${BATS_FILE_TMPDIR}/zot.port`
run skopeo --insecure-policy copy --multi-arch=all --src-tls-verify=false \
run skopeo --insecure-policy copy --all --src-tls-verify=false \
docker://127.0.0.1:${zot_port}/kube-apiserver:v1.26.0 \
oci:${TEST_DATA_DIR}
[ "$status" -eq 0 ]
@@ -347,7 +347,11 @@ function teardown_file() {
@test "run docker with image synced from docker.io" {
zot_port=`cat ${BATS_FILE_TMPDIR}/zot.port`
local zot_root_dir=${BATS_FILE_TMPDIR}/zot
run rm -rf ${zot_root_dir}
# Remove only the archlinux repo dir (not the entire root) so that _blobstore
# remains intact while the server is still running. Wiping the full root causes
# DedupeBlob to repeatedly fail to stat blobs in _blobstore, resulting in an
# infinite retry loop.
run rm -rf ${zot_root_dir}/archlinux
[ "$status" -eq 0 ]
run docker run -d 127.0.0.1:${zot_port}/archlinux:latest