diff --git a/go.mod b/go.mod index 1ef49a5b..86057726 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/dominikbraun/graph v0.23.0 github.com/elastic/go-elasticsearch/v8 v8.7.0 github.com/evanphx/json-patch v4.12.0+incompatible + github.com/go-chi/chi/v5 v5.0.10 github.com/google/gofuzz v1.1.0 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 diff --git a/go.sum b/go.sum index 6b1f9b36..d6b9ea01 100644 --- a/go.sum +++ b/go.sum @@ -124,6 +124,8 @@ github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNy github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= diff --git a/pkg/apis/config/config.go b/pkg/apis/config/config.go new file mode 100644 index 00000000..1aa5557c --- /dev/null +++ b/pkg/apis/config/config.go @@ -0,0 +1,40 @@ +// Copyright The Karbour 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 config + +import ( + "encoding/json" + "net/http" + + "github.com/KusionStack/karbour/pkg/controller/config" + "github.com/KusionStack/karbour/pkg/util/ctxutil" +) + +func Get(configCtrl *config.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + log := ctxutil.GetLogger(r.Context()) + + log.Info("Starting get config ...") + + b, err := json.MarshalIndent(configCtrl.Get(), "", " ") + if err != nil { + log.Error(err, "Failed to mashal json") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + w.Write(b) + } +} diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 7b6709ae..fcaca9b7 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -18,7 +18,6 @@ package apiserver import ( "fmt" - "net/http" "github.com/KusionStack/karbour/pkg/registry" clusterstorage "github.com/KusionStack/karbour/pkg/registry/cluster" @@ -28,10 +27,6 @@ import ( "k8s.io/klog/v2" ) -// DefaultStaticDirectory is the default static directory for -// dashboard. -const DefaultStaticDirectory = "./static" - // ExtraConfig holds custom apiserver config type ExtraConfig struct { SearchStorageType string @@ -118,8 +113,7 @@ func (c completedConfig) New() (*APIServer, error) { klog.Infof("Enabling API group %q.", groupName) } - klog.Infof("Dashboard's static directory use: %s", DefaultStaticDirectory) - s.GenericAPIServer.Handler.NonGoRestfulMux.HandlePrefix("/", http.FileServer(http.Dir(DefaultStaticDirectory))) + s.GenericAPIServer.Handler.NonGoRestfulMux.HandlePrefix("/", NewCoreAPIs()) return s, nil } diff --git a/pkg/apiserver/router.go b/pkg/apiserver/router.go new file mode 100644 index 00000000..dcb85bc9 --- /dev/null +++ b/pkg/apiserver/router.go @@ -0,0 +1,92 @@ +// Copyright The Karbour 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 apiserver + +import ( + "fmt" + "net/http" + "strings" + + confighandler "github.com/KusionStack/karbour/pkg/apis/config" + "github.com/KusionStack/karbour/pkg/controller/config" + appmiddleware "github.com/KusionStack/karbour/pkg/middleware" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "k8s.io/klog/v2" +) + +// DefaultStaticDirectory is the default static directory for +// dashboard. +const DefaultStaticDirectory = "./static" + +func NewCoreAPIs() http.Handler { + router := chi.NewRouter() + + // Set up middlewares + router.Use(middleware.RequestID) + router.Use(appmiddleware.AuditLogger) + router.Use(appmiddleware.APILogger) + router.Use(middleware.Recoverer) + + // Set up the frontend router + klog.Infof("Dashboard's static directory use: %s", DefaultStaticDirectory) + router.NotFound(http.FileServer(http.Dir(DefaultStaticDirectory)).ServeHTTP) + + // Set up the core api router + configCtrl := config.NewController(&config.Config{ + Verbose: false, + }) + + router.Route("/api/v1", func(r chi.Router) { + setupAPIV1(r, configCtrl) + }) + + router.Get("/endpoints", func(w http.ResponseWriter, req *http.Request) { + endpoints := listEndpoints(router) + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte(strings.Join(endpoints, "\n"))) + }) + + return router +} + +func setupAPIV1(r chi.Router, configCtrl *config.Controller) { + r.Route("/config", func(r chi.Router) { + r.Get("/", confighandler.Get(configCtrl)) + // r.Delete("/", confighandler.Delete(configCtrl)) + // r.Post("/", confighandler.Post(configCtrl)) + // r.Put("/", confighandler.Put(configCtrl)) + }) + + // r.Route("/topology", func(r chi.Router) { + // r.Get("/", topologyhandler.Get(topologyCtrl)) + // r.Delete("/", topologyhandler.Delete(topologyCtrl)) + // r.Post("/", topologyhandler.Post(topologyCtrl)) + // r.Put("/", topologyhandler.Put(topologyCtrl)) + // }) +} + +func listEndpoints(r chi.Router) []string { + var endpoints []string + walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { + endpoint := fmt.Sprintf("%s %s", method, route) + endpoints = append(endpoints, endpoint) + return nil + } + if err := chi.Walk(r, walkFunc); err != nil { + fmt.Printf("Walking routes error: %s\n", err.Error()) + } + return endpoints +} diff --git a/pkg/controller/config/controller.go b/pkg/controller/config/controller.go new file mode 100644 index 00000000..7c285868 --- /dev/null +++ b/pkg/controller/config/controller.go @@ -0,0 +1,29 @@ +// Copyright The Karbour 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 config + +type Controller struct { + config *Config +} + +func NewController(config *Config) *Controller { + return &Controller{ + config: config, + } +} + +func (c *Controller) Get() *Config { + return c.config +} diff --git a/pkg/controller/config/types.go b/pkg/controller/config/types.go new file mode 100644 index 00000000..08e64b2c --- /dev/null +++ b/pkg/controller/config/types.go @@ -0,0 +1,19 @@ +// Copyright The Karbour 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 config + +type Config struct { + Verbose bool `json:"verbose"` +} diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go new file mode 100644 index 00000000..e4da902f --- /dev/null +++ b/pkg/middleware/middleware.go @@ -0,0 +1,49 @@ +// Copyright The Karbour 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 middleware + +import ( + "context" + "net/http" + + "github.com/go-chi/chi/v5/middleware" + "k8s.io/klog/v2" +) + +type contextKey struct { + name string +} + +var APILoggerKey = &contextKey{"logger"} + +func APILogger(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + if requestID := middleware.GetReqID(r.Context()); len(requestID) > 0 { + logger := klog.FromContext(r.Context()). + WithValues("requestID", requestID). + WithValues("endpoint", r.RequestURI) + ctx = context.WithValue(r.Context(), APILoggerKey, logger) + } + + // continue serving request + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func AuditLogger(next http.Handler) http.Handler { + return middleware.Logger(next) +} diff --git a/pkg/util/ctxutil/ctxutil.go b/pkg/util/ctxutil/ctxutil.go new file mode 100644 index 00000000..23afc6a0 --- /dev/null +++ b/pkg/util/ctxutil/ctxutil.go @@ -0,0 +1,35 @@ +// Copyright The Karbour 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 ctxutil + +import ( + "context" + + "github.com/KusionStack/karbour/pkg/middleware" + "k8s.io/klog/v2" +) + +// GetLogger returns the logger from the given context. +// +// Example: +// +// logger := ctxutil.GetLogger(ctx) +func GetLogger(ctx context.Context) klog.Logger { + if logger, ok := ctx.Value(middleware.APILoggerKey).(klog.Logger); ok { + return logger + } + + return klog.NewKlogr() +}