mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-19 15:17:43 +01:00
Merge pull request #4396 from ywk253100/180309_label_resource
Implement adding/removing labels to/from repositories and images API
This commit is contained in:
commit
0efd8e3c54
@ -995,6 +995,86 @@ paths:
|
||||
description: Forbidden.
|
||||
'404':
|
||||
description: Repository not found.
|
||||
'/repositories/{repo_name}/labels':
|
||||
get:
|
||||
summary: Get labels of a repository.
|
||||
description: |
|
||||
Get labels of a repository specified by the repo_name.
|
||||
parameters:
|
||||
- name: repo_name
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
description: The name of repository.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Label'
|
||||
'401':
|
||||
description: Unauthorized.
|
||||
'403':
|
||||
description: Forbidden. User should have read permisson for the repository to perform the action.
|
||||
'404':
|
||||
description: Repository not found.
|
||||
post:
|
||||
summary: Add a label to the repository.
|
||||
description: |
|
||||
Add a label to the repository.
|
||||
parameters:
|
||||
- name: repo_name
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
description: The name of repository.
|
||||
- name: label
|
||||
in: body
|
||||
description: Only the ID property is required.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/Label'
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully.
|
||||
'401':
|
||||
description: Unauthorized.
|
||||
'403':
|
||||
description: Forbidden. User should have write permisson for the repository to perform the action.
|
||||
'404':
|
||||
description: Resource not found.
|
||||
'/repositories/{repo_name}/labels/{label_id}':
|
||||
delete:
|
||||
summary: Delete label from the repository.
|
||||
description: |
|
||||
Delete the label from the repository specified by the repo_name.
|
||||
parameters:
|
||||
- name: repo_name
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
description: The name of repository.
|
||||
- name: label_id
|
||||
in: path
|
||||
type: integer
|
||||
required: true
|
||||
description: The ID of label.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully.
|
||||
'401':
|
||||
description: Unauthorized.
|
||||
'403':
|
||||
description: Forbidden. User should have write permisson for the repository to perform the action.
|
||||
'404':
|
||||
description: Resource not found.
|
||||
'/repositories/{repo_name}/tags/{tag}':
|
||||
get:
|
||||
summary: Get the tag of the repository.
|
||||
@ -1075,6 +1155,101 @@ paths:
|
||||
$ref: '#/definitions/DetailedTag'
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/repositories/{repo_name}/tags/{tag}/labels':
|
||||
get:
|
||||
summary: Get labels of an image.
|
||||
description: |
|
||||
Get labels of an image specified by the repo_name and tag.
|
||||
parameters:
|
||||
- name: repo_name
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
description: The name of repository.
|
||||
- name: tag
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
description: The tag of the image.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Label'
|
||||
'401':
|
||||
description: Unauthorized.
|
||||
'403':
|
||||
description: Forbidden. User should have read permisson for the image to perform the action.
|
||||
'404':
|
||||
description: Resource not found.
|
||||
post:
|
||||
summary: Add a label to image.
|
||||
description: |
|
||||
Add a label to the image.
|
||||
parameters:
|
||||
- name: repo_name
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
description: The name of repository.
|
||||
- name: tag
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
description: The tag of the image.
|
||||
- name: label
|
||||
in: body
|
||||
description: Only the ID property is required.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/Label'
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully.
|
||||
'401':
|
||||
description: Unauthorized.
|
||||
'403':
|
||||
description: Forbidden. User should have write permisson for the image to perform the action.
|
||||
'404':
|
||||
description: Resource not found.
|
||||
'/repositories/{repo_name}/tags/{tag}/labels/{label_id}':
|
||||
delete:
|
||||
summary: Delete label from the image.
|
||||
description: |
|
||||
Delete the label from the image specified by the repo_name and tag.
|
||||
parameters:
|
||||
- name: repo_name
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
description: The name of repository.
|
||||
- name: tag
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
description: The tag of the image.
|
||||
- name: label_id
|
||||
in: path
|
||||
type: integer
|
||||
required: true
|
||||
description: The ID of label.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully.
|
||||
'401':
|
||||
description: Unauthorized.
|
||||
'403':
|
||||
description: Forbidden. User should have write permisson for the image to perform the action.
|
||||
'404':
|
||||
description: Resource not found.
|
||||
'/repositories/{repo_name}/tags/{tag}/manifest':
|
||||
get:
|
||||
summary: Get manifests of a relevant repository.
|
||||
@ -3039,6 +3214,11 @@ definitions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/ComponentOverviewEntry'
|
||||
labels:
|
||||
type: array
|
||||
description: The label list.
|
||||
items:
|
||||
$ref: '#/definitions/Label'
|
||||
ComponentOverviewEntry:
|
||||
type: object
|
||||
properties:
|
||||
@ -3072,6 +3252,11 @@ definitions:
|
||||
tags_count:
|
||||
type: integer
|
||||
description: The tags count of repository.
|
||||
labels:
|
||||
type: array
|
||||
description: The label list.
|
||||
items:
|
||||
$ref: '#/definitions/Label'
|
||||
creation_time:
|
||||
type: string
|
||||
description: The creation time of repository.
|
||||
|
@ -272,6 +272,23 @@ create table harbor_label (
|
||||
CONSTRAINT unique_name_and_scope UNIQUE (name,scope)
|
||||
);
|
||||
|
||||
create table harbor_resource_label (
|
||||
id int NOT NULL AUTO_INCREMENT,
|
||||
label_id int NOT NULL,
|
||||
# the resource_id is the ID of project when the resource_type is p
|
||||
# the resource_id is the ID of repository when the resource_type is r
|
||||
# the resource_id is the name of image when the resource_type is i
|
||||
resource_id varchar(256) NOT NULL,
|
||||
# 'p' for project
|
||||
# 'r' for repository
|
||||
# 'i' for image
|
||||
resource_type char(1) NOT NULL,
|
||||
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY(id),
|
||||
CONSTRAINT unique_label_resource UNIQUE (label_id,resource_id, resource_type)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `alembic_version` (
|
||||
`version_num` varchar(32) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
@ -261,6 +261,26 @@ create table harbor_label (
|
||||
UNIQUE(name, scope)
|
||||
);
|
||||
|
||||
create table harbor_resource_label (
|
||||
id INTEGER PRIMARY KEY,
|
||||
label_id int NOT NULL,
|
||||
/*
|
||||
the resource_id is the ID of project when the resource_type is p
|
||||
the resource_id is the ID of repository when the resource_type is r
|
||||
the resource_id is the name of image when the resource_type is i
|
||||
*/
|
||||
resource_id varchar(256) NOT NULL,
|
||||
/*
|
||||
'p' for project
|
||||
'r' for repository
|
||||
'i' for image
|
||||
*/
|
||||
resource_type char(1) NOT NULL,
|
||||
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||
update_time timestamp default CURRENT_TIMESTAMP,
|
||||
UNIQUE (label_id,resource_id, resource_type)
|
||||
);
|
||||
|
||||
create table alembic_version (
|
||||
version_num varchar(32) NOT NULL
|
||||
);
|
||||
|
74
src/common/dao/resource_label.go
Normal file
74
src/common/dao/resource_label.go
Normal file
@ -0,0 +1,74 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// AddResourceLabel add a label to a resource
|
||||
func AddResourceLabel(rl *models.ResourceLabel) (int64, error) {
|
||||
now := time.Now()
|
||||
rl.CreationTime = now
|
||||
rl.UpdateTime = now
|
||||
return GetOrmer().Insert(rl)
|
||||
}
|
||||
|
||||
// GetResourceLabel specified by ID
|
||||
func GetResourceLabel(rType, rID string, labelID int64) (*models.ResourceLabel, error) {
|
||||
rl := &models.ResourceLabel{
|
||||
ResourceType: rType,
|
||||
ResourceID: rID,
|
||||
LabelID: labelID,
|
||||
}
|
||||
if err := GetOrmer().Read(rl, "ResourceType", "ResourceID", "LabelID"); err != nil {
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rl, nil
|
||||
}
|
||||
|
||||
// GetLabelsOfResource returns the label list of the resource
|
||||
func GetLabelsOfResource(rType, rID string) ([]*models.Label, error) {
|
||||
sql := `select l.id, l.name, l.description, l.color, l.scope, l.project_id, l.creation_time, l.update_time
|
||||
from harbor_resource_label rl
|
||||
join harbor_label l on rl.label_id=l.id
|
||||
where rl.resource_type = ? and rl.resource_id = ?`
|
||||
labels := []*models.Label{}
|
||||
_, err := GetOrmer().Raw(sql, rType, rID).QueryRows(&labels)
|
||||
return labels, err
|
||||
}
|
||||
|
||||
// DeleteResourceLabel ...
|
||||
func DeleteResourceLabel(id int64) error {
|
||||
_, err := GetOrmer().Delete(&models.ResourceLabel{
|
||||
ID: id,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteLabelsOfResource removes all labels of resource specified by rType and rID
|
||||
func DeleteLabelsOfResource(rType, rID string) error {
|
||||
_, err := GetOrmer().QueryTable(&models.ResourceLabel{}).
|
||||
Filter("ResourceType", rType).
|
||||
Filter("ResourceID", rID).Delete()
|
||||
return err
|
||||
}
|
74
src/common/dao/resource_label_test.go
Normal file
74
src/common/dao/resource_label_test.go
Normal file
@ -0,0 +1,74 @@
|
||||
// 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/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/vmware/harbor/src/common"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
)
|
||||
|
||||
func TestMethodsOfResourceLabel(t *testing.T) {
|
||||
labelID, err := AddLabel(&models.Label{
|
||||
Name: "test_label",
|
||||
Level: common.LabelLevelUser,
|
||||
Scope: common.LabelScopeGlobal,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer DeleteLabel(labelID)
|
||||
|
||||
resourceID := "1"
|
||||
resourceType := common.ResourceTypeRepository
|
||||
|
||||
// add
|
||||
rl := &models.ResourceLabel{
|
||||
LabelID: labelID,
|
||||
ResourceType: resourceType,
|
||||
ResourceID: resourceID,
|
||||
}
|
||||
|
||||
id, err := AddResourceLabel(rl)
|
||||
require.Nil(t, err)
|
||||
|
||||
// get
|
||||
r, err := GetResourceLabel(resourceType, resourceID, labelID)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, id, r.ID)
|
||||
|
||||
// get by resource
|
||||
labels, err := GetLabelsOfResource(resourceType, resourceID)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, len(labels))
|
||||
assert.Equal(t, id, r.ID)
|
||||
|
||||
// delete
|
||||
err = DeleteResourceLabel(id)
|
||||
require.Nil(t, err)
|
||||
labels, err = GetLabelsOfResource(resourceType, resourceID)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 0, len(labels))
|
||||
|
||||
// delete by resource
|
||||
id, err = AddResourceLabel(rl)
|
||||
require.Nil(t, err)
|
||||
err = DeleteLabelsOfResource(resourceType, resourceID)
|
||||
require.Nil(t, err)
|
||||
labels, err = GetLabelsOfResource(resourceType, resourceID)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 0, len(labels))
|
||||
}
|
@ -33,5 +33,6 @@ func init() {
|
||||
new(WatchItem),
|
||||
new(ProjectMetadata),
|
||||
new(ConfigEntry),
|
||||
new(Label))
|
||||
new(Label),
|
||||
new(ResourceLabel))
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ type Label struct {
|
||||
UpdateTime time.Time `orm:"column(update_time)" json:"update_time"`
|
||||
}
|
||||
|
||||
//TableName ...
|
||||
// TableName ...
|
||||
func (l *Label) TableName() string {
|
||||
return "harbor_label"
|
||||
}
|
||||
@ -65,30 +65,17 @@ func (l *Label) Valid(v *validation.Validation) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// ResourceLabel records the relationship between resource and label
|
||||
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"`
|
||||
ID int64 `orm:"pk;auto;column(id)"`
|
||||
LabelID int64 `orm:"column(label_id)"`
|
||||
ResourceID string `orm:"column(resource_id)"`
|
||||
ResourceType string `orm:"column(resource_type)"`
|
||||
CreationTime time.Time `orm:"column(creation_time)"`
|
||||
UpdateTime time.Time `orm:"column(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))
|
||||
}
|
||||
// TableName ...
|
||||
func (r *ResourceLabel) TableName() string {
|
||||
return "harbor_resource_label"
|
||||
}
|
||||
*/
|
||||
|
@ -111,6 +111,10 @@ func init() {
|
||||
beego.Router("/api/users/?:id", &UserAPI{})
|
||||
beego.Router("/api/logs", &LogAPI{})
|
||||
beego.Router("/api/repositories/*", &RepositoryAPI{}, "put:Put")
|
||||
beego.Router("/api/repositories/*/labels", &RepositoryLabelAPI{}, "get:GetOfRepository;post:AddToRepository")
|
||||
beego.Router("/api/repositories/*/labels/:id([0-9]+", &RepositoryLabelAPI{}, "delete:RemoveFromRepository")
|
||||
beego.Router("/api/repositories/*/tags/:tag/labels", &RepositoryLabelAPI{}, "get:GetOfImage;post:AddToImage")
|
||||
beego.Router("/api/repositories/*/tags/:tag/labels/:id([0-9]+", &RepositoryLabelAPI{}, "delete:RemoveFromImage")
|
||||
beego.Router("/api/repositories/*/tags/:tag", &RepositoryAPI{}, "delete:Delete;get:GetTag")
|
||||
beego.Router("/api/repositories/*/tags", &RepositoryAPI{}, "get:GetTags")
|
||||
beego.Router("/api/repositories/*/tags/:tag/manifest", &RepositoryAPI{}, "get:GetManifests")
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/vmware/harbor/src/common"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/notifier"
|
||||
@ -47,15 +48,16 @@ type RepositoryAPI struct {
|
||||
}
|
||||
|
||||
type repoResp struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ProjectID int64 `json:"project_id"`
|
||||
Description string `json:"description"`
|
||||
PullCount int64 `json:"pull_count"`
|
||||
StarCount int64 `json:"star_count"`
|
||||
TagsCount int64 `json:"tags_count"`
|
||||
CreationTime time.Time `json:"creation_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ProjectID int64 `json:"project_id"`
|
||||
Description string `json:"description"`
|
||||
PullCount int64 `json:"pull_count"`
|
||||
StarCount int64 `json:"star_count"`
|
||||
TagsCount int64 `json:"tags_count"`
|
||||
Labels []*models.Label `json:"labels"`
|
||||
CreationTime time.Time `json:"creation_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
}
|
||||
|
||||
type tagDetail struct {
|
||||
@ -78,6 +80,7 @@ type tagResp struct {
|
||||
tagDetail
|
||||
Signature *notary.Target `json:"signature"`
|
||||
ScanOverview *models.ImgScanOverview `json:"scan_overview,omitempty"`
|
||||
Labels []*models.Label `json:"labels"`
|
||||
}
|
||||
|
||||
type manifestResp struct {
|
||||
@ -145,10 +148,10 @@ func getRepositories(projectID int64, keyword string,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return populateTagsCount(repositories)
|
||||
return assembleRepos(repositories)
|
||||
}
|
||||
|
||||
func populateTagsCount(repositories []*models.RepoRecord) ([]*repoResp, error) {
|
||||
func assembleRepos(repositories []*models.RepoRecord) ([]*repoResp, error) {
|
||||
result := []*repoResp{}
|
||||
for _, repository := range repositories {
|
||||
repo := &repoResp{
|
||||
@ -167,6 +170,15 @@ func populateTagsCount(repositories []*models.RepoRecord) ([]*repoResp, error) {
|
||||
return nil, err
|
||||
}
|
||||
repo.TagsCount = int64(len(tags))
|
||||
|
||||
labels, err := dao.GetLabelsOfResource(common.ResourceTypeRepository,
|
||||
strconv.FormatInt(repository.RepositoryID, 10))
|
||||
if err != nil {
|
||||
log.Errorf("failed to get labels of repository %s: %v", repository.Name, err)
|
||||
} else {
|
||||
repo.Labels = labels
|
||||
}
|
||||
|
||||
result = append(result, repo)
|
||||
}
|
||||
return result, nil
|
||||
@ -252,6 +264,11 @@ func (ra *RepositoryAPI) Delete() {
|
||||
}
|
||||
|
||||
for _, t := range tags {
|
||||
image := fmt.Sprintf("%s:%s", repoName, t)
|
||||
if err = dao.DeleteLabelsOfResource(common.ResourceTypeImage, image); err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("failed to delete labels of image %s: %v", image, err))
|
||||
return
|
||||
}
|
||||
if err = rc.DeleteTag(t); err != nil {
|
||||
if regErr, ok := err.(*registry_error.HTTPError); ok {
|
||||
if regErr.StatusCode == http.StatusNotFound {
|
||||
@ -296,6 +313,22 @@ func (ra *RepositoryAPI) Delete() {
|
||||
ra.CustomAbort(http.StatusInternalServerError, "")
|
||||
}
|
||||
if !exist {
|
||||
repository, err := dao.GetRepositoryByName(repoName)
|
||||
if err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("failed to get repository %s: %v", repoName, err))
|
||||
return
|
||||
}
|
||||
if repository == nil {
|
||||
ra.HandleNotFound(fmt.Sprintf("repository %s not found", repoName))
|
||||
return
|
||||
}
|
||||
|
||||
if err = dao.DeleteLabelsOfResource(common.ResourceTypeRepository,
|
||||
strconv.FormatInt(repository.RepositoryID, 10)); err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("failed to delete labels of repository %s: %v",
|
||||
repoName, err))
|
||||
return
|
||||
}
|
||||
if err = dao.DeleteRepository(repoName); err != nil {
|
||||
log.Errorf("failed to delete repository %s: %v", repoName, err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "")
|
||||
@ -343,7 +376,7 @@ func (ra *RepositoryAPI) GetTag() {
|
||||
return
|
||||
}
|
||||
|
||||
result := assemble(client, repository, []string{tag},
|
||||
result := assembleTags(client, repository, []string{tag},
|
||||
ra.SecurityCtx.GetUsername())
|
||||
ra.Data["json"] = result[0]
|
||||
ra.ServeJSON()
|
||||
@ -387,13 +420,13 @@ func (ra *RepositoryAPI) GetTags() {
|
||||
return
|
||||
}
|
||||
|
||||
ra.Data["json"] = assemble(client, repoName, tags, ra.SecurityCtx.GetUsername())
|
||||
ra.Data["json"] = assembleTags(client, repoName, tags, ra.SecurityCtx.GetUsername())
|
||||
ra.ServeJSON()
|
||||
}
|
||||
|
||||
// get config, signature and scan overview and assemble them into one
|
||||
// struct for each tag in tags
|
||||
func assemble(client *registry.Repository, repository string,
|
||||
func assembleTags(client *registry.Repository, repository string,
|
||||
tags []string, username string) []*tagResp {
|
||||
|
||||
var err error
|
||||
@ -435,6 +468,15 @@ func assemble(client *registry.Repository, repository string,
|
||||
}
|
||||
}
|
||||
|
||||
// labels
|
||||
image := fmt.Sprintf("%s:%s", repository, t)
|
||||
labels, err := dao.GetLabelsOfResource(common.ResourceTypeImage, image)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get labels of image %s: %v", image, err)
|
||||
} else {
|
||||
item.Labels = labels
|
||||
}
|
||||
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
@ -633,7 +675,7 @@ func (ra *RepositoryAPI) GetTopRepos() {
|
||||
ra.CustomAbort(http.StatusInternalServerError, "internal server error")
|
||||
}
|
||||
|
||||
result, err := populateTagsCount(repos)
|
||||
result, err := assembleRepos(repos)
|
||||
if err != nil {
|
||||
log.Errorf("failed to popultate tags count to repositories: %v", err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "internal server error")
|
||||
|
248
src/ui/api/repository_label.go
Normal file
248
src/ui/api/repository_label.go
Normal file
@ -0,0 +1,248 @@
|
||||
// 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"
|
||||
"github.com/vmware/harbor/src/common/utils"
|
||||
uiutils "github.com/vmware/harbor/src/ui/utils"
|
||||
)
|
||||
|
||||
// RepositoryLabelAPI handles requests for adding/removing label to/from repositories and images
|
||||
type RepositoryLabelAPI struct {
|
||||
BaseController
|
||||
repository *models.RepoRecord
|
||||
tag string
|
||||
label *models.Label
|
||||
}
|
||||
|
||||
// Prepare ...
|
||||
func (r *RepositoryLabelAPI) Prepare() {
|
||||
r.BaseController.Prepare()
|
||||
if !r.SecurityCtx.IsAuthenticated() {
|
||||
r.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
repository := r.GetString(":splat")
|
||||
project, _ := utils.ParseRepository(repository)
|
||||
if !r.SecurityCtx.HasWritePerm(project) {
|
||||
r.HandleForbidden(r.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
|
||||
repo, err := dao.GetRepositoryByName(repository)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to get repository %s: %v",
|
||||
repository, err))
|
||||
return
|
||||
}
|
||||
if repo == nil {
|
||||
r.HandleNotFound(fmt.Sprintf("repository %s not found", repository))
|
||||
return
|
||||
}
|
||||
r.repository = repo
|
||||
|
||||
tag := r.GetString(":tag")
|
||||
if len(tag) > 0 {
|
||||
exist, err := imageExist(r.SecurityCtx.GetUsername(), repository, tag)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to check the existence of image %s:%s: %v",
|
||||
repository, tag, err))
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
r.HandleNotFound(fmt.Sprintf("image %s:%s not found", repository, tag))
|
||||
return
|
||||
}
|
||||
r.tag = tag
|
||||
}
|
||||
|
||||
if r.Ctx.Request.Method == http.MethodPost {
|
||||
l := &models.Label{}
|
||||
r.DecodeJSONReq(l)
|
||||
|
||||
label, err := dao.GetLabel(l.ID)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to get label %d: %v", l.ID, err))
|
||||
return
|
||||
}
|
||||
|
||||
if label == nil {
|
||||
r.HandleNotFound(fmt.Sprintf("label %d not found", l.ID))
|
||||
return
|
||||
}
|
||||
|
||||
if label.Level != common.LabelLevelUser {
|
||||
r.HandleBadRequest("only user level labels can be used")
|
||||
return
|
||||
}
|
||||
|
||||
if label.Scope == common.LabelScopeProject {
|
||||
p, err := r.ProjectMgr.Get(project)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to get project %s: %v", project, err))
|
||||
return
|
||||
}
|
||||
|
||||
if p.ProjectID != label.ProjectID {
|
||||
r.HandleBadRequest("can not add labels which don't belong to the project to the resources under the project")
|
||||
return
|
||||
}
|
||||
}
|
||||
r.label = label
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if r.Ctx.Request.Method == http.MethodDelete {
|
||||
labelID, err := r.GetInt64FromPath(":id")
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to get ID parameter from path: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
label, err := dao.GetLabel(labelID)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to get label %d: %v", labelID, err))
|
||||
return
|
||||
}
|
||||
|
||||
if label == nil {
|
||||
r.HandleNotFound(fmt.Sprintf("label %d not found", labelID))
|
||||
return
|
||||
}
|
||||
r.label = label
|
||||
}
|
||||
}
|
||||
|
||||
// GetOfImage returns labels of an image
|
||||
func (r *RepositoryLabelAPI) GetOfImage() {
|
||||
r.getLabels(common.ResourceTypeImage, fmt.Sprintf("%s:%s", r.repository.Name, r.tag))
|
||||
}
|
||||
|
||||
// AddToImage adds the label to an image
|
||||
func (r *RepositoryLabelAPI) AddToImage() {
|
||||
rl := &models.ResourceLabel{
|
||||
LabelID: r.label.ID,
|
||||
ResourceType: common.ResourceTypeImage,
|
||||
ResourceID: fmt.Sprintf("%s:%s", r.repository.Name, r.tag),
|
||||
}
|
||||
r.addLabel(rl)
|
||||
}
|
||||
|
||||
// RemoveFromImage removes the label from an image
|
||||
func (r *RepositoryLabelAPI) RemoveFromImage() {
|
||||
rl := &models.ResourceLabel{
|
||||
LabelID: r.label.ID,
|
||||
ResourceType: common.ResourceTypeImage,
|
||||
ResourceID: fmt.Sprintf("%s:%s", r.repository.Name, r.tag),
|
||||
}
|
||||
r.removeLabel(rl)
|
||||
}
|
||||
|
||||
// GetOfRepository returns labels of a repository
|
||||
func (r *RepositoryLabelAPI) GetOfRepository() {
|
||||
r.getLabels(common.ResourceTypeRepository, strconv.FormatInt(r.repository.RepositoryID, 10))
|
||||
}
|
||||
|
||||
// AddToRepository adds the label to a repository
|
||||
func (r *RepositoryLabelAPI) AddToRepository() {
|
||||
rl := &models.ResourceLabel{
|
||||
LabelID: r.label.ID,
|
||||
ResourceType: common.ResourceTypeRepository,
|
||||
ResourceID: strconv.FormatInt(r.repository.RepositoryID, 10),
|
||||
}
|
||||
r.addLabel(rl)
|
||||
}
|
||||
|
||||
// RemoveFromRepository removes the label from a repository
|
||||
func (r *RepositoryLabelAPI) RemoveFromRepository() {
|
||||
rl := &models.ResourceLabel{
|
||||
LabelID: r.label.ID,
|
||||
ResourceType: common.ResourceTypeRepository,
|
||||
ResourceID: strconv.FormatInt(r.repository.RepositoryID, 10),
|
||||
}
|
||||
r.removeLabel(rl)
|
||||
}
|
||||
|
||||
func (r *RepositoryLabelAPI) getLabels(rType, rID string) {
|
||||
labels, err := dao.GetLabelsOfResource(rType, rID)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to get labels of resource %s %s: %v",
|
||||
rType, rID, err))
|
||||
return
|
||||
}
|
||||
r.Data["json"] = labels
|
||||
r.ServeJSON()
|
||||
}
|
||||
|
||||
func (r *RepositoryLabelAPI) addLabel(rl *models.ResourceLabel) {
|
||||
rlabel, err := dao.GetResourceLabel(rl.ResourceType, rl.ResourceID, rl.LabelID)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to check the existence of label %d for resource %s %s: %v",
|
||||
rl.LabelID, rl.ResourceType, rl.ResourceID, err))
|
||||
return
|
||||
}
|
||||
|
||||
if rlabel != nil {
|
||||
r.HandleConflict()
|
||||
return
|
||||
}
|
||||
if _, err := dao.AddResourceLabel(rl); err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to add label %d to resource %s %s: %v",
|
||||
rl.LabelID, rl.ResourceType, rl.ResourceID, err))
|
||||
return
|
||||
}
|
||||
|
||||
// return the ID of label and return status code 200 rather than 201 as the label is not created
|
||||
r.Redirect(http.StatusOK, strconv.FormatInt(rl.LabelID, 10))
|
||||
}
|
||||
|
||||
func (r *RepositoryLabelAPI) removeLabel(rl *models.ResourceLabel) {
|
||||
rlabel, err := dao.GetResourceLabel(rl.ResourceType, rl.ResourceID, rl.LabelID)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to check the existence of label %d for resource %s %s: %v",
|
||||
rl.LabelID, rl.ResourceType, rl.ResourceID, err))
|
||||
return
|
||||
}
|
||||
|
||||
if rlabel == nil {
|
||||
r.HandleNotFound(fmt.Sprintf("label %d of resource %s %s not found",
|
||||
rl.LabelID, rl.ResourceType, rl.ResourceID))
|
||||
return
|
||||
}
|
||||
if err = dao.DeleteResourceLabel(rlabel.ID); err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to delete resource label record %d: %v",
|
||||
rlabel.ID, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func imageExist(username, repository, tag string) (bool, error) {
|
||||
client, err := uiutils.NewRepositoryClientForUI(username, repository)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
_, exist, err := client.ManifestExist(tag)
|
||||
return exist, err
|
||||
}
|
253
src/ui/api/repository_label_test.go
Normal file
253
src/ui/api/repository_label_test.go
Normal file
@ -0,0 +1,253 @@
|
||||
// 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"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/vmware/harbor/src/common"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
)
|
||||
|
||||
var (
|
||||
resourceLabelAPIBasePath = "/api/repositories"
|
||||
repository = "library/hello-world"
|
||||
tag = "latest"
|
||||
proLibraryLabelID int64
|
||||
)
|
||||
|
||||
func TestAddToImage(t *testing.T) {
|
||||
sysLevelLabelID, err := dao.AddLabel(&models.Label{
|
||||
Name: "sys_level_label",
|
||||
Level: common.LabelLevelSystem,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer dao.DeleteLabel(sysLevelLabelID)
|
||||
|
||||
proTestLabelID, err := dao.AddLabel(&models.Label{
|
||||
Name: "pro_test_label",
|
||||
Level: common.LabelLevelUser,
|
||||
Scope: common.LabelScopeProject,
|
||||
ProjectID: 100,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer dao.DeleteLabel(proTestLabelID)
|
||||
|
||||
proLibraryLabelID, err = dao.AddLabel(&models.Label{
|
||||
Name: "pro_library_label",
|
||||
Level: common.LabelLevelUser,
|
||||
Scope: common.LabelScopeProject,
|
||||
ProjectID: 1,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
cases := []*codeCheckingCase{
|
||||
// 401
|
||||
&codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
|
||||
repository, tag),
|
||||
method: http.MethodPost,
|
||||
},
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
// 403
|
||||
&codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
|
||||
repository, tag),
|
||||
method: http.MethodPost,
|
||||
credential: projGuest,
|
||||
},
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
// 404 repository doesn't exist
|
||||
&codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: fmt.Sprintf("%s/library/non-exist-repo/tags/%s/labels", resourceLabelAPIBasePath, tag),
|
||||
method: http.MethodPost,
|
||||
credential: projDeveloper,
|
||||
},
|
||||
code: http.StatusNotFound,
|
||||
},
|
||||
// 404 image doesn't exist
|
||||
&codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: fmt.Sprintf("%s/%s/tags/non-exist-tag/labels", resourceLabelAPIBasePath, repository),
|
||||
method: http.MethodPost,
|
||||
credential: projDeveloper,
|
||||
},
|
||||
code: http.StatusNotFound,
|
||||
},
|
||||
// 400
|
||||
&codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath, repository, tag),
|
||||
method: http.MethodPost,
|
||||
credential: projDeveloper,
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 404 label doesn't exist
|
||||
&codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
|
||||
repository, tag),
|
||||
method: http.MethodPost,
|
||||
credential: projDeveloper,
|
||||
bodyJSON: struct {
|
||||
ID int64
|
||||
}{
|
||||
ID: 1000,
|
||||
},
|
||||
},
|
||||
code: http.StatusNotFound,
|
||||
},
|
||||
// 400 system level label
|
||||
&codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
|
||||
repository, tag),
|
||||
method: http.MethodPost,
|
||||
credential: projDeveloper,
|
||||
bodyJSON: struct {
|
||||
ID int64
|
||||
}{
|
||||
ID: sysLevelLabelID,
|
||||
},
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 400 try to add the label of project1 to the image under project2
|
||||
&codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
|
||||
repository, tag),
|
||||
method: http.MethodPost,
|
||||
credential: projDeveloper,
|
||||
bodyJSON: struct {
|
||||
ID int64
|
||||
}{
|
||||
ID: proTestLabelID,
|
||||
},
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
// 200
|
||||
&codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
|
||||
repository, tag),
|
||||
method: http.MethodPost,
|
||||
credential: projDeveloper,
|
||||
bodyJSON: struct {
|
||||
ID int64
|
||||
}{
|
||||
ID: proLibraryLabelID,
|
||||
},
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
}
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
||||
|
||||
func TestGetOfImage(t *testing.T) {
|
||||
labels := []*models.Label{}
|
||||
err := handleAndParse(&testingRequest{
|
||||
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath, repository, tag),
|
||||
method: http.MethodGet,
|
||||
credential: projDeveloper,
|
||||
}, &labels)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, len(labels))
|
||||
assert.Equal(t, proLibraryLabelID, labels[0].ID)
|
||||
}
|
||||
|
||||
func TestRemoveFromImage(t *testing.T) {
|
||||
runCodeCheckingCases(t, &codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: fmt.Sprintf("%s/%s/tags/%s/labels/%d", resourceLabelAPIBasePath,
|
||||
repository, tag, proLibraryLabelID),
|
||||
method: http.MethodDelete,
|
||||
credential: projDeveloper,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
})
|
||||
|
||||
labels := []*models.Label{}
|
||||
err := handleAndParse(&testingRequest{
|
||||
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
|
||||
repository, tag),
|
||||
method: http.MethodGet,
|
||||
credential: projDeveloper,
|
||||
}, &labels)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 0, len(labels))
|
||||
}
|
||||
|
||||
func TestAddToRepository(t *testing.T) {
|
||||
runCodeCheckingCases(t, &codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: fmt.Sprintf("%s/%s/labels", resourceLabelAPIBasePath, repository),
|
||||
method: http.MethodPost,
|
||||
bodyJSON: struct {
|
||||
ID int64
|
||||
}{
|
||||
ID: proLibraryLabelID,
|
||||
},
|
||||
credential: projDeveloper,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetOfRepository(t *testing.T) {
|
||||
labels := []*models.Label{}
|
||||
err := handleAndParse(&testingRequest{
|
||||
url: fmt.Sprintf("%s/%s/labels", resourceLabelAPIBasePath, repository),
|
||||
method: http.MethodGet,
|
||||
credential: projDeveloper,
|
||||
}, &labels)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, len(labels))
|
||||
assert.Equal(t, proLibraryLabelID, labels[0].ID)
|
||||
}
|
||||
|
||||
func TestRemoveFromRepository(t *testing.T) {
|
||||
runCodeCheckingCases(t, &codeCheckingCase{
|
||||
request: &testingRequest{
|
||||
url: fmt.Sprintf("%s/%s/labels/%d", resourceLabelAPIBasePath,
|
||||
repository, proLibraryLabelID),
|
||||
method: http.MethodDelete,
|
||||
credential: projDeveloper,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
})
|
||||
|
||||
labels := []*models.Label{}
|
||||
err := handleAndParse(&testingRequest{
|
||||
url: fmt.Sprintf("%s/%s/labels", resourceLabelAPIBasePath, repository),
|
||||
method: http.MethodGet,
|
||||
credential: projDeveloper,
|
||||
}, &labels)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 0, len(labels))
|
||||
}
|
@ -70,7 +70,11 @@ func initRouters() {
|
||||
beego.Router("/api/repositories", &api.RepositoryAPI{}, "get:Get")
|
||||
beego.Router("/api/repositories/scanAll", &api.RepositoryAPI{}, "post:ScanAll")
|
||||
beego.Router("/api/repositories/*", &api.RepositoryAPI{}, "delete:Delete;put:Put")
|
||||
beego.Router("/api/repositories/*/labels", &api.RepositoryLabelAPI{}, "get:GetOfRepository;post:AddToRepository")
|
||||
beego.Router("/api/repositories/*/labels/:id([0-9]+)", &api.RepositoryLabelAPI{}, "delete:RemoveFromRepository")
|
||||
beego.Router("/api/repositories/*/tags/:tag", &api.RepositoryAPI{}, "delete:Delete;get:GetTag")
|
||||
beego.Router("/api/repositories/*/tags/:tag/labels", &api.RepositoryLabelAPI{}, "get:GetOfImage;post:AddToImage")
|
||||
beego.Router("/api/repositories/*/tags/:tag/labels/:id([0-9]+)", &api.RepositoryLabelAPI{}, "delete:RemoveFromImage")
|
||||
beego.Router("/api/repositories/*/tags", &api.RepositoryAPI{}, "get:GetTags")
|
||||
beego.Router("/api/repositories/*/tags/:tag/scan", &api.RepositoryAPI{}, "post:ScanImage")
|
||||
beego.Router("/api/repositories/*/tags/:tag/vulnerability/details", &api.RepositoryAPI{}, "Get:VulnerabilityDetails")
|
||||
@ -95,7 +99,7 @@ func initRouters() {
|
||||
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/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")
|
||||
|
@ -68,4 +68,5 @@ Changelog for harbor database schema
|
||||
|
||||
## 1.5.0
|
||||
|
||||
- create table `harbor_label`
|
||||
- create table `harbor_label`
|
||||
- create table `harbor_resource_label`
|
Loading…
Reference in New Issue
Block a user