mirror of
https://github.com/goharbor/harbor.git
synced 2025-01-20 06:31:55 +01:00
Merge pull request #4372 from ywk253100/180302_label
Implement label management API
This commit is contained in:
commit
03bf000770
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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
99
src/common/dao/label.go
Normal 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
|
||||
}
|
91
src/common/dao/label_test.go
Normal file
91
src/common/dao/label_test.go
Normal 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)
|
||||
}
|
@ -32,5 +32,6 @@ func init() {
|
||||
new(ClairVulnTimestamp),
|
||||
new(WatchItem),
|
||||
new(ProjectMetadata),
|
||||
new(ConfigEntry))
|
||||
new(ConfigEntry),
|
||||
new(Label))
|
||||
}
|
||||
|
94
src/common/models/label.go
Normal file
94
src/common/models/label.go
Normal 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))
|
||||
}
|
||||
}
|
||||
*/
|
86
src/common/models/label_test.go
Normal file
86
src/common/models/label_test.go
Normal 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())
|
||||
}
|
||||
}
|
@ -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
263
src/ui/api/label.go
Normal 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
435
src/ui/api/label_test.go
Normal 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...)
|
||||
}
|
@ -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")
|
||||
|
@ -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`
|
Loading…
Reference in New Issue
Block a user