@@ -15,12 +15,17 @@ import (
1515 "github.com/gin-gonic/gin"
1616
1717 "crowdsec-manager/internal/api"
18+ "crowdsec-manager/internal/api/handlers"
19+ "crowdsec-manager/internal/api/middleware"
1820 "crowdsec-manager/internal/backup"
21+ "crowdsec-manager/internal/cache"
1922 "crowdsec-manager/internal/config"
23+ "crowdsec-manager/internal/configvalidator"
2024 "crowdsec-manager/internal/cron"
2125 "crowdsec-manager/internal/database"
2226 "crowdsec-manager/internal/docker"
2327 "crowdsec-manager/internal/logger"
28+ "crowdsec-manager/internal/messaging"
2429)
2530
2631// Main entry point for the CrowdSec Manager server
@@ -35,6 +40,7 @@ func main() {
3540
3641 // Initialize structured logger with configured level and output file
3742 logger .Init (cfg .LogLevel , cfg .LogFile )
43+ defer logger .Sync ()
3844
3945 // Initialize SQLite database connection with automatic schema migration
4046 db , err := database .New (cfg .DatabasePath )
@@ -44,12 +50,15 @@ func main() {
4450 defer db .Close ()
4551 logger .Info ("Database initialized" , "path" , cfg .DatabasePath )
4652
47- // Initialize Docker API client with automatic version negotiation
48- dockerClient , err := docker .NewClient ( )
53+ // Initialize multi-host Docker client (falls back to single host if DOCKER_HOSTS is empty)
54+ multiHost , err := docker .NewMultiHostClient ( cfg . DockerHosts )
4955 if err != nil {
5056 logger .Fatal ("Failed to initialize Docker client" , "error" , err )
5157 }
52- defer dockerClient .Close ()
58+ defer multiHost .Close ()
59+
60+ // Default client for backward compatibility with existing handler signatures
61+ dockerClient := multiHost .DefaultClient ()
5362
5463 dataDir := cfg .ConfigDir
5564
@@ -61,6 +70,29 @@ func main() {
6170 cronScheduler .Start ()
6271 defer cronScheduler .Stop ()
6372
73+ // Initialize WebSocket/SSE hub (always available for real-time events)
74+ hub := messaging .NewHub ()
75+ go hub .Run ()
76+ defer hub .Stop ()
77+
78+ // Initialize config validator for drift detection and recovery
79+ validator := configvalidator .NewValidator (db , dockerClient , hub , cfg )
80+ handlers .SetConfigValidator (validator )
81+
82+ // Snapshot all configs on startup (populates DB if empty)
83+ validator .SnapshotAll ()
84+
85+ // Validate configs and warn about drift
86+ if report := validator .ValidateAll (); report .Overall != "ok" {
87+ logger .Warn ("Config drift detected on startup" , "overall" , report .Overall )
88+ }
89+
90+ // Initialize NATS messaging (optional — nil-safe when disabled)
91+ publisher , natsCleanup := initMessaging (cfg , hub )
92+ if natsCleanup != nil {
93+ defer natsCleanup ()
94+ }
95+
6496 // Configure HTTP router with recovery middleware and custom logger
6597 router := gin .New ()
6698 router .Use (gin .Recovery ())
@@ -70,7 +102,7 @@ func main() {
70102 router .Use (cors .New (cors.Config {
71103 AllowOrigins : []string {"http://localhost:3000" , "http://localhost:5173" },
72104 AllowMethods : []string {"GET" , "POST" , "PUT" , "DELETE" , "OPTIONS" },
73- AllowHeaders : []string {"Origin" , "Content-Type" , "Authorization" },
105+ AllowHeaders : []string {"Origin" , "Content-Type" , "Authorization" , "X-Docker-Host" },
74106 ExposeHeaders : []string {"Content-Length" },
75107 AllowCredentials : true ,
76108 MaxAge : 12 * time .Hour ,
@@ -83,6 +115,13 @@ func main() {
83115
84116 // Register all API route groups under /api prefix
85117 apiGroup := router .Group ("/api" )
118+
119+ // Add rate limiting middleware (100 requests per minute per IP)
120+ apiGroup .Use (middleware .RateLimiter (100 ))
121+
122+ // Add Docker host selector middleware for multi-host support
123+ apiGroup .Use (middleware .DockerHostSelector (multiHost ))
124+
86125 {
87126 api .RegisterHealthRoutes (apiGroup , dockerClient , db , cfg )
88127 api .RegisterIPRoutes (apiGroup , dockerClient , cfg )
@@ -94,17 +133,47 @@ func main() {
94133 api .RegisterBackupRoutes (apiGroup , backupManager , dockerClient )
95134 api .RegisterUpdateRoutes (apiGroup , dockerClient , cfg )
96135 api .RegisterCronRoutes (apiGroup , cronScheduler )
97- api .RegisterServicesRoutes (apiGroup , dockerClient , db , cfg )
136+ ttlCache := cache .New ()
137+ api .RegisterServicesRoutes (apiGroup , dockerClient , db , cfg , ttlCache )
98138 api .RegisterNotificationRoutes (apiGroup , dockerClient , db , cfg )
99139 api .RegisterProfileRoutes (apiGroup , db , cfg , dockerClient )
140+ api .RegisterHostRoutes (apiGroup , multiHost )
141+ api .RegisterTerminalRoutes (apiGroup , dockerClient )
142+
143+ // Hub browser routes
144+ api .RegisterHubRoutes (apiGroup , dockerClient , db , cfg )
145+
146+ // Simulation mode routes
147+ api .RegisterSimulationRoutes (apiGroup , dockerClient , cfg )
148+
149+ // Event routes (hub is always available for SSE/WebSocket)
150+ api .RegisterEventRoutes (apiGroup , hub )
151+
152+ // Config validation routes
153+ api .RegisterConfigValidationRoutes (apiGroup , validator )
100154 }
101155
102- // Serve React frontend static assets and handle client-side routing
156+ // Bridge NATS events to WebSocket hub (if both are available)
157+ if publisher != nil && hub != nil {
158+ go bridgeNATSToHub (cfg , hub )
159+ }
160+ // Suppress unused variable warnings — publisher will be used by handlers in Phase 4
161+ _ = publisher
162+
163+ // Serve React frontend static assets and handle client-side routing.
164+ // Assets use content-hashed filenames so they can be cached indefinitely.
103165 router .Static ("/assets" , "./web/dist/assets" )
104- router .StaticFile ("/" , "./web/dist/index.html" )
105- router .NoRoute (func (c * gin.Context ) {
166+ // index.html must not be cached — it references hashed asset URLs that
167+ // change on each build. Without no-cache the browser may serve a stale
168+ // copy (304) that points to old, non-existent JS chunks.
169+ serveIndex := func (c * gin.Context ) {
170+ c .Header ("Cache-Control" , "no-cache, no-store, must-revalidate" )
171+ c .Header ("Pragma" , "no-cache" )
172+ c .Header ("Expires" , "0" )
106173 c .File ("./web/dist/index.html" )
107- })
174+ }
175+ router .GET ("/" , func (c * gin.Context ) { serveIndex (c ) })
176+ router .NoRoute (serveIndex )
108177
109178 // Create HTTP server with production-ready timeouts to prevent resource exhaustion
110179 srv := & http.Server {
@@ -141,6 +210,36 @@ func main() {
141210 logger .Info ("Server exited" )
142211}
143212
213+ // initMessaging initializes NATS client and publisher.
214+ // Returns nil values when NATS is disabled — all are nil-safe.
215+ func initMessaging (cfg * config.Config , hub * messaging.Hub ) (* messaging.Publisher , func ()) {
216+ if ! cfg .NatsEnabled || cfg .NatsURL == "" {
217+ logger .Info ("NATS messaging disabled" )
218+ return nil , nil
219+ }
220+
221+ natsClient , err := messaging .NewClient (cfg .NatsURL , cfg .NatsToken )
222+ if err != nil {
223+ logger .Error ("Failed to connect to NATS (messaging disabled)" , "error" , err )
224+ return nil , nil
225+ }
226+
227+ publisher := messaging .NewPublisher (natsClient )
228+
229+ logger .Info ("NATS messaging initialized" , "url" , cfg .NatsURL )
230+
231+ cleanup := func () {
232+ natsClient .Close ()
233+ }
234+ return publisher , cleanup
235+ }
236+
237+ // bridgeNATSToHub subscribes to NATS subjects and forwards events to the WebSocket hub
238+ func bridgeNATSToHub (cfg * config.Config , hub * messaging.Hub ) {
239+ // This will be wired up when NATS client Subscribe is implemented
240+ logger .Info ("NATS-to-WebSocket bridge started" )
241+ }
242+
144243// checkPrerequisites verifies that Docker daemon is running and required containers exist
145244// This function is defined but not currently called in main - consider adding prerequisite checks if needed
146245func checkPrerequisites (client * docker.Client , cfg * config.Config ) error {
0 commit comments