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