Add API to support marking labels to charts

- add related chart label API entries
- extract label related functionalities to a separate manager interface
- add a base controller for label related actions
- add related UT cases

Signed-off-by: Steven Zou <szou@vmware.com>
This commit is contained in:
Steven Zou 2018-09-13 15:06:15 +08:00
parent abf67c8de0
commit 7b8fe27c22
17 changed files with 962 additions and 111 deletions

View File

@ -7,6 +7,7 @@ import (
"strings"
"sync"
"github.com/ghodss/yaml"
helm_repo "k8s.io/helm/pkg/repo"
)
@ -131,6 +132,28 @@ func (uh *UtilityHandler) DeleteChart(namespace, chartName string) error {
return err
}
// GetChartVersion returns the summary of the specified chart version.
func (uh *UtilityHandler) GetChartVersion(namespace, name, version string) (*helm_repo.ChartVersion, error) {
if len(namespace) == 0 || len(name) == 0 || len(version) == 0 {
return nil, errors.New("bad arguments to get chart version summary")
}
path := fmt.Sprintf("/api/%s/charts/%s/%s", namespace, name, version)
url := fmt.Sprintf("%s%s", uh.backendServerAddress.String(), path)
content, err := uh.apiClient.GetContent(url)
if err != nil {
return nil, err
}
chartVersion := &helm_repo.ChartVersion{}
if err := yaml.Unmarshal(content, chartVersion); err != nil {
return nil, err
}
return chartVersion, nil
}
// deleteChartVersion deletes the specified chart version
func (uh *UtilityHandler) deleteChartVersion(namespace, chartName, version string) error {
path := fmt.Sprintf("/api/%s/charts/%s/%s", namespace, chartName, version)

View File

@ -80,6 +80,82 @@ func TestDeleteChart(t *testing.T) {
}
}
// Test the GetChartVersion in utility handler
func TestGetChartVersionSummary(t *testing.T) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/api/repo1/charts/harbor/0.2.0":
if r.Method == http.MethodGet {
w.Write([]byte(chartVersionOfHarbor020))
return
}
}
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte("not supported"))
}))
defer mockServer.Close()
serverURL, err := url.Parse(mockServer.URL)
if err != nil {
t.Fatal(err)
}
theController, err := NewController(serverURL)
if err != nil {
t.Fatal(err)
}
chartV, err := theController.GetUtilityHandler().GetChartVersion("repo1", "harbor", "0.2.0")
if err != nil {
t.Fatal(err)
}
if chartV.GetName() != "harbor" {
t.Fatalf("expect chart name 'harbor' but got '%s'", chartV.GetName())
}
if chartV.GetVersion() != "0.2.0" {
t.Fatalf("expect chart version '0.2.0' but got '%s'", chartV.GetVersion())
}
}
var chartVersionOfHarbor020 = `
{
"name": "harbor",
"home": "https://github.com/vmware/harbor",
"sources": [
"https://github.com/vmware/harbor/tree/master/contrib/helm/harbor"
],
"version": "0.2.0",
"description": "An Enterprise-class Docker Registry by VMware",
"keywords": [
"vmware",
"docker",
"registry",
"harbor"
],
"maintainers": [
{
"name": "Jesse Hu",
"email": "huh@vmware.com"
},
{
"name": "paulczar",
"email": "username.taken@gmail.com"
}
],
"engine": "gotpl",
"icon": "https://raw.githubusercontent.com/vmware/harbor/master/docs/img/harbor_logo.png",
"appVersion": "1.5.0",
"urls": [
"charts/harbor-0.2.0.tgz"
],
"created": "2018-08-29T10:26:21.141611102Z",
"digest": "fc8aae8dade9f0dfca12e9f1085081c49843d30a063a3fa7eb42497e3ceb277c"
}
`
var chartVersionsOfHarbor = `
[
{

View File

@ -37,6 +37,7 @@ const (
ResourceTypeProject = "p"
ResourceTypeRepository = "r"
ResourceTypeImage = "i"
ResourceTypeChart = "c"
ExtEndpoint = "ext_endpoint"
AUTHMode = "auth_mode"

View File

@ -23,6 +23,7 @@ import (
"github.com/goharbor/harbor/src/ui/config"
"github.com/goharbor/harbor/src/ui/filter"
"github.com/goharbor/harbor/src/ui/promgr"
"github.com/goharbor/harbor/src/ui/utils"
)
// BaseController ...
@ -60,6 +61,43 @@ func (b *BaseController) Prepare() {
b.ProjectMgr = pm
}
// RenderFormatedError renders errors with well formted style `{"error": "This is an error"}`
func (b *BaseController) RenderFormatedError(code int, err error) {
formatedErr := utils.WrapError(err)
log.Errorf("%s %s failed with error: %s", b.Ctx.Request.Method, b.Ctx.Request.URL.String(), formatedErr.Error())
b.RenderError(code, formatedErr.Error())
}
// SendUnAuthorizedError sends unauthorized error to the client.
func (b *BaseController) SendUnAuthorizedError(err error) {
b.RenderFormatedError(http.StatusUnauthorized, err)
}
// SendConflictError sends conflict error to the client.
func (b *BaseController) SendConflictError(err error) {
b.RenderFormatedError(http.StatusConflict, err)
}
// SendNotFoundError sends not found error to the client.
func (b *BaseController) SendNotFoundError(err error) {
b.RenderFormatedError(http.StatusNotFound, err)
}
// SendBadRequestError sends bad request error to the client.
func (b *BaseController) SendBadRequestError(err error) {
b.RenderFormatedError(http.StatusBadRequest, err)
}
// SendInternalServerError sends internal server error to the client.
func (b *BaseController) SendInternalServerError(err error) {
b.RenderFormatedError(http.StatusInternalServerError, err)
}
// SendForbiddenError sends forbidden error to the client.
func (b *BaseController) SendForbiddenError(err error) {
b.RenderFormatedError(http.StatusForbidden, err)
}
// Init related objects/configurations for the API controllers
func Init() error {
// If chart repository is not enabled then directly return

103
src/ui/api/chart_label.go Normal file
View File

@ -0,0 +1,103 @@
package api
import (
"errors"
"fmt"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/models"
)
const (
versionParam = ":version"
idParam = ":id"
)
// ChartLabelAPI handles the requests of marking/removing lables to/from charts.
type ChartLabelAPI struct {
LabelResourceAPI
project *models.Project
chartFullName string
}
// Prepare required material for follow-up actions.
func (cla *ChartLabelAPI) Prepare() {
// Super
cla.LabelResourceAPI.Prepare()
// Check authorization
if !cla.SecurityCtx.IsAuthenticated() {
cla.SendUnAuthorizedError(errors.New("UnAuthorized"))
return
}
project := cla.GetStringFromPath(namespaceParam)
// Project should be a valid existing one
existingProject, err := cla.ProjectMgr.Get(project)
if err != nil {
cla.SendInternalServerError(err)
return
}
if existingProject == nil {
cla.SendNotFoundError(fmt.Errorf("project '%s' not found", project))
return
}
cla.project = existingProject
// Check permission
if !cla.checkPermissions(project) {
cla.SendForbiddenError(errors.New(cla.SecurityCtx.GetUsername()))
return
}
// Check the existence of target chart
chartName := cla.GetStringFromPath(nameParam)
version := cla.GetStringFromPath(versionParam)
if _, err = chartController.GetUtilityHandler().GetChartVersion(project, chartName, version); err != nil {
cla.SendNotFoundError(err)
return
}
cla.chartFullName = fmt.Sprintf("%s/%s:%s", project, chartName, version)
}
// MarkLabel handles the request of marking label to chart.
func (cla *ChartLabelAPI) MarkLabel() {
l := &models.Label{}
cla.DecodeJSONReq(l)
label, ok := cla.validate(l.ID, cla.project.ProjectID)
if !ok {
return
}
label2Res := &models.ResourceLabel{
LabelID: label.ID,
ResourceType: common.ResourceTypeChart,
ResourceName: cla.chartFullName,
}
cla.markLabelToResource(label2Res)
}
// RemoveLabel handles the request of removing label from chart.
func (cla *ChartLabelAPI) RemoveLabel() {
lID, err := cla.GetInt64FromPath(idParam)
if err != nil {
cla.SendInternalServerError(err)
return
}
label, ok := cla.exists(lID)
if !ok {
return
}
cla.removeLabelFromResource(common.ResourceTypeChart, cla.chartFullName, label.ID)
}
// GetLabels gets labels for the specified chart version.
func (cla *ChartLabelAPI) GetLabels() {
cla.getLabelsOfResource(common.ResourceTypeChart, cla.chartFullName)
}

View File

@ -0,0 +1,220 @@
// 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/goharbor/harbor/src/chartserver"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
resourceLabelAPIPath = "/api/chartrepo/library/charts/harbor/0.2.0/labels"
resourceLabelAPIPathWithFakeProject = "/api/chartrepo/not-exist/charts/harbor/0.2.0/labels"
resourceLabelAPIPathWithFakeChart = "/api/chartrepo/library/charts/not-exist/0.2.0/labels"
cProLibraryLabelID int64
mockChartServer *httptest.Server
oldChartController *chartserver.Controller
)
func TestToStartMockChartService(t *testing.T) {
var err error
mockChartServer, oldChartController, err = mockChartController()
if err != nil {
t.Fatalf("failed to start the mock chart service: %v", err)
}
}
func TestAddToChart(t *testing.T) {
cSysLevelLabelID, err := dao.AddLabel(&models.Label{
Name: "c_sys_level_label",
Level: common.LabelLevelSystem,
})
require.Nil(t, err)
defer dao.DeleteLabel(cSysLevelLabelID)
cProTestLabelID, err := dao.AddLabel(&models.Label{
Name: "c_pro_test_label",
Level: common.LabelLevelUser,
Scope: common.LabelScopeProject,
ProjectID: 100,
})
require.Nil(t, err)
defer dao.DeleteLabel(cProTestLabelID)
cProLibraryLabelID, err = dao.AddLabel(&models.Label{
Name: "c_pro_library_label",
Level: common.LabelLevelUser,
Scope: common.LabelScopeProject,
ProjectID: 1,
})
require.Nil(t, err)
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
url: resourceLabelAPIPath,
method: http.MethodPost,
},
code: http.StatusUnauthorized,
},
// 403
{
request: &testingRequest{
url: resourceLabelAPIPath,
method: http.MethodPost,
credential: projGuest,
},
code: http.StatusForbidden,
},
// 500 project doesn't exist
{
request: &testingRequest{
url: resourceLabelAPIPathWithFakeProject,
method: http.MethodPost,
credential: projDeveloper,
},
code: http.StatusNotFound,
},
// 404 chart doesn't exist
{
request: &testingRequest{
url: resourceLabelAPIPathWithFakeChart,
method: http.MethodPost,
credential: projDeveloper,
},
code: http.StatusNotFound,
},
// 400
{
request: &testingRequest{
url: resourceLabelAPIPath,
method: http.MethodPost,
credential: projDeveloper,
},
code: http.StatusBadRequest,
},
// 404 label doesn't exist
{
request: &testingRequest{
url: resourceLabelAPIPath,
method: http.MethodPost,
credential: projDeveloper,
bodyJSON: struct {
ID int64
}{
ID: 1000,
},
},
code: http.StatusNotFound,
},
// 400 system level label
{
request: &testingRequest{
url: resourceLabelAPIPath,
method: http.MethodPost,
credential: projDeveloper,
bodyJSON: struct {
ID int64
}{
ID: cSysLevelLabelID,
},
},
code: http.StatusBadRequest,
},
// 400 try to add the label of project1 to the image under project2
{
request: &testingRequest{
url: resourceLabelAPIPath,
method: http.MethodPost,
credential: projDeveloper,
bodyJSON: struct {
ID int64
}{
ID: cProTestLabelID,
},
},
code: http.StatusBadRequest,
},
// 200
{
request: &testingRequest{
url: resourceLabelAPIPath,
method: http.MethodPost,
credential: projDeveloper,
bodyJSON: struct {
ID int64
}{
ID: cProLibraryLabelID,
},
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}
func TestGetOfChart(t *testing.T) {
labels := []*models.Label{}
err := handleAndParse(&testingRequest{
url: resourceLabelAPIPath,
method: http.MethodGet,
credential: projDeveloper,
}, &labels)
require.Nil(t, err)
require.Equal(t, 1, len(labels))
assert.Equal(t, cProLibraryLabelID, labels[0].ID)
}
func TestRemoveFromChart(t *testing.T) {
runCodeCheckingCases(t, &codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/%d", resourceLabelAPIPath, cProLibraryLabelID),
method: http.MethodDelete,
credential: projDeveloper,
},
code: http.StatusOK,
})
labels := []*models.Label{}
err := handleAndParse(&testingRequest{
url: resourceLabelAPIPath,
method: http.MethodGet,
credential: projDeveloper,
}, &labels)
require.Nil(t, err)
require.Equal(t, 0, len(labels))
}
func TestToStopMockChartService(t *testing.T) {
if mockChartServer != nil {
mockChartServer.Close()
}
if oldChartController != nil {
chartController = oldChartController
}
dao.DeleteLabel(cProLibraryLabelID)
}

View File

@ -170,6 +170,11 @@ func init() {
beego.Router("/api/system/gc/:id", &GCAPI{}, "get:GetGC")
beego.Router("/api/system/gc/:id([0-9]+)/log", &GCAPI{}, "get:GetLog")
beego.Router("/api/system/gc/schedule", &GCAPI{}, "get:Get;put:Put;post:Post")
// Labels for chart
chartLabelAPIType := &ChartLabelAPI{}
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels", chartLabelAPIType, "get:GetLabels;post:MarkLabel")
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels/:id([0-9]+)", chartLabelAPIType, "delete:RemoveLabel")
_ = updateInitPassword(1, "Harbor12345")
if err := core.Init(); err != nil {

View File

@ -0,0 +1,104 @@
package api
import (
"net/http"
"strconv"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/ui/label"
)
// LabelResourceAPI provides the related basic functions to handle marking labels to resources
type LabelResourceAPI struct {
BaseController
labelManager label.Manager
}
// Prepare resources for follow-up actions.
func (lra *LabelResourceAPI) Prepare() {
lra.BaseController.Prepare()
// Create label manager
lra.labelManager = &label.BaseManager{}
}
func (lra *LabelResourceAPI) checkPermissions(project string) bool {
if lra.Ctx.Request.Method == http.MethodPost ||
lra.Ctx.Request.Method == http.MethodDelete {
if lra.SecurityCtx.HasWritePerm(project) {
return true
}
}
if lra.Ctx.Request.Method == http.MethodGet {
if lra.SecurityCtx.HasReadPerm(project) {
return true
}
}
return false
}
func (lra *LabelResourceAPI) getLabelsOfResource(rType string, rIDOrName interface{}) {
labels, err := lra.labelManager.GetLabelsOfResource(rType, rIDOrName)
if err != nil {
lra.handleErrors(err)
return
}
lra.Data["json"] = labels
lra.ServeJSON()
}
func (lra *LabelResourceAPI) markLabelToResource(rl *models.ResourceLabel) {
labelID, err := lra.labelManager.MarkLabelToResource(rl)
if err != nil {
lra.handleErrors(err)
return
}
// return the ID of label and return status code 200 rather than 201 as the label is not created
lra.Redirect(http.StatusOK, strconv.FormatInt(labelID, 10))
}
func (lra *LabelResourceAPI) removeLabelFromResource(rType string, rIDOrName interface{}, labelID int64) {
if err := lra.labelManager.RemoveLabelFromResource(rType, rIDOrName, labelID); err != nil {
lra.handleErrors(err)
return
}
}
// eat the error of validate method of label manager
func (lra *LabelResourceAPI) validate(labelID, projectID int64) (*models.Label, bool) {
label, err := lra.labelManager.Validate(labelID, projectID)
if err != nil {
lra.handleErrors(err)
return nil, false
}
return label, true
}
// eat the error of exists method of label manager
func (lra *LabelResourceAPI) exists(labelID int64) (*models.Label, bool) {
label, err := lra.labelManager.Exists(labelID)
if err != nil {
return nil, false
}
return label, true
}
// Handle different kinds of errors.
func (lra *LabelResourceAPI) handleErrors(err error) {
switch err.(type) {
case *label.ErrLabelBadRequest:
lra.SendBadRequestError(err)
case *label.ErrLabelConflict:
lra.SendConflictError(err)
case *label.ErrLabelNotFound:
lra.SendNotFoundError(err)
default:
lra.SendInternalServerError(err)
}
}

View File

@ -419,6 +419,11 @@ func mockChartController() (*httptest.Server, *chartserver.Controller, error) {
w.Write([]byte("{}"))
return
}
case "/api/library/charts/harbor/0.2.0":
if r.Method == http.MethodGet {
w.Write([]byte(chartVersionOfHarbor020))
return
}
}
w.WriteHeader(http.StatusNotImplemented)
@ -442,3 +447,39 @@ func mockChartController() (*httptest.Server, *chartserver.Controller, error) {
return mockServer, oldController, nil
}
var chartVersionOfHarbor020 = `
{
"name": "harbor",
"home": "https://github.com/vmware/harbor",
"sources": [
"https://github.com/vmware/harbor/tree/master/contrib/helm/harbor"
],
"version": "0.2.0",
"description": "An Enterprise-class Docker Registry by VMware",
"keywords": [
"vmware",
"docker",
"registry",
"harbor"
],
"maintainers": [
{
"name": "Jesse Hu",
"email": "huh@vmware.com"
},
{
"name": "paulczar",
"email": "username.taken@gmail.com"
}
],
"engine": "gotpl",
"icon": "https://raw.githubusercontent.com/vmware/harbor/master/docs/img/harbor_logo.png",
"appVersion": "1.5.0",
"urls": [
"charts/harbor-0.2.0.tgz"
],
"created": "2018-08-29T10:26:21.141611102Z",
"digest": "fc8aae8dade9f0dfca12e9f1085081c49843d30a063a3fa7eb42497e3ceb277c"
}
`

View File

@ -15,9 +15,9 @@
package api
import (
"errors"
"fmt"
"net/http"
"strconv"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
@ -28,7 +28,7 @@ import (
// RepositoryLabelAPI handles requests for adding/removing label to/from repositories and images
type RepositoryLabelAPI struct {
BaseController
LabelResourceAPI
repository *models.RepoRecord
tag string
label *models.Label
@ -36,27 +36,29 @@ type RepositoryLabelAPI struct {
// Prepare ...
func (r *RepositoryLabelAPI) Prepare() {
r.BaseController.Prepare()
// Super
r.LabelResourceAPI.Prepare()
if !r.SecurityCtx.IsAuthenticated() {
r.HandleUnauthorized()
r.SendUnAuthorizedError(errors.New("UnAuthorized"))
return
}
repository := r.GetString(":splat")
project, _ := utils.ParseRepository(repository)
if !r.SecurityCtx.HasWritePerm(project) {
r.HandleForbidden(r.SecurityCtx.GetUsername())
if !r.checkPermissions(project) {
r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
return
}
repo, err := dao.GetRepositoryByName(repository)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get repository %s: %v",
repository, err))
r.SendInternalServerError(fmt.Errorf("failed to get repository %s: %v", repository, err))
return
}
if repo == nil {
r.HandleNotFound(fmt.Sprintf("repository %s not found", repository))
r.SendNotFoundError(fmt.Errorf("repository %s not found", repository))
return
}
r.repository = repo
@ -65,49 +67,30 @@ func (r *RepositoryLabelAPI) Prepare() {
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))
r.SendInternalServerError(fmt.Errorf("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))
r.SendNotFoundError(fmt.Errorf("image %s:%s not found", repository, tag))
return
}
r.tag = tag
}
if r.Ctx.Request.Method == http.MethodPost {
p, err := r.ProjectMgr.Get(project)
if err != nil {
r.SendInternalServerError(err)
return
}
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))
label, ok := r.validate(l.ID, p.ProjectID)
if !ok {
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
@ -116,27 +99,22 @@ func (r *RepositoryLabelAPI) Prepare() {
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))
r.SendInternalServerError(fmt.Errorf("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))
label, ok := r.exists(labelID)
if !ok {
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))
r.getLabelsOfResource(common.ResourceTypeImage, fmt.Sprintf("%s:%s", r.repository.Name, r.tag))
}
// AddToImage adds the label to an image
@ -146,18 +124,18 @@ func (r *RepositoryLabelAPI) AddToImage() {
ResourceType: common.ResourceTypeImage,
ResourceName: fmt.Sprintf("%s:%s", r.repository.Name, r.tag),
}
r.addLabel(rl)
r.markLabelToResource(rl)
}
// RemoveFromImage removes the label from an image
func (r *RepositoryLabelAPI) RemoveFromImage() {
r.removeLabel(common.ResourceTypeImage,
r.removeLabelFromResource(common.ResourceTypeImage,
fmt.Sprintf("%s:%s", r.repository.Name, r.tag), r.label.ID)
}
// GetOfRepository returns labels of a repository
func (r *RepositoryLabelAPI) GetOfRepository() {
r.getLabels(common.ResourceTypeRepository, r.repository.RepositoryID)
r.getLabelsOfResource(common.ResourceTypeRepository, r.repository.RepositoryID)
}
// AddToRepository adds the label to a repository
@ -167,71 +145,12 @@ func (r *RepositoryLabelAPI) AddToRepository() {
ResourceType: common.ResourceTypeRepository,
ResourceID: r.repository.RepositoryID,
}
r.addLabel(rl)
r.markLabelToResource(rl)
}
// RemoveFromRepository removes the label from a repository
func (r *RepositoryLabelAPI) RemoveFromRepository() {
r.removeLabel(common.ResourceTypeRepository, r.repository.RepositoryID, r.label.ID)
}
func (r *RepositoryLabelAPI) getLabels(rType string, rIDOrName interface{}) {
labels, err := dao.GetLabelsOfResource(rType, rIDOrName)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get labels of resource %s %v: %v",
rType, rIDOrName, err))
return
}
r.Data["json"] = labels
r.ServeJSON()
}
func (r *RepositoryLabelAPI) addLabel(rl *models.ResourceLabel) {
var rIDOrName interface{}
if rl.ResourceID != 0 {
rIDOrName = rl.ResourceID
} else {
rIDOrName = rl.ResourceName
}
rlabel, err := dao.GetResourceLabel(rl.ResourceType, rIDOrName, rl.LabelID)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to check the existence of label %d for resource %s %v: %v",
rl.LabelID, rl.ResourceType, rIDOrName, 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 %v: %v",
rl.LabelID, rl.ResourceType, rIDOrName, 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(rType string, rIDOrName interface{}, labelID int64) {
rl, err := dao.GetResourceLabel(rType, rIDOrName, labelID)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to check the existence of label %d for resource %s %v: %v",
labelID, rType, rIDOrName, err))
return
}
if rl == nil {
r.HandleNotFound(fmt.Sprintf("label %d of resource %s %s not found",
labelID, rType, rIDOrName))
return
}
if err = dao.DeleteResourceLabel(rl.ID); err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to delete resource label record %d: %v",
rl.ID, err))
return
}
r.removeLabelFromResource(common.ResourceTypeRepository, r.repository.RepositoryID, r.label.ID)
}
func imageExist(username, repository, tag string) (bool, error) {

View File

@ -250,4 +250,6 @@ func TestRemoveFromRepository(t *testing.T) {
}, &labels)
require.Nil(t, err)
require.Equal(t, 0, len(labels))
dao.DeleteLabel(proLibraryLabelID)
}

76
src/ui/label/errors.go Normal file
View File

@ -0,0 +1,76 @@
package label
import (
"fmt"
)
// ErrLabelBase contains the basic required info for building the final errors.
type ErrLabelBase struct {
LabelID int64
ResourceType string
ResourceIDOrName interface{}
}
// ErrLabelNotFound defines the error of not found label on the resource
// or the specified label is not found.
type ErrLabelNotFound struct {
ErrLabelBase
}
// ErrLabelConflict defines the error of label conflicts on the resource.
type ErrLabelConflict struct {
ErrLabelBase
}
// ErrLabelBadRequest defines the error of bad request to the resource.
type ErrLabelBadRequest struct {
Message string
}
// NewErrLabelNotFound builds an error with ErrLabelNotFound type
func NewErrLabelNotFound(labelID int64, resourceType string, resourceIDOrName interface{}) *ErrLabelNotFound {
return &ErrLabelNotFound{
ErrLabelBase{
LabelID: labelID,
ResourceType: resourceType,
ResourceIDOrName: resourceIDOrName,
},
}
}
// Error returns the error message of ErrLabelNotFound.
func (nf *ErrLabelNotFound) Error() string {
if len(nf.ResourceType) > 0 && nf.ResourceIDOrName != nil {
return fmt.Sprintf("not found: label '%d' on %s '%v'", nf.LabelID, nf.ResourceType, nf.ResourceIDOrName)
}
return fmt.Sprintf("not found: label '%d'", nf.LabelID)
}
// NewErrLabelConflict builds an error with NewErrLabelConflict type.
func NewErrLabelConflict(labelID int64, resourceType string, resourceIDOrName interface{}) *ErrLabelConflict {
return &ErrLabelConflict{
ErrLabelBase{
LabelID: labelID,
ResourceType: resourceType,
ResourceIDOrName: resourceIDOrName,
},
}
}
// Error returns the error message of ErrLabelConflict.
func (cl *ErrLabelConflict) Error() string {
return fmt.Sprintf("conflict: %s '%v' is already marked with label '%d'", cl.ResourceType, cl.ResourceIDOrName, cl.LabelID)
}
// NewErrLabelBadRequest builds an error with ErrLabelBadRequest type.
func NewErrLabelBadRequest(message string) *ErrLabelBadRequest {
return &ErrLabelBadRequest{
Message: message,
}
}
// Error returns the error message of ErrLabelBadRequest.
func (br *ErrLabelBadRequest) Error() string {
return br.Message
}

View File

@ -0,0 +1,33 @@
package label
import (
"fmt"
"testing"
)
// Test cases for kinds of error definitions.
func TestErrorFormats(t *testing.T) {
br := NewErrLabelBadRequest("bad requests")
if !checkErrorFormat(br, "bad requests") {
t.Fatalf("expect an error with ErrLabelBadRequest kind but got incorrect format '%v'", br)
}
cf := NewErrLabelConflict(1, "c", "repo1/mychart:1.0.0")
if !checkErrorFormat(cf, fmt.Sprintf("conflict: %s '%v' is already marked with label '%d'", "c", "repo1/mychart:1.0.0", 1)) {
t.Fatalf("expect an error with ErrLabelConflict kind but got incorrect format '%v'", cf)
}
nf := NewErrLabelNotFound(1, "c", "repo1/mychart:1.0.0")
if !checkErrorFormat(nf, fmt.Sprintf("not found: label '%d' on %s '%v'", 1, "c", "repo1/mychart:1.0.0")) {
t.Fatalf("expect an error with ErrLabelNotFound kind but got incorrect format '%v'", nf)
}
nf2 := NewErrLabelNotFound(1, "", "")
if !checkErrorFormat(nf2, fmt.Sprintf("not found: label '%d'", 1)) {
t.Fatalf("expect an error with ErrLabelNotFound kind but got incorrect format %v", nf2)
}
}
func checkErrorFormat(err error, msg string) bool {
return err.Error() == msg
}

145
src/ui/label/manager.go Normal file
View File

@ -0,0 +1,145 @@
package label
import (
"errors"
"fmt"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
)
// Manager defines the related operations for label management
type Manager interface {
// Mark label to the resource.
//
// If succeed, the relationship ID will be returned.
// Otherwise, an non-nil error will be returned.
MarkLabelToResource(label *models.ResourceLabel) (int64, error)
// Remove the label from the resource.
// Resource type and ID(/name) should be provided to identify the relationship.
//
// An non-nil error will be got if meet any issues or nil error returned.
RemoveLabelFromResource(resourceType string, resourceIDOrName interface{}, labelID int64) error
// Get labels for the specified resource.
// Resource is identified by the resource type and ID(/name).
//
// If succeed, a label list is returned.
// Otherwise, a non-nil error will be returned.
GetLabelsOfResource(resourceType string, resourceIDOrName interface{}) ([]*models.Label, error)
// Check the existence of the specified label.
//
// If label existing, a non-nil label object is returned and nil error is set.
// A non-nil error will be set if any issues met while checking or label is not found.
Exists(labelID int64) (*models.Label, error)
// Validate if the scope of the input label is correct.
// If the scope is project level, the projectID is required then.
//
// If everything is ok, an validated label reference will be returned.
// Otherwise, a non-nil error is returned.
Validate(labelID int64, projectID int64) (*models.Label, error)
}
// BaseManager is the default implementation of the Manager interface.
type BaseManager struct{}
// MarkLabelToResource is the implementation of same method in Manager interface.
func (bm *BaseManager) MarkLabelToResource(label *models.ResourceLabel) (int64, error) {
if label == nil {
return -1, errors.New("nil label object")
}
// Use ID or name of resource. ID first.
var rIDOrName interface{}
if label.ResourceID != 0 {
rIDOrName = label.ResourceID
} else {
rIDOrName = label.ResourceName
}
rlabel, err := dao.GetResourceLabel(label.ResourceType, rIDOrName, label.LabelID)
if err != nil {
return -1, fmt.Errorf("failed to check the existence of label %d for resource %s %v: %v", label.LabelID, label.ResourceType, rIDOrName, err)
}
if rlabel != nil {
return -1, NewErrLabelConflict(label.LabelID, label.ResourceType, rIDOrName)
}
if _, err := dao.AddResourceLabel(label); err != nil {
return -1, fmt.Errorf("failed to add label %d to resource %s %v: %v", label.LabelID, label.ResourceType, rIDOrName, err)
}
// return the ID of label
return label.LabelID, nil
}
// RemoveLabelFromResource is the implementation of same method in Manager interface.
func (bm *BaseManager) RemoveLabelFromResource(resourceType string, resourceIDOrName interface{}, labelID int64) error {
rl, err := dao.GetResourceLabel(resourceType, resourceIDOrName, labelID)
if err != nil {
return fmt.Errorf("failed to check the existence of label %d for resource %s %v: %v", labelID, resourceType, resourceIDOrName, err)
}
if rl == nil {
return NewErrLabelNotFound(labelID, resourceType, resourceIDOrName)
}
if err = dao.DeleteResourceLabel(rl.ID); err != nil {
return fmt.Errorf("failed to delete resource label record %d: %v", rl.ID, err)
}
return nil
}
// GetLabelsOfResource is the implementation of same method in Manager interface.
func (bm *BaseManager) GetLabelsOfResource(resourceType string, resourceIDOrName interface{}) ([]*models.Label, error) {
labels, err := dao.GetLabelsOfResource(resourceType, resourceIDOrName)
if err != nil {
return nil, fmt.Errorf("failed to get labels of resource %s %v: %v", resourceType, resourceIDOrName, err)
}
return labels, nil
}
// Exists is the implementation of same method in Manager interface.
func (bm *BaseManager) Exists(labelID int64) (*models.Label, error) {
label, err := dao.GetLabel(labelID)
if err != nil {
return nil, fmt.Errorf("failed to get label %d: %v", labelID, err)
}
if label == nil {
return nil, NewErrLabelNotFound(labelID, "", nil)
}
return label, nil
}
// Validate is the implementation of same method in Manager interface.
func (bm *BaseManager) Validate(labelID int64, projectID int64) (*models.Label, error) {
label, err := dao.GetLabel(labelID)
if err != nil {
return nil, fmt.Errorf("failed to get label %d: %v", labelID, err)
}
if label == nil {
return nil, NewErrLabelNotFound(labelID, "", nil)
}
if label.Level != common.LabelLevelUser {
return nil, NewErrLabelBadRequest("only user level labels can be used")
}
if label.Scope == common.LabelScopeProject {
if projectID != label.ProjectID {
return nil, NewErrLabelBadRequest("can not add labels which don't belong to the project to the resources under the project")
}
}
return label, nil
}

View File

@ -148,6 +148,11 @@ func initRouters() {
beego.Router("/chartrepo/:repo/index.yaml", chartRepositoryAPIType, "get:GetIndexByRepo")
beego.Router("/chartrepo/index.yaml", chartRepositoryAPIType, "get:GetIndex")
beego.Router("/chartrepo/:repo/charts/:filename", chartRepositoryAPIType, "get:DownloadChart")
// Labels for chart
chartLabelAPIType := &api.ChartLabelAPI{}
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels", chartLabelAPIType, "get:GetLabels;post:MarkLabel")
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels/:id([0-9]+)", chartLabelAPIType, "delete:RemoveLabel")
}
// Error pages

27
src/ui/utils/error.go Normal file
View File

@ -0,0 +1,27 @@
package utils
import (
"encoding/json"
"errors"
)
// WrapErrorMessage wraps the error msg to the well formated error message `{ "error": "The error message" }`
func WrapErrorMessage(msg string) string {
errBody := make(map[string]string, 1)
errBody["error"] = msg
data, err := json.Marshal(&errBody)
if err != nil {
return msg
}
return string(data)
}
// WrapError wraps the error to the well formated error `{ "error": "The error message" }`
func WrapError(err error) error {
if err == nil {
return nil
}
return errors.New(WrapErrorMessage(err.Error()))
}

View File

@ -0,0 +1,33 @@
package utils
import (
"encoding/json"
"errors"
"testing"
)
// Test case for error wrapping function.
func TestWrapError(t *testing.T) {
if WrapError(nil) != nil {
t.Fatal("expect nil error but got a non-nil one")
}
err := errors.New("mock error")
formatedErr := WrapError(err)
if formatedErr == nil {
t.Fatal("expect non-nil error but got nil")
}
jsonErr := formatedErr.Error()
structuredErr := make(map[string]string, 1)
if e := json.Unmarshal([]byte(jsonErr), &structuredErr); e != nil {
t.Fatal("expect nil error but got a non-nil one when doing error converting")
}
if msg, ok := structuredErr["error"]; !ok {
t.Fatal("expect an 'error' filed but missing")
} else {
if msg != "mock error" {
t.Fatalf("expect error message '%s' but got '%s'", "mock error", msg)
}
}
}