Skip to content

Commit 3c2a9c6

Browse files
committed
fix: gracefully handle manifests missing from storage (prepare for sparse indexes)
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>
1 parent a8a6d3b commit 3c2a9c6

File tree

20 files changed

+1243
-141
lines changed

20 files changed

+1243
-141
lines changed

pkg/extensions/search/cve/scan_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,11 @@ func TestScanGeneratorWithMockedData(t *testing.T) { //nolint: gocyclo
384384
return false, err
385385
}
386386

387+
// If all manifests are missing (e.g., from an index), Manifests will be empty
388+
if len(manifestData.Manifests) == 0 {
389+
return false, nil
390+
}
391+
387392
for _, imageLayer := range manifestData.Manifests[0].Manifest.Layers {
388393
switch imageLayer.MediaType {
389394
case ispec.MediaTypeImageLayerGzip, ispec.MediaTypeImageLayer, string(regTypes.DockerLayer):

pkg/extensions/search/search_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7406,6 +7406,98 @@ type repoRef struct {
74067406
Tag string
74077407
}
74087408

7409+
func TestSearchWithMissingManifest(t *testing.T) {
7410+
Convey("Search with missing manifest", t, func() {
7411+
dir := t.TempDir()
7412+
7413+
// 1. Write the image to the disk
7414+
log := log.NewTestLogger()
7415+
storeCtlr := ociutils.GetDefaultStoreController(dir, log)
7416+
7417+
// Create a multiarch image with exactly 2 manifests
7418+
multiarchImage := CreateMultiarchWith().RandomImages(2).Build()
7419+
7420+
// Write the multiarch image to filesystem
7421+
err := WriteMultiArchImageToFileSystem(multiarchImage, "testrepo", "latest", storeCtlr)
7422+
So(err, ShouldBeNil)
7423+
7424+
// Get the image store to access index and manifests
7425+
imageStore := storeCtlr.GetDefaultImageStore()
7426+
7427+
// Get the index content to find all manifest digests
7428+
indexBlob, err := imageStore.GetIndexContent("testrepo")
7429+
So(err, ShouldBeNil)
7430+
7431+
var indexContent ispec.Index
7432+
err = json.Unmarshal(indexBlob, &indexContent)
7433+
So(err, ShouldBeNil)
7434+
7435+
So(len(indexContent.Manifests), ShouldBeGreaterThanOrEqualTo, 2)
7436+
7437+
// Get the first manifest digest to delete
7438+
firstManifestDigest := indexContent.Manifests[0].Digest
7439+
7440+
// Get the second manifest digest (should remain valid)
7441+
secondManifestDigest := indexContent.Manifests[1].Digest
7442+
7443+
// 2. Delete the manifest from the disk
7444+
manifestBlobPath := path.Join(dir, "testrepo", "blobs", "sha256", firstManifestDigest.Encoded())
7445+
err = os.Remove(manifestBlobPath)
7446+
So(err, ShouldBeNil)
7447+
7448+
// 3. Start the controller (MetaDB parsing would be done in the background)
7449+
port := GetFreePort()
7450+
baseURL := GetBaseURL(port)
7451+
conf := config.New()
7452+
conf.HTTP.Port = port
7453+
conf.Storage.RootDirectory = dir
7454+
defaultVal := true
7455+
conf.Extensions = &extconf.ExtensionConfig{
7456+
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
7457+
}
7458+
7459+
conf.Extensions.Search.CVE = nil
7460+
7461+
ctlr := api.NewController(conf)
7462+
7463+
ctlrManager := NewControllerManager(ctlr)
7464+
ctlrManager.StartAndWait(port)
7465+
defer ctlrManager.StopServer()
7466+
7467+
// Search for the repository
7468+
query := `
7469+
{
7470+
GlobalSearch(query:"testrepo:latest"){
7471+
Images {
7472+
RepoName Tag
7473+
Manifests {
7474+
Digest
7475+
}
7476+
}
7477+
}
7478+
}`
7479+
7480+
resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query))
7481+
So(resp, ShouldNotBeNil)
7482+
So(err, ShouldBeNil)
7483+
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
7484+
7485+
responseStruct := &zcommon.GlobalSearchResultResp{}
7486+
err = json.Unmarshal(resp.Body(), responseStruct)
7487+
So(err, ShouldBeNil)
7488+
7489+
// Verify we found the image
7490+
So(responseStruct.GlobalSearchResult.GlobalSearch.Images, ShouldNotBeEmpty)
7491+
foundImage := responseStruct.GlobalSearchResult.GlobalSearch.Images[0]
7492+
So(foundImage.RepoName, ShouldEqual, "testrepo")
7493+
So(foundImage.Tag, ShouldEqual, "latest")
7494+
7495+
// Verify only the valid manifest is found in search results (missing one was skipped by ParseStorage)
7496+
So(len(foundImage.Manifests), ShouldEqual, 1)
7497+
So(foundImage.Manifests[0].Digest, ShouldEqual, secondManifestDigest.String())
7498+
})
7499+
}
7500+
74097501
func deleteUsedImages(repoTags []repoRef, baseURL string) {
74107502
for _, image := range repoTags {
74117503
status, err := DeleteImage(image.Repo, image.Tag, baseURL)

pkg/extensions/sync/destination.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"path"
1313
"strings"
1414

15+
"github.com/distribution/distribution/v3/registry/storage/driver"
1516
godigest "github.com/opencontainers/go-digest"
1617
ispec "github.com/opencontainers/image-spec/specs-go/v1"
1718
"github.com/regclient/regclient/types/mediatype"
@@ -227,11 +228,27 @@ func (registry *DestinationRegistry) copyManifest(repo string, desc ispec.Descri
227228
return err
228229
}
229230

231+
var firstMissingErr error
232+
230233
for _, manifest := range indexManifest.Manifests {
231234
reference := GetDescriptorReference(manifest)
232235

233236
manifestBuf, err := tempImageStore.GetBlobContent(repo, manifest.Digest)
234237
if err != nil {
238+
// Handle missing manifest blobs gracefully - log warning and continue with other manifests
239+
var pathNotFoundErr driver.PathNotFoundError
240+
if errors.Is(err, zerr.ErrBlobNotFound) || errors.As(err, &pathNotFoundErr) {
241+
if firstMissingErr == nil {
242+
firstMissingErr = err
243+
}
244+
245+
registry.log.Warn().Err(err).Str("dir", path.Join(tempImageStore.RootDir(), repo)).
246+
Str("digest", manifest.Digest.String()).
247+
Msg("skipping missing manifest blob in image index, continuing sync with other manifests")
248+
249+
continue
250+
}
251+
235252
registry.log.Error().Str("errorType", common.TypeOf(err)).
236253
Err(err).Str("dir", path.Join(tempImageStore.RootDir(), repo)).Str("digest", manifest.Digest.String()).
237254
Msg("failed find manifest which is part of an image index")
@@ -254,6 +271,11 @@ func (registry *DestinationRegistry) copyManifest(repo string, desc ispec.Descri
254271
}
255272
}
256273

274+
// Return error if we encountered any missing manifests
275+
if firstMissingErr != nil {
276+
return firstMissingErr
277+
}
278+
257279
_, _, err := imageStore.PutImageManifest(repo, reference, desc.MediaType, manifestContent)
258280
if err != nil {
259281
registry.log.Error().Str("errorType", common.TypeOf(err)).Str("repo", repo).Str("reference", reference).

pkg/meta/boltdb/boltdb.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,11 @@ func getAllContainedMeta(imageBuck *bbolt.Bucket, imageIndexData *proto_go.Image
500500

501501
imageManifestData, err := getProtoImageMeta(imageBuck, manifest.Digest)
502502
if err != nil {
503+
// Skip manifests that don't have MetaDB entries (missing from storage)
504+
if errors.Is(err, zerr.ErrImageMetaNotFound) {
505+
continue
506+
}
507+
503508
return imageMetaList, manifestDataList, err
504509
}
505510

@@ -511,6 +516,8 @@ func getAllContainedMeta(imageBuck *bbolt.Bucket, imageIndexData *proto_go.Image
511516
compat.IsCompatibleManifestListMediaType(imageManifestData.MediaType) {
512517
partialImageDataList, partialManifestDataList, err := getAllContainedMeta(imageBuck, imageManifestData)
513518
if err != nil {
519+
// getAllContainedMeta skips missing items internally, so any error returned
520+
// is a real error that should be propagated
514521
return imageMetaList, manifestDataList, err
515522
}
516523

pkg/meta/boltdb/boltdb_test.go

Lines changed: 148 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"crypto/rand"
66
"encoding/base64"
7+
"errors"
78
"math"
89
"testing"
910
"time"
@@ -32,6 +33,8 @@ func (its imgTrustStore) VerifySignature(
3233
return "", time.Time{}, false, nil
3334
}
3435

36+
var errImageMetaBucketNotFound = errors.New("ImageMeta bucket not found")
37+
3538
func TestWrapperErrors(t *testing.T) {
3639
image := CreateDefaultImage()
3740
imageMeta := image.AsImageMeta()
@@ -302,23 +305,63 @@ func TestWrapperErrors(t *testing.T) {
302305
So(err, ShouldNotBeNil)
303306
})
304307

305-
Convey("image is index, fail to get manifests", func() {
308+
Convey("image is index, missing manifests are skipped gracefully", func() {
309+
err := boltdbWrapper.SetRepoReference(ctx, "repo", "tag", multiarchImageMeta)
310+
So(err, ShouldBeNil)
311+
312+
// Missing manifests are skipped gracefully, so GetFullImageMeta succeeds
313+
// but returns an index with no manifests
314+
fullImageMeta, err := boltdbWrapper.GetFullImageMeta(ctx, "repo", "tag")
315+
So(err, ShouldBeNil)
316+
So(len(fullImageMeta.Manifests), ShouldEqual, 0)
317+
})
318+
319+
Convey("image is index, corrupted manifest data returns error", func() {
320+
// Create a multiarch image with multiple manifests
321+
multiarchImage := CreateMultiarchWith().RandomImages(2).Build()
322+
multiarchImageMeta := multiarchImage.AsImageMeta()
306323
err := boltdbWrapper.SetImageMeta(multiarchImageMeta.Digest, multiarchImageMeta)
307324
So(err, ShouldBeNil)
308325

309-
err = boltdbWrapper.SetRepoMeta("repo", mTypes.RepoMeta{
310-
Name: "repo",
311-
Tags: map[mTypes.Tag]mTypes.Descriptor{
312-
"tag": {
313-
MediaType: ispec.MediaTypeImageIndex,
314-
Digest: multiarchImageMeta.Digest.String(),
315-
},
316-
},
326+
// Store the first manifest normally
327+
firstManifest := multiarchImage.Images[0]
328+
firstManifestMeta := firstManifest.AsImageMeta()
329+
err = boltdbWrapper.SetImageMeta(firstManifestMeta.Digest, firstManifestMeta)
330+
So(err, ShouldBeNil)
331+
332+
// Store the second manifest normally first, then corrupt it
333+
secondManifest := multiarchImage.Images[1]
334+
secondManifestMeta := secondManifest.AsImageMeta()
335+
err = boltdbWrapper.SetImageMeta(secondManifestMeta.Digest, secondManifestMeta)
336+
So(err, ShouldBeNil)
337+
338+
secondManifestDigest := secondManifest.ManifestDescriptor.Digest
339+
340+
// Corrupt the data for the second manifest by storing invalid protobuf data
341+
// This will cause getProtoImageMeta to return an unmarshaling error
342+
// which is not ErrImageMetaNotFound, so it will propagate through getAllContainedMeta
343+
corruptedData := []byte("invalid protobuf data")
344+
345+
// Access BoltDB directly to corrupt the data
346+
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
347+
imageBuck := tx.Bucket([]byte(boltdb.ImageMetaBuck))
348+
if imageBuck == nil {
349+
return errImageMetaBucketNotFound
350+
}
351+
// Store corrupted protobuf data
352+
return imageBuck.Put([]byte(secondManifestDigest.String()), corruptedData)
317353
})
318354
So(err, ShouldBeNil)
319355

320-
_, err = boltdbWrapper.GetFullImageMeta(ctx, "repo", "tag")
356+
err = boltdbWrapper.SetRepoReference(ctx, "repo", "tag", multiarchImageMeta)
357+
So(err, ShouldBeNil)
358+
359+
// GetFullImageMeta should return an error due to corrupted manifest data
360+
// The error from getAllContainedMeta should propagate
361+
fullImageMeta, err := boltdbWrapper.GetFullImageMeta(ctx, "repo", "tag")
321362
So(err, ShouldNotBeNil)
363+
// Should still return a FullImageMeta object (even with error)
364+
So(fullImageMeta, ShouldNotBeNil)
322365
})
323366
})
324367

@@ -443,6 +486,54 @@ func TestWrapperErrors(t *testing.T) {
443486
_, err = boltdbWrapper.FilterTags(ctx, mTypes.AcceptAllRepoTag, mTypes.AcceptAllImageMeta)
444487
So(err, ShouldBeNil)
445488
})
489+
490+
Convey("getAllContainedMeta error for index is joined and processing continues", func() {
491+
// Create a multiarch image with multiple manifests
492+
multiarchImage := CreateMultiarchWith().RandomImages(2).Build()
493+
multiarchImageMeta := multiarchImage.AsImageMeta()
494+
err := boltdbWrapper.SetImageMeta(multiarchImageMeta.Digest, multiarchImageMeta)
495+
So(err, ShouldBeNil)
496+
497+
// Store the first manifest normally
498+
firstManifest := multiarchImage.Images[0]
499+
firstManifestMeta := firstManifest.AsImageMeta()
500+
err = boltdbWrapper.SetImageMeta(firstManifestMeta.Digest, firstManifestMeta)
501+
So(err, ShouldBeNil)
502+
503+
// Store the second manifest normally first, then corrupt it
504+
secondManifest := multiarchImage.Images[1]
505+
secondManifestMeta := secondManifest.AsImageMeta()
506+
err = boltdbWrapper.SetImageMeta(secondManifestMeta.Digest, secondManifestMeta)
507+
So(err, ShouldBeNil)
508+
509+
secondManifestDigest := secondManifest.ManifestDescriptor.Digest
510+
511+
// Corrupt the data for the second manifest by storing invalid protobuf data
512+
// This will cause getProtoImageMeta to return an unmarshaling error
513+
// which is not ErrImageMetaNotFound, so it will propagate through getAllContainedMeta
514+
corruptedData := []byte("invalid protobuf data")
515+
516+
// Access BoltDB directly to corrupt the data
517+
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
518+
imageBuck := tx.Bucket([]byte(boltdb.ImageMetaBuck))
519+
if imageBuck == nil {
520+
return errImageMetaBucketNotFound
521+
}
522+
// Store corrupted protobuf data
523+
return imageBuck.Put([]byte(secondManifestDigest.String()), corruptedData)
524+
})
525+
So(err, ShouldBeNil)
526+
527+
err = boltdbWrapper.SetRepoReference(ctx, "repo", "tag", multiarchImageMeta)
528+
So(err, ShouldBeNil)
529+
530+
// FilterTags should return an error due to corrupted manifest data
531+
// The error from getAllContainedMeta should be joined with viewError
532+
images, err := boltdbWrapper.FilterTags(ctx, mTypes.AcceptAllRepoTag, mTypes.AcceptAllImageMeta)
533+
So(err, ShouldNotBeNil)
534+
// Should still return some images (the first valid manifest might be processed)
535+
So(images, ShouldNotBeNil)
536+
})
446537
})
447538

448539
Convey("SearchRepos", func() {
@@ -470,6 +561,53 @@ func TestWrapperErrors(t *testing.T) {
470561
})
471562
})
472563

564+
Convey("GetImageMeta", func() {
565+
Convey("image is index, getAllContainedMeta error returns error", func() {
566+
// Create a multiarch image with multiple manifests
567+
multiarchImage := CreateMultiarchWith().RandomImages(2).Build()
568+
multiarchImageMeta := multiarchImage.AsImageMeta()
569+
err := boltdbWrapper.SetImageMeta(multiarchImageMeta.Digest, multiarchImageMeta)
570+
So(err, ShouldBeNil)
571+
572+
// Store the first manifest normally
573+
firstManifest := multiarchImage.Images[0]
574+
firstManifestMeta := firstManifest.AsImageMeta()
575+
err = boltdbWrapper.SetImageMeta(firstManifestMeta.Digest, firstManifestMeta)
576+
So(err, ShouldBeNil)
577+
578+
// Store the second manifest normally first, then corrupt it
579+
secondManifest := multiarchImage.Images[1]
580+
secondManifestMeta := secondManifest.AsImageMeta()
581+
err = boltdbWrapper.SetImageMeta(secondManifestMeta.Digest, secondManifestMeta)
582+
So(err, ShouldBeNil)
583+
584+
secondManifestDigest := secondManifest.ManifestDescriptor.Digest
585+
586+
// Corrupt the data for the second manifest by storing invalid protobuf data
587+
// This will cause getProtoImageMeta to return an unmarshaling error
588+
// which is not ErrImageMetaNotFound, so it will propagate through getAllContainedMeta
589+
corruptedData := []byte("invalid protobuf data")
590+
591+
// Access BoltDB directly to corrupt the data
592+
err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error {
593+
imageBuck := tx.Bucket([]byte(boltdb.ImageMetaBuck))
594+
if imageBuck == nil {
595+
return errImageMetaBucketNotFound
596+
}
597+
// Store corrupted protobuf data
598+
return imageBuck.Put([]byte(secondManifestDigest.String()), corruptedData)
599+
})
600+
So(err, ShouldBeNil)
601+
602+
// GetImageMeta should return an error due to corrupted manifest data
603+
// The error from getAllContainedMeta should propagate
604+
imageMeta, err := boltdbWrapper.GetImageMeta(multiarchImageMeta.Digest)
605+
So(err, ShouldNotBeNil)
606+
// Should still return an ImageMeta object (even with error)
607+
So(imageMeta, ShouldNotBeNil)
608+
})
609+
})
610+
473611
Convey("SetRepoReference", func() {
474612
Convey("getProtoRepoMeta errors", func() {
475613
err := setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB)

0 commit comments

Comments
 (0)