Skip to content

Commit

Permalink
feat: init web framework implement (#88)
Browse files Browse the repository at this point in the history
## What type of PR is this?
/kind feature

## What this PR does / why we need it:
Implement a web framework, including:
* Automatically inject `requestID`, `auditLogger`, `bizLogger`
* Support automatic recovery when api call panic
* 100% compatible with standard library(`net/http`)
* Except for `go-chi/chi`, there is no indirect dependence
  • Loading branch information
elliotxx authored Dec 5, 2023
1 parent 921bdd7 commit 0724684
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 7 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
40 changes: 40 additions & 0 deletions pkg/apis/config/config.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
8 changes: 1 addition & 7 deletions pkg/apiserver/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package apiserver

import (
"fmt"
"net/http"

"github.com/KusionStack/karbour/pkg/registry"
clusterstorage "github.com/KusionStack/karbour/pkg/registry/cluster"
Expand All @@ -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
Expand Down Expand Up @@ -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
}
92 changes: 92 additions & 0 deletions pkg/apiserver/router.go
Original file line number Diff line number Diff line change
@@ -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
}
29 changes: 29 additions & 0 deletions pkg/controller/config/controller.go
Original file line number Diff line number Diff line change
@@ -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
}
19 changes: 19 additions & 0 deletions pkg/controller/config/types.go
Original file line number Diff line number Diff line change
@@ -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"`
}
49 changes: 49 additions & 0 deletions pkg/middleware/middleware.go
Original file line number Diff line number Diff line change
@@ -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)
}
35 changes: 35 additions & 0 deletions pkg/util/ctxutil/ctxutil.go
Original file line number Diff line number Diff line change
@@ -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()
}

0 comments on commit 0724684

Please sign in to comment.