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
This commit is contained in:
Steven Zou 2018-07-12 13:43:49 +08:00
parent 6ea9291c09
commit 3a44f76b94
4 changed files with 298 additions and 2 deletions

View File

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

View File

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

View File

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

View File

@ -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{})