mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-27 04:35:16 +01:00
Merge pull request #8384 from heww/quota-apis
feat(quota,api): APIs for quotas
This commit is contained in:
commit
f3a2280033
@ -311,6 +311,34 @@ paths:
|
||||
description: User need to log in first.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/projects/{project_id}/summary':
|
||||
get:
|
||||
summary: Get summary of the project.
|
||||
description: Get summary of the project.
|
||||
parameters:
|
||||
- name: project_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: Relevant project ID
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Get summary of the project successfully.
|
||||
schema:
|
||||
$ref: '#/definitions/ProjectSummary'
|
||||
'400':
|
||||
description: Illegal format of provided ID value.
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'404':
|
||||
description: Project ID does not exist.
|
||||
'403':
|
||||
description: User does not have permission to get summary of the project.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/projects/{project_id}/metadatas':
|
||||
get:
|
||||
summary: Get project metadata.
|
||||
@ -3547,6 +3575,113 @@ paths:
|
||||
description: User does not have permission to call this API.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/quotas':
|
||||
get:
|
||||
summary: List quotas
|
||||
description: List quotas
|
||||
tags:
|
||||
- quota
|
||||
parameters:
|
||||
- name: reference
|
||||
in: query
|
||||
description: The reference type of quota.
|
||||
required: false
|
||||
type: string
|
||||
- name: sort
|
||||
in: query
|
||||
type: string
|
||||
required: false
|
||||
description: |
|
||||
Sort method, valid values include:
|
||||
'hard.resource_name', '-hard.resource_name', 'used.resource_name', '-used.resource_name'.
|
||||
Here '-' stands for descending order, resource_name should be the real resource name of the quota.
|
||||
- name: page
|
||||
in: query
|
||||
type: integer
|
||||
format: int32
|
||||
required: false
|
||||
description: 'The page nubmer, default is 1.'
|
||||
- name: page_size
|
||||
in: query
|
||||
type: integer
|
||||
format: int32
|
||||
required: false
|
||||
description: 'The size of per page, default is 10, maximum is 100.'
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully retrieved the quotas.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Quota'
|
||||
headers:
|
||||
X-Total-Count:
|
||||
description: The total count of access logs
|
||||
type: integer
|
||||
Link:
|
||||
description: Link refers to the previous page and next page
|
||||
type: string
|
||||
'401':
|
||||
description: User is not authenticated.
|
||||
'403':
|
||||
description: User does not have permission to call this API.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/quotas/{id}':
|
||||
get:
|
||||
summary: Get the specified quota
|
||||
description: Get the specified quota
|
||||
tags:
|
||||
- quota
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
type: integer
|
||||
required: true
|
||||
description: Quota ID
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully retrieved the quota.
|
||||
schema:
|
||||
$ref: '#/definitions/Quota'
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'403':
|
||||
description: User does not have permission to call this API
|
||||
'404':
|
||||
description: Quota does not exist.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
put:
|
||||
summary: Update the specified quota
|
||||
description: Update hard limits of the specified quota
|
||||
tags:
|
||||
- quota
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
type: integer
|
||||
required: true
|
||||
description: Quota ID
|
||||
- name: hard
|
||||
in: body
|
||||
required: true
|
||||
description: The new hard limits for the quota
|
||||
schema:
|
||||
$ref: '#/definitions/QuotaUpdateReq'
|
||||
responses:
|
||||
'200':
|
||||
description: Updated quota hard limits successfully.
|
||||
'400':
|
||||
description: Illegal format of quota update request.
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'403':
|
||||
description: User does not have permission to the quota.
|
||||
'404':
|
||||
description: Quota ID does not exist.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
responses:
|
||||
OK:
|
||||
description: 'Success'
|
||||
@ -3699,6 +3834,36 @@ definitions:
|
||||
auto_scan:
|
||||
type: string
|
||||
description: 'Whether scan images automatically when pushing. The valid values are "true", "false".'
|
||||
ProjectSummary:
|
||||
type: object
|
||||
properties:
|
||||
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.
|
||||
project_admin_count:
|
||||
type: integer
|
||||
description: The total number of project admin members.
|
||||
master_count:
|
||||
type: integer
|
||||
description: The total number of master members.
|
||||
developer_count:
|
||||
type: integer
|
||||
description: The total number of developer members.
|
||||
guest_count:
|
||||
type: integer
|
||||
description: The total number of guest members.
|
||||
quota:
|
||||
type: object
|
||||
properties:
|
||||
hard:
|
||||
$ref: "#/definitions/ResourceList"
|
||||
description: The hard limits of the quota
|
||||
used:
|
||||
$ref: "#/definitions/ResourceList"
|
||||
description: The used status of the quota
|
||||
Manifest:
|
||||
type: object
|
||||
properties:
|
||||
@ -5186,3 +5351,38 @@ definitions:
|
||||
cve_id:
|
||||
type: string
|
||||
description: The ID of the CVE, such as "CVE-2019-10164"
|
||||
ResourceList:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: integer
|
||||
QuotaUpdateReq:
|
||||
type: object
|
||||
properties:
|
||||
hard:
|
||||
$ref: "#/definitions/ResourceList"
|
||||
description: The new hard limits for the quota
|
||||
QuotaRefObject:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
Quota:
|
||||
type: object
|
||||
description: The quota object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
description: ID of the quota
|
||||
ref:
|
||||
$ref: "#/definitions/QuotaRefObject"
|
||||
description: The reference object of the quota
|
||||
hard:
|
||||
$ref: "#/definitions/ResourceList"
|
||||
description: The hard limits of the quota
|
||||
used:
|
||||
$ref: "#/definitions/ResourceList"
|
||||
description: The used status of the quota
|
||||
creation_time:
|
||||
type: string
|
||||
description: the creation time of the quota
|
||||
update_time:
|
||||
type: string
|
||||
description: the update time of the quota
|
@ -30,13 +30,13 @@ func GetProjectMember(queryMember models.Member) ([]*models.Member, error) {
|
||||
}
|
||||
|
||||
o := dao.GetOrmer()
|
||||
sql := ` select a.* from (select pm.id as id, pm.project_id as project_id, ug.id as entity_id, ug.group_name as entity_name, ug.creation_time, ug.update_time, r.name as rolename,
|
||||
r.role_id as role, pm.entity_type as entity_type from user_group ug join project_member pm
|
||||
sql := ` select a.* from (select pm.id as id, pm.project_id as project_id, ug.id as entity_id, ug.group_name as entity_name, ug.creation_time, ug.update_time, r.name as rolename,
|
||||
r.role_id as role, pm.entity_type as entity_type from user_group ug join project_member pm
|
||||
on pm.project_id = ? and ug.id = pm.entity_id join role r on pm.role = r.role_id where pm.entity_type = 'g'
|
||||
union
|
||||
select pm.id as id, pm.project_id as project_id, u.user_id as entity_id, u.username as entity_name, u.creation_time, u.update_time, r.name as rolename,
|
||||
r.role_id as role, pm.entity_type as entity_type from harbor_user u join project_member pm
|
||||
on pm.project_id = ? and u.user_id = pm.entity_id
|
||||
select pm.id as id, pm.project_id as project_id, u.user_id as entity_id, u.username as entity_name, u.creation_time, u.update_time, r.name as rolename,
|
||||
r.role_id as role, pm.entity_type as entity_type from harbor_user u join project_member pm
|
||||
on pm.project_id = ? and u.user_id = pm.entity_id
|
||||
join role r on pm.role = r.role_id where u.deleted = false and pm.entity_type = 'u') as a where a.project_id = ? `
|
||||
|
||||
queryParam := make([]interface{}, 1)
|
||||
@ -70,6 +70,27 @@ func GetProjectMember(queryMember models.Member) ([]*models.Member, error) {
|
||||
return members, err
|
||||
}
|
||||
|
||||
// GetTotalOfProjectMembers returns total of project members
|
||||
func GetTotalOfProjectMembers(projectID int64, roles ...int) (int64, error) {
|
||||
log.Debugf("Query condition %+v", projectID)
|
||||
if projectID == 0 {
|
||||
return 0, fmt.Errorf("failed to get total of project members, project id required %v", projectID)
|
||||
}
|
||||
|
||||
sql := "SELECT COUNT(1) FROM project_member WHERE project_id = ?"
|
||||
|
||||
queryParam := []interface{}{projectID}
|
||||
|
||||
if len(roles) > 0 {
|
||||
sql += " AND role = ?"
|
||||
queryParam = append(queryParam, roles[0])
|
||||
}
|
||||
|
||||
var count int64
|
||||
err := dao.GetOrmer().Raw(sql, queryParam).QueryRow(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
// AddProjectMember inserts a record to table project_member
|
||||
func AddProjectMember(member models.Member) (int, error) {
|
||||
|
||||
@ -120,23 +141,23 @@ func DeleteProjectMemberByID(pmid int) error {
|
||||
// SearchMemberByName search members of the project by entity_name
|
||||
func SearchMemberByName(projectID int64, entityName string) ([]*models.Member, error) {
|
||||
o := dao.GetOrmer()
|
||||
sql := `select pm.id, pm.project_id,
|
||||
u.username as entity_name,
|
||||
sql := `select pm.id, pm.project_id,
|
||||
u.username as entity_name,
|
||||
r.name as rolename,
|
||||
pm.role, pm.entity_id, pm.entity_type
|
||||
pm.role, pm.entity_id, pm.entity_type
|
||||
from project_member pm
|
||||
left join harbor_user u on pm.entity_id = u.user_id and pm.entity_type = 'u'
|
||||
left join role r on pm.role = r.role_id
|
||||
where u.deleted = false and pm.project_id = ? and u.username like ?
|
||||
where u.deleted = false and pm.project_id = ? and u.username like ?
|
||||
union
|
||||
select pm.id, pm.project_id,
|
||||
ug.group_name as entity_name,
|
||||
select pm.id, pm.project_id,
|
||||
ug.group_name as entity_name,
|
||||
r.name as rolename,
|
||||
pm.role, pm.entity_id, pm.entity_type
|
||||
pm.role, pm.entity_id, pm.entity_type
|
||||
from project_member pm
|
||||
left join user_group ug on pm.entity_id = ug.id and pm.entity_type = 'g'
|
||||
left join role r on pm.role = r.role_id
|
||||
where pm.project_id = ? and ug.group_name like ?
|
||||
where pm.project_id = ? and ug.group_name like ?
|
||||
order by entity_name `
|
||||
queryParam := make([]interface{}, 4)
|
||||
queryParam = append(queryParam, projectID)
|
||||
|
@ -51,11 +51,18 @@ func TestMain(m *testing.M) {
|
||||
"update project set owner_id = (select user_id from harbor_user where username = 'member_test_01') where name = 'member_test_01'",
|
||||
"insert into project_member (project_id, entity_id, entity_type, role) values ( (select project_id from project where name = 'member_test_01') , (select user_id from harbor_user where username = 'member_test_01'), 'u', 1)",
|
||||
"insert into project_member (project_id, entity_id, entity_type, role) values ( (select project_id from project where name = 'member_test_01') , (select id from user_group where group_name = 'test_group_01'), 'g', 1)",
|
||||
|
||||
"insert into harbor_user (username, email, password, realname) values ('member_test_02', 'member_test_02@example.com', '123456', 'member_test_02')",
|
||||
"insert into project (name, owner_id) values ('member_test_02', 1)",
|
||||
"insert into user_group (group_name, group_type, ldap_group_dn) values ('test_group_02', 1, 'CN=harbor_users,OU=sample,OU=vmware,DC=harbor,DC=com')",
|
||||
"update project set owner_id = (select user_id from harbor_user where username = 'member_test_02') where name = 'member_test_02'",
|
||||
"insert into project_member (project_id, entity_id, entity_type, role) values ( (select project_id from project where name = 'member_test_02') , (select user_id from harbor_user where username = 'member_test_02'), 'u', 1)",
|
||||
"insert into project_member (project_id, entity_id, entity_type, role) values ( (select project_id from project where name = 'member_test_02') , (select id from user_group where group_name = 'test_group_02'), 'g', 1)",
|
||||
}
|
||||
|
||||
clearSqls := []string{
|
||||
"delete from project where name='member_test_01'",
|
||||
"delete from harbor_user where username='member_test_01' or username='pm_sample'",
|
||||
"delete from project where name='member_test_01' or name='member_test_02'",
|
||||
"delete from harbor_user where username='member_test_01' or username='member_test_02' or username='pm_sample'",
|
||||
"delete from user_group",
|
||||
"delete from project_member",
|
||||
}
|
||||
@ -285,6 +292,39 @@ func TestGetProjectMember(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetTotalOfProjectMembers(t *testing.T) {
|
||||
currentProject, _ := dao.GetProjectByName("member_test_02")
|
||||
|
||||
type args struct {
|
||||
projectID int64
|
||||
roles []int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int64
|
||||
wantErr bool
|
||||
}{
|
||||
{"Get total of project admin", args{currentProject.ProjectID, []int{common.RoleProjectAdmin}}, 2, false},
|
||||
{"Get total of master", args{currentProject.ProjectID, []int{common.RoleMaster}}, 0, false},
|
||||
{"Get total of developer", args{currentProject.ProjectID, []int{common.RoleDeveloper}}, 0, false},
|
||||
{"Get total of guest", args{currentProject.ProjectID, []int{common.RoleGuest}}, 0, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetTotalOfProjectMembers(tt.args.projectID, tt.args.roles...)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetTotalOfProjectMembers() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("GetTotalOfProjectMembers() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func PrepareGroupTest() {
|
||||
initSqls := []string{
|
||||
`insert into user_group (group_name, group_type, ldap_group_dn) values ('harbor_group_01', 1, 'cn=harbor_user,dc=example,dc=com')`,
|
||||
|
@ -15,25 +15,26 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/astaxie/beego/orm"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/quota/driver"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/pkg/types"
|
||||
)
|
||||
|
||||
var (
|
||||
quotaOrderMap = map[string]string{
|
||||
"id": "id asc",
|
||||
"+id": "id asc",
|
||||
"-id": "id desc",
|
||||
"creation_time": "creation_time asc",
|
||||
"+creation_time": "creation_time asc",
|
||||
"-creation_time": "creation_time desc",
|
||||
"update_time": "update_time asc",
|
||||
"+update_time": "update_time asc",
|
||||
"-update_time": "update_time desc",
|
||||
"creation_time": "b.creation_time asc",
|
||||
"+creation_time": "b.creation_time asc",
|
||||
"-creation_time": "b.creation_time desc",
|
||||
"update_time": "b.update_time asc",
|
||||
"+update_time": "b.update_time asc",
|
||||
"-update_time": "b.update_time desc",
|
||||
}
|
||||
)
|
||||
|
||||
@ -62,10 +63,58 @@ func UpdateQuota(quota models.Quota) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Quota quota mode for api
|
||||
type Quota struct {
|
||||
ID int64 `orm:"pk;auto;column(id)" json:"id"`
|
||||
Ref driver.RefObject `json:"ref"`
|
||||
Reference string `orm:"column(reference)" json:"-"`
|
||||
ReferenceID string `orm:"column(reference_id)" json:"-"`
|
||||
Hard string `orm:"column(hard);type(jsonb)" json:"-"`
|
||||
Used string `orm:"column(used);type(jsonb)" json:"-"`
|
||||
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (q *Quota) MarshalJSON() ([]byte, error) {
|
||||
hard, err := types.NewResourceList(q.Hard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
used, err := types.NewResourceList(q.Used)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type Alias Quota
|
||||
return json.Marshal(&struct {
|
||||
*Alias
|
||||
Hard types.ResourceList `json:"hard"`
|
||||
Used types.ResourceList `json:"used"`
|
||||
}{
|
||||
Alias: (*Alias)(q),
|
||||
Hard: hard,
|
||||
Used: used,
|
||||
})
|
||||
}
|
||||
|
||||
// ListQuotas returns quotas by query.
|
||||
func ListQuotas(query ...*models.QuotaQuery) ([]*models.Quota, error) {
|
||||
func ListQuotas(query ...*models.QuotaQuery) ([]*Quota, error) {
|
||||
condition, params := quotaQueryConditions(query...)
|
||||
sql := fmt.Sprintf(`select * %s`, condition)
|
||||
|
||||
sql := fmt.Sprintf(`
|
||||
SELECT
|
||||
a.id,
|
||||
a.reference,
|
||||
a.reference_id,
|
||||
a.hard,
|
||||
b.used,
|
||||
b.creation_time,
|
||||
b.update_time
|
||||
FROM
|
||||
quota AS a
|
||||
JOIN quota_usage AS b ON a.id = b.id %s`, condition)
|
||||
|
||||
orderBy := quotaOrderBy(query...)
|
||||
if orderBy != "" {
|
||||
@ -84,42 +133,80 @@ func ListQuotas(query ...*models.QuotaQuery) ([]*models.Quota, error) {
|
||||
}
|
||||
}
|
||||
|
||||
var quotas []*models.Quota
|
||||
var quotas []*Quota
|
||||
if _, err := GetOrmer().Raw(sql, params).QueryRows("as); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, quota := range quotas {
|
||||
d, ok := driver.Get(quota.Reference)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
ref, err := d.Load(quota.ReferenceID)
|
||||
if err != nil {
|
||||
log.Warning(fmt.Sprintf("Load quota reference object (%s, %s) failed: %v", quota.Reference, quota.ReferenceID, err))
|
||||
continue
|
||||
}
|
||||
|
||||
quota.Ref = ref
|
||||
}
|
||||
|
||||
return quotas, nil
|
||||
}
|
||||
|
||||
// GetTotalOfQuotas returns total of quotas
|
||||
func GetTotalOfQuotas(query ...*models.QuotaQuery) (int64, error) {
|
||||
condition, params := quotaQueryConditions(query...)
|
||||
sql := fmt.Sprintf("SELECT COUNT(1) FROM quota AS a JOIN quota_usage AS b ON a.id = b.id %s", condition)
|
||||
|
||||
var count int64
|
||||
if err := GetOrmer().Raw(sql, params).QueryRow(&count); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func quotaQueryConditions(query ...*models.QuotaQuery) (string, []interface{}) {
|
||||
params := []interface{}{}
|
||||
sql := `from quota `
|
||||
sql := ""
|
||||
if len(query) == 0 || query[0] == nil {
|
||||
return sql, params
|
||||
}
|
||||
|
||||
sql += `where 1=1 `
|
||||
sql += `WHERE 1=1 `
|
||||
|
||||
q := query[0]
|
||||
if q.ID != 0 {
|
||||
sql += `AND a.id = ? `
|
||||
params = append(params, q.ID)
|
||||
}
|
||||
if q.Reference != "" {
|
||||
sql += `and reference = ? `
|
||||
sql += `AND a.reference = ? `
|
||||
params = append(params, q.Reference)
|
||||
}
|
||||
if q.ReferenceID != "" {
|
||||
sql += `and reference_id = ? `
|
||||
sql += `AND a.reference_id = ? `
|
||||
params = append(params, q.ReferenceID)
|
||||
}
|
||||
|
||||
if len(q.ReferenceIDs) != 0 {
|
||||
sql += fmt.Sprintf(`and reference_id in (%s) `, paramPlaceholder(len(q.ReferenceIDs)))
|
||||
sql += fmt.Sprintf(`AND a.reference_id IN (%s) `, paramPlaceholder(len(q.ReferenceIDs)))
|
||||
params = append(params, q.ReferenceIDs)
|
||||
}
|
||||
|
||||
return sql, params
|
||||
}
|
||||
|
||||
func castQuantity(field string) string {
|
||||
// cast -1 to max int64 when order by field
|
||||
return fmt.Sprintf("CAST( (CASE WHEN (%[1]s) IS NULL THEN '0' WHEN (%[1]s) = '-1' THEN '9223372036854775807' ELSE (%[1]s) END) AS BIGINT )", field)
|
||||
}
|
||||
|
||||
func quotaOrderBy(query ...*models.QuotaQuery) string {
|
||||
orderBy := ""
|
||||
orderBy := "b.creation_time DESC"
|
||||
|
||||
if len(query) > 0 && query[0] != nil && query[0].Sort != "" {
|
||||
if val, ok := quotaOrderMap[query[0].Sort]; ok {
|
||||
@ -127,15 +214,19 @@ func quotaOrderBy(query ...*models.QuotaQuery) string {
|
||||
} else {
|
||||
sort := query[0].Sort
|
||||
|
||||
order := "asc"
|
||||
order := "ASC"
|
||||
if sort[0] == '-' {
|
||||
order = "desc"
|
||||
order = "DESC"
|
||||
sort = sort[1:]
|
||||
}
|
||||
|
||||
prefix := "hard."
|
||||
if strings.HasPrefix(sort, prefix) {
|
||||
orderBy = fmt.Sprintf("hard->>'%s' %s", strings.TrimPrefix(sort, prefix), order)
|
||||
prefix := []string{"hard.", "used."}
|
||||
for _, p := range prefix {
|
||||
if strings.HasPrefix(sort, p) {
|
||||
field := fmt.Sprintf("%s->>'%s'", strings.TrimSuffix(p, "."), strings.TrimPrefix(sort, p))
|
||||
orderBy = fmt.Sprintf("(%s) %s", castQuantity(field), order)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,8 @@ package models
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/types"
|
||||
)
|
||||
|
||||
// ProjectTable is the table name for project
|
||||
@ -183,3 +185,19 @@ type ProjectQueryResult struct {
|
||||
func (p *Project) TableName() string {
|
||||
return ProjectTable
|
||||
}
|
||||
|
||||
// ProjectSummary ...
|
||||
type ProjectSummary struct {
|
||||
RepoCount int64 `json:"repo_count"`
|
||||
ChartCount uint64 `json:"chart_count"`
|
||||
|
||||
ProjectAdminCount int64 `json:"project_admin_count"`
|
||||
MasterCount int64 `json:"master_count"`
|
||||
DeveloperCount int64 `json:"developer_count"`
|
||||
GuestCount int64 `json:"guest_count"`
|
||||
|
||||
Quota struct {
|
||||
Hard types.ResourceList `json:"hard"`
|
||||
Used types.ResourceList `json:"used"`
|
||||
} `json:"quota"`
|
||||
}
|
||||
|
@ -17,6 +17,8 @@ package models
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/types"
|
||||
)
|
||||
|
||||
// QuotaHard a map for the quota hard
|
||||
@ -69,9 +71,15 @@ func (q *Quota) SetHard(hard QuotaHard) {
|
||||
|
||||
// QuotaQuery query parameters for quota
|
||||
type QuotaQuery struct {
|
||||
ID int64
|
||||
Reference string
|
||||
ReferenceID string
|
||||
ReferenceIDs []string
|
||||
Pagination
|
||||
Sorting
|
||||
}
|
||||
|
||||
// QuotaUpdateRequest the request for quota update
|
||||
type QuotaUpdateRequest struct {
|
||||
Hard types.ResourceList `json:"hard"`
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/quota/driver"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/pkg/types"
|
||||
)
|
||||
|
||||
@ -207,6 +208,7 @@ func NewManager(reference string, referenceID string) (*Manager, error) {
|
||||
}
|
||||
|
||||
if _, err := d.Load(referenceID); err != nil {
|
||||
log.Warning(fmt.Sprintf("Load quota reference object (%s, %s) failed: %v", reference, referenceID, err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -230,7 +230,14 @@ func GetStrValueOfAnyType(value interface{}) string {
|
||||
}
|
||||
strVal = string(b)
|
||||
} else {
|
||||
strVal = fmt.Sprintf("%v", value)
|
||||
switch val := value.(type) {
|
||||
case float64:
|
||||
strVal = strconv.FormatFloat(val, 'f', -1, 64)
|
||||
case float32:
|
||||
strVal = strconv.FormatFloat(float64(val), 'f', -1, 32)
|
||||
default:
|
||||
strVal = fmt.Sprintf("%v", value)
|
||||
}
|
||||
}
|
||||
return strVal
|
||||
}
|
||||
|
@ -381,3 +381,31 @@ func TestTrimLower(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStrValueOfAnyType(t *testing.T) {
|
||||
type args struct {
|
||||
value interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{"float", args{float32(1048576.1)}, "1048576.1"},
|
||||
{"float", args{float64(1048576.12)}, "1048576.12"},
|
||||
{"float", args{1048576.000}, "1048576"},
|
||||
{"int", args{1048576}, "1048576"},
|
||||
{"int", args{9223372036854775807}, "9223372036854775807"},
|
||||
{"string", args{"hello world"}, "hello world"},
|
||||
{"bool", args{true}, "true"},
|
||||
{"bool", args{false}, "false"},
|
||||
{"map", args{map[string]interface{}{"key1": "value1"}}, "{\"key1\":\"value1\"}"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := GetStrValueOfAnyType(tt.args.value); got != tt.want {
|
||||
t.Errorf("GetStrValueOfAnyType() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -103,6 +103,7 @@ func init() {
|
||||
beego.Router("/api/users/:id/permissions", &UserAPI{}, "get:ListUserPermissions")
|
||||
beego.Router("/api/users/:id/sysadmin", &UserAPI{}, "put:ToggleUserAdminRole")
|
||||
beego.Router("/api/projects/:id([0-9]+)/logs", &ProjectAPI{}, "get:Logs")
|
||||
beego.Router("/api/projects/:id([0-9]+)/summary", &ProjectAPI{}, "get:Summary")
|
||||
beego.Router("/api/projects/:id([0-9]+)/_deletable", &ProjectAPI{}, "get:Deletable")
|
||||
beego.Router("/api/projects/:id([0-9]+)/metadatas/?:name", &MetadataAPI{}, "get:Get")
|
||||
beego.Router("/api/projects/:id([0-9]+)/metadatas/", &MetadataAPI{}, "post:Post")
|
||||
@ -180,6 +181,10 @@ func init() {
|
||||
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels", chartLabelAPIType, "get:GetLabels;post:MarkLabel")
|
||||
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels/:id([0-9]+)", chartLabelAPIType, "delete:RemoveLabel")
|
||||
|
||||
quotaAPIType := &QuotaAPI{}
|
||||
beego.Router("/api/quotas", quotaAPIType, "get:List")
|
||||
beego.Router("/api/quotas/:id([0-9]+)", quotaAPIType, "get:Get;put:Put")
|
||||
|
||||
// syncRegistry
|
||||
if err := SyncRegistry(config.GlobalProjectMgr); err != nil {
|
||||
log.Fatalf("failed to sync repositories from registry: %v", err)
|
||||
@ -454,6 +459,23 @@ func (a testapi) ProjectDeletable(prjUsr usrInfo, projectID int64) (int, bool, e
|
||||
return code, deletable.Deletable, nil
|
||||
}
|
||||
|
||||
// ProjectSummary returns summary for the project
|
||||
func (a testapi) ProjectSummary(prjUsr usrInfo, projectID string) (int, apilib.ProjectSummary, error) {
|
||||
_sling := sling.New().Get(a.basePath)
|
||||
|
||||
// create api path
|
||||
path := "api/projects/" + projectID + "/summary"
|
||||
_sling = _sling.Path(path)
|
||||
|
||||
var successPayload apilib.ProjectSummary
|
||||
|
||||
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, prjUsr)
|
||||
if err == nil && httpStatusCode == 200 {
|
||||
err = json.Unmarshal(body, &successPayload)
|
||||
}
|
||||
return httpStatusCode, successPayload, err
|
||||
}
|
||||
|
||||
// -------------------------Member Test---------------------------------------//
|
||||
|
||||
// Return relevant role members of projectID
|
||||
@ -1213,3 +1235,55 @@ func (a testapi) RegistryUpdate(authInfo usrInfo, registryID int64, req *apimode
|
||||
|
||||
return code, nil
|
||||
}
|
||||
|
||||
// QuotasGet returns quotas
|
||||
func (a testapi) QuotasGet(query *apilib.QuotaQuery, authInfo ...usrInfo) (int, []apilib.Quota, error) {
|
||||
_sling := sling.New().Get(a.basePath).
|
||||
Path("api/quotas").
|
||||
QueryStruct(query)
|
||||
|
||||
var successPayload []apilib.Quota
|
||||
|
||||
var httpStatusCode int
|
||||
var err error
|
||||
var body []byte
|
||||
if len(authInfo) > 0 {
|
||||
httpStatusCode, body, err = request(_sling, jsonAcceptHeader, authInfo[0])
|
||||
} else {
|
||||
httpStatusCode, body, err = request(_sling, jsonAcceptHeader)
|
||||
}
|
||||
|
||||
if err == nil && httpStatusCode == 200 {
|
||||
err = json.Unmarshal(body, &successPayload)
|
||||
} else {
|
||||
log.Println(string(body))
|
||||
}
|
||||
|
||||
return httpStatusCode, successPayload, err
|
||||
}
|
||||
|
||||
// Return specific quota
|
||||
func (a testapi) QuotasGetByID(authInfo usrInfo, quotaID string) (int, apilib.Quota, error) {
|
||||
_sling := sling.New().Get(a.basePath)
|
||||
|
||||
// create api path
|
||||
path := "api/quotas/" + quotaID
|
||||
_sling = _sling.Path(path)
|
||||
|
||||
var successPayload apilib.Quota
|
||||
|
||||
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
if err == nil && httpStatusCode == 200 {
|
||||
err = json.Unmarshal(body, &successPayload)
|
||||
}
|
||||
return httpStatusCode, successPayload, err
|
||||
}
|
||||
|
||||
// Update spec for the quota
|
||||
func (a testapi) QuotasPut(authInfo usrInfo, quotaID string, req models.QuotaUpdateRequest) (int, error) {
|
||||
path := "/api/quotas/" + quotaID
|
||||
_sling := sling.New().Put(a.basePath).Path(path).BodyJSON(req)
|
||||
|
||||
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
return httpStatusCode, err
|
||||
}
|
||||
|
@ -19,10 +19,12 @@ import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/project"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/quota"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
@ -577,6 +579,33 @@ func (p *ProjectAPI) Logs() {
|
||||
p.ServeJSON()
|
||||
}
|
||||
|
||||
// Summary returns the summary of the project
|
||||
func (p *ProjectAPI) Summary() {
|
||||
if !p.requireAccess(rbac.ActionRead) {
|
||||
return
|
||||
}
|
||||
|
||||
summary := &models.ProjectSummary{
|
||||
RepoCount: p.project.RepoCount,
|
||||
ChartCount: p.project.ChartCount,
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, fn := range []func(int64, *models.ProjectSummary){getProjectQuotaSummary, getProjectMemberSummary} {
|
||||
fn := fn
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
fn(p.project.ProjectID, summary)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
p.Data["json"] = summary
|
||||
p.ServeJSON()
|
||||
}
|
||||
|
||||
// TODO move this to pa ckage models
|
||||
func validateProjectReq(req *models.ProjectRequest) error {
|
||||
pn := req.Name
|
||||
@ -618,3 +647,50 @@ func projectQuotaHardLimits(req *models.ProjectRequest, setting *models.QuotaSet
|
||||
|
||||
return hardLimits, nil
|
||||
}
|
||||
|
||||
func getProjectQuotaSummary(projectID int64, summary *models.ProjectSummary) {
|
||||
quotas, err := dao.ListQuotas(&models.QuotaQuery{Reference: "project", ReferenceID: strconv.FormatInt(projectID, 10)})
|
||||
if err != nil {
|
||||
log.Debugf("failed to get quota for project: %d", projectID)
|
||||
return
|
||||
}
|
||||
|
||||
if len(quotas) == 0 {
|
||||
log.Debugf("quota not found for project: %d", projectID)
|
||||
return
|
||||
}
|
||||
|
||||
quota := quotas[0]
|
||||
|
||||
summary.Quota.Hard, _ = types.NewResourceList(quota.Hard)
|
||||
summary.Quota.Used, _ = types.NewResourceList(quota.Used)
|
||||
}
|
||||
|
||||
func getProjectMemberSummary(projectID int64, summary *models.ProjectSummary) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, e := range []struct {
|
||||
role int
|
||||
count *int64
|
||||
}{
|
||||
{common.RoleProjectAdmin, &summary.ProjectAdminCount},
|
||||
{common.RoleMaster, &summary.MasterCount},
|
||||
{common.RoleDeveloper, &summary.DeveloperCount},
|
||||
{common.RoleGuest, &summary.GuestCount},
|
||||
} {
|
||||
wg.Add(1)
|
||||
go func(role int, count *int64) {
|
||||
defer wg.Done()
|
||||
|
||||
total, err := project.GetTotalOfProjectMembers(projectID, role)
|
||||
if err != nil {
|
||||
log.Debugf("failed to get total of project members of role %d", role)
|
||||
return
|
||||
}
|
||||
|
||||
*count = total
|
||||
}(e.role, e.count)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
@ -30,6 +30,42 @@ import (
|
||||
var addProject *apilib.ProjectReq
|
||||
var addPID int
|
||||
|
||||
func addProjectByName(apiTest *testapi, projectName string) (int32, error) {
|
||||
req := apilib.ProjectReq{ProjectName: projectName}
|
||||
code, err := apiTest.ProjectsPost(*admin, req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if code != http.StatusCreated {
|
||||
return 0, fmt.Errorf("created failed")
|
||||
}
|
||||
|
||||
code, projects, err := apiTest.ProjectsGet(&apilib.ProjectQuery{Name: projectName}, *admin)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
return 0, fmt.Errorf("get failed")
|
||||
}
|
||||
|
||||
if len(projects) == 0 {
|
||||
return 0, fmt.Errorf("oops")
|
||||
}
|
||||
|
||||
return projects[0].ProjectId, nil
|
||||
}
|
||||
|
||||
func deleteProjectByIDs(apiTest *testapi, projectIDs ...int32) error {
|
||||
for _, projectID := range projectIDs {
|
||||
_, err := apiTest.ProjectsDelete(*admin, fmt.Sprintf("%d", projectID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitAddPro() {
|
||||
addProject = &apilib.ProjectReq{ProjectName: "add_project", Metadata: map[string]string{models.ProMetaPublic: "true"}}
|
||||
}
|
||||
@ -448,3 +484,30 @@ func TestDeletable(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
assert.False(t, del)
|
||||
}
|
||||
|
||||
func TestProjectSummary(t *testing.T) {
|
||||
fmt.Println("\nTest for Project Summary API")
|
||||
assert := assert.New(t)
|
||||
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
projectID, err := addProjectByName(apiTest, "project-summary")
|
||||
assert.Nil(err)
|
||||
defer func() {
|
||||
deleteProjectByIDs(apiTest, projectID)
|
||||
}()
|
||||
|
||||
// ----------------------------case 1 : Response Code=200----------------------------//
|
||||
fmt.Println("case 1: respose code:200")
|
||||
httpStatusCode, summary, err := apiTest.ProjectSummary(*admin, fmt.Sprintf("%d", projectID))
|
||||
if err != nil {
|
||||
t.Error("Error while search project by proName", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
||||
assert.Equal(int64(1), summary.ProjectAdminCount)
|
||||
assert.Equal(map[string]int64{"count": -1, "storage": -1}, summary.Quota.Hard)
|
||||
}
|
||||
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
155
src/core/api/quota.go
Normal file
155
src/core/api/quota.go
Normal file
@ -0,0 +1,155 @@
|
||||
// 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/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/quota"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// QuotaAPI handles request to /api/quotas/
|
||||
type QuotaAPI struct {
|
||||
BaseController
|
||||
quota *models.Quota
|
||||
}
|
||||
|
||||
// Prepare validates the URL and the user
|
||||
func (qa *QuotaAPI) Prepare() {
|
||||
qa.BaseController.Prepare()
|
||||
|
||||
if !qa.SecurityCtx.IsAuthenticated() {
|
||||
qa.SendUnAuthorizedError(errors.New("Unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
if !qa.SecurityCtx.IsSysAdmin() {
|
||||
qa.SendForbiddenError(errors.New(qa.SecurityCtx.GetUsername()))
|
||||
return
|
||||
}
|
||||
|
||||
if len(qa.GetStringFromPath(":id")) != 0 {
|
||||
id, err := qa.GetInt64FromPath(":id")
|
||||
if err != nil || id <= 0 {
|
||||
text := "invalid quota ID: "
|
||||
if err != nil {
|
||||
text += err.Error()
|
||||
} else {
|
||||
text += fmt.Sprintf("%d", id)
|
||||
}
|
||||
qa.SendBadRequestError(errors.New(text))
|
||||
return
|
||||
}
|
||||
|
||||
quota, err := dao.GetQuota(id)
|
||||
if err != nil {
|
||||
qa.SendInternalServerError(fmt.Errorf("failed to get quota %d, error: %v", id, err))
|
||||
return
|
||||
}
|
||||
|
||||
if quota == nil {
|
||||
qa.SendNotFoundError(fmt.Errorf("quota %d not found", id))
|
||||
return
|
||||
}
|
||||
|
||||
qa.quota = quota
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns quota by id
|
||||
func (qa *QuotaAPI) Get() {
|
||||
query := &models.QuotaQuery{
|
||||
ID: qa.quota.ID,
|
||||
}
|
||||
|
||||
quotas, err := dao.ListQuotas(query)
|
||||
if err != nil {
|
||||
qa.SendInternalServerError(fmt.Errorf("failed to get quota %d, error: %v", qa.quota.ID, err))
|
||||
return
|
||||
}
|
||||
|
||||
if len(quotas) == 0 {
|
||||
qa.SendNotFoundError(fmt.Errorf("quota %d not found", qa.quota.ID))
|
||||
return
|
||||
}
|
||||
|
||||
qa.Data["json"] = quotas[0]
|
||||
qa.ServeJSON()
|
||||
}
|
||||
|
||||
// Put update the quota
|
||||
func (qa *QuotaAPI) Put() {
|
||||
var req *models.QuotaUpdateRequest
|
||||
if err := qa.DecodeJSONReq(&req); err != nil {
|
||||
qa.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := quota.Validate(qa.quota.Reference, req.Hard); err != nil {
|
||||
qa.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
|
||||
mgr, err := quota.NewManager(qa.quota.Reference, qa.quota.ReferenceID)
|
||||
if err != nil {
|
||||
qa.SendInternalServerError(fmt.Errorf("failed to create quota manager, error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := mgr.UpdateQuota(req.Hard); err != nil {
|
||||
qa.SendInternalServerError(fmt.Errorf("failed to update hard limits of the quota, error: %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// List returns quotas by query
|
||||
func (qa *QuotaAPI) List() {
|
||||
page, size, err := qa.GetPaginationParams()
|
||||
if err != nil {
|
||||
qa.SendBadRequestError(err)
|
||||
return
|
||||
}
|
||||
|
||||
query := &models.QuotaQuery{
|
||||
Reference: qa.GetString("reference"),
|
||||
ReferenceID: qa.GetString("reference_id"),
|
||||
Pagination: models.Pagination{
|
||||
Page: page,
|
||||
Size: size,
|
||||
},
|
||||
Sorting: models.Sorting{
|
||||
Sort: qa.GetString("sort"),
|
||||
},
|
||||
}
|
||||
|
||||
total, err := dao.GetTotalOfQuotas(query)
|
||||
if err != nil {
|
||||
qa.SendInternalServerError(fmt.Errorf("failed to query database for total of quotas, error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
quotas, err := dao.ListQuotas(query)
|
||||
if err != nil {
|
||||
qa.SendInternalServerError(fmt.Errorf("failed to query database for quotas, error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
qa.SetPaginationHeader(total, page, size)
|
||||
qa.Data["json"] = quotas
|
||||
qa.ServeJSON()
|
||||
}
|
133
src/core/api/quota_test.go
Normal file
133
src/core/api/quota_test.go
Normal file
@ -0,0 +1,133 @@
|
||||
// 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"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/quota"
|
||||
"github.com/goharbor/harbor/src/common/quota/driver"
|
||||
"github.com/goharbor/harbor/src/common/quota/driver/mocks"
|
||||
"github.com/goharbor/harbor/src/pkg/types"
|
||||
"github.com/goharbor/harbor/src/testing/apitests/apilib"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
var (
|
||||
reference = "mock"
|
||||
hardLimits = types.ResourceList{types.ResourceCount: -1, types.ResourceStorage: -1}
|
||||
)
|
||||
|
||||
func init() {
|
||||
mockDriver := &mocks.Driver{}
|
||||
|
||||
mockHardLimitsFn := func() types.ResourceList {
|
||||
return hardLimits
|
||||
}
|
||||
|
||||
mockLoadFn := func(key string) driver.RefObject {
|
||||
return driver.RefObject{"id": key}
|
||||
}
|
||||
|
||||
mockValidateFn := func(hardLimits types.ResourceList) error {
|
||||
if len(hardLimits) == 0 {
|
||||
return fmt.Errorf("no resources found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
mockDriver.On("HardLimits").Return(mockHardLimitsFn)
|
||||
mockDriver.On("Load", mock.AnythingOfType("string")).Return(mockLoadFn, nil)
|
||||
mockDriver.On("Validate", mock.AnythingOfType("types.ResourceList")).Return(mockValidateFn)
|
||||
|
||||
driver.Register(reference, mockDriver)
|
||||
}
|
||||
|
||||
func TestQuotaAPIList(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
count := 10
|
||||
for i := 0; i < count; i++ {
|
||||
mgr, err := quota.NewManager(reference, fmt.Sprintf("%d", i))
|
||||
assert.Nil(err)
|
||||
|
||||
_, err = mgr.NewQuota(hardLimits)
|
||||
assert.Nil(err)
|
||||
}
|
||||
|
||||
code, quotas, err := apiTest.QuotasGet(&apilib.QuotaQuery{Reference: reference}, *admin)
|
||||
assert.Nil(err)
|
||||
assert.Equal(int(200), code)
|
||||
assert.Len(quotas, count, fmt.Sprintf("quotas len should be %d", count))
|
||||
|
||||
code, quotas, err = apiTest.QuotasGet(&apilib.QuotaQuery{Reference: reference, PageSize: 1}, *admin)
|
||||
assert.Nil(err)
|
||||
assert.Equal(int(200), code)
|
||||
assert.Len(quotas, 1)
|
||||
}
|
||||
|
||||
func TestQuotaAPIGet(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
mgr, err := quota.NewManager(reference, "quota-get")
|
||||
assert.Nil(err)
|
||||
|
||||
quotaID, err := mgr.NewQuota(hardLimits)
|
||||
assert.Nil(err)
|
||||
|
||||
code, quota, err := apiTest.QuotasGetByID(*admin, fmt.Sprintf("%d", quotaID))
|
||||
assert.Nil(err)
|
||||
assert.Equal(int(200), code)
|
||||
assert.Equal(map[string]int64{"storage": -1, "count": -1}, quota.Hard)
|
||||
|
||||
code, _, err = apiTest.QuotasGetByID(*admin, "100")
|
||||
assert.Nil(err)
|
||||
assert.Equal(int(404), code)
|
||||
}
|
||||
|
||||
func TestQuotaPut(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
mgr, err := quota.NewManager(reference, "quota-put")
|
||||
assert.Nil(err)
|
||||
|
||||
quotaID, err := mgr.NewQuota(hardLimits)
|
||||
assert.Nil(err)
|
||||
|
||||
code, quota, err := apiTest.QuotasGetByID(*admin, fmt.Sprintf("%d", quotaID))
|
||||
assert.Nil(err)
|
||||
assert.Equal(int(200), code)
|
||||
assert.Equal(map[string]int64{"count": -1, "storage": -1}, quota.Hard)
|
||||
|
||||
code, err = apiTest.QuotasPut(*admin, fmt.Sprintf("%d", quotaID), models.QuotaUpdateRequest{})
|
||||
assert.Nil(err, err)
|
||||
assert.Equal(int(400), code)
|
||||
|
||||
code, err = apiTest.QuotasPut(*admin, fmt.Sprintf("%d", quotaID), models.QuotaUpdateRequest{Hard: types.ResourceList{types.ResourceCount: 100, types.ResourceStorage: 100}})
|
||||
assert.Nil(err)
|
||||
assert.Equal(int(200), code)
|
||||
|
||||
code, quota, err = apiTest.QuotasGetByID(*admin, fmt.Sprintf("%d", quotaID))
|
||||
assert.Nil(err)
|
||||
assert.Equal(int(200), code)
|
||||
assert.Equal(map[string]int64{"count": 100, "storage": 100}, quota.Hard)
|
||||
}
|
@ -68,6 +68,7 @@ func initRouters() {
|
||||
beego.Router("/api/ping", &api.SystemInfoAPI{}, "get:Ping")
|
||||
beego.Router("/api/search", &api.SearchAPI{})
|
||||
beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post")
|
||||
beego.Router("/api/projects/:id([0-9]+)/summary", &api.ProjectAPI{}, "get:Summary")
|
||||
beego.Router("/api/projects/:id([0-9]+)/logs", &api.ProjectAPI{}, "get:Logs")
|
||||
beego.Router("/api/projects/:id([0-9]+)/_deletable", &api.ProjectAPI{}, "get:Deletable")
|
||||
beego.Router("/api/projects/:id([0-9]+)/metadatas/?:name", &api.MetadataAPI{}, "get:Get")
|
||||
@ -77,6 +78,9 @@ func initRouters() {
|
||||
beego.Router("/api/projects/:pid([0-9]+)/robots", &api.RobotAPI{}, "post:Post;get:List")
|
||||
beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &api.RobotAPI{}, "get:Get;put:Put;delete:Delete")
|
||||
|
||||
beego.Router("/api/quotas", &api.QuotaAPI{}, "get:List")
|
||||
beego.Router("/api/quotas/:id([0-9]+)", &api.QuotaAPI{}, "get:Get;put:Put")
|
||||
|
||||
beego.Router("/api/repositories", &api.RepositoryAPI{}, "get:Get")
|
||||
beego.Router("/api/repositories/*", &api.RepositoryAPI{}, "delete:Delete;put:Put")
|
||||
beego.Router("/api/repositories/*/labels", &api.RepositoryLabelAPI{}, "get:GetOfRepository;post:AddToRepository")
|
||||
|
@ -67,3 +67,19 @@ type ProjectQuery struct {
|
||||
Page int64 `url:"page,omitempty"`
|
||||
PageSize int64 `url:"page_size,omitempty"`
|
||||
}
|
||||
|
||||
// ProjectSummary ...
|
||||
type ProjectSummary struct {
|
||||
RepoCount int64 `json:"repo_count"`
|
||||
ChartCount uint64 `json:"chart_count"`
|
||||
|
||||
ProjectAdminCount int64 `json:"project_admin_count"`
|
||||
MasterCount int64 `json:"master_count"`
|
||||
DeveloperCount int64 `json:"developer_count"`
|
||||
GuestCount int64 `json:"guest_count"`
|
||||
|
||||
Quota struct {
|
||||
Hard map[string]int64 `json:"hard"`
|
||||
Used map[string]int64 `json:"used"`
|
||||
} `json:"quota"`
|
||||
}
|
||||
|
39
src/testing/apitests/apilib/quota.go
Normal file
39
src/testing/apitests/apilib/quota.go
Normal file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Harbor API
|
||||
*
|
||||
* These APIs provide services for manipulating Harbor project.
|
||||
*
|
||||
* OpenAPI spec version: 0.3.0
|
||||
*
|
||||
* Generated by: https://github.com/swagger-api/swagger-codegen.git
|
||||
*
|
||||
* 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 apilib
|
||||
|
||||
// QuotaQuery query for quota
|
||||
type QuotaQuery struct {
|
||||
Reference string `url:"reference,omitempty"`
|
||||
ReferenceID string `url:"reference_id,omitempty"`
|
||||
Page int64 `url:"page,omitempty"`
|
||||
PageSize int64 `url:"page_size,omitempty"`
|
||||
}
|
||||
|
||||
// Quota ...
|
||||
type Quota struct {
|
||||
ID int `json:"id"`
|
||||
Ref map[string]interface{} `json:"ref"`
|
||||
Hard map[string]int64 `json:"hard"`
|
||||
Used map[string]int64 `json:"used"`
|
||||
}
|
Loading…
Reference in New Issue
Block a user