Implement label management API

This commit is contained in:
Wenkai Yin 2018-03-07 13:20:28 +08:00
parent 685140cda8
commit 379f113452
14 changed files with 1312 additions and 2 deletions

View File

@ -1639,6 +1639,164 @@ paths:
project and target.
'500':
description: Unexpected internal errors.
/labels:
get:
summary: List labels according to the query strings.
description: >
This endpoint let user list labels by name, scope and project_id
parameters:
- name: name
in: query
type: string
required: false
description: The label name.
- name: scope
in: query
type: string
required: true
description: The label scope. Valid values are g and p. g for global labels and p for project labels.
- name: project_id
in: query
type: integer
format: int64
required: false
description: Relevant project ID, required when scope is p.
- name: page
in: query
type: integer
format: int32
required: false
description: The page nubmer.
- name: page_size
in: query
type: integer
format: int32
required: false
description: The size of per page.
tags:
- Products
responses:
'200':
description: Get successfully.
schema:
type: array
items:
$ref: '#/definitions/Label'
'400':
description: Invalid parameters.
'401':
description: User need to log in first.
'500':
description: Unexpected internal errors.
post:
summary: Post creates a label
description: >
This endpoint let user creates a label.
parameters:
- name: label
in: body
description: The json object of label.
required: true
schema:
$ref: '#/definitions/Label'
tags:
- Products
responses:
'201':
description: Create successfully.
'400':
description: Invalid parameters.
'401':
description: User need to log in first.
'409':
description: >-
Label with the same name and same scope already exists.
'415':
$ref: '#/responses/UnsupportedMediaType'
'500':
description: Unexpected internal errors.
'/labels/{id}':
get:
summary: Get the label specified by ID.
description: |
This endpoint let user get the label by specific ID.
parameters:
- name: id
in: path
type: integer
format: int64
required: true
description: Label ID
tags:
- Products
responses:
'200':
description: Get successfully.
schema:
$ref: '#/definitions/Label'
'401':
description: User need to log in first.
'404':
description: The resource does not exist.
'500':
description: Unexpected internal errors.
put:
summary: Update the label properties.
description: >
This endpoint let user update label properties.
parameters:
- name: id
in: path
type: integer
format: int64
required: true
description: Label ID
- name: label
in: body
description: The updated label json object.
required: true
schema:
$ref: '#/definitions/Label'
tags:
- Products
responses:
'200':
description: Update successfully.
'400':
description: Invalid parameters.
'401':
description: User need to log in first.
'404':
description: The resource does not exist.
'409':
description: >-
The label with the same name already exists.
'500':
description: Unexpected internal errors.
delete:
summary: Delete the label specified by ID.
description: >
Delete the label specified by ID.
parameters:
- name: id
in: path
type: integer
format: int64
required: true
description: Label ID
tags:
- Products
responses:
'200':
description: Delete successfully.
'400':
description: Invalid parameters.
'401':
description: User need to log in first.
'404':
description: The resource does not exist.
'500':
description: Unexpected internal errors.
/replications:
post:
summary: Trigger the replication according to the specified policy.
@ -3056,3 +3214,30 @@ definitions:
status:
type: string
description: The status of jobs. The only valid value is stop for now.
Label:
type: object
properties:
id:
type: integer
description: The ID of label.
name:
type: string
description: The name of label.
description:
type: string
description: The description of label.
color:
type: string
description: The color of label.
scope:
type: integer
description: The scope of label, g for global labels and p for project labels.
project_id:
type: integer
description: The project ID if the label is a project label.
creation_time:
type: string
description: The creation time of label.
update_time:
type: string
description: The update time of label.

View File

