Merge pull request #4396 from ywk253100/180309_label_resource

Implement adding/removing labels to/from repositories and images API
This commit is contained in:
Daniel Jiang 2018-03-14 14:15:31 +08:00 committed by GitHub
commit 0efd8e3c54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 952 additions and 42 deletions

View File

@ -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.

View File

@ -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;

View File

@ -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
);

View 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
}

View 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))
}

View File

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

View File

@ -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"
}
*/

View File

@ -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")

View File

@ -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"
@ -54,6 +55,7 @@ type repoResp struct {
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"`
}
@ -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")

View 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
}

View 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))
}

View File

@ -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")

View File

@ -69,3 +69,4 @@ Changelog for harbor database schema
## 1.5.0
- create table `harbor_label`
- create table `harbor_resource_label`