From 0df2782a8160524bec54a8a75171ecb604d22cb8 Mon Sep 17 00:00:00 2001 From: Henry Barreto Date: Mon, 16 Dec 2024 14:49:26 -0300 Subject: [PATCH] chore(api,pkg): create generic server package for services --- api/go.mod | 1 - api/routes/handler.go | 10 +++-- api/routes/routes.go | 15 +++++-- api/server.go | 70 +++++++++------------------------ go.mod | 46 ++++++++++++++-------- pkg/server/echo.go | 57 +++++++++++++++++++++++++++ pkg/server/server.go | 91 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 214 insertions(+), 76 deletions(-) create mode 100644 pkg/server/echo.go create mode 100644 pkg/server/server.go diff --git a/api/go.mod b/api/go.mod index d9803828b67..e986513f11d 100644 --- a/api/go.mod +++ b/api/go.mod @@ -124,7 +124,6 @@ require ( golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.8.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/api/routes/handler.go b/api/routes/handler.go index a1a2598f1ef..5f4498bf662 100644 --- a/api/routes/handler.go +++ b/api/routes/handler.go @@ -1,13 +1,17 @@ package routes import ( - svc "github.com/shellhub-io/shellhub/api/services" + "github.com/shellhub-io/shellhub/api/services" ) type Handler struct { - service svc.Service + service services.Service } -func NewHandler(s svc.Service) *Handler { +func (h *Handler) GetService() any { + return h.service +} + +func NewHandler(s services.Service) *Handler { return &Handler{service: s} } diff --git a/api/routes/routes.go b/api/routes/routes.go index 5e1ee8ed54a..566bcf7a6fa 100644 --- a/api/routes/routes.go +++ b/api/routes/routes.go @@ -48,7 +48,14 @@ func NewRouter(service services.Service, opts ...Option) *echo.Echo { } } - // Internal routes only accessible by other services in the local container network + APIInternalRoutes(e, handler) + APIPublicRoutes(e, handler) + + return e +} + +// APIInternalRoutes sets to echo instance all internal routes for the service. +func APIInternalRoutes(e *echo.Echo, handler *Handler) { internalAPI := e.Group("/internal") internalAPI.GET(AuthRequestURL, gateway.Handler(handler.AuthRequest)) @@ -68,8 +75,10 @@ func NewRouter(service services.Service, opts ...Option) *echo.Echo { internalAPI.POST(CreatePrivateKeyURL, gateway.Handler(handler.CreatePrivateKey)) internalAPI.POST(EvaluateKeyURL, gateway.Handler(handler.EvaluateKey)) internalAPI.POST(EventsSessionsURL, gateway.Handler(handler.EventSession)) +} - // Public routes for external access through API gateway +// APIPublicRoutes sets to echo instance all public routes for the service. +func APIPublicRoutes(e *echo.Echo, handler *Handler) { publicAPI := e.Group("/api") publicAPI.GET(HealthCheckURL, gateway.Handler(handler.EvaluateHealth)) @@ -147,6 +156,4 @@ func NewRouter(service services.Service, opts ...Option) *echo.Echo { "/api/containers?*": "/api/devices?$1&connector=true", "/api/containers/*": "/api/devices/$1", })) - - return e } diff --git a/api/server.go b/api/server.go index 9f680544530..2b672b08cff 100644 --- a/api/server.go +++ b/api/server.go @@ -2,12 +2,10 @@ package main import ( "context" - "errors" "os" "os/signal" "syscall" - "github.com/getsentry/sentry-go" "github.com/shellhub-io/shellhub/api/routes" "github.com/shellhub-io/shellhub/api/services" "github.com/shellhub-io/shellhub/api/store" @@ -16,6 +14,7 @@ import ( "github.com/shellhub-io/shellhub/pkg/api/internalclient" storecache "github.com/shellhub-io/shellhub/pkg/cache" "github.com/shellhub-io/shellhub/pkg/geoip/geolite2" + "github.com/shellhub-io/shellhub/pkg/server" "github.com/shellhub-io/shellhub/pkg/worker/asynq" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -71,7 +70,7 @@ var serverCmd = &cobra.Command{ cancel() }() - return startServer(ctx, cfg, store, cache) + return Server(ctx, cfg, store, cache) }, } @@ -122,33 +121,7 @@ type config struct { GeoipMaxmindLicense string `env:"MAXMIND_LICENSE,default="` } -// startSentry initializes the Sentry client. -// -// The Sentry client is used to report errors to the Sentry server, and is initialized only if the `SHELLHUB_SENTRY_DSN` -// environment variable is set. Else, the function returns a error with a not initialized Sentry client. -func startSentry(dsn string) (*sentry.Client, error) { - if dsn != "" { - var err error - reporter, err := sentry.NewClient(sentry.ClientOptions{ //nolint:exhaustruct - Dsn: dsn, - Release: os.Getenv("SHELLHUB_VERSION"), - EnableTracing: true, - TracesSampleRate: 1, - }) - if err != nil { - log.WithError(err).Error("Failed to create Sentry client") - - return nil, err - } - log.Info("Sentry client started") - - return reporter, nil - } - - return nil, errors.New("sentry DSN not provided") -} - -func startServer(ctx context.Context, cfg *config, store store.Store, cache storecache.Cache) error { +func Server(ctx context.Context, cfg *config, store store.Store, cache storecache.Cache) error { log.Info("Starting API server") apiClient, err := internalclient.NewClient(internalclient.WithAsynqWorker(cfg.RedisURI)) @@ -181,21 +154,6 @@ func startServer(ctx context.Context, cfg *config, store store.Store, cache stor service := services.NewService(store, nil, nil, cache, apiClient, servicesOptions...) - routerOptions := []routes.Option{} - - if cfg.SentryDSN != "" { - log.Info("Sentry report is enabled") - - reporter, err := startSentry(cfg.SentryDSN) - if err != nil { - log.WithField("DSN", cfg.SentryDSN).WithError(err).Warn("Failed to start Sentry") - } else { - log.Info("Sentry client started") - } - - routerOptions = append(routerOptions, routes.WithReporter(reporter)) - } - worker := asynq.NewServer( cfg.RedisURI, asynq.BatchConfig(cfg.AsynqGroupMaxSize, cfg.AsynqGroupMaxDelay, int(cfg.AsynqGroupGracePeriod)), @@ -209,19 +167,29 @@ func startServer(ctx context.Context, cfg *config, store store.Store, cache stor Fatal("failed to start the worker") } - router := routes.NewRouter(service, routerOptions...) - go func() { <-ctx.Done() log.Debug("Closing HTTP server due context cancellation") worker.Shutdown() - router.Close() }() - err = router.Start(":8080") //nolint:errcheck - log.WithError(err).Info("HTTP server closed") + handler := routes.NewHandler(service) + + routes := []server.Route[*routes.Handler]{ + routes.APIInternalRoutes, + routes.APIPublicRoutes, + } + + options := []server.Option{ + // NOTE: Now, when something was customized on the HTTP server based on some configuration, a new closure + // should be created on this slice, simplifying and centralizing the HTTP options. In this case, when + // Sentry monitoring is enabled, we set a "reporter" for the global error handler. + server.SentryOption(cfg.SentryDSN), + } - return nil + return server. + NewDefaultServer(ctx, handler, nil, routes, options). + Listen() } diff --git a/go.mod b/go.mod index fb99717bc38..7c2611d2d04 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/adhocore/gronx v1.8.1 github.com/creack/pty v1.1.18 github.com/docker/docker v27.1.1+incompatible + github.com/getsentry/sentry-go v0.28.1 github.com/gliderlabs/ssh v0.3.5 github.com/go-playground/assert/v2 v2.2.0 github.com/go-playground/validator/v10 v10.11.2 @@ -19,7 +20,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/hibiken/asynq v0.24.1 github.com/jarcoal/httpmock v1.3.1 - github.com/labstack/echo/v4 v4.10.2 + github.com/labstack/echo/v4 v4.12.0 github.com/mattn/go-shellwords v1.0.12 github.com/mholt/archiver/v4 v4.0.0-alpha.8 github.com/openwall/yescrypt-go v1.0.0 @@ -27,6 +28,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/pkg/sftp v1.13.5 github.com/sethvargo/go-envconfig v0.9.0 + github.com/shellhub-io/shellhub/api v0.16.3 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 github.com/testcontainers/testcontainers-go/modules/redis v0.32.0 @@ -37,16 +39,17 @@ require ( require ( dario.cat/mergo v1.0.0 // indirect - github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/Microsoft/hcsshim v0.11.5 // indirect - github.com/andybalholm/brotli v1.0.5 // indirect + github.com/Microsoft/hcsshim v0.12.2 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/bodgit/plumbing v1.2.0 // indirect github.com/bodgit/sevenzip v1.3.0 // indirect github.com/bodgit/windows v1.0.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 // indirect github.com/connesc/cipherio v0.2.1 // indirect github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/errdefs v0.1.0 // indirect @@ -61,28 +64,30 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/klauspost/compress v1.17.4 // indirect + github.com/klauspost/compress v1.17.8 // indirect github.com/klauspost/pgzip v1.2.5 // indirect github.com/kr/fs v0.1.0 // indirect - github.com/labstack/gommon v0.4.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect github.com/leodido/go-urn v1.2.2 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/lufia/plan9stats v0.0.0-20240408141607-282e7b5d6b74 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/sequential v0.5.0 // indirect github.com/moby/sys/user v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect @@ -90,25 +95,32 @@ require ( github.com/oschwald/maxminddb-golang v1.10.0 // indirect github.com/pierrec/lz4/v4 v4.1.17 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/redis/go-redis/v9 v9.0.3 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect - github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shirou/gopsutil/v3 v3.24.3 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/spf13/cast v1.3.1 // indirect + github.com/square/mongo-lock v0.0.0-20230808145049-cfcf499f6bf0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/testcontainers/testcontainers-go v0.32.0 // indirect github.com/therootcompany/xz v1.0.1 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect + github.com/tklauser/go-sysconf v0.3.13 // indirect + github.com/tklauser/numcpus v0.7.0 // indirect github.com/ulikunitz/xz v0.5.11 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/vmihailenco/go-tinylfu v0.2.2 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/yusufpapurcu/wmi v1.2.3 // indirect + github.com/xakep666/mongo-migrate v0.3.2 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.mongodb.org/mongo-driver v1.16.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect go.opentelemetry.io/otel v1.26.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 // indirect @@ -116,10 +128,10 @@ require ( go.opentelemetry.io/otel/sdk v1.26.0 // indirect go.opentelemetry.io/otel/trace v1.26.0 // indirect go4.org v0.0.0-20200411211856-f5505b9728dd // indirect - golang.org/x/net v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.3.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect google.golang.org/grpc v1.63.2 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/pkg/server/echo.go b/pkg/server/echo.go new file mode 100644 index 00000000000..7cc54e79f50 --- /dev/null +++ b/pkg/server/echo.go @@ -0,0 +1,57 @@ +package server + +import ( + "os" + + "github.com/getsentry/sentry-go" + "github.com/labstack/echo/v4" + "github.com/shellhub-io/shellhub/api/pkg/echo/handlers" +) + +// Echo is a wrapper around the Echo HTTP server with simplified lifecycle management. +type Echo struct { + echo *echo.Echo +} + +var _ Server[*echo.Echo] = new(Echo) + +// Underlying returns the underlying HTTP server. +func (s *Echo) Underlying() *echo.Echo { + return s.echo +} + +// Close closes the server. +func (s *Echo) Close() error { + return s.echo.Close() +} + +// Start starts the server at a given address. +func (s *Echo) Start(addr string) error { + return s.echo.Start(addr) +} + +// Listen starts the HTTP server, listing for connections in [ServerListenDefaultAddress]. +func (s *Echo) Listen() error { + return s.echo.Start(ServerListenDefaultAddress) +} + +// SentryOption enables, if DSN is a valid value, the error reporter for a Sentry's server. +var SentryOption = func(dsn string) func(server *echo.Echo) { + return func(server *echo.Echo) { + if dsn != "" { + reporter, err := sentry.NewClient(sentry.ClientOptions{ //nolint:exhaustruct + Dsn: dsn, + Release: os.Getenv("SHELLHUB_VERSION"), + EnableTracing: true, + TracesSampleRate: 1, + }) + if err != nil { + server.HTTPErrorHandler = handlers.NewErrors(nil) + + return + } + + server.HTTPErrorHandler = handlers.NewErrors(reporter) + } + } +} diff --git a/pkg/server/server.go b/pkg/server/server.go new file mode 100644 index 00000000000..a3468814519 --- /dev/null +++ b/pkg/server/server.go @@ -0,0 +1,91 @@ +// Package server The server package provides a flexible and configurable HTTP server framework for ShellHub services. +// It offers a generic server creation mechanism with built-in middleware, error handling, and routing capabilities. +package server + +import ( + "context" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/shellhub-io/shellhub/api/pkg/echo/handlers" + "github.com/shellhub-io/shellhub/api/pkg/gateway" + pkgmiddleware "github.com/shellhub-io/shellhub/pkg/middleware" +) + +// ServerListenDefaultAddress is the default address used by the HTTP server to listen for connections. +const ServerListenDefaultAddress = ":8080" + +// Server is the interface that should be implemented to create a HTTP server for ShellHub services. +type Server[S any] interface { + // Underlying returns the underlying HTTP server used. + Underlying() S + // Close closes the server. + Close() error + // Start starts the server at a given address. + Start(addr string) error + // Listen starts the HTTP server, listing for connections in [ServerListenDefaultAddress]. + Listen() error +} + +// Route represents a loader of routes to the underlying HTTP server. +type Route[H any] func(server *echo.Echo, handler H) + +// Option is used to pass custom configurations to the underlying HTTP server. +type Option func(server *echo.Echo) + +type Handler[S any] interface { + GetService() S +} + +// NewDefaultServer uses [echo] to create a default HTTP server meet to be used as ShellHub services, aggregating the +// middlewares, binder, validator, environmental variables, features, and anything related to service providing. +func NewDefaultServer[H Handler[any]]( + ctx context.Context, + handler H, + middlewares []echo.MiddlewareFunc, + routes []Route[H], + options []Option, +) Server[*echo.Echo] { //nolint:whitespace + e := echo.New() + + e.Binder = handlers.NewBinder() + e.Validator = handlers.NewValidator() + e.HTTPErrorHandler = handlers.NewErrors(nil) + e.IPExtractor = echo.ExtractIPFromRealIPHeader() + + for _, option := range options { + option(e) + } + + // NOTE: As recommend by Echo's, this middleware should be registered before any other middleware, and custom + // context cannot be defined in a middleware before the router ran (Pre) + e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + ctx := gateway.NewContext(handler.GetService(), c) + + return next(ctx) + } + }) + e.Use(middleware.RequestID()) + e.Use(middleware.Secure()) + e.Use(pkgmiddleware.Log) + + if middlewares != nil { + e.Use(middlewares...) + } + + for _, route := range routes { + route(e, handler) + } + + // NOTE: When context received is done, we close the HTTP server. + go func() { + <-ctx.Done() + + e.Close() + }() + + return &Echo{ + echo: e, + } +}