refactor: generate quota APIs by go-swagger

Signed-off-by: He Weiwei <hweiwei@vmware.com>
This commit is contained in:
He Weiwei 2021-02-25 08:19:55 +00:00
parent a4a995327b
commit 4b033c266a
18 changed files with 652 additions and 632 deletions

View File

@ -1806,120 +1806,6 @@ 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:
- Products
parameters:
- name: reference
in: query
description: The reference type of quota.
required: false
type: string
- name: reference_id
in: query
description: The reference id 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 number, 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:
- Products
- 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:
- Products
- 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.
'/projects/{project_id}/webhook/policies':
get:
summary: List project webhook policies.
@ -2736,30 +2622,6 @@ definitions:
artifact_count:
type: integer
description: The count of artifacts in the repository
ProjectReq:
type: object
properties:
project_name:
type: string
description: The name of the project.
metadata:
description: The metadata of the project.
$ref: '#/definitions/ProjectMetadata'
cve_allowlist:
description: The CVE allowlist of the project.
$ref: '#/definitions/CVEAllowlist'
count_limit:
type: integer
format: int64
description: The count quota of the project.
storage_limit:
type: integer
format: int64
description: The storage quota of the project.
registry_id:
type: integer
format: int64
description: The ID of referenced registry when creating the proxy cache project
Project:
type: object
properties:
@ -2836,38 +2698,6 @@ definitions:
type: string
description: 'Whether this project reuse the system level CVE allowlist as the allowlist of its own. The valid values are "true", "false".
If it is set to "true" the actual allowlist associate with this project, if any, will be ignored.'
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.
maintainer_count:
type: integer
description: The total number of maintainer 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
registry:
$ref: "#/definitions/Registry"
User:
type: object
properties:
@ -3833,42 +3663,6 @@ definitions:
cve_id:
type: string
description: The ID of the CVE, such as "CVE-2019-10164"
ResourceList:
type: object
additionalProperties:
type: integer
format: int64
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
WebhookTargetObject:
type: object
description: The webhook policy target object.

View File

@ -1700,6 +1700,114 @@ paths:
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
'/quotas':
get:
summary: List quotas
description: List quotas
tags:
- quota
operationId: listQuotas
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize'
- name: reference
in: query
description: The reference type of quota.
required: false
type: string
- name: reference_id
in: query
description: The reference id 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.
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':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'500':
$ref: '#/responses/500'
'/quotas/{id}':
get:
summary: Get the specified quota
description: Get the specified quota
tags:
- quota
operationId: getQuota
parameters:
- $ref: '#/parameters/requestId'
- name: id
in: path
type: integer
required: true
description: Quota ID
responses:
'200':
description: Successfully retrieved the quota.
schema:
$ref: '#/definitions/Quota'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
put:
summary: Update the specified quota
description: Update hard limits of the specified quota
tags:
- quota
operationId: updateQuota
parameters:
- $ref: '#/parameters/requestId'
- 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':
$ref: '#/responses/200'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/robots/{robot_id}:
get:
summary: Get a robot account
@ -3585,16 +3693,18 @@ definitions:
type: integer
description: The total number of limited 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
$ref: "#/definitions/ProjectSummaryQuota"
registry:
$ref: "#/definitions/Registry"
ProjectSummaryQuota:
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
CVEAllowlist:
type: object
description: The CVE Allowlist for system or project
@ -3678,6 +3788,10 @@ definitions:
additionalProperties:
type: integer
format: int64
x-go-type:
type: ResourceList
import:
package: "github.com/goharbor/harbor/src/pkg/quota/types"
ReplicationExecution:
type: object
description: The replication execution
@ -4328,3 +4442,38 @@ definitions:
retained:
type: integer
x-omitempty: false
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
x-omitempty: false
used:
$ref: "#/definitions/ResourceList"
description: The used status of the quota
x-omitempty: false
creation_time:
type: string
format: date-time
description: the creation time of the quota
update_time:
type: string
format: date-time
description: the update time of the quota

View File

