diff --git a/supernode/server/api/api.go b/supernode/server/api/api.go index c235b7f1f..42dc860b7 100644 --- a/supernode/server/api/api.go +++ b/supernode/server/api/api.go @@ -47,6 +47,9 @@ func newCategory(name, prefix string) *category { apiCategories[name] = &category{ name: name, prefix: prefix, + handlerSpecs: []*HandlerSpec{ + listHandler(name), + }, } return apiCategories[name] } @@ -59,11 +62,12 @@ type category struct { } // Register registers an API into this API category. -func (c *category) Register(h *HandlerSpec) *category { - if !validate(h) { - return c +func (c *category) Register(handlers ...*HandlerSpec) *category { + for _, h := range handlers { + if valid(h) { + c.handlerSpecs = append(c.handlerSpecs, h) + } } - c.handlerSpecs = append(c.handlerSpecs, h) return c } @@ -82,8 +86,11 @@ func (c *category) Handlers() []*HandlerSpec { return c.handlerSpecs } -// ----------------------------------------------------------------------------- - -func validate(h *HandlerSpec) bool { - return h != nil && h.HandlerFunc != nil && h.Method != "" +// Range traverses all the handlers in this category. +func (c *category) Range(f func(prefix string, h *HandlerSpec)) { + for _, h := range c.handlerSpecs { + if h != nil { + f(c.prefix, h) + } + } } diff --git a/supernode/server/api/utils.go b/supernode/server/api/utils.go index d5efb5663..71f8abfc8 100644 --- a/supernode/server/api/utils.go +++ b/supernode/server/api/utils.go @@ -52,15 +52,17 @@ func ParseJSONRequest(req io.Reader, target interface{}, validator ValidateFunc) return nil } -// EncodeResponse encodes response in json. -// The response body is empty if the data is nil or empty value. -func EncodeResponse(w http.ResponseWriter, code int, data interface{}) error { +// SendResponse encodes response in json. +// +// TODO: +// Should the response body should be empty if the data is nil or empty +// string? Now it's incompatible with the client. +func SendResponse(w http.ResponseWriter, code int, data interface{}) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) - if util.IsNil(data) || data == "" { - return nil - } - return json.NewEncoder(w).Encode(data) + enc := json.NewEncoder(w) + enc.SetEscapeHTML(false) + return enc.Encode(data) } // HandleErrorResponse handles err from server side and constructs response @@ -68,10 +70,10 @@ func EncodeResponse(w http.ResponseWriter, code int, data interface{}) error { func HandleErrorResponse(w http.ResponseWriter, err error) { switch e := err.(type) { case *errortypes.HTTPError: - _ = EncodeResponse(w, e.Code, errResp(e.Code, e.Msg)) + _ = SendResponse(w, e.Code, errResp(e.Code, e.Msg)) default: // By default, server side returns code 500 if error happens. - _ = EncodeResponse(w, http.StatusInternalServerError, + _ = SendResponse(w, http.StatusInternalServerError, errResp(http.StatusInternalServerError, e.Error())) } } @@ -101,3 +103,36 @@ func errResp(code int, msg string) *types.ErrorResponse { Message: msg, } } + +func valid(h *HandlerSpec) bool { + return h != nil && h.HandlerFunc != nil && h.Method != "" +} + +func listHandler(name string) *HandlerSpec { + h := &HandlerSpec{ + Method: http.MethodGet, + Path: "/", + HandlerFunc: func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { + c := apiCategories[name] + if c == nil { + return errortypes.NewHTTPError(http.StatusBadRequest, "no such category") + } + + result := map[string]interface{}{ + "category": c.name, + "prefix": c.prefix, + } + handlers := make([]map[string]string, len(c.handlerSpecs)) + for i, v := range c.handlerSpecs { + handlers[i] = map[string]string{ + "method": v.Method, + "path": v.Path, + } + } + result["api"] = handlers + + return SendResponse(rw, http.StatusOK, result) + }, + } + return h +} diff --git a/supernode/server/api/utils_test.go b/supernode/server/api/utils_test.go index 6c4666d0d..e762a8f41 100644 --- a/supernode/server/api/utils_test.go +++ b/supernode/server/api/utils_test.go @@ -29,7 +29,6 @@ import ( "github.com/stretchr/testify/suite" "github.com/dragonflyoss/Dragonfly/pkg/errortypes" - "github.com/dragonflyoss/Dragonfly/pkg/util" ) func TestUtil(t *testing.T) { @@ -89,7 +88,7 @@ func (s *TestUtilSuite) TestEncodeResponse() { err string }{ {200, "", ""}, - {200, nil, ""}, + {200, (*testStruct)(nil), ""}, {200, 0, ""}, {200, newT(1), ""}, {400, newT(1), ""}, @@ -98,12 +97,12 @@ func (s *TestUtilSuite) TestEncodeResponse() { for i, c := range cases { msg := fmt.Sprintf("case %d: %v", i, c) w := httptest.NewRecorder() - e := EncodeResponse(w, c.code, c.data) + e := SendResponse(w, c.code, c.data) if c.err == "" { s.Nil(e, msg) s.Equal(c.code, w.Code, msg) - if util.IsNil(c.data) { - s.Equal("", strings.TrimSpace(w.Body.String()), msg) + if c.data == "" { + s.Equal("\"\"", strings.TrimSpace(w.Body.String()), msg) } else { s.Equal(fmt.Sprintf("%v", c.data), strings.TrimSpace(w.Body.String()), msg) } @@ -146,7 +145,7 @@ func (s *TestUtilSuite) TestWrapHandler() { case "POST": return errortypes.NewHTTPError(400, "test") } - _ = EncodeResponse(rw, 200, "test") + _ = SendResponse(rw, 200, "test") return nil } cases := []struct { @@ -201,7 +200,7 @@ func (t *testStruct) validate(registry strfmt.Registry) error { func (t *testStruct) String() string { if t == nil { - return "" + return "null" } return fmt.Sprintf("{\"A\":%d}", t.A) } diff --git a/supernode/server/handler.go b/supernode/server/handler.go deleted file mode 100644 index d55b84902..000000000 --- a/supernode/server/handler.go +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright The Dragonfly 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 ( - "context" - "net/http" -) - -// HandlerSpec is used to describe a HTTP API. -type HandlerSpec struct { - Method string - Path string - HandlerFunc Handler -} - -// Handler is the http request handler. -type Handler func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error - -// NewHandlerSpec constructs a brand new HandlerSpec. -func NewHandlerSpec(method, path string, handler Handler) *HandlerSpec { - return &HandlerSpec{ - Method: method, - Path: path, - HandlerFunc: handler, - } -} diff --git a/supernode/server/preheat_bridge.go b/supernode/server/preheat_bridge.go index f3e302d9e..e2815f7ad 100644 --- a/supernode/server/preheat_bridge.go +++ b/supernode/server/preheat_bridge.go @@ -18,16 +18,14 @@ package server import ( "context" - "encoding/json" - "io" "net/http" "github.com/dragonflyoss/Dragonfly/apis/types" "github.com/dragonflyoss/Dragonfly/pkg/errortypes" + "github.com/dragonflyoss/Dragonfly/supernode/server/api" "github.com/go-openapi/strfmt" "github.com/gorilla/mux" - "github.com/sirupsen/logrus" ) // --------------------------------------------------------------------------- @@ -35,12 +33,12 @@ import ( func (s *Server) createPreheatTask(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { request := &types.PreheatCreateRequest{} - if err := parseRequest(req.Body, request, request.Validate); err != nil { + if err := api.ParseJSONRequest(req.Body, request, request.Validate); err != nil { return err } preheatID, err := s.PreheatMgr.Create(ctx, request) if err != nil { - return err + return httpErr(err) } resp := types.PreheatCreateResponse{ID: preheatID} return EncodeResponse(rw, http.StatusCreated, resp) @@ -49,7 +47,7 @@ func (s *Server) createPreheatTask(ctx context.Context, rw http.ResponseWriter, func (s *Server) getAllPreheatTasks(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { tasks, err := s.PreheatMgr.GetAll(ctx) if err != nil { - return err + return httpErr(err) } return EncodeResponse(rw, http.StatusOK, tasks) } @@ -58,7 +56,7 @@ func (s *Server) getPreheatTask(ctx context.Context, rw http.ResponseWriter, req id := mux.Vars(req)["id"] task, err := s.PreheatMgr.Get(ctx, id) if err != nil { - return err + return httpErr(err) } resp := types.PreheatInfo{ ID: task.ID, @@ -72,7 +70,7 @@ func (s *Server) getPreheatTask(ctx context.Context, rw http.ResponseWriter, req func (s *Server) deletePreheatTask(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { id := mux.Vars(req)["id"] if err := s.PreheatMgr.Delete(ctx, id); err != nil { - return err + return httpErr(err) } return EncodeResponse(rw, http.StatusOK, true) } @@ -80,76 +78,19 @@ func (s *Server) deletePreheatTask(ctx context.Context, rw http.ResponseWriter, // --------------------------------------------------------------------------- // helper functions -type validateFunc func(registry strfmt.Registry) error - -func parseRequest(body io.Reader, request interface{}, validator validateFunc) error { - if err := json.NewDecoder(body).Decode(request); err != nil { - if err == io.EOF { - return errortypes.New(http.StatusBadRequest, "empty body") - } - return errortypes.New(http.StatusBadRequest, err.Error()) - } - if validator != nil { - if err := validator(strfmt.NewFormats()); err != nil { - return errortypes.New(http.StatusBadRequest, err.Error()) - } +func httpErr(err error) error { + if e, ok := err.(*errortypes.DfError); ok { + return errortypes.NewHTTPError(e.Code, e.Msg) } - return nil + return err } -// initPreheatHandlers register preheat apis -func initPreheatHandlers(s *Server, r *mux.Router) { - handlers := []*HandlerSpec{ +// preheatHandlers returns all the preheats handlers. +func preheatHandlers(s *Server) []*api.HandlerSpec { + return []*api.HandlerSpec{ {Method: http.MethodPost, Path: "/preheats", HandlerFunc: s.createPreheatTask}, {Method: http.MethodGet, Path: "/preheats", HandlerFunc: s.getAllPreheatTasks}, {Method: http.MethodGet, Path: "/preheats/{id}", HandlerFunc: s.getPreheatTask}, {Method: http.MethodDelete, Path: "/preheats/{id}", HandlerFunc: s.deletePreheatTask}, } - // register API - for _, h := range handlers { - if h != nil { - r.Path(versionMatcher + h.Path).Methods(h.Method). - Handler(m.instrumentHandler(h.Path, postPreheatHandler(h.HandlerFunc))) - r.Path("/api/v1" + h.Path).Methods(h.Method). - Handler(m.instrumentHandler(h.Path, postPreheatHandler(h.HandlerFunc))) - r.Path(h.Path).Methods(h.Method). - Handler(m.instrumentHandler(h.Path, postPreheatHandler(h.HandlerFunc))) - } - } -} - -func postPreheatHandler(h Handler) http.HandlerFunc { - pctx := context.Background() - - return func(w http.ResponseWriter, req *http.Request) { - ctx, cancel := context.WithCancel(pctx) - defer cancel() - - // Start to handle request. - err := h(ctx, w, req) - if err != nil { - // Handle error if request handling fails. - handlePreheatErrorResponse(w, err) - } - logrus.Debugf("%s %v err:%v", req.Method, req.URL, err) - } -} - -func handlePreheatErrorResponse(w http.ResponseWriter, err error) { - var ( - code int - errMsg string - ) - - // By default, daemon side returns code 500 if error happens. - code = http.StatusInternalServerError - if e, ok := err.(*errortypes.DfError); ok { - code = e.Code - errMsg = e.Msg - } - - _ = EncodeResponse(w, code, types.ErrorResponse{ - Code: int64(code), - Message: errMsg, - }) } diff --git a/supernode/server/router.go b/supernode/server/router.go index 18397d6f7..d2a15357a 100644 --- a/supernode/server/router.go +++ b/supernode/server/router.go @@ -18,11 +18,11 @@ package server import ( "context" - "encoding/json" "net/http" "net/http/pprof" + "strings" - "github.com/dragonflyoss/Dragonfly/apis/types" + "github.com/dragonflyoss/Dragonfly/supernode/server/api" "github.com/dragonflyoss/Dragonfly/version" "github.com/gorilla/mux" @@ -30,18 +30,63 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" ) -// versionMatcher defines to parse version url path. -const versionMatcher = "/v{version:[0-9.]+}" - var m = newMetrics(prometheus.DefaultRegisterer) -func initRoute(s *Server) *mux.Router { +func createRouter(s *Server) *mux.Router { + registerCoreHandlers(s) + r := mux.NewRouter() - handlers := []*HandlerSpec{ + if s.Config.Debug || s.Config.EnableProfiler { + initDebugRoutes(r) + } + initAPIRoutes(r) + return r +} + +func registerCoreHandlers(s *Server) { + registerV1(s) + registerLegacy(s) + registerSystem(s) +} + +func registerV1(s *Server) { + v1Handlers := []*api.HandlerSpec{ + // peer + {Method: http.MethodPost, Path: "/peers", HandlerFunc: s.registerPeer}, + {Method: http.MethodDelete, Path: "/peers/{id}", HandlerFunc: s.deRegisterPeer}, + {Method: http.MethodGet, Path: "/peers/{id}", HandlerFunc: s.getPeer}, + {Method: http.MethodGet, Path: "/peers", HandlerFunc: s.listPeers}, + {Method: http.MethodGet, Path: "/tasks/{id}", HandlerFunc: s.getTaskInfo}, + {Method: http.MethodPost, Path: "/peer/network", HandlerFunc: s.fetchP2PNetworkInfo}, + {Method: http.MethodPost, Path: "/peer/heartbeat", HandlerFunc: s.reportPeerHealth}, + + // task + {Method: http.MethodDelete, Path: "/tasks/{id}", HandlerFunc: s.deleteTask}, + + // piece + {Method: http.MethodGet, Path: "/tasks/{id}/pieces/{pieceRange}/error", HandlerFunc: s.handlePieceError}, + } + + api.V1.Register(v1Handlers...) + // add preheat APIs to v1 category + api.V1.Register(preheatHandlers(s)...) +} + +func registerSystem(s *Server) { + systemHandlers := []*api.HandlerSpec{ // system {Method: http.MethodGet, Path: "/_ping", HandlerFunc: s.ping}, {Method: http.MethodGet, Path: "/version", HandlerFunc: version.HandlerWithCtx}, + // metrics + {Method: http.MethodGet, Path: "/metrics", HandlerFunc: handleMetrics}, + {Method: http.MethodPost, Path: "/task/metrics", HandlerFunc: m.handleMetricsReport}, + } + api.Legacy.Register(systemHandlers...) +} + +func registerLegacy(s *Server) { + legacyHandlers := []*api.HandlerSpec{ // v0.3 {Method: http.MethodPost, Path: "/peer/registry", HandlerFunc: s.registry}, {Method: http.MethodGet, Path: "/peer/task", HandlerFunc: s.pullPieceTask}, @@ -50,91 +95,43 @@ func initRoute(s *Server) *mux.Router { {Method: http.MethodGet, Path: "/peer/piece/error", HandlerFunc: s.reportPieceError}, {Method: http.MethodPost, Path: "/peer/network", HandlerFunc: s.fetchP2PNetworkInfo}, {Method: http.MethodPost, Path: "/peer/heartbeat", HandlerFunc: s.reportPeerHealth}, - - // v1 - // peer - {Method: http.MethodPost, Path: "/api/v1/peers", HandlerFunc: s.registerPeer}, - {Method: http.MethodDelete, Path: "/api/v1/peers/{id}", HandlerFunc: s.deRegisterPeer}, - {Method: http.MethodGet, Path: "/api/v1/peers/{id}", HandlerFunc: s.getPeer}, - {Method: http.MethodGet, Path: "/api/v1/peers", HandlerFunc: s.listPeers}, - {Method: http.MethodGet, Path: "/api/v1/tasks/{id}", HandlerFunc: s.getTaskInfo}, - - // task - {Method: http.MethodDelete, Path: "/api/v1/tasks/{id}", HandlerFunc: s.deleteTask}, - - // piece - {Method: http.MethodGet, Path: "/api/v1/tasks/{id}/pieces/{pieceRange}/error", HandlerFunc: s.handlePieceError}, - - // metrics - {Method: http.MethodGet, Path: "/metrics", HandlerFunc: handleMetrics}, - {Method: http.MethodPost, Path: "/task/metrics", HandlerFunc: m.handleMetricsReport}, } + api.Legacy.Register(legacyHandlers...) + api.Legacy.Register(preheatHandlers(s)...) +} - // register API - for _, h := range handlers { - if h != nil { - r.Path(versionMatcher + h.Path).Methods(h.Method).Handler(m.instrumentHandler(h.Path, filter(h.HandlerFunc))) - r.Path(h.Path).Methods(h.Method).Handler(m.instrumentHandler(h.Path, filter(h.HandlerFunc))) +func initAPIRoutes(r *mux.Router) { + add := func(prefix string, h *api.HandlerSpec) { + path := h.Path + if path == "" || path[0] != '/' { + path = "/" + path } + if !strings.HasPrefix(path, prefix) { + path = prefix + h.Path + } + r.Path(path).Methods(h.Method). + Handler(m.instrumentHandler(path, api.WrapHandler(h.HandlerFunc))) } - initPreheatHandlers(s, r) - if s.Config.Debug || s.Config.EnableProfiler { - r.PathPrefix("/debug/pprof/cmdline").HandlerFunc(pprof.Cmdline) - r.PathPrefix("/debug/pprof/profile").HandlerFunc(pprof.Profile) - r.PathPrefix("/debug/pprof/symbol").HandlerFunc(pprof.Symbol) - r.PathPrefix("/debug/pprof/trace").HandlerFunc(pprof.Trace) - r.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index) - } - return r + api.V1.Range(add) + api.Extension.Range(add) + api.Legacy.Range(add) } -func handleMetrics(ctx context.Context, rw http.ResponseWriter, req *http.Request) (err error) { - promhttp.Handler().ServeHTTP(rw, req) - return nil +func initDebugRoutes(r *mux.Router) { + r.PathPrefix("/debug/pprof/cmdline").HandlerFunc(pprof.Cmdline) + r.PathPrefix("/debug/pprof/profile").HandlerFunc(pprof.Profile) + r.PathPrefix("/debug/pprof/symbol").HandlerFunc(pprof.Symbol) + r.PathPrefix("/debug/pprof/trace").HandlerFunc(pprof.Trace) + r.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index) } -func filter(handler Handler) http.HandlerFunc { - pctx := context.Background() - - return func(w http.ResponseWriter, req *http.Request) { - ctx, cancel := context.WithCancel(pctx) - defer cancel() - - // Start to handle request. - - if err := handler(ctx, w, req); err != nil { - // Handle error if request handling fails. - HandleErrorResponse(w, err) - } - } +func handleMetrics(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { + promhttp.Handler().ServeHTTP(rw, req) + return nil } -// EncodeResponse encodes response in json. +// SendResponse encodes response in json. func EncodeResponse(rw http.ResponseWriter, statusCode int, data interface{}) error { - rw.Header().Set("Content-Type", "application/json") - rw.WriteHeader(statusCode) - return json.NewEncoder(rw).Encode(data) -} - -// HandleErrorResponse handles err from daemon side and constructs response for client side. -func HandleErrorResponse(w http.ResponseWriter, err error) { - var ( - code int - errMsg string - ) - - // By default, daemon side returns code 500 if error happens. - code = http.StatusInternalServerError - errMsg = NewResultInfoWithError(err).Error() - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(code) - enc := json.NewEncoder(w) - enc.SetEscapeHTML(false) - - resp := types.Error{ - Message: errMsg, - } - enc.Encode(resp) + return api.SendResponse(rw, statusCode, data) } diff --git a/supernode/server/router_test.go b/supernode/server/router_test.go index da6645b5a..b44df76f3 100644 --- a/supernode/server/router_test.go +++ b/supernode/server/router_test.go @@ -82,7 +82,7 @@ func (rs *RouterTestSuite) SetUpSuite(c *check.C) { GoVersion: runtime.Version(), } - rs.router = initRoute(s) + rs.router = createRouter(s) rs.listener, err = net.Listen("tcp", rs.addr) c.Check(err, check.IsNil) go http.Serve(rs.listener, rs.router) diff --git a/supernode/server/server.go b/supernode/server/server.go index ed89372a9..17504b524 100644 --- a/supernode/server/server.go +++ b/supernode/server/server.go @@ -136,7 +136,7 @@ func New(cfg *config.Config, logger *logrus.Logger, register prometheus.Register // Start runs supernode server. func (s *Server) Start() error { - router := initRoute(s) + router := createRouter(s) address := fmt.Sprintf("0.0.0.0:%d", s.Config.ListenPort)