From 3a44f76b947e9c7ff52b0917d267d678c34af434 Mon Sep 17 00:00:00 2001 From: Steven Zou Date: Thu, 12 Jul 2018 13:43:49 +0800 Subject: [PATCH] Add API routes of chart repo services in harbor API router add chart repo API controller update GetChartVersion to extract repo name from the request update the corresponding UI cases update the chart controller initialization process --- src/chartserver/base_test.go | 1 + src/chartserver/manipulation_handler.go | 24 ++- src/ui/api/chart_repository.go | 260 ++++++++++++++++++++++++ src/ui/router.go | 15 ++ 4 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 src/ui/api/chart_repository.go diff --git a/src/chartserver/base_test.go b/src/chartserver/base_test.go index df57c7a41..a4fe65cd2 100644 --- a/src/chartserver/base_test.go +++ b/src/chartserver/base_test.go @@ -113,6 +113,7 @@ var frontServer = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.Respo var getChartV = regexp.MustCompile(`^/api/\w+/charts/.+/.+$`) if r.Method == http.MethodGet && getChartV.MatchString(r.RequestURI) { + *r = *(r.WithContext(context.WithValue(r.Context(), NamespaceContextKey, "repo1"))) mockController.GetManipulationHandler().GetChartVersion(w, r) return } diff --git a/src/chartserver/manipulation_handler.go b/src/chartserver/manipulation_handler.go index 89a6c1e35..e246d4f6c 100644 --- a/src/chartserver/manipulation_handler.go +++ b/src/chartserver/manipulation_handler.go @@ -2,6 +2,7 @@ package chartserver import ( "encoding/json" + "errors" "fmt" "net/http" "net/url" @@ -12,6 +13,14 @@ import ( helm_repo "k8s.io/helm/pkg/repo" ) +const ( + //NamespaceContextKey is context key for the namespace + NamespaceContextKey ContextKey = ":repo" +) + +//ContextKey is defined for add value in the context of http request +type ContextKey string + //ManipulationHandler includes all the handler methods for the purpose of manipulating the //chart repository type ManipulationHandler struct { @@ -77,9 +86,20 @@ func (mh *ManipulationHandler) GetChartVersion(w http.ResponseWriter, req *http. chartDetails := mh.chartCache.GetChart(chartV.Digest) if chartDetails == nil { //NOT hit!! + var namespace string + + repoValue := req.Context().Value(NamespaceContextKey) + if repoValue != nil { + if ns, ok := repoValue.(string); ok { + namespace = ns + } + } + + if len(strings.TrimSpace(namespace)) == 0 { + writeInternalError(w, errors.New("failed to extract namespace from the request")) + return + } - //TODO: - namespace := "repo1" content, err := mh.getChartVersionContent(namespace, chartV.URLs[0]) if err != nil { writeInternalError(w, err) diff --git a/src/ui/api/chart_repository.go b/src/ui/api/chart_repository.go new file mode 100644 index 000000000..1d8d89634 --- /dev/null +++ b/src/ui/api/chart_repository.go @@ -0,0 +1,260 @@ +package api + +import ( + "context" + "fmt" + "net/http" + "net/url" + "os" + "strings" + + "github.com/vmware/harbor/src/chartserver" + hlog "github.com/vmware/harbor/src/common/utils/log" +) + +const ( + backendChartServerAddr = "BACKEND_CHART_SERVER" + namespaceParam = ":repo" + + accessLevelPublic = iota + accessLevelRead + accessLevelWrite + accessLevelAll + accessLevelSystem +) + +//chartController is a singleton instance +var chartController = initializeChartController() + +//ChartRepositoryAPI provides related API handlers for the chart repository APIs +type ChartRepositoryAPI struct { + //The base controller to provide common utilities + BaseController + + //Keep the namespace if existing + namespace string +} + +//Prepare something for the following actions +func (cra *ChartRepositoryAPI) Prepare() { + //Call super prepare method + cra.BaseController.Prepare() + + //Try to extract namespace for parameter of path + //It may not exist + cra.namespace = strings.TrimSpace(cra.GetStringFromPath(namespaceParam)) + + //Check the existence of namespace + //Exclude the following URI + // -/index.yaml + // -/api/chartserver/health + incomingURI := cra.Ctx.Request.RequestURI + if incomingURI != "/index.yaml" && incomingURI != "/api/chartserver/health" { + if !cra.requireNamespace(cra.namespace) { + return + } + } +} + +//GetHealthStatus handles GET /api/chartserver/health +func (cra *ChartRepositoryAPI) GetHealthStatus() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelSystem) { + return + } + + //Override the request path to '/health' + req := cra.Ctx.Request + req.URL.Path = "/health" + + chartController.GetBaseHandler().GetHealthStatus(cra.Ctx.ResponseWriter, req) +} + +//GetIndexByRepo handles GET /:repo/index.yaml +func (cra *ChartRepositoryAPI) GetIndexByRepo() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelRead) { + return + } + + chartController.GetRepositoryHandler().GetIndexFileWithNS(cra.Ctx.ResponseWriter, cra.Ctx.Request) +} + +//GetIndex handles GET /index.yaml +func (cra *ChartRepositoryAPI) GetIndex() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelSystem) { + return + } + + chartController.GetRepositoryHandler().GetIndexFile(cra.Ctx.ResponseWriter, cra.Ctx.Request) +} + +//DownloadChart handles GET /:repo/charts/:filename +func (cra *ChartRepositoryAPI) DownloadChart() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelRead) { + return + } + + chartController.GetRepositoryHandler().DownloadChartObject(cra.Ctx.ResponseWriter, cra.Ctx.Request) +} + +//ListCharts handles GET /api/:repo/charts +func (cra *ChartRepositoryAPI) ListCharts() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelRead) { + return + } + + chartController.GetManipulationHandler().ListCharts(cra.Ctx.ResponseWriter, cra.Ctx.Request) +} + +//ListChartVersions GET /api/:repo/charts/:name +func (cra *ChartRepositoryAPI) ListChartVersions() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelRead) { + return + } + + chartController.GetManipulationHandler().GetChart(cra.Ctx.ResponseWriter, cra.Ctx.Request) +} + +//GetChartVersion handles GET /api/:repo/charts/:name/:version +func (cra *ChartRepositoryAPI) GetChartVersion() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelRead) { + return + } + + //Let's pass the namespace via the context of request + req := cra.Ctx.Request + *req = *(req.WithContext(context.WithValue(req.Context(), chartserver.NamespaceContextKey, cra.namespace))) + + chartController.GetManipulationHandler().GetChartVersion(cra.Ctx.ResponseWriter, req) +} + +//DeleteChartVersion handles DELETE /api/:repo/charts/:name/:version +func (cra *ChartRepositoryAPI) DeleteChartVersion() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelAll) { + return + } + + chartController.GetManipulationHandler().DeleteChartVersion(cra.Ctx.ResponseWriter, cra.Ctx.Request) +} + +//UploadChartVersion handles POST /api/:repo/charts +func (cra *ChartRepositoryAPI) UploadChartVersion() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelWrite) { + return + } + + chartController.GetManipulationHandler().UploadChartVersion(cra.Ctx.ResponseWriter, cra.Ctx.Request) +} + +//UploadChartProvFile handles POST /api/:repo/prov +func (cra *ChartRepositoryAPI) UploadChartProvFile() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelWrite) { + return + } + + chartController.GetManipulationHandler().UploadProvenanceFile(cra.Ctx.ResponseWriter, cra.Ctx.Request) +} + +//Check if there exists a valid namespace +//Return true if it does +//Return false if it does not +func (cra *ChartRepositoryAPI) requireNamespace(namespace string) bool { + //Actually, never should be like this + if len(namespace) == 0 { + cra.HandleBadRequest(":repo should be in the request URL") + return false + } + + existsing, err := cra.ProjectMgr.Exists(namespace) + if err != nil { + //Check failed with error + cra.RenderError(http.StatusInternalServerError, fmt.Sprintf("failed to check existence of namespace %s with error: %s", namespace, err.Error())) + return false + } + + //Not existing + if !existsing { + cra.HandleBadRequest(fmt.Sprintf("namespace %s is not existing", namespace)) + return false + } + + return true +} + +//Check if the related access match the expected requirement +//If with right access, return true +//If without right access, return false +func (cra *ChartRepositoryAPI) requireAccess(namespace string, accessLevel uint) bool { + if accessLevel == accessLevelPublic { + return true //do nothing + } + + //At least, authentication is necessary when level > public + if !cra.SecurityCtx.IsAuthenticated() { + cra.HandleUnauthorized() + return false + } + + theLevel := accessLevel + //If repo is empty, system admin role must be required + if len(namespace) == 0 { + theLevel = accessLevelSystem + } + + switch theLevel { + //Should be system admin role + case accessLevelSystem: + if !cra.SecurityCtx.IsSysAdmin() { + cra.RenderError(http.StatusForbidden, fmt.Sprintf("system admin role is required but user '%s' is not", cra.SecurityCtx.GetUsername())) + return false + } + case accessLevelAll: + if !cra.SecurityCtx.HasAllPerm(namespace) { + cra.RenderError(http.StatusForbidden, fmt.Sprintf("project admin role is required but user '%s' does not have", cra.SecurityCtx.GetUsername())) + return false + } + case accessLevelWrite: + if !cra.SecurityCtx.HasWritePerm(namespace) { + cra.RenderError(http.StatusForbidden, fmt.Sprintf("developer role is required but user '%s' does not have", cra.SecurityCtx.GetUsername())) + return false + } + case accessLevelRead: + if !cra.SecurityCtx.HasReadPerm(namespace) { + cra.RenderError(http.StatusForbidden, fmt.Sprintf("at least a guest role is required for user '%s'", cra.SecurityCtx.GetUsername())) + return false + } + default: + //access rejected for invalid scope + cra.RenderError(http.StatusForbidden, "unrecognized access scope") + return false + } + + return true +} + +//Initialize the chart service controller +func initializeChartController() *chartserver.Controller { + addr := os.Getenv(backendChartServerAddr) + url, err := url.Parse(addr) + if err != nil { + hlog.Fatal("chart storage server is not correctly configured") + } + + controller, err := chartserver.NewController(url) + if err != nil { + hlog.Fatal("failed to initialize chart API controller") + } + + hlog.Info("API controller for chart repository server is successfully initialized") + + return controller +} diff --git a/src/ui/router.go b/src/ui/router.go index a080a659a..4445273b8 100644 --- a/src/ui/router.go +++ b/src/ui/router.go @@ -122,6 +122,21 @@ func initRouters() { beego.Router("/registryproxy/*", &controllers.RegistryProxy{}, "*:Handle") + //APIs for chart repository + chartRepositoryAPIType := &api.ChartRepositoryAPI{} + beego.Router("/api/chartserver/health", chartRepositoryAPIType, "get:GetHealthStatus") + beego.Router("/api/:repo/charts", chartRepositoryAPIType, "get:ListCharts") + beego.Router("/api/:repo/charts/:name", chartRepositoryAPIType, "get:ListChartVersions") + beego.Router("/api/:repo/charts/:name/:version", chartRepositoryAPIType, "get:GetChartVersion") + beego.Router("/api/:repo/charts/:name/:version", chartRepositoryAPIType, "delete:DeleteChartVersion") + beego.Router("/api/:repo/charts", chartRepositoryAPIType, "post:UploadChartVersion") + beego.Router("/api/:repo/prov", chartRepositoryAPIType, "post:UploadChartProvFile") + + //Repository services + beego.Router("/:repo/index.yaml", chartRepositoryAPIType, "get:GetIndexByRepo") + beego.Router("/index.yaml", chartRepositoryAPIType, "get:GetIndex") + beego.Router("/:repo/charts/:filename", chartRepositoryAPIType, "get:DownloadChart") + //Error pages beego.ErrorController(&controllers.ErrorController{})