v2.0.0 ae3e752

start.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close