diff --git a/supernode/server/api/api.go b/supernode/server/api/api.go new file mode 100644 index 000000000..d7d73de34 --- /dev/null +++ b/supernode/server/api/api.go @@ -0,0 +1,80 @@ +/* + * 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 api + +var ( + handlerCategories = make(map[*Category][]*HandlerSpec) +) + +const ( + // VersionedPrefix is recommended, any new API should start with this prefix. + VersionedPrefix = "/api/v1" + + // ExtensionPrefix allows users to customized their own APIs with this prefix. + ExtensionPrefix = "/api/ext" + + // LegacyPrefix is deprecated, just for compatibility with the old version. + LegacyPrefix = "" +) + +var ( + VersionedCategory = &Category{Name: "versioned api", Prefix: VersionedPrefix} + LegacyCategory = &Category{Name: "legacy api", Prefix: LegacyPrefix} + ExtensionCategory = &Category{Name: "extension api", Prefix: ExtensionPrefix} +) + +// Category groups the APIs. +type Category struct { + Name string + Prefix string +} + +// Register adds APIs into supernode server with current api version. +func Register(handlers ...*HandlerSpec) { + for _, h := range handlers { + registerAPI(VersionedCategory, h) + } +} + +// RegisterExt registers extension APIs into supernode server. +// Customized APIs should be registered by using this function. +// It can distinguish between Dragonfly's core APIs and customized APIs. +// And supernode provides `/api/ext` to list all the registered APIs in this +// category. +func RegisterExt(handlers ...*HandlerSpec) { + for _, h := range handlers { + registerAPI(ExtensionCategory, h) + } +} + +// RegisterLegacy is deprecated, just for compatibility with the old version, +// please do not use it to add new API. +func RegisterLegacy(h *HandlerSpec) { + registerAPI(LegacyCategory, h) +} + +func registerAPI(c *Category, h *HandlerSpec) { + if c == nil || !validate(h) { + return + } + handlers := handlerCategories[c] + handlerCategories[c] = append(handlers, h) +} + +func validate(h *HandlerSpec) bool { + return h != nil && h.HandlerFunc != nil && h.Method != "" +} diff --git a/supernode/server/api/api_test.go b/supernode/server/api/api_test.go new file mode 100644 index 000000000..f02f3ae17 --- /dev/null +++ b/supernode/server/api/api_test.go @@ -0,0 +1,99 @@ +/* + * 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 api + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/suite" +) + +func TestSuite(t *testing.T) { + suite.Run(t, new(APISuite)) +} + +type APISuite struct { + suite.Suite + validHandler *HandlerSpec + invalidHandler *HandlerSpec +} + +func (s *APISuite) SetupSuite() { + s.validHandler = &HandlerSpec{ + Method: "GET", + HandlerFunc: func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { + return nil + }, + } + s.invalidHandler = &HandlerSpec{} +} + +func (s *APISuite) SetupTest() { + delete(handlerCategories, VersionedCategory) + delete(handlerCategories, ExtensionCategory) + delete(handlerCategories, LegacyCategory) +} + +func (s *APISuite) TestRegister() { + before := handlerCategories[VersionedCategory] + Register(s.validHandler) + after := handlerCategories[VersionedCategory] + s.Equal(len(before)+1, len(after)) + s.Equal(after[len(after)-1], s.validHandler) +} + +func (s *APISuite) TestRegisterExt() { + before := handlerCategories[ExtensionCategory] + RegisterExt(s.validHandler) + after := handlerCategories[ExtensionCategory] + s.Equal(len(before)+1, len(after)) + s.Equal(after[len(after)-1], s.validHandler) +} + +func (s *APISuite) TestRegisterLegacy() { + before := handlerCategories[LegacyCategory] + RegisterLegacy(s.validHandler) + after := handlerCategories[LegacyCategory] + s.Equal(len(before)+1, len(after)) + s.Equal(after[len(after)-1], s.validHandler) +} + +func (s *APISuite) TestRegisterAPI() { + var cases = []struct { + category *Category + handler *HandlerSpec + }{ + {VersionedCategory, s.invalidHandler}, + {VersionedCategory, s.validHandler}, + {ExtensionCategory, s.validHandler}, + {LegacyCategory, s.validHandler}, + } + + for _, v := range cases { + before := handlerCategories[v.category] + registerAPI(v.category, v.handler) + after := handlerCategories[v.category] + if s.invalidHandler == v.handler { + s.Equal(before, after) + } else if s.validHandler == v.handler { + s.Equal(len(before)+1, len(after)) + s.Equal(after[len(after)-1], v.handler) + } + } +} diff --git a/supernode/server/api/handler.go b/supernode/server/api/handler.go new file mode 100644 index 000000000..f8f33266a --- /dev/null +++ b/supernode/server/api/handler.go @@ -0,0 +1,32 @@ +/* + * 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 api + +import ( + "context" + "net/http" +) + +// HandlerSpec describes an HTTP api +type HandlerSpec struct { + Method string + Path string + HandlerFunc HandlerFunc +} + +// HandlerFunc is the http request handler. +type HandlerFunc func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error diff --git a/supernode/server/api/utils.go b/supernode/server/api/utils.go new file mode 100644 index 000000000..f753b541f --- /dev/null +++ b/supernode/server/api/utils.go @@ -0,0 +1,74 @@ +/* + * 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 api + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/dragonflyoss/Dragonfly/apis/types" + "github.com/dragonflyoss/Dragonfly/pkg/errortypes" + + "github.com/go-openapi/strfmt" +) + +// ValidateFunc validates the request parameters. +type ValidateFunc func(registry strfmt.Registry) error + +// ParseJSONRequest parses the request parameter formed by JSON to a object. +func ParseJSONRequest(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()) + } + } + return nil +} + +// EncodeResponse 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 + 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 fc34c272a..18397d6f7 100644 --- a/supernode/server/router.go +++ b/supernode/server/router.go @@ -24,6 +24,7 @@ import ( "github.com/dragonflyoss/Dragonfly/apis/types" "github.com/dragonflyoss/Dragonfly/version" + "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp"