From 0de2210686f6f4cdb5eacde89f444b9bd8c9189b Mon Sep 17 00:00:00 2001 From: LaurentiuNiculae Date: Mon, 27 Nov 2023 18:52:52 +0200 Subject: [PATCH] feat(metadb): add support for querying for images by a blob digest (#2077) Signed-off-by: Laurentiu Niculae --- errors/errors.go | 1 + pkg/extensions/search/resolver.go | 110 +++++++++++++++++-------- pkg/extensions/search/resolver_test.go | 110 ++++++++++++++++++------- pkg/extensions/search/search_test.go | 65 +++++++++++++++ pkg/meta/boltdb/boltdb.go | 12 +-- pkg/meta/common/common.go | 32 +++++++ pkg/meta/common/common_test.go | 4 + pkg/meta/convert/convert.go | 4 +- pkg/meta/dynamodb/dynamodb.go | 12 +-- pkg/test/mocks/repo_db_mock.go | 3 - 10 files changed, 273 insertions(+), 80 deletions(-) diff --git a/errors/errors.go b/errors/errors.go index c60c4af6..12e178de 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -160,6 +160,7 @@ var ( ErrBadHTTPStatusCode = errors.New("cli: the response doesn't contain the expected status code") ErrFormatNotSupported = errors.New("cli: the given output format is not supported") ErrAPINotSupported = errors.New("registry at the given address doesn't implement the correct API") + ErrInvalidSearchQuery = errors.New("invalid search query") ErrFileAlreadyCancelled = errors.New("storageDriver: file already cancelled") ErrFileAlreadyClosed = errors.New("storageDriver: file already closed") ErrFileAlreadyCommitted = errors.New("storageDriver: file already committed") diff --git a/pkg/extensions/search/resolver.go b/pkg/extensions/search/resolver.go index adfe0c00..f5b3e053 100644 --- a/pkg/extensions/search/resolver.go +++ b/pkg/extensions/search/resolver.go @@ -76,7 +76,12 @@ func FilterByDigest(digest string) mTypes.FilterFunc { // imageMeta will always contain 1 manifest return func(repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta) bool { lookupDigest := digest - contains := false + + // Check in case of an index if the index digest matches the search digest + // For Manifests, this is equivalent to imageMeta.Manifests[0] + if imageMeta.Digest.String() == lookupDigest { + return true + } manifest := imageMeta.Manifests[0] @@ -85,24 +90,24 @@ func FilterByDigest(digest string) mTypes.FilterFunc { // Check the image manifest in index.json matches the search digest // This is a blob with mediaType application/vnd.oci.image.manifest.v1+json if strings.Contains(manifestDigest, lookupDigest) { - contains = true + return true } // Check the image config matches the search digest // This is a blob with mediaType application/vnd.oci.image.config.v1+json if strings.Contains(manifest.Manifest.Config.Digest.String(), lookupDigest) { - contains = true + return true } // Check to see if the individual layers in the oci image manifest match the digest // These are blobs with mediaType application/vnd.oci.image.layer.v1.tar+gzip for _, layer := range manifest.Manifest.Layers { if strings.Contains(layer.Digest.String(), lookupDigest) { - contains = true + return true } } - return contains + return false } } @@ -628,18 +633,10 @@ func globalSearch(ctx context.Context, query string, metaDB mTypes.MetaDB, filte } } - if searchingForRepos(query) { - skip := convert.SkipQGLField{ - Vulnerabilities: canSkipField(preloads, "Repos.NewestImage.Vulnerabilities"), - } - - pageInput := pagination.PageInput{ - Limit: deref(requestedPage.Limit, 0), - Offset: deref(requestedPage.Offset, 0), - SortBy: pagination.SortCriteria( - deref(requestedPage.SortBy, gql_generated.SortCriteriaRelevance), - ), - } + switch getSearchTarget(query) { + case RepoTarget: + skip := convert.SkipQGLField{Vulnerabilities: canSkipField(preloads, "Repos.NewestImage.Vulnerabilities")} + pageInput := getPageInput(requestedPage) repoMetaList, err := metaDB.SearchRepos(ctx, query) if err != nil { @@ -667,18 +664,9 @@ func globalSearch(ctx context.Context, query string, metaDB mTypes.MetaDB, filte } paginatedRepos.Results = repos - } else { // search for images - skip := convert.SkipQGLField{ - Vulnerabilities: canSkipField(preloads, "Images.Vulnerabilities"), - } - - pageInput := pagination.PageInput{ - Limit: deref(requestedPage.Limit, 0), - Offset: deref(requestedPage.Offset, 0), - SortBy: pagination.SortCriteria( - deref(requestedPage.SortBy, gql_generated.SortCriteriaRelevance), - ), - } + case ImageTarget: + skip := convert.SkipQGLField{Vulnerabilities: canSkipField(preloads, "Images.Vulnerabilities")} + pageInput := getPageInput(requestedPage) fullImageMetaList, err := metaDB.SearchTags(ctx, query) if err != nil { @@ -697,11 +685,71 @@ func globalSearch(ctx context.Context, query string, metaDB mTypes.MetaDB, filte TotalCount: pageInfo.TotalCount, ItemCount: pageInfo.ItemCount, } + case DigestTarget: + skip := convert.SkipQGLField{Vulnerabilities: canSkipField(preloads, "Images.Vulnerabilities")} + pageInput := getPageInput(requestedPage) + + searchedDigest := query + + fullImageMetaList, err := metaDB.FilterTags(ctx, mTypes.AcceptAllRepoTag, FilterByDigest(searchedDigest)) + if err != nil { + return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{}, []*gql_generated.LayerSummary{}, err + } + + imageSummaries, pageInfo, err := convert.PaginatedFullImageMeta2ImageSummaries(ctx, fullImageMetaList, skip, cveInfo, + localFilter, pageInput) + if err != nil { + return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{}, []*gql_generated.LayerSummary{}, err + } + + images = imageSummaries + + paginatedRepos.Page = &gql_generated.PageInfo{ + TotalCount: pageInfo.TotalCount, + ItemCount: pageInfo.ItemCount, + } + default: + return &paginatedRepos, images, layers, zerr.ErrInvalidSearchQuery } return &paginatedRepos, images, layers, nil } +func getPageInput(requestedPage *gql_generated.PageInput) pagination.PageInput { + return pagination.PageInput{ + Limit: deref(requestedPage.Limit, 0), + Offset: deref(requestedPage.Offset, 0), + SortBy: pagination.SortCriteria( + deref(requestedPage.SortBy, gql_generated.SortCriteriaRelevance), + ), + } +} + +type SearchTarget int + +const ( + RepoTarget = iota + ImageTarget + DigestTarget + InvalidTarget +) + +func getSearchTarget(query string) SearchTarget { + if !strings.ContainsAny(query, ":@") { + return RepoTarget + } + + if strings.HasPrefix(query, string(godigest.SHA256)+":") { + return DigestTarget + } + + if before, _, found := strings.Cut(query, ":"); found && before != "" { + return ImageTarget + } + + return InvalidTarget +} + func canSkipField(preloads map[string]bool, s string) bool { fieldIsPresent := preloads[s] @@ -1100,10 +1148,6 @@ func deref[T any](pointer *T, defaultVal T) T { return defaultVal } -func searchingForRepos(query string) bool { - return !strings.Contains(query, ":") -} - func getImageList(ctx context.Context, repo string, metaDB mTypes.MetaDB, cveInfo cveinfo.CveInfo, requestedPage *gql_generated.PageInput, log log.Logger, //nolint:unparam ) (*gql_generated.PaginatedImagesResult, error) { diff --git a/pkg/extensions/search/resolver_test.go b/pkg/extensions/search/resolver_test.go index 7b6af27d..2412a4b5 100644 --- a/pkg/extensions/search/resolver_test.go +++ b/pkg/extensions/search/resolver_test.go @@ -88,6 +88,52 @@ func TestResolverGlobalSearch(t *testing.T) { So(layers, ShouldBeEmpty) So(repos.Results, ShouldBeEmpty) }) + + Convey("Searching by digest", func() { + ctx := context.Background() + query := "sha256:aabb12341baf2" + mockMetaDB := mocks.MetaDBMock{ + FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, + filterFunc mTypes.FilterFunc, + ) ([]mTypes.FullImageMeta, error) { + return []mTypes.FullImageMeta{}, ErrTestError + }, + } + + responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) + repos, images, layers, err := globalSearch(responseContext, query, mockMetaDB, &gql_generated.Filter{}, + &gql_generated.PageInput{}, mocks.CveInfoMock{}, log.NewLogger("debug", "")) + So(err, ShouldNotBeNil) + So(images, ShouldBeEmpty) + So(layers, ShouldBeEmpty) + So(repos.Results, ShouldBeEmpty) + }) + + Convey("Searching by digest with bad pagination", func() { + ctx := context.Background() + query := "sha256:aabb12341baf2" + + responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) + repos, images, layers, err := globalSearch(responseContext, query, mocks.MetaDBMock{}, &gql_generated.Filter{}, + &gql_generated.PageInput{Limit: ref(-10)}, mocks.CveInfoMock{}, log.NewLogger("debug", "")) + So(err, ShouldNotBeNil) + So(images, ShouldBeEmpty) + So(layers, ShouldBeEmpty) + So(repos.Results, ShouldBeEmpty) + }) + + Convey("Searching with a bad query", func() { + ctx := context.Background() + query := ":test" + + responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) + repos, images, layers, err := globalSearch(responseContext, query, mocks.MetaDBMock{}, &gql_generated.Filter{}, + &gql_generated.PageInput{}, mocks.CveInfoMock{}, log.NewLogger("debug", "")) + So(err, ShouldNotBeNil) + So(images, ShouldBeEmpty) + So(layers, ShouldBeEmpty) + So(repos.Results, ShouldBeEmpty) + }) }) } @@ -647,7 +693,7 @@ func TestQueryResolverErrors(t *testing.T) { }, mocks.CveInfoMock{}) resolver := queryResolver{resolverConfig} - _, err := resolver.GlobalSearch(ctx, "some_string", &gql_generated.Filter{}, getPageInput(1, 1)) + _, err := resolver.GlobalSearch(ctx, "some_string", &gql_generated.Filter{}, getGQLPageInput(1, 1)) So(err, ShouldNotBeNil) }) @@ -1240,7 +1286,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo }) Convey("context done", func() { - pageInput := getPageInput(1, 0) + pageInput := getGQLPageInput(1, 0) ctx, cancel := context.WithCancel(ctx) cancel() @@ -1270,7 +1316,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo graphql.DefaultRecover, ) - pageInput := getPageInput(1, 0) + pageInput := getGQLPageInput(1, 0) images, err := getImageListForCVE(responseContext, "CVE1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1284,7 +1330,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(1, 1) + pageInput = getGQLPageInput(1, 1) images, err = getImageListForCVE(responseContext, "CVE1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1298,19 +1344,19 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(1, 2) + pageInput = getGQLPageInput(1, 2) images, err = getImageListForCVE(responseContext, "CVE1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) So(len(images.Results), ShouldEqual, 0) - pageInput = getPageInput(1, 5) + pageInput = getGQLPageInput(1, 5) images, err = getImageListForCVE(responseContext, "CVE1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) So(len(images.Results), ShouldEqual, 0) - pageInput = getPageInput(2, 0) + pageInput = getGQLPageInput(2, 0) images, err = getImageListForCVE(responseContext, "CVE1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1325,7 +1371,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(5, 0) + pageInput = getGQLPageInput(5, 0) images, err = getImageListForCVE(responseContext, "CVE1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1340,7 +1386,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(5, 1) + pageInput = getGQLPageInput(5, 1) images, err = getImageListForCVE(responseContext, "CVE1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1354,19 +1400,19 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(5, 2) + pageInput = getGQLPageInput(5, 2) images, err = getImageListForCVE(responseContext, "CVE1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) So(len(images.Results), ShouldEqual, 0) - pageInput = getPageInput(5, 5) + pageInput = getGQLPageInput(5, 5) images, err = getImageListForCVE(responseContext, "CVE1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) So(len(images.Results), ShouldEqual, 0) - pageInput = getPageInput(5, 0) + pageInput = getGQLPageInput(5, 0) images, err = getImageListForCVE(responseContext, "CVE2", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1382,7 +1428,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(5, 3) + pageInput = getGQLPageInput(5, 3) images, err = getImageListForCVE(responseContext, "CVE2", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1397,7 +1443,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(5, 0) + pageInput = getGQLPageInput(5, 0) images, err = getImageListForCVE(responseContext, "CVE3", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1412,7 +1458,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(5, 5) + pageInput = getGQLPageInput(5, 5) images, err = getImageListForCVE(responseContext, "CVE3", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1427,7 +1473,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(5, 10) + pageInput = getGQLPageInput(5, 10) images, err = getImageListForCVE(responseContext, "CVE3", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1442,7 +1488,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo } amdFilter := &gql_generated.Filter{Arch: []*string{&AMD}} - pageInput = getPageInput(5, 0) + pageInput = getGQLPageInput(5, 0) images, err = getImageListForCVE(responseContext, "CVE3", cveInfo, amdFilter, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1458,7 +1504,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(2, 2) + pageInput = getGQLPageInput(2, 2) images, err = getImageListForCVE(responseContext, "CVE3", cveInfo, amdFilter, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1548,7 +1594,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo graphql.DefaultRecover, ) - pageInput := getPageInput(1, 0) + pageInput := getGQLPageInput(1, 0) images, err := getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1562,7 +1608,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(1, 1) + pageInput = getGQLPageInput(1, 1) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1576,7 +1622,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(1, 2) + pageInput = getGQLPageInput(1, 2) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1590,19 +1636,19 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(1, 3) + pageInput = getGQLPageInput(1, 3) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) So(len(images.Results), ShouldEqual, 0) - pageInput = getPageInput(1, 10) + pageInput = getGQLPageInput(1, 10) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) So(len(images.Results), ShouldEqual, 0) - pageInput = getPageInput(2, 0) + pageInput = getGQLPageInput(2, 0) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1616,7 +1662,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(2, 1) + pageInput = getGQLPageInput(2, 1) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1630,7 +1676,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(2, 2) + pageInput = getGQLPageInput(2, 2) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1644,7 +1690,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(5, 0) + pageInput = getGQLPageInput(5, 0) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1658,7 +1704,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(5, 0) + pageInput = getGQLPageInput(5, 0) images, err = getImageListWithCVEFixed(responseContext, "CVE2", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1672,7 +1718,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(5, 2) + pageInput = getGQLPageInput(5, 2) images, err = getImageListWithCVEFixed(responseContext, "CVE2", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1681,7 +1727,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo amdFilter := &gql_generated.Filter{Arch: []*string{&AMD}} armFilter := &gql_generated.Filter{Arch: []*string{&ARM}} - pageInput = getPageInput(3, 0) + pageInput = getGQLPageInput(3, 0) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, amdFilter, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -1703,7 +1749,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } - pageInput = getPageInput(1, 1) + pageInput = getGQLPageInput(1, 1) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, armFilter, pageInput, metaDB, log) So(err, ShouldBeNil) @@ -2213,7 +2259,7 @@ func ref[T any](input T) *T { return &ref } -func getPageInput(limit int, offset int) *gql_generated.PageInput { +func getGQLPageInput(limit int, offset int) *gql_generated.PageInput { sortCriteria := gql_generated.SortCriteriaAlphabeticAsc return &gql_generated.PageInput{ diff --git a/pkg/extensions/search/search_test.go b/pkg/extensions/search/search_test.go index adbd522f..e0101c8e 100644 --- a/pkg/extensions/search/search_test.go +++ b/pkg/extensions/search/search_test.go @@ -3651,6 +3651,71 @@ func TestGlobalSearch(t *testing.T) { So(actualImageSummary.Vulnerabilities.Count, ShouldEqual, 4) So(actualImageSummary.Vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL") }) + + Convey("global searching by digest", t, func() { + log := log.NewLogger("debug", "") + rootDir := t.TempDir() + port := GetFreePort() + baseURL := GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + conf.Storage.RootDirectory = rootDir + defaultVal := true + conf.Extensions = &extconf.ExtensionConfig{ + Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, + } + conf.Extensions.Search.CVE = nil + + ctlr := api.NewController(conf) + ctlrManager := NewControllerManager(ctlr) + + storeCtlr := ociutils.GetDefaultStoreController(rootDir, log) + + image1 := CreateRandomImage() + image2 := CreateRandomImage() + multiArch := CreateRandomMultiarch() + + err := WriteImageToFileSystem(image1, "repo1", "tag1", storeCtlr) + So(err, ShouldBeNil) + err = WriteImageToFileSystem(image2, "repo1", "tag2", storeCtlr) + So(err, ShouldBeNil) + err = WriteMultiArchImageToFileSystem(multiArch, "repo1", "tag-multi", storeCtlr) + So(err, ShouldBeNil) + + err = WriteImageToFileSystem(image2, "repo2", "tag2", storeCtlr) + So(err, ShouldBeNil) + + ctlrManager.StartAndWait(port) + defer ctlrManager.StopServer() + + // simple image + results := GlobalSearchGQL(image1.DigestStr(), baseURL).GlobalSearch + So(len(results.Images), ShouldEqual, 1) + So(results.Images[0].Digest, ShouldResemble, image1.DigestStr()) + So(results.Images[0].RepoName, ShouldResemble, "repo1") + + results = GlobalSearchGQL(image2.DigestStr(), baseURL).GlobalSearch + So(len(results.Images), ShouldEqual, 2) + + repos := AccumulateField(results.Images, + func(is zcommon.ImageSummary) string { return is.RepoName }) + So(repos, ShouldContain, "repo1") + So(repos, ShouldContain, "repo2") + + // multiarch + results = GlobalSearchGQL(multiArch.DigestStr(), baseURL).GlobalSearch + So(len(results.Images), ShouldEqual, 1) + So(results.Images[0].Digest, ShouldResemble, multiArch.DigestStr()) + So(len(results.Images[0].Manifests), ShouldEqual, len(multiArch.Images)) + So(results.Images[0].RepoName, ShouldResemble, "repo1") + + results = GlobalSearchGQL(multiArch.Images[0].DigestStr(), baseURL).GlobalSearch + So(len(results.Images), ShouldEqual, 1) + So(results.Images[0].Digest, ShouldResemble, multiArch.DigestStr()) + So(len(results.Images[0].Manifests), ShouldEqual, 1) + So(results.Images[0].Manifests[0].Digest, ShouldResemble, multiArch.Images[0].DigestStr()) + So(results.Images[0].RepoName, ShouldResemble, "repo1") + }) } func TestCleaningFilteringParamsGlobalSearch(t *testing.T) { diff --git a/pkg/meta/boltdb/boltdb.go b/pkg/meta/boltdb/boltdb.go index 7ed0fc1e..02cc3b5a 100644 --- a/pkg/meta/boltdb/boltdb.go +++ b/pkg/meta/boltdb/boltdb.go @@ -639,16 +639,17 @@ func (bdw *BoltDB) FilterTags(ctx context.Context, filterRepoTag mTypes.FilterRe case ispec.MediaTypeImageIndex: indexDigest := descriptor.Digest - imageIndexData, err := getProtoImageMeta(imageMetaBuck, indexDigest) + protoImageIndexMeta, err := getProtoImageMeta(imageMetaBuck, indexDigest) if err != nil { viewError = errors.Join(viewError, err) continue } + imageIndexMeta := mConvert.GetImageMeta(protoImageIndexMeta) matchedManifests := []*proto_go.ManifestMeta{} - for _, manifest := range imageIndexData.Index.Index.Manifests { + for _, manifest := range protoImageIndexMeta.Index.Index.Manifests { manifestDigest := manifest.Digest imageManifestData, err := getProtoImageMeta(imageMetaBuck, manifestDigest) @@ -659,16 +660,17 @@ func (bdw *BoltDB) FilterTags(ctx context.Context, filterRepoTag mTypes.FilterRe } imageMeta := mConvert.GetImageMeta(imageManifestData) + partialImageMeta := common.GetPartialImageMeta(imageIndexMeta, imageMeta) - if filterFunc(repoMeta, imageMeta) { + if filterFunc(repoMeta, partialImageMeta) { matchedManifests = append(matchedManifests, imageManifestData.Manifests[0]) } } if len(matchedManifests) > 0 { - imageIndexData.Manifests = matchedManifests + protoImageIndexMeta.Manifests = matchedManifests - images = append(images, mConvert.GetFullImageMetaFromProto(tag, protoRepoMeta, imageIndexData)) + images = append(images, mConvert.GetFullImageMetaFromProto(tag, protoRepoMeta, protoImageIndexMeta)) } default: bdw.Log.Error().Str("mediaType", descriptor.MediaType).Msg("Unsupported media type") diff --git a/pkg/meta/common/common.go b/pkg/meta/common/common.go index 756c2f42..405fd0ce 100644 --- a/pkg/meta/common/common.go +++ b/pkg/meta/common/common.go @@ -386,3 +386,35 @@ func GetAnnotationValue(annotations map[string]string, annotationKey, labelKey s return value } + +func GetPartialImageMeta(imageIndexMeta mTypes.ImageMeta, imageMeta mTypes.ImageMeta) mTypes.ImageMeta { + partialImageMeta := imageIndexMeta + partialImageMeta.Manifests = imageMeta.Manifests + + partialIndex := deref(imageIndexMeta.Index, ispec.Index{}) + partialIndex.Manifests = getPartialManifestList(partialIndex.Manifests, imageMeta.Digest.String()) + + partialImageMeta.Index = &partialIndex + + return partialImageMeta +} + +func getPartialManifestList(descriptors []ispec.Descriptor, manifestDigest string) []ispec.Descriptor { + result := []ispec.Descriptor{} + + for i := range descriptors { + if descriptors[i].Digest.String() == manifestDigest { + result = append(result, descriptors[i]) + } + } + + return result +} + +func deref[T any](pointer *T, defaultVal T) T { + if pointer != nil { + return *pointer + } + + return defaultVal +} diff --git a/pkg/meta/common/common_test.go b/pkg/meta/common/common_test.go index 54315e78..657b4221 100644 --- a/pkg/meta/common/common_test.go +++ b/pkg/meta/common/common_test.go @@ -14,6 +14,10 @@ import ( var ErrTestError = errors.New("test error") func TestUtils(t *testing.T) { + Convey("GetPartialImageMeta", t, func() { + So(func() { common.GetPartialImageMeta(mTypes.ImageMeta{}, mTypes.ImageMeta{}) }, ShouldNotPanic) + }) + Convey("MatchesArtifactTypes", t, func() { res := common.MatchesArtifactTypes("", nil) So(res, ShouldBeTrue) diff --git a/pkg/meta/convert/convert.go b/pkg/meta/convert/convert.go index 07db40e0..297e8185 100644 --- a/pkg/meta/convert/convert.go +++ b/pkg/meta/convert/convert.go @@ -528,9 +528,9 @@ func GetImageMeta(dbImageMeta *proto_go.ImageMeta) mTypes.ImageMeta { if dbImageMeta.MediaType == ispec.MediaTypeImageIndex { manifests := make([]ispec.Descriptor, 0, len(dbImageMeta.Manifests)) - for _, manifest := range dbImageMeta.Manifests { + for _, manifest := range deref(dbImageMeta.Index, proto_go.IndexMeta{}).Index.Manifests { manifests = append(manifests, ispec.Descriptor{ - MediaType: deref(manifest.Manifest.MediaType, ""), + MediaType: manifest.MediaType, Digest: godigest.Digest(manifest.Digest), Size: manifest.Size, }) diff --git a/pkg/meta/dynamodb/dynamodb.go b/pkg/meta/dynamodb/dynamodb.go index 229b8fec..9a50b4a2 100644 --- a/pkg/meta/dynamodb/dynamodb.go +++ b/pkg/meta/dynamodb/dynamodb.go @@ -729,16 +729,17 @@ func (dwr *DynamoDB) FilterTags(ctx context.Context, filterRepoTag mTypes.Filter case ispec.MediaTypeImageIndex: indexDigest := descriptor.Digest - imageIndexData, err := dwr.GetProtoImageMeta(ctx, godigest.Digest(indexDigest)) + protoImageIndexMeta, err := dwr.GetProtoImageMeta(ctx, godigest.Digest(indexDigest)) if err != nil { viewError = errors.Join(viewError, err) continue } + imageIndexMeta := mConvert.GetImageMeta(protoImageIndexMeta) matchedManifests := []*proto_go.ManifestMeta{} - for _, manifest := range imageIndexData.Index.Index.Manifests { + for _, manifest := range protoImageIndexMeta.Index.Index.Manifests { manifestDigest := manifest.Digest imageManifestData, err := dwr.GetProtoImageMeta(ctx, godigest.Digest(manifestDigest)) @@ -749,16 +750,17 @@ func (dwr *DynamoDB) FilterTags(ctx context.Context, filterRepoTag mTypes.Filter } imageMeta := mConvert.GetImageMeta(imageManifestData) + partialImageMeta := common.GetPartialImageMeta(imageIndexMeta, imageMeta) - if filterFunc(repoMeta, imageMeta) { + if filterFunc(repoMeta, partialImageMeta) { matchedManifests = append(matchedManifests, imageManifestData.Manifests[0]) } } if len(matchedManifests) > 0 { - imageIndexData.Manifests = matchedManifests + protoImageIndexMeta.Manifests = matchedManifests - images = append(images, mConvert.GetFullImageMetaFromProto(tag, protoRepoMeta, imageIndexData)) + images = append(images, mConvert.GetFullImageMetaFromProto(tag, protoRepoMeta, protoImageIndexMeta)) } default: dwr.Log.Error().Str("mediaType", descriptor.MediaType).Msg("Unsupported media type") diff --git a/pkg/test/mocks/repo_db_mock.go b/pkg/test/mocks/repo_db_mock.go index af39f0ea..a2521c1d 100644 --- a/pkg/test/mocks/repo_db_mock.go +++ b/pkg/test/mocks/repo_db_mock.go @@ -57,9 +57,6 @@ type MetaDBMock struct { SearchTagsFn func(ctx context.Context, searchText string) ([]mTypes.FullImageMeta, error) - FilterTagFn func(ctx context.Context, filterFunc mTypes.FilterFunc, - ) ([]mTypes.RepoMeta, map[string]mTypes.ImageMeta, error) - GetImageMetaFn func(digest godigest.Digest) (mTypes.ImageMeta, error) GetMultipleRepoMetaFn func(ctx context.Context, filter func(repoMeta mTypes.RepoMeta) bool,