package bell import ( "context" "errors" "fmt" "log/slog" "net/http" "os" "os/signal" "strings" "syscall" "time" alertLifecycle "git.ilapage.cn/ila/yovision/Bell/server/app/alert/lifecycle" alertQuery "git.ilapage.cn/ila/yovision/Bell/server/app/alert/query" "git.ilapage.cn/ila/yovision/Bell/server/app/audit" "git.ilapage.cn/ila/yovision/Bell/server/app/auth" "git.ilapage.cn/ila/yovision/Bell/server/app/event" "git.ilapage.cn/ila/yovision/Bell/server/app/rule" "git.ilapage.cn/ila/yovision/Bell/server/app/synthetic" "git.ilapage.cn/ila/yovision/Bell/server/config" "git.ilapage.cn/ila/yovision/Bell/server/internal/platform" "git.ilapage.cn/ila/yovision/Bell/server/migrations" ) const Version = "0.1.0" func Run(ctx context.Context, args []string) error { command := "serve" if len(args) > 0 { command = args[0] } if command == "version" { fmt.Println(Version) return nil } if command != "serve" && command != "migrate" && command != "create-admin" { return fmt.Errorf("unknown command %q (use serve, migrate, create-admin, or version)", command) } cfg, err := config.Load() if err != nil { return err } db, err := platform.OpenDatabase(ctx, cfg.DatabaseURL) if err != nil { return err } defer db.Close() if err := migrations.Apply(ctx, db); err != nil { return err } if command == "migrate" { return nil } auditStore := audit.Store{DB: db} authStore := auth.Store{DB: db} authService := auth.Service{Store: authStore, Audit: auditStore, Secret: []byte(cfg.SessionSecret), SessionTTL: 8 * time.Hour} if command == "create-admin" { username, displayName := argument(args, "--username"), argument(args, "--display-name") password := os.Getenv("BELL_BOOTSTRAP_PASSWORD") if displayName == "" { displayName = username } if password == "" { return fmt.Errorf("BELL_BOOTSTRAP_PASSWORD is required for create-admin") } return authService.BootstrapAdministrator(ctx, username, displayName, password) } app := platform.NewHTTPApp(db) authHTTP := auth.HTTP{Service: authService, Store: authStore, Audit: auditStore, SecureCookie: cfg.CookieSecure} authHTTP.Register(app.Router()) ruleService := rule.Service{DB: db} eventService := event.Service{DB: db, AfterPersist: ruleService.Match} event.HTTP{Service: eventService, Auth: authHTTP}.Register(app.Router()) rule.HTTP{Service: ruleService, Auth: authHTTP, Audit: auditStore}.Register(app.Router()) alertQueryService := alertQuery.Service{DB: db} alertQuery.HTTP{Service: alertQueryService, Auth: authHTTP}.Register(app.Router()) alertLifecycle.HTTP{Service: alertLifecycle.Service{DB: db, Audit: auditStore, Query: alertQueryService}, Auth: authHTTP}.Register(app.Router()) if cfg.Environment == "development" || cfg.Environment == "test" { synthetic.HTTP{Service: synthetic.Service{Events: eventService}, Auth: authHTTP}.Register(app.Router()) } server := &http.Server{Addr: cfg.HTTPAddress, Handler: app.Handler()} serverCtx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) defer stop() go func() { <-serverCtx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout) defer cancel() _ = server.Shutdown(shutdownCtx) }() slog.Info("Bell listening", "address", cfg.HTTPAddress, "environment", cfg.Environment) if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { return err } return nil } func argument(args []string, name string) string { for i := 1; i < len(args)-1; i++ { if strings.EqualFold(args[i], name) { return args[i+1] } } return "" }