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:
parent
580df421bf
commit
a2b923b6fd
3 changed files with 202 additions and 3 deletions
|
@ -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
|
||||||
|
|
|
@ -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{},
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue