379 lines
7.8 KiB
Go
379 lines
7.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"cmroubao/backend-api/internal/config"
|
|
"cmroubao/backend-api/internal/platform/assetstore"
|
|
"cmroubao/backend-api/internal/platform/database"
|
|
"cmroubao/backend-api/internal/platform/erpconnector"
|
|
"cmroubao/backend-api/internal/platform/migration"
|
|
"cmroubao/backend-api/internal/platform/password"
|
|
repository "cmroubao/backend-api/internal/repository/sqlite"
|
|
"cmroubao/backend-api/internal/transport/authcommon"
|
|
"cmroubao/backend-api/internal/transport/httpapi"
|
|
"cmroubao/backend-api/internal/transport/webui"
|
|
"cmroubao/backend-api/internal/usecase"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func main() {
|
|
if err := run(); err != nil {
|
|
log.Printf("cmroubao API stopped: %v", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func run() error {
|
|
cfg, err := config.Load(os.LookupEnv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
startupContext, cancelStartup := context.WithTimeout(
|
|
context.Background(),
|
|
cfg.ReadTimeout,
|
|
)
|
|
defer cancelStartup()
|
|
db, err := database.Open(startupContext, cfg.DatabasePath)
|
|
if err != nil {
|
|
return errors.New("database startup failed")
|
|
}
|
|
defer func() {
|
|
if err := db.Close(); err != nil {
|
|
log.Print("database close failed")
|
|
}
|
|
}()
|
|
|
|
router, err := buildRouter(startupContext, cfg, db)
|
|
if err != nil {
|
|
if errors.Is(err, errPendingMigrations) {
|
|
return err
|
|
}
|
|
return errors.New("HTTP router setup failed")
|
|
}
|
|
server := httpapi.NewServer(cfg, router)
|
|
|
|
signalContext, stopSignals := signal.NotifyContext(
|
|
context.Background(),
|
|
os.Interrupt,
|
|
syscall.SIGTERM,
|
|
)
|
|
defer stopSignals()
|
|
|
|
serverErrors := make(chan error, 1)
|
|
go func() {
|
|
if cfg.TLSCertificate != "" {
|
|
serverErrors <- server.ListenAndServeTLS(
|
|
cfg.TLSCertificate,
|
|
cfg.TLSPrivateKey,
|
|
)
|
|
return
|
|
}
|
|
serverErrors <- server.ListenAndServe()
|
|
}()
|
|
|
|
select {
|
|
case err := <-serverErrors:
|
|
if errors.Is(err, http.ErrServerClosed) {
|
|
return nil
|
|
}
|
|
return errors.New("HTTP server failed")
|
|
case <-signalContext.Done():
|
|
return shutdownServer(server, serverErrors, cfg.ShutdownTimeout)
|
|
}
|
|
}
|
|
|
|
func shutdownServer(
|
|
server *http.Server,
|
|
serverErrors <-chan error,
|
|
timeout time.Duration,
|
|
) error {
|
|
shutdownContext, cancelShutdown := context.WithTimeout(
|
|
context.Background(),
|
|
timeout,
|
|
)
|
|
defer cancelShutdown()
|
|
shutdownErr := server.Shutdown(shutdownContext)
|
|
if shutdownErr != nil {
|
|
if err := server.Close(); err != nil {
|
|
return errors.New("HTTP server force close failed")
|
|
}
|
|
}
|
|
|
|
waitTimeout := timeout
|
|
if waitTimeout > time.Second {
|
|
waitTimeout = time.Second
|
|
}
|
|
timer := time.NewTimer(waitTimeout)
|
|
defer timer.Stop()
|
|
select {
|
|
case err := <-serverErrors:
|
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
return errors.New("HTTP server failed during shutdown")
|
|
}
|
|
case <-timer.C:
|
|
_ = server.Close()
|
|
return errors.New("HTTP server did not stop")
|
|
}
|
|
if shutdownErr != nil {
|
|
return errors.New("HTTP server graceful shutdown timed out")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type migrationStatusReader interface {
|
|
Status(context.Context) ([]migration.Status, error)
|
|
}
|
|
|
|
var errPendingMigrations = errors.New(
|
|
"database migrations are pending; run migrate up",
|
|
)
|
|
|
|
func buildRouter(
|
|
ctx context.Context,
|
|
cfg config.Config,
|
|
db *sql.DB,
|
|
) (http.Handler, error) {
|
|
runner, err := migration.New(db)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := requireCurrentMigrations(ctx, runner); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
store, err := repository.New(db)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
files, err := assetstore.New(cfg.AssetDirectory)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
clock := usecase.SystemClock{}
|
|
ids := usecase.UUIDGenerator{}
|
|
assets, err := usecase.NewAssetService(store, files, clock, ids)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tasks, err := usecase.NewTaskService(store, clock, ids)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
lifecycle, err := usecase.NewLifecycleService(
|
|
store,
|
|
clock,
|
|
ids,
|
|
cfg.ClaimLease,
|
|
cfg.RunningLease,
|
|
cfg.ReadinessTTL,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
results, err := usecase.NewExecutionResultService(
|
|
store,
|
|
files,
|
|
clock,
|
|
ids,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
authorizations, err := usecase.NewOrderAuthorizationService(
|
|
store,
|
|
clock,
|
|
ids,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
commands, err := usecase.NewDeviceOrderCommandService(
|
|
store,
|
|
clock,
|
|
ids,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dryRuns, err := usecase.NewOrderDryRunService(store, clock, ids)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
submissions, err := usecase.NewOrderSubmissionService(
|
|
store,
|
|
clock,
|
|
ids,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
erpTimeout := cfg.ERPConnectorTimeout
|
|
if erpTimeout <= 0 {
|
|
erpTimeout = 90 * time.Second
|
|
}
|
|
erpClient, err := erpconnector.New(
|
|
cfg.ERPConnectorURL,
|
|
cfg.ERPConnectorAPIKey,
|
|
erpTimeout,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
freight, err := usecase.NewFreightService(
|
|
store,
|
|
erpClient,
|
|
clock,
|
|
ids,
|
|
erpTimeout,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := freight.RecoverInterrupted(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
procurement, err := usecase.NewProcurementService(store, clock, ids)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
passwords, err := password.NewBcrypt(12)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
registerDeviceRoutes, err := httpapi.NewDeviceRouteRegistrar(
|
|
httpapi.DeviceServices{
|
|
Lifecycle: lifecycle,
|
|
Assets: assets,
|
|
Results: results,
|
|
Commands: commands,
|
|
DryRuns: dryRuns,
|
|
Submissions: submissions,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
auth, err := usecase.NewAuthService(
|
|
store,
|
|
passwords,
|
|
clock,
|
|
ids,
|
|
usecase.CryptoTokenGenerator{},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
webService, err := webui.NewUsecaseAdapter(
|
|
tasks,
|
|
assets,
|
|
authorizations,
|
|
freight,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
webService.SetProcurement(procurement)
|
|
renderer, err := webui.NewRenderer()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
webHandler, err := webui.NewHandler(webService, renderer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
authWebService, err := webui.NewAuthUsecaseAdapter(auth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
loginLimiter, err := authcommon.NewAttemptLimiter(
|
|
10,
|
|
5*time.Minute,
|
|
4096,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
authWebHandler, err := webui.NewAuthHandler(
|
|
authWebService,
|
|
renderer,
|
|
loginLimiter,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
registerPublicAuth, err := httpapi.NewPublicAuthRegistrar(
|
|
auth,
|
|
loginLimiter,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
registerAdminRoutes, err := httpapi.NewAdminRouteRegistrar(
|
|
httpapi.AdminServices{
|
|
Assets: assets,
|
|
Tasks: tasks,
|
|
Results: results,
|
|
Authorizations: authorizations,
|
|
Freight: freight,
|
|
Procurement: procurement,
|
|
},
|
|
webHandler,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
registerProtectedRoutes := func(routes gin.IRoutes) error {
|
|
if err := registerAdminRoutes(routes); err != nil {
|
|
return err
|
|
}
|
|
authWebHandler.RegisterProtected(routes)
|
|
return nil
|
|
}
|
|
return httpapi.NewRouter(httpapi.RouterDependencies{
|
|
Database: db,
|
|
RegisterPublicRoutes: func(routes gin.IRoutes) error {
|
|
webHandler.RegisterStatic(routes)
|
|
authWebHandler.RegisterPublic(routes)
|
|
return registerPublicAuth(routes)
|
|
},
|
|
RegisterAdminRoutes: registerProtectedRoutes,
|
|
RegisterDeviceRoutes: registerDeviceRoutes,
|
|
AdminSessions: auth,
|
|
DeviceAccess: auth,
|
|
LogEvent: func(event string) {
|
|
log.Print(event)
|
|
},
|
|
})
|
|
}
|
|
|
|
func requireCurrentMigrations(
|
|
ctx context.Context,
|
|
reader migrationStatusReader,
|
|
) error {
|
|
statuses, err := reader.Status(ctx)
|
|
if err != nil {
|
|
return errors.New("database migration status failed")
|
|
}
|
|
if len(statuses) == 0 {
|
|
return errors.New("database has no known migrations")
|
|
}
|
|
for _, status := range statuses {
|
|
if !status.Applied {
|
|
return errPendingMigrations
|
|
}
|
|
}
|
|
return nil
|
|
}
|