// Package server provides a generic http server and metrics server with overridable // implementations for Kubernetes probes package server import ( "context" "fmt" "net" "net/http" "sync" "time" "github.com/gin-gonic/gin" "github.com/prometheus/client_golang/prometheus/promhttp" ) func init() { gin.SetMode(gin.ReleaseMode) gin.DisableConsoleColor() } // Handler is a function that can handle a request from a gin server. type Handler func(c *gin.Context) // Config contains unexported configuration parameters for a server. // The fields are set using Option functions. type Config struct { port int listener net.Listener handlers map[string]Handler secure bool secureCertFile string secureKeyFile string } // Validate checks that the config does not contain conflicting options. func (c Config) Validate() error { if c.port != 0 && c.listener != nil { return fmt.Errorf("OptionPort and OptionListener cannot both be set") } if c.secure && c.listener != nil { return fmt.Errorf("OptionSecure can not be set with OptionListener") } return nil } // Addr creates the http.Server's Addr. func (c Config) Addr() string { if c.port == 0 { if c.secure { return ":443" } return ":80" } return fmt.Sprintf(":%d", c.port) } // Server represents the HTTP server. type Server struct { Router *gin.Engine Config Config // shutdownFunc is set to an http.Server.Shutdown function. shutdownFunc func(context.Context) error } // NewServer creates an http server that runs with the desired options. // // To include a prometheus "/metrics" endpoint, provide the OptionMetrics option. // It uses the "github.com/prometheus/client_golang/prometheus/promhttp" package to serve your scrapable prometheus metrics. // // If the OptionPort and OptionListener options are omitted, then the default addr ":80" or ":443" will be used, // depending on the secure option. func NewServer(options ...Option) (*Server, error) { var s Server s.Router = gin.New() for _, option := range options { if err := option(&s.Config); err != nil { return nil, err } } if err := s.Config.Validate(); err != nil { return nil, err } for path, h := range s.Config.handlers { s.Router.GET(path, gin.HandlerFunc(h)) } return &s, nil } // NewHealthServer creates a server that serves 200 OK on the endpoint "/healthz" func NewHealthServer(opts ...Option) (*Server, error) { var h = func(c *gin.Context) { c.JSON(http.StatusOK, "UP") } // copy for no side affects on passed in slice. var alloptscopy = []Option{ OptionHandler("/healthz", h), } alloptscopy = append(alloptscopy, opts...) return NewServer(alloptscopy...) } // NewReadyServer serves http.StatusServiceUnavailable on "/readyz" until the returned `ready` function is called, then it serves 200 OK. func NewReadyServer(opts ...Option) (s *Server, ready func(), err error) { var ch = make(chan bool) ready = func() { close(ch) } var h = func(c *gin.Context) { select { case <-ch: // when the channel is closed, then this case will always run. c.JSON(http.StatusOK, "READY") default: c.JSON(http.StatusServiceUnavailable, "NOT READY") } } // copy for no side affects on passed in slice. var alloptscopy = []Option{ OptionHandler("/readyz", h), } alloptscopy = append(alloptscopy, opts...) s, err = NewServer(alloptscopy...) if err != nil { close(ch) return nil, nil, err } return s, ready, err } var PrometheusMetricsHandler = gin.WrapH(promhttp.Handler()) // NewMetricsServer serves prometheus metrics on the "/metrics" endpoint. // // To add your own metrics, use the prometheus client_golang libraries as normal, and prometheus will wire them up for you. func NewMetricsServer(opts ...Option) (*Server, error) { var oh = OptionHandler("/metrics", PrometheusMetricsHandler) s, ready, err := NewReadyServer(append([]Option{oh}, opts...)...) if err != nil { return nil, err } ready() return s, err } // NewLivenessServer serves http.StatusServiceUnavailable on "/livez" until the `setLivenessStatus` function is called. // After calling the `setLivenessStatus` function, it serves whatever status code you pass in. // The `setLivenessStatus` function can be called more than once. func NewLivenessServer(opts ...Option) (s *Server, setLivenessStatus func(code int), err error) { // liveValue holds the livez status in a threadsafe manner. var liveValue struct { sync.Mutex code int } // this func safely sets the liveValue fields after locking. var set = func(code int) { liveValue.Lock() liveValue.code = code liveValue.Unlock() } // this handler safely returns the liveValue values. var h = func(c *gin.Context) { liveValue.Lock() var code = liveValue.code liveValue.Unlock() // Default to status http.StatusServiceUnavailable if code == 0 { code = http.StatusServiceUnavailable } if code >= 400 { c.JSON(code, "DOWN") } else { c.JSON(code, "LIVE") } } // copy for no side affects on passed in slice. var alloptscopy = []Option{ OptionHandler("/livez", h), } alloptscopy = append(alloptscopy, opts...) s, err = NewServer(alloptscopy...) if err != nil { return nil, nil, err } return s, set, nil } // ReadHeaderTimeout needs to be set to preven a "slowloris attack" according to the linter gosec. // // This is a public var so that consumers of this library can modify it before calling Run. var ReadHeaderTimeout = time.Minute // Run runs the Gin Router on the configured port or listener. func (s *Server) Run() error { // Serve on a net/http.Server at the desired location. var hs = &http.Server{ Handler: s.Router, Addr: s.Config.Addr(), ReadHeaderTimeout: ReadHeaderTimeout, } s.shutdownFunc = hs.Shutdown // Allows the gin server be shut down gracefully. if s.Config.listener != nil { // Serve on a custom listener return hs.Serve(s.Config.listener) } else if s.Config.secure { // Serve TLS on a tcp port. return hs.ListenAndServeTLS(s.Config.secureCertFile, s.Config.secureKeyFile) } // Serve insecure HTTP 1.1 on a tcp port. return hs.ListenAndServe() } // Shutdown stops the running server. See golang.org/pkg/net/http#Server.Shutdown func (s *Server) Shutdown(ctx context.Context) error { if s.shutdownFunc == nil { return nil } return s.shutdownFunc(ctx) }