@ -254,6 +254,24 @@ create table properties (
UNIQUE (k)
);
create table harbor_label (
id int NOT NULL AUTO_INCREMENT,
name varchar(128) NOT NULL,
description text,
color varchar(16),
# 's' for system level labels
# 'u' for user level labels
level char(1) NOT NULL,
# 'g' for global labels
# 'p' for project labels
scope char(1) NOT NULL,
project_id int,
creation_time timestamp default CURRENT_TIMESTAMP,
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
PRIMARY KEY(id),
CONSTRAINT unique_name_and_scope UNIQUE (name,scope)
);
CREATE TABLE IF NOT EXISTS `alembic_version` (
`version_num` varchar(32) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View File

@ -111,7 +111,7 @@ create table project_metadata (
creation_time timestamp,
update_time timestamp,
deleted tinyint (1) DEFAULT 0 NOT NULL,
UNIQUE(project_id, name) ON CONFLICT REPLACE,
UNIQUE(project_id, name),
FOREIGN KEY (project_id) REFERENCES project(project_id)
);
@ -240,6 +240,27 @@ create table properties (
UNIQUE(k)
);
create table harbor_label (
id INTEGER PRIMARY KEY,
name varchar(128) NOT NULL,
description text,
color varchar(16),
/*
's' for system level labels
'u' for user level labels
*/
level char(1) NOT NULL,
/*
'g' for global labels
'p' for project labels
*/
scope char(1) NOT NULL,
project_id int,
creation_time timestamp default CURRENT_TIMESTAMP,
update_time timestamp default CURRENT_TIMESTAMP,
UNIQUE(name, scope)
);
create table alembic_version (
version_num varchar(32) NOT NULL
);

View File

@ -29,6 +29,15 @@ const (
RoleDeveloper = 2
RoleGuest = 3
LabelLevelSystem = "s"
LabelLevelUser = "u"
LabelScopeGlobal = "g"
LabelScopeProject = "p"
ResourceTypeProject = "p"
ResourceTypeRepository = "r"
ResourceTypeImage = "i"
ExtEndpoint = "ext_endpoint"
AUTHMode = "auth_mode"
DatabaseType = "database_type"

99
src/common/dao/label.go Normal file
View File

@ -0,0 +1,99 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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 dao
import (
"time"
"github.com/astaxie/beego/orm"
"github.com/vmware/harbor/src/common/models"
)
// AddLabel creates a label
func AddLabel(label *models.Label) (int64, error) {
now := time.Now()
label.CreationTime = now
label.UpdateTime = now
return GetOrmer().Insert(label)
}
// GetLabel specified by ID
func GetLabel(id int64) (*models.Label, error) {
label := &models.Label{
ID: id,
}
if err := GetOrmer().Read(label); err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
return label, nil
}
// GetTotalOfLabels returns the total count of labels
func GetTotalOfLabels(query *models.LabelQuery) (int64, error) {
qs := getLabelQuerySetter(query)
return qs.Count()
}
// ListLabels list labels according to the query conditions
func ListLabels(query *models.LabelQuery) ([]*models.Label, error) {
qs := getLabelQuerySetter(query)
if query.Size > 0 {
qs = qs.Limit(query.Size)
if query.Page > 0 {
qs = qs.Offset((query.Page - 1) * query.Size)
}
}
qs = qs.OrderBy("Name")
labels := []*models.Label{}
_, err := qs.All(&labels)
return labels, err
}
func getLabelQuerySetter(query *models.LabelQuery) orm.QuerySeter {
qs := GetOrmer().QueryTable(&models.Label{})
if len(query.Name) > 0 {
qs = qs.Filter("Name", query.Name)
}
if len(query.Level) > 0 {
qs = qs.Filter("Level", query.Level)
}
if len(query.Scope) > 0 {
qs = qs.Filter("Scope", query.Scope)
}
if query.ProjectID != 0 {
qs = qs.Filter("ProjectID", query.ProjectID)
}
return qs
}
// UpdateLabel ...
func UpdateLabel(label *models.Label) error {
label.UpdateTime = time.Now()
_, err := GetOrmer().Update(label)
return err
}
// DeleteLabel ...
func DeleteLabel(id int64) error {
_, err := GetOrmer().Delete(&models.Label{
ID: id,
})
return err
}

View File

@ -0,0 +1,91 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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 dao
import (
"testing"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMethodsOfLabel(t *testing.T) {
label := &models.Label{
Name: "test",
Level: common.LabelLevelUser,
Scope: common.LabelScopeProject,
ProjectID: 1,
}
// add
id, err := AddLabel(label)
require.Nil(t, err)
label.ID = id
// get
l, err := GetLabel(id)
require.Nil(t, err)
assert.Equal(t, label.ID, l.ID)
assert.Equal(t, label.Name, l.Name)
assert.Equal(t, label.Scope, l.Scope)
assert.Equal(t, label.ProjectID, l.ProjectID)
// get total count
total, err := GetTotalOfLabels(&models.LabelQuery{
Scope: common.LabelScopeProject,
ProjectID: 1,
})
require.Nil(t, err)
assert.Equal(t, int64(1), total)
// list
labels, err := ListLabels(&models.LabelQuery{
Scope: common.LabelScopeProject,
ProjectID: 1,
Name: label.Name,
})
require.Nil(t, err)
assert.Equal(t, 1, len(labels))
// list
labels, err = ListLabels(&models.LabelQuery{
Scope: common.LabelScopeProject,
ProjectID: 1,
Name: "not_exist_label",
})
require.Nil(t, err)
assert.Equal(t, 0, len(labels))
// update
newName := "dev"
label.Name = newName
err = UpdateLabel(label)
require.Nil(t, err)
l, err = GetLabel(id)
require.Nil(t, err)
assert.Equal(t, newName, l.Name)
// delete
err = DeleteLabel(id)
require.Nil(t, err)
l, err = GetLabel(id)
require.Nil(t, err)
assert.Nil(t, l)
}

View File

@ -32,5 +32,6 @@ func init() {
new(ClairVulnTimestamp),
new(WatchItem),
new(ProjectMetadata),
new(ConfigEntry))
new(ConfigEntry),
new(Label))
}

View File

@ -0,0 +1,94 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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 models
import (
"fmt"
"time"
"github.com/astaxie/beego/validation"
"github.com/vmware/harbor/src/common"
)
// Label holds information used for a label
type Label struct {
ID int64 `orm:"pk;auto;column(id)" json:"id"`
Name string `orm:"column(name)" json:"name"`
Description string `orm:"column(description)" json:"description"`
Color string `orm:"column(color)" json:"color"`
Level string `orm:"column(level)" json:"-"`
Scope string `orm:"column(scope)" json:"scope"`
ProjectID int64 `orm:"column(project_id)" json:"project_id"`
CreationTime time.Time `orm:"column(creation_time)" json:"creation_time"`
UpdateTime time.Time `orm:"column(update_time)" json:"update_time"`
}
//TableName ...
func (l *Label) TableName() string {
return "harbor_label"
}
// LabelQuery : query parameters for labels
type LabelQuery struct {
Name string
Level string
Scope string
ProjectID int64
Pagination
}
// Valid ...
func (l *Label) Valid(v *validation.Validation) {
if len(l.Name) == 0 {
v.SetError("name", "cannot be empty")
}
if len(l.Name) > 128 {
v.SetError("name", "max length is 128")
}
if l.Scope != common.LabelScopeGlobal && l.Scope != common.LabelScopeProject {
v.SetError("scope", fmt.Sprintf("invalid: %s", l.Scope))
} else if l.Scope == common.LabelScopeProject && l.ProjectID <= 0 {
v.SetError("project_id", fmt.Sprintf("invalid: %d", l.ProjectID))
}
}
/*
type ResourceLabel struct {
ID int64 `orm:"pk;auto;column(id)" json:"id"`
LabelID int64 `orm:"column(label_id)" json:"label_id"`
ResourceID string `orm:"column(resource_id)" json:"resource_id"`
ResourceType rune `orm:"column(resource_type)" json:"resource_type"`
CreationTime time.Time `orm:"column(creation_time)" json:"creation_time"`
UpdateTime time.Time `orm:"column(update_time)" json:"update_time"`
}
// Valid ...
func (r *ResourceLabel) Valid(v *validation.Validation) {
if r.LabelID <= 0 {
v.SetError("label_id", fmt.Sprintf("invalid: %d", r.LabelID))
}
// TODO
//if r.ResourceID <= 0 {
// v.SetError("resource_id", fmt.Sprintf("invalid: %v", r.ResourceID))
//}
if r.ResourceType != common.ResourceTypeProject &&
r.ResourceType != common.ResourceTypeRepository &&
r.ResourceType != common.ResourceTypeImage {
v.SetError("resource_type", fmt.Sprintf("invalid: %d", r.ResourceType))
}
}
*/

View File

@ -0,0 +1,86 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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 models
import (
"testing"
"github.com/astaxie/beego/validation"
"github.com/stretchr/testify/assert"
)
func TestValidOfLabel(t *testing.T) {
cases := []struct {
label *Label
hasError bool
}{
{
label: &Label{
Name: "",
},
hasError: true,
},
{
label: &Label{
Name: "test",
Scope: "",
},
hasError: true,
},
{
label: &Label{
Name: "test",
Scope: "invalid_scope",
},
hasError: true,
},
{
label: &Label{
Name: "test",
Scope: "g",
},
hasError: false,
},
{
label: &Label{
Name: "test",
Scope: "p",
},
hasError: true,
},
{
label: &Label{
Name: "test",
Scope: "p",
ProjectID: -1,
},
hasError: true,
},
{
label: &Label{
Name: "test",
Scope: "p",
ProjectID: 1,
},
hasError: false,
},
}
for _, c := range cases {
v := &validation.Validation{}
c.label.Valid(v)
assert.Equal(t, c.hasError, v.HasErrors())
}
}

View File

@ -132,6 +132,8 @@ func init() {
beego.Router("/api/configurations/reset", &ConfigAPI{}, "post:Reset")
beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping")
beego.Router("/api/replications", &ReplicationAPI{})
beego.Router("/api/labels", &LabelAPI{}, "post:Post;get:List")
beego.Router("/api/labels/:id([0-9]+", &LabelAPI{}, "get:Get;put:Put;delete:Delete")
_ = updateInitPassword(1, "Harbor12345")

263
src/ui/api/label.go Normal file
View File

@ -0,0 +1,263 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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"
"strconv"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
)
// LabelAPI handles requests for label management
type LabelAPI struct {
label *models.Label
BaseController
}
// Prepare ...
func (l *LabelAPI) Prepare() {
l.BaseController.Prepare()
method := l.Ctx.Request.Method
if method == http.MethodGet {
return
}
// POST, PUT, DELETE need login first
if !l.SecurityCtx.IsAuthenticated() {
l.HandleUnauthorized()
return
}
if method == http.MethodPut || method == http.MethodDelete {
id, err := l.GetInt64FromPath(":id")
if err != nil || id <= 0 {
l.HandleBadRequest("invalid label ID")
return
}
label, err := dao.GetLabel(id)
if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to get label %d: %v", id, err))
return
}
if label == nil {
l.HandleNotFound(fmt.Sprintf("label %d not found", id))
return
}
if label.Scope == common.LabelScopeGlobal && !l.SecurityCtx.IsSysAdmin() ||
label.Scope == common.LabelScopeProject && !l.SecurityCtx.HasAllPerm(label.ProjectID) {
l.HandleForbidden(l.SecurityCtx.GetUsername())
return
}
l.label = label
}
}
// Post creates a label
func (l *LabelAPI) Post() {
label := &models.Label{}
l.DecodeJSONReqAndValidate(label)
label.Level = common.LabelLevelUser
switch label.Scope {
case common.LabelScopeGlobal:
if !l.SecurityCtx.IsSysAdmin() {
l.HandleForbidden(l.SecurityCtx.GetUsername())
return
}
label.ProjectID = 0
case common.LabelScopeProject:
exist, err := l.ProjectMgr.Exists(label.ProjectID)
if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to check the existence of project %d: %v",
label.ProjectID, err))
return
}
if !exist {
l.HandleNotFound(fmt.Sprintf("project %d not found", label.ProjectID))
return
}
if !l.SecurityCtx.HasAllPerm(label.ProjectID) {
l.HandleForbidden(l.SecurityCtx.GetUsername())
return
}
}
labels, err := dao.ListLabels(&models.LabelQuery{
Name: label.Name,
Level: label.Level,
Scope: label.Scope,
ProjectID: label.ProjectID,
})
if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to list labels: %v", err))
return
}
if len(labels) > 0 {
l.HandleConflict()
return
}
id, err := dao.AddLabel(label)
if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to create label: %v", err))
return
}
l.Redirect(http.StatusCreated, strconv.FormatInt(id, 10))
}
// Get the label specified by ID
func (l *LabelAPI) Get() {
id, err := l.GetInt64FromPath(":id")
if err != nil || id <= 0 {
l.HandleBadRequest(fmt.Sprintf("invalid label ID: %s", l.GetStringFromPath(":id")))
return
}
label, err := dao.GetLabel(id)
if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to get label %d: %v", id, err))
return
}
if label == nil {
l.HandleNotFound(fmt.Sprintf("label %d not found", id))
return
}
if label.Scope == common.LabelScopeProject {
if !l.SecurityCtx.HasReadPerm(label.ProjectID) {
if !l.SecurityCtx.IsAuthenticated() {
l.HandleUnauthorized()
return
}
l.HandleForbidden(l.SecurityCtx.GetUsername())
return
}
}
l.Data["json"] = label
l.ServeJSON()
}
// List labels according to the query strings
func (l *LabelAPI) List() {
query := &models.LabelQuery{
Name: l.GetString("name"),
Level: common.LabelLevelUser,
}
scope := l.GetString("scope")
if scope != common.LabelScopeGlobal && scope != common.LabelScopeProject {
l.HandleBadRequest(fmt.Sprintf("invalid scope: %s", scope))
return
}
query.Scope = scope
if scope == common.LabelScopeProject {
projectIDStr := l.GetString("project_id")
if len(projectIDStr) == 0 {
l.HandleBadRequest("project_id is required")
return
}
projectID, err := strconv.ParseInt(projectIDStr, 10, 64)
if err != nil || projectID <= 0 {
l.HandleBadRequest(fmt.Sprintf("invalid project_id: %s", projectIDStr))
return
}
if !l.SecurityCtx.HasReadPerm(projectID) {
if !l.SecurityCtx.IsAuthenticated() {
l.HandleUnauthorized()
return
}
l.HandleForbidden(l.SecurityCtx.GetUsername())
return
}
query.ProjectID = projectID
}
total, err := dao.GetTotalOfLabels(query)
if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to get total count of labels: %v", err))
return
}
query.Page, query.Size = l.GetPaginationParams()
labels, err := dao.ListLabels(query)
if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to list labels: %v", err))
return
}
l.SetPaginationHeader(total, query.Page, query.Size)
l.Data["json"] = labels
l.ServeJSON()
}
// Put updates the label
func (l *LabelAPI) Put() {
label := &models.Label{}
l.DecodeJSONReq(label)
oldName := l.label.Name
// only name, description and color can be changed
l.label.Name = label.Name
l.label.Description = label.Description
l.label.Color = label.Color
l.Validate(l.label)
if l.label.Name != oldName {
labels, err := dao.ListLabels(&models.LabelQuery{
Name: l.label.Name,
Level: l.label.Level,
Scope: l.label.Scope,
ProjectID: l.label.ProjectID,
})
if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to list labels: %v", err))
return
}
if len(labels) > 0 {
l.HandleConflict()
return
}
}
if err := dao.UpdateLabel(l.label); err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to update label %d: %v", l.label.ID, err))
return
}
}
// Delete the label
func (l *LabelAPI) Delete() {
id := l.label.ID
if err := dao.DeleteLabel(id); err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to delete label %d: %v", id, err))
return
}
}

