diff --git a/src/chartserver/utility_handler.go b/src/chartserver/utility_handler.go index 1215a04d1..e9aa9ccb6 100644 --- a/src/chartserver/utility_handler.go +++ b/src/chartserver/utility_handler.go @@ -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) diff --git a/src/chartserver/utility_handler_test.go b/src/chartserver/utility_handler_test.go index d1bb2d1a7..4562800ac 100644 --- a/src/chartserver/utility_handler_test.go +++ b/src/chartserver/utility_handler_test.go @@ -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 = ` [ { diff --git a/src/common/const.go b/src/common/const.go index 159d7bf4f..c0f42420d 100644 --- a/src/common/const.go +++ b/src/common/const.go @@ -37,6 +37,7 @@ const ( ResourceTypeProject = "p" ResourceTypeRepository = "r" ResourceTypeImage = "i" + ResourceTypeChart = "c" ExtEndpoint = "ext_endpoint" AUTHMode = "auth_mode" diff --git a/src/ui/api/base.go b/src/ui/api/base.go index 3e607f3e9..2abe521a9 100644 --- a/src/ui/api/base.go +++ b/src/ui/api/base.go @@ -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 diff --git a/src/ui/api/chart_label.go b/src/ui/api/chart_label.go new file mode 100644 index 000000000..a08bbd567 --- /dev/null +++ b/src/ui/api/chart_label.go @@ -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) +} diff --git a/src/ui/api/chart_label_test.go b/src/ui/api/chart_label_test.go new file mode 100644 index 000000000..671615714 --- /dev/null +++ b/src/ui/api/chart_label_test.go @@ -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) +} diff --git a/src/ui/api/harborapi_test.go b/src/ui/api/harborapi_test.go index 28747dd05..5cc12a567 100644 --- a/src/ui/api/harborapi_test.go +++ b/src/ui/api/harborapi_test.go @@ -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 { diff --git a/src/ui/api/label_resource.go b/src/ui/api/label_resource.go new file mode 100644 index 000000000..ba590a3aa --- /dev/null +++ b/src/ui/api/label_resource.go @@ -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) + } +} diff --git a/src/ui/api/project_test.go b/src/ui/api/project_test.go index 32355eab1..8312fe9e7 100644 --- a/src/ui/api/project_test.go +++ b/src/ui/api/project_test.go @@ -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" +} +` diff --git a/src/ui/api/repository_label.go b/src/ui/api/repository_label.go index fd2510c37..6a241a255 100644 --- a/src/ui/api/repository_label.go +++ b/src/ui/api/repository_label.go @@ -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) { diff --git a/src/ui/api/repository_label_test.go b/src/ui/api/repository_label_test.go index 18bab78be..e40286ccb 100644 --- a/src/ui/api/repository_label_test.go +++ b/src/ui/api/repository_label_test.go @@ -250,4 +250,6 @@ func TestRemoveFromRepository(t *testing.T) { }, &labels) require.Nil(t, err) require.Equal(t, 0, len(labels)) + + dao.DeleteLabel(proLibraryLabelID) } diff --git a/src/ui/label/errors.go b/src/ui/label/errors.go new file mode 100644 index 000000000..3d2e67a27 --- /dev/null +++ b/src/ui/label/errors.go @@ -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 +} diff --git a/src/ui/label/errors_test.go b/src/ui/label/errors_test.go new file mode 100644 index 000000000..3ecda7d67 --- /dev/null +++ b/src/ui/label/errors_test.go @@ -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 +} diff --git a/src/ui/label/manager.go b/src/ui/label/manager.go new file mode 100644 index 000000000..7ac5a4d3c --- /dev/null +++ b/src/ui/label/manager.go @@ -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 +} diff --git a/src/ui/router.go b/src/ui/router.go index a2277d18c..3d84e4d13 100644 --- a/src/ui/router.go +++ b/src/ui/router.go @@ -142,6 +142,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 diff --git a/src/ui/utils/error.go b/src/ui/utils/error.go new file mode 100644 index 000000000..62121f6b3 --- /dev/null +++ b/src/ui/utils/error.go @@ -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())) +} diff --git a/src/ui/utils/error_test.go b/src/ui/utils/error_test.go new file mode 100644 index 000000000..8e5c7d673 --- /dev/null +++ b/src/ui/utils/error_test.go @@ -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) + } + } +}