refactor: generate search API by go-swagger (#14422)

Signed-off-by: He Weiwei <hweiwei@vmware.com>
This commit is contained in:
He Weiwei 2021-03-22 14:35:44 +08:00 committed by GitHub
parent 634be34236
commit a2b08446d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 406 additions and 662 deletions

View File

@ -31,28 +31,6 @@ paths:
description: The system health status.
schema:
$ref: '#/definitions/OverallHealthStatus'
/search:
get:
summary: 'Search for projects, repositories and helm charts'
description: |
The Search endpoint returns information about the projects ,repositories and helm charts offered at public status or related to the current logged in user. The response includes the project, repository list and charts in a proper display order.
parameters:
- name: q
in: query
description: Search parameter for project and repository name.
required: true
type: string
tags:
- Products
responses:
'200':
description: An array of search results
schema:
type: array
items:
$ref: '#/definitions/Search'
'500':
description: Unexpected internal errors.
'/projects/{project_id}/metadatas':
get:
summary: Get project metadata.
@ -1981,99 +1959,6 @@ responses:
InternalServerError:
description: 'Internal Server Error'
definitions:
Search:
type: object
properties:
project:
description: Search results of the projects that matched the filter keywords.
type: array
items:
$ref: '#/definitions/Project'
repository:
description: Search results of the repositories that matched the filter keywords.
type: array
items:
$ref: '#/definitions/SearchRepository'
chart:
description: Search results of the charts that macthed the filter keywords.
type: array
items:
$ref: '#/definitions/SearchResult'
SearchRepository:
type: object
properties:
project_id:
type: integer
description: The ID of the project that the repository belongs to
project_name:
type: string
description: The name of the project that the repository belongs to
project_public:
type: boolean
description: 'The flag to indicate the publicity of the project that the repository belongs to (1 is public, 0 is not)'
repository_name:
type: string
description: The name of the repository
pull_count:
type: integer
description: The count how many times the repository is pulled
artifact_count:
type: integer
description: The count of artifacts in the repository
Project:
type: object
properties:
project_id:
type: integer
format: int32
description: Project ID
owner_id:
type: integer
format: int32
description: The owner ID of the project always means the creator of the project.
name:
type: string
description: The name of the project.
registry_id:
type: integer
format: int64
description: The ID of referenced registry when the project is a proxy cache project.
creation_time:
type: string
description: The creation time of the project.
update_time:
type: string
description: The update time of the project.
deleted:
type: boolean
description: A deletion mark of the project.
owner_name:
type: string
description: The owner name of the project.
togglable:
type: boolean
description: Correspond to the UI about whether the project's publicity is updatable (for UI)
current_user_role_id:
type: integer
description: The role ID with highest permission of the current user who triggered the API (for UI). This attribute is deprecated and will be removed in future versions.
current_user_role_ids:
type: array
items:
type: integer
format: int32
description: The list of role ID of the current user who triggered the API (for UI)
repo_count:
type: integer
description: The number of the repositories under this project.
chart_count:
type: integer
description: The total number of charts under this project.
metadata:
description: The metadata of the project.
$ref: '#/definitions/ProjectMetadata'
cve_allowlist:
description: The CVE allowlist of this project.
$ref: '#/definitions/CVEAllowlist'
ProjectMetadata:
type: object
properties:
@ -2941,18 +2826,6 @@ definitions:
properties:
labels:
$ref: '#/definitions/Labels'
SearchResult:
type: object
description: The chart search result item
properties:
name:
type: string
description: The chart name with repo name
score:
type: integer
description: The matched level
chart:
$ref: '#/definitions/ChartVersion'
Labels:
type: array
description: A list of label

View File

@ -19,6 +19,27 @@ security:
- basic: []
- {}
paths:
/search:
get:
summary: 'Search for projects, repositories and helm charts'
description: |-
The Search endpoint returns information about the projects, repositories and helm charts offered at public status or related to the current logged in user. The response includes the project, repository list and charts in a proper display order.
parameters:
- name: q
in: query
description: Search parameter for project and repository name.
required: true
type: string
tags:
- search
operationId: search
responses:
'200':
description: An array of search results
schema:
$ref: '#/definitions/Search'
'500':
$ref: '#/responses/500'
/ldap/ping:
post:
operationId: pingLdap
@ -3656,6 +3677,134 @@ definitions:
message:
type: string
description: The error message
Search:
type: object
properties:
project:
description: Search results of the projects that matched the filter keywords.
type: array
items:
$ref: '#/definitions/Project'
repository:
description: Search results of the repositories that matched the filter keywords.
type: array
items:
$ref: '#/definitions/SearchRepository'
chart:
description: Search results of the charts that macthed the filter keywords.
type: array
items:
$ref: '#/definitions/SearchResult'
x-omitempty: true
x-isnullable: true
SearchRepository:
type: object
properties:
project_id:
type: integer
description: The ID of the project that the repository belongs to
project_name:
type: string
description: The name of the project that the repository belongs to
project_public:
type: boolean
description: 'The flag to indicate the publicity of the project that the repository belongs to (1 is public, 0 is not)'
repository_name:
type: string
description: The name of the repository
pull_count:
type: integer
description: The count how many times the repository is pulled
artifact_count:
type: integer
description: The count of artifacts in the repository
SearchResult:
type: object
description: The chart search result item
properties:
Name:
type: string
description: The chart name with repo name
Score:
type: integer
description: The matched level
Chart:
$ref: '#/definitions/ChartVersion'
ChartVersion:
type: object
description: A specified chart entry
allOf:
- $ref: '#/definitions/ChartMetadata'
- type: object
properties:
created:
type: string
description: The created time of the chart entry
removed:
type: boolean
description: A flag to indicate if the chart entry is removed
digest:
type: string
description: The digest value of the chart entry
urls:
type: array
description: The urls of the chart entry
items:
type: string
properties:
labels:
type: array
description: A list of label
items:
$ref: '#/definitions/Label'
ChartMetadata:
type: object
description: The metadata of chart version
required:
- name
- version
- engine
- icon
- apiVersion
- appVersion
properties:
name:
type: string
description: The name of the chart
home:
type: string
description: The URL to the relevant project page
sources:
type: array
description: The URL to the source code of chart
items:
type: string
version:
type: string
description: A SemVer 2 version of chart
description:
type: string
description: A one-sentence description of chart
keywords:
type: array
description: A list of string keywords
items:
type: string
engine:
type: string
description: The name of template engine
icon:
type: string
description: The URL to an icon file
apiVersion:
type: string
description: The API version of this chart
appVersion:
type: string
description: The version of the application enclosed in the chart
deprecated:
type: boolean
description: Whether or not this chart is deprecated
Repository:
type: object
properties:

View File

@ -86,38 +86,6 @@ func GetTotalOfRepositories(query ...*models.RepositoryQuery) (int64, error) {
return total, nil
}
// GetRepositories ...
func GetRepositories(query ...*models.RepositoryQuery) ([]*models.RepoRecord, error) {
repositories := []*models.RepoRecord{}
order := "name asc"
if len(query) > 0 && query[0] != nil {
if s, ok := orderMap[query[0].Sort]; ok {
order = s
}
}
condition, params := repositoryQueryConditions(query...)
sql := fmt.Sprintf(`select r.repository_id, r.name, r.project_id, r.description, r.pull_count,
r.star_count, r.creation_time, r.update_time %s order by r.%s `, condition, order)
if len(query) > 0 && query[0] != nil {
page, size := query[0].Page, query[0].Size
if size > 0 {
sql += `limit ? `
params = append(params, size)
if page > 0 {
sql += `offset ? `
params = append(params, size*(page-1))
}
}
}
if _, err := GetOrmer().Raw(sql, params).QueryRows(&repositories); err != nil {
return nil, err
}
return repositories, nil
}
func repositoryQueryConditions(query ...*models.RepositoryQuery) (string, []interface{}) {
params := []interface{}{}
sql := `from repository r `
@ -127,7 +95,7 @@ func repositoryQueryConditions(query ...*models.RepositoryQuery) (string, []inte
q := query[0]
if q.LabelID > 0 {
sql += `join harbor_resource_label rl on r.repository_id = rl.resource_id
sql += `join harbor_resource_label rl on r.repository_id = rl.resource_id
and rl.resource_type = 'r' `
}
sql += `where 1=1 `

View File

@ -17,7 +17,6 @@ package dao
import (
"testing"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -45,82 +44,6 @@ func TestGetTotalOfRepositories(t *testing.T) {
assert.Equal(t, total+1, n)
}
func TestGetRepositories(t *testing.T) {
// no query
repositories, err := GetRepositories()
require.Nil(t, err)
n := len(repositories)
err = addRepository(repository)
require.Nil(t, err)
defer deleteRepository(name)
repositories, err = GetRepositories()
require.Nil(t, err)
assert.Equal(t, n+1, len(repositories))
// query by name
repositories, err = GetRepositories(&models.RepositoryQuery{
Name: name,
})
require.Nil(t, err)
require.Equal(t, 1, len(repositories))
assert.Equal(t, name, repositories[0].Name)
// query by project name
repositories, err = GetRepositories(&models.RepositoryQuery{
ProjectName: project,
})
require.Nil(t, err)
found := false
for _, repository := range repositories {
if repository.Name == name {
found = true
break
}
}
assert.True(t, found)
// query by project ID
repositories, err = GetRepositories(&models.RepositoryQuery{
ProjectIDs: []int64{1},
})
require.Nil(t, err)
found = false
for _, repository := range repositories {
if repository.Name == name {
found = true
break
}
}
assert.True(t, found)
// query by label ID
labelID, err := AddLabel(&models.Label{
Name: "label_for_test",
})
require.Nil(t, err)
defer DeleteLabel(labelID)
r, err := GetRepositoryByName(name)
require.Nil(t, err)
rlID, err := AddResourceLabel(&models.ResourceLabel{
LabelID: labelID,
ResourceID: r.RepositoryID,
ResourceType: common.ResourceTypeRepository,
})
require.Nil(t, err)
defer DeleteResourceLabel(rlID)
repositories, err = GetRepositories(&models.RepositoryQuery{
LabelID: labelID,
})
require.Nil(t, err)
require.Equal(t, 1, len(repositories))
assert.Equal(t, name, repositories[0].Name)
}
func addRepository(repository *models.RepoRecord) error {
return AddRepository(*repository)
}

View File

@ -95,7 +95,6 @@ func init() {
beego.TestBeegoInit(apppath)
beego.Router("/api/health", &HealthAPI{}, "get:CheckHealth")
beego.Router("/api/search/", &SearchAPI{})
beego.Router("/api/users/:id", &UserAPI{}, "get:Get")
beego.Router("/api/users", &UserAPI{}, "get:List;post:Post;delete:Delete;put:Put")
beego.Router("/api/users/search", &UserAPI{}, "get:Search")

View File

@ -1,215 +0,0 @@
// Copyright 2018 Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"fmt"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/security/local"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"helm.sh/helm/v3/cmd/helm/search"
)
type chartSearchHandler func(string, []string) ([]*search.Result, error)
var searchHandler chartSearchHandler
// SearchAPI handles request to /api/search
type SearchAPI struct {
BaseController
}
type searchResult struct {
Project []*models.Project `json:"project"`
Repository []map[string]interface{} `json:"repository"`
Chart *[]*search.Result `json:"chart,omitempty"`
}
// Get ...
func (s *SearchAPI) Get() {
keyword := s.GetString("q")
query := q.New(q.KeyWords{})
if keyword != "" {
query.Keywords["name"] = &q.FuzzyMatchValue{Value: keyword}
}
if !s.SecurityCtx.IsSysAdmin() {
if sc, ok := s.SecurityCtx.(*local.SecurityContext); ok && sc.IsAuthenticated() {
user := sc.User()
member := &project.MemberQuery{
UserID: user.UserID,
GroupIDs: user.GroupIDs,
WithPublic: true,
}
query.Keywords["member"] = member
} else {
query.Keywords["public"] = true
}
}
projects, err := s.ProjectCtl.List(s.Context(), query)
if err != nil {
s.ParseAndHandleError("failed to get projects", err)
return
}
projectResult := []*models.Project{}
proNames := []string{}
for _, p := range projects {
proNames = append(proNames, p.Name)
if sc, ok := s.SecurityCtx.(*local.SecurityContext); ok && sc.IsAuthenticated() {
roles, err := s.ProjectCtl.ListRoles(s.Context(), p.ProjectID, sc.User())
if err != nil {
s.SendInternalServerError(fmt.Errorf("failed to list roles: %v", err))
return
}
p.RoleList = roles
p.Role = highestRole(roles)
}
total, err := dao.GetTotalOfRepositories(&models.RepositoryQuery{
ProjectIDs: []int64{p.ProjectID},
})
if err != nil {
log.Errorf("failed to get total of repositories of project %d: %v", p.ProjectID, err)
s.SendInternalServerError(fmt.Errorf("failed to get total of repositories of project %d: %v", p.ProjectID, err))
return
}
p.RepoCount = total
projectResult = append(projectResult, p)
}
repositoryResult, err := filterRepositories(projects, keyword)
if err != nil {
log.Errorf("failed to filter repositories: %v", err)
s.SendInternalServerError(fmt.Errorf("failed to filter repositories: %v", err))
return
}
result := &searchResult{
Project: projectResult,
Repository: repositoryResult,
}
// If enable chart repository
if config.WithChartMuseum() {
if searchHandler == nil {
searchHandler = chartController.SearchChart
}
chartResults, err := searchHandler(keyword, proNames)
if err != nil {
log.Errorf("failed to filter charts: %v", err)
s.SendInternalServerError(err)
return
}
result.Chart = &chartResults
}
s.Data["json"] = result
s.ServeJSON()
}
func filterRepositories(projects []*models.Project, keyword string) (
[]map[string]interface{}, error) {
result := []map[string]interface{}{}
if len(projects) == 0 {
return result, nil
}
repositories, err := dao.GetRepositories(&models.RepositoryQuery{
Name: keyword,
})
if err != nil {
return nil, err
}
if len(repositories) == 0 {
return result, nil
}
projectMap := map[string]*models.Project{}
for _, project := range projects {
projectMap[project.Name] = project
}
ctx := orm.NewContext(nil, dao.GetOrmer())
for _, repository := range repositories {
projectName, _ := utils.ParseRepository(repository.Name)
project, exist := projectMap[projectName]
if !exist {
continue
}
entry := make(map[string]interface{})
entry["repository_name"] = repository.Name
entry["project_name"] = project.Name
entry["project_id"] = project.ProjectID
entry["project_public"] = project.IsPublic()
entry["pull_count"] = repository.PullCount
count, err := artifact.Ctl.Count(ctx, &q.Query{
Keywords: map[string]interface{}{
"RepositoryID": repository.RepositoryID,
},
})
if err != nil {
log.Errorf("failed to get the count of artifacts under the repository %s: %v",
repository.Name, err)
} else {
entry["artifact_count"] = count
}
result = append(result, entry)
}
return result, nil
}
// Returns the highest role in the role list.
// This func should be removed once we deprecate the "current_user_role_id" in project API
// A user can have multiple roles and they may not have a strict ranking relationship
func highestRole(roles []int) int {
if roles == nil {
return 0
}
rolePower := map[int]int{
common.RoleProjectAdmin: 50,
common.RoleMaintainer: 40,
common.RoleDeveloper: 30,
common.RoleGuest: 20,
common.RoleLimitedGuest: 10,
}
var highest, highestPower int
for _, role := range roles {
if p, ok := rolePower[role]; ok && p > highestPower {
highest = role
highestPower = p
}
}
return highest
}

View File

@ -1,208 +0,0 @@
// Copyright 2018 Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"fmt"
"net/http"
"testing"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/common/dao"
member "github.com/goharbor/harbor/src/common/dao/project"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v3/cmd/helm/search"
helm_repo "helm.sh/helm/v3/pkg/repo"
)
func TestSearch(t *testing.T) {
fmt.Println("Testing Search(SearchGet) API")
// Use mock chart search handler
searchHandler = func(string, []string) ([]*search.Result, error) {
results := []*search.Result{}
results = append(results, &search.Result{
Name: "library/harbor",
Score: 0,
Chart: &helm_repo.ChartVersion{},
})
return results, nil
}
// create a public project named "search"
projectID1, err := dao.AddProject(models.Project{
Name: "search",
OwnerID: int(nonSysAdminID),
})
require.Nil(t, err)
defer dao.DeleteProject(projectID1)
err = dao.AddProjectMetadata(&models.ProjectMetadata{
ProjectID: projectID1,
Name: "public",
Value: "true",
})
require.Nil(t, err)
memberID1, err := member.AddProjectMember(models.Member{
ProjectID: projectID1,
EntityID: int(nonSysAdminID),
EntityType: common.UserMember,
Role: common.RoleGuest,
})
require.Nil(t, err)
defer member.DeleteProjectMemberByID(memberID1)
// create a private project named "search-2", the "-" is necessary
// in the project name to test some corner cases
projectID2, err := dao.AddProject(models.Project{
Name: "search-2",
OwnerID: int(nonSysAdminID),
})
require.Nil(t, err)
defer dao.DeleteProject(projectID2)
memberID2, err := member.AddProjectMember(models.Member{
ProjectID: projectID2,
EntityID: int(nonSysAdminID),
EntityType: common.UserMember,
Role: common.RoleGuest,
})
require.Nil(t, err)
defer member.DeleteProjectMemberByID(memberID2)
// add a repository in project "search"
err = dao.AddRepository(models.RepoRecord{
ProjectID: projectID1,
Name: "search/hello-world",
})
require.Nil(t, err)
// add a repository in project "search-2"
err = dao.AddRepository(models.RepoRecord{
ProjectID: projectID2,
Name: "search-2/hello-world",
})
require.Nil(t, err)
// search without login
result := &searchResult{}
err = handleAndParse(&testingRequest{
method: http.MethodGet,
url: "/api/search",
queryStruct: struct {
Keyword string `url:"q"`
}{
Keyword: "search",
},
}, result)
require.Nil(t, err)
require.Equal(t, 1, len(result.Project))
require.Equal(t, 1, len(result.Repository))
assert.Equal(t, "search", result.Project[0].Name)
assert.Equal(t, "search/hello-world", result.Repository[0]["repository_name"].(string))
// search with user who is the member of the project
err = handleAndParse(&testingRequest{
method: http.MethodGet,
url: "/api/search",
queryStruct: struct {
Keyword string `url:"q"`
}{
Keyword: "search",
},
credential: nonSysAdmin,
}, result)
require.Nil(t, err)
require.Equal(t, 2, len(result.Project))
require.Equal(t, 2, len(result.Repository))
projects := map[string]struct{}{}
repositories := map[string]struct{}{}
for _, project := range result.Project {
projects[project.Name] = struct{}{}
}
for _, repository := range result.Repository {
repositories[repository["repository_name"].(string)] = struct{}{}
}
_, exist := projects["search"]
assert.True(t, exist)
_, exist = projects["search-2"]
assert.True(t, exist)
_, exist = repositories["search/hello-world"]
assert.True(t, exist)
_, exist = repositories["search-2/hello-world"]
assert.True(t, exist)
// search with system admin
err = handleAndParse(&testingRequest{
method: http.MethodGet,
url: "/api/search",
queryStruct: struct {
Keyword string `url:"q"`
}{
Keyword: "search",
},
credential: sysAdmin,
}, result)
require.Nil(t, err)
require.Equal(t, 2, len(result.Project))
require.Equal(t, 2, len(result.Repository))
projects = map[string]struct{}{}
repositories = map[string]struct{}{}
for _, project := range result.Project {
projects[project.Name] = struct{}{}
}
for _, repository := range result.Repository {
repositories[repository["repository_name"].(string)] = struct{}{}
}
_, exist = projects["search"]
assert.True(t, exist)
_, exist = projects["search-2"]
assert.True(t, exist)
_, exist = repositories["search/hello-world"]
assert.True(t, exist)
_, exist = repositories["search-2/hello-world"]
assert.True(t, exist)
chartSettings := map[string]interface{}{
common.WithChartMuseum: true,
}
config.InitWithSettings(chartSettings)
defer func() {
// reset config
config.Init()
}()
// Search chart
err = handleAndParse(&testingRequest{
method: http.MethodGet,
url: "/api/search",
queryStruct: struct {
Keyword string `url:"q"`
}{
Keyword: "harbor",
},
credential: sysAdmin,
}, result)
require.Nil(t, err)
require.Equal(t, 1, len(*(result.Chart)))
require.Equal(t, "library/harbor", (*result.Chart)[0].Name)
// Restore chart search handler
searchHandler = nil
}

View File

@ -37,6 +37,7 @@ func New() http.Handler {
ScannerAPI: newScannerAPI(),
ScanAPI: newScanAPI(),
ScanAllAPI: newScanAllAPI(),
SearchAPI: newSearchAPI(),
ProjectAPI: newProjectAPI(),
PreheatAPI: newPreheatAPI(),
IconAPI: newIconAPI(),

View File

@ -0,0 +1,255 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package handler
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/common/security/local"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/controller/repository"
"github.com/goharbor/harbor/src/core/api"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
"github.com/goharbor/harbor/src/server/v2.0/models"
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/search"
"helm.sh/helm/v3/cmd/helm/search"
)
func newSearchAPI() *searchAPI {
return &searchAPI{
artifactCtl: artifact.Ctl,
projectCtl: project.Ctl,
repositoryCtl: repository.Ctl,
chartMuseumEnabled: config.WithChartMuseum(),
searchCharts: func(q string, namespaces []string) ([]*search.Result, error) {
return api.GetChartController().SearchChart(q, namespaces)
},
}
}
type searchAPI struct {
BaseAPI
artifactCtl artifact.Controller
projectCtl project.Controller
repositoryCtl repository.Controller
chartMuseumEnabled bool
searchCharts func(string, []string) ([]*search.Result, error)
}
func (s *searchAPI) Search(ctx context.Context, params operation.SearchParams) middleware.Responder {
secCtx, ok := security.FromContext(ctx)
if !ok {
return s.SendError(ctx, fmt.Errorf("security not found in the context"))
}
kw := q.KeyWords{}
if !secCtx.IsSysAdmin() {
if sc, ok := secCtx.(*local.SecurityContext); ok && sc.IsAuthenticated() {
user := sc.User()
kw["member"] = &project.MemberQuery{
UserID: user.UserID,
GroupIDs: user.GroupIDs,
WithPublic: true,
}
} else {
kw["public"] = true
}
}
projects, err := s.projectCtl.List(ctx, q.New(kw))
if err != nil {
return s.SendError(ctx, err)
}
projectResult := []*models.Project{}
proNames := []string{}
for _, p := range projects {
proNames = append(proNames, p.Name)
if params.Q != "" && !strings.Contains(p.Name, params.Q) {
continue
}
if sc, ok := secCtx.(*local.SecurityContext); ok && sc.IsAuthenticated() {
roles, err := s.projectCtl.ListRoles(ctx, p.ProjectID, sc.User())
if err != nil {
return s.SendError(ctx, errors.Wrap(err, "failed to list roles"))
}
p.RoleList = roles
p.Role = highestRole(roles)
}
total, err := s.repositoryCtl.Count(ctx, q.New(q.KeyWords{"project_id": p.ProjectID}))
if err != nil {
log.Errorf("failed to get total of repositories of project %d: %v", p.ProjectID, err)
return s.SendError(ctx, errors.Wrapf(err, "failed to get total of repositories of project %d", p.ProjectID))
}
p.RepoCount = total
projectResult = append(projectResult, model.NewProject(p).ToSwagger())
}
repositoryResult, err := s.filterRepositories(ctx, projects, params.Q)
if err != nil {
log.Errorf("failed to filter repositories: %v", err)
return s.SendError(ctx, errors.Wrap(err, "failed to filter repositories"))
}
chartResult, err := s.filterCharts(ctx, params.Q, proNames)
if err != nil {
log.Errorf("failed to filter charts: %v", err)
return s.SendError(ctx, errors.Wrap(err, "failed to filter charts"))
}
return newSearchOK().WithPayload(&models.Search{
Project: projectResult,
Repository: repositoryResult,
Chart: chartResult,
})
}
func (s *searchAPI) filterRepositories(ctx context.Context, projects []*project.Project, keyword string) ([]*models.SearchRepository, error) {
result := []*models.SearchRepository{}
if len(projects) == 0 {
return result, nil
}
repositories, err := s.repositoryCtl.List(ctx, q.New(q.KeyWords{"name": &q.FuzzyMatchValue{Value: keyword}}))
if err != nil {
return nil, err
}
if len(repositories) == 0 {
return result, nil
}
projectMap := map[string]*project.Project{}
for _, project := range projects {
projectMap[project.Name] = project
}
for _, repository := range repositories {
projectName, _ := utils.ParseRepository(repository.Name)
project, exist := projectMap[projectName]
if !exist {
continue
}
entry := models.SearchRepository{
RepositoryName: repository.Name,
ProjectName: project.Name,
ProjectID: repository.ProjectID,
ProjectPublic: project.IsPublic(),
PullCount: repository.PullCount,
}
count, err := s.artifactCtl.Count(ctx, q.New(q.KeyWords{"RepositoryID": repository.RepositoryID}))
if err != nil {
log.Errorf("failed to get the count of artifacts under the repository %s: %v",
repository.Name, err)
} else {
entry.ArtifactCount = count
}
result = append(result, &entry)
}
return result, nil
}
func (s *searchAPI) filterCharts(ctx context.Context, q string, namespaces []string) ([]*models.SearchResult, error) {
if !s.chartMuseumEnabled {
return nil, nil
}
result := []*models.SearchResult{}
if len(namespaces) == 0 {
return result, nil
}
charts, err := s.searchCharts(q, namespaces)
if err != nil {
return nil, err
}
for _, chart := range charts {
var entry models.SearchResult
if err := lib.JSONCopy(&entry, chart); err != nil {
return nil, err
}
result = append(result, &entry)
}
return result, nil
}
// searchOK removing the chart from the response when the chartmuseum is disabled
type searchOK struct {
Payload interface{}
}
func (o *searchOK) WithPayload(payload *models.Search) *searchOK {
if payload != nil {
p := &struct {
Chart *[]*models.SearchResult `json:"chart,omitempty"`
Project []*models.Project `json:"project"`
Repository []*models.SearchRepository `json:"repository"`
}{
Project: payload.Project,
Repository: payload.Repository,
}
if payload.Chart != nil {
p.Chart = &payload.Chart
}
o.Payload = p
}
return o
}
func (o *searchOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(200)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}
func newSearchOK() *searchOK {
return &searchOK{}
}

View File

@ -34,7 +34,6 @@ func registerLegacyRoutes() {
beego.Router("/api/"+version+"/usergroups/?:ugid([0-9]+)", &api.UserGroupAPI{})
beego.Router("/api/"+version+"/email/ping", &api.EmailAPI{}, "post:Ping")
beego.Router("/api/"+version+"/health", &api.HealthAPI{}, "get:CheckHealth")
beego.Router("/api/"+version+"/search", &api.SearchAPI{})
beego.Router("/api/"+version+"/projects/:id([0-9]+)/metadatas/?:name", &api.MetadataAPI{}, "get:Get")
beego.Router("/api/"+version+"/projects/:id([0-9]+)/metadatas/", &api.MetadataAPI{}, "post:Post")