Files
podinfo/pkg/api/http/server.go
T

358 lines
11 KiB
Go

package http
import (
"context"
"fmt"
"net/http"
_ "net/http/pprof"
"os"
"path"
"strings"
"sync/atomic"
"time"
"github.com/gomodule/redigo/redis"
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/stefanprodan/podinfo/pkg/api/http/docs"
"github.com/stefanprodan/podinfo/pkg/fscache"
httpSwagger "github.com/swaggo/http-swagger"
"github.com/swaggo/swag"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
// @title Podinfo API
// @version 2.0
// @description Go microservice template for Kubernetes.
// @contact.name Source Code
// @contact.url https://github.com/stefanprodan/podinfo
// @license.name MIT License
// @license.url https://github.com/stefanprodan/podinfo/blob/master/LICENSE
// @BasePath /
// @schemes http https
var (
healthy int32
ready int32
watcher *fscache.Watcher
)
type Config struct {
HttpClientTimeout time.Duration `mapstructure:"http-client-timeout"`
HttpServerTimeout time.Duration `mapstructure:"http-server-timeout"`
ServerShutdownTimeout time.Duration `mapstructure:"server-shutdown-timeout"`
BackendURL []string `mapstructure:"backend-url"`
UILogo string `mapstructure:"ui-logo"`
UIMessage string `mapstructure:"ui-message"`
UIColor string `mapstructure:"ui-color"`
UIPath string `mapstructure:"ui-path"`
DataPath string `mapstructure:"data-path"`
ConfigPath string `mapstructure:"config-path"`
CertPath string `mapstructure:"cert-path"`
Host string `mapstructure:"host"`
Prefix string `mapstructure:"prefix"`
Port string `mapstructure:"port"`
SecurePort string `mapstructure:"secure-port"`
PortMetrics int `mapstructure:"port-metrics"`
Hostname string `mapstructure:"hostname"`
H2C bool `mapstructure:"h2c"`
RandomDelay bool `mapstructure:"random-delay"`
RandomDelayUnit string `mapstructure:"random-delay-unit"`
RandomDelayMin int `mapstructure:"random-delay-min"`
RandomDelayMax int `mapstructure:"random-delay-max"`
RandomError bool `mapstructure:"random-error"`
Unhealthy bool `mapstructure:"unhealthy"`
Unready bool `mapstructure:"unready"`
JWTSecret string `mapstructure:"jwt-secret"`
CacheServer string `mapstructure:"cache-server"`
}
type Server struct {
router *mux.Router
logger *zap.Logger
config *Config
pool *redis.Pool
handler http.Handler
tracer trace.Tracer
tracerProvider *sdktrace.TracerProvider
}
func NewServer(config *Config, logger *zap.Logger) (*Server, error) {
config.Prefix = normalizePrefix(config.Prefix)
docs.SwaggerInfo.BasePath = config.Prefix
srv := &Server{
router: mux.NewRouter(),
logger: logger,
config: config,
}
return srv, nil
}
func normalizePrefix(prefix string) string {
prefix = strings.TrimSpace(prefix)
if prefix == "" || prefix == "/" {
return "/"
}
if !strings.HasPrefix(prefix, "/") {
prefix = "/" + prefix
}
prefix = path.Clean(prefix)
if prefix == "." || prefix == "" {
return "/"
}
return prefix
}
func (s *Server) prefixedPath(route string) string {
prefix := s.config.Prefix
if prefix == "" || prefix == "/" {
return route
}
if route == "/" {
return prefix
}
if strings.HasPrefix(route, "/") {
return prefix + route
}
return prefix + "/" + route
}
func (s *Server) registerHandlers() {
rootPath := s.prefixedPath("/")
s.router.Handle(s.prefixedPath("/metrics"), promhttp.Handler())
s.router.PathPrefix(s.prefixedPath("/debug/pprof/")).Handler(http.DefaultServeMux)
s.router.HandleFunc(rootPath, s.indexHandler).HeadersRegexp("User-Agent", "^Mozilla.*").Methods("GET")
s.router.HandleFunc(rootPath, s.infoHandler).Methods("GET")
if rootPath != "/" {
s.router.HandleFunc(rootPath+"/", s.indexHandler).HeadersRegexp("User-Agent", "^Mozilla.*").Methods("GET")
s.router.HandleFunc(rootPath+"/", s.infoHandler).Methods("GET")
}
s.router.HandleFunc(s.prefixedPath("/version"), s.versionHandler).Methods("GET")
s.router.HandleFunc(s.prefixedPath("/echo"), s.echoHandler)
s.router.PathPrefix(s.prefixedPath("/echo/")).HandlerFunc(s.echoHandler)
s.router.HandleFunc(s.prefixedPath("/env"), s.envHandler).Methods("GET", "POST")
s.router.HandleFunc(s.prefixedPath("/headers"), s.echoHeadersHandler).Methods("GET", "POST")
s.router.HandleFunc(s.prefixedPath("/delay/{wait:[0-9]+}"), s.delayHandler).Methods("GET").Name("delay")
s.router.HandleFunc(s.prefixedPath("/healthz"), s.healthzHandler).Methods("GET")
s.router.HandleFunc(s.prefixedPath("/readyz"), s.readyzHandler).Methods("GET")
s.router.HandleFunc(s.prefixedPath("/readyz/enable"), s.enableReadyHandler).Methods("POST")
s.router.HandleFunc(s.prefixedPath("/readyz/disable"), s.disableReadyHandler).Methods("POST")
s.router.HandleFunc(s.prefixedPath("/panic"), s.panicHandler).Methods("GET")
s.router.HandleFunc(s.prefixedPath("/status/{code:[0-9]+}"), s.statusHandler).Methods("GET", "POST", "PUT").Name("status")
s.router.HandleFunc(s.prefixedPath("/store"), s.storeWriteHandler).Methods("POST", "PUT")
s.router.HandleFunc(s.prefixedPath("/store/{hash}"), s.storeReadHandler).Methods("GET").Name("store")
s.router.HandleFunc(s.prefixedPath("/cache/{key}"), s.cacheWriteHandler).Methods("POST", "PUT")
s.router.HandleFunc(s.prefixedPath("/cache/{key}"), s.cacheDeleteHandler).Methods("DELETE")
s.router.HandleFunc(s.prefixedPath("/cache/{key}"), s.cacheReadHandler).Methods("GET").Name("cache")
s.router.HandleFunc(s.prefixedPath("/configs"), s.configReadHandler).Methods("GET")
s.router.HandleFunc(s.prefixedPath("/token"), s.tokenGenerateHandler).Methods("POST")
s.router.HandleFunc(s.prefixedPath("/token/validate"), s.tokenValidateHandler).Methods("GET")
s.router.HandleFunc(s.prefixedPath("/api/info"), s.infoHandler).Methods("GET")
s.router.HandleFunc(s.prefixedPath("/api/echo"), s.echoHandler)
s.router.PathPrefix(s.prefixedPath("/api/echo/")).HandlerFunc(s.echoHandler)
s.router.HandleFunc(s.prefixedPath("/ws/echo"), s.echoWsHandler)
s.router.HandleFunc(s.prefixedPath("/chunked"), s.chunkedHandler)
s.router.HandleFunc(s.prefixedPath("/chunked/{wait:[0-9]+}"), s.chunkedHandler)
s.router.PathPrefix(s.prefixedPath("/swagger/")).Handler(httpSwagger.Handler(
httpSwagger.URL(s.prefixedPath("/swagger/doc.json")),
))
s.router.HandleFunc(s.prefixedPath("/swagger.json"), func(w http.ResponseWriter, r *http.Request) {
doc, err := swag.ReadDoc()
if err != nil {
s.logger.Error("swagger error", zap.Error(err), zap.String("path", s.prefixedPath("/swagger.json")))
}
w.Write([]byte(doc))
})
}
func (s *Server) registerMiddlewares() {
prom := NewPrometheusMiddleware()
s.router.Use(prom.Handler)
otel := NewOpenTelemetryMiddleware()
s.router.Use(otel)
httpLogger := NewLoggingMiddleware(s.logger)
s.router.Use(httpLogger.Handler)
s.router.Use(versionMiddleware)
if s.config.RandomDelay {
randomDelayer := NewRandomDelayMiddleware(s.config.RandomDelayMin, s.config.RandomDelayMax, s.config.RandomDelayUnit)
s.router.Use(randomDelayer.Handler)
}
if s.config.RandomError {
s.router.Use(randomErrorMiddleware)
}
}
func (s *Server) ListenAndServe() (*http.Server, *http.Server, *int32, *int32) {
ctx := context.Background()
go s.startMetricsServer()
s.initTracer(ctx)
s.registerHandlers()
s.registerMiddlewares()
if s.config.H2C {
s.handler = h2c.NewHandler(s.router, &http2.Server{})
} else {
s.handler = s.router
}
//s.printRoutes()
// load configs in memory and start watching for changes in the config dir
if stat, err := os.Stat(s.config.ConfigPath); err == nil && stat.IsDir() {
var err error
watcher, err = fscache.NewWatch(s.config.ConfigPath)
if err != nil {
s.logger.Error("config watch error", zap.Error(err), zap.String("path", s.config.ConfigPath))
} else {
watcher.Watch()
}
}
// start redis connection pool
ticker := time.NewTicker(30 * time.Second)
s.startCachePool(ticker)
// create the http server
srv := s.startServer()
// create the secure server
secureSrv := s.startSecureServer()
// signal Kubernetes the server is ready to receive traffic
if !s.config.Unhealthy {
atomic.StoreInt32(&healthy, 1)
}
if !s.config.Unready {
atomic.StoreInt32(&ready, 1)
}
return srv, secureSrv, &healthy, &ready
}
func (s *Server) startServer() *http.Server {
// determine if the port is specified
if s.config.Port == "0" {
// move on immediately
return nil
}
srv := &http.Server{
Addr: s.config.Host + ":" + s.config.Port,
WriteTimeout: s.config.HttpServerTimeout,
ReadTimeout: s.config.HttpServerTimeout,
IdleTimeout: 2 * s.config.HttpServerTimeout,
Handler: s.handler,
}
// start the server in the background
go func() {
s.logger.Info("Starting HTTP Server.", zap.String("addr", srv.Addr))
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
s.logger.Fatal("HTTP server crashed", zap.Error(err))
}
}()
// return the server and routine
return srv
}
func (s *Server) startSecureServer() *http.Server {
// determine if the port is specified
if s.config.SecurePort == "0" {
// move on immediately
return nil
}
srv := &http.Server{
Addr: s.config.Host + ":" + s.config.SecurePort,
WriteTimeout: s.config.HttpServerTimeout,
ReadTimeout: s.config.HttpServerTimeout,
IdleTimeout: 2 * s.config.HttpServerTimeout,
Handler: s.handler,
}
cert := path.Join(s.config.CertPath, "tls.crt")
key := path.Join(s.config.CertPath, "tls.key")
// start the server in the background
go func() {
s.logger.Info("Starting HTTPS Server.", zap.String("addr", srv.Addr))
if err := srv.ListenAndServeTLS(cert, key); err != http.ErrServerClosed {
s.logger.Fatal("HTTPS server crashed", zap.Error(err))
}
}()
// return the server
return srv
}
func (s *Server) startMetricsServer() {
if s.config.PortMetrics > 0 {
mux := http.DefaultServeMux
mux.Handle("/metrics", promhttp.Handler())
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
srv := &http.Server{
Addr: fmt.Sprintf(":%v", s.config.PortMetrics),
Handler: mux,
}
srv.ListenAndServe()
}
}
func (s *Server) printRoutes() {
s.router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
pathTemplate, err := route.GetPathTemplate()
if err == nil {
fmt.Println("ROUTE:", pathTemplate)
}
pathRegexp, err := route.GetPathRegexp()
if err == nil {
fmt.Println("Path regexp:", pathRegexp)
}
queriesTemplates, err := route.GetQueriesTemplates()
if err == nil {
fmt.Println("Queries templates:", strings.Join(queriesTemplates, ","))
}
queriesRegexps, err := route.GetQueriesRegexp()
if err == nil {
fmt.Println("Queries regexps:", strings.Join(queriesRegexps, ","))
}
methods, err := route.GetMethods()
if err == nil {
fmt.Println("Methods:", strings.Join(methods, ","))
}
fmt.Println()
return nil
})
}
type ArrayResponse []string
type MapResponse map[string]string