From 8aeabc7717cdeae079e44d0c520851d82ef62947 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Fri, 17 Jan 2020 16:46:17 +0800 Subject: [PATCH] Wrap the beego router and provide a unified view for users to register routes Wrap the beego router and provide a unified view for users to register routes Signed-off-by: Wenkai Yin --- src/core/main.go | 18 +-- src/server/registry/handler.go | 1 - src/server/registry/manifest/manifest.go | 16 ++- src/server/registry/route.go | 45 +++++++ src/server/router/router.go | 120 ++++++++++++++++++ src/server/router/router_test.go | 82 ++++++++++++ src/server/server.go | 29 +++++ .../router.go => server/v1.0/route/route.go} | 22 +--- src/server/v2.0/route/route.go | 25 ++++ 9 files changed, 327 insertions(+), 31 deletions(-) create mode 100644 src/server/registry/route.go create mode 100644 src/server/router/router.go create mode 100644 src/server/router/router_test.go create mode 100644 src/server/server.go rename src/{core/router.go => server/v1.0/route/route.go} (97%) create mode 100644 src/server/v2.0/route/route.go diff --git a/src/core/main.go b/src/core/main.go index 88caf7b13..ff06b9195 100755 --- a/src/core/main.go +++ b/src/core/main.go @@ -17,14 +17,6 @@ package main import ( "encoding/gob" "fmt" - "net/http" - "os" - "os/signal" - "strconv" - "strings" - "syscall" - "time" - "github.com/astaxie/beego" _ "github.com/astaxie/beego/session/redis" "github.com/goharbor/harbor/src/common/dao" @@ -55,8 +47,16 @@ import ( "github.com/goharbor/harbor/src/pkg/types" "github.com/goharbor/harbor/src/pkg/version" "github.com/goharbor/harbor/src/replication" + "github.com/goharbor/harbor/src/server" "github.com/goharbor/harbor/src/server/middleware/orm" "github.com/goharbor/harbor/src/server/middleware/requestid" + "net/http" + "os" + "os/signal" + "strconv" + "strings" + "syscall" + "time" ) const ( @@ -254,7 +254,7 @@ func main() { beego.InsertFilter("/*", beego.BeforeRouter, filter.SecurityFilter) beego.InsertFilter("/*", beego.BeforeRouter, filter.ReadonlyFilter) - initRouters() + server.RegisterRoutes() syncRegistry := os.Getenv("SYNC_REGISTRY") sync, err := strconv.ParseBool(syncRegistry) diff --git a/src/server/registry/handler.go b/src/server/registry/handler.go index 65de1e047..e43d2e725 100644 --- a/src/server/registry/handler.go +++ b/src/server/registry/handler.go @@ -55,7 +55,6 @@ func New(url *url.URL) http.Handler { manifestRouter := rootRouter.Path("/v2/{name:.*}/manifests/{reference}").Subrouter() manifestRouter.NewRoute().Methods(http.MethodGet).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), manifestinfo.Middleware(), regtoken.Middleware())) manifestRouter.NewRoute().Methods(http.MethodHead).Handler(manifest.NewHandler(project.Mgr, proxy)) - manifestRouter.NewRoute().Methods(http.MethodPut).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), readonly.Middleware(), manifestinfo.Middleware(), immutable.MiddlewarePush())) manifestRouter.NewRoute().Methods(http.MethodDelete).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), readonly.Middleware(), manifestinfo.Middleware(), immutable.MiddlewareDelete())) // handle blob diff --git a/src/server/registry/manifest/manifest.go b/src/server/registry/manifest/manifest.go index 19d269de1..9052f7c3e 100644 --- a/src/server/registry/manifest/manifest.go +++ b/src/server/registry/manifest/manifest.go @@ -23,13 +23,14 @@ import ( ierror "github.com/goharbor/harbor/src/internal/error" "github.com/goharbor/harbor/src/pkg/project" "github.com/goharbor/harbor/src/server/registry/error" - "github.com/gorilla/mux" + "github.com/goharbor/harbor/src/server/router" "github.com/opencontainers/go-digest" "net/http" "net/http/httputil" ) // NewHandler returns the handler to handler manifest requests +// TODO the parameters aren't required, use the global variables instead func NewHandler(proMgr project.Manager, proxy *httputil.ReverseProxy) http.Handler { return &handler{ proMgr: proMgr, @@ -72,8 +73,11 @@ func (h *handler) delete(w http.ResponseWriter, req *http.Request) { } func (h *handler) put(w http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) - repositoryName := vars["name"] + repositoryName, err := router.Param(req.Context(), ":splat") + if err != nil { + error.Handle(w, req, err) + return + } projectName, _ := utils.ParseRepository(repositoryName) project, err := h.proMgr.Get(projectName) if err != nil { @@ -109,7 +113,11 @@ func (h *handler) put(w http.ResponseWriter, req *http.Request) { var tags []string var dgt string - reference := vars["reference"] + reference, err := router.Param(req.Context(), ":reference") + if err != nil { + error.Handle(w, req, err) + return + } dg, err := digest.Parse(reference) if err == nil { // the reference is digest diff --git a/src/server/registry/route.go b/src/server/registry/route.go new file mode 100644 index 000000000..ecb75a02e --- /dev/null +++ b/src/server/registry/route.go @@ -0,0 +1,45 @@ +// Copyright Project Harbor Authors +// +// 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 registry + +import ( + "github.com/goharbor/harbor/src/core/config" + "github.com/goharbor/harbor/src/pkg/project" + "github.com/goharbor/harbor/src/server/middleware/immutable" + "github.com/goharbor/harbor/src/server/middleware/manifestinfo" + "github.com/goharbor/harbor/src/server/middleware/readonly" + "github.com/goharbor/harbor/src/server/registry/manifest" + "github.com/goharbor/harbor/src/server/router" + "net/http" + "net/http/httputil" + "net/url" +) + +// RegisterRoutes for OCI registry APIs +func RegisterRoutes() { + // TODO remove + regURL, _ := config.RegistryURL() + url, _ := url.Parse(regURL) + proxy := httputil.NewSingleHostReverseProxy(url) + + router.NewRoute().Path("/v2/*").Handler(New(url)) + router.NewRoute(). + Method(http.MethodPut). + Path("/v2/*/manifests/:reference"). + Middleware(readonly.Middleware()). + Middleware(manifestinfo.Middleware()). + Middleware(immutable.MiddlewarePush()). + Handler(manifest.NewHandler(project.Mgr, proxy)) +} diff --git a/src/server/router/router.go b/src/server/router/router.go new file mode 100644 index 000000000..4afcdad18 --- /dev/null +++ b/src/server/router/router.go @@ -0,0 +1,120 @@ +// Copyright Project Harbor Authors +// +// 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 router + +import ( + "context" + "errors" + "github.com/astaxie/beego" + beegocontext "github.com/astaxie/beego/context" + "github.com/goharbor/harbor/src/server/middleware" + "net/http" +) + +type contextKeyInput struct{} + +// NewRoute creates a new route +func NewRoute() *Route { + return &Route{} +} + +// Route stores the information that matches a request +type Route struct { + methods []string + path string + middlewares []middleware.Middleware +} + +// Method sets the method that the route matches +func (r *Route) Method(method string) *Route { + r.methods = append(r.methods, method) + return r +} + +// Path sets the path that the route matches. Path uses the beego router path pattern +func (r *Route) Path(path string) *Route { + r.path = path + return r +} + +// Middleware sets the middleware that executed when handling the request +func (r *Route) Middleware(middleware middleware.Middleware) *Route { + r.middlewares = append(r.middlewares, middleware) + return r +} + +// Handler sets the handler that handles the request +func (r *Route) Handler(handler http.Handler) { + filterFunc := beego.FilterFunc(func(ctx *beegocontext.Context) { + ctx.Request = ctx.Request.WithContext( + context.WithValue(ctx.Request.Context(), contextKeyInput{}, ctx.Input)) + // TODO remove the WithMiddlewares? + middleware.WithMiddlewares(handler, r.middlewares...). + ServeHTTP(ctx.ResponseWriter, ctx.Request) + }) + if len(r.methods) == 0 { + beego.Any(r.path, filterFunc) + return + } + for _, method := range r.methods { + switch method { + case http.MethodGet: + beego.Get(r.path, filterFunc) + case http.MethodHead: + beego.Head(r.path, filterFunc) + case http.MethodPut: + beego.Put(r.path, filterFunc) + case http.MethodPatch: + beego.Patch(r.path, filterFunc) + case http.MethodPost: + beego.Post(r.path, filterFunc) + case http.MethodDelete: + beego.Delete(r.path, filterFunc) + case http.MethodOptions: + beego.Options(r.path, filterFunc) + } + } +} + +// HandlerFunc sets the handler function that handles the request +func (r *Route) HandlerFunc(f http.HandlerFunc) { + r.Handler(f) +} + +// GetInput returns the input object from the context +func GetInput(context context.Context) (*beegocontext.BeegoInput, error) { + if context == nil { + return nil, errors.New("context is nil") + } + input, ok := context.Value(contextKeyInput{}).(*beegocontext.BeegoInput) + if !ok { + return nil, errors.New("input not found in the context") + } + return input, nil +} + +// Param returns the router param by a given key from the context +func Param(ctx context.Context, key string) (string, error) { + input, err := GetInput(ctx) + if err != nil { + return "", err + } + return input.Param(key), nil +} + +// Middleware registers the global middleware that executed for all requests that match the path +func Middleware(path string, middleware middleware.Middleware) { + // TODO add middleware function to register global middleware after upgrading to the latest version of beego +} diff --git a/src/server/router/router_test.go b/src/server/router/router_test.go new file mode 100644 index 000000000..14964df99 --- /dev/null +++ b/src/server/router/router_test.go @@ -0,0 +1,82 @@ +// Copyright Project Harbor Authors +// +// 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 router + +import ( + "context" + beegocontext "github.com/astaxie/beego/context" + "github.com/goharbor/harbor/src/server/middleware" + "github.com/stretchr/testify/suite" + "net/http" + "testing" +) + +type routerTestSuite struct { + suite.Suite + route *Route +} + +func (r *routerTestSuite) SetupTest() { + r.route = NewRoute() +} + +func (r *routerTestSuite) TestMethod() { + r.route.Method(http.MethodGet) + r.Equal(http.MethodGet, r.route.methods[0]) + r.route.Method(http.MethodDelete) + r.Equal(http.MethodDelete, r.route.methods[1]) +} + +func (r *routerTestSuite) TestPath() { + r.route.Path("/api/*") + r.Equal("/api/*", r.route.path) +} + +func (r *routerTestSuite) TestMiddleware() { + m1 := middleware.Middleware(func(handler http.Handler) http.Handler { return nil }) + m2 := middleware.Middleware(func(handler http.Handler) http.Handler { return nil }) + r.route.Middleware(m1) + r.Len(r.route.middlewares, 1) + r.route.Middleware(m2) + r.Len(r.route.middlewares, 2) +} + +func (r *routerTestSuite) TestGetInput() { + // nil context + _, err := GetInput(nil) + r.Require().NotNil(err) + + // context contains wrong type input + _, err = GetInput(context.WithValue(context.Background(), contextKeyInput{}, &Route{})) + r.Require().NotNil(err) + + // context contains input + input, err := GetInput(context.WithValue(context.Background(), contextKeyInput{}, &beegocontext.BeegoInput{})) + r.Require().Nil(err) + r.Assert().NotNil(input) +} + +func (r *routerTestSuite) TestParam() { + input := &beegocontext.BeegoInput{} + input.SetParam("key", "value") + value, err := Param(context.WithValue(context.Background(), contextKeyInput{}, input), "key") + r.Require().Nil(err) + r.Assert().NotNil(input) + r.Equal("value", value) +} + +func TestRouterTestSuite(t *testing.T) { + suite.Run(t, &routerTestSuite{}) +} diff --git a/src/server/server.go b/src/server/server.go new file mode 100644 index 000000000..b0934471b --- /dev/null +++ b/src/server/server.go @@ -0,0 +1,29 @@ +// Copyright Project Harbor Authors +// +// 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 server + +import ( + // "github.com/goharbor/harbor/src/server/registry" + v1 "github.com/goharbor/harbor/src/server/v1.0/route" + v2 "github.com/goharbor/harbor/src/server/v2.0/route" +) + +// RegisterRoutes register all routes +func RegisterRoutes() { + // TODO move the v1 APIs to v2 + v1.RegisterRoutes() // v1.0 APIs + v2.RegisterRoutes() // v2.0 APIs + // registry.RegisterRoutes() // OCI registry APIs +} diff --git a/src/core/router.go b/src/server/v1.0/route/route.go similarity index 97% rename from src/core/router.go rename to src/server/v1.0/route/route.go index dcfaeb1bf..2fc8b17cd 100755 --- a/src/core/router.go +++ b/src/server/v1.0/route/route.go @@ -12,11 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package route import ( - "net/url" - "github.com/astaxie/beego" "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/core/api" @@ -27,11 +25,11 @@ import ( "github.com/goharbor/harbor/src/core/service/notifications/registry" "github.com/goharbor/harbor/src/core/service/notifications/scheduler" "github.com/goharbor/harbor/src/core/service/token" - reg "github.com/goharbor/harbor/src/server/registry" - "github.com/goharbor/harbor/src/server/v2.0/handler" ) -func initRouters() { +// RegisterRoutes for Harbor v1.0 APIs +// TODO split the APIs and other services/controllers +func RegisterRoutes() { // Controller API: beego.Router("/c/login", &controllers.CommonController{}, "post:Login") beego.Router("/c/log_out", &controllers.CommonController{}, "get:LogOut") @@ -162,14 +160,6 @@ func initRouters() { beego.Router("/api/projects/:pid([0-9]+)/immutabletagrules", &api.ImmutableTagRuleAPI{}, "get:List;post:Post") beego.Router("/api/projects/:pid([0-9]+)/immutabletagrules/:id([0-9]+)", &api.ImmutableTagRuleAPI{}) - // TODO remove - regURL, _ := config.RegistryURL() - url, _ := url.Parse(regURL) - registryHandler := reg.New(url) - _ = registryHandler - // beego.Handler("/v2/*", registryHandler) - beego.Router("/v2/*", &controllers.RegistryProxy{}, "*:Handle") - // APIs for chart repository if config.WithChartMuseum() { // Charts are controlled under projects @@ -220,10 +210,8 @@ func initRouters() { beego.Router("/api/scans/all/metrics", scanAllAPI, "get:GetScanAllMetrics") beego.Router("/api/scans/schedule/metrics", scanAllAPI, "get:GetScheduleMetrics") - // Add handler for api v2.0 - beego.Handler("/api/v2.0/*", handler.New()) - // Error pages beego.ErrorController(&controllers.ErrorController{}) + beego.Router("/v2/*", &controllers.RegistryProxy{}, "*:Handle") } diff --git a/src/server/v2.0/route/route.go b/src/server/v2.0/route/route.go new file mode 100644 index 000000000..9d962c1c2 --- /dev/null +++ b/src/server/v2.0/route/route.go @@ -0,0 +1,25 @@ +// Copyright Project Harbor Authors +// +// 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 route + +import ( + "github.com/goharbor/harbor/src/server/router" + "github.com/goharbor/harbor/src/server/v2.0/handler" +) + +// RegisterRoutes for Harbor v2.0 APIs +func RegisterRoutes() { + router.NewRoute().Path("/api/v2.0/*").Handler(handler.New()) +}