v2.0.0 ae3e752

server.go RawHistoryFolder

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

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