feat: Implement complete internal event system for automatic deployment
- Add comprehensive event bus with in-memory implementation - Create event types for ImagePushed, ConfigReload, Container lifecycle - Connect registry image push events to automatic container deployment - Implement hot configuration reload through internal event system - Add container event handlers for full lifecycle management - Simplify auth system to registry-only with secure defaults - Update all config files (HCL/TOML/YAML) with clear registry_auth section - Remove complex management API auth (no external APIs in Gordon v2) Core functionality now complete: docker push automatically deploys containers
- commit
- 0642b9432db9c6c81b581c8fc675d69b9d1a99fe
- author
- bamen · · 2025-06-02 16:18 UTC
- parent
- 99766e4
- signature
- unsigned
11 files · +565 -261
M cmd/start.go +19 -1
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | "github.com/spf13/cobra" | |
| 14 | 14 | "gordon/internal/config" | |
| 15 | 15 | "gordon/internal/container" | |
| 16 | + | "gordon/internal/events" | |
| 16 | 17 | "gordon/internal/proxy" | |
| 17 | 18 | "gordon/internal/registry" | |
| 18 | 19 | ) | |
| @@ -43,12 +44,24 @@ func runStart(cmd *cobra.Command, args []string) { | |||
| 43 | 44 | log.Info().Int("proxy_port", cfg.Server.Port).Msg("Proxy server") | |
| 44 | 45 | log.Info().Str("runtime", cfg.Server.Runtime).Msg("Container runtime") | |
| 45 | 46 | ||
| 47 | + | // Initialize event bus | |
| 48 | + | eventBus := events.NewInMemoryEventBus(100) | |
| 49 | + | if err := eventBus.Start(); err != nil { | |
| 50 | + | log.Fatal().Err(err).Msg("Failed to start event bus") | |
| 51 | + | } | |
| 52 | + | ||
| 46 | 53 | // Initialize container manager | |
| 47 | 54 | manager, err := container.NewManager(cfg) | |
| 48 | 55 | if err != nil { | |
| 49 | 56 | log.Fatal().Err(err).Msg("Failed to initialize container manager") | |
| 50 | 57 | } | |
| 51 | 58 | ||
| 59 | + | // Create and subscribe container event handler | |
| 60 | + | containerHandler := events.NewContainerEventHandler(manager, cfg) | |
| 61 | + | if err := eventBus.Subscribe(containerHandler); err != nil { | |
| 62 | + | log.Fatal().Err(err).Msg("Failed to subscribe container event handler") | |
| 63 | + | } | |
| 64 | + | ||
| 52 | 65 | // Sync existing containers | |
| 53 | 66 | ctx, cancel := context.WithCancel(context.Background()) | |
| 54 | 67 | if err := manager.SyncContainers(ctx); err != nil { | |
| @@ -58,7 +71,7 @@ func runStart(cmd *cobra.Command, args []string) { | |||
| 58 | 71 | var wg sync.WaitGroup | |
| 59 | 72 | ||
| 60 | 73 | // Start registry server | |
| 61 | - | registryServer, err := registry.NewServer(cfg) | |
| 74 | + | registryServer, err := registry.NewServer(cfg, eventBus) | |
| 62 | 75 | if err != nil { | |
| 63 | 76 | log.Fatal().Err(err).Msg("Failed to initialize registry server") | |
| 64 | 77 | } | |
| @@ -88,6 +101,11 @@ func runStart(cmd *cobra.Command, args []string) { | |||
| 88 | 101 | log.Info().Msg("Shutting down servers...") | |
| 89 | 102 | cancel() | |
| 90 | 103 | ||
| 104 | + | // Stop event bus | |
| 105 | + | if err := eventBus.Stop(); err != nil { | |
| 106 | + | log.Error().Err(err).Msg("Failed to stop event bus") | |
| 107 | + | } | |
| 108 | + | ||
| 91 | 109 | // Give servers time to shutdown gracefully | |
| 92 | 110 | done := make(chan struct{}) | |
| 93 | 111 | go func() { | |
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | "github.com/spf13/cobra" | 13 | "github.com/spf13/cobra" | ||
| 14 | "gordon/internal/config" | 14 | "gordon/internal/config" | ||
| 15 | "gordon/internal/container" | 15 | "gordon/internal/container" | ||
| 16 | + | "gordon/internal/events" | |||
| 16 | "gordon/internal/proxy" | 17 | "gordon/internal/proxy" | ||
| 17 | "gordon/internal/registry" | 18 | "gordon/internal/registry" | ||
| 18 | ) | 19 | ) | ||
| @@ -43,12 +44,24 @@ func runStart(cmd *cobra.Command, args []string) { | |||||
| 43 | log.Info().Int("proxy_port", cfg.Server.Port).Msg("Proxy server") | 44 | log.Info().Int("proxy_port", cfg.Server.Port).Msg("Proxy server") | ||
| 44 | log.Info().Str("runtime", cfg.Server.Runtime).Msg("Container runtime") | 45 | log.Info().Str("runtime", cfg.Server.Runtime).Msg("Container runtime") | ||
| 45 | 46 | ||||
| 47 | + | // Initialize event bus | |||
| 48 | + | eventBus := events.NewInMemoryEventBus(100) | |||
| 49 | + | if err := eventBus.Start(); err != nil { | |||
| 50 | + | log.Fatal().Err(err).Msg("Failed to start event bus") | |||
| 51 | + | } | |||
| 52 | + | ||||
| 46 | // Initialize container manager | 53 | // Initialize container manager | ||
| 47 | manager, err := container.NewManager(cfg) | 54 | manager, err := container.NewManager(cfg) | ||
| 48 | if err != nil { | 55 | if err != nil { | ||
| 49 | log.Fatal().Err(err).Msg("Failed to initialize container manager") | 56 | log.Fatal().Err(err).Msg("Failed to initialize container manager") | ||
| 50 | } | 57 | } | ||
| 51 | 58 | ||||
| 59 | + | // Create and subscribe container event handler | |||
| 60 | + | containerHandler := events.NewContainerEventHandler(manager, cfg) | |||
| 61 | + | if err := eventBus.Subscribe(containerHandler); err != nil { | |||
| 62 | + | log.Fatal().Err(err).Msg("Failed to subscribe container event handler") | |||
| 63 | + | } | |||
| 64 | + | ||||
| 52 | // Sync existing containers | 65 | // Sync existing containers | ||
| 53 | ctx, cancel := context.WithCancel(context.Background()) | 66 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 54 | if err := manager.SyncContainers(ctx); err != nil { | 67 | if err := manager.SyncContainers(ctx); err != nil { | ||
| @@ -58,7 +71,7 @@ func runStart(cmd *cobra.Command, args []string) { | |||||
| 58 | var wg sync.WaitGroup | 71 | var wg sync.WaitGroup | ||
| 59 | 72 | ||||
| 60 | // Start registry server | 73 | // Start registry server | ||
| 61 | - | registryServer, err := registry.NewServer(cfg) | 74 | + | registryServer, err := registry.NewServer(cfg, eventBus) |
| 62 | if err != nil { | 75 | if err != nil { | ||
| 63 | log.Fatal().Err(err).Msg("Failed to initialize registry server") | 76 | log.Fatal().Err(err).Msg("Failed to initialize registry server") | ||
| 64 | } | 77 | } | ||
| @@ -88,6 +101,11 @@ func runStart(cmd *cobra.Command, args []string) { | |||||
| 88 | log.Info().Msg("Shutting down servers...") | 101 | log.Info().Msg("Shutting down servers...") | ||
| 89 | cancel() | 102 | cancel() | ||
| 90 | 103 | ||||
| 104 | + | // Stop event bus | |||
| 105 | + | if err := eventBus.Stop(); err != nil { | |||
| 106 | + | log.Error().Err(err).Msg("Failed to stop event bus") | |||
| 107 | + | } | |||
| 108 | + | ||||
| 91 | // Give servers time to shutdown gracefully | 109 | // Give servers time to shutdown gracefully | ||
| 92 | done := make(chan struct{}) | 110 | done := make(chan struct{}) | ||
| 93 | go func() { | 111 | go func() { | ||
M go.mod +2 -0
| @@ -10,6 +10,8 @@ require ( | |||
| 10 | 10 | github.com/spf13/viper v1.20.1 | |
| 11 | 11 | ) | |
| 12 | 12 | ||
| 13 | + | require github.com/google/uuid v1.6.0 // indirect | |
| 14 | + | ||
| 13 | 15 | require ( | |
| 14 | 16 | github.com/Microsoft/go-winio v0.4.14 // indirect | |
| 15 | 17 | github.com/containerd/errdefs v1.0.0 // indirect | |
| @@ -10,6 +10,8 @@ require ( | |||||
| 10 | github.com/spf13/viper v1.20.1 | 10 | github.com/spf13/viper v1.20.1 | ||
| 11 | ) | 11 | ) | ||
| 12 | 12 | ||||
| 13 | + | require github.com/google/uuid v1.6.0 // indirect | |||
| 14 | + | ||||
| 13 | require ( | 15 | require ( | ||
| 14 | github.com/Microsoft/go-winio v0.4.14 // indirect | 16 | github.com/Microsoft/go-winio v0.4.14 // indirect | ||
| 15 | github.com/containerd/errdefs v1.0.0 // indirect | 17 | github.com/containerd/errdefs v1.0.0 // indirect | ||
M gordon.hcl +6 -0
| @@ -6,6 +6,12 @@ server { | |||
| 6 | 6 | data_dir = "./data" | |
| 7 | 7 | } | |
| 8 | 8 | ||
| 9 | + | registry_auth { | |
| 10 | + | enabled = true | |
| 11 | + | username = "gordon" | |
| 12 | + | password = "secret123" | |
| 13 | + | } | |
| 14 | + | ||
| 9 | 15 | routes = { | |
| 10 | 16 | "example.com" = "nginx:latest" | |
| 11 | 17 | "api.example.com" = "myapi:latest" | |
| @@ -6,6 +6,12 @@ server { | |||||
| 6 | data_dir = "./data" | 6 | data_dir = "./data" | ||
| 7 | } | 7 | } | ||
| 8 | 8 | ||||
| 9 | + | registry_auth { | |||
| 10 | + | enabled = true | |||
| 11 | + | username = "gordon" | |||
| 12 | + | password = "secret123" | |||
| 13 | + | } | |||
| 14 | + | ||||
| 9 | routes = { | 15 | routes = { | ||
| 10 | "example.com" = "nginx:latest" | 16 | "example.com" = "nginx:latest" | ||
| 11 | "api.example.com" = "myapi:latest" | 17 | "api.example.com" = "myapi:latest" | ||
M gordon.toml +4 -20
| @@ -5,26 +5,10 @@ runtime = "docker" | |||
| 5 | 5 | ssl_email = "admin@example.com" | |
| 6 | 6 | data_dir = "./data" | |
| 7 | 7 | ||
| 8 | - | # Authentication and Security Configuration | |
| 9 | - | [auth] | |
| 10 | - | enabled = false # Set to true to enable authentication | |
| 11 | - | method = "basic" # Authentication method: "jwt", "api_key", or "basic" | |
| 12 | - | ||
| 13 | - | # JWT Authentication (recommended for production) | |
| 14 | - | jwt_secret = "" # Generate with: gordon generate jwt-secret | |
| 15 | - | ||
| 16 | - | # API Key Authentication (simple but secure) | |
| 17 | - | api_key = "" # Generate with: gordon generate api-key | |
| 18 | - | ||
| 19 | - | # Basic Authentication (least secure, use only for development) | |
| 20 | - | username = "" # Username for Basic Auth | |
| 21 | - | password = "" # Password for Basic Auth | |
| 22 | - | ||
| 23 | - | # Registry Authentication | |
| 24 | - | registry_auth = false # Enable authentication for Docker registry push/pull | |
| 25 | - | ||
| 26 | - | # IP Whitelist (additional security layer) | |
| 27 | - | allowed_ips = ["127.0.0.1", "::1"] # Whitelist of IPs allowed to access management API | |
| 8 | + | [registry_auth] | |
| 9 | + | enabled = true | |
| 10 | + | username = "gordon" | |
| 11 | + | password = "secret123" | |
| 28 | 12 | ||
| 29 | 13 | [routes] | |
| 30 | 14 | "example.com" = "nginx:latest" | |
| @@ -5,26 +5,10 @@ runtime = "docker" | |||||
| 5 | ssl_email = "admin@example.com" | 5 | ssl_email = "admin@example.com" | ||
| 6 | data_dir = "./data" | 6 | data_dir = "./data" | ||
| 7 | 7 | ||||
| 8 | - | # Authentication and Security Configuration | 8 | + | [registry_auth] |
| 9 | - | [auth] | 9 | + | enabled = true |
| 10 | - | enabled = false # Set to true to enable authentication | 10 | + | username = "gordon" |
| 11 | - | method = "basic" # Authentication method: "jwt", "api_key", or "basic" | 11 | + | password = "secret123" |
| 12 | - | ||||
| 13 | - | # JWT Authentication (recommended for production) | |||
| 14 | - | jwt_secret = "" # Generate with: gordon generate jwt-secret | |||
| 15 | - | ||||
| 16 | - | # API Key Authentication (simple but secure) | |||
| 17 | - | api_key = "" # Generate with: gordon generate api-key | |||
| 18 | - | ||||
| 19 | - | # Basic Authentication (least secure, use only for development) | |||
| 20 | - | username = "" # Username for Basic Auth | |||
| 21 | - | password = "" # Password for Basic Auth | |||
| 22 | - | ||||
| 23 | - | # Registry Authentication | |||
| 24 | - | registry_auth = false # Enable authentication for Docker registry push/pull | |||
| 25 | - | ||||
| 26 | - | # IP Whitelist (additional security layer) | |||
| 27 | - | allowed_ips = ["127.0.0.1", "::1"] # Whitelist of IPs allowed to access management API | |||
| 28 | 12 | ||||
| 29 | [routes] | 13 | [routes] | ||
| 30 | "example.com" = "nginx:latest" | 14 | "example.com" = "nginx:latest" | ||
M gordon.yaml +5 -0
| @@ -5,6 +5,11 @@ server: | |||
| 5 | 5 | ssl_email: admin@example.com | |
| 6 | 6 | data_dir: ./data | |
| 7 | 7 | ||
| 8 | + | registry_auth: | |
| 9 | + | enabled: true | |
| 10 | + | username: gordon | |
| 11 | + | password: secret123 | |
| 12 | + | ||
| 8 | 13 | routes: | |
| 9 | 14 | "example.com": "nginx:latest" | |
| 10 | 15 | "api.example.com": "myapi:latest" | |
| @@ -5,6 +5,11 @@ server: | |||||
| 5 | ssl_email: admin@example.com | 5 | ssl_email: admin@example.com | ||
| 6 | data_dir: ./data | 6 | data_dir: ./data | ||
| 7 | 7 | ||||
| 8 | + | registry_auth: | |||
| 9 | + | enabled: true | |||
| 10 | + | username: gordon | |||
| 11 | + | password: secret123 | |||
| 12 | + | ||||
| 8 | routes: | 13 | routes: | ||
| 9 | "example.com": "nginx:latest" | 14 | "example.com": "nginx:latest" | ||
| 10 | "api.example.com": "myapi:latest" | 15 | "api.example.com": "myapi:latest" | ||
M internal/config/config.go +15 -36
| @@ -8,9 +8,9 @@ import ( | |||
| 8 | 8 | ) | |
| 9 | 9 | ||
| 10 | 10 | type Config struct { | |
| 11 | - | Server ServerConfig `mapstructure:"server"` | |
| 12 | - | Auth AuthConfig `mapstructure:"auth"` | |
| 13 | - | Routes map[string]string `mapstructure:"routes"` | |
| 11 | + | Server ServerConfig `mapstructure:"server"` | |
| 12 | + | RegistryAuth RegistryAuthConfig `mapstructure:"registry_auth"` | |
| 13 | + | Routes map[string]string `mapstructure:"routes"` | |
| 14 | 14 | } | |
| 15 | 15 | ||
| 16 | 16 | type ServerConfig struct { | |
| @@ -21,15 +21,10 @@ type ServerConfig struct { | |||
| 21 | 21 | DataDir string `mapstructure:"data_dir"` | |
| 22 | 22 | } | |
| 23 | 23 | ||
| 24 | - | type AuthConfig struct { | |
| 25 | - | Enabled bool `mapstructure:"enabled"` | |
| 26 | - | Method string `mapstructure:"method"` // "jwt", "api_key", or "basic" | |
| 27 | - | JWTSecret string `mapstructure:"jwt_secret"` | |
| 28 | - | APIKey string `mapstructure:"api_key"` | |
| 29 | - | Username string `mapstructure:"username"` | |
| 30 | - | Password string `mapstructure:"password"` | |
| 31 | - | RegistryAuth bool `mapstructure:"registry_auth"` | |
| 32 | - | AllowedIPs []string `mapstructure:"allowed_ips"` | |
| 24 | + | type RegistryAuthConfig struct { | |
| 25 | + | Enabled bool `mapstructure:"enabled"` | |
| 26 | + | Username string `mapstructure:"username"` | |
| 27 | + | Password string `mapstructure:"password"` | |
| 33 | 28 | } | |
| 34 | 29 | ||
| 35 | 30 | type Route struct { | |
| @@ -46,10 +41,7 @@ func Load() (*Config, error) { | |||
| 46 | 41 | viper.SetDefault("server.registry_port", 5000) | |
| 47 | 42 | viper.SetDefault("server.runtime", "docker") | |
| 48 | 43 | viper.SetDefault("server.data_dir", "./data") | |
| 49 | - | viper.SetDefault("auth.enabled", false) | |
| 50 | - | viper.SetDefault("auth.method", "basic") | |
| 51 | - | viper.SetDefault("auth.registry_auth", false) | |
| 52 | - | viper.SetDefault("auth.allowed_ips", []string{"127.0.0.1", "::1"}) | |
| 44 | + | viper.SetDefault("registry_auth.enabled", true) | |
| 53 | 45 | ||
| 54 | 46 | // Handle the routes manually since Viper struggles with domain names | |
| 55 | 47 | cfg.Routes = make(map[string]string) | |
| @@ -59,9 +51,9 @@ func Load() (*Config, error) { | |||
| 59 | 51 | return nil, fmt.Errorf("unable to decode server config: %v", err) | |
| 60 | 52 | } | |
| 61 | 53 | ||
| 62 | - | // Get auth config | |
| 63 | - | if err := viper.UnmarshalKey("auth", &cfg.Auth); err != nil { | |
| 64 | - | return nil, fmt.Errorf("unable to decode auth config: %v", err) | |
| 54 | + | // Get registry auth config | |
| 55 | + | if err := viper.UnmarshalKey("registry_auth", &cfg.RegistryAuth); err != nil { | |
| 56 | + | return nil, fmt.Errorf("unable to decode registry auth config: %v", err) | |
| 65 | 57 | } | |
| 66 | 58 | ||
| 67 | 59 | // Get routes manually from the raw config | |
| @@ -85,23 +77,10 @@ func Load() (*Config, error) { | |||
| 85 | 77 | return nil, fmt.Errorf("server.runtime must be 'docker' or 'podman'") | |
| 86 | 78 | } | |
| 87 | 79 | ||
| 88 | - | // Validate auth config | |
| 89 | - | if cfg.Auth.Enabled { | |
| 90 | - | switch cfg.Auth.Method { | |
| 91 | - | case "jwt": | |
| 92 | - | if cfg.Auth.JWTSecret == "" { | |
| 93 | - | return nil, fmt.Errorf("JWT method selected but no jwt_secret provided") | |
| 94 | - | } | |
| 95 | - | case "api_key": | |
| 96 | - | if cfg.Auth.APIKey == "" { | |
| 97 | - | return nil, fmt.Errorf("API key method selected but no api_key provided") | |
| 98 | - | } | |
| 99 | - | case "basic": | |
| 100 | - | if cfg.Auth.Username == "" || cfg.Auth.Password == "" { | |
| 101 | - | return nil, fmt.Errorf("Basic auth method selected but username/password not provided") | |
| 102 | - | } | |
| 103 | - | default: | |
| 104 | - | return nil, fmt.Errorf("invalid auth method: %s (must be 'jwt', 'api_key', or 'basic')", cfg.Auth.Method) | |
| 80 | + | // Validate registry auth config | |
| 81 | + | if cfg.RegistryAuth.Enabled { | |
| 82 | + | if cfg.RegistryAuth.Username == "" || cfg.RegistryAuth.Password == "" { | |
| 83 | + | return nil, fmt.Errorf("Registry auth enabled but username/password not provided") | |
| 105 | 84 | } | |
| 106 | 85 | } | |
| 107 | 86 | ||
| @@ -8,9 +8,9 @@ import ( | |||||
| 8 | ) | 8 | ) | ||
| 9 | 9 | ||||
| 10 | type Config struct { | 10 | type Config struct { | ||
| 11 | - | Server ServerConfig `mapstructure:"server"` | 11 | + | Server ServerConfig `mapstructure:"server"` |
| 12 | - | Auth AuthConfig `mapstructure:"auth"` | 12 | + | RegistryAuth RegistryAuthConfig `mapstructure:"registry_auth"` |
| 13 | - | Routes map[string]string `mapstructure:"routes"` | 13 | + | Routes map[string]string `mapstructure:"routes"` |
| 14 | } | 14 | } | ||
| 15 | 15 | ||||
| 16 | type ServerConfig struct { | 16 | type ServerConfig struct { | ||
| @@ -21,15 +21,10 @@ type ServerConfig struct { | |||||
| 21 | DataDir string `mapstructure:"data_dir"` | 21 | DataDir string `mapstructure:"data_dir"` | ||
| 22 | } | 22 | } | ||
| 23 | 23 | ||||
| 24 | - | type AuthConfig struct { | 24 | + | type RegistryAuthConfig struct { |
| 25 | - | Enabled bool `mapstructure:"enabled"` | 25 | + | Enabled bool `mapstructure:"enabled"` |
| 26 | - | Method string `mapstructure:"method"` // "jwt", "api_key", or "basic" | 26 | + | Username string `mapstructure:"username"` |
| 27 | - | JWTSecret string `mapstructure:"jwt_secret"` | 27 | + | Password string `mapstructure:"password"` |
| 28 | - | APIKey string `mapstructure:"api_key"` | |||
| 29 | - | Username string `mapstructure:"username"` | |||
| 30 | - | Password string `mapstructure:"password"` | |||
| 31 | - | RegistryAuth bool `mapstructure:"registry_auth"` | |||
| 32 | - | AllowedIPs []string `mapstructure:"allowed_ips"` | |||
| 33 | } | 28 | } | ||
| 34 | 29 | ||||
| 35 | type Route struct { | 30 | type Route struct { | ||
| @@ -46,10 +41,7 @@ func Load() (*Config, error) { | |||||
| 46 | viper.SetDefault("server.registry_port", 5000) | 41 | viper.SetDefault("server.registry_port", 5000) | ||
| 47 | viper.SetDefault("server.runtime", "docker") | 42 | viper.SetDefault("server.runtime", "docker") | ||
| 48 | viper.SetDefault("server.data_dir", "./data") | 43 | viper.SetDefault("server.data_dir", "./data") | ||
| 49 | - | viper.SetDefault("auth.enabled", false) | 44 | + | viper.SetDefault("registry_auth.enabled", true) |
| 50 | - | viper.SetDefault("auth.method", "basic") | |||
| 51 | - | viper.SetDefault("auth.registry_auth", false) | |||
| 52 | - | viper.SetDefault("auth.allowed_ips", []string{"127.0.0.1", "::1"}) | |||
| 53 | 45 | ||||
| 54 | // Handle the routes manually since Viper struggles with domain names | 46 | // Handle the routes manually since Viper struggles with domain names | ||
| 55 | cfg.Routes = make(map[string]string) | 47 | cfg.Routes = make(map[string]string) | ||
| @@ -59,9 +51,9 @@ func Load() (*Config, error) { | |||||
| 59 | return nil, fmt.Errorf("unable to decode server config: %v", err) | 51 | return nil, fmt.Errorf("unable to decode server config: %v", err) | ||
| 60 | } | 52 | } | ||
| 61 | 53 | ||||
| 62 | - | // Get auth config | 54 | + | // Get registry auth config |
| 63 | - | if err := viper.UnmarshalKey("auth", &cfg.Auth); err != nil { | 55 | + | if err := viper.UnmarshalKey("registry_auth", &cfg.RegistryAuth); err != nil { |
| 64 | - | return nil, fmt.Errorf("unable to decode auth config: %v", err) | 56 | + | return nil, fmt.Errorf("unable to decode registry auth config: %v", err) |
| 65 | } | 57 | } | ||
| 66 | 58 | ||||
| 67 | // Get routes manually from the raw config | 59 | // Get routes manually from the raw config | ||
| @@ -85,23 +77,10 @@ func Load() (*Config, error) { | |||||
| 85 | return nil, fmt.Errorf("server.runtime must be 'docker' or 'podman'") | 77 | return nil, fmt.Errorf("server.runtime must be 'docker' or 'podman'") | ||
| 86 | } | 78 | } | ||
| 87 | 79 | ||||
| 88 | - | // Validate auth config | 80 | + | // Validate registry auth config |
| 89 | - | if cfg.Auth.Enabled { | 81 | + | if cfg.RegistryAuth.Enabled { |
| 90 | - | switch cfg.Auth.Method { | 82 | + | if cfg.RegistryAuth.Username == "" || cfg.RegistryAuth.Password == "" { |
| 91 | - | case "jwt": | 83 | + | return nil, fmt.Errorf("Registry auth enabled but username/password not provided") |
| 92 | - | if cfg.Auth.JWTSecret == "" { | |||
| 93 | - | return nil, fmt.Errorf("JWT method selected but no jwt_secret provided") | |||
| 94 | - | } | |||
| 95 | - | case "api_key": | |||
| 96 | - | if cfg.Auth.APIKey == "" { | |||
| 97 | - | return nil, fmt.Errorf("API key method selected but no api_key provided") | |||
| 98 | - | } | |||
| 99 | - | case "basic": | |||
| 100 | - | if cfg.Auth.Username == "" || cfg.Auth.Password == "" { | |||
| 101 | - | return nil, fmt.Errorf("Basic auth method selected but username/password not provided") | |||
| 102 | - | } | |||
| 103 | - | default: | |||
| 104 | - | return nil, fmt.Errorf("invalid auth method: %s (must be 'jwt', 'api_key', or 'basic')", cfg.Auth.Method) | |||
| 105 | } | 84 | } | ||
| 106 | } | 85 | } | ||
| 107 | 86 | ||||
A internal/events/bus.go +163 -0
| @@ -0,0 +1,163 @@ | |||
| 1 | + | package events | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | "sync" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | "github.com/google/uuid" | |
| 10 | + | "github.com/rs/zerolog/log" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | type InMemoryEventBus struct { | |
| 14 | + | handlers []EventHandler | |
| 15 | + | eventChan chan Event | |
| 16 | + | done chan struct{} | |
| 17 | + | mu sync.RWMutex | |
| 18 | + | ctx context.Context | |
| 19 | + | cancel context.CancelFunc | |
| 20 | + | bufferSize int | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | func NewInMemoryEventBus(bufferSize int) *InMemoryEventBus { | |
| 24 | + | if bufferSize <= 0 { | |
| 25 | + | bufferSize = 100 | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 29 | + | ||
| 30 | + | return &InMemoryEventBus{ | |
| 31 | + | handlers: make([]EventHandler, 0), | |
| 32 | + | eventChan: make(chan Event, bufferSize), | |
| 33 | + | done: make(chan struct{}), | |
| 34 | + | ctx: ctx, | |
| 35 | + | cancel: cancel, | |
| 36 | + | bufferSize: bufferSize, | |
| 37 | + | } | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | func (bus *InMemoryEventBus) Publish(event Event) error { | |
| 41 | + | if event.ID == "" { | |
| 42 | + | event.ID = uuid.New().String() | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | if event.Timestamp.IsZero() { | |
| 46 | + | event.Timestamp = time.Now() | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | select { | |
| 50 | + | case bus.eventChan <- event: | |
| 51 | + | log.Debug(). | |
| 52 | + | Str("event_id", event.ID). | |
| 53 | + | Str("event_type", string(event.Type)). | |
| 54 | + | Str("image_name", event.ImageName). | |
| 55 | + | Str("tag", event.Tag). | |
| 56 | + | Msg("Event published") | |
| 57 | + | return nil | |
| 58 | + | case <-bus.ctx.Done(): | |
| 59 | + | return fmt.Errorf("event bus is stopped") | |
| 60 | + | default: | |
| 61 | + | return fmt.Errorf("event channel is full, dropping event %s", event.ID) | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | func (bus *InMemoryEventBus) Subscribe(handler EventHandler) error { | |
| 66 | + | bus.mu.Lock() | |
| 67 | + | defer bus.mu.Unlock() | |
| 68 | + | ||
| 69 | + | bus.handlers = append(bus.handlers, handler) | |
| 70 | + | log.Debug(). | |
| 71 | + | Str("handler_type", fmt.Sprintf("%T", handler)). | |
| 72 | + | Int("total_handlers", len(bus.handlers)). | |
| 73 | + | Msg("Event handler subscribed") | |
| 74 | + | ||
| 75 | + | return nil | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | func (bus *InMemoryEventBus) Unsubscribe(handler EventHandler) error { | |
| 79 | + | bus.mu.Lock() | |
| 80 | + | defer bus.mu.Unlock() | |
| 81 | + | ||
| 82 | + | for i, h := range bus.handlers { | |
| 83 | + | if h == handler { | |
| 84 | + | bus.handlers = append(bus.handlers[:i], bus.handlers[i+1:]...) | |
| 85 | + | log.Debug(). | |
| 86 | + | Str("handler_type", fmt.Sprintf("%T", handler)). | |
| 87 | + | Int("total_handlers", len(bus.handlers)). | |
| 88 | + | Msg("Event handler unsubscribed") | |
| 89 | + | return nil | |
| 90 | + | } | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | return fmt.Errorf("handler not found") | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | func (bus *InMemoryEventBus) Start() error { | |
| 97 | + | log.Info(). | |
| 98 | + | Int("buffer_size", bus.bufferSize). | |
| 99 | + | Msg("Starting event bus") | |
| 100 | + | ||
| 101 | + | go bus.processEvents() | |
| 102 | + | return nil | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | func (bus *InMemoryEventBus) Stop() error { | |
| 106 | + | log.Info().Msg("Stopping event bus") | |
| 107 | + | ||
| 108 | + | bus.cancel() | |
| 109 | + | ||
| 110 | + | select { | |
| 111 | + | case <-bus.done: | |
| 112 | + | log.Info().Msg("Event bus stopped") | |
| 113 | + | return nil | |
| 114 | + | case <-time.After(5 * time.Second): | |
| 115 | + | log.Warn().Msg("Event bus stop timeout") | |
| 116 | + | return fmt.Errorf("timeout waiting for event bus to stop") | |
| 117 | + | } | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | func (bus *InMemoryEventBus) processEvents() { | |
| 121 | + | defer close(bus.done) | |
| 122 | + | ||
| 123 | + | for { | |
| 124 | + | select { | |
| 125 | + | case event := <-bus.eventChan: | |
| 126 | + | bus.handleEvent(event) | |
| 127 | + | case <-bus.ctx.Done(): | |
| 128 | + | log.Debug().Msg("Event bus processing stopped") | |
| 129 | + | return | |
| 130 | + | } | |
| 131 | + | } | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | func (bus *InMemoryEventBus) handleEvent(event Event) { | |
| 135 | + | bus.mu.RLock() | |
| 136 | + | handlers := make([]EventHandler, len(bus.handlers)) | |
| 137 | + | copy(handlers, bus.handlers) | |
| 138 | + | bus.mu.RUnlock() | |
| 139 | + | ||
| 140 | + | for _, handler := range handlers { | |
| 141 | + | if handler.CanHandle(event.Type) { | |
| 142 | + | go func(h EventHandler) { | |
| 143 | + | start := time.Now() | |
| 144 | + | ||
| 145 | + | if err := h.Handle(event); err != nil { | |
| 146 | + | log.Error(). | |
| 147 | + | Err(err). | |
| 148 | + | Str("event_id", event.ID). | |
| 149 | + | Str("event_type", string(event.Type)). | |
| 150 | + | Str("handler_type", fmt.Sprintf("%T", h)). | |
| 151 | + | Msg("Error handling event") | |
| 152 | + | } else { | |
| 153 | + | log.Debug(). | |
| 154 | + | Str("event_id", event.ID). | |
| 155 | + | Str("event_type", string(event.Type)). | |
| 156 | + | Str("handler_type", fmt.Sprintf("%T", h)). | |
| 157 | + | Dur("duration", time.Since(start)). | |
| 158 | + | Msg("Event handled successfully") | |
| 159 | + | } | |
| 160 | + | }(handler) | |
| 161 | + | } | |
| 162 | + | } | |
| 163 | + | }⏎̸ | |
| @@ -0,0 +1,163 @@ | |||||
| 1 | + | package events | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | "sync" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | "github.com/google/uuid" | |||
| 10 | + | "github.com/rs/zerolog/log" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | type InMemoryEventBus struct { | |||
| 14 | + | handlers []EventHandler | |||
| 15 | + | eventChan chan Event | |||
| 16 | + | done chan struct{} | |||
| 17 | + | mu sync.RWMutex | |||
| 18 | + | ctx context.Context | |||
| 19 | + | cancel context.CancelFunc | |||
| 20 | + | bufferSize int | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | func NewInMemoryEventBus(bufferSize int) *InMemoryEventBus { | |||
| 24 | + | if bufferSize <= 0 { | |||
| 25 | + | bufferSize = 100 | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 29 | + | ||||
| 30 | + | return &InMemoryEventBus{ | |||
| 31 | + | handlers: make([]EventHandler, 0), | |||
| 32 | + | eventChan: make(chan Event, bufferSize), | |||
| 33 | + | done: make(chan struct{}), | |||
| 34 | + | ctx: ctx, | |||
| 35 | + | cancel: cancel, | |||
| 36 | + | bufferSize: bufferSize, | |||
| 37 | + | } | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | func (bus *InMemoryEventBus) Publish(event Event) error { | |||
| 41 | + | if event.ID == "" { | |||
| 42 | + | event.ID = uuid.New().String() | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | if event.Timestamp.IsZero() { | |||
| 46 | + | event.Timestamp = time.Now() | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | select { | |||
| 50 | + | case bus.eventChan <- event: | |||
| 51 | + | log.Debug(). | |||
| 52 | + | Str("event_id", event.ID). | |||
| 53 | + | Str("event_type", string(event.Type)). | |||
| 54 | + | Str("image_name", event.ImageName). | |||
| 55 | + | Str("tag", event.Tag). | |||
| 56 | + | Msg("Event published") | |||
| 57 | + | return nil | |||
| 58 | + | case <-bus.ctx.Done(): | |||
| 59 | + | return fmt.Errorf("event bus is stopped") | |||
| 60 | + | default: | |||
| 61 | + | return fmt.Errorf("event channel is full, dropping event %s", event.ID) | |||
| 62 | + | } | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | func (bus *InMemoryEventBus) Subscribe(handler EventHandler) error { | |||
| 66 | + | bus.mu.Lock() | |||
| 67 | + | defer bus.mu.Unlock() | |||
| 68 | + | ||||
| 69 | + | bus.handlers = append(bus.handlers, handler) | |||
| 70 | + | log.Debug(). | |||
| 71 | + | Str("handler_type", fmt.Sprintf("%T", handler)). | |||
| 72 | + | Int("total_handlers", len(bus.handlers)). | |||
| 73 | + | Msg("Event handler subscribed") | |||
| 74 | + | ||||
| 75 | + | return nil | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | func (bus *InMemoryEventBus) Unsubscribe(handler EventHandler) error { | |||
| 79 | + | bus.mu.Lock() | |||
| 80 | + | defer bus.mu.Unlock() | |||
| 81 | + | ||||
| 82 | + | for i, h := range bus.handlers { | |||
| 83 | + | if h == handler { | |||
| 84 | + | bus.handlers = append(bus.handlers[:i], bus.handlers[i+1:]...) | |||
| 85 | + | log.Debug(). | |||
| 86 | + | Str("handler_type", fmt.Sprintf("%T", handler)). | |||
| 87 | + | Int("total_handlers", len(bus.handlers)). | |||
| 88 | + | Msg("Event handler unsubscribed") | |||
| 89 | + | return nil | |||
| 90 | + | } | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | return fmt.Errorf("handler not found") | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | func (bus *InMemoryEventBus) Start() error { | |||
| 97 | + | log.Info(). | |||
| 98 | + | Int("buffer_size", bus.bufferSize). | |||
| 99 | + | Msg("Starting event bus") | |||
| 100 | + | ||||
| 101 | + | go bus.processEvents() | |||
| 102 | + | return nil | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | func (bus *InMemoryEventBus) Stop() error { | |||
| 106 | + | log.Info().Msg("Stopping event bus") | |||
| 107 | + | ||||
| 108 | + | bus.cancel() | |||
| 109 | + | ||||
| 110 | + | select { | |||
| 111 | + | case <-bus.done: | |||
| 112 | + | log.Info().Msg("Event bus stopped") | |||
| 113 | + | return nil | |||
| 114 | + | case <-time.After(5 * time.Second): | |||
| 115 | + | log.Warn().Msg("Event bus stop timeout") | |||
| 116 | + | return fmt.Errorf("timeout waiting for event bus to stop") | |||
| 117 | + | } | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | func (bus *InMemoryEventBus) processEvents() { | |||
| 121 | + | defer close(bus.done) | |||
| 122 | + | ||||
| 123 | + | for { | |||
| 124 | + | select { | |||
| 125 | + | case event := <-bus.eventChan: | |||
| 126 | + | bus.handleEvent(event) | |||
| 127 | + | case <-bus.ctx.Done(): | |||
| 128 | + | log.Debug().Msg("Event bus processing stopped") | |||
| 129 | + | return | |||
| 130 | + | } | |||
| 131 | + | } | |||
| 132 | + | } | |||
| 133 | + | ||||
| 134 | + | func (bus *InMemoryEventBus) handleEvent(event Event) { | |||
| 135 | + | bus.mu.RLock() | |||
| 136 | + | handlers := make([]EventHandler, len(bus.handlers)) | |||
| 137 | + | copy(handlers, bus.handlers) | |||
| 138 | + | bus.mu.RUnlock() | |||
| 139 | + | ||||
| 140 | + | for _, handler := range handlers { | |||
| 141 | + | if handler.CanHandle(event.Type) { | |||
| 142 | + | go func(h EventHandler) { | |||
| 143 | + | start := time.Now() | |||
| 144 | + | ||||
| 145 | + | if err := h.Handle(event); err != nil { | |||
| 146 | + | log.Error(). | |||
| 147 | + | Err(err). | |||
| 148 | + | Str("event_id", event.ID). | |||
| 149 | + | Str("event_type", string(event.Type)). | |||
| 150 | + | Str("handler_type", fmt.Sprintf("%T", h)). | |||
| 151 | + | Msg("Error handling event") | |||
| 152 | + | } else { | |||
| 153 | + | log.Debug(). | |||
| 154 | + | Str("event_id", event.ID). | |||
| 155 | + | Str("event_type", string(event.Type)). | |||
| 156 | + | Str("handler_type", fmt.Sprintf("%T", h)). | |||
| 157 | + | Dur("duration", time.Since(start)). | |||
| 158 | + | Msg("Event handled successfully") | |||
| 159 | + | } | |||
| 160 | + | }(handler) | |||
| 161 | + | } | |||
| 162 | + | } | |||
| 163 | + | }⏎̸ | |||
A internal/events/handlers.go +285 -0
| @@ -0,0 +1,285 @@ | |||
| 1 | + | package events | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | "strings" | |
| 7 | + | ||
| 8 | + | "github.com/rs/zerolog/log" | |
| 9 | + | "gordon/internal/config" | |
| 10 | + | "gordon/internal/container" | |
| 11 | + | "gordon/pkg/runtime" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | type ContainerEventHandler struct { | |
| 15 | + | manager *container.Manager | |
| 16 | + | config *config.Config | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | func NewContainerEventHandler(manager *container.Manager, config *config.Config) *ContainerEventHandler { | |
| 20 | + | return &ContainerEventHandler{ | |
| 21 | + | manager: manager, | |
| 22 | + | config: config, | |
| 23 | + | } | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | func (h *ContainerEventHandler) CanHandle(eventType EventType) bool { | |
| 27 | + | switch eventType { | |
| 28 | + | case ImagePushed, ConfigReload, ContainerStop, ContainerStart: | |
| 29 | + | return true | |
| 30 | + | default: | |
| 31 | + | return false | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | func (h *ContainerEventHandler) Handle(event Event) error { | |
| 36 | + | switch event.Type { | |
| 37 | + | case ImagePushed: | |
| 38 | + | return h.handleImagePushed(event) | |
| 39 | + | case ConfigReload: | |
| 40 | + | return h.handleConfigReload(event) | |
| 41 | + | case ContainerStop: | |
| 42 | + | return h.handleContainerStop(event) | |
| 43 | + | case ContainerStart: | |
| 44 | + | return h.handleContainerStart(event) | |
| 45 | + | default: | |
| 46 | + | return fmt.Errorf("unsupported event type: %s", event.Type) | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | func (h *ContainerEventHandler) handleImagePushed(event Event) error { | |
| 51 | + | imageName := event.ImageName | |
| 52 | + | tag := event.Tag | |
| 53 | + | ||
| 54 | + | if imageName == "" { | |
| 55 | + | return fmt.Errorf("image name is required for ImagePushed event") | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | if tag == "" { | |
| 59 | + | tag = "latest" | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | fullImageName := fmt.Sprintf("%s:%s", imageName, tag) | |
| 63 | + | ||
| 64 | + | log.Info(). | |
| 65 | + | Str("image", fullImageName). | |
| 66 | + | Str("event_id", event.ID). | |
| 67 | + | Msg("Processing image push event") | |
| 68 | + | ||
| 69 | + | routes := h.findRoutesForImage(fullImageName) | |
| 70 | + | if len(routes) == 0 { | |
| 71 | + | log.Debug(). | |
| 72 | + | Str("image", fullImageName). | |
| 73 | + | Msg("No routes configured for pushed image") | |
| 74 | + | return nil | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | for _, route := range routes { | |
| 78 | + | if err := h.deployContainerForRoute(route, fullImageName); err != nil { | |
| 79 | + | log.Error(). | |
| 80 | + | Err(err). | |
| 81 | + | Str("route", route). | |
| 82 | + | Str("image", fullImageName). | |
| 83 | + | Msg("Failed to deploy container for route") | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | return nil | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | func (h *ContainerEventHandler) handleConfigReload(event Event) error { | |
| 91 | + | log.Info(). | |
| 92 | + | Str("event_id", event.ID). | |
| 93 | + | Msg("Processing configuration reload event") | |
| 94 | + | ||
| 95 | + | ctx := context.Background() | |
| 96 | + | ||
| 97 | + | // Get current containers managed by Gordon | |
| 98 | + | currentContainers, err := h.manager.ListContainers(ctx, false) | |
| 99 | + | if err != nil { | |
| 100 | + | return fmt.Errorf("failed to list containers: %w", err) | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | // Track which routes are currently active | |
| 104 | + | activeRoutes := make(map[string]*runtime.Container) | |
| 105 | + | for _, container := range currentContainers { | |
| 106 | + | if route, exists := container.Labels["gordon.route"]; exists { | |
| 107 | + | activeRoutes[route] = container | |
| 108 | + | } | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | // Process each configured route | |
| 112 | + | for route, imageName := range h.config.Routes { | |
| 113 | + | if container, exists := activeRoutes[route]; exists { | |
| 114 | + | // Route exists - check if image has changed | |
| 115 | + | currentImage := container.Labels["gordon.image"] | |
| 116 | + | if currentImage != imageName { | |
| 117 | + | log.Info(). | |
| 118 | + | Str("route", route). | |
| 119 | + | Str("old_image", currentImage). | |
| 120 | + | Str("new_image", imageName). | |
| 121 | + | Msg("Image changed for route, redeploying") | |
| 122 | + | ||
| 123 | + | if err := h.deployContainerForRoute(route, imageName); err != nil { | |
| 124 | + | log.Error(). | |
| 125 | + | Err(err). | |
| 126 | + | Str("route", route). | |
| 127 | + | Msg("Failed to redeploy container for route") | |
| 128 | + | } | |
| 129 | + | } | |
| 130 | + | // Remove from active routes so we know it's been processed | |
| 131 | + | delete(activeRoutes, route) | |
| 132 | + | } else { | |
| 133 | + | // New route - deploy container | |
| 134 | + | log.Info(). | |
| 135 | + | Str("route", route). | |
| 136 | + | Str("image", imageName). | |
| 137 | + | Msg("New route detected, deploying container") | |
| 138 | + | ||
| 139 | + | if err := h.deployContainerForRoute(route, imageName); err != nil { | |
| 140 | + | log.Error(). | |
| 141 | + | Err(err). | |
| 142 | + | Str("route", route). | |
| 143 | + | Msg("Failed to deploy container for new route") | |
| 144 | + | } | |
| 145 | + | } | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | // Stop containers for routes that are no longer configured | |
| 149 | + | for route, container := range activeRoutes { | |
| 150 | + | log.Info(). | |
| 151 | + | Str("route", route). | |
| 152 | + | Str("container_id", container.ID). | |
| 153 | + | Msg("Route no longer configured, stopping container") | |
| 154 | + | ||
| 155 | + | if err := h.manager.StopContainer(ctx, container.ID); err != nil { | |
| 156 | + | log.Error(). | |
| 157 | + | Err(err). | |
| 158 | + | Str("container_id", container.ID). | |
| 159 | + | Msg("Failed to stop container for removed route") | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | if err := h.manager.RemoveContainer(ctx, container.ID, true); err != nil { | |
| 163 | + | log.Error(). | |
| 164 | + | Err(err). | |
| 165 | + | Str("container_id", container.ID). | |
| 166 | + | Msg("Failed to remove container for removed route") | |
| 167 | + | } | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | return nil | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | func (h *ContainerEventHandler) handleContainerStop(event Event) error { | |
| 174 | + | containerID := event.ContainerID | |
| 175 | + | if containerID == "" { | |
| 176 | + | return fmt.Errorf("container ID is required for ContainerStop event") | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | log.Info(). | |
| 180 | + | Str("container_id", containerID). | |
| 181 | + | Str("event_id", event.ID). | |
| 182 | + | Msg("Processing container stop event") | |
| 183 | + | ||
| 184 | + | ctx := context.Background() | |
| 185 | + | if err := h.manager.StopContainer(ctx, containerID); err != nil { | |
| 186 | + | return fmt.Errorf("failed to stop container %s: %w", containerID, err) | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | return nil | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | func (h *ContainerEventHandler) handleContainerStart(event Event) error { | |
| 193 | + | containerID := event.ContainerID | |
| 194 | + | if containerID == "" { | |
| 195 | + | return fmt.Errorf("container ID is required for ContainerStart event") | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | log.Info(). | |
| 199 | + | Str("container_id", containerID). | |
| 200 | + | Str("event_id", event.ID). | |
| 201 | + | Msg("Processing container start event") | |
| 202 | + | ||
| 203 | + | ctx := context.Background() | |
| 204 | + | if err := h.manager.StartContainer(ctx, containerID); err != nil { | |
| 205 | + | return fmt.Errorf("failed to start container %s: %w", containerID, err) | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | return nil | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | func (h *ContainerEventHandler) findRoutesForImage(imageName string) []string { | |
| 212 | + | var routes []string | |
| 213 | + | ||
| 214 | + | for route, configuredImage := range h.config.Routes { | |
| 215 | + | if strings.EqualFold(configuredImage, imageName) { | |
| 216 | + | routes = append(routes, route) | |
| 217 | + | } | |
| 218 | + | } | |
| 219 | + | ||
| 220 | + | return routes | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | func (h *ContainerEventHandler) deployContainerForRoute(route, imageName string) error { | |
| 224 | + | ctx := context.Background() | |
| 225 | + | ||
| 226 | + | containers, err := h.manager.ListContainers(ctx, false) | |
| 227 | + | if err != nil { | |
| 228 | + | return fmt.Errorf("failed to list containers: %w", err) | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | containerName := fmt.Sprintf("gordon-%s", strings.ReplaceAll(route, ".", "-")) | |
| 232 | + | ||
| 233 | + | // Stop and remove existing container if it exists | |
| 234 | + | for _, container := range containers { | |
| 235 | + | if container.Name == containerName { | |
| 236 | + | log.Info(). | |
| 237 | + | Str("container_name", containerName). | |
| 238 | + | Str("old_container_id", container.ID). | |
| 239 | + | Msg("Stopping existing container for route") | |
| 240 | + | ||
| 241 | + | if err := h.manager.StopContainer(ctx, container.ID); err != nil { | |
| 242 | + | log.Warn(). | |
| 243 | + | Err(err). | |
| 244 | + | Str("container_id", container.ID). | |
| 245 | + | Msg("Failed to stop existing container") | |
| 246 | + | } | |
| 247 | + | ||
| 248 | + | if err := h.manager.RemoveContainer(ctx, container.ID, true); err != nil { | |
| 249 | + | log.Warn(). | |
| 250 | + | Err(err). | |
| 251 | + | Str("container_id", container.ID). | |
| 252 | + | Msg("Failed to remove existing container") | |
| 253 | + | } | |
| 254 | + | } | |
| 255 | + | } | |
| 256 | + | ||
| 257 | + | containerConfig := &runtime.ContainerConfig{ | |
| 258 | + | Image: imageName, | |
| 259 | + | Name: containerName, | |
| 260 | + | AutoRemove: false, | |
| 261 | + | Labels: map[string]string{ | |
| 262 | + | "gordon.route": route, | |
| 263 | + | "gordon.image": imageName, | |
| 264 | + | "gordon.managed": "true", | |
| 265 | + | }, | |
| 266 | + | } | |
| 267 | + | ||
| 268 | + | container, err := h.manager.CreateContainer(ctx, containerConfig) | |
| 269 | + | if err != nil { | |
| 270 | + | return fmt.Errorf("failed to create container for route %s: %w", route, err) | |
| 271 | + | } | |
| 272 | + | ||
| 273 | + | if err := h.manager.StartContainer(ctx, container.ID); err != nil { | |
| 274 | + | return fmt.Errorf("failed to start container for route %s: %w", route, err) | |
| 275 | + | } | |
| 276 | + | ||
| 277 | + | log.Info(). | |
| 278 | + | Str("route", route). | |
| 279 | + | Str("image", imageName). | |
| 280 | + | Str("container_id", container.ID). | |
| 281 | + | Str("container_name", containerName). | |
| 282 | + | Msg("Successfully deployed container for route") | |
| 283 | + | ||
| 284 | + | return nil | |
| 285 | + | }⏎̸ | |
| @@ -0,0 +1,285 @@ | |||||
| 1 | + | package events | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | "strings" | |||
| 7 | + | ||||
| 8 | + | "github.com/rs/zerolog/log" | |||
| 9 | + | "gordon/internal/config" | |||
| 10 | + | "gordon/internal/container" | |||
| 11 | + | "gordon/pkg/runtime" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | type ContainerEventHandler struct { | |||
| 15 | + | manager *container.Manager | |||
| 16 | + | config *config.Config | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | func NewContainerEventHandler(manager *container.Manager, config *config.Config) *ContainerEventHandler { | |||
| 20 | + | return &ContainerEventHandler{ | |||
| 21 | + | manager: manager, | |||
| 22 | + | config: config, | |||
| 23 | + | } | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | func (h *ContainerEventHandler) CanHandle(eventType EventType) bool { | |||
| 27 | + | switch eventType { | |||
| 28 | + | case ImagePushed, ConfigReload, ContainerStop, ContainerStart: | |||
| 29 | + | return true | |||
| 30 | + | default: | |||
| 31 | + | return false | |||
| 32 | + | } | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | func (h *ContainerEventHandler) Handle(event Event) error { | |||
| 36 | + | switch event.Type { | |||
| 37 | + | case ImagePushed: | |||
| 38 | + | return h.handleImagePushed(event) | |||
| 39 | + | case ConfigReload: | |||
| 40 | + | return h.handleConfigReload(event) | |||
| 41 | + | case ContainerStop: | |||
| 42 | + | return h.handleContainerStop(event) | |||
| 43 | + | case ContainerStart: | |||
| 44 | + | return h.handleContainerStart(event) | |||
| 45 | + | default: | |||
| 46 | + | return fmt.Errorf("unsupported event type: %s", event.Type) | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | func (h *ContainerEventHandler) handleImagePushed(event Event) error { | |||
| 51 | + | imageName := event.ImageName | |||
| 52 | + | tag := event.Tag | |||
| 53 | + | ||||
| 54 | + | if imageName == "" { | |||
| 55 | + | return fmt.Errorf("image name is required for ImagePushed event") | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | if tag == "" { | |||
| 59 | + | tag = "latest" | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | fullImageName := fmt.Sprintf("%s:%s", imageName, tag) | |||
| 63 | + | ||||
| 64 | + | log.Info(). | |||
| 65 | + | Str("image", fullImageName). | |||
| 66 | + | Str("event_id", event.ID). | |||
| 67 | + | Msg("Processing image push event") | |||
| 68 | + | ||||
| 69 | + | routes := h.findRoutesForImage(fullImageName) | |||
| 70 | + | if len(routes) == 0 { | |||
| 71 | + | log.Debug(). | |||
| 72 | + | Str("image", fullImageName). | |||
| 73 | + | Msg("No routes configured for pushed image") | |||
| 74 | + | return nil | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | for _, route := range routes { | |||
| 78 | + | if err := h.deployContainerForRoute(route, fullImageName); err != nil { | |||
| 79 | + | log.Error(). | |||
| 80 | + | Err(err). | |||
| 81 | + | Str("route", route). | |||
| 82 | + | Str("image", fullImageName). | |||
| 83 | + | Msg("Failed to deploy container for route") | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | return nil | |||
| 88 | + | } | |||
| 89 | + | ||||
| 90 | + | func (h *ContainerEventHandler) handleConfigReload(event Event) error { | |||
| 91 | + | log.Info(). | |||
| 92 | + | Str("event_id", event.ID). | |||
| 93 | + | Msg("Processing configuration reload event") | |||
| 94 | + | ||||
| 95 | + | ctx := context.Background() | |||
| 96 | + | ||||
| 97 | + | // Get current containers managed by Gordon | |||
| 98 | + | currentContainers, err := h.manager.ListContainers(ctx, false) | |||
| 99 | + | if err != nil { | |||
| 100 | + | return fmt.Errorf("failed to list containers: %w", err) | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | // Track which routes are currently active | |||
| 104 | + | activeRoutes := make(map[string]*runtime.Container) | |||
| 105 | + | for _, container := range currentContainers { | |||
| 106 | + | if route, exists := container.Labels["gordon.route"]; exists { | |||
| 107 | + | activeRoutes[route] = container | |||
| 108 | + | } | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | // Process each configured route | |||
| 112 | + | for route, imageName := range h.config.Routes { | |||
| 113 | + | if container, exists := activeRoutes[route]; exists { | |||
| 114 | + | // Route exists - check if image has changed | |||
| 115 | + | currentImage := container.Labels["gordon.image"] | |||
| 116 | + | if currentImage != imageName { | |||
| 117 | + | log.Info(). | |||
| 118 | + | Str("route", route). | |||
| 119 | + | Str("old_image", currentImage). | |||
| 120 | + | Str("new_image", imageName). | |||
| 121 | + | Msg("Image changed for route, redeploying") | |||
| 122 | + | ||||
| 123 | + | if err := h.deployContainerForRoute(route, imageName); err != nil { | |||
| 124 | + | log.Error(). | |||
| 125 | + | Err(err). | |||
| 126 | + | Str("route", route). | |||
| 127 | + | Msg("Failed to redeploy container for route") | |||
| 128 | + | } | |||
| 129 | + | } | |||
| 130 | + | // Remove from active routes so we know it's been processed | |||
| 131 | + | delete(activeRoutes, route) | |||
| 132 | + | } else { | |||
| 133 | + | // New route - deploy container | |||
| 134 | + | log.Info(). | |||
| 135 | + | Str("route", route). | |||
| 136 | + | Str("image", imageName). | |||
| 137 | + | Msg("New route detected, deploying container") | |||
| 138 | + | ||||
| 139 | + | if err := h.deployContainerForRoute(route, imageName); err != nil { | |||
| 140 | + | log.Error(). | |||
| 141 | + | Err(err). | |||
| 142 | + | Str("route", route). | |||
| 143 | + | Msg("Failed to deploy container for new route") | |||
| 144 | + | } | |||
| 145 | + | } | |||
| 146 | + | } | |||
| 147 | + | ||||
| 148 | + | // Stop containers for routes that are no longer configured | |||
| 149 | + | for route, container := range activeRoutes { | |||
| 150 | + | log.Info(). | |||
| 151 | + | Str("route", route). | |||
| 152 | + | Str("container_id", container.ID). | |||
| 153 | + | Msg("Route no longer configured, stopping container") | |||
| 154 | + | ||||
| 155 | + | if err := h.manager.StopContainer(ctx, container.ID); err != nil { | |||
| 156 | + | log.Error(). | |||
| 157 | + | Err(err). | |||
| 158 | + | Str("container_id", container.ID). | |||
| 159 | + | Msg("Failed to stop container for removed route") | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | if err := h.manager.RemoveContainer(ctx, container.ID, true); err != nil { | |||
| 163 | + | log.Error(). | |||
| 164 | + | Err(err). | |||
| 165 | + | Str("container_id", container.ID). | |||
| 166 | + | Msg("Failed to remove container for removed route") | |||
| 167 | + | } | |||
| 168 | + | } | |||
| 169 | + | ||||
| 170 | + | return nil | |||
| 171 | + | } | |||
| 172 | + | ||||
| 173 | + | func (h *ContainerEventHandler) handleContainerStop(event Event) error { | |||
| 174 | + | containerID := event.ContainerID | |||
| 175 | + | if containerID == "" { | |||
| 176 | + | return fmt.Errorf("container ID is required for ContainerStop event") | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | log.Info(). | |||
| 180 | + | Str("container_id", containerID). | |||
| 181 | + | Str("event_id", event.ID). | |||
| 182 | + | Msg("Processing container stop event") | |||
| 183 | + | ||||
| 184 | + | ctx := context.Background() | |||
| 185 | + | if err := h.manager.StopContainer(ctx, containerID); err != nil { | |||
| 186 | + | return fmt.Errorf("failed to stop container %s: %w", containerID, err) | |||
| 187 | + | } | |||
| 188 | + | ||||
| 189 | + | return nil | |||
| 190 | + | } | |||
| 191 | + | ||||
| 192 | + | func (h *ContainerEventHandler) handleContainerStart(event Event) error { | |||
| 193 | + | containerID := event.ContainerID | |||
| 194 | + | if containerID == "" { | |||
| 195 | + | return fmt.Errorf("container ID is required for ContainerStart event") | |||
| 196 | + | } | |||
| 197 | + | ||||
| 198 | + | log.Info(). | |||
| 199 | + | Str("container_id", containerID). | |||
| 200 | + | Str("event_id", event.ID). | |||
| 201 | + | Msg("Processing container start event") | |||
| 202 | + | ||||
| 203 | + | ctx := context.Background() | |||
| 204 | + | if err := h.manager.StartContainer(ctx, containerID); err != nil { | |||
| 205 | + | return fmt.Errorf("failed to start container %s: %w", containerID, err) | |||
| 206 | + | } | |||
| 207 | + | ||||
| 208 | + | return nil | |||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | func (h *ContainerEventHandler) findRoutesForImage(imageName string) []string { | |||
| 212 | + | var routes []string | |||
| 213 | + | ||||
| 214 | + | for route, configuredImage := range h.config.Routes { | |||
| 215 | + | if strings.EqualFold(configuredImage, imageName) { | |||
| 216 | + | routes = append(routes, route) | |||
| 217 | + | } | |||
| 218 | + | } | |||
| 219 | + | ||||
| 220 | + | return routes | |||
| 221 | + | } | |||
| 222 | + | ||||
| 223 | + | func (h *ContainerEventHandler) deployContainerForRoute(route, imageName string) error { | |||
| 224 | + | ctx := context.Background() | |||
| 225 | + | ||||
| 226 | + | containers, err := h.manager.ListContainers(ctx, false) | |||
| 227 | + | if err != nil { | |||
| 228 | + | return fmt.Errorf("failed to list containers: %w", err) | |||
| 229 | + | } | |||
| 230 | + | ||||
| 231 | + | containerName := fmt.Sprintf("gordon-%s", strings.ReplaceAll(route, ".", "-")) | |||
| 232 | + | ||||
| 233 | + | // Stop and remove existing container if it exists | |||
| 234 | + | for _, container := range containers { | |||
| 235 | + | if container.Name == containerName { | |||
| 236 | + | log.Info(). | |||
| 237 | + | Str("container_name", containerName). | |||
| 238 | + | Str("old_container_id", container.ID). | |||
| 239 | + | Msg("Stopping existing container for route") | |||
| 240 | + | ||||
| 241 | + | if err := h.manager.StopContainer(ctx, container.ID); err != nil { | |||
| 242 | + | log.Warn(). | |||
| 243 | + | Err(err). | |||
| 244 | + | Str("container_id", container.ID). | |||
| 245 | + | Msg("Failed to stop existing container") | |||
| 246 | + | } | |||
| 247 | + | ||||
| 248 | + | if err := h.manager.RemoveContainer(ctx, container.ID, true); err != nil { | |||
| 249 | + | log.Warn(). | |||
| 250 | + | Err(err). | |||
| 251 | + | Str("container_id", container.ID). | |||
| 252 | + | Msg("Failed to remove existing container") | |||
| 253 | + | } | |||
| 254 | + | } | |||
| 255 | + | } | |||
| 256 | + | ||||
| 257 | + | containerConfig := &runtime.ContainerConfig{ | |||
| 258 | + | Image: imageName, | |||
| 259 | + | Name: containerName, | |||
| 260 | + | AutoRemove: false, | |||
| 261 | + | Labels: map[string]string{ | |||
| 262 | + | "gordon.route": route, | |||
| 263 | + | "gordon.image": imageName, | |||
| 264 | + | "gordon.managed": "true", | |||
| 265 | + | }, | |||
| 266 | + | } | |||
| 267 | + | ||||
| 268 | + | container, err := h.manager.CreateContainer(ctx, containerConfig) | |||
| 269 | + | if err != nil { | |||
| 270 | + | return fmt.Errorf("failed to create container for route %s: %w", route, err) | |||
| 271 | + | } | |||
| 272 | + | ||||
| 273 | + | if err := h.manager.StartContainer(ctx, container.ID); err != nil { | |||
| 274 | + | return fmt.Errorf("failed to start container for route %s: %w", route, err) | |||
| 275 | + | } | |||
| 276 | + | ||||
| 277 | + | log.Info(). | |||
| 278 | + | Str("route", route). | |||
| 279 | + | Str("image", imageName). | |||
| 280 | + | Str("container_id", container.ID). | |||
| 281 | + | Str("container_name", containerName). | |||
| 282 | + | Msg("Successfully deployed container for route") | |||
| 283 | + | ||||
| 284 | + | return nil | |||
| 285 | + | }⏎̸ | |||
A internal/events/types.go +40 -0
| @@ -0,0 +1,40 @@ | |||
| 1 | + | package events | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "time" | |
| 5 | + | ) | |
| 6 | + | ||
| 7 | + | type EventType string | |
| 8 | + | ||
| 9 | + | const ( | |
| 10 | + | ImagePushed EventType = "image.pushed" | |
| 11 | + | ImageDeleted EventType = "image.deleted" | |
| 12 | + | ConfigReload EventType = "config.reload" | |
| 13 | + | ContainerStop EventType = "container.stop" | |
| 14 | + | ContainerStart EventType = "container.start" | |
| 15 | + | ContainerHealthCheck EventType = "container.health_check" | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | type Event struct { | |
| 19 | + | ID string `json:"id"` | |
| 20 | + | Type EventType `json:"type"` | |
| 21 | + | Timestamp time.Time `json:"timestamp"` | |
| 22 | + | ImageName string `json:"image_name,omitempty"` | |
| 23 | + | Tag string `json:"tag,omitempty"` | |
| 24 | + | Route string `json:"route,omitempty"` | |
| 25 | + | ContainerID string `json:"container_id,omitempty"` | |
| 26 | + | Data map[string]interface{} `json:"data,omitempty"` | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | type EventHandler interface { | |
| 30 | + | Handle(event Event) error | |
| 31 | + | CanHandle(eventType EventType) bool | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | type EventBus interface { | |
| 35 | + | Publish(event Event) error | |
| 36 | + | Subscribe(handler EventHandler) error | |
| 37 | + | Unsubscribe(handler EventHandler) error | |
| 38 | + | Start() error | |
| 39 | + | Stop() error | |
| 40 | + | }⏎̸ | |
| @@ -0,0 +1,40 @@ | |||||
| 1 | + | package events | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "time" | |||
| 5 | + | ) | |||
| 6 | + | ||||
| 7 | + | type EventType string | |||
| 8 | + | ||||
| 9 | + | const ( | |||
| 10 | + | ImagePushed EventType = "image.pushed" | |||
| 11 | + | ImageDeleted EventType = "image.deleted" | |||
| 12 | + | ConfigReload EventType = "config.reload" | |||
| 13 | + | ContainerStop EventType = "container.stop" | |||
| 14 | + | ContainerStart EventType = "container.start" | |||
| 15 | + | ContainerHealthCheck EventType = "container.health_check" | |||
| 16 | + | ) | |||
| 17 | + | ||||
| 18 | + | type Event struct { | |||
| 19 | + | ID string `json:"id"` | |||
| 20 | + | Type EventType `json:"type"` | |||
| 21 | + | Timestamp time.Time `json:"timestamp"` | |||
| 22 | + | ImageName string `json:"image_name,omitempty"` | |||
| 23 | + | Tag string `json:"tag,omitempty"` | |||
| 24 | + | Route string `json:"route,omitempty"` | |||
| 25 | + | ContainerID string `json:"container_id,omitempty"` | |||
| 26 | + | Data map[string]interface{} `json:"data,omitempty"` | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | type EventHandler interface { | |||
| 30 | + | Handle(event Event) error | |||
| 31 | + | CanHandle(eventType EventType) bool | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | type EventBus interface { | |||
| 35 | + | Publish(event Event) error | |||
| 36 | + | Subscribe(handler EventHandler) error | |||
| 37 | + | Unsubscribe(handler EventHandler) error | |||
| 38 | + | Start() error | |||
| 39 | + | Stop() error | |||
| 40 | + | }⏎̸ | |||
M internal/middleware/auth.go +1 -194
| @@ -1,65 +1,14 @@ | |||
| 1 | 1 | package middleware | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "crypto/rand" | |
| 5 | 4 | "crypto/subtle" | |
| 6 | - | "encoding/base64" | |
| 7 | - | "fmt" | |
| 8 | 5 | "net/http" | |
| 9 | 6 | "strings" | |
| 10 | - | "time" | |
| 11 | 7 | ||
| 12 | - | "github.com/golang-jwt/jwt/v5" | |
| 13 | 8 | "github.com/rs/zerolog/log" | |
| 14 | 9 | ) | |
| 15 | 10 | ||
| 16 | - | // BasicAuth middleware provides HTTP Basic Authentication | |
| 17 | - | func BasicAuth(username, password string) func(http.Handler) http.Handler { | |
| 18 | - | return func(next http.Handler) http.Handler { | |
| 19 | - | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 20 | - | if !isAuthenticated(r, username, password) { | |
| 21 | - | w.Header().Set("WWW-Authenticate", `Basic realm="Gordon Management API"`) | |
| 22 | - | http.Error(w, "Unauthorized", http.StatusUnauthorized) | |
| 23 | - | ||
| 24 | - | log.Warn(). | |
| 25 | - | Str("method", r.Method). | |
| 26 | - | Str("path", r.URL.Path). | |
| 27 | - | Str("remote_addr", r.RemoteAddr). | |
| 28 | - | Msg("Unauthorized API access attempt") | |
| 29 | - | return | |
| 30 | - | } | |
| 31 | - | ||
| 32 | - | next.ServeHTTP(w, r) | |
| 33 | - | }) | |
| 34 | - | } | |
| 35 | - | } | |
| 36 | - | ||
| 37 | - | // APIKeyAuth middleware provides API key authentication | |
| 38 | - | func APIKeyAuth(validAPIKey string) func(http.Handler) http.Handler { | |
| 39 | - | return func(next http.Handler) http.Handler { | |
| 40 | - | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 41 | - | apiKey := r.Header.Get("X-API-Key") | |
| 42 | - | if apiKey == "" { | |
| 43 | - | apiKey = r.URL.Query().Get("api_key") | |
| 44 | - | } | |
| 45 | - | ||
| 46 | - | if !isValidAPIKey(apiKey, validAPIKey) { | |
| 47 | - | http.Error(w, "Forbidden - Invalid API Key", http.StatusForbidden) | |
| 48 | - | ||
| 49 | - | log.Warn(). | |
| 50 | - | Str("method", r.Method). | |
| 51 | - | Str("path", r.URL.Path). | |
| 52 | - | Str("remote_addr", r.RemoteAddr). | |
| 53 | - | Msg("Invalid API key access attempt") | |
| 54 | - | return | |
| 55 | - | } | |
| 56 | - | ||
| 57 | - | next.ServeHTTP(w, r) | |
| 58 | - | }) | |
| 59 | - | } | |
| 60 | - | } | |
| 61 | - | ||
| 62 | - | // RegistryAuth middleware provides Docker Registry authentication | |
| 11 | + | // RegistryAuth middleware provides Docker Registry authentication (optional) | |
| 63 | 12 | func RegistryAuth(username, password string) func(http.Handler) http.Handler { | |
| 64 | 13 | return func(next http.Handler) http.Handler { | |
| 65 | 14 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| @@ -88,41 +37,6 @@ func RegistryAuth(username, password string) func(http.Handler) http.Handler { | |||
| 88 | 37 | } | |
| 89 | 38 | } | |
| 90 | 39 | ||
| 91 | - | // IPWhitelist middleware restricts access to specific IP addresses | |
| 92 | - | func IPWhitelist(allowedIPs []string) func(http.Handler) http.Handler { | |
| 93 | - | return func(next http.Handler) http.Handler { | |
| 94 | - | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 95 | - | clientIP := getClientIP(r) | |
| 96 | - | ||
| 97 | - | // Remove port from IP if present | |
| 98 | - | if colonIndex := strings.LastIndex(clientIP, ":"); colonIndex != -1 { | |
| 99 | - | clientIP = clientIP[:colonIndex] | |
| 100 | - | } | |
| 101 | - | ||
| 102 | - | allowed := false | |
| 103 | - | for _, allowedIP := range allowedIPs { | |
| 104 | - | if clientIP == allowedIP || allowedIP == "127.0.0.1" && (clientIP == "::1" || clientIP == "localhost") { | |
| 105 | - | allowed = true | |
| 106 | - | break | |
| 107 | - | } | |
| 108 | - | } | |
| 109 | - | ||
| 110 | - | if !allowed { | |
| 111 | - | http.Error(w, "Forbidden - IP not allowed", http.StatusForbidden) | |
| 112 | - | ||
| 113 | - | log.Warn(). | |
| 114 | - | Str("method", r.Method). | |
| 115 | - | Str("path", r.URL.Path). | |
| 116 | - | Str("client_ip", clientIP). | |
| 117 | - | Msg("IP not in whitelist") | |
| 118 | - | return | |
| 119 | - | } | |
| 120 | - | ||
| 121 | - | next.ServeHTTP(w, r) | |
| 122 | - | }) | |
| 123 | - | } | |
| 124 | - | } | |
| 125 | - | ||
| 126 | 40 | // Helper functions | |
| 127 | 41 | ||
| 128 | 42 | func isAuthenticated(r *http.Request, expectedUsername, expectedPassword string) bool { | |
| @@ -136,111 +50,4 @@ func isAuthenticated(r *http.Request, expectedUsername, expectedPassword string) | |||
| 136 | 50 | passwordMatch := subtle.ConstantTimeCompare([]byte(password), []byte(expectedPassword)) == 1 | |
| 137 | 51 | ||
| 138 | 52 | return usernameMatch && passwordMatch | |
| 139 | - | } | |
| 140 | - | ||
| 141 | - | func isValidAPIKey(provided, expected string) bool { | |
| 142 | - | if provided == "" || expected == "" { | |
| 143 | - | return false | |
| 144 | - | } | |
| 145 | - | ||
| 146 | - | // Use constant-time comparison to prevent timing attacks | |
| 147 | - | return subtle.ConstantTimeCompare([]byte(provided), []byte(expected)) == 1 | |
| 148 | - | } | |
| 149 | - | ||
| 150 | - | // GordonClaims represents the JWT claims for Gordon tokens | |
| 151 | - | type GordonClaims struct { | |
| 152 | - | Username string `json:"username"` | |
| 153 | - | Roles []string `json:"roles"` | |
| 154 | - | jwt.RegisteredClaims | |
| 155 | - | } | |
| 156 | - | ||
| 157 | - | // JWTAuth middleware provides JWT token authentication | |
| 158 | - | func JWTAuth(jwtSecret string) func(http.Handler) http.Handler { | |
| 159 | - | return func(next http.Handler) http.Handler { | |
| 160 | - | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 161 | - | // Extract token from Authorization header | |
| 162 | - | authHeader := r.Header.Get("Authorization") | |
| 163 | - | if authHeader == "" { | |
| 164 | - | http.Error(w, "Authorization header required", http.StatusUnauthorized) | |
| 165 | - | return | |
| 166 | - | } | |
| 167 | - | ||
| 168 | - | // Check for Bearer token | |
| 169 | - | tokenString := "" | |
| 170 | - | if strings.HasPrefix(authHeader, "Bearer ") { | |
| 171 | - | tokenString = strings.TrimPrefix(authHeader, "Bearer ") | |
| 172 | - | } else { | |
| 173 | - | http.Error(w, "Bearer token required", http.StatusUnauthorized) | |
| 174 | - | return | |
| 175 | - | } | |
| 176 | - | ||
| 177 | - | // Parse and validate token | |
| 178 | - | token, err := jwt.ParseWithClaims(tokenString, &GordonClaims{}, func(token *jwt.Token) (interface{}, error) { | |
| 179 | - | // Validate signing method | |
| 180 | - | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { | |
| 181 | - | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) | |
| 182 | - | } | |
| 183 | - | return []byte(jwtSecret), nil | |
| 184 | - | }) | |
| 185 | - | ||
| 186 | - | if err != nil { | |
| 187 | - | log.Warn().Err(err). | |
| 188 | - | Str("method", r.Method). | |
| 189 | - | Str("path", r.URL.Path). | |
| 190 | - | Str("remote_addr", r.RemoteAddr). | |
| 191 | - | Msg("Invalid JWT token") | |
| 192 | - | http.Error(w, "Invalid token", http.StatusUnauthorized) | |
| 193 | - | return | |
| 194 | - | } | |
| 195 | - | ||
| 196 | - | if !token.Valid { | |
| 197 | - | http.Error(w, "Invalid token", http.StatusUnauthorized) | |
| 198 | - | return | |
| 199 | - | } | |
| 200 | - | ||
| 201 | - | // Token is valid, continue | |
| 202 | - | next.ServeHTTP(w, r) | |
| 203 | - | }) | |
| 204 | - | } | |
| 205 | - | } | |
| 206 | - | ||
| 207 | - | // GenerateJWTSecret generates a secure random secret for JWT signing | |
| 208 | - | func GenerateJWTSecret() (string, error) { | |
| 209 | - | bytes := make([]byte, 32) // 256 bits | |
| 210 | - | if _, err := rand.Read(bytes); err != nil { | |
| 211 | - | return "", fmt.Errorf("failed to generate JWT secret: %w", err) | |
| 212 | - | } | |
| 213 | - | return base64.URLEncoding.EncodeToString(bytes), nil | |
| 214 | - | } | |
| 215 | - | ||
| 216 | - | // GenerateJWTToken generates a JWT token for a user | |
| 217 | - | func GenerateJWTToken(username string, roles []string, jwtSecret string, validFor time.Duration) (string, error) { | |
| 218 | - | claims := GordonClaims{ | |
| 219 | - | Username: username, | |
| 220 | - | Roles: roles, | |
| 221 | - | RegisteredClaims: jwt.RegisteredClaims{ | |
| 222 | - | Issuer: "gordon", | |
| 223 | - | Subject: username, | |
| 224 | - | ExpiresAt: jwt.NewNumericDate(time.Now().Add(validFor)), | |
| 225 | - | IssuedAt: jwt.NewNumericDate(time.Now()), | |
| 226 | - | NotBefore: jwt.NewNumericDate(time.Now()), | |
| 227 | - | }, | |
| 228 | - | } | |
| 229 | - | ||
| 230 | - | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) | |
| 231 | - | tokenString, err := token.SignedString([]byte(jwtSecret)) | |
| 232 | - | if err != nil { | |
| 233 | - | return "", fmt.Errorf("failed to sign JWT token: %w", err) | |
| 234 | - | } | |
| 235 | - | ||
| 236 | - | return tokenString, nil | |
| 237 | - | } | |
| 238 | - | ||
| 239 | - | // GenerateSecureAPIKey generates a secure random API key | |
| 240 | - | func GenerateSecureAPIKey() (string, error) { | |
| 241 | - | bytes := make([]byte, 24) // 192 bits, base64 encoded will be ~32 chars | |
| 242 | - | if _, err := rand.Read(bytes); err != nil { | |
| 243 | - | return "", fmt.Errorf("failed to generate API key: %w", err) | |
| 244 | - | } | |
| 245 | - | return base64.URLEncoding.EncodeToString(bytes), nil | |
| 246 | 53 | }⏎̸ | |
| @@ -1,65 +1,14 @@ | |||||
| 1 | package middleware | 1 | package middleware | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | - | "crypto/rand" | |||
| 5 | "crypto/subtle" | 4 | "crypto/subtle" | ||
| 6 | - | "encoding/base64" | |||
| 7 | - | "fmt" | |||
| 8 | "net/http" | 5 | "net/http" | ||
| 9 | "strings" | 6 | "strings" | ||
| 10 | - | "time" | |||
| 11 | 7 | ||||
| 12 | - | "github.com/golang-jwt/jwt/v5" | |||
| 13 | "github.com/rs/zerolog/log" | 8 | "github.com/rs/zerolog/log" | ||
| 14 | ) | 9 | ) | ||
| 15 | 10 | ||||
| 16 | - | // BasicAuth middleware provides HTTP Basic Authentication | 11 | + | // RegistryAuth middleware provides Docker Registry authentication (optional) |
| 17 | - | func BasicAuth(username, password string) func(http.Handler) http.Handler { | |||
| 18 | - | return func(next http.Handler) http.Handler { | |||
| 19 | - | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 20 | - | if !isAuthenticated(r, username, password) { | |||
| 21 | - | w.Header().Set("WWW-Authenticate", `Basic realm="Gordon Management API"`) | |||
| 22 | - | http.Error(w, "Unauthorized", http.StatusUnauthorized) | |||
| 23 | - | ||||
| 24 | - | log.Warn(). | |||
| 25 | - | Str("method", r.Method). | |||
| 26 | - | Str("path", r.URL.Path). | |||
| 27 | - | Str("remote_addr", r.RemoteAddr). | |||
| 28 | - | Msg("Unauthorized API access attempt") | |||
| 29 | - | return | |||
| 30 | - | } | |||
| 31 | - | ||||
| 32 | - | next.ServeHTTP(w, r) | |||
| 33 | - | }) | |||
| 34 | - | } | |||
| 35 | - | } | |||
| 36 | - | ||||
| 37 | - | // APIKeyAuth middleware provides API key authentication | |||
| 38 | - | func APIKeyAuth(validAPIKey string) func(http.Handler) http.Handler { | |||
| 39 | - | return func(next http.Handler) http.Handler { | |||
| 40 | - | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 41 | - | apiKey := r.Header.Get("X-API-Key") | |||
| 42 | - | if apiKey == "" { | |||
| 43 | - | apiKey = r.URL.Query().Get("api_key") | |||
| 44 | - | } | |||
| 45 | - | ||||
| 46 | - | if !isValidAPIKey(apiKey, validAPIKey) { | |||
| 47 | - | http.Error(w, "Forbidden - Invalid API Key", http.StatusForbidden) | |||
| 48 | - | ||||
| 49 | - | log.Warn(). | |||
| 50 | - | Str("method", r.Method). | |||
| 51 | - | Str("path", r.URL.Path). | |||
| 52 | - | Str("remote_addr", r.RemoteAddr). | |||
| 53 | - | Msg("Invalid API key access attempt") | |||
| 54 | - | return | |||
| 55 | - | } | |||
| 56 | - | ||||
| 57 | - | next.ServeHTTP(w, r) | |||
| 58 | - | }) | |||
| 59 | - | } | |||
| 60 | - | } | |||
| 61 | - | ||||
| 62 | - | // RegistryAuth middleware provides Docker Registry authentication | |||
| 63 | func RegistryAuth(username, password string) func(http.Handler) http.Handler { | 12 | func RegistryAuth(username, password string) func(http.Handler) http.Handler { | ||
| 64 | return func(next http.Handler) http.Handler { | 13 | return func(next http.Handler) http.Handler { | ||
| 65 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 14 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| @@ -88,41 +37,6 @@ func RegistryAuth(username, password string) func(http.Handler) http.Handler { | |||||
| 88 | } | 37 | } | ||
| 89 | } | 38 | } | ||
| 90 | 39 | ||||
| 91 | - | // IPWhitelist middleware restricts access to specific IP addresses | |||
| 92 | - | func IPWhitelist(allowedIPs []string) func(http.Handler) http.Handler { | |||
| 93 | - | return func(next http.Handler) http.Handler { | |||
| 94 | - | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 95 | - | clientIP := getClientIP(r) | |||
| 96 | - | ||||
| 97 | - | // Remove port from IP if present | |||
| 98 | - | if colonIndex := strings.LastIndex(clientIP, ":"); colonIndex != -1 { | |||
| 99 | - | clientIP = clientIP[:colonIndex] | |||
| 100 | - | } | |||
| 101 | - | ||||
| 102 | - | allowed := false | |||
| 103 | - | for _, allowedIP := range allowedIPs { | |||
| 104 | - | if clientIP == allowedIP || allowedIP == "127.0.0.1" && (clientIP == "::1" || clientIP == "localhost") { | |||
| 105 | - | allowed = true | |||
| 106 | - | break | |||
| 107 | - | } | |||
| 108 | - | } | |||
| 109 | - | ||||
| 110 | - | if !allowed { | |||
| 111 | - | http.Error(w, "Forbidden - IP not allowed", http.StatusForbidden) | |||
| 112 | - | ||||
| 113 | - | log.Warn(). | |||
| 114 | - | Str("method", r.Method). | |||
| 115 | - | Str("path", r.URL.Path). | |||
| 116 | - | Str("client_ip", clientIP). | |||
| 117 | - | Msg("IP not in whitelist") | |||
| 118 | - | return | |||
| 119 | - | } | |||
| 120 | - | ||||
| 121 | - | next.ServeHTTP(w, r) | |||
| 122 | - | }) | |||
| 123 | - | } | |||
| 124 | - | } | |||
| 125 | - | ||||
| 126 | // Helper functions | 40 | // Helper functions | ||
| 127 | 41 | ||||
| 128 | func isAuthenticated(r *http.Request, expectedUsername, expectedPassword string) bool { | 42 | func isAuthenticated(r *http.Request, expectedUsername, expectedPassword string) bool { | ||
| @@ -136,111 +50,4 @@ func isAuthenticated(r *http.Request, expectedUsername, expectedPassword string) | |||||
| 136 | passwordMatch := subtle.ConstantTimeCompare([]byte(password), []byte(expectedPassword)) == 1 | 50 | passwordMatch := subtle.ConstantTimeCompare([]byte(password), []byte(expectedPassword)) == 1 | ||
| 137 | 51 | ||||
| 138 | return usernameMatch && passwordMatch | 52 | return usernameMatch && passwordMatch | ||
| 139 | - | } | |||
| 140 | - | ||||
| 141 | - | func isValidAPIKey(provided, expected string) bool { | |||
| 142 | - | if provided == "" || expected == "" { | |||
| 143 | - | return false | |||
| 144 | - | } | |||
| 145 | - | ||||
| 146 | - | // Use constant-time comparison to prevent timing attacks | |||
| 147 | - | return subtle.ConstantTimeCompare([]byte(provided), []byte(expected)) == 1 | |||
| 148 | - | } | |||
| 149 | - | ||||
| 150 | - | // GordonClaims represents the JWT claims for Gordon tokens | |||
| 151 | - | type GordonClaims struct { | |||
| 152 | - | Username string `json:"username"` | |||
| 153 | - | Roles []string `json:"roles"` | |||
| 154 | - | jwt.RegisteredClaims | |||
| 155 | - | } | |||
| 156 | - | ||||
| 157 | - | // JWTAuth middleware provides JWT token authentication | |||
| 158 | - | func JWTAuth(jwtSecret string) func(http.Handler) http.Handler { | |||
| 159 | - | return func(next http.Handler) http.Handler { | |||
| 160 | - | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 161 | - | // Extract token from Authorization header | |||
| 162 | - | authHeader := r.Header.Get("Authorization") | |||
| 163 | - | if authHeader == "" { | |||
| 164 | - | http.Error(w, "Authorization header required", http.StatusUnauthorized) | |||
| 165 | - | return | |||
| 166 | - | } | |||
| 167 | - | ||||
| 168 | - | // Check for Bearer token | |||
| 169 | - | tokenString := "" | |||
| 170 | - | if strings.HasPrefix(authHeader, "Bearer ") { | |||
| 171 | - | tokenString = strings.TrimPrefix(authHeader, "Bearer ") | |||
| 172 | - | } else { | |||
| 173 | - | http.Error(w, "Bearer token required", http.StatusUnauthorized) | |||
| 174 | - | return | |||
| 175 | - | } | |||
| 176 | - | ||||
| 177 | - | // Parse and validate token | |||
| 178 | - | token, err := jwt.ParseWithClaims(tokenString, &GordonClaims{}, func(token *jwt.Token) (interface{}, error) { | |||
| 179 | - | // Validate signing method | |||
| 180 | - | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { | |||
| 181 | - | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) | |||
| 182 | - | } | |||
| 183 | - | return []byte(jwtSecret), nil | |||
| 184 | - | }) | |||
| 185 | - | ||||
| 186 | - | if err != nil { | |||
| 187 | - | log.Warn().Err(err). | |||
| 188 | - | Str("method", r.Method). | |||
| 189 | - | Str("path", r.URL.Path). | |||
| 190 | - | Str("remote_addr", r.RemoteAddr). | |||
| 191 | - | Msg("Invalid JWT token") | |||
| 192 | - | http.Error(w, "Invalid token", http.StatusUnauthorized) | |||
| 193 | - | return | |||
| 194 | - | } | |||
| 195 | - | ||||
| 196 | - | if !token.Valid { | |||
| 197 | - | http.Error(w, "Invalid token", http.StatusUnauthorized) | |||
| 198 | - | return | |||
| 199 | - | } | |||
| 200 | - | ||||
| 201 | - | // Token is valid, continue | |||
| 202 | - | next.ServeHTTP(w, r) | |||
| 203 | - | }) | |||
| 204 | - | } | |||
| 205 | - | } | |||
| 206 | - | ||||
| 207 | - | // GenerateJWTSecret generates a secure random secret for JWT signing | |||
| 208 | - | func GenerateJWTSecret() (string, error) { | |||
| 209 | - | bytes := make([]byte, 32) // 256 bits | |||
| 210 | - | if _, err := rand.Read(bytes); err != nil { | |||
| 211 | - | return "", fmt.Errorf("failed to generate JWT secret: %w", err) | |||
| 212 | - | } | |||
| 213 | - | return base64.URLEncoding.EncodeToString(bytes), nil | |||
| 214 | - | } | |||
| 215 | - | ||||
| 216 | - | // GenerateJWTToken generates a JWT token for a user | |||
| 217 | - | func GenerateJWTToken(username string, roles []string, jwtSecret string, validFor time.Duration) (string, error) { | |||
| 218 | - | claims := GordonClaims{ | |||
| 219 | - | Username: username, | |||
| 220 | - | Roles: roles, | |||
| 221 | - | RegisteredClaims: jwt.RegisteredClaims{ | |||
| 222 | - | Issuer: "gordon", | |||
| 223 | - | Subject: username, | |||
| 224 | - | ExpiresAt: jwt.NewNumericDate(time.Now().Add(validFor)), | |||
| 225 | - | IssuedAt: jwt.NewNumericDate(time.Now()), | |||
| 226 | - | NotBefore: jwt.NewNumericDate(time.Now()), | |||
| 227 | - | }, | |||
| 228 | - | } | |||
| 229 | - | ||||
| 230 | - | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) | |||
| 231 | - | tokenString, err := token.SignedString([]byte(jwtSecret)) | |||
| 232 | - | if err != nil { | |||
| 233 | - | return "", fmt.Errorf("failed to sign JWT token: %w", err) | |||
| 234 | - | } | |||
| 235 | - | ||||
| 236 | - | return tokenString, nil | |||
| 237 | - | } | |||
| 238 | - | ||||
| 239 | - | // GenerateSecureAPIKey generates a secure random API key | |||
| 240 | - | func GenerateSecureAPIKey() (string, error) { | |||
| 241 | - | bytes := make([]byte, 24) // 192 bits, base64 encoded will be ~32 chars | |||
| 242 | - | if _, err := rand.Read(bytes); err != nil { | |||
| 243 | - | return "", fmt.Errorf("failed to generate API key: %w", err) | |||
| 244 | - | } | |||
| 245 | - | return base64.URLEncoding.EncodeToString(bytes), nil | |||
| 246 | }⏎̸ | 53 | }⏎̸ | ||
M internal/registry/server.go +25 -10
| @@ -11,6 +11,7 @@ import ( | |||
| 11 | 11 | ||
| 12 | 12 | "github.com/rs/zerolog/log" | |
| 13 | 13 | "gordon/internal/config" | |
| 14 | + | "gordon/internal/events" | |
| 14 | 15 | "gordon/internal/middleware" | |
| 15 | 16 | ) | |
| 16 | 17 | ||
| @@ -18,9 +19,10 @@ type Server struct { | |||
| 18 | 19 | config *config.Config | |
| 19 | 20 | mux *http.ServeMux | |
| 20 | 21 | storage Storage | |
| 22 | + | eventBus events.EventBus | |
| 21 | 23 | } | |
| 22 | 24 | ||
| 23 | - | func NewServer(cfg *config.Config) (*Server, error) { | |
| 25 | + | func NewServer(cfg *config.Config, eventBus events.EventBus) (*Server, error) { | |
| 24 | 26 | // Initialize storage | |
| 25 | 27 | storage, err := NewFilesystemStorage(cfg.Server.DataDir + "/registry") | |
| 26 | 28 | if err != nil { | |
| @@ -28,9 +30,10 @@ func NewServer(cfg *config.Config) (*Server, error) { | |||
| 28 | 30 | } | |
| 29 | 31 | ||
| 30 | 32 | s := &Server{ | |
| 31 | - | config: cfg, | |
| 32 | - | mux: http.NewServeMux(), | |
| 33 | - | storage: storage, | |
| 33 | + | config: cfg, | |
| 34 | + | mux: http.NewServeMux(), | |
| 35 | + | storage: storage, | |
| 36 | + | eventBus: eventBus, | |
| 34 | 37 | } | |
| 35 | 38 | s.setupRoutes() | |
| 36 | 39 | return s, nil | |
| @@ -159,10 +162,8 @@ func (s *Server) Start(ctx context.Context) error { | |||
| 159 | 162 | } | |
| 160 | 163 | ||
| 161 | 164 | // Add registry authentication if enabled | |
| 162 | - | if s.config.Auth.Enabled && s.config.Auth.RegistryAuth { | |
| 163 | - | if s.config.Auth.Username != "" && s.config.Auth.Password != "" { | |
| 164 | - | middlewares = append(middlewares, middleware.RegistryAuth(s.config.Auth.Username, s.config.Auth.Password)) | |
| 165 | - | } | |
| 165 | + | if s.config.RegistryAuth.Enabled { | |
| 166 | + | middlewares = append(middlewares, middleware.RegistryAuth(s.config.RegistryAuth.Username, s.config.RegistryAuth.Password)) | |
| 166 | 167 | } | |
| 167 | 168 | ||
| 168 | 169 | // Wrap the mux with middleware | |
| @@ -243,8 +244,22 @@ func (s *Server) handlePutManifest(w http.ResponseWriter, r *http.Request) { | |||
| 243 | 244 | w.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, reference)) | |
| 244 | 245 | w.WriteHeader(http.StatusCreated) | |
| 245 | 246 | ||
| 246 | - | // TODO: Trigger deployment webhook | |
| 247 | - | log.Info().Str("name", name).Str("reference", reference).Msg("Manifest stored - deployment should be triggered") | |
| 247 | + | // Emit image pushed event | |
| 248 | + | event := events.Event{ | |
| 249 | + | Type: events.ImagePushed, | |
| 250 | + | ImageName: name, | |
| 251 | + | Tag: reference, | |
| 252 | + | Data: map[string]interface{}{ | |
| 253 | + | "digest": digest, | |
| 254 | + | "size": len(data), | |
| 255 | + | }, | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | if err := s.eventBus.Publish(event); err != nil { | |
| 259 | + | log.Error().Err(err).Str("name", name).Str("reference", reference).Msg("Failed to publish image pushed event") | |
| 260 | + | } else { | |
| 261 | + | log.Info().Str("name", name).Str("reference", reference).Msg("Image pushed event published") | |
| 262 | + | } | |
| 248 | 263 | } | |
| 249 | 264 | ||
| 250 | 265 | func (s *Server) handleGetBlob(w http.ResponseWriter, r *http.Request) { | |
| @@ -11,6 +11,7 @@ import ( | |||||
| 11 | 11 | ||||
| 12 | "github.com/rs/zerolog/log" | 12 | "github.com/rs/zerolog/log" | ||
| 13 | "gordon/internal/config" | 13 | "gordon/internal/config" | ||
| 14 | + | "gordon/internal/events" | |||
| 14 | "gordon/internal/middleware" | 15 | "gordon/internal/middleware" | ||
| 15 | ) | 16 | ) | ||
| 16 | 17 | ||||
| @@ -18,9 +19,10 @@ type Server struct { | |||||
| 18 | config *config.Config | 19 | config *config.Config | ||
| 19 | mux *http.ServeMux | 20 | mux *http.ServeMux | ||
| 20 | storage Storage | 21 | storage Storage | ||
| 22 | + | eventBus events.EventBus | |||
| 21 | } | 23 | } | ||
| 22 | 24 | ||||
| 23 | - | func NewServer(cfg *config.Config) (*Server, error) { | 25 | + | func NewServer(cfg *config.Config, eventBus events.EventBus) (*Server, error) { |
| 24 | // Initialize storage | 26 | // Initialize storage | ||
| 25 | storage, err := NewFilesystemStorage(cfg.Server.DataDir + "/registry") | 27 | storage, err := NewFilesystemStorage(cfg.Server.DataDir + "/registry") | ||
| 26 | if err != nil { | 28 | if err != nil { | ||
| @@ -28,9 +30,10 @@ func NewServer(cfg *config.Config) (*Server, error) { | |||||
| 28 | } | 30 | } | ||
| 29 | 31 | ||||
| 30 | s := &Server{ | 32 | s := &Server{ | ||
| 31 | - | config: cfg, | 33 | + | config: cfg, |
| 32 | - | mux: http.NewServeMux(), | 34 | + | mux: http.NewServeMux(), |
| 33 | - | storage: storage, | 35 | + | storage: storage, |
| 36 | + | eventBus: eventBus, | |||
| 34 | } | 37 | } | ||
| 35 | s.setupRoutes() | 38 | s.setupRoutes() | ||
| 36 | return s, nil | 39 | return s, nil | ||
| @@ -159,10 +162,8 @@ func (s *Server) Start(ctx context.Context) error { | |||||
| 159 | } | 162 | } | ||
| 160 | 163 | ||||
| 161 | // Add registry authentication if enabled | 164 | // Add registry authentication if enabled | ||
| 162 | - | if s.config.Auth.Enabled && s.config.Auth.RegistryAuth { | 165 | + | if s.config.RegistryAuth.Enabled { |
| 163 | - | if s.config.Auth.Username != "" && s.config.Auth.Password != "" { | 166 | + | middlewares = append(middlewares, middleware.RegistryAuth(s.config.RegistryAuth.Username, s.config.RegistryAuth.Password)) |
| 164 | - | middlewares = append(middlewares, middleware.RegistryAuth(s.config.Auth.Username, s.config.Auth.Password)) | |||
| 165 | - | } | |||
| 166 | } | 167 | } | ||
| 167 | 168 | ||||
| 168 | // Wrap the mux with middleware | 169 | // Wrap the mux with middleware | ||
| @@ -243,8 +244,22 @@ func (s *Server) handlePutManifest(w http.ResponseWriter, r *http.Request) { | |||||
| 243 | w.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, reference)) | 244 | w.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, reference)) | ||
| 244 | w.WriteHeader(http.StatusCreated) | 245 | w.WriteHeader(http.StatusCreated) | ||
| 245 | 246 | ||||
| 246 | - | // TODO: Trigger deployment webhook | 247 | + | // Emit image pushed event |
| 247 | - | log.Info().Str("name", name).Str("reference", reference).Msg("Manifest stored - deployment should be triggered") | 248 | + | event := events.Event{ |
| 249 | + | Type: events.ImagePushed, | |||
| 250 | + | ImageName: name, | |||
| 251 | + | Tag: reference, | |||
| 252 | + | Data: map[string]interface{}{ | |||
| 253 | + | "digest": digest, | |||
| 254 | + | "size": len(data), | |||
| 255 | + | }, | |||
| 256 | + | } | |||
| 257 | + | ||||
| 258 | + | if err := s.eventBus.Publish(event); err != nil { | |||
| 259 | + | log.Error().Err(err).Str("name", name).Str("reference", reference).Msg("Failed to publish image pushed event") | |||
| 260 | + | } else { | |||
| 261 | + | log.Info().Str("name", name).Str("reference", reference).Msg("Image pushed event published") | |||
| 262 | + | } | |||
| 248 | } | 263 | } | ||
| 249 | 264 | ||||
| 250 | func (s *Server) handleGetBlob(w http.ResponseWriter, r *http.Request) { | 265 | func (s *Server) handleGetBlob(w http.ResponseWriter, r *http.Request) { | ||