diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index 3f5892cd..1adfbb3c 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -1580,6 +1580,14 @@ input Filter { Only return images or repositories with at least one signature """ HasToBeSigned: Boolean + """ + Only returns images or repositories that are bookmarked or not bookmarked + """ + IsBookmarked: Boolean + """ + Only returns images or repositories that are starred or not starred + """ + IsStarred: Boolean } """ @@ -8750,7 +8758,7 @@ func (ec *executionContext) unmarshalInputFilter(ctx context.Context, obj interf asMap[k] = v } - fieldsInOrder := [...]string{"Os", "Arch", "HasToBeSigned"} + fieldsInOrder := [...]string{"Os", "Arch", "HasToBeSigned", "IsBookmarked", "IsStarred"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -8781,6 +8789,22 @@ func (ec *executionContext) unmarshalInputFilter(ctx context.Context, obj interf if err != nil { return it, err } + case "IsBookmarked": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("IsBookmarked")) + it.IsBookmarked, err = ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + case "IsStarred": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("IsStarred")) + it.IsStarred, err = ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } } } diff --git a/pkg/extensions/search/gql_generated/models_gen.go b/pkg/extensions/search/gql_generated/models_gen.go index 42f58508..8ca876f7 100644 --- a/pkg/extensions/search/gql_generated/models_gen.go +++ b/pkg/extensions/search/gql_generated/models_gen.go @@ -55,6 +55,10 @@ type Filter struct { Arch []*string `json:"Arch,omitempty"` // Only return images or repositories with at least one signature HasToBeSigned *bool `json:"HasToBeSigned,omitempty"` + // Only returns images or repositories that are bookmarked or not bookmarked + IsBookmarked *bool `json:"IsBookmarked,omitempty"` + // Only returns images or repositories that are starred or not starred + IsStarred *bool `json:"IsStarred,omitempty"` } // Search results, can contain images, repositories and layers diff --git a/pkg/extensions/search/resolver.go b/pkg/extensions/search/resolver.go index 2d3b0b70..2ff7aa03 100644 --- a/pkg/extensions/search/resolver.go +++ b/pkg/extensions/search/resolver.go @@ -667,6 +667,8 @@ func globalSearch(ctx context.Context, query string, repoDB repodb.RepoDB, filte Os: filter.Os, Arch: filter.Arch, HasToBeSigned: filter.HasToBeSigned, + IsBookmarked: filter.IsBookmarked, + IsStarred: filter.IsStarred, } } diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index 8b33a64c..b8e6eb70 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -544,6 +544,14 @@ input Filter { Only return images or repositories with at least one signature """ HasToBeSigned: Boolean + """ + Only returns images or repositories that are bookmarked or not bookmarked + """ + IsBookmarked: Boolean + """ + Only returns images or repositories that are starred or not starred + """ + IsStarred: Boolean } """ diff --git a/pkg/extensions/search/userprefs_test.go b/pkg/extensions/search/userprefs_test.go index f1309fb5..d61a710c 100644 --- a/pkg/extensions/search/userprefs_test.go +++ b/pkg/extensions/search/userprefs_test.go @@ -17,6 +17,7 @@ import ( "zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/constants" + "zotregistry.io/zot/pkg/common" extconf "zotregistry.io/zot/pkg/extensions/config" "zotregistry.io/zot/pkg/extensions/monitoring" "zotregistry.io/zot/pkg/log" @@ -612,6 +613,199 @@ func TestChangingRepoState(t *testing.T) { }) } +func TestGlobalSearchWithUserPrefFiltering(t *testing.T) { + Convey("Bookmarks and Stars filtering", t, func() { + dir := t.TempDir() + port := GetFreePort() + baseURL := GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + conf.Storage.RootDirectory = dir + + simpleUser := "simpleUser" + simpleUserPassword := "simpleUserPass" + credTests := fmt.Sprintf("%s\n\n", getCredString(simpleUser, simpleUserPassword)) + + htpasswdPath := MakeHtpasswdFileFromString(credTests) + defer os.Remove(htpasswdPath) + + conf.HTTP.Auth = &config.AuthConfig{ + HTPasswd: config.AuthHTPasswd{ + Path: htpasswdPath, + }, + } + + conf.HTTP.AccessControl = &config.AccessControlConfig{ + Repositories: config.Repositories{ + "**": config.PolicyGroup{ + Policies: []config.Policy{ + { + Users: []string{simpleUser}, + Actions: []string{"read", "create"}, + }, + }, + }, + }, + } + + defaultVal := true + conf.Extensions = &extconf.ExtensionConfig{ + Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, + } + + ctlr := api.NewController(conf) + + ctlrManager := NewControllerManager(ctlr) + ctlrManager.StartAndWait(port) + defer ctlrManager.StopServer() + + preferencesBaseURL := baseURL + constants.FullUserPreferencesPrefix + simpleUserClient := resty.R().SetBasicAuth(simpleUser, simpleUserPassword) + + // ------ Add simple repo + repo := "repo" + img, err := GetRandomImage("tag") + So(err, ShouldBeNil) + err = UploadImageWithBasicAuth(img, baseURL, repo, simpleUser, simpleUserPassword) + So(err, ShouldBeNil) + + // ------ Add repo and star it + sRepo := "starred-repo" + img, err = GetRandomImage("tag") + So(err, ShouldBeNil) + err = UploadImageWithBasicAuth(img, baseURL, sRepo, simpleUser, simpleUserPassword) + So(err, ShouldBeNil) + + resp, err := simpleUserClient.Put(preferencesBaseURL + PutRepoStarURL(sRepo)) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + So(err, ShouldBeNil) + + // ------ Add repo and bookmark it + bRepo := "bookmarked-repo" + img, err = GetRandomImage("tag") + So(err, ShouldBeNil) + err = UploadImageWithBasicAuth(img, baseURL, bRepo, simpleUser, simpleUserPassword) + So(err, ShouldBeNil) + + resp, err = simpleUserClient.Put(preferencesBaseURL + PutRepoBookmarkURL(bRepo)) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + So(err, ShouldBeNil) + + // ------ Add repo, star and bookmark it + sbRepo := "starred-bookmarked-repo" + img, err = GetRandomImage("tag") + So(err, ShouldBeNil) + err = UploadImageWithBasicAuth(img, baseURL, sbRepo, simpleUser, simpleUserPassword) + So(err, ShouldBeNil) + + resp, err = simpleUserClient.Put(preferencesBaseURL + PutRepoStarURL(sbRepo)) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + So(err, ShouldBeNil) + resp, err = simpleUserClient.Put(preferencesBaseURL + PutRepoBookmarkURL(sbRepo)) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + So(err, ShouldBeNil) + + // Make global search requests filterin by IsStarred and IsBookmarked + + query := `{ GlobalSearch(query:"repo", ){ Repos { Name } } }` + + resp, err = simpleUserClient.Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + responseStruct := &GlobalSearchResultResp{} + + err = json.Unmarshal(resp.Body(), responseStruct) + So(err, ShouldBeNil) + + foundRepos := responseStruct.GlobalSearchResult.GlobalSearch.Repos + So(len(foundRepos), ShouldEqual, 4) + + // Filter by IsStarred = true + query = `{ GlobalSearch(query:"repo", filter:{ IsStarred:true }) { Repos { Name IsStarred IsBookmarked }}}` + + resp, err = simpleUserClient.Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + responseStruct = &GlobalSearchResultResp{} + + err = json.Unmarshal(resp.Body(), responseStruct) + So(err, ShouldBeNil) + + foundRepos = responseStruct.GlobalSearchResult.GlobalSearch.Repos + So(len(foundRepos), ShouldEqual, 2) + So(foundRepos, ShouldContain, common.RepoSummary{Name: sRepo, IsStarred: true, IsBookmarked: false}) + So(foundRepos, ShouldContain, common.RepoSummary{Name: sbRepo, IsStarred: true, IsBookmarked: true}) + + // Filter by IsStarred = true && IsBookmarked = false + query = `{ + GlobalSearch(query:"repo", filter:{ IsStarred:true, IsBookmarked:false }) { + Repos { Name IsStarred IsBookmarked } + } + }` + + resp, err = simpleUserClient.Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + responseStruct = &GlobalSearchResultResp{} + + err = json.Unmarshal(resp.Body(), responseStruct) + So(err, ShouldBeNil) + + foundRepos = responseStruct.GlobalSearchResult.GlobalSearch.Repos + So(len(foundRepos), ShouldEqual, 1) + So(foundRepos, ShouldContain, common.RepoSummary{Name: sRepo, IsStarred: true, IsBookmarked: false}) + + // Filter by IsBookmarked = true + query = `{ + GlobalSearch(query:"repo", filter:{ IsBookmarked:true }) { + Repos { Name IsStarred IsBookmarked } + } + }` + + resp, err = simpleUserClient.Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + responseStruct = &GlobalSearchResultResp{} + + err = json.Unmarshal(resp.Body(), responseStruct) + So(err, ShouldBeNil) + + foundRepos = responseStruct.GlobalSearchResult.GlobalSearch.Repos + So(len(foundRepos), ShouldEqual, 2) + So(foundRepos, ShouldContain, common.RepoSummary{Name: bRepo, IsStarred: false, IsBookmarked: true}) + So(foundRepos, ShouldContain, common.RepoSummary{Name: sbRepo, IsStarred: true, IsBookmarked: true}) + + // Filter by IsBookmarked = true && IsStarred = false + query = `{ + GlobalSearch(query:"repo", filter:{ IsBookmarked:true, IsStarred:false }) { + Repos { Name IsStarred IsBookmarked } + } + }` + + resp, err = simpleUserClient.Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + responseStruct = &GlobalSearchResultResp{} + + err = json.Unmarshal(resp.Body(), responseStruct) + So(err, ShouldBeNil) + + foundRepos = responseStruct.GlobalSearchResult.GlobalSearch.Repos + So(len(foundRepos), ShouldEqual, 1) + So(foundRepos, ShouldContain, common.RepoSummary{Name: bRepo, IsStarred: false, IsBookmarked: true}) + }) +} + func PutRepoStarURL(repo string) string { return fmt.Sprintf("?repo=%s&action=toggleStar", repo) } diff --git a/pkg/meta/common/common.go b/pkg/meta/common/common.go index 80b72789..387d40dc 100644 --- a/pkg/meta/common/common.go +++ b/pkg/meta/common/common.go @@ -224,6 +224,14 @@ func AcceptedByFilter(filter repodb.Filter, data repodb.FilterData) bool { return false } + if filter.IsBookmarked != nil && *filter.IsBookmarked != data.IsBookmarked { + return false + } + + if filter.IsStarred != nil && *filter.IsStarred != data.IsStarred { + return false + } + return true } diff --git a/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go b/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go index 21e91421..c7bc7cfb 100644 --- a/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go +++ b/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go @@ -916,7 +916,7 @@ func (bdw *DBWrapper) SearchRepos(ctx context.Context, searchText string, filter repoMeta.IsBookmarked = zcommon.Contains(userBookmarks, repoMeta.Name) repoMeta.IsStarred = zcommon.Contains(userStars, repoMeta.Name) - rank := common.RankRepoName(searchText, string(repoName)) + rank := common.RankRepoName(searchText, repoMeta.Name) if rank == -1 { continue } @@ -1013,6 +1013,8 @@ func (bdw *DBWrapper) SearchRepos(ctx context.Context, searchText string, filter LastUpdated: repoLastUpdated, DownloadCount: repoDownloads, IsSigned: isSigned, + IsBookmarked: repoMeta.IsBookmarked, + IsStarred: repoMeta.IsStarred, } if !common.AcceptedByFilter(filter, repoFilterData) { diff --git a/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper_test.go b/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper_test.go index 5f3227ab..44b90b05 100644 --- a/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper_test.go +++ b/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper_test.go @@ -406,6 +406,7 @@ func TestWrapperErrors(t *testing.T) { } repoMeta := repodb.RepoMetadata{ + Name: "repo1", Tags: map[string]repodb.Descriptor{ "tag1": {Digest: "dig1", MediaType: ispec.MediaTypeImageManifest}, }, @@ -420,6 +421,7 @@ func TestWrapperErrors(t *testing.T) { } repoMeta = repodb.RepoMetadata{ + Name: "repo2", Tags: map[string]repodb.Descriptor{ "tag2": {Digest: "dig2", MediaType: ispec.MediaTypeImageManifest}, }, @@ -459,6 +461,7 @@ func TestWrapperErrors(t *testing.T) { } repoMeta = repodb.RepoMetadata{ + Name: "repo1", Tags: map[string]repodb.Descriptor{ "tag1": {Digest: "dig1", MediaType: ispec.MediaTypeImageManifest}, }, @@ -593,6 +596,7 @@ func TestWrapperErrors(t *testing.T) { // manifest data doesn't exist repoMeta = repodb.RepoMetadata{ + Name: "repo1", Tags: map[string]repodb.Descriptor{ "tag2": {Digest: "dig2", MediaType: ispec.MediaTypeImageManifest}, }, @@ -608,6 +612,7 @@ func TestWrapperErrors(t *testing.T) { // manifest data is wrong repoMeta = repodb.RepoMetadata{ + Name: "repo2", Tags: map[string]repodb.Descriptor{ "tag2": {Digest: "wrongManifestData", MediaType: ispec.MediaTypeImageManifest}, }, @@ -622,6 +627,7 @@ func TestWrapperErrors(t *testing.T) { } repoMeta = repodb.RepoMetadata{ + Name: "repo3", Tags: map[string]repodb.Descriptor{ "tag1": {Digest: "dig1", MediaType: ispec.MediaTypeImageManifest}, }, diff --git a/pkg/meta/repodb/repodb.go b/pkg/meta/repodb/repodb.go index 3bf4b872..0bf0c304 100644 --- a/pkg/meta/repodb/repodb.go +++ b/pkg/meta/repodb/repodb.go @@ -234,6 +234,8 @@ type Filter struct { Os []*string Arch []*string HasToBeSigned *bool + IsBookmarked *bool + IsStarred *bool } type FilterData struct { @@ -242,4 +244,6 @@ type FilterData struct { OsList []string ArchList []string IsSigned bool + IsStarred bool + IsBookmarked bool }