435
src/ui/api/label_test.go Normal file
View File

@ -0,0 +1,435 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/models"
)
var (
labelAPIBasePath = "/api/labels"
labelID int64
)
func TestLabelAPIPost(t *testing.T) {
postFunc := func(resp *httptest.ResponseRecorder) error {
id, err := parseResourceID(resp)
if err != nil {
return err
}
labelID = id
return nil
}
cases := []*codeCheckingCase{
// 401
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPost,
url: labelAPIBasePath,
},
code: http.StatusUnauthorized,
},
// 400
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPost,
url: labelAPIBasePath,
bodyJSON: &models.Label{},
credential: nonSysAdmin,
},
code: http.StatusBadRequest,
},
// 403 non-sysadmin try to create global label
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPost,
url: labelAPIBasePath,
bodyJSON: &models.Label{
Name: "test",
Scope: common.LabelScopeGlobal,
},
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 403 non-member user try to create project label
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPost,
url: labelAPIBasePath,
bodyJSON: &models.Label{
Name: "test",
Scope: common.LabelScopeProject,
ProjectID: 1,
},
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 403 developer try to create project label
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPost,
url: labelAPIBasePath,
bodyJSON: &models.Label{
Name: "test",
Scope: common.LabelScopeProject,
ProjectID: 1,
},
credential: projDeveloper,
},
code: http.StatusForbidden,
},
// 404 non-exist project
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPost,
url: labelAPIBasePath,
bodyJSON: &models.Label{
Name: "test",
Scope: common.LabelScopeProject,
ProjectID: 10000,
},
credential: projAdmin,
},
code: http.StatusNotFound,
},
// 200
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPost,
url: labelAPIBasePath,
bodyJSON: &models.Label{
Name: "test",
Scope: common.LabelScopeProject,
ProjectID: 1,
},
credential: projAdmin,
},
code: http.StatusCreated,
postFunc: postFunc,
},
// 409
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPost,
url: labelAPIBasePath,
bodyJSON: &models.Label{
Name: "test",
Scope: common.LabelScopeProject,
ProjectID: 1,
},
credential: projAdmin,
},
code: http.StatusConflict,
},
}
runCodeCheckingCases(t, cases...)
}
func TestLabelAPIGet(t *testing.T) {
cases := []*codeCheckingCase{
// 400
&codeCheckingCase{
request: &testingRequest{
method: http.MethodGet,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, 0),
},
code: http.StatusBadRequest,
},
// 404
&codeCheckingCase{
request: &testingRequest{
method: http.MethodGet,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, 1000),
},
code: http.StatusNotFound,
},
// 200
&codeCheckingCase{
request: &testingRequest{
method: http.MethodGet,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}
func TestLabelAPIList(t *testing.T) {
cases := []*codeCheckingCase{
// 400 no scope query string
&codeCheckingCase{
request: &testingRequest{
method: http.MethodGet,
url: labelAPIBasePath,
},
code: http.StatusBadRequest,
},
// 400 invalid scope
&codeCheckingCase{
request: &testingRequest{
method: http.MethodGet,
url: labelAPIBasePath,
queryStruct: struct {
Scope string `url:"scope"`
}{
Scope: "invalid_scope",
},
},
code: http.StatusBadRequest,
},
// 400 invalid project_id
&codeCheckingCase{
request: &testingRequest{
method: http.MethodGet,
url: labelAPIBasePath,
queryStruct: struct {
Scope string `url:"scope"`
ProjectID int64 `url:"project_id"`
}{
Scope: "p",
ProjectID: 0,
},
},
code: http.StatusBadRequest,
},
}
runCodeCheckingCases(t, cases...)
// 200
labels := []*models.Label{}
err := handleAndParse(&testingRequest{
method: http.MethodGet,
url: labelAPIBasePath,
queryStruct: struct {
Scope string `url:"scope"`
ProjectID int64 `url:"project_id"`
Name string `url:"name"`
}{
Scope: "p",
ProjectID: 1,
Name: "test",
},
}, &labels)
require.Nil(t, err)
assert.Equal(t, 1, len(labels))
err = handleAndParse(&testingRequest{
method: http.MethodGet,
url: labelAPIBasePath,
queryStruct: struct {
Scope string `url:"scope"`
ProjectID int64 `url:"project_id"`
Name string `url:"name"`
}{
Scope: "p",
ProjectID: 1,
Name: "dev",
},
}, &labels)
require.Nil(t, err)
assert.Equal(t, 0, len(labels))
}
func TestLabelAPIPut(t *testing.T) {
cases := []*codeCheckingCase{
// 401
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
},
code: http.StatusUnauthorized,
},
// 400
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, 0),
credential: nonSysAdmin,
},
code: http.StatusBadRequest,
},
// 404
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, 10000),
credential: nonSysAdmin,
},
code: http.StatusNotFound,
},
// 403 non-member user
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 403 developer
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
credential: projDeveloper,
},
code: http.StatusForbidden,
},
// 400
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
bodyJSON: &models.Label{
Name: "",
Scope: common.LabelScopeProject,
ProjectID: 1,
},
credential: projAdmin,
},
code: http.StatusBadRequest,
},
// 200
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
bodyJSON: &models.Label{
Name: "product",
Scope: common.LabelScopeProject,
ProjectID: 1,
},
credential: projAdmin,
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
label := &models.Label{}
err := handleAndParse(&testingRequest{
method: http.MethodGet,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
}, label)
require.Nil(t, err)
assert.Equal(t, "product", label.Name)
}
func TestLabelAPIDelete(t *testing.T) {
cases := []*codeCheckingCase{
// 401
&codeCheckingCase{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
},
code: http.StatusUnauthorized,
},
// 400
&codeCheckingCase{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, 0),
credential: nonSysAdmin,
},
code: http.StatusBadRequest,
},
// 404
&codeCheckingCase{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, 10000),
credential: nonSysAdmin,
},
code: http.StatusNotFound,
},
// 403 non-member user
&codeCheckingCase{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 403 developer
&codeCheckingCase{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
credential: projDeveloper,
},
code: http.StatusForbidden,
},
// 200
&codeCheckingCase{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
credential: projAdmin,
},
code: http.StatusOK,
},
// 404
&codeCheckingCase{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
credential: projAdmin,
},
code: http.StatusNotFound,
},
}
runCodeCheckingCases(t, cases...)
}

View File

@ -94,6 +94,8 @@ func initRouters() {
beego.Router("/api/configurations/reset", &api.ConfigAPI{}, "post:Reset")
beego.Router("/api/statistics", &api.StatisticAPI{})
beego.Router("/api/replications", &api.ReplicationAPI{})
beego.Router("/api/labels", &api.LabelAPI{}, "post:Post;get:List")
beego.Router("/api/labels/:id([0-9]+", &api.LabelAPI{}, "get:Get;put:Put;delete:Delete")
beego.Router("/api/systeminfo", &api.SystemInfoAPI{}, "get:GetGeneralInfo")
beego.Router("/api/systeminfo/volumes", &api.SystemInfoAPI{}, "get:GetVolumeInfo")

View File

@ -65,3 +65,7 @@ Changelog for harbor database schema
- add pk `id` to table `properties`
- remove pk index from column 'k' of table `properties`
- alter `name` length from 41 to 256 of table `project`
## 1.5.0
- create table `harbor_label`