Bootstrap copied BFF and local accounts with public Sendico contracts (#34)
This commit is contained in:
1 parent
67dc5b5b1e
commit
7062672a87
556 files changed
+42291
-777
No files matched your search
@@ -0,0 +1,34 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.sendico.io/tech/sxchnge/bff/edge/shared/interface/api"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIncompleteConfigReturnsProjectError(t *testing.T) {
|
||||
for _, data := range []string{"api: {}", "api: {middleware: {}}\ndatabase: {driver: mongodb}", "api: {middleware: {}}\ndatabase: {driver: mongodb}\nhttp_server: {}"} {
|
||||
instance := &Imp{runtime: api.RuntimeConfig{}}
|
||||
if err := instance.decodeConfig([]byte(data)); !errors.Is(err, merrors.ErrInvalidArg) {
|
||||
t.Fatalf("expected configuration error, got %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestExampleConfigMatchesCopiedRuntime(t *testing.T) {
|
||||
data, err := os.ReadFile("../../../../config.example.yml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
instance := &Imp{runtime: api.RuntimeConfig{}}
|
||||
if err := instance.decodeConfig(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := instance.config.API.SharedConfig().Mw.Token.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := instance.config.API.SharedConfig().Mw.CORS.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
ac "git.sendico.io/tech/sxchnge/bff/edge/shared/interface/api"
|
||||
sharedruntime "git.sendico.io/tech/sxchnge/bff/edge/shared/internal/runtime"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mlogger"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
mduration "git.sendico.io/tech/sxchnge/bff/pkg/mutil/duration"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type httpServerConf struct {
|
||||
ListenAddress string `yaml:"listen_address"`
|
||||
ReadHeaderTimeout int `yaml:"read_header_timeout"`
|
||||
ShutdownTimeout int `yaml:"shutdown_timeout"`
|
||||
}
|
||||
|
||||
// Config represents the server configuration
|
||||
type Config struct {
|
||||
API ac.ConfigProvider
|
||||
DB *db.Config `yaml:"database"`
|
||||
HTTPServer *httpServerConf `yaml:"http_server"`
|
||||
}
|
||||
|
||||
// Instance represents an instance of the server
|
||||
type Imp struct {
|
||||
lifecycleCtx context.Context
|
||||
lifecycleCancel context.CancelFunc
|
||||
logger mlogger.Logger
|
||||
api mservice.MicroService
|
||||
config *Config
|
||||
db db.Factory
|
||||
httpServer *http.Server
|
||||
runtime ac.RuntimeConfig
|
||||
debug bool
|
||||
file string
|
||||
}
|
||||
|
||||
// Shutdown stops the server
|
||||
func (i *Imp) Shutdown() {
|
||||
if i.lifecycleCancel != nil {
|
||||
i.lifecycleCancel()
|
||||
}
|
||||
if i.httpServer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Shutdown HTTP server
|
||||
ctx, cancel := context.WithTimeout(context.Background(), mduration.Param2Duration(i.config.HTTPServer.ShutdownTimeout, time.Second))
|
||||
i.logger.Info("Shutting HTTP server down...")
|
||||
if err := i.httpServer.Shutdown(ctx); err != nil {
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
i.logger.Warn("Failed to shutdown HTTP server gracefully", zap.Error(err))
|
||||
cancel()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
|
||||
func (i *Imp) Run() error {
|
||||
err := i.httpServer.ListenAndServe()
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
err = nil
|
||||
}
|
||||
i.logger.Info("HTTP Server stopped")
|
||||
|
||||
if err := i.api.Finish(context.Background()); err != nil {
|
||||
i.logger.Warn("Error when finishing service", zap.Error(err))
|
||||
}
|
||||
|
||||
i.db.CloseConnection()
|
||||
|
||||
if err != nil {
|
||||
return merrors.OpaqueWrap(merrors.ErrInternal, err, "HTTP server stopped unexpectedly")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts the server
|
||||
func (i *Imp) Start() error {
|
||||
if i.lifecycleCancel != nil {
|
||||
defer i.lifecycleCancel()
|
||||
}
|
||||
i.logger.Info("Starting...", zap.String("config_file", i.file), zap.Bool("debug_mode", i.debug))
|
||||
// Load configuration file
|
||||
data, err := os.ReadFile(i.file)
|
||||
if err != nil {
|
||||
i.logger.Error("Could not load configuration", zap.Error(err), zap.String("config_file", i.file))
|
||||
return err
|
||||
}
|
||||
|
||||
if err = i.decodeConfig(data); err != nil {
|
||||
i.logger.Error("Failed to parse configuration", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if i.db, err = db.NewConnection(i.logger, i.config.DB); err != nil {
|
||||
i.logger.Error("Could not open database connection", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
rootRouter := chi.NewRouter()
|
||||
apiRouter := chi.NewRouter()
|
||||
if i.api, err = sharedruntime.CreateAPI(i.lifecycleCtx, i.logger, i.config.API.SharedConfig(), i.config.API, i.db, rootRouter, apiRouter, i.debug, i.runtime); err != nil {
|
||||
i.db.CloseConnection()
|
||||
return err
|
||||
}
|
||||
rootRouter.Mount("/", apiRouter)
|
||||
|
||||
// Startup the HTTP Server in a way that we can gracefully shut it down again
|
||||
i.httpServer = &http.Server{
|
||||
Addr: i.config.HTTPServer.ListenAddress,
|
||||
Handler: rootRouter,
|
||||
ReadHeaderTimeout: mduration.Param2Duration(i.config.HTTPServer.ReadHeaderTimeout, time.Second),
|
||||
}
|
||||
|
||||
return i.Run()
|
||||
}
|
||||
|
||||
func (i *Imp) decodeConfig(data []byte) error {
|
||||
var raw struct {
|
||||
API yaml.Node `yaml:"api"`
|
||||
DB *db.Config `yaml:"database"`
|
||||
HTTPServer *httpServerConf `yaml:"http_server"`
|
||||
}
|
||||
if err := yaml.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiConfig := i.runtime.NewConfig()
|
||||
if apiConfig == nil {
|
||||
return merrors.Internal("runtime config factory returned nil api config")
|
||||
}
|
||||
if raw.API.Kind == 0 {
|
||||
return merrors.InvalidArgument("api configuration is required", "api")
|
||||
}
|
||||
if err := raw.API.Decode(apiConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if raw.DB == nil || raw.HTTPServer == nil || apiConfig.SharedConfig() == nil || apiConfig.SharedConfig().Mw == nil {
|
||||
return merrors.InvalidArgument("database, http_server and api.middleware configuration are required")
|
||||
}
|
||||
if raw.HTTPServer.ListenAddress == "" || raw.HTTPServer.ReadHeaderTimeout <= 0 || raw.HTTPServer.ShutdownTimeout <= 0 {
|
||||
return merrors.InvalidArgument("HTTP address and positive timeouts are required")
|
||||
}
|
||||
i.config = &Config{
|
||||
API: apiConfig,
|
||||
DB: raw.DB,
|
||||
HTTPServer: raw.HTTPServer,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Create(logger mlogger.Logger, file string, debug bool, runtime ac.RuntimeConfig) (*Imp, error) {
|
||||
lifecycleCtx, lifecycleCancel := context.WithCancel(context.Background())
|
||||
srv := &Imp{
|
||||
lifecycleCtx: lifecycleCtx,
|
||||
lifecycleCancel: lifecycleCancel,
|
||||
logger: logger,
|
||||
debug: debug,
|
||||
file: file,
|
||||
runtime: runtime,
|
||||
}
|
||||
return srv, nil
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/edge/shared/interface/api"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestShutdownCancelsLifecycleContextBeforeStartupCompletes(t *testing.T) {
|
||||
instance, err := Create(zap.NewNop(), "config.yml", false, api.RuntimeConfig{})
|
||||
if err != nil {
|
||||
t.Fatalf("Create() error = %v", err)
|
||||
}
|
||||
|
||||
instance.Shutdown()
|
||||
if !errors.Is(instance.lifecycleCtx.Err(), context.Canceled) {
|
||||
t.Fatalf("lifecycle context error = %v, want context.Canceled", instance.lifecycleCtx.Err())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user