mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 21:17:58 +08:00
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:
committed by
GitHub
parent
273b15364b
commit
936a60d4f7
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
Vendored
+65
-44
@@ -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
|
||||
}
|
||||
|
||||
Vendored
+13
-4
@@ -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"})
|
||||
})
|
||||
}
|
||||
|
||||
Vendored
+71
-17
@@ -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) {
|
||||
|
||||
Vendored
+7
-7
@@ -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"})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Vendored
+60
-41
@@ -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
|
||||
}
|
||||
|
||||
Vendored
+45
-105
@@ -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)
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user