1// Package app provides the application initialization and wiring.
2package app
3
4import (
5 "context"
6 "crypto/rand"
7 "encoding/hex"
8 "fmt"
9 "net/http"
10 "os"
11 "os/signal"
12 "path/filepath"
13 "syscall"
14 "time"
15
16 "github.com/bnema/zerowrap"
17 "github.com/fsnotify/fsnotify"
18 "github.com/spf13/viper"
19
20 // Adapters - Output
21 "gordon/internal/adapters/out/docker"
22 "gordon/internal/adapters/out/envloader"
23 "gordon/internal/adapters/out/eventbus"
24 "gordon/internal/adapters/out/filesystem"
25 "gordon/internal/adapters/out/logwriter"
26 "gordon/internal/adapters/out/secrets"
27 "gordon/internal/adapters/out/tokenstore"
28
29 // Adapters - Input
30 "gordon/internal/adapters/in/http/middleware"
31 "gordon/internal/adapters/in/http/registry"
32
33 // Boundaries
34 "gordon/internal/boundaries/out"
35
36 // Domain
37 "gordon/internal/domain"
38
39 // Use cases
40 "gordon/internal/usecase/auth"
41 "gordon/internal/usecase/config"
42 "gordon/internal/usecase/container"
43 "gordon/internal/usecase/proxy"
44 registrySvc "gordon/internal/usecase/registry"
45)
46
47// Config holds the application configuration.
48type Config struct {
49 Server struct {
50 Port int `mapstructure:"port"`
51 RegistryPort int `mapstructure:"registry_port"`
52 RegistryDomain string `mapstructure:"registry_domain"`
53 DataDir string `mapstructure:"data_dir"`
54 } `mapstructure:"server"`
55
56 Logging struct {
57 Level string `mapstructure:"level"`
58 Format string `mapstructure:"format"`
59 File struct {
60 Enabled bool `mapstructure:"enabled"`
61 Path string `mapstructure:"path"`
62 MaxSize int `mapstructure:"max_size"`
63 MaxBackups int `mapstructure:"max_backups"`
64 MaxAge int `mapstructure:"max_age"`
65 } `mapstructure:"file"`
66 ContainerLogs struct {
67 Enabled bool `mapstructure:"enabled"`
68 Dir string `mapstructure:"dir"`
69 MaxSize int `mapstructure:"max_size"`
70 MaxBackups int `mapstructure:"max_backups"`
71 MaxAge int `mapstructure:"max_age"`
72 } `mapstructure:"container_logs"`
73 } `mapstructure:"logging"`
74
75 Env struct {
76 Dir string `mapstructure:"dir"`
77 } `mapstructure:"env"`
78
79 Secrets struct {
80 Backend string `mapstructure:"backend"` // "pass", "sops", or "unsafe"
81 } `mapstructure:"secrets"`
82
83 RegistryAuth struct {
84 Enabled bool `mapstructure:"enabled"`
85 Type string `mapstructure:"type"` // "password" or "token"
86 Username string `mapstructure:"username"`
87 Password string `mapstructure:"password"` // deprecated: use password_hash
88 PasswordHash string `mapstructure:"password_hash"` // path in secrets backend
89 TokenSecret string `mapstructure:"token_secret"` // path in secrets backend
90 TokenExpiry string `mapstructure:"token_expiry"` // e.g., "720h"
91 } `mapstructure:"registry_auth"`
92}
93
94// services holds all the services used by the application.
95type services struct {
96 runtime *docker.Runtime
97 eventBus *eventbus.InMemory
98 blobStorage *filesystem.BlobStorage
99 manifestStorage *filesystem.ManifestStorage
100 envLoader *envloader.FileLoader
101 logWriter *logwriter.LogWriter
102 tokenStore out.TokenStore
103 configSvc *config.Service
104 containerSvc *container.Service
105 registrySvc *registrySvc.Service
106 proxySvc *proxy.Service
107 authSvc *auth.Service
108 tokenHandler *registry.TokenHandler
109 internalRegUser string
110 internalRegPass string
111}
112
113// Run initializes and starts the Gordon application.
114func Run(ctx context.Context, configPath string) error {
115 // Load configuration
116 v, cfg, err := initConfig(configPath)
117 if err != nil {
118 return err
119 }
120
121 // Initialize logger
122 log, cleanup, err := initLogger(cfg)
123 if err != nil {
124 return err
125 }
126 if cleanup != nil {
127 defer cleanup()
128 }
129
130 ctx = zerowrap.WithCtx(ctx, log)
131 log.Info().Msg("Gordon starting")
132
133 // Create PID file
134 pidFile := createPidFile(log)
135 if pidFile != "" {
136 defer removePidFile(pidFile, log)
137 }
138
139 // Create all services
140 svc, err := createServices(ctx, v, cfg, log)
141 if err != nil {
142 return err
143 }
144
145 // Register event handlers
146 if err := registerEventHandlers(ctx, svc); err != nil {
147 return err
148 }
149
150 // Set up config hot reload
151 setupConfigHotReload(ctx, v, svc, log)
152
153 // Start event bus
154 if err := svc.eventBus.Start(); err != nil {
155 return log.WrapErr(err, "failed to start event bus")
156 }
157 defer svc.eventBus.Stop()
158
159 // Sync and auto-start containers
160 syncAndAutoStart(ctx, svc, log)
161
162 // Create HTTP handlers
163 registryHandler, proxyHandler := createHTTPHandlers(svc, cfg, log)
164
165 // Start servers and wait for shutdown
166 return runServers(ctx, cfg, registryHandler, proxyHandler, svc.containerSvc, log)
167}
168
169// initConfig loads configuration from file.
170func initConfig(configPath string) (*viper.Viper, Config, error) {
171 v := viper.New()
172 if err := loadConfig(v, configPath); err != nil {
173 return nil, Config{}, fmt.Errorf("failed to load config: %w", err)
174 }
175
176 var cfg Config
177 if err := v.Unmarshal(&cfg); err != nil {
178 return nil, Config{}, fmt.Errorf("failed to unmarshal config: %w", err)
179 }
180
181 return v, cfg, nil
182}
183
184// initLogger initializes the zerowrap logger.
185func initLogger(cfg Config) (zerowrap.Logger, func(), error) {
186 logConfig := zerowrap.Config{
187 Level: cfg.Logging.Level,
188 Format: cfg.Logging.Format,
189 }
190
191 if cfg.Logging.File.Enabled {
192 log, cleanup, err := zerowrap.NewWithFile(logConfig, zerowrap.FileConfig{
193 Enabled: true,
194 Path: cfg.Logging.File.Path,
195 MaxSize: cfg.Logging.File.MaxSize,
196 MaxBackups: cfg.Logging.File.MaxBackups,
197 MaxAge: cfg.Logging.File.MaxAge,
198 Compress: true,
199 })
200 if err != nil {
201 return zerowrap.Default(), nil, fmt.Errorf("failed to create logger with file: %w", err)
202 }
203 return log, cleanup, nil
204 }
205
206 return zerowrap.New(logConfig), nil, nil
207}
208
209// createServices creates all the application services.
210func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowrap.Logger) (*services, error) {
211 svc := &services{}
212 var err error
213
214 // Create output adapters
215 if svc.runtime, svc.eventBus, err = createOutputAdapters(ctx, log); err != nil {
216 return nil, err
217 }
218
219 // Create storage
220 if svc.blobStorage, svc.manifestStorage, err = createStorage(cfg, log); err != nil {
221 return nil, err
222 }
223
224 // Create env loader
225 if svc.envLoader, err = createEnvLoader(cfg, log); err != nil {
226 return nil, err
227 }
228
229 // Create log writer
230 if svc.logWriter, err = createLogWriter(cfg, log); err != nil {
231 return nil, err
232 }
233
234 // Create auth service (if enabled)
235 if svc.tokenStore, svc.authSvc, err = createAuthService(ctx, cfg, log); err != nil {
236 return nil, err
237 }
238
239 // Generate internal registry credentials for loopback-only pulls
240 if cfg.RegistryAuth.Enabled {
241 svc.internalRegUser, svc.internalRegPass, err = generateInternalRegistryAuth()
242 if err != nil {
243 return nil, log.WrapErr(err, "failed to generate internal registry credentials")
244 }
245 log.Debug().Msg("internal registry auth generated for loopback pulls")
246 }
247
248 // Create use case services
249 svc.configSvc = config.NewService(v, svc.eventBus)
250 if err := svc.configSvc.Load(ctx); err != nil {
251 return nil, log.WrapErr(err, "failed to load configuration")
252 }
253
254 svc.containerSvc = createContainerService(v, cfg, svc)
255 svc.registrySvc = registrySvc.NewService(svc.blobStorage, svc.manifestStorage, svc.eventBus)
256 svc.proxySvc = proxy.NewService(svc.runtime, svc.containerSvc, svc.configSvc, proxy.Config{
257 RegistryDomain: cfg.Server.RegistryDomain,
258 RegistryPort: cfg.Server.RegistryPort,
259 })
260
261 // Create token handler for registry token endpoint
262 if svc.authSvc != nil {
263 internalAuth := registry.InternalAuth{
264 Username: svc.internalRegUser,
265 Password: svc.internalRegPass,
266 }
267 svc.tokenHandler = registry.NewTokenHandler(svc.authSvc, internalAuth, log)
268 }
269
270 return svc, nil
271}
272
273// createOutputAdapters creates the Docker runtime and event bus.
274func createOutputAdapters(ctx context.Context, log zerowrap.Logger) (*docker.Runtime, *eventbus.InMemory, error) {
275 runtime, err := docker.NewRuntime()
276 if err != nil {
277 return nil, nil, log.WrapErr(err, "failed to create Docker runtime")
278 }
279
280 if err := runtime.Ping(ctx); err != nil {
281 return nil, nil, log.WrapErr(err, "Docker is not available")
282 }
283
284 dockerVersion, _ := runtime.Version(ctx)
285 log.Info().Str("docker_version", dockerVersion).Msg("Docker runtime initialized")
286
287 eventBus := eventbus.NewInMemory(100, log)
288
289 return runtime, eventBus, nil
290}
291
292// createStorage creates blob and manifest storage.
293func createStorage(cfg Config, log zerowrap.Logger) (*filesystem.BlobStorage, *filesystem.ManifestStorage, error) {
294 dataDir := cfg.Server.DataDir
295 if dataDir == "" {
296 dataDir = DefaultDataDir()
297 }
298
299 registryDir := filepath.Join(dataDir, "registry")
300
301 blobStorage, err := filesystem.NewBlobStorage(registryDir, log)
302 if err != nil {
303 return nil, nil, log.WrapErr(err, "failed to create blob storage")
304 }
305
306 manifestStorage, err := filesystem.NewManifestStorage(registryDir, log)
307 if err != nil {
308 return nil, nil, log.WrapErr(err, "failed to create manifest storage")
309 }
310
311 return blobStorage, manifestStorage, nil
312}
313
314// createEnvLoader creates the environment loader with secret providers.
315func createEnvLoader(cfg Config, log zerowrap.Logger) (*envloader.FileLoader, error) {
316 dataDir := cfg.Server.DataDir
317 if dataDir == "" {
318 dataDir = DefaultDataDir()
319 }
320
321 envDir := cfg.Env.Dir
322 if envDir == "" {
323 envDir = filepath.Join(dataDir, "env")
324 }
325
326 envLoader, err := envloader.NewFileLoader(envDir, log)
327 if err != nil {
328 return nil, log.WrapErr(err, "failed to create env loader")
329 }
330
331 // Register secret providers
332 passProvider := secrets.NewPassProvider(log)
333 if passProvider.IsAvailable() {
334 envLoader.RegisterSecretProvider(passProvider)
335 log.Debug().Msg("pass secret provider registered")
336 }
337
338 sopsProvider := secrets.NewSopsProvider(log)
339 if sopsProvider.IsAvailable() {
340 envLoader.RegisterSecretProvider(sopsProvider)
341 log.Debug().Msg("sops secret provider registered")
342 }
343
344 return envLoader, nil
345}
346
347// createLogWriter creates the container log writer.
348func createLogWriter(cfg Config, log zerowrap.Logger) (*logwriter.LogWriter, error) {
349 if !cfg.Logging.ContainerLogs.Enabled {
350 log.Debug().Msg("container log collection disabled")
351 return nil, nil
352 }
353
354 // Determine log directory
355 logDir := cfg.Logging.ContainerLogs.Dir
356 if logDir == "" {
357 dataDir := cfg.Server.DataDir
358 if dataDir == "" {
359 dataDir = DefaultDataDir()
360 }
361 logDir = filepath.Join(dataDir, "logs", "containers")
362 }
363
364 writer, err := logwriter.New(logwriter.Config{
365 Dir: logDir,
366 MaxSize: cfg.Logging.ContainerLogs.MaxSize,
367 MaxBackups: cfg.Logging.ContainerLogs.MaxBackups,
368 MaxAge: cfg.Logging.ContainerLogs.MaxAge,
369 })
370 if err != nil {
371 return nil, log.WrapErr(err, "failed to create container log writer")
372 }
373
374 log.Info().Str("dir", logDir).Msg("container log collection enabled")
375 return writer, nil
376}
377
378const internalRegistryUsername = "gordon-internal"
379
380func generateInternalRegistryAuth() (string, string, error) {
381 password, err := randomTokenHex(32)
382 if err != nil {
383 return "", "", err
384 }
385 return internalRegistryUsername, password, nil
386}
387
388func randomTokenHex(size int) (string, error) {
389 buf := make([]byte, size)
390 if _, err := rand.Read(buf); err != nil {
391 return "", err
392 }
393 return hex.EncodeToString(buf), nil
394}
395
396// createAuthService creates the authentication service and token store.
397func createAuthService(ctx context.Context, cfg Config, log zerowrap.Logger) (out.TokenStore, *auth.Service, error) {
398 if !cfg.RegistryAuth.Enabled {
399 return nil, nil, nil
400 }
401
402 authType := resolveAuthType(cfg.RegistryAuth.Type)
403 backend := resolveSecretsBackend(cfg.Secrets.Backend)
404 dataDir := resolveDataDir(cfg.Server.DataDir)
405
406 store, err := createTokenStore(authType, backend, dataDir, log)
407 if err != nil {
408 return nil, nil, err
409 }
410
411 authConfig, err := buildAuthConfig(ctx, cfg, authType, backend, dataDir, log)
412 if err != nil {
413 return nil, nil, err
414 }
415
416 authSvc := auth.NewService(authConfig, store, log)
417
418 log.Info().
419 Str("type", string(authType)).
420 Str("backend", string(backend)).
421 Msg("registry authentication enabled")
422
423 return store, authSvc, nil
424}
425
426func resolveAuthType(authType string) domain.AuthType {
427 if authType == "token" {
428 return domain.AuthTypeToken
429 }
430 return domain.AuthTypePassword
431}
432
433func resolveSecretsBackend(backend string) domain.SecretsBackend {
434 switch backend {
435 case "pass":
436 return domain.SecretsBackendPass
437 case "sops":
438 return domain.SecretsBackendSops
439 case "unsafe", "":
440 return domain.SecretsBackendUnsafe
441 default:
442 return domain.SecretsBackendUnsafe
443 }
444}
445
446func resolveDataDir(dataDir string) string {
447 if dataDir == "" {
448 return DefaultDataDir()
449 }
450 return dataDir
451}
452
453func createTokenStore(authType domain.AuthType, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (out.TokenStore, error) {
454 if authType != domain.AuthTypeToken {
455 return nil, nil
456 }
457
458 store, err := tokenstore.NewStore(backend, dataDir, log)
459 if err != nil {
460 return nil, log.WrapErr(err, "failed to create token store")
461 }
462 return store, nil
463}
464
465func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (auth.Config, error) {
466 authConfig := auth.Config{
467 Enabled: cfg.RegistryAuth.Enabled,
468 AuthType: authType,
469 Username: cfg.RegistryAuth.Username,
470 }
471
472 switch authType {
473 case domain.AuthTypePassword:
474 hash, err := loadPasswordHash(ctx, cfg, backend, dataDir, log)
475 if err != nil {
476 return auth.Config{}, err
477 }
478 authConfig.PasswordHash = hash
479 case domain.AuthTypeToken:
480 secret, expiry, err := loadTokenConfig(ctx, cfg, backend, dataDir, log)
481 if err != nil {
482 return auth.Config{}, err
483 }
484 authConfig.TokenSecret = secret
485 authConfig.TokenExpiry = expiry
486 }
487
488 return authConfig, nil
489}
490
491func loadPasswordHash(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (string, error) {
492 if cfg.RegistryAuth.PasswordHash != "" {
493 hash, err := loadSecret(ctx, backend, cfg.RegistryAuth.PasswordHash, dataDir, log)
494 if err != nil {
495 return "", log.WrapErr(err, "failed to load password hash")
496 }
497 return hash, nil
498 }
499
500 if cfg.RegistryAuth.Password != "" {
501 log.Warn().Msg("using plain password in config is deprecated, use password_hash with a secrets backend")
502 return cfg.RegistryAuth.Password, nil
503 }
504
505 return "", nil
506}
507
508func loadTokenConfig(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) ([]byte, time.Duration, error) {
509 secret, err := loadTokenSecret(ctx, cfg, backend, dataDir, log)
510 if err != nil {
511 return nil, 0, err
512 }
513
514 expiry, err := parseTokenExpiry(cfg.RegistryAuth.TokenExpiry)
515 if err != nil {
516 return nil, 0, err
517 }
518
519 return secret, expiry, nil
520}
521
522func loadTokenSecret(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) ([]byte, error) {
523 if cfg.RegistryAuth.TokenSecret == "" {
524 return nil, fmt.Errorf("token_secret is required for token authentication")
525 }
526
527 secret, err := loadSecret(ctx, backend, cfg.RegistryAuth.TokenSecret, dataDir, log)
528 if err != nil {
529 return nil, log.WrapErr(err, "failed to load token secret")
530 }
531
532 return []byte(secret), nil
533}
534
535func parseTokenExpiry(expiry string) (time.Duration, error) {
536 if expiry == "" {
537 return 0, nil
538 }
539
540 parsed, err := time.ParseDuration(expiry)
541 if err != nil {
542 return 0, fmt.Errorf("invalid token_expiry: %w", err)
543 }
544
545 return parsed, nil
546}
547
548// loadSecret loads a secret from the configured backend.
549func loadSecret(ctx context.Context, backend domain.SecretsBackend, path, dataDir string, log zerowrap.Logger) (string, error) {
550 switch backend {
551 case domain.SecretsBackendPass:
552 provider := secrets.NewPassProvider(log)
553 return provider.GetSecret(ctx, path)
554 case domain.SecretsBackendSops:
555 provider := secrets.NewSopsProvider(log)
556 return provider.GetSecret(ctx, path)
557 case domain.SecretsBackendUnsafe:
558 // For unsafe backend, path is relative to dataDir/secrets/
559 secretFile := filepath.Join(dataDir, "secrets", path)
560 data, err := os.ReadFile(secretFile)
561 if err != nil {
562 return "", fmt.Errorf("failed to read secret file: %w", err)
563 }
564 return string(data), nil
565 default:
566 return "", fmt.Errorf("unknown secrets backend: %s", backend)
567 }
568}
569
570// createContainerService creates the container service with configuration.
571func createContainerService(v *viper.Viper, cfg Config, svc *services) *container.Service {
572 containerConfig := container.Config{
573 RegistryAuthEnabled: cfg.RegistryAuth.Enabled,
574 RegistryDomain: cfg.Server.RegistryDomain,
575 RegistryPort: cfg.Server.RegistryPort,
576 RegistryUsername: cfg.RegistryAuth.Username,
577 RegistryPassword: cfg.RegistryAuth.Password,
578 InternalRegistryUsername: svc.internalRegUser,
579 InternalRegistryPassword: svc.internalRegPass,
580 VolumeAutoCreate: v.GetBool("volumes.auto_create"),
581 VolumePrefix: v.GetString("volumes.prefix"),
582 VolumePreserve: v.GetBool("volumes.preserve"),
583 NetworkIsolation: v.GetBool("network_isolation.enabled"),
584 NetworkPrefix: v.GetString("network_isolation.network_prefix"),
585 DNSSuffix: v.GetString("network_isolation.dns_suffix"),
586 NetworkGroups: svc.configSvc.GetNetworkGroups(),
587 Attachments: svc.configSvc.GetAttachments(),
588 }
589 return container.NewService(svc.runtime, svc.envLoader, svc.eventBus, svc.logWriter, containerConfig)
590}
591
592// registerEventHandlers registers all event handlers.
593func registerEventHandlers(ctx context.Context, svc *services) error {
594 imagePushedHandler := container.NewImagePushedHandler(ctx, svc.containerSvc, svc.configSvc)
595 if err := svc.eventBus.Subscribe(imagePushedHandler); err != nil {
596 return fmt.Errorf("failed to subscribe image pushed handler: %w", err)
597 }
598
599 configReloadHandler := container.NewConfigReloadHandler(ctx, svc.containerSvc, svc.configSvc)
600 if err := svc.eventBus.Subscribe(configReloadHandler); err != nil {
601 return fmt.Errorf("failed to subscribe config reload handler: %w", err)
602 }
603
604 // Proxy cache invalidation on container deployment (for zero-downtime)
605 containerDeployedHandler := proxy.NewContainerDeployedHandler(ctx, svc.proxySvc)
606 if err := svc.eventBus.Subscribe(containerDeployedHandler); err != nil {
607 return fmt.Errorf("failed to subscribe container deployed handler: %w", err)
608 }
609
610 return nil
611}
612
613// setupConfigHotReload sets up Viper config hot reload.
614func setupConfigHotReload(ctx context.Context, v *viper.Viper, svc *services, log zerowrap.Logger) {
615 v.OnConfigChange(func(e fsnotify.Event) {
616 log.Info().Str("file", e.Name).Msg("config file changed")
617
618 if err := v.ReadInConfig(); err != nil {
619 log.Error().Err(err).Msg("failed to reload config")
620 return
621 }
622
623 if err := svc.configSvc.Load(ctx); err != nil {
624 log.Error().Err(err).Msg("failed to reload configuration")
625 return
626 }
627
628 svc.proxySvc.UpdateConfig(proxy.Config{
629 RegistryDomain: v.GetString("server.registry_domain"),
630 RegistryPort: v.GetInt("server.registry_port"),
631 })
632 })
633 v.WatchConfig()
634}
635
636// syncAndAutoStart syncs existing containers and auto-starts if configured.
637func syncAndAutoStart(ctx context.Context, svc *services, log zerowrap.Logger) {
638 if err := svc.containerSvc.SyncContainers(ctx); err != nil {
639 log.Warn().Err(err).Msg("failed to sync existing containers")
640 }
641
642 if svc.configSvc.IsAutoRouteEnabled() {
643 if err := svc.containerSvc.AutoStart(ctx); err != nil {
644 log.Warn().Err(err).Msg("failed to auto-start containers")
645 }
646 }
647}
648
649// createHTTPHandlers creates HTTP handlers with middleware.
650func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Handler, http.Handler) {
651 registryHandler := registry.NewHandler(svc.registrySvc, svc.blobStorage, svc.eventBus, log)
652
653 registryMiddlewares := []func(http.Handler) http.Handler{
654 middleware.PanicRecovery(log),
655 middleware.RequestLogger(log),
656 }
657
658 if cfg.RegistryAuth.Enabled && svc.authSvc != nil {
659 internalAuth := middleware.InternalRegistryAuth{
660 Username: svc.internalRegUser,
661 Password: svc.internalRegPass,
662 }
663 registryMiddlewares = append(registryMiddlewares,
664 middleware.RegistryAuthV2(svc.authSvc, internalAuth, log))
665 }
666
667 registryWithMiddleware := middleware.Chain(registryMiddlewares...)(registryHandler)
668
669 // Create a mux that routes /v2/token to the token handler (no auth required)
670 // and all other /v2/* routes to the registry handler (with auth)
671 registryMux := http.NewServeMux()
672 if svc.tokenHandler != nil {
673 // Token endpoint is NOT protected by auth - it's where clients get tokens
674 tokenWithLogging := middleware.Chain(
675 middleware.PanicRecovery(log),
676 middleware.RequestLogger(log),
677 )(svc.tokenHandler)
678 registryMux.Handle("/v2/token", tokenWithLogging)
679 }
680 registryMux.Handle("/v2/", registryWithMiddleware)
681
682 proxyMiddlewares := []func(http.Handler) http.Handler{
683 middleware.PanicRecovery(log),
684 middleware.RequestLogger(log),
685 middleware.CORS,
686 }
687
688 proxyWithMiddleware := middleware.Chain(proxyMiddlewares...)(svc.proxySvc)
689
690 return registryMux, proxyWithMiddleware
691}
692
693// runServers starts the HTTP servers and waits for shutdown.
694func runServers(ctx context.Context, cfg Config, registryHandler, proxyHandler http.Handler, containerSvc *container.Service, log zerowrap.Logger) error {
695 ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
696 defer cancel()
697
698 errChan := make(chan error, 2)
699
700 go startServer(fmt.Sprintf(":%d", cfg.Server.RegistryPort), registryHandler, "registry", errChan, log)
701 go startServer(fmt.Sprintf(":%d", cfg.Server.Port), proxyHandler, "proxy", errChan, log)
702
703 log.Info().
704 Int("proxy_port", cfg.Server.Port).
705 Int("registry_port", cfg.Server.RegistryPort).
706 Msg("Gordon is running")
707
708 select {
709 case err := <-errChan:
710 return err
711 case <-ctx.Done():
712 log.Info().Msg("shutdown signal received")
713 }
714
715 log.Info().Msg("shutting down Gordon...")
716
717 if err := containerSvc.Shutdown(ctx); err != nil {
718 log.Warn().Err(err).Msg("error during container shutdown")
719 }
720
721 log.Info().Msg("Gordon stopped")
722 return nil
723}
724
725// startServer starts an HTTP server.
726func startServer(addr string, handler http.Handler, name string, errChan chan<- error, log zerowrap.Logger) {
727 log.Info().Str("address", addr).Msgf("%s server starting", name)
728
729 server := &http.Server{
730 Addr: addr,
731 Handler: handler,
732 ReadHeaderTimeout: 10 * time.Second,
733 }
734
735 if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
736 errChan <- fmt.Errorf("%s server error: %w", name, err)
737 }
738}
739
740// SendReloadSignal sends SIGUSR1 to the running Gordon process.
741func SendReloadSignal() error {
742 pidFile := findPidFile()
743 if pidFile == "" {
744 return fmt.Errorf("gordon PID file not found, is Gordon running?")
745 }
746
747 pidBytes, err := os.ReadFile(pidFile)
748 if err != nil {
749 return fmt.Errorf("failed to read PID file: %w", err)
750 }
751
752 var pid int
753 if _, err := fmt.Sscanf(string(pidBytes), "%d", &pid); err != nil {
754 return fmt.Errorf("failed to parse PID: %w", err)
755 }
756
757 process, err := os.FindProcess(pid)
758 if err != nil {
759 return fmt.Errorf("failed to find process: %w", err)
760 }
761
762 if err := process.Signal(syscall.SIGUSR1); err != nil {
763 return fmt.Errorf("failed to send reload signal: %w", err)
764 }
765
766 return nil
767}
768
769// createPidFile creates a PID file for the Gordon process.
770func createPidFile(log zerowrap.Logger) string {
771 pid := os.Getpid()
772
773 locations := []string{
774 "/tmp/gordon.pid",
775 filepath.Join(os.TempDir(), "gordon.pid"),
776 }
777
778 if homeDir, err := os.UserHomeDir(); err == nil {
779 locations = append(locations, filepath.Join(homeDir, ".gordon.pid"))
780 }
781
782 for _, location := range locations {
783 if err := os.WriteFile(location, []byte(fmt.Sprintf("%d", pid)), 0600); err == nil {
784 log.Debug().Str("pid_file", location).Int("pid", pid).Msg("created PID file")
785 return location
786 }
787 }
788
789 log.Warn().Int("pid", pid).Msg("failed to create PID file in any location")
790 return ""
791}
792
793// removePidFile removes the PID file.
794func removePidFile(pidFile string, log zerowrap.Logger) {
795 if err := os.Remove(pidFile); err != nil {
796 log.Warn().Err(err).Str("pid_file", pidFile).Msg("failed to remove PID file")
797 } else {
798 log.Debug().Str("pid_file", pidFile).Msg("removed PID file")
799 }
800}
801
802// findPidFile finds the Gordon PID file.
803func findPidFile() string {
804 locations := []string{
805 "/tmp/gordon.pid",
806 filepath.Join(os.TempDir(), "gordon.pid"),
807 }
808
809 if homeDir, err := os.UserHomeDir(); err == nil {
810 locations = append(locations, filepath.Join(homeDir, ".gordon.pid"))
811 }
812
813 for _, location := range locations {
814 if _, err := os.Stat(location); err == nil {
815 return location
816 }
817 }
818
819 return ""
820}
821
822// loadConfig loads configuration from file and sets defaults.
823func loadConfig(v *viper.Viper, configPath string) error {
824 v.SetDefault("server.port", 80)
825 v.SetDefault("server.registry_port", 5000)
826 v.SetDefault("server.data_dir", DefaultDataDir())
827 v.SetDefault("logging.level", "info")
828 v.SetDefault("logging.format", "console")
829 v.SetDefault("logging.file.enabled", false)
830 v.SetDefault("logging.file.max_size", 100)
831 v.SetDefault("logging.file.max_backups", 3)
832 v.SetDefault("logging.file.max_age", 28)
833 v.SetDefault("logging.container_logs.enabled", true)
834 v.SetDefault("logging.container_logs.dir", "")
835 v.SetDefault("logging.container_logs.max_size", 100)
836 v.SetDefault("logging.container_logs.max_backups", 3)
837 v.SetDefault("logging.container_logs.max_age", 28)
838 v.SetDefault("env.dir", "") // defaults to {data_dir}/env when empty
839 v.SetDefault("secrets.backend", "unsafe")
840 v.SetDefault("registry_auth.enabled", false)
841 v.SetDefault("registry_auth.type", "password")
842 v.SetDefault("registry_auth.token_expiry", "720h")
843
844 ConfigureViper(v, configPath)
845
846 if err := v.ReadInConfig(); err != nil {
847 if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
848 return fmt.Errorf("failed to read config file: %w", err)
849 }
850 }
851
852 v.SetEnvPrefix("GORDON")
853 v.AutomaticEnv()
854
855 return nil
856}