detached eca9fad

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
eca9fadf6369e85c375dee2b6d9aa17fc13e5907
author
bamen · · 2025-06-02 16:18 UTC
parent
0383cdf
signature
unsigned
11 files · +565 -261
M cmd/start.go +19 -1
@@ -13,6 +13,7 @@ import (
1313 "github.com/spf13/cobra"
1414 "gordon/internal/config"
1515 "gordon/internal/container"
16+ "gordon/internal/events"
1617 "gordon/internal/proxy"
1718 "gordon/internal/registry"
1819 )
@@ -43,12 +44,24 @@ func runStart(cmd *cobra.Command, args []string) {
4344 log.Info().Int("proxy_port", cfg.Server.Port).Msg("Proxy server")
4445 log.Info().Str("runtime", cfg.Server.Runtime).Msg("Container runtime")
4546
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+
4653 // Initialize container manager
4754 manager, err := container.NewManager(cfg)
4855 if err != nil {
4956 log.Fatal().Err(err).Msg("Failed to initialize container manager")
5057 }
5158
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+
5265 // Sync existing containers
5366 ctx, cancel := context.WithCancel(context.Background())
5467 if err := manager.SyncContainers(ctx); err != nil {
@@ -58,7 +71,7 @@ func runStart(cmd *cobra.Command, args []string) {
5871 var wg sync.WaitGroup
5972
6073 // Start registry server
61- registryServer, err := registry.NewServer(cfg)
74+ registryServer, err := registry.NewServer(cfg, eventBus)
6275 if err != nil {
6376 log.Fatal().Err(err).Msg("Failed to initialize registry server")
6477 }
@@ -88,6 +101,11 @@ func runStart(cmd *cobra.Command, args []string) {
88101 log.Info().Msg("Shutting down servers...")
89102 cancel()
90103
104+ // Stop event bus
105+ if err := eventBus.Stop(); err != nil {
106+ log.Error().Err(err).Msg("Failed to stop event bus")
107+ }
108+
91109 // Give servers time to shutdown gracefully
92110 done := make(chan struct{})
93111 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 manager53 // 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 containers65 // 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.WaitGroup71 var wg sync.WaitGroup
59 72
60 // Start registry server73 // 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 gracefully109 // 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 (
1010 github.com/spf13/viper v1.20.1
1111 )
1212
13+require github.com/google/uuid v1.6.0 // indirect
14+
1315 require (
1416 github.com/Microsoft/go-winio v0.4.14 // indirect
1517 github.com/containerd/errdefs v1.0.0 // indirect
@@ -10,6 +10,8 @@ require (
10 github.com/spf13/viper v1.20.110 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 // indirect16 github.com/Microsoft/go-winio v0.4.14 // indirect
15 github.com/containerd/errdefs v1.0.0 // indirect17 github.com/containerd/errdefs v1.0.0 // indirect
M gordon.hcl +6 -0
@@ -6,6 +6,12 @@ server {
66 data_dir = "./data"
77 }
88
9+registry_auth {
10+ enabled = true
11+ username = "gordon"
12+ password = "secret123"
13+}
14+
915 routes = {
1016 "example.com" = "nginx:latest"
1117 "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"
55 ssl_email = "admin@example.com"
66 data_dir = "./data"
77
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"
2812
2913 [routes]
3014 "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 Configuration8+[registry_auth]
9-[auth]9+enabled = true
10-enabled = false # Set to true to enable authentication10+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:
55 ssl_email: admin@example.com
66 data_dir: ./data
77
8+registry_auth:
9+ enabled: true
10+ username: gordon
11+ password: secret123
12+
813 routes:
914 "example.com": "nginx:latest"
1015 "api.example.com": "myapi:latest"
@@ -5,6 +5,11 @@ server:
5 ssl_email: admin@example.com5 ssl_email: admin@example.com
6 data_dir: ./data6 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 (
88 )
99
1010 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"`
1414 }
1515
1616 type ServerConfig struct {
@@ -21,15 +21,10 @@ type ServerConfig struct {
2121 DataDir string `mapstructure:"data_dir"`
2222 }
2323
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"`
3328 }
3429
3530 type Route struct {
@@ -46,10 +41,7 @@ func Load() (*Config, error) {
4641 viper.SetDefault("server.registry_port", 5000)
4742 viper.SetDefault("server.runtime", "docker")
4843 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)
5345
5446 // Handle the routes manually since Viper struggles with domain names
5547 cfg.Routes = make(map[string]string)
@@ -59,9 +51,9 @@ func Load() (*Config, error) {
5951 return nil, fmt.Errorf("unable to decode server config: %v", err)
6052 }
6153
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)
6557 }
6658
6759 // Get routes manually from the raw config
@@ -85,23 +77,10 @@ func Load() (*Config, error) {
8577 return nil, fmt.Errorf("server.runtime must be 'docker' or 'podman'")
8678 }
8779
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")
10584 }
10685 }
10786
@@ -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 names46 // 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 config54+ // 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 config59 // 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 config80+ // 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 @@
11 package middleware
22
33 import (
4- "crypto/rand"
54 "crypto/subtle"
6- "encoding/base64"
7- "fmt"
85 "net/http"
96 "strings"
10- "time"
117
12- "github.com/golang-jwt/jwt/v5"
138 "github.com/rs/zerolog/log"
149 )
1510
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)
6312 func RegistryAuth(username, password string) func(http.Handler) http.Handler {
6413 return func(next http.Handler) http.Handler {
6514 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -88,41 +37,6 @@ func RegistryAuth(username, password string) func(http.Handler) http.Handler {
8837 }
8938 }
9039
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-
12640 // Helper functions
12741
12842 func isAuthenticated(r *http.Request, expectedUsername, expectedPassword string) bool {
@@ -136,111 +50,4 @@ func isAuthenticated(r *http.Request, expectedUsername, expectedPassword string)
13650 passwordMatch := subtle.ConstantTimeCompare([]byte(password), []byte(expectedPassword)) == 1
13751
13852 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
24653 }⏎̸
@@ -1,65 +1,14 @@
1 package middleware1 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 Authentication11+// 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 functions40 // 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)) == 150 passwordMatch := subtle.ConstantTimeCompare([]byte(password), []byte(expectedPassword)) == 1
137 51
138 return usernameMatch && passwordMatch52 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 (
1111
1212 "github.com/rs/zerolog/log"
1313 "gordon/internal/config"
14+ "gordon/internal/events"
1415 "gordon/internal/middleware"
1516 )
1617
@@ -18,9 +19,10 @@ type Server struct {
1819 config *config.Config
1920 mux *http.ServeMux
2021 storage Storage
22+ eventBus events.EventBus
2123 }
2224
23-func NewServer(cfg *config.Config) (*Server, error) {
25+func NewServer(cfg *config.Config, eventBus events.EventBus) (*Server, error) {
2426 // Initialize storage
2527 storage, err := NewFilesystemStorage(cfg.Server.DataDir + "/registry")
2628 if err != nil {
@@ -28,9 +30,10 @@ func NewServer(cfg *config.Config) (*Server, error) {
2830 }
2931
3032 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,
3437 }
3538 s.setupRoutes()
3639 return s, nil
@@ -159,10 +162,8 @@ func (s *Server) Start(ctx context.Context) error {
159162 }
160163
161164 // 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))
166167 }
167168
168169 // Wrap the mux with middleware
@@ -243,8 +244,22 @@ func (s *Server) handlePutManifest(w http.ResponseWriter, r *http.Request) {
243244 w.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, reference))
244245 w.WriteHeader(http.StatusCreated)
245246
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+ }
248263 }
249264
250265 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.Config19 config *config.Config
19 mux *http.ServeMux20 mux *http.ServeMux
20 storage Storage21 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 storage26 // 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, nil39 return s, nil
@@ -159,10 +162,8 @@ func (s *Server) Start(ctx context.Context) error {
159 }162 }
160 163
161 // Add registry authentication if enabled164 // 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 middleware169 // 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 webhook247+ // 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) {

Keyboard shortcuts

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

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

Branches 7

Tags 106

↑↓ moveenter openesc close