Organize backend into product executable and shared Go modules
This commit is contained in:
1 parent
981e84a4b4
commit
f0720ec66b
382 files changed
+1944
-6989
No files matched your search
@@ -0,0 +1,69 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/product/app/api"
|
||||
amr "git.sendico.io/tech/sxchnge/product/app/health"
|
||||
httptransport "git.sendico.io/tech/sxchnge/product/app/http"
|
||||
"git.sendico.io/tech/sxchnge/product/app/urls"
|
||||
"git.sendico.io/tech/sxchnge/shared/database"
|
||||
"git.sendico.io/tech/sxchnge/shared/logging"
|
||||
"git.sendico.io/tech/sxchnge/shared/service"
|
||||
)
|
||||
|
||||
type Microservices = []mservice.MicroService
|
||||
|
||||
// APIImp represents the structure of the APIImp
|
||||
type APIImp struct {
|
||||
lifecycleCtx context.Context
|
||||
logger mlogger.Logger
|
||||
db db.Factory
|
||||
domain domainprovider.DomainProvider
|
||||
config *api.Config
|
||||
profile api.ConfigProvider
|
||||
runtime api.RuntimeConfig
|
||||
components api.RuntimeComponents
|
||||
services Microservices
|
||||
health amr.Health
|
||||
mw *httptransport.Middleware
|
||||
}
|
||||
|
||||
func (a *APIImp) LifecycleContext() context.Context {
|
||||
return a.lifecycleCtx
|
||||
}
|
||||
|
||||
func (a *APIImp) Logger() mlogger.Logger {
|
||||
return a.logger
|
||||
}
|
||||
|
||||
func (a *APIImp) Config() *api.Config {
|
||||
return a.config
|
||||
}
|
||||
|
||||
func (a *APIImp) ProfileConfig() api.ConfigProvider {
|
||||
return a.profile
|
||||
}
|
||||
|
||||
func (a *APIImp) DBFactory() db.Factory {
|
||||
return a.db
|
||||
}
|
||||
|
||||
func (a *APIImp) DomainProvider() domainprovider.DomainProvider {
|
||||
return a.domain
|
||||
}
|
||||
|
||||
func (a *APIImp) Register() api.Register {
|
||||
return a.mw
|
||||
}
|
||||
|
||||
func (a *APIImp) RuntimeComponent(name string) any {
|
||||
if a == nil || a.components == nil {
|
||||
return nil
|
||||
}
|
||||
return a.components[name]
|
||||
}
|
||||
|
||||
func (a *APIImp) Name() string {
|
||||
return "api"
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/product/app/api"
|
||||
amr "git.sendico.io/tech/sxchnge/product/app/health"
|
||||
httptransport "git.sendico.io/tech/sxchnge/product/app/http"
|
||||
"git.sendico.io/tech/sxchnge/product/app/urls"
|
||||
"git.sendico.io/tech/sxchnge/shared/database"
|
||||
"git.sendico.io/tech/sxchnge/shared/logging"
|
||||
"git.sendico.io/tech/sxchnge/shared/service"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func CreateAPI(
|
||||
lifecycleCtx context.Context,
|
||||
logger mlogger.Logger,
|
||||
config *api.Config,
|
||||
profileConfig api.ConfigProvider,
|
||||
db db.Factory,
|
||||
rootRouter *chi.Mux,
|
||||
apiRouter *chi.Mux,
|
||||
debug bool,
|
||||
runtime api.RuntimeConfig,
|
||||
) (mservice.MicroService, error) {
|
||||
p := &APIImp{
|
||||
lifecycleCtx: lifecycleCtx,
|
||||
logger: logger.Named("api"),
|
||||
config: config,
|
||||
profile: profileConfig,
|
||||
db: db,
|
||||
runtime: runtime,
|
||||
}
|
||||
|
||||
if err := p.installRuntimeBasics(logger, db, rootRouter, apiRouter, debug); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p.bootstrapConfiguredRuntime(config, db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.logger.Info("Installing microservices...", zap.String("runtime", runtime.RuntimeName()))
|
||||
if err := p.installServices(); err != nil {
|
||||
p.logger.Error("Failed to install a microservice", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
p.logger.Info("Microservices installation complete", zap.Int("microservices", len(p.services)))
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (a *APIImp) installRuntimeBasics(
|
||||
logger mlogger.Logger,
|
||||
db db.Factory,
|
||||
rootRouter *chi.Mux,
|
||||
apiRouter *chi.Mux,
|
||||
debug bool,
|
||||
) error {
|
||||
var err error
|
||||
if a.domain, err = domainprovider.CreateDomainProvider(
|
||||
a.logger,
|
||||
a.config.Mw.DomainEnv,
|
||||
a.config.Mw.APIProtocolEnv,
|
||||
a.config.Mw.EndPointEnv,
|
||||
); err != nil {
|
||||
a.logger.Error("Failed to initialize domain provider")
|
||||
return err
|
||||
}
|
||||
a.logger.Info("Domain provider installed")
|
||||
|
||||
apiEndpoint := os.Getenv(a.config.Mw.EndPointEnv)
|
||||
if a.health, err = amr.NewHealthRouter(a.logger, rootRouter, apiEndpoint); err != nil {
|
||||
a.logger.Error("Failed to create healthcheck router", zap.Error(err), zap.String("api_endpoint", apiEndpoint))
|
||||
return err
|
||||
}
|
||||
if a.mw, err = httptransport.CreateMiddleware(
|
||||
logger,
|
||||
db,
|
||||
apiRouter,
|
||||
a.config.Mw,
|
||||
a.config.AuthStorage,
|
||||
debug,
|
||||
); err != nil {
|
||||
a.logger.Error("Failed to create middleware", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
a.logger.Info("Middleware installed", zap.Bool("debug_mode", debug))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *APIImp) bootstrapConfiguredRuntime(_ *api.Config, _ db.Factory) error {
|
||||
if a.runtime.Bootstrap != nil {
|
||||
components, err := a.runtime.Bootstrap(a)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to bootstrap runtime profile", zap.Error(err), zap.String("runtime", a.runtime.RuntimeName()))
|
||||
return err
|
||||
}
|
||||
a.components = components
|
||||
a.logger.Info("Runtime profile bootstrap complete",
|
||||
zap.String("runtime", a.runtime.RuntimeName()),
|
||||
zap.Int("components", len(components)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package runtime_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
sharedmodel "git.sendico.io/tech/sxchnge/shared/model"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/product/app/api"
|
||||
"git.sendico.io/tech/sxchnge/product/app/profile"
|
||||
sharedruntime "git.sendico.io/tech/sxchnge/product/app/runtime"
|
||||
"git.sendico.io/tech/sxchnge/shared/database"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/testcontainers/testcontainers-go/modules/mongodb"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestProfileStartsOnEmptyDatabase(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
container, err := mongodb.Run(ctx, "mongo:8", mongodb.WithReplicaSet("rs"))
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.NoError(t, container.Terminate(context.Background())) })
|
||||
uri, err := container.ConnectionString(ctx)
|
||||
require.NoError(t, err)
|
||||
endpoint, err := url.Parse(uri)
|
||||
require.NoError(t, err)
|
||||
query := endpoint.Query()
|
||||
query.Set("directConnection", "true")
|
||||
endpoint.RawQuery = query.Encode()
|
||||
uri = endpoint.String()
|
||||
factory, err := db.NewConnection(zap.NewNop(), &db.Config{Driver: db.Mongo, Settings: sharedmodel.SettingsT{"uri": uri, "database": "sxchnge_runtime"}})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(factory.CloseConnection)
|
||||
data, err := os.ReadFile("../../config.example.yml")
|
||||
require.NoError(t, err)
|
||||
var example struct {
|
||||
API api.Config `yaml:"api"`
|
||||
}
|
||||
require.NoError(t, yaml.Unmarshal(data, &example))
|
||||
t.Setenv("SXCHNGE_DOMAIN", "localhost")
|
||||
t.Setenv("SXCHNGE_API_PROTOCOL", "http")
|
||||
t.Setenv("SXCHNGE_API_ENDPOINT", "/api/v1")
|
||||
t.Setenv("SXCHNGE_JWT_SECRET", "test-only-signing-key-at-least-32-bytes")
|
||||
root, routes := chi.NewRouter(), chi.NewRouter()
|
||||
runtime, err := sharedruntime.CreateAPI(ctx, zap.NewNop(), &example.API, &example.API, factory, root, routes, false, profile.Runtime())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.NoError(t, runtime.Finish(context.Background())) })
|
||||
root.Mount("/", routes)
|
||||
result := httptest.NewRecorder()
|
||||
root.ServeHTTP(result, httptest.NewRequest(http.MethodGet, "/api/v1/health", nil))
|
||||
require.Equal(t, http.StatusOK, result.Code)
|
||||
// Runtime auth routes are installed even with zero customers and no seed.
|
||||
result = httptest.NewRecorder()
|
||||
root.ServeHTTP(result, httptest.NewRequest(http.MethodPost, "/api/v1/accounts/login", nil))
|
||||
require.Equal(t, http.StatusBadRequest, result.Code)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/product/app/health/status"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (a *APIImp) Finish(ctx context.Context) error {
|
||||
a.health.SetStatus(health.SSTerminating)
|
||||
a.health.Finish()
|
||||
a.mw.Finish()
|
||||
var lastError error
|
||||
for i := len(a.services) - 1; i >= 0; i-- {
|
||||
if err := (a.services[i]).Finish(ctx); err != nil {
|
||||
lastError = err
|
||||
a.logger.Warn("Error occurred when finishing service",
|
||||
zap.Error(err), zap.String("service_name", (a.services[i]).Name()))
|
||||
} else {
|
||||
a.logger.Info("Microservice is down", zap.String("service_name", (a.services[i]).Name()))
|
||||
}
|
||||
}
|
||||
if a.runtime.Finish != nil {
|
||||
if err := a.runtime.Finish(a); err != nil {
|
||||
lastError = err
|
||||
a.logger.Warn("Error occurred when finishing runtime profile", zap.String("runtime", a.runtime.RuntimeName()), zap.Error(err))
|
||||
}
|
||||
}
|
||||
return lastError
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"git.sendico.io/tech/sxchnge/product/app/api"
|
||||
"git.sendico.io/tech/sxchnge/product/app/health/status"
|
||||
"git.sendico.io/tech/sxchnge/shared/errors"
|
||||
"git.sendico.io/tech/sxchnge/shared/service"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (a *APIImp) installMicroservice(srv mservice.MicroService) {
|
||||
a.services = append(a.services, srv)
|
||||
a.logger.Info("Microservice installed", zap.String("service", srv.Name()))
|
||||
}
|
||||
|
||||
func (a *APIImp) addMicroservice(srvf api.MicroServiceFactoryT) error {
|
||||
srv, err := srvf(a)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to install a microservice", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
a.installMicroservice(srv)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *APIImp) installServices() error {
|
||||
for _, service := range a.runtime.Services {
|
||||
if service.Factory == nil {
|
||||
return merrors.InvalidArgument("microservice factory is required", "runtime.services")
|
||||
}
|
||||
if err := a.addMicroservice(service.Factory); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
a.health.SetStatus(health.SSRunning)
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user