2.0.1 dcf7d6b

server.go RawHistoryFolder

1package proxy
2
3import (
4	"context"
5	"fmt"
6	"net/http"
7	"net/http/httputil"
8	"net/url"
9	"strconv"
10
11	"github.com/rs/zerolog/log"
12	"gordon/internal/config"
13	"gordon/internal/container"
14	"gordon/internal/middleware"
15)
16
17type Server struct {
18	config   *config.Config
19	mux      *http.ServeMux
20	routes   []config.Route
21	manager  *container.Manager
22}
23
24func NewServer(cfg *config.Config, manager *container.Manager) *Server {
25	s := &Server{
26		config:  cfg,
27		mux:     http.NewServeMux(),
28		routes:  cfg.GetRoutes(),
29		manager: manager,
30	}
31	s.setupRoutes()
32	return s
33}
34
35func (s *Server) setupRoutes() {
36	// Domain-based routing handler
37	s.mux.HandleFunc("/", s.handleDomainRouting)
38}
39
40func (s *Server) handleDomainRouting(w http.ResponseWriter, r *http.Request) {
41	host := r.Host
42	
43	// Check if this is the registry domain
44	if s.config.Server.RegistryDomain != "" && host == s.config.Server.RegistryDomain {
45		log.Info().Str("domain", host).Msg("Routing request to registry")
46		s.proxyToRegistry(w, r)
47		return
48	}
49	
50	// Find matching route for this domain
51	for _, route := range s.routes {
52		if route.Domain == host {
53			log.Info().Str("domain", host).Str("image", route.Image).Msg("Routing request")
54			s.proxyToContainer(w, r, route)
55			return
56		}
57	}
58	
59	// No route found
60	log.Warn().Str("domain", host).Msg("No route found for domain")
61	http.NotFound(w, r)
62}
63
64func (s *Server) proxyToContainer(w http.ResponseWriter, r *http.Request, route config.Route) {
65	ctx := r.Context()
66	
67	// Check if container exists and is running
68	container, exists := s.manager.GetContainer(route.Domain)
69	if !exists {
70		log.Warn().Str("domain", route.Domain).Msg("No container found for route - container should be auto-deployed on image push")
71		http.Error(w, "Service Unavailable - Container not deployed", http.StatusServiceUnavailable)
72		return
73	}
74
75	// Get container's internal network info (Traefik-style automatic detection)
76	containerIP, containerPort, err := s.manager.Runtime().GetContainerNetworkInfo(ctx, container.ID)
77	if err != nil {
78		log.Error().Err(err).Str("domain", route.Domain).Str("container_id", container.ID).Msg("Failed to get container network info")
79		http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
80		return
81	}
82	
83	target, err := url.Parse(fmt.Sprintf("http://%s:%d", containerIP, containerPort))
84	if err != nil {
85		log.Error().Err(err).Msg("Failed to parse target URL")
86		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
87		return
88	}
89	
90	proxy := httputil.NewSingleHostReverseProxy(target)
91	
92	// Customize the proxy to handle errors
93	proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
94		log.Error().Err(err).Str("domain", route.Domain).Str("target", target.String()).Msg("Proxy error")
95		http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
96	}
97	
98	// Add custom headers
99	proxy.ModifyResponse = func(resp *http.Response) error {
100		resp.Header.Set("X-Proxied-By", "Gordon")
101		resp.Header.Set("X-Container-ID", container.ID)
102		return nil
103	}
104	
105	log.Debug().
106		Str("method", r.Method).
107		Str("path", r.URL.Path).
108		Str("domain", route.Domain).
109		Str("target", target.String()).
110		Str("container", container.ID).
111		Msg("Proxying request")
112	proxy.ServeHTTP(w, r)
113}
114
115func (s *Server) proxyToRegistry(w http.ResponseWriter, r *http.Request) {
116	// Proxy to the internal registry server
117	target, err := url.Parse(fmt.Sprintf("http://localhost:%d", s.config.Server.RegistryPort))
118	if err != nil {
119		log.Error().Err(err).Msg("Failed to parse registry target URL")
120		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
121		return
122	}
123	
124	proxy := httputil.NewSingleHostReverseProxy(target)
125	
126	// Customize the proxy to handle errors
127	proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
128		log.Error().Err(err).Int("registry_port", s.config.Server.RegistryPort).Msg("Registry proxy error")
129		http.Error(w, "Registry Unavailable", http.StatusServiceUnavailable)
130	}
131	
132	// Add custom headers
133	proxy.ModifyResponse = func(resp *http.Response) error {
134		resp.Header.Set("X-Proxied-By", "Gordon")
135		resp.Header.Set("X-Registry-Backend", "gordon-registry")
136		return nil
137	}
138	
139	log.Debug().
140		Str("method", r.Method).
141		Str("path", r.URL.Path).
142		Str("domain", s.config.Server.RegistryDomain).
143		Str("target", target.String()).
144		Msg("Proxying request to registry")
145	
146	proxy.ServeHTTP(w, r)
147}
148
149func (s *Server) Start(ctx context.Context) error {
150	addr := ":" + strconv.Itoa(s.config.Server.Port)
151	
152	// Wrap the mux with middleware
153	handler := middleware.Chain(
154		middleware.PanicRecovery,
155		middleware.RequestLogger,
156		middleware.CORS,
157	)(s.mux)
158	
159	// TODO: Implement HTTPS support with Let's Encrypt
160	server := &http.Server{
161		Addr:    addr,
162		Handler: handler,
163	}
164
165	log.Info().Str("address", addr).Msg("Proxy server starting")
166
167	// Start server in a goroutine
168	errChan := make(chan error, 1)
169	go func() {
170		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
171			errChan <- err
172		}
173	}()
174
175	// Wait for context cancellation or server error
176	select {
177	case err := <-errChan:
178		return err
179	case <-ctx.Done():
180		log.Info().Msg("Proxy server shutting down...")
181		return server.Shutdown(context.Background())
182	}
183}
184

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