1package cmd
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "os/signal"
8 "path/filepath"
9 "sync"
10 "syscall"
11 "time"
12
13 "gordon/internal/config"
14 "gordon/internal/container"
15 "gordon/internal/events"
16 "gordon/internal/logging"
17 "gordon/internal/proxy"
18 "gordon/internal/registry"
19
20 "github.com/fsnotify/fsnotify"
21 "github.com/rs/zerolog"
22 "github.com/rs/zerolog/log"
23 "github.com/spf13/cobra"
24 "github.com/spf13/viper"
25)
26
27var startCmd = &cobra.Command{
28 Use: "start",
29 Short: "Start the Gordon server",
30 Long: `Start the container registry and reverse proxy server`,
31 Run: runStart,
32}
33
34func init() {
35 rootCmd.AddCommand(startCmd)
36}
37
38func runStart(cmd *cobra.Command, args []string) {
39 // Load configuration first
40 cfg, err := config.Load()
41 if err != nil {
42 // Use basic console logging for config errors
43 zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
44 log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
45 log.Fatal().Err(err).Msg("Failed to load config")
46 }
47
48 // Initialize logging system
49 if err := logging.Setup(cfg); err != nil {
50 log.Fatal().Err(err).Msg("Failed to initialize logging")
51 }
52 defer logging.Close()
53
54 log.Info().Msg("Starting Gordon server...")
55 log.Info().Int("registry_port", cfg.Server.RegistryPort).Msg("Registry server")
56 log.Info().Int("proxy_port", cfg.Server.Port).Msg("Proxy server")
57 log.Info().Str("runtime", cfg.Server.Runtime).Msg("Container runtime")
58
59 // Create PID file for reload command
60 pidFile := createPidFile()
61 if pidFile != "" {
62 defer removePidFile(pidFile)
63 }
64
65 // Initialize event bus
66 eventBus := events.NewInMemoryEventBus(100)
67 if err := eventBus.Start(); err != nil {
68 log.Fatal().Err(err).Msg("Failed to start event bus")
69 }
70
71 // Initialize container manager
72 manager, err := container.NewManager(cfg)
73 if err != nil {
74 log.Fatal().Err(err).Msg("Failed to initialize container manager")
75 }
76
77 // Create and subscribe container event handler
78 containerHandler := events.NewContainerEventHandler(manager, cfg)
79 if err := eventBus.Subscribe(containerHandler); err != nil {
80 log.Fatal().Err(err).Msg("Failed to subscribe container event handler")
81 }
82
83 // Create and subscribe auto-route event handler
84 var autoRouteHandler *events.AutoRouteHandler
85 autoRouteHandler = events.NewAutoRouteHandler(cfg, manager)
86 if err := eventBus.Subscribe(autoRouteHandler); err != nil {
87 log.Fatal().Err(err).Msg("Failed to subscribe auto-route event handler")
88 }
89
90 // Create and subscribe version event handler
91 var versionHandler *events.VersionHandler
92 versionHandler = events.NewVersionHandler(manager, cfg)
93 if err := eventBus.Subscribe(versionHandler); err != nil {
94 log.Fatal().Err(err).Msg("Failed to subscribe version event handler")
95 }
96
97 // Start proxy server (needs to be accessible for config reload)
98 proxyServer := proxy.NewServer(cfg, manager)
99
100 // Create and subscribe proxy event handler
101 var proxyHandler *proxy.ProxyEventHandler
102 proxyHandler = proxy.NewProxyEventHandler(proxyServer)
103 if err := eventBus.Subscribe(proxyHandler); err != nil {
104 log.Fatal().Err(err).Msg("Failed to subscribe proxy event handler")
105 }
106
107 // Create context for the entire server lifecycle
108 ctx, cancel := context.WithCancel(context.Background())
109
110 // Setup config file watching for live reload with debouncing
111 var reloadTimer *time.Timer
112 var reloadMutex sync.Mutex
113
114 viper.WatchConfig()
115 viper.OnConfigChange(func(e fsnotify.Event) {
116 log.Info().
117 Str("file", e.Name).
118 Str("op", e.Op.String()).
119 Msg("Configuration file changed, reloading...")
120
121 // Debounce config reloads to prevent multiple events from a single logical change
122 reloadMutex.Lock()
123 defer reloadMutex.Unlock()
124
125 if reloadTimer != nil {
126 reloadTimer.Stop()
127 }
128
129 reloadTimer = time.AfterFunc(500*time.Millisecond, func() {
130 // Reload configuration
131 newCfg, err := config.Load()
132 if err != nil {
133 log.Error().Err(err).Msg("Failed to reload configuration")
134 return
135 }
136
137 // Update the configuration reference
138 cfg = newCfg
139
140 // Reinitialize logging with new config
141 if err := logging.Setup(cfg); err != nil {
142 log.Error().Err(err).Msg("Failed to reinitialize logging after config reload")
143 }
144
145 // Update proxy server config
146 proxyServer.UpdateConfig(cfg)
147
148 // Update container manager config (includes env file creation for new routes)
149 manager.UpdateConfig(cfg)
150
151 // Unsubscribe old handlers before creating new ones
152 if err := eventBus.Unsubscribe(containerHandler); err != nil {
153 log.Warn().Err(err).Msg("Failed to unsubscribe old container event handler")
154 }
155 if err := eventBus.Unsubscribe(autoRouteHandler); err != nil {
156 log.Warn().Err(err).Msg("Failed to unsubscribe old auto-route event handler")
157 }
158 if err := eventBus.Unsubscribe(versionHandler); err != nil {
159 log.Warn().Err(err).Msg("Failed to unsubscribe old version event handler")
160 }
161 if err := eventBus.Unsubscribe(proxyHandler); err != nil {
162 log.Warn().Err(err).Msg("Failed to unsubscribe old proxy event handler")
163 }
164
165 // Update container handler config
166 containerHandler = events.NewContainerEventHandler(manager, cfg)
167 if err := eventBus.Subscribe(containerHandler); err != nil {
168 log.Error().Err(err).Msg("Failed to re-subscribe container event handler")
169 return
170 }
171
172 // Update auto-route handler config
173 autoRouteHandler = events.NewAutoRouteHandler(cfg, manager)
174 if err := eventBus.Subscribe(autoRouteHandler); err != nil {
175 log.Error().Err(err).Msg("Failed to re-subscribe auto-route event handler")
176 return
177 }
178
179 // Update version handler config
180 versionHandler = events.NewVersionHandler(manager, cfg)
181 if err := eventBus.Subscribe(versionHandler); err != nil {
182 log.Error().Err(err).Msg("Failed to re-subscribe version event handler")
183 return
184 }
185
186 // Re-subscribe proxy handler (config ref updated automatically)
187 proxyHandler = proxy.NewProxyEventHandler(proxyServer)
188 if err := eventBus.Subscribe(proxyHandler); err != nil {
189 log.Error().Err(err).Msg("Failed to re-subscribe proxy event handler")
190 return
191 }
192
193 // Publish config reload event to handle route changes
194 if err := eventBus.Publish(events.ConfigReload, nil); err != nil {
195 log.Error().Err(err).Msg("Failed to publish config reload event")
196 } else {
197 log.Info().Msg("Configuration reloaded successfully")
198 }
199 })
200 })
201
202 // Sync existing containers
203 if err := manager.SyncContainers(ctx); err != nil {
204 log.Warn().Err(err).Msg("Failed to sync existing containers")
205 }
206
207 // Auto-start containers that match config routes
208 log.Info().Msg("Auto-starting containers for configured routes...")
209 if err := manager.AutoStartContainers(ctx); err != nil {
210 log.Warn().Err(err).Msg("Failed to auto-start containers")
211 }
212
213 var wg sync.WaitGroup
214
215 // Start registry server
216 registryServer, err := registry.NewServer(cfg, eventBus)
217 if err != nil {
218 log.Fatal().Err(err).Msg("Failed to initialize registry server")
219 }
220 wg.Add(1)
221 go func() {
222 defer wg.Done()
223 if err := registryServer.Start(ctx); err != nil {
224 log.Error().Err(err).Msg("Registry server error")
225 }
226 }()
227
228 // Start proxy server (already created above)
229 wg.Add(1)
230 go func() {
231 defer wg.Done()
232 if err := proxyServer.Start(ctx); err != nil {
233 log.Error().Err(err).Msg("Proxy server error")
234 }
235 }()
236
237 // Wait for interrupt or reload signals
238 sigChan := make(chan os.Signal, 1)
239 reloadChan := make(chan os.Signal, 1)
240 signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
241 signal.Notify(reloadChan, syscall.SIGUSR1)
242
243 // Handle signals in a loop
244 for {
245 select {
246 case <-sigChan:
247 log.Info().Msg("Received shutdown signal")
248 goto shutdown
249 case <-reloadChan:
250 log.Info().Msg("Received manual reload signal (SIGUSR1)")
251 // Trigger manual reload event
252 if err := eventBus.Publish(events.ManualReload, nil); err != nil {
253 log.Error().Err(err).Msg("Failed to publish manual reload event")
254 } else {
255 log.Info().Msg("Manual reload event published successfully")
256 }
257 }
258 }
259
260shutdown:
261
262 log.Info().Msg("Shutting down servers...")
263 cancel()
264
265 // Stop all managed containers with a separate context for shutdown
266 shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
267 defer shutdownCancel()
268
269 log.Info().Msg("Shutting down container manager...")
270 if err := manager.Shutdown(shutdownCtx); err != nil {
271 log.Error().Err(err).Msg("Failed to shutdown container manager")
272 }
273
274 // Stop event bus
275 if err := eventBus.Stop(); err != nil {
276 log.Error().Err(err).Msg("Failed to stop event bus")
277 }
278
279 // Give servers time to shutdown gracefully
280 done := make(chan struct{})
281 go func() {
282 wg.Wait()
283 close(done)
284 }()
285
286 select {
287 case <-done:
288 log.Info().Msg("Servers stopped gracefully")
289 case <-time.After(10 * time.Second):
290 log.Warn().Msg("Force shutdown after timeout")
291 }
292}
293
294// createPidFile creates a PID file for the Gordon process
295func createPidFile() string {
296 pid := os.Getpid()
297
298 // Try multiple locations for the PID file
299 locations := []string{
300 "/tmp/gordon.pid",
301 filepath.Join(os.TempDir(), "gordon.pid"),
302 }
303
304 // Also try user's home directory
305 if homeDir, err := os.UserHomeDir(); err == nil {
306 locations = append(locations, filepath.Join(homeDir, ".gordon.pid"))
307 }
308
309 for _, location := range locations {
310 if err := os.WriteFile(location, []byte(fmt.Sprintf("%d", pid)), 0644); err == nil {
311 log.Debug().Str("pid_file", location).Int("pid", pid).Msg("Created PID file")
312 return location
313 }
314 }
315
316 log.Warn().Int("pid", pid).Msg("Failed to create PID file in any location")
317 return ""
318}
319
320// removePidFile removes the PID file
321func removePidFile(pidFile string) {
322 if err := os.Remove(pidFile); err != nil {
323 log.Warn().Err(err).Str("pid_file", pidFile).Msg("Failed to remove PID file")
324 } else {
325 log.Debug().Str("pid_file", pidFile).Msg("Removed PID file")
326 }
327}