1package proxy
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "net/http/httputil"
8 "net/url"
9 "os"
10 "strconv"
11 "strings"
12
13 "github.com/rs/zerolog/log"
14 "gordon/internal/config"
15 "gordon/internal/container"
16 "gordon/internal/events"
17 "gordon/internal/middleware"
18)
19
20type Server struct {
21 config *config.Config
22 mux *http.ServeMux
23 routes []config.Route
24 manager container.ManagerInterface
25}
26
27func NewServer(cfg *config.Config, manager container.ManagerInterface) *Server {
28 s := &Server{
29 config: cfg,
30 mux: http.NewServeMux(),
31 routes: cfg.GetRoutes(),
32 manager: manager,
33 }
34 s.setupRoutes()
35 return s
36}
37
38// Handler returns the HTTP handler for testing
39func (s *Server) Handler() http.Handler {
40 return middleware.Chain(
41 middleware.PanicRecovery,
42 middleware.RequestLogger,
43 )(s.mux)
44}
45
46func (s *Server) setupRoutes() {
47 // Domain-based routing handler
48 s.mux.HandleFunc("/", s.handleDomainRouting)
49}
50
51func (s *Server) handleDomainRouting(w http.ResponseWriter, r *http.Request) {
52 host := r.Host
53
54 // Check if this is the registry domain
55 if s.config.Server.RegistryDomain != "" && host == s.config.Server.RegistryDomain {
56 log.Info().Str("domain", host).Msg("Routing request to registry")
57 s.proxyToRegistry(w, r)
58 return
59 }
60
61 // Find matching route for this domain
62 for _, route := range s.routes {
63 if route.Domain == host {
64 log.Info().Str("domain", host).Str("image", route.Image).Msg("Routing request")
65 s.proxyToContainer(w, r, route)
66 return
67 }
68 }
69
70 // No route found
71 log.Warn().Str("domain", host).Msg("No route found for domain")
72 http.NotFound(w, r)
73}
74
75func (s *Server) proxyToContainer(w http.ResponseWriter, r *http.Request, route config.Route) {
76 ctx := r.Context()
77
78 // Check if container exists and is running
79 container, exists := s.manager.GetContainer(route.Domain)
80 if !exists {
81 log.Warn().Str("domain", route.Domain).Msg("No container found for route - container should be auto-deployed on image push")
82 http.Error(w, "Service Unavailable - Container not deployed", http.StatusServiceUnavailable)
83 return
84 }
85
86 // Detect if Gordon is running in a container or on the host
87 var target *url.URL
88 if s.isRunningInContainer() {
89 // Gordon is in a container - use container network
90 containerIP, containerPort, err := s.manager.Runtime().GetContainerNetworkInfo(ctx, container.ID)
91 if err != nil {
92 log.Error().Err(err).Str("domain", route.Domain).Str("container_id", container.ID).Msg("Failed to get container network info")
93 http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
94 return
95 }
96 target, err = url.Parse(fmt.Sprintf("http://%s:%d", containerIP, containerPort))
97 if err != nil {
98 log.Error().Err(err).Msg("Failed to parse container target URL")
99 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
100 return
101 }
102 log.Debug().Str("mode", "container").Str("target", target.String()).Msg("Using container-to-container networking")
103 } else {
104 // Gordon is on the host - use host port mapping
105 exposedPorts, err := s.manager.Runtime().GetImageExposedPorts(ctx, route.Image)
106 if err != nil {
107 log.Error().Err(err).Str("domain", route.Domain).Str("image", route.Image).Msg("Failed to get exposed ports from image")
108 http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
109 return
110 }
111
112 hostPort, err := s.manager.Runtime().GetContainerPort(ctx, container.ID, exposedPorts[0])
113 if err != nil {
114 log.Error().Err(err).Str("domain", route.Domain).Str("container_id", container.ID).Int("internal_port", exposedPorts[0]).Msg("Failed to get host port mapping")
115 http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
116 return
117 }
118
119 target, err = url.Parse(fmt.Sprintf("http://localhost:%d", hostPort))
120 if err != nil {
121 log.Error().Err(err).Msg("Failed to parse host target URL")
122 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
123 return
124 }
125 log.Debug().Str("mode", "host").Str("target", target.String()).Msg("Using host port mapping")
126 }
127
128 proxy := httputil.NewSingleHostReverseProxy(target)
129
130 // Customize the proxy to handle errors
131 proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
132 log.Error().Err(err).Str("domain", route.Domain).Str("target", target.String()).Msg("Proxy error")
133 http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
134 }
135
136 // Add custom headers
137 proxy.ModifyResponse = func(resp *http.Response) error {
138 resp.Header.Set("X-Proxied-By", "Gordon")
139 resp.Header.Set("X-Container-ID", container.ID)
140 return nil
141 }
142
143 log.Debug().
144 Str("method", r.Method).
145 Str("path", r.URL.Path).
146 Str("domain", route.Domain).
147 Str("target", target.String()).
148 Str("container", container.ID).
149 Msg("Proxying request")
150 proxy.ServeHTTP(w, r)
151}
152
153func (s *Server) proxyToRegistry(w http.ResponseWriter, r *http.Request) {
154 // Proxy to the internal registry server
155 target, err := url.Parse(fmt.Sprintf("http://localhost:%d", s.config.Server.RegistryPort))
156 if err != nil {
157 log.Error().Err(err).Msg("Failed to parse registry target URL")
158 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
159 return
160 }
161
162 proxy := httputil.NewSingleHostReverseProxy(target)
163
164 // Customize the proxy to handle errors
165 proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
166 log.Error().Err(err).Int("registry_port", s.config.Server.RegistryPort).Msg("Registry proxy error")
167 http.Error(w, "Registry Unavailable", http.StatusServiceUnavailable)
168 }
169
170 // Add custom headers
171 proxy.ModifyResponse = func(resp *http.Response) error {
172 resp.Header.Set("X-Proxied-By", "Gordon")
173 resp.Header.Set("X-Registry-Backend", "gordon-registry")
174 return nil
175 }
176
177 log.Debug().
178 Str("method", r.Method).
179 Str("path", r.URL.Path).
180 Str("domain", s.config.Server.RegistryDomain).
181 Str("target", target.String()).
182 Msg("Proxying request to registry")
183
184 proxy.ServeHTTP(w, r)
185}
186
187// isRunningInContainer detects if Gordon is running inside a container
188func (s *Server) isRunningInContainer() bool {
189 // Check for common container indicators
190
191 // 1. Check for /.dockerenv file (Docker creates this)
192 if _, err := os.Stat("/.dockerenv"); err == nil {
193 return true
194 }
195
196 // 2. Check cgroup for container indicators
197 if data, err := os.ReadFile("/proc/1/cgroup"); err == nil {
198 content := string(data)
199 if strings.Contains(content, "docker") ||
200 strings.Contains(content, "containerd") ||
201 strings.Contains(content, "podman") {
202 return true
203 }
204 }
205
206 // 3. Check if hostname matches container ID pattern (64 hex chars)
207 if hostname, err := os.Hostname(); err == nil {
208 if len(hostname) == 12 || len(hostname) == 64 {
209 // Container hostnames are typically 12 or 64 character hex strings
210 return true
211 }
212 }
213
214 // 4. Check environment variables set by container runtimes
215 if os.Getenv("KUBERNETES_SERVICE_HOST") != "" ||
216 os.Getenv("DOCKER_CONTAINER") != "" ||
217 os.Getenv("container") != "" {
218 return true
219 }
220
221 return false
222}
223
224// UpdateConfig updates the config reference and refreshes routes
225func (s *Server) UpdateConfig(newConfig *config.Config) {
226 s.config = newConfig
227 s.routes = s.config.GetRoutes()
228 log.Debug().Int("route_count", len(s.routes)).Msg("Config and routes updated in proxy server")
229}
230
231// UpdateRoutes refreshes the routes from the current config
232func (s *Server) UpdateRoutes() {
233 s.routes = s.config.GetRoutes()
234 log.Debug().Int("route_count", len(s.routes)).Msg("Routes updated in proxy server")
235}
236
237func (s *Server) Start(ctx context.Context) error {
238 addr := ":" + strconv.Itoa(s.config.Server.Port)
239
240 // Wrap the mux with middleware
241 handler := middleware.Chain(
242 middleware.PanicRecovery,
243 middleware.RequestLogger,
244 middleware.CORS,
245 )(s.mux)
246
247 // TODO: Implement HTTPS support with Let's Encrypt
248 server := &http.Server{
249 Addr: addr,
250 Handler: handler,
251 }
252
253 log.Info().Str("address", addr).Msg("Proxy server starting")
254
255 // Start server in a goroutine
256 errChan := make(chan error, 1)
257 go func() {
258 if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
259 errChan <- err
260 }
261 }()
262
263 // Wait for context cancellation or server error
264 select {
265 case err := <-errChan:
266 return err
267 case <-ctx.Done():
268 log.Info().Msg("Proxy server shutting down...")
269 return server.Shutdown(context.Background())
270 }
271}
272
273// ProxyEventHandler handles events for the proxy server
274type ProxyEventHandler struct {
275 server *Server
276}
277
278func NewProxyEventHandler(server *Server) *ProxyEventHandler {
279 return &ProxyEventHandler{
280 server: server,
281 }
282}
283
284func (h *ProxyEventHandler) CanHandle(eventType events.EventType) bool {
285 return eventType == events.ConfigReload
286}
287
288func (h *ProxyEventHandler) Handle(event events.Event) error {
289 if event.Type == events.ConfigReload {
290 h.server.UpdateRoutes()
291 return nil
292 }
293 return fmt.Errorf("unsupported event type: %s", event.Type)
294}
295
296