- Pkg HTTP Server : add config option to disabled a server

- Pkg HTTP Server : add option to force TLS as mandatory to start and server
- Pkg HTTP Server : add function to listen server with multiple handler (map[key string]http.handler)
- Pkg HTTP Server : add option to define an handler key to associate one server with one handler
This commit is contained in:
Nicolas JUHEL
2021-03-04 11:14:31 +01:00
parent 838b1c0c98
commit e457f641d2
5 changed files with 231 additions and 21 deletions

View File

@@ -31,6 +31,7 @@ import (
"fmt" "fmt"
"net" "net"
"net/url" "net/url"
"strings"
"time" "time"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
@@ -132,6 +133,18 @@ type ServerConfig struct {
getTLSDefault func() libtls.TLSConfig getTLSDefault func() libtls.TLSConfig
getParentContext func() context.Context getParentContext func() context.Context
// Enabled allow to disable a server without clean his configuration
Disabled bool
// Mandatory defined if the component for status is mandatory or not
Mandatory bool
// TimeoutCacheInfo defined the validity time of cache for info (name, version, hash)
TimeoutCacheInfo time.Duration
// TimeoutCacheHealth defined the validity time of cache for healthcheck of this server
TimeoutCacheHealth time.Duration
/*** http options ***/ /*** http options ***/
// ReadTimeout is the maximum duration for reading the entire // ReadTimeout is the maximum duration for reading the entire
@@ -219,12 +232,44 @@ type ServerConfig struct {
// Expose is the address use to call this server. This can be allow to use a single fqdn to multiple server" // Expose is the address use to call this server. This can be allow to use a single fqdn to multiple server"
Expose string `mapstructure:"expose" json:"expose" yaml:"expose" toml:"expose" validate:"required,url"` Expose string `mapstructure:"expose" json:"expose" yaml:"expose" toml:"expose" validate:"required,url"`
// HandlerKeys is an options to associate current server with a specifc handler defined by the key
// This allow to defined multiple server in only one config for different handler to start multiple api
HandlerKeys string
// TLSMandatory is a flag to defined that TLS must be valid to start current server.
TLSMandatory bool
// TLS is the tls configuration for this server. // TLS is the tls configuration for this server.
// To allow tls on this server, at least the TLS Config option InheritDefault must be at true and the default TLS config must be set. // To allow tls on this server, at least the TLS Config option InheritDefault must be at true and the default TLS config must be set.
// If you don't want any tls config, just omit or set an empty struct. // If you don't want any tls config, just omit or set an empty struct.
TLS libtls.Config `mapstructure:"tls" json:"tls" yaml:"tls" toml:"tls"` TLS libtls.Config `mapstructure:"tls" json:"tls" yaml:"tls" toml:"tls"`
} }
func (c *ServerConfig) Clone() ServerConfig {
return ServerConfig{
Disabled: c.Disabled,
getTLSDefault: c.getTLSDefault,
getParentContext: c.getParentContext,
ReadTimeout: c.ReadTimeout,
ReadHeaderTimeout: c.ReadHeaderTimeout,
WriteTimeout: c.WriteTimeout,
MaxHeaderBytes: c.MaxHeaderBytes,
MaxHandlers: c.MaxHandlers,
MaxConcurrentStreams: c.MaxConcurrentStreams,
MaxReadFrameSize: c.MaxReadFrameSize,
PermitProhibitedCipherSuites: c.PermitProhibitedCipherSuites,
IdleTimeout: c.IdleTimeout,
MaxUploadBufferPerConnection: c.MaxUploadBufferPerConnection,
MaxUploadBufferPerStream: c.MaxUploadBufferPerStream,
Name: c.Name,
Listen: c.Listen,
Expose: c.Expose,
HandlerKeys: strings.ToLower(c.HandlerKeys),
TLSMandatory: c.TLSMandatory,
TLS: c.TLS,
}
}
func (c *ServerConfig) SetDefaultTLS(f func() libtls.TLSConfig) { func (c *ServerConfig) SetDefaultTLS(f func() libtls.TLSConfig) {
c.getTLSDefault = f c.getTLSDefault = f
} }
@@ -317,6 +362,10 @@ func (c ServerConfig) GetExpose() *url.URL {
return add return add
} }
func (c ServerConfig) GetHandlerKey() string {
return c.HandlerKeys
}
func (c ServerConfig) Validate() liberr.Error { func (c ServerConfig) Validate() liberr.Error {
val := validator.New() val := validator.New()
err := val.Struct(c) err := val.Struct(c)

View File

@@ -28,6 +28,7 @@ package httpserver
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
@@ -35,6 +36,8 @@ import (
"strings" "strings"
"syscall" "syscall"
"github.com/nabbar/golib/status"
"github.com/nabbar/golib/logger" "github.com/nabbar/golib/logger"
"github.com/nabbar/golib/semaphore" "github.com/nabbar/golib/semaphore"
@@ -45,7 +48,8 @@ import (
type FieldType uint8 type FieldType uint8
const ( const (
FieldName FieldType = iota HandlerDefault = "default"
FieldName FieldType = iota
FieldBind FieldBind
FieldExpose FieldExpose
) )
@@ -72,8 +76,13 @@ type PoolServer interface {
WaitNotify(ctx context.Context, cancel context.CancelFunc) WaitNotify(ctx context.Context, cancel context.CancelFunc)
Listen(handler http.Handler) liberr.Error Listen(handler http.Handler) liberr.Error
ListenMultiHandler(handler map[string]http.Handler) liberr.Error
Restart() Restart()
Shutdown() Shutdown()
StatusInfo(bindAddress string) (name string, release string, hash string)
StatusHealth(bindAddress string) error
StatusRoute(prefix string, fctMessage status.FctMessage, sts status.RouteStatus)
} }
func NewPool(srv ...Server) PoolServer { func NewPool(srv ...Server) PoolServer {
@@ -333,6 +342,10 @@ func (p pool) WaitNotify(ctx context.Context, cancel context.CancelFunc) {
} }
func (p pool) Listen(handler http.Handler) liberr.Error { func (p pool) Listen(handler http.Handler) liberr.Error {
return p.ListenMultiHandler(map[string]http.Handler{HandlerDefault: handler})
}
func (p pool) ListenMultiHandler(handler map[string]http.Handler) liberr.Error {
if p.Len() < 1 { if p.Len() < 1 {
return nil return nil
} }
@@ -341,9 +354,23 @@ func (p pool) Listen(handler http.Handler) liberr.Error {
e = ErrorPoolListen.Error(nil) e = ErrorPoolListen.Error(nil)
logger.InfoLevel.Log("Calling listen for All Servers") logger.InfoLevel.Log("Calling listen for All Servers")
p.MapRun(func(srv Server) { p.MapRun(func(srv Server) {
e.AddParentError(srv.Listen(handler)) if len(handler) < 1 {
e.AddParentError(srv.Listen(nil))
} else {
for k := range handler {
if len(handler) == 1 {
e.AddParentError(srv.Listen(handler[k]))
break
} else if strings.ToLower(k) == srv.GetHandlerKey() {
e.AddParentError(srv.Listen(handler[k]))
break
}
}
}
}) })
logger.InfoLevel.Log("End of Calling listen for All Servers") logger.InfoLevel.Log("End of Calling listen for All Servers")
if !e.HasParent() { if !e.HasParent() {
@@ -412,3 +439,26 @@ func (p pool) Shutdown() {
_ = s.WaitAll() _ = s.WaitAll()
} }
func (p pool) StatusInfo(bindAddress string) (name string, release string, hash string) {
if s := p.Get(bindAddress); s != nil {
return s.StatusInfo()
}
return fmt.Sprintf("missing server '%s'", bindAddress), "", ""
}
func (p pool) StatusHealth(bindAddress string) error {
if s := p.Get(bindAddress); s != nil {
return s.StatusHealth()
}
return fmt.Errorf("missing server '%s'", bindAddress)
}
func (p pool) StatusRoute(keyPrefix string, fctMessage status.FctMessage, sts status.RouteStatus) {
p.MapRun(func(srv Server) {
bind := srv.GetBindable()
sts.ComponentNew(fmt.Sprintf("%s-%s", keyPrefix, bind), srv.StatusComponent(fctMessage))
})
}

View File

@@ -30,6 +30,7 @@ package httpserver
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"log" "log"
"net" "net"
"net/http" "net/http"
@@ -45,6 +46,7 @@ import (
) )
type srvRun struct { type srvRun struct {
err *atomic.Value
run *atomic.Value run *atomic.Value
snm string snm string
srv *http.Server srv *http.Server
@@ -54,6 +56,7 @@ type srvRun struct {
type run interface { type run interface {
IsRunning() bool IsRunning() bool
GetError() error
WaitNotify() WaitNotify()
Listen(cfg *ServerConfig, handler http.Handler) liberr.Error Listen(cfg *ServerConfig, handler http.Handler) liberr.Error
Restart(cfg *ServerConfig) Restart(cfg *ServerConfig)
@@ -62,6 +65,7 @@ type run interface {
func newRun() run { func newRun() run {
return &srvRun{ return &srvRun{
err: new(atomic.Value),
run: new(atomic.Value), run: new(atomic.Value),
srv: nil, srv: nil,
} }
@@ -87,7 +91,37 @@ func (s *srvRun) IsRunning() bool {
return s.getRunning() return s.getRunning()
} }
func (s *srvRun) getErr() error {
if s.err == nil {
return nil
} else if i := s.err.Load(); i == nil {
return nil
} else if e, ok := i.(error); !ok {
return nil
} else if e.Error() == "" {
return nil
} else {
return e
}
}
func (s *srvRun) setErr(e error) {
if e != nil {
s.err.Store(e)
} else {
s.err.Store(errors.New(""))
}
}
func (s *srvRun) GetError() error {
return s.getErr()
}
func (s *srvRun) WaitNotify() { func (s *srvRun) WaitNotify() {
if !s.IsRunning() {
return
}
// Wait for interrupt signal to gracefully shutdown the server with // Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds. // a timeout of 5 seconds.
quit := make(chan os.Signal, 1) quit := make(chan os.Signal, 1)
@@ -113,6 +147,7 @@ func (s *srvRun) Listen(cfg *ServerConfig, handler http.Handler) liberr.Error {
return err return err
} }
sTls := cfg.TLSMandatory
bind := cfg.GetListen().Host bind := cfg.GetListen().Host
name := cfg.Name name := cfg.Name
if name == "" { if name == "" {
@@ -181,6 +216,7 @@ func (s *srvRun) Listen(cfg *ServerConfig, handler http.Handler) liberr.Error {
} }
if e := http2.ConfigureServer(srv, s2); e != nil { if e := http2.ConfigureServer(srv, s2); e != nil {
s.setErr(e)
return ErrorHTTP2Configure.ErrorParent(e) return ErrorHTTP2Configure.ErrorParent(e)
} }
@@ -206,7 +242,7 @@ func (s *srvRun) Listen(cfg *ServerConfig, handler http.Handler) liberr.Error {
s.snm = name s.snm = name
s.srv = srv s.srv = srv
go func(name, host string) { go func(name, host string, tlsMandatory bool) {
defer func() { defer func() {
if s.ctx != nil && s.cnl != nil && s.ctx.Err() == nil { if s.ctx != nil && s.cnl != nil && s.ctx.Err() == nil {
@@ -226,6 +262,8 @@ func (s *srvRun) Listen(cfg *ServerConfig, handler http.Handler) liberr.Error {
s.setRunning(true) s.setRunning(true)
err = s.srv.ListenAndServeTLS("", "") err = s.srv.ListenAndServeTLS("", "")
} else if tlsMandatory {
err = fmt.Errorf("missing valid server certificates")
} else { } else {
liblog.InfoLevel.Logf("Server '%s' is starting with bindable: %s", name, host) liblog.InfoLevel.Logf("Server '%s' is starting with bindable: %s", name, host)
@@ -238,9 +276,10 @@ func (s *srvRun) Listen(cfg *ServerConfig, handler http.Handler) liberr.Error {
} else if err != nil && errors.Is(err, http.ErrServerClosed) { } else if err != nil && errors.Is(err, http.ErrServerClosed) {
return return
} else if err != nil { } else if err != nil {
s.setErr(err)
liblog.ErrorLevel.LogErrorCtxf(liblog.NilLevel, "Listen Server '%s'", err, name) liblog.ErrorLevel.LogErrorCtxf(liblog.NilLevel, "Listen Server '%s'", err, name)
} }
}(name, bind) }(name, bind, sTls)
return nil return nil
} }

View File

@@ -27,11 +27,15 @@
package httpserver package httpserver
import ( import (
"context" "fmt"
"net/http" "net/http"
"runtime"
"strings"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/nabbar/golib/status"
liberr "github.com/nabbar/golib/errors" liberr "github.com/nabbar/golib/errors"
) )
@@ -42,8 +46,7 @@ const (
type server struct { type server struct {
run *atomic.Value run *atomic.Value
cfg *ServerConfig cfg ServerConfig
cnl context.CancelFunc
} }
type Server interface { type Server interface {
@@ -53,6 +56,7 @@ type Server interface {
GetName() string GetName() string
GetBindable() string GetBindable() string
GetExpose() string GetExpose() string
GetHandlerKey() string
IsRunning() bool IsRunning() bool
IsTLS() bool IsTLS() bool
@@ -62,13 +66,16 @@ type Server interface {
Listen(handler http.Handler) liberr.Error Listen(handler http.Handler) liberr.Error
Restart() Restart()
Shutdown() Shutdown()
StatusInfo() (name string, release string, hash string)
StatusHealth() error
StatusComponent(message status.FctMessage) status.Component
} }
func NewServer(cfg *ServerConfig) Server { func NewServer(cfg *ServerConfig) Server {
return &server{ return &server{
cfg: cfg, cfg: cfg.Clone(),
run: new(atomic.Value), run: new(atomic.Value),
cnl: nil,
} }
} }
@@ -88,12 +95,20 @@ func (s *server) setRun(r run) {
s.run.Store(r) s.run.Store(r)
} }
func (s *server) getErr() error {
if r := s.getRun(); r == nil {
return nil
} else {
return r.GetError()
}
}
func (s *server) GetConfig() *ServerConfig { func (s *server) GetConfig() *ServerConfig {
return s.cfg return &s.cfg
} }
func (s *server) SetConfig(cfg *ServerConfig) { func (s *server) SetConfig(cfg *ServerConfig) {
s.cfg = cfg s.cfg = cfg.Clone()
} }
func (s server) GetName() string { func (s server) GetName() string {
@@ -112,6 +127,10 @@ func (s *server) GetExpose() string {
return s.cfg.GetExpose().String() return s.cfg.GetExpose().String()
} }
func (s *server) GetHandlerKey() string {
return s.cfg.GetHandlerKey()
}
func (s *server) IsRunning() bool { func (s *server) IsRunning() bool {
return s.getRun().IsRunning() return s.getRun().IsRunning()
} }
@@ -122,8 +141,9 @@ func (s *server) IsTLS() bool {
func (s *server) Listen(handler http.Handler) liberr.Error { func (s *server) Listen(handler http.Handler) liberr.Error {
r := s.getRun() r := s.getRun()
e := r.Listen(s.cfg, handler) e := r.Listen(&s.cfg, handler)
s.setRun(r) s.setRun(r)
return e return e
} }
@@ -150,3 +170,27 @@ func (s *server) Merge(srv Server) bool {
return false return false
} }
func (s *server) StatusInfo() (name string, release string, hash string) {
vers := strings.TrimLeft(runtime.Version(), "go")
vers = strings.TrimLeft(vers, "Go")
vers = strings.TrimLeft(vers, "GO")
return fmt.Sprintf("%s [%s]", s.GetName(), s.GetBindable()), vers, ""
}
func (s *server) StatusHealth() error {
if !s.cfg.Disabled && s.IsRunning() {
return nil
} else if s.cfg.Disabled {
return fmt.Errorf("server disabled")
} else if e := s.getErr(); e != nil {
return e
} else {
return fmt.Errorf("server is offline -- missing error")
}
}
func (s *server) StatusComponent(message status.FctMessage) status.Component {
return status.NewComponent(s.cfg.Mandatory, s.StatusInfo, s.StatusHealth, message, s.cfg.TimeoutCacheInfo, s.cfg.TimeoutCacheHealth)
}

View File

@@ -46,17 +46,27 @@ var tlsConfigSrv = libtls.Config{
} }
var cfgSrv01 = libsrv.ServerConfig{ var cfgSrv01 = libsrv.ServerConfig{
Name: "test-01", Name: "test-01",
Listen: "0.0.0.0:61001", Listen: "0.0.0.0:61001",
Expose: "0.0.0.0:61000", Expose: "0.0.0.0:61000",
TLS: tlsConfigSrv, TLSMandatory: false,
TLS: tlsConfigSrv,
} }
var cfgSrv02 = libsrv.ServerConfig{ var cfgSrv02 = libsrv.ServerConfig{
Name: "test-02", Name: "test-02",
Listen: "0.0.0.0:61002", Listen: "0.0.0.0:61002",
Expose: "0.0.0.0:61000", Expose: "0.0.0.0:61000",
TLS: tlsConfigSrv, TLSMandatory: false,
TLS: tlsConfigSrv,
}
var cfgSrv03 = libsrv.ServerConfig{
Name: "test-03",
Listen: "0.0.0.0:61003",
Expose: "0.0.0.0:61000",
TLSMandatory: true,
TLS: tlsConfigSrv,
} }
var ( var (
@@ -77,7 +87,7 @@ func init() {
ctx, cnl = context.WithCancel(context.Background()) ctx, cnl = context.WithCancel(context.Background())
cfgPool = libsrv.PoolServerConfig{cfgSrv01, cfgSrv02} cfgPool = libsrv.PoolServerConfig{cfgSrv01, cfgSrv02, cfgSrv03}
cfgPool.MapUpdate(func(cfg libsrv.ServerConfig) libsrv.ServerConfig { cfgPool.MapUpdate(func(cfg libsrv.ServerConfig) libsrv.ServerConfig {
cfg.SetParentContext(func() context.Context { cfg.SetParentContext(func() context.Context {
return ctx return ctx
@@ -112,6 +122,24 @@ func main() {
go pool.WaitNotify(ctx, cnl) go pool.WaitNotify(ctx, cnl)
go func() {
for {
time.Sleep(5 * time.Second)
if ctx.Err() != nil {
return
}
pool.MapRun(func(srv libsrv.Server) {
n, v, _ := srv.StatusInfo()
if e := srv.StatusHealth(); e != nil {
fmt.Printf("%s - %s : %v\n", n, v, e)
} else {
fmt.Printf("%s - %s : OK\n", n, v)
}
})
}
}()
var i = 0 var i = 0
for { for {