mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 21:17:58 +08:00
fix: gracefully handle manifests missing from storage (prepare for sparse indexes) (#3503)
GC and scrub should not stop if a manifest or index is missing from storage. Other similar changes are also included. WRT metadb, the missing manifests cannot be added, and the results returned from metadb do not include the descriptors for these manifests. Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
This commit is contained in:
@@ -500,6 +500,11 @@ func getAllContainedMeta(imageBuck *bbolt.Bucket, imageIndexData *proto_go.Image
|
||||
|
||||
imageManifestData, err := getProtoImageMeta(imageBuck, manifest.Digest)
|
||||
if err != nil {
|
||||
// Skip manifests that don't have MetaDB entries (missing from storage)
|
||||
if errors.Is(err, zerr.ErrImageMetaNotFound) {
|
||||
continue
|
||||
}
|
||||
|
||||
return imageMetaList, manifestDataList, err
|
||||
}
|
||||
|
||||
@@ -511,6 +516,8 @@ func getAllContainedMeta(imageBuck *bbolt.Bucket, imageIndexData *proto_go.Image
|
||||
compat.IsCompatibleManifestListMediaType(imageManifestData.MediaType) {
|
||||
partialImageDataList, partialManifestDataList, err := getAllContainedMeta(imageBuck, imageManifestData)
|
||||
if err != nil {
|
||||
// getAllContainedMeta skips missing items internally, so any error returned
|
||||
// is a real error that should be propagated
|
||||
return imageMetaList, manifestDataList, err
|
||||
}
|
||||
|
||||
|
||||
+148
-10
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -32,6 +33,8 @@ func (its imgTrustStore) VerifySignature(
|
||||
return "", time.Time{}, false, nil
|
||||
}
|
||||
|
||||
var errImageMetaBucketNotFound = errors.New("ImageMeta bucket not found")
|
||||
|
||||
func TestWrapperErrors(t *testing.T) {
|
||||
image := CreateDefaultImage()
|
||||
imageMeta := image.AsImageMeta()
|
||||
@@ -302,23 +305,63 @@ func TestWrapperErrors(t *testing.T) {
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("image is index, fail to get manifests", func() {
|
||||
Convey("image is index, missing manifests are skipped gracefully", func() {
|
||||
err := boltdbWrapper.SetRepoReference(ctx, "repo", "tag", multiarchImageMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Missing manifests are skipped gracefully, so GetFullImageMeta succeeds
|
||||
// but returns an index with no manifests
|
||||
fullImageMeta, err := boltdbWrapper.GetFullImageMeta(ctx, "repo", "tag")
|
||||
So(err, ShouldBeNil)
|
||||
So(len(fullImageMeta.Manifests), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("image is index, corrupted manifest data returns error", func() {
|
||||
// Create a multiarch image with multiple manifests
|
||||
multiarchImage := CreateMultiarchWith().RandomImages(2).Build()
|
||||
multiarchImageMeta := multiarchImage.AsImageMeta()
|
||||
err := boltdbWrapper.SetImageMeta(multiarchImageMeta.Digest, multiarchImageMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = boltdbWrapper.SetRepoMeta("repo", mTypes.RepoMeta{
|
||||
Name: "repo",
|
||||
Tags: map[mTypes.Tag]mTypes.Descriptor{
|
||||
"tag": {
|
||||
MediaType: ispec.MediaTypeImageIndex,
|
||||
Digest: multiarchImageMeta.Digest.String(),
|
||||
},
|
||||
},
|
||||
// Store the first manifest normally
|
||||
firstManifest := multiarchImage.Images[0]
|
||||
firstManifestMeta := firstManifest.AsImageMeta()
|
||||
err = boltdbWrapper.SetImageMeta(firstManifestMeta.Digest, firstManifestMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Store the second manifest normally first, then corrupt it
|
||||
secondManifest := multiarchImage.Images[1]
|
||||
secondManifestMeta := secondManifest.AsImageMeta()
|
||||
err = boltdbWrapper.SetImageMeta(secondManifestMeta.Digest, secondManifestMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
secondManifestDigest := secondManifest.ManifestDescriptor.Digest
|
||||
|
||||
// Corrupt the data for the second manifest by storing invalid protobuf data
|
||||
// This will cause getProtoImageMeta to return an unmarshaling error
|
||||
// which is not ErrImageMetaNotFound, so it will propagate through getAllContainedMeta
|
||||
corruptedData := []byte("invalid protobuf data")
|
||||
|
||||
// Access BoltDB directly to corrupt the data
|
||||
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
imageBuck := tx.Bucket([]byte(boltdb.ImageMetaBuck))
|
||||
if imageBuck == nil {
|
||||
return errImageMetaBucketNotFound
|
||||
}
|
||||
// Store corrupted protobuf data
|
||||
return imageBuck.Put([]byte(secondManifestDigest.String()), corruptedData)
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = boltdbWrapper.GetFullImageMeta(ctx, "repo", "tag")
|
||||
err = boltdbWrapper.SetRepoReference(ctx, "repo", "tag", multiarchImageMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// GetFullImageMeta should return an error due to corrupted manifest data
|
||||
// The error from getAllContainedMeta should propagate
|
||||
fullImageMeta, err := boltdbWrapper.GetFullImageMeta(ctx, "repo", "tag")
|
||||
So(err, ShouldNotBeNil)
|
||||
// Should still return a FullImageMeta object (even with error)
|
||||
So(fullImageMeta, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -443,6 +486,54 @@ func TestWrapperErrors(t *testing.T) {
|
||||
_, err = boltdbWrapper.FilterTags(ctx, mTypes.AcceptAllRepoTag, mTypes.AcceptAllImageMeta)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("getAllContainedMeta error for index is joined and processing continues", func() {
|
||||
// Create a multiarch image with multiple manifests
|
||||
multiarchImage := CreateMultiarchWith().RandomImages(2).Build()
|
||||
multiarchImageMeta := multiarchImage.AsImageMeta()
|
||||
err := boltdbWrapper.SetImageMeta(multiarchImageMeta.Digest, multiarchImageMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Store the first manifest normally
|
||||
firstManifest := multiarchImage.Images[0]
|
||||
firstManifestMeta := firstManifest.AsImageMeta()
|
||||
err = boltdbWrapper.SetImageMeta(firstManifestMeta.Digest, firstManifestMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Store the second manifest normally first, then corrupt it
|
||||
secondManifest := multiarchImage.Images[1]
|
||||
secondManifestMeta := secondManifest.AsImageMeta()
|
||||
err = boltdbWrapper.SetImageMeta(secondManifestMeta.Digest, secondManifestMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
secondManifestDigest := secondManifest.ManifestDescriptor.Digest
|
||||
|
||||
// Corrupt the data for the second manifest by storing invalid protobuf data
|
||||
// This will cause getProtoImageMeta to return an unmarshaling error
|
||||
// which is not ErrImageMetaNotFound, so it will propagate through getAllContainedMeta
|
||||
corruptedData := []byte("invalid protobuf data")
|
||||
|
||||
// Access BoltDB directly to corrupt the data
|
||||
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
imageBuck := tx.Bucket([]byte(boltdb.ImageMetaBuck))
|
||||
if imageBuck == nil {
|
||||
return errImageMetaBucketNotFound
|
||||
}
|
||||
// Store corrupted protobuf data
|
||||
return imageBuck.Put([]byte(secondManifestDigest.String()), corruptedData)
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = boltdbWrapper.SetRepoReference(ctx, "repo", "tag", multiarchImageMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// FilterTags should return an error due to corrupted manifest data
|
||||
// The error from getAllContainedMeta should be joined with viewError
|
||||
images, err := boltdbWrapper.FilterTags(ctx, mTypes.AcceptAllRepoTag, mTypes.AcceptAllImageMeta)
|
||||
So(err, ShouldNotBeNil)
|
||||
// Should still return some images (the first valid manifest might be processed)
|
||||
So(images, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("SearchRepos", func() {
|
||||
@@ -470,6 +561,53 @@ func TestWrapperErrors(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("GetImageMeta", func() {
|
||||
Convey("image is index, getAllContainedMeta error returns error", func() {
|
||||
// Create a multiarch image with multiple manifests
|
||||
multiarchImage := CreateMultiarchWith().RandomImages(2).Build()
|
||||
multiarchImageMeta := multiarchImage.AsImageMeta()
|
||||
err := boltdbWrapper.SetImageMeta(multiarchImageMeta.Digest, multiarchImageMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Store the first manifest normally
|
||||
firstManifest := multiarchImage.Images[0]
|
||||
firstManifestMeta := firstManifest.AsImageMeta()
|
||||
err = boltdbWrapper.SetImageMeta(firstManifestMeta.Digest, firstManifestMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Store the second manifest normally first, then corrupt it
|
||||
secondManifest := multiarchImage.Images[1]
|
||||
secondManifestMeta := secondManifest.AsImageMeta()
|
||||
err = boltdbWrapper.SetImageMeta(secondManifestMeta.Digest, secondManifestMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
secondManifestDigest := secondManifest.ManifestDescriptor.Digest
|
||||
|
||||
// Corrupt the data for the second manifest by storing invalid protobuf data
|
||||
// This will cause getProtoImageMeta to return an unmarshaling error
|
||||
// which is not ErrImageMetaNotFound, so it will propagate through getAllContainedMeta
|
||||
corruptedData := []byte("invalid protobuf data")
|
||||
|
||||
// Access BoltDB directly to corrupt the data
|
||||
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
|
||||
imageBuck := tx.Bucket([]byte(boltdb.ImageMetaBuck))
|
||||
if imageBuck == nil {
|
||||
return errImageMetaBucketNotFound
|
||||
}
|
||||
// Store corrupted protobuf data
|
||||
return imageBuck.Put([]byte(secondManifestDigest.String()), corruptedData)
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// GetImageMeta should return an error due to corrupted manifest data
|
||||
// The error from getAllContainedMeta should propagate
|
||||
imageMeta, err := boltdbWrapper.GetImageMeta(multiarchImageMeta.Digest)
|
||||
So(err, ShouldNotBeNil)
|
||||
// Should still return an ImageMeta object (even with error)
|
||||
So(imageMeta, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("SetRepoReference", func() {
|
||||
Convey("getProtoRepoMeta errors", func() {
|
||||
err := setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB)
|
||||
|
||||
@@ -1358,6 +1358,27 @@ func (dwr *DynamoDB) FilterImageMeta(ctx context.Context, digests []string,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if all requested digests were found
|
||||
// FilterImageMeta should return an error if any digest is missing (unlike getAllContainedMeta)
|
||||
if len(imageMetaAttributes) != len(digests) {
|
||||
// Build a map of found digests to identify which ones are missing
|
||||
foundDigests := make(map[string]bool)
|
||||
|
||||
for _, attributes := range imageMetaAttributes {
|
||||
var digest string
|
||||
if err := attributevalue.Unmarshal(attributes["TableKey"], &digest); err == nil {
|
||||
foundDigests[digest] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Find the first missing digest
|
||||
for _, digest := range digests {
|
||||
if !foundDigests[digest] {
|
||||
return nil, fmt.Errorf("%w for digest %s", zerr.ErrImageMetaNotFound, digest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results := map[string]mTypes.ImageMeta{}
|
||||
|
||||
for _, attributes := range imageMetaAttributes {
|
||||
@@ -2021,10 +2042,7 @@ func (dwr *DynamoDB) fetchImageMetaAttributesByDigest(ctx context.Context, diges
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(resp.Responses[dwr.ImageMetaTablename]) != size {
|
||||
return nil, zerr.ErrImageMetaNotFound
|
||||
}
|
||||
|
||||
// Missing manifests are allowed - append whatever was found
|
||||
batchedResp = append(batchedResp, resp.Responses[dwr.ImageMetaTablename]...)
|
||||
start = end
|
||||
}
|
||||
@@ -2045,13 +2063,12 @@ func (dwr *DynamoDB) fetchImageMetaAttributesByDigest(ctx context.Context, diges
|
||||
respMap[digest] = item
|
||||
}
|
||||
|
||||
// Only include digests that were actually found (missing ones are skipped gracefully)
|
||||
for _, digest := range digests {
|
||||
imageMeta, ok := respMap[digest]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w for digest %s", zerr.ErrImageMetaNotFound, digest)
|
||||
if ok {
|
||||
orderedResp = append(orderedResp, imageMeta)
|
||||
}
|
||||
|
||||
orderedResp = append(orderedResp, imageMeta)
|
||||
}
|
||||
|
||||
return orderedResp, nil
|
||||
@@ -2228,30 +2245,28 @@ func (dwr *DynamoDB) createVersionTable() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
mdAttributeValue, err := attributevalue.Marshal(version.CurrentVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mdAttributeValue, err := attributevalue.Marshal(version.CurrentVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = dwr.Client.UpdateItem(context.TODO(), &dynamodb.UpdateItemInput{
|
||||
ExpressionAttributeNames: map[string]string{
|
||||
"#V": "Version",
|
||||
_, err = dwr.Client.UpdateItem(context.TODO(), &dynamodb.UpdateItemInput{
|
||||
ExpressionAttributeNames: map[string]string{
|
||||
"#V": "Version",
|
||||
},
|
||||
ExpressionAttributeValues: map[string]types.AttributeValue{
|
||||
":Version": mdAttributeValue,
|
||||
},
|
||||
Key: map[string]types.AttributeValue{
|
||||
"TableKey": &types.AttributeValueMemberS{
|
||||
Value: version.DBVersionKey,
|
||||
},
|
||||
ExpressionAttributeValues: map[string]types.AttributeValue{
|
||||
":Version": mdAttributeValue,
|
||||
},
|
||||
Key: map[string]types.AttributeValue{
|
||||
"TableKey": &types.AttributeValueMemberS{
|
||||
Value: version.DBVersionKey,
|
||||
},
|
||||
},
|
||||
TableName: aws.String(dwr.VersionTablename),
|
||||
UpdateExpression: aws.String("SET #V = :Version"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
},
|
||||
TableName: aws.String(dwr.VersionTablename),
|
||||
UpdateExpression: aws.String("SET #V = :Version"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -383,12 +383,15 @@ func TestWrapperErrors(t *testing.T) {
|
||||
_, err := dynamoWrapper.GetImageMeta(testDigest)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
Convey("image index, get manifest meta fails", func() {
|
||||
Convey("image index, missing manifests are skipped gracefully", func() {
|
||||
err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", multiarchImageMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = dynamoWrapper.GetImageMeta(multiarchImageMeta.Digest) //nolint: contextcheck
|
||||
So(err, ShouldNotBeNil)
|
||||
// Missing manifests are skipped gracefully, so GetImageMeta succeeds
|
||||
// but returns an index with no manifests
|
||||
imageMeta, err := dynamoWrapper.GetImageMeta(multiarchImageMeta.Digest) //nolint: contextcheck
|
||||
So(err, ShouldBeNil)
|
||||
So(len(imageMeta.Manifests), ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
Convey("GetFullImageMeta", func() {
|
||||
@@ -429,7 +432,7 @@ func TestWrapperErrors(t *testing.T) {
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("image is index, fail to get manifests", func() {
|
||||
Convey("image is index, missing manifests are skipped gracefully", func() {
|
||||
err := dynamoWrapper.SetImageMeta(multiarchImageMeta.Digest, multiarchImageMeta) //nolint: contextcheck
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
@@ -444,8 +447,11 @@ func TestWrapperErrors(t *testing.T) {
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = dynamoWrapper.GetFullImageMeta(ctx, "repo", "tag")
|
||||
So(err, ShouldNotBeNil)
|
||||
// Missing manifests are skipped gracefully, so GetFullImageMeta succeeds
|
||||
// but returns an index with no manifests
|
||||
fullImageMeta, err := dynamoWrapper.GetFullImageMeta(ctx, "repo", "tag")
|
||||
So(err, ShouldBeNil)
|
||||
So(len(fullImageMeta.Manifests), ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -572,6 +572,75 @@ func RunMetaDBTests(t *testing.T, metaDB mTypes.MetaDB, preparationFuncs ...func
|
||||
So(fullImageMeta.Digest.String(), ShouldResemble, multi.DigestStr())
|
||||
})
|
||||
|
||||
Convey("GetFullImageMeta with nested index missing manifests", func() {
|
||||
// Create a nested index structure:
|
||||
// - Top-level index contains a nested index and a manifest
|
||||
// - The nested index references manifests that are missing from MetaDB
|
||||
// - The manifest in the top-level index exists
|
||||
// Create a valid manifest
|
||||
validImage := CreateDefaultImage()
|
||||
validImageMeta := validImage.AsImageMeta()
|
||||
err := metaDB.SetImageMeta(validImageMeta.Digest, validImageMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Create a nested index that references missing manifests
|
||||
nestedIndex := CreateMultiarchWith().Images([]Image{CreateRandomImage()}).Build()
|
||||
nestedIndexMeta := nestedIndex.AsImageMeta()
|
||||
// Store the nested index metadata (but not its manifests - they're missing)
|
||||
err = metaDB.SetImageMeta(nestedIndexMeta.Digest, nestedIndexMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Create a top-level index that contains:
|
||||
// 1. The nested index (with missing manifests)
|
||||
// 2. The valid manifest
|
||||
topLevelIndexContent := ispec.Index{
|
||||
Versioned: specs.Versioned{SchemaVersion: 2},
|
||||
MediaType: ispec.MediaTypeImageIndex,
|
||||
Manifests: []ispec.Descriptor{
|
||||
{
|
||||
MediaType: ispec.MediaTypeImageManifest,
|
||||
Digest: validImageMeta.Digest,
|
||||
Size: validImageMeta.Size,
|
||||
},
|
||||
{
|
||||
MediaType: ispec.MediaTypeImageIndex,
|
||||
Digest: nestedIndexMeta.Digest,
|
||||
Size: nestedIndexMeta.Size,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create and store the top-level index metadata
|
||||
topLevelIndexBlob, err := json.Marshal(topLevelIndexContent)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Calculate digest from the index content
|
||||
topLevelIndexDigest := godigest.FromBytes(topLevelIndexBlob)
|
||||
|
||||
topLevelIndexMeta := mTypes.ImageMeta{
|
||||
MediaType: ispec.MediaTypeImageIndex,
|
||||
Digest: topLevelIndexDigest,
|
||||
Size: int64(len(topLevelIndexBlob)),
|
||||
Index: &topLevelIndexContent,
|
||||
Manifests: []mTypes.ManifestMeta{},
|
||||
}
|
||||
|
||||
err = metaDB.SetImageMeta(topLevelIndexMeta.Digest, topLevelIndexMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = metaDB.SetRepoReference(ctx, "repo", "tag", topLevelIndexMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// GetFullImageMeta should succeed, skipping the nested index with missing manifests
|
||||
// but including the valid manifest from the top-level index
|
||||
fullImageMeta, err := metaDB.GetFullImageMeta(ctx, "repo", "tag")
|
||||
So(err, ShouldBeNil)
|
||||
// Should have the valid manifest from the top-level index
|
||||
// The nested index with missing manifests should be skipped
|
||||
So(len(fullImageMeta.Manifests), ShouldEqual, 1)
|
||||
So(fullImageMeta.Manifests[0].Digest, ShouldEqual, validImageMeta.Digest)
|
||||
})
|
||||
|
||||
Convey("Set/Get RepoMeta", func() {
|
||||
err := metaDB.SetRepoMeta("repo", mTypes.RepoMeta{
|
||||
Name: "repo",
|
||||
|
||||
+11
-1
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/distribution/v3/registry/storage/driver"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
@@ -143,8 +144,17 @@ func ParseRepo(repo string, metaDB mTypes.MetaDB, storeController stypes.StoreCo
|
||||
continue
|
||||
}
|
||||
|
||||
manifestBlob, _, _, err := imageStore.GetImageManifest(repo, manifest.Digest.String())
|
||||
manifestBlob, err := imageStore.GetBlobContent(repo, manifest.Digest)
|
||||
if err != nil {
|
||||
// Handle missing blobs gracefully - log warning and continue with other manifests
|
||||
var pathNotFoundErr driver.PathNotFoundError
|
||||
if errors.Is(err, zerr.ErrBlobNotFound) || errors.As(err, &pathNotFoundErr) {
|
||||
log.Warn().Err(err).Str("repository", repo).Str("digest", manifest.Digest.String()).
|
||||
Msg("skipping missing manifest blob, continuing repo parse")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
log.Error().Err(err).Str("repository", repo).Str("digest", manifest.Digest.String()).
|
||||
Msg("failed to get blob for image")
|
||||
|
||||
|
||||
+70
-4
@@ -16,6 +16,7 @@ import (
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
zerr "zotregistry.dev/zot/v2/errors"
|
||||
rediscfg "zotregistry.dev/zot/v2/pkg/api/config/redis"
|
||||
zcommon "zotregistry.dev/zot/v2/pkg/common"
|
||||
"zotregistry.dev/zot/v2/pkg/extensions/monitoring"
|
||||
@@ -165,13 +166,14 @@ func TestParseStorageErrors(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("imageStore.GetImageManifest errors", func() {
|
||||
Convey("imageStore.GetBlobContent non-missing error", func() {
|
||||
manifestDigest := godigest.FromString("digest")
|
||||
imageStore.GetIndexContentFn = func(repo string) ([]byte, error) {
|
||||
return getIndexBlob(ispec.Index{
|
||||
Manifests: []ispec.Descriptor{
|
||||
{
|
||||
MediaType: ispec.MediaTypeImageManifest,
|
||||
Digest: godigest.FromString("digest"),
|
||||
Digest: manifestDigest,
|
||||
Annotations: map[string]string{
|
||||
ispec.AnnotationRefName: "tag",
|
||||
},
|
||||
@@ -179,11 +181,75 @@ func TestParseStorageErrors(t *testing.T) {
|
||||
},
|
||||
}), nil
|
||||
}
|
||||
imageStore.GetImageManifestFn = func(repo, reference string) ([]byte, godigest.Digest, string, error) {
|
||||
return nil, "", "", ErrTestError
|
||||
imageStore.GetBlobContentFn = func(repo string, digest godigest.Digest) ([]byte, error) {
|
||||
// Return a non-missing error (not ErrBlobNotFound or PathNotFoundError)
|
||||
return nil, ErrTestError
|
||||
}
|
||||
err := meta.ParseRepo("repo", metaDB, storeController, log)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, ErrTestError)
|
||||
})
|
||||
|
||||
Convey("imageStore.GetImageManifest missing blob - graceful handling", func() {
|
||||
digest1 := godigest.FromString("digest1")
|
||||
digest2 := godigest.FromString("digest2")
|
||||
imageStore.GetIndexContentFn = func(repo string) ([]byte, error) {
|
||||
return getIndexBlob(ispec.Index{
|
||||
Manifests: []ispec.Descriptor{
|
||||
{
|
||||
MediaType: ispec.MediaTypeImageManifest,
|
||||
Digest: digest1,
|
||||
Annotations: map[string]string{
|
||||
ispec.AnnotationRefName: "tag1",
|
||||
},
|
||||
},
|
||||
{
|
||||
MediaType: ispec.MediaTypeImageManifest,
|
||||
Digest: digest2,
|
||||
Annotations: map[string]string{
|
||||
ispec.AnnotationRefName: "tag2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}), nil
|
||||
}
|
||||
callCount := 0
|
||||
setRepoRefCount := 0
|
||||
// Create a valid image for the second manifest
|
||||
validImage := CreateRandomImage()
|
||||
manifestBlob, _ := json.Marshal(validImage.Manifest)
|
||||
configBlob, _ := json.Marshal(validImage.Config)
|
||||
imageStore.GetBlobContentFn = func(repo string, digest godigest.Digest) ([]byte, error) {
|
||||
callCount++
|
||||
// First manifest is missing, second one succeeds
|
||||
if digest == digest1 {
|
||||
return nil, zerr.ErrBlobNotFound
|
||||
}
|
||||
|
||||
if digest == digest2 {
|
||||
// Return valid manifest for second one
|
||||
return manifestBlob, nil
|
||||
}
|
||||
// Return config blob when requested
|
||||
if digest == validImage.ConfigDescriptor.Digest {
|
||||
return configBlob, nil
|
||||
}
|
||||
|
||||
return nil, zerr.ErrBlobNotFound
|
||||
}
|
||||
metaDB.SetRepoReferenceFn = func(ctx context.Context, repo, reference string, imageMeta mTypes.ImageMeta) error {
|
||||
setRepoRefCount++
|
||||
// Verify it's only called for tag2 (the second manifest)
|
||||
So(reference, ShouldEqual, "tag2")
|
||||
|
||||
return nil
|
||||
}
|
||||
err := meta.ParseRepo("repo", metaDB, storeController, log)
|
||||
So(err, ShouldBeNil)
|
||||
// Should have called GetBlobContent for both manifests (and config)
|
||||
So(callCount, ShouldEqual, 3)
|
||||
// Should have called SetRepoReference only once for the second manifest (first was skipped)
|
||||
So(setRepoRefCount, ShouldEqual, 1)
|
||||
})
|
||||
|
||||
Convey("manifestMetaIsPresent true", func() {
|
||||
|
||||
@@ -2197,6 +2197,11 @@ func (rc *RedisDB) getAllContainedMeta(ctx context.Context, imageIndexData *prot
|
||||
|
||||
imageManifestData, err := rc.getProtoImageMeta(ctx, manifest.Digest)
|
||||
if err != nil {
|
||||
// Skip manifests that don't have MetaDB entries (missing from storage)
|
||||
if errors.Is(err, zerr.ErrImageMetaNotFound) {
|
||||
continue
|
||||
}
|
||||
|
||||
return imageMetaList, manifestDataList, err
|
||||
}
|
||||
|
||||
@@ -2208,6 +2213,8 @@ func (rc *RedisDB) getAllContainedMeta(ctx context.Context, imageIndexData *prot
|
||||
compat.IsCompatibleManifestListMediaType(imageManifestData.MediaType) {
|
||||
partialImageDataList, partialManifestDataList, err := rc.getAllContainedMeta(ctx, imageManifestData)
|
||||
if err != nil {
|
||||
// getAllContainedMeta skips missing items internally, so any error returned
|
||||
// is a real error that should be propagated
|
||||
return imageMetaList, manifestDataList, err
|
||||
}
|
||||
|
||||
|
||||
@@ -857,23 +857,56 @@ func TestWrapperErrors(t *testing.T) {
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("image is index, fail to get manifests", func() {
|
||||
Convey("image is index, missing manifests are skipped gracefully", func() {
|
||||
err := metaDB.SetRepoReference(ctx, "repo", "tag", multiarchImageMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Missing manifests are skipped gracefully, so GetFullImageMeta succeeds
|
||||
// but returns an index with no manifests
|
||||
fullImageMeta, err := metaDB.GetFullImageMeta(ctx, "repo", "tag")
|
||||
So(err, ShouldBeNil)
|
||||
So(len(fullImageMeta.Manifests), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("image is index, corrupted manifest data returns error", func() {
|
||||
// Create a multiarch image with multiple manifests
|
||||
multiarchImage := CreateMultiarchWith().RandomImages(2).Build()
|
||||
multiarchImageMeta := multiarchImage.AsImageMeta()
|
||||
err := metaDB.SetImageMeta(multiarchImageMeta.Digest, multiarchImageMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = metaDB.SetRepoMeta("repo", mTypes.RepoMeta{
|
||||
Name: "repo",
|
||||
Tags: map[mTypes.Tag]mTypes.Descriptor{
|
||||
"tag": {
|
||||
MediaType: ispec.MediaTypeImageIndex,
|
||||
Digest: multiarchImageMeta.Digest.String(),
|
||||
},
|
||||
},
|
||||
})
|
||||
// Store the first manifest normally
|
||||
firstManifest := multiarchImage.Images[0]
|
||||
firstManifestMeta := firstManifest.AsImageMeta()
|
||||
err = metaDB.SetImageMeta(firstManifestMeta.Digest, firstManifestMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
_, err = metaDB.GetFullImageMeta(ctx, "repo", "tag")
|
||||
// Store the second manifest normally first, then corrupt it
|
||||
secondManifest := multiarchImage.Images[1]
|
||||
secondManifestMeta := secondManifest.AsImageMeta()
|
||||
err = metaDB.SetImageMeta(secondManifestMeta.Digest, secondManifestMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
secondManifestDigest := secondManifest.ManifestDescriptor.Digest
|
||||
|
||||
// Corrupt the data for the second manifest by storing invalid protobuf data
|
||||
// This will cause getProtoImageMeta to return an unmarshaling error
|
||||
// which is not ErrImageMetaNotFound, so it will propagate through getAllContainedMeta
|
||||
corruptedData := []byte("invalid protobuf data")
|
||||
|
||||
// Access Redis directly to corrupt the data using the helper function pattern
|
||||
err = setImageMeta(secondManifestDigest, corruptedData, client)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = metaDB.SetRepoReference(ctx, "repo", "tag", multiarchImageMeta)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// GetFullImageMeta should return an error due to corrupted manifest data
|
||||
// The error from getAllContainedMeta should propagate
|
||||
fullImageMeta, err := metaDB.GetFullImageMeta(ctx, "repo", "tag")
|
||||
So(err, ShouldNotBeNil)
|
||||
// Should still return a FullImageMeta object (even with error)
|
||||
So(fullImageMeta, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user