@ -158,7 +158,6 @@ var (
{Name: common.MetricPath, Scope: SystemScope, Group: BasicGroup, EnvKey: "METRIC_PATH", DefaultValue: "/metrics", ItemType: &StringType{}, Editable: true},
{Name: common.QuotaPerProjectEnable, Scope: UserScope, Group: QuotaGroup, EnvKey: "QUOTA_PER_PROJECT_ENABLE", DefaultValue: "true", ItemType: &BoolType{}, Editable: true},
{Name: common.CountPerProject, Scope: UserScope, Group: QuotaGroup, EnvKey: "COUNT_PER_PROJECT", DefaultValue: "-1", ItemType: &QuotaType{}, Editable: true},
{Name: common.StoragePerProject, Scope: UserScope, Group: QuotaGroup, EnvKey: "STORAGE_PER_PROJECT", DefaultValue: "-1", ItemType: &QuotaType{}, Editable: true},
}
)

View File

@ -156,7 +156,6 @@ const (
// Quota setting items for project
QuotaPerProjectEnable = "quota_per_project_enable"
CountPerProject = "count_per_project"
StoragePerProject = "storage_per_project"
// DefaultGCTimeWindowHours is the reserve blob time window used by GC, default is 2 hours

View File

@ -22,8 +22,6 @@ import (
"time"
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/pkg/quota/types"
"github.com/goharbor/harbor/src/replication/model"
"github.com/lib/pq"
)
@ -263,24 +261,3 @@ type ProjectQueryResult struct {
func (p *Project) TableName() string {
return ProjectTable
}
// QuotaSummary ...
type QuotaSummary struct {
Hard types.ResourceList `json:"hard"`
Used types.ResourceList `json:"used"`
}
// ProjectSummary ...
type ProjectSummary struct {
RepoCount int64 `json:"repo_count"`
ChartCount uint64 `json:"chart_count"`
ProjectAdminCount int64 `json:"project_admin_count"`
MaintainerCount int64 `json:"maintainer_count"`
DeveloperCount int64 `json:"developer_count"`
GuestCount int64 `json:"guest_count"`
LimitedGuestCount int64 `json:"limited_guest_count"`
Quota *QuotaSummary `json:"quota,omitempty"`
Registry *model.Registry `json:"registry"`
}

View File

@ -157,10 +157,6 @@ func init() {
beego.Router("/api/"+api.APIVersion+"/chartrepo/:repo/charts/:name/:version/labels", chartLabelAPIType, "get:GetLabels;post:MarkLabel")
beego.Router("/api/"+api.APIVersion+"/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")
beego.Router("/api/internal/switchquota", &InternalAPI{}, "put:SwitchQuota")
beego.Router("/api/internal/syncquota", &InternalAPI{}, "post:SyncQuota")
@ -918,55 +914,3 @@ 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 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
}

View File

@ -1,149 +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/rbac"
"github.com/goharbor/harbor/src/controller/quota"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/quota/types"
)
// QuotaUpdateRequest struct for the body of put quota API
type QuotaUpdateRequest struct {
Hard types.ResourceList `json:"hard"`
}
// QuotaAPI handles request to /api/quotas/
type QuotaAPI struct {
BaseController
id int64
}
// 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 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
}
qa.id = id
}
}
// Get returns quota by id
func (qa *QuotaAPI) Get() {
if !qa.SecurityCtx.Can(orm.Context(), rbac.ActionRead, rbac.ResourceQuota) {
qa.SendForbiddenError(errors.New(qa.SecurityCtx.GetUsername()))
return
}
quota, err := quota.Ctl.Get(qa.Ctx.Request.Context(), qa.id)
if err != nil {
qa.SendError(err)
return
}
qa.Data["json"] = quota
qa.ServeJSON()
}
// Put update the quota
func (qa *QuotaAPI) Put() {
if !qa.SecurityCtx.Can(orm.Context(), rbac.ActionUpdate, rbac.ResourceQuota) {
qa.SendForbiddenError(errors.New(qa.SecurityCtx.GetUsername()))
return
}
var req *QuotaUpdateRequest
if err := qa.DecodeJSONReq(&req); err != nil {
qa.SendBadRequestError(err)
return
}
ctx := qa.Ctx.Request.Context()
q, err := quota.Ctl.Get(ctx, qa.id)
if err != nil {
qa.SendError(err)
return
}
if err := quota.Validate(ctx, q.Reference, req.Hard); err != nil {
qa.SendBadRequestError(err)
return
}
q.SetHard(req.Hard)
if err := quota.Ctl.Update(ctx, q); 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() {
if !qa.SecurityCtx.Can(orm.Context(), rbac.ActionList, rbac.ResourceQuota) {
qa.SendForbiddenError(errors.New(qa.SecurityCtx.GetUsername()))
return
}
page, size, err := qa.GetPaginationParams()
if err != nil {
qa.SendBadRequestError(err)
return
}
query := &q.Query{
Keywords: q.KeyWords{
"reference": qa.GetString("reference"),
"reference_id": qa.GetString("reference_id"),
},
PageNumber: page,
PageSize: size,
Sorting: qa.GetString("sort"),
}
ctx := qa.Ctx.Request.Context()
total, err := quota.Ctl.Count(ctx, query)
if err != nil {
qa.SendInternalServerError(fmt.Errorf("failed to query database for total of quotas, error: %v", err))
return
}
quotas, err := quota.Ctl.List(ctx, query, quota.WithReferenceObject())
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()
}

View File

@ -1,140 +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 (
"context"
"fmt"
"testing"
o "github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/controller/quota"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/quota/driver"
"github.com/goharbor/harbor/src/pkg/quota/types"
"github.com/goharbor/harbor/src/testing/apitests/apilib"
"github.com/goharbor/harbor/src/testing/mock"
drivertesting "github.com/goharbor/harbor/src/testing/pkg/quota/driver"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
var (
reference = uuid.New().String()
hardLimits = types.ResourceList{types.ResourceStorage: -1}
)
func init() {
mockDriver := &drivertesting.Driver{}
mockHardLimitsFn := func() types.ResourceList {
return hardLimits
}
mockLoadFn := func(ctx context.Context, 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)
mock.OnAnything(mockDriver, "Load").Return(mockLoadFn, nil)
mock.OnAnything(mockDriver, "Validate").Return(mockValidateFn)
driver.Register(reference, mockDriver)
}
func TestQuotaAPIList(t *testing.T) {
assert := assert.New(t)
apiTest := newHarborAPI()
ctx := orm.NewContext(context.TODO(), o.NewOrm())
var quotaIDs []int64
defer func() {
for _, quotaID := range quotaIDs {
quota.Ctl.Delete(ctx, quotaID)
}
}()
count := 10
for i := 0; i < count; i++ {
quotaID, err := quota.Ctl.Create(ctx, reference, uuid.New().String(), hardLimits)
assert.Nil(err)
quotaIDs = append(quotaIDs, quotaID)
}
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()
ctx := orm.NewContext(context.TODO(), o.NewOrm())
quotaID, err := quota.Ctl.Create(ctx, reference, uuid.New().String(), hardLimits)
assert.Nil(err)
defer quota.Ctl.Delete(ctx, quotaID)
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}, 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()
ctx := orm.NewContext(context.TODO(), o.NewOrm())
quotaID, err := quota.Ctl.Create(ctx, reference, uuid.New().String(), hardLimits)
assert.Nil(err)
defer quota.Ctl.Delete(ctx, quotaID)
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}, quota.Hard)
code, err = apiTest.QuotasPut(*admin, fmt.Sprintf("%d", quotaID), QuotaUpdateRequest{})
assert.Nil(err, err)
assert.Equal(int(400), code)
code, err = apiTest.QuotasPut(*admin, fmt.Sprintf("%d", quotaID), QuotaUpdateRequest{Hard: types.ResourceList{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{"storage": 100}, quota.Hard)
}

View File

@ -17,6 +17,8 @@ package types
import (
"encoding/json"
"strconv"
"github.com/go-openapi/strfmt"
)
const (
@ -43,6 +45,11 @@ func (resource ResourceName) FormatValue(value int64) string {
// ResourceList is a set of (resource name, value) pairs.
type ResourceList map[ResourceName]int64
// Validate validates this resource list
func (resources ResourceList) Validate(formats strfmt.Registry) error {
return nil
}
func (resources ResourceList) String() string {
bytes, _ := json.Marshal(resources)
return string(bytes)

View File

@ -45,6 +45,7 @@ func New() http.Handler {
SysteminfoAPI: newSystemInfoAPI(),
PingAPI: newPingAPI(),
GCAPI: newGCAPI(),
QuotaAPI: newQuotaAPI(),
RetentionAPI: newRetentionAPI(),
})
if err != nil {

View File

@ -0,0 +1,67 @@
// 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 model
import (
"context"
"github.com/go-openapi/strfmt"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/quota"
"github.com/goharbor/harbor/src/pkg/quota/types"
"github.com/goharbor/harbor/src/server/v2.0/models"
)
// Quota model
type Quota struct {
*quota.Quota
}
// ToSwagger converts the quota to the swagger model
func (q *Quota) ToSwagger(ctx context.Context) *models.Quota {
if q.Quota == nil {
return nil
}
hard, err := q.GetHard()
if err != nil {
fields := log.Fields{"quota_id": q.ID, "error": err}
log.G(ctx).WithFields(fields).Warningf("failed to get hard from quota")
hard = types.ResourceList{}
}
used, err := q.GetUsed()
if err != nil {
fields := log.Fields{"quota_id": q.ID, "error": err}
log.G(ctx).WithFields(fields).Warningf("failed to get used from quota")
used = types.ResourceList{}
}
return &models.Quota{
ID: q.ID,
Ref: q.Ref,
Hard: hard,
Used: used,
CreationTime: strfmt.DateTime(q.CreationTime),
UpdateTime: strfmt.DateTime(q.UpdateTime),
}
}
// NewQuota new quota instance
func NewQuota(quota *quota.Quota) *Quota {
return &Quota{Quota: quota}
}

View File

@ -1,9 +1,22 @@
// 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"
"github.com/goharbor/harbor/src/controller/retention"
"strconv"
"strings"
"sync"
@ -19,6 +32,7 @@ import (
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/controller/quota"
"github.com/goharbor/harbor/src/controller/repository"
"github.com/goharbor/harbor/src/controller/retention"
"github.com/goharbor/harbor/src/core/api"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib"
@ -606,10 +620,10 @@ func getProjectQuotaSummary(ctx context.Context, p *project.Project, summary *mo
summary.Quota = &models.ProjectSummaryQuota{}
if hard, err := q.GetHard(); err == nil {
lib.JSONCopy(&summary.Quota.Hard, hard)
summary.Quota.Hard = hard
}
if used, err := q.GetUsed(); err == nil {
lib.JSONCopy(&summary.Quota.Used, used)
summary.Quota.Used = used
}
}

View File

@ -0,0 +1,116 @@
// 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"
"github.com/go-openapi/runtime/middleware"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/controller/quota"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
"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/quota"
)
func newQuotaAPI() *quotaAPI {
return &quotaAPI{
quotaCtl: quota.Ctl,
}
}
type quotaAPI struct {
BaseAPI
quotaCtl quota.Controller
}
func (qa *quotaAPI) GetQuota(ctx context.Context, params operation.GetQuotaParams) middleware.Responder {
if err := qa.RequireSystemAccess(ctx, rbac.ActionRead, rbac.ResourceQuota); err != nil {
return qa.SendError(ctx, err)
}
quota, err := qa.quotaCtl.Get(ctx, params.ID, quota.WithReferenceObject())
if err != nil {
return qa.SendError(ctx, err)
}
return operation.NewGetQuotaOK().WithPayload(model.NewQuota(quota).ToSwagger(ctx))
}
func (qa *quotaAPI) ListQuotas(ctx context.Context, params operation.ListQuotasParams) middleware.Responder {
if err := qa.RequireSystemAccess(ctx, rbac.ActionList, rbac.ResourceQuota); err != nil {
return qa.SendError(ctx, err)
}
query := &q.Query{
Keywords: q.KeyWords{
"reference": lib.StringValue(params.Reference),
"reference_id": lib.StringValue(params.ReferenceID),
},
PageNumber: *params.Page,
PageSize: *params.PageSize,
Sorting: lib.StringValue(params.Sort),
}
total, err := qa.quotaCtl.Count(ctx, query)
if err != nil {
return qa.SendError(ctx, err)
}
quotas, err := qa.quotaCtl.List(ctx, query, quota.WithReferenceObject())
if err != nil {
return qa.SendError(ctx, err)
}
payload := make([]*models.Quota, len(quotas))
for i, quota := range quotas {
payload[i] = model.NewQuota(quota).ToSwagger(ctx)
}
return operation.NewListQuotasOK().
WithXTotalCount(total).
WithLink(qa.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
WithPayload(payload)
}
func (qa *quotaAPI) UpdateQuota(ctx context.Context, params operation.UpdateQuotaParams) middleware.Responder {
if err := qa.RequireSystemAccess(ctx, rbac.ActionUpdate, rbac.ResourceQuota); err != nil {
return qa.SendError(ctx, err)
}
if params.Hard == nil || len(params.Hard.Hard) == 0 {
return qa.SendError(ctx, errors.BadRequestError(nil).WithMessage("hard required in body"))
}
q, err := qa.quotaCtl.Get(ctx, params.ID)
if err != nil {
return qa.SendError(ctx, err)
}
if err := quota.Validate(ctx, q.Reference, params.Hard.Hard); err != nil {
return qa.SendError(ctx, errors.BadRequestError(nil).WithMessage(err.Error()))
}
q.SetHard(params.Hard.Hard)
if err := qa.quotaCtl.Update(ctx, q); err != nil {
return qa.SendError(ctx, err)
}
return operation.NewUpdateQuotaOK()
}

View File

@ -0,0 +1,283 @@
// 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 (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"testing"
"time"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/pkg/quota"
"github.com/goharbor/harbor/src/pkg/quota/types"
"github.com/goharbor/harbor/src/server/v2.0/models"
"github.com/goharbor/harbor/src/server/v2.0/restapi"
quotatesting "github.com/goharbor/harbor/src/testing/controller/quota"
"github.com/goharbor/harbor/src/testing/mock"
htesting "github.com/goharbor/harbor/src/testing/server/v2.0/handler"
"github.com/stretchr/testify/suite"
)
type QuotaTestSuite struct {
htesting.Suite
quotaCtl *quotatesting.Controller
quota *quota.Quota
}
func (suite *QuotaTestSuite) SetupSuite() {
suite.quota = &quota.Quota{
ID: 1,
Reference: "project",
ReferenceID: "1",
Hard: `{"storage": 100}`,
Used: `{"storage": 1000}`,
CreationTime: time.Now(),
UpdateTime: time.Now(),
}
suite.quotaCtl = &quotatesting.Controller{}
suite.Config = &restapi.Config{
QuotaAPI: &quotaAPI{
quotaCtl: suite.quotaCtl,
},
}
suite.Suite.SetupSuite()
}
func (suite *QuotaTestSuite) TestAuthorization() {
newBody := func(body interface{}) io.Reader {
if body == nil {
return nil
}
buf, err := json.Marshal(body)
suite.Require().NoError(err)
return bytes.NewBuffer(buf)
}
quota := models.QuotaUpdateReq{
Hard: types.ResourceList{"storage": 1000},
}
reqs := []struct {
method string
url string
body interface{}
}{
{http.MethodGet, "/quotas/1", nil},
{http.MethodGet, "/quotas", nil},
{http.MethodPut, "/quotas/1", quota},
}
for _, req := range reqs {
{
// authorized required
suite.Security.On("IsAuthenticated").Return(false).Once()
res, err := suite.DoReq(req.method, req.url, newBody(req.body))
suite.NoError(err)
suite.Equal(401, res.StatusCode)
}
{
// permission required
suite.Security.On("IsAuthenticated").Return(true).Once()
suite.Security.On("GetUsername").Return("username").Once()
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(false).Once()
res, err := suite.DoReq(req.method, req.url, newBody(req.body))
suite.NoError(err)
suite.Equal(403, res.StatusCode)
}
}
}
func (suite *QuotaTestSuite) TestGetQuota() {
times := 3
suite.Security.On("IsAuthenticated").Return(true).Times(times)
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(times)
{
// get quota failed
mock.OnAnything(suite.quotaCtl, "Get").Return(nil, fmt.Errorf("failed to get quota")).Once()
res, err := suite.Get("/quotas/1")
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
// quota not found
mock.OnAnything(suite.quotaCtl, "Get").Return(nil, errors.NotFoundError(nil)).Once()
var quota map[string]interface{}
res, err := suite.GetJSON("/quotas/1", &quota)
suite.NoError(err)
suite.Equal(404, res.StatusCode)
}
{
// quota found
mock.OnAnything(suite.quotaCtl, "Get").Return(suite.quota, nil).Once()
var quota map[string]interface{}
res, err := suite.GetJSON("/quotas/1", &quota)
suite.NoError(err)
suite.Equal(200, res.StatusCode)
suite.Equal(float64(1), quota["id"])
}
}
func (suite *QuotaTestSuite) TestListQuotas() {
times := 5
suite.Security.On("IsAuthenticated").Return(true).Times(times)
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(times)
{
// list quotas failed
mock.OnAnything(suite.quotaCtl, "Count").Return(int64(0), fmt.Errorf("failed to count quotas")).Once()
res, err := suite.Get("/quotas")
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
// list quotas failed
mock.OnAnything(suite.quotaCtl, "Count").Return(int64(1), nil).Once()
mock.OnAnything(suite.quotaCtl, "List").Return(nil, fmt.Errorf("failed to list quotas")).Once()
res, err := suite.Get("/quotas")
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
// quotas not found
mock.OnAnything(suite.quotaCtl, "Count").Return(int64(0), nil).Once()
mock.OnAnything(suite.quotaCtl, "List").Return(nil, nil).Once()
var quotas []interface{}
res, err := suite.GetJSON("/quotas", &quotas)
suite.NoError(err)
suite.Equal(200, res.StatusCode)
suite.Len(quotas, 0)
}
{
// quotas found
mock.OnAnything(suite.quotaCtl, "Count").Return(int64(3), nil).Once()
mock.OnAnything(suite.quotaCtl, "List").Return([]*quota.Quota{suite.quota}, nil).Once()
var quotas []interface{}
res, err := suite.GetJSON("/quotas?page_size=1&page=2", &quotas)
suite.NoError(err)
suite.Equal(200, res.StatusCode)
suite.Len(quotas, 1)
suite.Equal("3", res.Header.Get("X-Total-Count"))
suite.Contains(res.Header, "Link")
suite.Equal(`</api/v2.0/quotas?page=1&page_size=1>; rel="prev" , </api/v2.0/quotas?page=3&page_size=1>; rel="next"`, res.Header.Get("Link"))
}
}
func (suite *QuotaTestSuite) TestUpdateQuota() {
times := 6
suite.Security.On("IsAuthenticated").Return(true).Times(times)
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Times(times)
{
// update quota no body
res, err := suite.Put("/quotas/1", nil)
suite.NoError(err)
suite.Equal(422, res.StatusCode)
}
{
// update quota with empty hard
quota := models.QuotaUpdateReq{
Hard: types.ResourceList{},
}
res, err := suite.PutJSON("/quotas/1", quota)
suite.NoError(err)
suite.Equal(400, res.StatusCode)
}
{
// quota not found
mock.OnAnything(suite.quotaCtl, "Get").Return(nil, errors.NotFoundError(nil)).Once()
quota := models.QuotaUpdateReq{
Hard: types.ResourceList{"storage": 1000},
}
res, err := suite.PutJSON("/quotas/1", quota)
suite.NoError(err)
suite.Equal(404, res.StatusCode)
}
{
// update quota
mock.OnAnything(suite.quotaCtl, "Get").Return(suite.quota, nil).Once()
mock.OnAnything(suite.quotaCtl, "Update").Return(nil).Once()
quota := models.QuotaUpdateReq{
Hard: types.ResourceList{"storage": 1000},
}
res, err := suite.PutJSON("/quotas/1", quota)
suite.NoError(err)
suite.Equal(200, res.StatusCode)
}
{
// update quota failed
mock.OnAnything(suite.quotaCtl, "Get").Return(suite.quota, nil).Once()
mock.OnAnything(suite.quotaCtl, "Update").Return(fmt.Errorf("failed to update the quota")).Once()
quota := models.QuotaUpdateReq{
Hard: types.ResourceList{"storage": 1000},
}
res, err := suite.PutJSON("/quotas/1", quota)
suite.NoError(err)
suite.Equal(500, res.StatusCode)
}
{
// resource not support
mock.OnAnything(suite.quotaCtl, "Get").Return(suite.quota, nil).Once()
mock.OnAnything(suite.quotaCtl, "Update").Return(nil).Once()
quota := models.QuotaUpdateReq{
Hard: types.ResourceList{"size": 1000},
}
res, err := suite.PutJSON("/quotas/1", quota)
suite.NoError(err)
suite.Equal(400, res.StatusCode)
}
}
func TestQuotaTestSuite(t *testing.T) {
suite.Run(t, &QuotaTestSuite{})
}

View File

@ -42,9 +42,6 @@ func registerLegacyRoutes() {
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")
beego.Router("/api/"+version+"/quotas", &api.QuotaAPI{}, "get:List")
beego.Router("/api/"+version+"/quotas/:id([0-9]+)", &api.QuotaAPI{}, "get:Get;put:Put")
beego.Router("/api/"+version+"/system/CVEAllowlist", &api.SysCVEAllowlistAPI{}, "get:Get;put:Put")
beego.Router("/api/"+version+"/system/oidc/ping", &api.OIDCAPI{}, "post:Ping")

View File

@ -1,39 +0,0 @@
/*
* 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"`
}

View File

@ -28,7 +28,7 @@ def get_endpoint():
def _create_client(server, credential, debug, api_type="products"):
cfg = None
if api_type in ('projectv2', 'artifact', 'repository', 'scan', 'scanall', 'preheat', 'replication', 'robot', 'gc', 'retention'):
if api_type in ('projectv2', 'artifact', 'repository', 'scan', 'scanall', 'preheat', 'quota', 'replication', 'robot', 'gc', 'retention'):
cfg = v2_swagger_client.Configuration()
else:
cfg = swagger_client.Configuration()
@ -56,6 +56,7 @@ def _create_client(server, credential, debug, api_type="products"):
"projectv2": v2_swagger_client.ProjectApi(v2_swagger_client.ApiClient(cfg)),
"artifact": v2_swagger_client.ArtifactApi(v2_swagger_client.ApiClient(cfg)),
"preheat": v2_swagger_client.PreheatApi(v2_swagger_client.ApiClient(cfg)),
"quota": v2_swagger_client.QuotaApi(v2_swagger_client.ApiClient(cfg)),
"repository": v2_swagger_client.RepositoryApi(v2_swagger_client.ApiClient(cfg)),
"scan": v2_swagger_client.ScanApi(v2_swagger_client.ApiClient(cfg)),
"scanall": v2_swagger_client.ScanAllApi(v2_swagger_client.ApiClient(cfg)),

View File

@ -137,7 +137,7 @@ class System(base.Base):
params['reference'] = reference
params['reference_id'] = reference_id
client = self._get_client(**kwargs)
data, status_code, _ = client.quotas_get_with_http_info(**params)
client = self._get_client(api_type='quota', **kwargs)
data, status_code, _ = client.list_quotas_with_http_info(**params)
base._assert_status_code(200, status_code)
return data