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:
Andrei Aaron
2025-11-13 19:26:18 +02:00
committed by GitHub
parent 2b6fba7059
commit 008527b7bb
20 changed files with 1240 additions and 138 deletions
+7
View File
@@ -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
View File
@@ -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)
+45 -30
View File
@@ -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
+12 -6
View File
@@ -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)
})
})
+69
View File
@@ -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
View File
@@ -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
View File
@@ -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() {
+7
View File
@@ -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
}
+44 -11
View File
@@ -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)
})
})