0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00

fix(loadrepodb): statistics are now preserved after reloading zot (#1289)

- before, the download count for a manifest and repo star count were lost after reload

- now we are keeping these values when we reset the repo-meta structure

Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
This commit is contained in:
LaurentiuNiculae 2023-03-23 20:08:11 +02:00 committed by GitHub
parent 906f8ce621
commit 91e14bee00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 172 additions and 281 deletions

View file

@ -492,9 +492,17 @@ func (bdw *DBWrapper) SetRepoReference(repo string, reference string, manifestDi
}
}
repoMeta.Statistics[manifestDigest.String()] = repodb.DescriptorStatistics{DownloadCount: 0}
repoMeta.Signatures[manifestDigest.String()] = repodb.ManifestSignatures{}
repoMeta.Referrers[manifestDigest.String()] = []repodb.ReferrerInfo{}
if _, ok := repoMeta.Statistics[manifestDigest.String()]; !ok {
repoMeta.Statistics[manifestDigest.String()] = repodb.DescriptorStatistics{DownloadCount: 0}
}
if _, ok := repoMeta.Signatures[manifestDigest.String()]; !ok {
repoMeta.Signatures[manifestDigest.String()] = repodb.ManifestSignatures{}
}
if _, ok := repoMeta.Referrers[manifestDigest.String()]; !ok {
repoMeta.Referrers[manifestDigest.String()] = []repodb.ReferrerInfo{}
}
repoMetaBlob, err = json.Marshal(repoMeta)
if err != nil {
@ -532,6 +540,23 @@ func (bdw *DBWrapper) GetRepoMeta(repo string) (repodb.RepoMetadata, error) {
return repoMeta, err
}
func (bdw *DBWrapper) SetRepoMeta(repo string, repoMeta repodb.RepoMetadata) error {
err := bdw.DB.Update(func(tx *bolt.Tx) error {
buck := tx.Bucket([]byte(repodb.RepoMetadataBucket))
repoMeta.Name = repo
repoMetaBlob, err := json.Marshal(repoMeta)
if err != nil {
return err
}
return buck.Put([]byte(repo), repoMetaBlob)
})
return err
}
func (bdw *DBWrapper) DeleteRepoTag(repo string, tag string) error {
err := bdw.DB.Update(func(tx *bolt.Tx) error {
buck := tx.Bucket([]byte(repodb.RepoMetadataBucket))
@ -553,10 +578,6 @@ func (bdw *DBWrapper) DeleteRepoTag(repo string, tag string) error {
delete(repoMeta.Tags, tag)
if len(repoMeta.Tags) == 0 {
return buck.Delete([]byte(repo))
}
repoMetaBlob, err = json.Marshal(repoMeta)
if err != nil {
return err

View file

@ -182,7 +182,7 @@ func (dwr *DBWrapper) SetManifestMeta(repo string, manifestDigest godigest.Diges
updatedRepoMeta := common.UpdateManifestMeta(repoMeta, manifestDigest, manifestMeta)
err = dwr.setRepoMeta(repo, updatedRepoMeta)
err = dwr.SetRepoMeta(repo, updatedRepoMeta)
if err != nil {
return err
}
@ -235,7 +235,7 @@ func (dwr *DBWrapper) IncrementRepoStars(repo string) error {
repoMeta.Stars++
err = dwr.setRepoMeta(repo, repoMeta)
err = dwr.SetRepoMeta(repo, repoMeta)
return err
}
@ -250,7 +250,7 @@ func (dwr *DBWrapper) DecrementRepoStars(repo string) error {
repoMeta.Stars--
}
err = dwr.setRepoMeta(repo, repoMeta)
err = dwr.SetRepoMeta(repo, repoMeta)
return err
}
@ -406,7 +406,7 @@ func (dwr DBWrapper) SetReferrer(repo string, referredDigest godigest.Digest, re
repoMeta.Referrers[referredDigest.String()] = refferers
return dwr.setRepoMeta(repo, repoMeta)
return dwr.SetRepoMeta(repo, repoMeta)
}
func (dwr DBWrapper) GetReferrers(repo string, referredDigest godigest.Digest) ([]repodb.ReferrerInfo, error) {
@ -478,7 +478,7 @@ func (dwr DBWrapper) DeleteReferrer(repo string, referredDigest godigest.Digest,
repoMeta.Referrers[referredDigest.String()] = referrers
return dwr.setRepoMeta(repo, repoMeta)
return dwr.SetRepoMeta(repo, repoMeta)
}
func (dwr DBWrapper) GetReferrersInfo(repo string, referredDigest godigest.Digest,
@ -541,11 +541,19 @@ func (dwr *DBWrapper) SetRepoReference(repo string, reference string, manifestDi
}
}
repoMeta.Statistics[manifestDigest.String()] = repodb.DescriptorStatistics{DownloadCount: 0}
repoMeta.Signatures[manifestDigest.String()] = repodb.ManifestSignatures{}
repoMeta.Referrers[manifestDigest.String()] = []repodb.ReferrerInfo{}
if _, ok := repoMeta.Statistics[manifestDigest.String()]; !ok {
repoMeta.Statistics[manifestDigest.String()] = repodb.DescriptorStatistics{DownloadCount: 0}
}
err = dwr.setRepoMeta(repo, repoMeta)
if _, ok := repoMeta.Signatures[manifestDigest.String()]; !ok {
repoMeta.Signatures[manifestDigest.String()] = repodb.ManifestSignatures{}
}
if _, ok := repoMeta.Referrers[manifestDigest.String()]; !ok {
repoMeta.Referrers[manifestDigest.String()] = []repodb.ReferrerInfo{}
}
err = dwr.SetRepoMeta(repo, repoMeta)
return err
}
@ -574,17 +582,6 @@ func (dwr *DBWrapper) DeleteRepoTag(repo string, tag string) error {
delete(repoMeta.Tags, tag)
if len(repoMeta.Tags) == 0 {
_, err := dwr.Client.DeleteItem(context.Background(), &dynamodb.DeleteItemInput{
TableName: aws.String(dwr.RepoMetaTablename),
Key: map[string]types.AttributeValue{
"RepoName": &types.AttributeValueMemberS{Value: repo},
},
})
return err
}
repoAttributeValue, err := attributevalue.Marshal(repoMeta)
if err != nil {
return err
@ -657,7 +654,7 @@ func (dwr *DBWrapper) IncrementImageDownloads(repo string, reference string) err
manifestStatistics.DownloadCount++
repoMeta.Statistics[descriptorDigest] = manifestStatistics
return dwr.setRepoMeta(repo, repoMeta)
return dwr.SetRepoMeta(repo, repoMeta)
}
func (dwr *DBWrapper) AddManifestSignature(repo string, signedManifestDigest godigest.Digest,
@ -696,7 +693,7 @@ func (dwr *DBWrapper) AddManifestSignature(repo string, signedManifestDigest god
repoMeta.Signatures[signedManifestDigest.String()] = manifestSignatures
err = dwr.setRepoMeta(repoMeta.Name, repoMeta)
err = dwr.SetRepoMeta(repoMeta.Name, repoMeta)
return err
}
@ -734,7 +731,7 @@ func (dwr *DBWrapper) DeleteSignature(repo string, signedManifestDigest godigest
repoMeta.Signatures[signedManifestDigest.String()] = manifestSignatures
err = dwr.setRepoMeta(repoMeta.Name, repoMeta)
err = dwr.SetRepoMeta(repoMeta.Name, repoMeta)
return err
}
@ -1453,7 +1450,9 @@ func (dwr *DBWrapper) PatchDB() error {
return nil
}
func (dwr *DBWrapper) setRepoMeta(repo string, repoMeta repodb.RepoMetadata) error {
func (dwr *DBWrapper) SetRepoMeta(repo string, repoMeta repodb.RepoMetadata) error {
repoMeta.Name = repo
repoAttributeValue, err := attributevalue.Marshal(repoMeta)
if err != nil {
return err

View file

@ -45,6 +45,9 @@ type RepoDB interface { //nolint:interfacebloat
// GetRepoMeta returns RepoMetadata of a repo from the database
GetRepoMeta(repo string) (RepoMetadata, error)
// GetRepoMeta returns RepoMetadata of a repo from the database
SetRepoMeta(repo string, repoMeta RepoMetadata) error
// GetMultipleRepoMeta returns information about all repositories as map[string]RepoMetadata filtered by the filter
// function
GetMultipleRepoMeta(ctx context.Context, filter func(repoMeta RepoMetadata) bool, requestedPage PageInput) (

View file

@ -198,6 +198,18 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) {
So(err, ShouldBeNil)
So(repoMeta.Name, ShouldResemble, repo1)
So(repoMeta.Tags[tag1].Digest, ShouldEqual, manifestDigest1)
err = repoDB.SetRepoMeta(repo2, repodb.RepoMetadata{Tags: map[string]repodb.Descriptor{
tag2: {
Digest: manifestDigest2.String(),
},
}})
So(err, ShouldBeNil)
repoMeta, err = repoDB.GetRepoMeta(repo2)
So(err, ShouldBeNil)
So(repoMeta.Name, ShouldResemble, repo2)
So(repoMeta.Tags[tag2].Digest, ShouldEqual, manifestDigest2)
})
Convey("Setting a good repo using a digest", func() {
@ -325,17 +337,6 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) {
So(repoMeta.Tags[tag2].Digest, ShouldResemble, manifestDigest2.String())
})
Convey("Delete all tags from repo", func() {
err := repoDB.DeleteRepoTag(repo, tag1)
So(err, ShouldBeNil)
err = repoDB.DeleteRepoTag(repo, tag2)
So(err, ShouldBeNil)
repoMeta, err := repoDB.GetRepoMeta(repo)
So(err, ShouldNotBeNil)
So(repoMeta, ShouldBeZeroValue)
})
Convey("Delete inexistent tag from repo", func() {
err := repoDB.DeleteRepoTag(repo, "InexistentTag")
So(err, ShouldBeNil)

View file

@ -175,17 +175,14 @@ func resetRepoMetaTags(repo string, repoDB RepoDB, log log.Logger) error {
return nil
}
for tag := range repoMeta.Tags {
// We should have a way to delete all tags at once
err := repoDB.DeleteRepoTag(repo, tag)
if err != nil {
log.Error().Err(err).Msgf("load-repo: failed to delete tag %s from RepoMeta for repo %s", tag, repo)
return err
}
}
return nil
return repoDB.SetRepoMeta(repo, RepoMetadata{
Name: repoMeta.Name,
Tags: map[string]Descriptor{},
Statistics: repoMeta.Statistics,
Signatures: map[string]ManifestSignatures{},
Referrers: map[string][]ReferrerInfo{},
Stars: repoMeta.Stars,
})
}
func getAllRepos(storeController storage.StoreController) ([]string, error) {

View file

@ -31,8 +31,8 @@ const repo = "repo"
var ErrTestError = errors.New("test error")
func TestLoadOCILayoutErrors(t *testing.T) {
Convey("LoadOCILayout", t, func() {
func TestParseStorageErrors(t *testing.T) {
Convey("ParseStorag", t, func() {
imageStore := mocks.MockedImageStore{
GetIndexContentFn: func(repo string) ([]byte, error) {
return nil, ErrTestError
@ -108,25 +108,6 @@ func TestLoadOCILayoutErrors(t *testing.T) {
err := repodb.ParseRepo("repo", repoDB, storeController, log)
So(err, ShouldNotBeNil)
})
Convey("repoDB.DeleteRepoTag errors", func() {
repoDB.GetRepoMetaFn = func(repo string) (repodb.RepoMetadata, error) {
return repodb.RepoMetadata{
Tags: map[string]repodb.Descriptor{
"digest1": {
Digest: "tag1",
MediaType: ispec.MediaTypeImageManifest,
},
},
}, nil
}
repoDB.DeleteRepoTagFn = func(repo, tag string) error {
return ErrTestError
}
err := repodb.ParseRepo("repo", repoDB, storeController, log)
So(err, ShouldNotBeNil)
})
})
Convey("isManifestMetaPresent errors", func() {
@ -282,10 +263,48 @@ func TestLoadOCILayoutErrors(t *testing.T) {
})
}
func TestLoadOCILayoutWithStorage(t *testing.T) {
func TestParseStorageWithStorage(t *testing.T) {
Convey("Boltdb", t, func() {
rootDir := t.TempDir()
repoDB, err := bolt.NewBoltDBWrapper(bolt.DBParameters{
RootDir: rootDir,
})
So(err, ShouldBeNil)
RunParseStorageTests(rootDir, repoDB)
})
}
func TestParseStorageDynamoWrapper(t *testing.T) {
skipIt(t)
Convey("Dynamodb", t, func() {
rootDir := t.TempDir()
dynamoWrapper, err := dynamo.NewDynamoDBWrapper(dynamoParams.DBDriverParameters{
Endpoint: os.Getenv("DYNAMODBMOCK_ENDPOINT"),
Region: "us-east-2",
RepoMetaTablename: "RepoMetadataTable",
ManifestDataTablename: "ManifestDataTable",
IndexDataTablename: "IndexDataTable",
ArtifactDataTablename: "ArtifactDataTable",
VersionTablename: "Version",
})
So(err, ShouldBeNil)
err = dynamoWrapper.ResetManifestDataTable()
So(err, ShouldBeNil)
err = dynamoWrapper.ResetRepoMetaTable()
So(err, ShouldBeNil)
RunParseStorageTests(rootDir, dynamoWrapper)
})
}
func RunParseStorageTests(rootDir string, repoDB repodb.RepoDB) {
Convey("test", func() {
imageStore := local.NewImageStore(rootDir, false, 0, false, false,
log.NewLogger("debug", ""), monitoring.NewMetricsServer(false, log.NewLogger("debug", "")), nil, nil)
@ -355,11 +374,6 @@ func TestLoadOCILayoutWithStorage(t *testing.T) {
err = os.WriteFile(indexPath, buf, 0o600)
So(err, ShouldBeNil)
repoDB, err := bolt.NewBoltDBWrapper(bolt.DBParameters{
RootDir: rootDir,
})
So(err, ShouldBeNil)
err = repodb.ParseStorage(repoDB, storeController, log.NewLogger("debug", ""))
So(err, ShouldBeNil)
@ -379,200 +393,13 @@ func TestLoadOCILayoutWithStorage(t *testing.T) {
So(manifestMeta.ManifestBlob, ShouldNotBeNil)
So(manifestMeta.ConfigBlob, ShouldNotBeNil)
if descriptor.Digest == signedManifestDigest.String() {
So(repos[0].Signatures[descriptor.Digest], ShouldNotBeEmpty)
So(manifestMeta.Signatures["cosign"], ShouldNotBeEmpty)
}
}
})
Convey("Ignore orphan signatures", t, func() {
rootDir := t.TempDir()
imageStore := local.NewImageStore(rootDir, false, 0, false, false,
log.NewLogger("debug", ""), monitoring.NewMetricsServer(false, log.NewLogger("debug", "")), nil, nil)
storeController := storage.StoreController{DefaultStore: imageStore}
// add an image
config, layers, manifest, err := test.GetRandomImageComponents(100)
So(err, ShouldBeNil)
err = test.WriteImageToFileSystem(
test.Image{
Config: config,
Layers: layers,
Manifest: manifest,
Reference: "tag1",
},
repo,
storeController)
So(err, ShouldBeNil)
// add mock cosign signature without pushing the signed image
_, _, manifest, err = test.GetRandomImageComponents(100)
So(err, ShouldBeNil)
signatureTag, err := test.GetCosignSignatureTagForManifest(manifest)
So(err, ShouldBeNil)
// get the body of the signature
config, layers, manifest, err = test.GetRandomImageComponents(100)
So(err, ShouldBeNil)
err = test.WriteImageToFileSystem(
test.Image{
Config: config,
Layers: layers,
Manifest: manifest,
Reference: signatureTag,
},
repo,
storeController)
So(err, ShouldBeNil)
// test that we have only 1 image inside the repo
repoDB, err := bolt.NewBoltDBWrapper(bolt.DBParameters{
RootDir: rootDir,
})
So(err, ShouldBeNil)
err = repodb.ParseStorage(repoDB, storeController, log.NewLogger("debug", ""))
So(err, ShouldBeNil)
repos, err := repoDB.GetMultipleRepoMeta(
context.Background(),
func(repoMeta repodb.RepoMetadata) bool { return true },
repodb.PageInput{},
)
So(err, ShouldBeNil)
So(len(repos), ShouldEqual, 1)
So(repos[0].Tags, ShouldContainKey, "tag1")
So(repos[0].Tags, ShouldNotContainKey, signatureTag)
})
}
func TestLoadOCILayoutDynamoWrapper(t *testing.T) {
skipIt(t)
Convey("Dynamodb", t, func() {
rootDir := t.TempDir()
imageStore := local.NewImageStore(rootDir, false, 0, false, false,
log.NewLogger("debug", ""), monitoring.NewMetricsServer(false, log.NewLogger("debug", "")), nil, nil)
storeController := storage.StoreController{DefaultStore: imageStore}
manifests := []ispec.Manifest{}
for i := 0; i < 3; i++ {
config, layers, manifest, err := test.GetRandomImageComponents(100)
So(err, ShouldBeNil)
manifests = append(manifests, manifest)
err = test.WriteImageToFileSystem(
test.Image{
Config: config,
Layers: layers,
Manifest: manifest,
Reference: fmt.Sprintf("tag%d", i),
},
repo,
storeController)
So(err, ShouldBeNil)
}
// add fake signature for tag1
signatureTag, err := test.GetCosignSignatureTagForManifest(manifests[1])
So(err, ShouldBeNil)
manifestBlob, err := json.Marshal(manifests[1])
So(err, ShouldBeNil)
signedManifestDigest := godigest.FromBytes(manifestBlob)
config, layers, manifest, err := test.GetRandomImageComponents(100)
So(err, ShouldBeNil)
err = test.WriteImageToFileSystem(
test.Image{
Config: config,
Layers: layers,
Manifest: manifest,
Reference: signatureTag,
},
repo,
storeController)
So(err, ShouldBeNil)
// remove tag2 from index.json
indexPath := path.Join(rootDir, repo, "index.json")
indexFile, err := os.Open(indexPath)
So(err, ShouldBeNil)
buf, err := io.ReadAll(indexFile)
So(err, ShouldBeNil)
var index ispec.Index
if err = json.Unmarshal(buf, &index); err == nil {
for _, manifest := range index.Manifests {
if val, ok := manifest.Annotations[ispec.AnnotationRefName]; ok && val == "tag2" {
delete(manifest.Annotations, ispec.AnnotationRefName)
break
}
}
}
buf, err = json.Marshal(index)
So(err, ShouldBeNil)
err = os.WriteFile(indexPath, buf, 0o600)
So(err, ShouldBeNil)
dynamoWrapper, err := dynamo.NewDynamoDBWrapper(dynamoParams.DBDriverParameters{
Endpoint: os.Getenv("DYNAMODBMOCK_ENDPOINT"),
Region: "us-east-2",
RepoMetaTablename: "RepoMetadataTable",
ManifestDataTablename: "ManifestDataTable",
IndexDataTablename: "IndexDataTable",
ArtifactDataTablename: "ArtifactDataTable",
VersionTablename: "Version",
})
So(err, ShouldBeNil)
err = dynamoWrapper.ResetManifestDataTable()
So(err, ShouldBeNil)
err = dynamoWrapper.ResetRepoMetaTable()
So(err, ShouldBeNil)
err = repodb.ParseStorage(dynamoWrapper, storeController, log.NewLogger("debug", ""))
So(err, ShouldBeNil)
repos, err := dynamoWrapper.GetMultipleRepoMeta(
context.Background(),
func(repoMeta repodb.RepoMetadata) bool { return true },
repodb.PageInput{},
)
t.Logf("%#v", repos)
So(err, ShouldBeNil)
So(len(repos), ShouldEqual, 1)
So(len(repos[0].Tags), ShouldEqual, 2)
for _, descriptor := range repos[0].Tags {
manifestMeta, err := dynamoWrapper.GetManifestMeta(repo, godigest.Digest(descriptor.Digest))
So(err, ShouldBeNil)
So(manifestMeta.ManifestBlob, ShouldNotBeNil)
So(manifestMeta.ConfigBlob, ShouldNotBeNil)
if descriptor.Digest == signedManifestDigest.String() {
So(manifestMeta.Signatures, ShouldNotBeEmpty)
}
}
})
Convey("Ignore orphan signatures", t, func() {
rootDir := t.TempDir()
Convey("Ignore orphan signatures", func() {
imageStore := local.NewImageStore(rootDir, false, 0, false, false,
log.NewLogger("debug", ""), monitoring.NewMetricsServer(false, log.NewLogger("debug", "")), nil, nil)
@ -614,18 +441,6 @@ func TestLoadOCILayoutDynamoWrapper(t *testing.T) {
storeController)
So(err, ShouldBeNil)
// test that we have only 1 image inside the repo
repoDB, err := dynamo.NewDynamoDBWrapper(dynamoParams.DBDriverParameters{
Endpoint: os.Getenv("DYNAMODBMOCK_ENDPOINT"),
Region: "us-east-2",
RepoMetaTablename: "RepoMetadataTable",
ManifestDataTablename: "ManifestDataTable",
ArtifactDataTablename: "ArtifactDataTable",
IndexDataTablename: "IndexDataTable",
VersionTablename: "Version",
})
So(err, ShouldBeNil)
err = repodb.ParseStorage(repoDB, storeController, log.NewLogger("debug", ""))
So(err, ShouldBeNil)
@ -635,12 +450,57 @@ func TestLoadOCILayoutDynamoWrapper(t *testing.T) {
repodb.PageInput{},
)
So(err, ShouldBeNil)
t.Logf("%#v", repos)
So(len(repos), ShouldEqual, 1)
So(repos[0].Tags, ShouldContainKey, "tag1")
So(repos[0].Tags, ShouldNotContainKey, signatureTag)
})
Convey("Check statistics after load", func() {
imageStore := local.NewImageStore(rootDir, false, 0, false, false,
log.NewLogger("debug", ""), monitoring.NewMetricsServer(false, log.NewLogger("debug", "")), nil, nil)
storeController := storage.StoreController{DefaultStore: imageStore}
// add an image
image, err := test.GetRandomImage("tag")
So(err, ShouldBeNil)
manifestDigest, err := image.Digest()
So(err, ShouldBeNil)
err = test.WriteImageToFileSystem(
image,
repo,
storeController)
So(err, ShouldBeNil)
err = repoDB.SetRepoReference(repo, "tag", manifestDigest, ispec.MediaTypeImageManifest)
So(err, ShouldBeNil)
err = repoDB.IncrementRepoStars(repo)
So(err, ShouldBeNil)
err = repoDB.IncrementImageDownloads(repo, "tag")
So(err, ShouldBeNil)
err = repoDB.IncrementImageDownloads(repo, "tag")
So(err, ShouldBeNil)
err = repoDB.IncrementImageDownloads(repo, "tag")
So(err, ShouldBeNil)
repoMeta, err := repoDB.GetRepoMeta(repo)
So(err, ShouldBeNil)
So(repoMeta.Statistics[manifestDigest.String()].DownloadCount, ShouldEqual, 3)
So(repoMeta.Stars, ShouldEqual, 1)
err = repodb.ParseStorage(repoDB, storeController, log.NewLogger("debug", ""))
So(err, ShouldBeNil)
repoMeta, err = repoDB.GetRepoMeta(repo)
So(err, ShouldBeNil)
So(repoMeta.Statistics[manifestDigest.String()].DownloadCount, ShouldEqual, 3)
So(repoMeta.Stars, ShouldEqual, 1)
})
}
func TestGetReferredSubject(t *testing.T) {

View file

@ -25,6 +25,8 @@ type RepoDBMock struct {
GetRepoMetaFn func(repo string) (repodb.RepoMetadata, error)
SetRepoMetaFn func(repo string, repoMeta repodb.RepoMetadata) error
GetMultipleRepoMetaFn func(ctx context.Context, filter func(repoMeta repodb.RepoMetadata) bool,
requestedPage repodb.PageInput) ([]repodb.RepoMetadata, error)
@ -142,6 +144,14 @@ func (sdm RepoDBMock) GetRepoMeta(repo string) (repodb.RepoMetadata, error) {
return repodb.RepoMetadata{}, nil
}
func (sdm RepoDBMock) SetRepoMeta(repo string, repoMeta repodb.RepoMetadata) error {
if sdm.SetRepoMetaFn != nil {
return sdm.SetRepoMetaFn(repo, repoMeta)
}
return nil
}
func (sdm RepoDBMock) GetMultipleRepoMeta(ctx context.Context, filter func(repoMeta repodb.RepoMetadata) bool,
requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, error) {