0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-30 22:34:13 -05:00

feat(search): search for a specific tag cross-repo (#2211)

Syntax to search for `<tag_name>` accross all repos is `:<tag_name>`

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
This commit is contained in:
Andrei Aaron 2024-01-30 19:12:41 +02:00 committed by GitHub
parent 580df421bf
commit a2b923b6fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 202 additions and 3 deletions

View file

@ -694,6 +694,30 @@ func globalSearch(ctx context.Context, query string, metaDB mTypes.MetaDB, filte
images = imageSummaries images = imageSummaries
paginatedRepos.Page = &gql_generated.PageInfo{
TotalCount: pageInfo.TotalCount,
ItemCount: pageInfo.ItemCount,
}
case TagTarget:
skip := convert.SkipQGLField{Vulnerabilities: canSkipField(preloads, "Images.Vulnerabilities")}
pageInput := getPageInput(requestedPage)
expectedTag := strings.TrimPrefix(query, `:`)
matchTagName := func(repoName, actualTag string) bool { return strings.Contains(actualTag, expectedTag) }
fullImageMetaList, err := metaDB.FilterTags(ctx, matchTagName, mTypes.AcceptAllImageMeta)
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{ paginatedRepos.Page = &gql_generated.PageInfo{
TotalCount: pageInfo.TotalCount, TotalCount: pageInfo.TotalCount,
ItemCount: pageInfo.ItemCount, ItemCount: pageInfo.ItemCount,
@ -745,6 +769,7 @@ const (
ImageTarget ImageTarget
DigestTarget DigestTarget
InvalidTarget InvalidTarget
TagTarget
) )
func getSearchTarget(query string) SearchTarget { func getSearchTarget(query string) SearchTarget {
@ -756,8 +781,12 @@ func getSearchTarget(query string) SearchTarget {
return DigestTarget return DigestTarget
} }
if before, _, found := strings.Cut(query, ":"); found && before != "" { if before, after, found := strings.Cut(query, ":"); found {
return ImageTarget if before != "" {
return ImageTarget
} else if after != "" {
return TagTarget
}
} }
return InvalidTarget return InvalidTarget

View file

@ -122,9 +122,42 @@ func TestResolverGlobalSearch(t *testing.T) {
So(repos.Results, ShouldBeEmpty) So(repos.Results, ShouldBeEmpty)
}) })
Convey("Searching with a bad query", func() { Convey("Searching by tag returns a filter error", func() {
ctx := context.Background() ctx := context.Background()
query := ":test" query := ":test"
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 tag returns a pagination error", 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{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 := ":"
responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover)
repos, images, layers, err := globalSearch(responseContext, query, mocks.MetaDBMock{}, &gql_generated.Filter{}, repos, images, layers, err := globalSearch(responseContext, query, mocks.MetaDBMock{}, &gql_generated.Filter{},

View file

@ -3729,6 +3729,143 @@ func TestGlobalSearch(t *testing.T) {
So(results.Images[0].Manifests[0].Digest, ShouldResemble, multiArch.Images[0].DigestStr()) So(results.Images[0].Manifests[0].Digest, ShouldResemble, multiArch.Images[0].DigestStr())
So(results.Images[0].RepoName, ShouldResemble, "repo1") So(results.Images[0].RepoName, ShouldResemble, "repo1")
}) })
Convey("global searching by tag cross repo", 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)
image11 := CreateRandomImage()
image12 := CreateRandomImage()
err := WriteImageToFileSystem(image11, "repo1", "tag1", storeCtlr)
So(err, ShouldBeNil)
err = WriteImageToFileSystem(image12, "repo1", "tag2", storeCtlr)
So(err, ShouldBeNil)
image21 := CreateRandomImage()
image22 := CreateRandomImage()
multiArch2 := CreateRandomMultiarch()
err = WriteImageToFileSystem(image21, "repo2", "tag1", storeCtlr)
So(err, ShouldBeNil)
err = WriteImageToFileSystem(image22, "repo2", "tag2", storeCtlr)
So(err, ShouldBeNil)
err = WriteMultiArchImageToFileSystem(multiArch2, "repo2", "tag-multi", storeCtlr)
So(err, ShouldBeNil)
image31 := CreateRandomImage()
image32 := CreateRandomImage()
err = WriteImageToFileSystem(image31, "repo3", "tag1", storeCtlr)
So(err, ShouldBeNil)
err = WriteImageToFileSystem(image32, "repo3", "tag2", storeCtlr)
So(err, ShouldBeNil)
image41 := CreateRandomImage()
image42 := CreateRandomImage()
multiArch4 := CreateRandomMultiarch()
err = WriteImageToFileSystem(image41, "repo4", "tag1", storeCtlr)
So(err, ShouldBeNil)
err = WriteImageToFileSystem(image42, "repo4", "tag2", storeCtlr)
So(err, ShouldBeNil)
err = WriteMultiArchImageToFileSystem(multiArch4, "repo4", "tag-multi", storeCtlr)
So(err, ShouldBeNil)
image51 := CreateRandomImage()
err = WriteImageToFileSystem(image51, "repo5", "tag1", storeCtlr)
So(err, ShouldBeNil)
multiArch62 := CreateRandomMultiarch()
err = WriteMultiArchImageToFileSystem(multiArch62, "repo6", "tag2", storeCtlr)
So(err, ShouldBeNil)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
// Search for a specific tag cross-repo and return single arch images
results := GlobalSearchGQL(":tag1", baseURL).GlobalSearch
So(len(results.Images), ShouldEqual, 5)
So(len(results.Repos), ShouldEqual, 0)
expectedRepos := []string{"repo1", "repo2", "repo3", "repo4", "repo5"}
for _, image := range results.Images {
So(image.Tag, ShouldEqual, "tag1")
So(image.RepoName, ShouldBeIn, expectedRepos)
So(len(image.Manifests), ShouldEqual, 1)
}
// Search for a specific tag cross-repo and return multi arch images
results = GlobalSearchGQL(":tag-multi", baseURL).GlobalSearch
So(len(results.Images), ShouldEqual, 2)
So(len(results.Repos), ShouldEqual, 0)
expectedRepos = []string{"repo2", "repo4"}
for _, image := range results.Images {
So(image.Tag, ShouldEqual, "tag-multi")
So(image.RepoName, ShouldBeIn, expectedRepos)
So(len(image.Manifests), ShouldEqual, 3)
}
// Search for a specific tag cross-repo and return mixed single and multiarch images
results = GlobalSearchGQL(":tag2", baseURL).GlobalSearch
So(len(results.Images), ShouldEqual, 5)
So(len(results.Repos), ShouldEqual, 0)
expectedRepos = []string{"repo1", "repo2", "repo3", "repo4", "repo6"}
for _, image := range results.Images {
So(image.Tag, ShouldEqual, "tag2")
So(image.RepoName, ShouldBeIn, expectedRepos)
if image.RepoName == "repo6" {
So(len(image.Manifests), ShouldEqual, 3)
} else {
So(len(image.Manifests), ShouldEqual, 1)
}
}
// Search for multiple tags using a partial match cross-repo and return multiarch images
results = GlobalSearchGQL(":multi", baseURL).GlobalSearch
So(len(results.Images), ShouldEqual, 2)
So(len(results.Repos), ShouldEqual, 0)
expectedRepos = []string{"repo2", "repo4"}
for _, image := range results.Images {
So(image.Tag, ShouldContainSubstring, "multi")
So(image.RepoName, ShouldBeIn, expectedRepos)
So(len(image.Manifests), ShouldEqual, 3)
}
// Search for multiple tags using a partial match cross-repo and return mixed single and multiarch images
results = GlobalSearchGQL(":tag", baseURL).GlobalSearch
So(len(results.Images), ShouldEqual, 12)
So(len(results.Repos), ShouldEqual, 0)
expectedRepos = []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6"}
for _, image := range results.Images {
So(image.Tag, ShouldContainSubstring, "tag")
So(image.RepoName, ShouldBeIn, expectedRepos)
}
// Search for a specific tag cross-repo and return mixt single and multiarch images
result := GlobalSearchGQL(":", baseURL)
errors := result.Errors
So(len(errors), ShouldEqual, 1)
results = result.GlobalSearch
So(len(results.Images), ShouldEqual, 0)
So(len(results.Repos), ShouldEqual, 0)
})
} }
func TestCleaningFilteringParamsGlobalSearch(t *testing.T) { func TestCleaningFilteringParamsGlobalSearch(t *testing.T) {