mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-22 18:25:56 +01:00
refactor: generate search API by go-swagger (#14422)
Signed-off-by: He Weiwei <hweiwei@vmware.com>
This commit is contained in:
parent
634be34236
commit
a2b08446d7
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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 `
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -37,6 +37,7 @@ func New() http.Handler {
|
||||
ScannerAPI: newScannerAPI(),
|
||||
ScanAPI: newScanAPI(),
|
||||
ScanAllAPI: newScanAllAPI(),
|
||||
SearchAPI: newSearchAPI(),
|
||||
ProjectAPI: newProjectAPI(),
|
||||
PreheatAPI: newPreheatAPI(),
|
||||
IconAPI: newIconAPI(),
|
||||
|
255
src/server/v2.0/handler/search.go
Normal file
255
src/server/v2.0/handler/search.go
Normal 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{}
|
||||
}
|
@ -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")
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user