detached 532f1f1

refactor: Improve domain and logging handling in proxy routing

- Normalize domain handling by removing leading dots and converting to lowercase
- Update domain parsing in container route discovery to support service subdomains
- Replace Charmbracelet logger with centralized custom logger package
- Enhance domain and IP detection logic in proxy routing
- Improve logging consistency and debug information for container and domain routing
- Add more robust domain and IP normalization across proxy components
commit
532f1f1a7707f7b955577631e9074ca54ede059e
author
bamen · · 2025-03-11 14:27 UTC
parent
b4314ac
signature
unsigned
12 files · +730 -920
M internal/httpserve/handlers/endpoint_helpers.go +4 -0
@@ -253,6 +253,10 @@ func normalizePort(port string) string {
253253
254254 // normalizeTargetDomain ensures that the target domain is in the format of "https://domain" or "http://domain"
255255 func normalizeTargetDomain(domain string) string {
256+ // First, remove any leading dot from the domain
257+ domain = strings.TrimPrefix(domain, ".")
258+
259+ // Then check if we need to add the protocol
256260 if !regexp.MustCompile(`^https?:\/\/`).MatchString(domain) {
257261 return "https://" + domain
258262 }
@@ -253,6 +253,10 @@ func normalizePort(port string) string {
253 253
254 // normalizeTargetDomain ensures that the target domain is in the format of "https://domain" or "http://domain"254 // normalizeTargetDomain ensures that the target domain is in the format of "https://domain" or "http://domain"
255 func normalizeTargetDomain(domain string) string {255 func normalizeTargetDomain(domain string) string {
256+ // First, remove any leading dot from the domain
257+ domain = strings.TrimPrefix(domain, ".")
258+
259+ // Then check if we need to add the protocol
256 if !regexp.MustCompile(`^https?:\/\/`).MatchString(domain) {260 if !regexp.MustCompile(`^https?:\/\/`).MatchString(domain) {
257 return "https://" + domain261 return "https://" + domain
258 }262 }
M internal/proxy/blacklist.go +16 -16
@@ -9,7 +9,7 @@ import (
99 "sync"
1010 "time"
1111
12- "github.com/charmbracelet/log"
12+ "github.com/bnema/gordon/pkg/logger"
1313 "gopkg.in/yaml.v3"
1414 )
1515
@@ -64,7 +64,7 @@ func NewBlacklist(path string) (*BlacklistConfig, error) {
6464 return nil, fmt.Errorf("failed to create blacklist file: %w", err)
6565 }
6666
67- log.Info("Created new blacklist file", "path", path)
67+ logger.Info("Created new blacklist file", "path", path)
6868 }
6969
7070 // Load the blacklist
@@ -72,7 +72,7 @@ func NewBlacklist(path string) (*BlacklistConfig, error) {
7272 return nil, fmt.Errorf("failed to load blacklist: %w", err)
7373 }
7474
75- log.Info("Blacklist loaded",
75+ logger.Info("Blacklist loaded",
7676 "ips", len(blacklist.IPs),
7777 "ranges", len(blacklist.Ranges))
7878
@@ -119,7 +119,7 @@ func (b *BlacklistConfig) Load() error {
119119
120120 _, network, err := net.ParseCIDR(cidr)
121121 if err != nil {
122- log.Warn("Invalid CIDR in blacklist", "cidr", cidr, "error", err)
122+ logger.Warn("Invalid CIDR in blacklist", "cidr", cidr, "error", err)
123123 continue
124124 }
125125 networks = append(networks, network)
@@ -136,12 +136,12 @@ func (b *BlacklistConfig) Load() error {
136136
137137 // If the file has actually changed, log about it
138138 if fileInfo.ModTime() != b.LastMod {
139- log.Info("Loaded blacklist",
139+ logger.Info("Loaded blacklist",
140140 "ips", len(b.IPs),
141141 "ranges", len(b.Ranges),
142142 "last_modified", b.LastMod.Format(time.RFC3339))
143143 } else if forceReload {
144- log.Debug("Reloaded blacklist (forced refresh)",
144+ logger.Debug("Reloaded blacklist (forced refresh)",
145145 "ips", len(b.IPs),
146146 "ranges", len(b.Ranges))
147147 }
@@ -162,14 +162,14 @@ func (b *BlacklistConfig) rebuildBlockedCache() {
162162 // Replace the cache atomically
163163 b.blockedCache = newCache
164164
165- log.Debug("Rebuilt blocked IP cache", "direct_ips", len(b.blockedCache))
165+ logger.Debug("Rebuilt blocked IP cache", "direct_ips", len(b.blockedCache))
166166 }
167167
168168 // IsBlocked checks if an IP is in the blacklist
169169 func (b *BlacklistConfig) IsBlocked(ip string) bool {
170170 // Try to reload the blacklist first
171171 if err := b.Load(); err != nil {
172- log.Error("Failed to reload blacklist", "error", err)
172+ logger.Error("Failed to reload blacklist", "error", err)
173173 }
174174
175175 b.mu.RLock()
@@ -183,7 +183,7 @@ func (b *BlacklistConfig) IsBlocked(ip string) bool {
183183 // Parse the IP
184184 parsedIP := net.ParseIP(ip)
185185 if parsedIP == nil {
186- log.Warn("Failed to parse IP", "ip", ip)
186+ logger.Warn("Failed to parse IP", "ip", ip)
187187 return false
188188 }
189189
@@ -239,7 +239,7 @@ func (b *BlacklistConfig) AddIP(ip string) error {
239239
240240 // Add to in-memory cache immediately
241241 b.blockedCache[ip] = true
242- log.Info("Added IP to blacklist", "ip", ip)
242+ logger.Info("Added IP to blacklist", "ip", ip)
243243
244244 // Force a reload
245245 return b.Load()
@@ -292,7 +292,7 @@ func (b *BlacklistConfig) AddRange(cidr string) error {
292292 return err
293293 }
294294
295- log.Info("Added CIDR range to blacklist", "cidr", cidr)
295+ logger.Info("Added CIDR range to blacklist", "cidr", cidr)
296296
297297 // Force a reload to update Networks and the cache
298298 return b.Load()
@@ -345,7 +345,7 @@ func (b *BlacklistConfig) RemoveIP(ip string) error {
345345
346346 // Remove from cache
347347 delete(b.blockedCache, ip)
348- log.Info("Removed IP from blacklist", "ip", ip)
348+ logger.Info("Removed IP from blacklist", "ip", ip)
349349
350350 // Force a reload
351351 return b.Load()
@@ -401,7 +401,7 @@ func (b *BlacklistConfig) RemoveRange(cidr string) error {
401401 return err
402402 }
403403
404- log.Info("Removed CIDR range from blacklist", "cidr", cidr)
404+ logger.Info("Removed CIDR range from blacklist", "cidr", cidr)
405405
406406 // Force a reload to rebuild Networks and the cache
407407 return b.Load()
@@ -421,7 +421,7 @@ func (p *Proxy) logBlockedIP(clientIP, path, userAgent string) {
421421 p.blockedIPCounter[clientIP] == 0 {
422422
423423 // Log this block and update counters
424- log.Info("Blocked request from blacklisted IP",
424+ logger.Info("Blocked request from blacklisted IP",
425425 "ip", clientIP,
426426 "path", path,
427427 "user_agent", userAgent)
@@ -447,7 +447,7 @@ func (p *Proxy) logBlockedIP(clientIP, path, userAgent string) {
447447 totalBlocked += count
448448 }
449449
450- log.Info("Blocked IP summary",
450+ logger.Info("Blocked IP summary",
451451 "unique_ips", len(p.blockedIPCounter),
452452 "total_requests", totalBlocked,
453453 "since", p.lastBlockedLog.Format(time.RFC3339))
@@ -459,6 +459,6 @@ func (p *Proxy) logBlockedIP(clientIP, path, userAgent string) {
459459
460460 // Debug logging only if needed
461461 if false {
462- log.Debug("Request marked as blacklisted for logging skip", "ip", clientIP)
462+ logger.Debug("Request marked as blacklisted for logging skip", "ip", clientIP)
463463 }
464464 }
@@ -9,7 +9,7 @@ import (
9 "sync"9 "sync"
10 "time"10 "time"
11 11
12- "github.com/charmbracelet/log"12+ "github.com/bnema/gordon/pkg/logger"
13 "gopkg.in/yaml.v3"13 "gopkg.in/yaml.v3"
14 )14 )
15 15
@@ -64,7 +64,7 @@ func NewBlacklist(path string) (*BlacklistConfig, error) {
64 return nil, fmt.Errorf("failed to create blacklist file: %w", err)64 return nil, fmt.Errorf("failed to create blacklist file: %w", err)
65 }65 }
66 66
67- log.Info("Created new blacklist file", "path", path)67+ logger.Info("Created new blacklist file", "path", path)
68 }68 }
69 69
70 // Load the blacklist70 // Load the blacklist
@@ -72,7 +72,7 @@ func NewBlacklist(path string) (*BlacklistConfig, error) {
72 return nil, fmt.Errorf("failed to load blacklist: %w", err)72 return nil, fmt.Errorf("failed to load blacklist: %w", err)
73 }73 }
74 74
75- log.Info("Blacklist loaded",75+ logger.Info("Blacklist loaded",
76 "ips", len(blacklist.IPs),76 "ips", len(blacklist.IPs),
77 "ranges", len(blacklist.Ranges))77 "ranges", len(blacklist.Ranges))
78 78
@@ -119,7 +119,7 @@ func (b *BlacklistConfig) Load() error {
119 119
120 _, network, err := net.ParseCIDR(cidr)120 _, network, err := net.ParseCIDR(cidr)
121 if err != nil {121 if err != nil {
122- log.Warn("Invalid CIDR in blacklist", "cidr", cidr, "error", err)122+ logger.Warn("Invalid CIDR in blacklist", "cidr", cidr, "error", err)
123 continue123 continue
124 }124 }
125 networks = append(networks, network)125 networks = append(networks, network)
@@ -136,12 +136,12 @@ func (b *BlacklistConfig) Load() error {
136 136
137 // If the file has actually changed, log about it137 // If the file has actually changed, log about it
138 if fileInfo.ModTime() != b.LastMod {138 if fileInfo.ModTime() != b.LastMod {
139- log.Info("Loaded blacklist",139+ logger.Info("Loaded blacklist",
140 "ips", len(b.IPs),140 "ips", len(b.IPs),
141 "ranges", len(b.Ranges),141 "ranges", len(b.Ranges),
142 "last_modified", b.LastMod.Format(time.RFC3339))142 "last_modified", b.LastMod.Format(time.RFC3339))
143 } else if forceReload {143 } else if forceReload {
144- log.Debug("Reloaded blacklist (forced refresh)",144+ logger.Debug("Reloaded blacklist (forced refresh)",
145 "ips", len(b.IPs),145 "ips", len(b.IPs),
146 "ranges", len(b.Ranges))146 "ranges", len(b.Ranges))
147 }147 }
@@ -162,14 +162,14 @@ func (b *BlacklistConfig) rebuildBlockedCache() {
162 // Replace the cache atomically162 // Replace the cache atomically
163 b.blockedCache = newCache163 b.blockedCache = newCache
164 164
165- log.Debug("Rebuilt blocked IP cache", "direct_ips", len(b.blockedCache))165+ logger.Debug("Rebuilt blocked IP cache", "direct_ips", len(b.blockedCache))
166 }166 }
167 167
168 // IsBlocked checks if an IP is in the blacklist168 // IsBlocked checks if an IP is in the blacklist
169 func (b *BlacklistConfig) IsBlocked(ip string) bool {169 func (b *BlacklistConfig) IsBlocked(ip string) bool {
170 // Try to reload the blacklist first170 // Try to reload the blacklist first
171 if err := b.Load(); err != nil {171 if err := b.Load(); err != nil {
172- log.Error("Failed to reload blacklist", "error", err)172+ logger.Error("Failed to reload blacklist", "error", err)
173 }173 }
174 174
175 b.mu.RLock()175 b.mu.RLock()
@@ -183,7 +183,7 @@ func (b *BlacklistConfig) IsBlocked(ip string) bool {
183 // Parse the IP183 // Parse the IP
184 parsedIP := net.ParseIP(ip)184 parsedIP := net.ParseIP(ip)
185 if parsedIP == nil {185 if parsedIP == nil {
186- log.Warn("Failed to parse IP", "ip", ip)186+ logger.Warn("Failed to parse IP", "ip", ip)
187 return false187 return false
188 }188 }
189 189
@@ -239,7 +239,7 @@ func (b *BlacklistConfig) AddIP(ip string) error {
239 239
240 // Add to in-memory cache immediately240 // Add to in-memory cache immediately
241 b.blockedCache[ip] = true241 b.blockedCache[ip] = true
242- log.Info("Added IP to blacklist", "ip", ip)242+ logger.Info("Added IP to blacklist", "ip", ip)
243 243
244 // Force a reload244 // Force a reload
245 return b.Load()245 return b.Load()
@@ -292,7 +292,7 @@ func (b *BlacklistConfig) AddRange(cidr string) error {
292 return err292 return err
293 }293 }
294 294
295- log.Info("Added CIDR range to blacklist", "cidr", cidr)295+ logger.Info("Added CIDR range to blacklist", "cidr", cidr)
296 296
297 // Force a reload to update Networks and the cache297 // Force a reload to update Networks and the cache
298 return b.Load()298 return b.Load()
@@ -345,7 +345,7 @@ func (b *BlacklistConfig) RemoveIP(ip string) error {
345 345
346 // Remove from cache346 // Remove from cache
347 delete(b.blockedCache, ip)347 delete(b.blockedCache, ip)
348- log.Info("Removed IP from blacklist", "ip", ip)348+ logger.Info("Removed IP from blacklist", "ip", ip)
349 349
350 // Force a reload350 // Force a reload
351 return b.Load()351 return b.Load()
@@ -401,7 +401,7 @@ func (b *BlacklistConfig) RemoveRange(cidr string) error {
401 return err401 return err
402 }402 }
403 403
404- log.Info("Removed CIDR range from blacklist", "cidr", cidr)404+ logger.Info("Removed CIDR range from blacklist", "cidr", cidr)
405 405
406 // Force a reload to rebuild Networks and the cache406 // Force a reload to rebuild Networks and the cache
407 return b.Load()407 return b.Load()
@@ -421,7 +421,7 @@ func (p *Proxy) logBlockedIP(clientIP, path, userAgent string) {
421 p.blockedIPCounter[clientIP] == 0 {421 p.blockedIPCounter[clientIP] == 0 {
422 422
423 // Log this block and update counters423 // Log this block and update counters
424- log.Info("Blocked request from blacklisted IP",424+ logger.Info("Blocked request from blacklisted IP",
425 "ip", clientIP,425 "ip", clientIP,
426 "path", path,426 "path", path,
427 "user_agent", userAgent)427 "user_agent", userAgent)
@@ -447,7 +447,7 @@ func (p *Proxy) logBlockedIP(clientIP, path, userAgent string) {
447 totalBlocked += count447 totalBlocked += count
448 }448 }
449 449
450- log.Info("Blocked IP summary",450+ logger.Info("Blocked IP summary",
451 "unique_ips", len(p.blockedIPCounter),451 "unique_ips", len(p.blockedIPCounter),
452 "total_requests", totalBlocked,452 "total_requests", totalBlocked,
453 "since", p.lastBlockedLog.Format(time.RFC3339))453 "since", p.lastBlockedLog.Format(time.RFC3339))
@@ -459,6 +459,6 @@ func (p *Proxy) logBlockedIP(clientIP, path, userAgent string) {
459 459
460 // Debug logging only if needed460 // Debug logging only if needed
461 if false {461 if false {
462- log.Debug("Request marked as blacklisted for logging skip", "ip", clientIP)462+ logger.Debug("Request marked as blacklisted for logging skip", "ip", clientIP)
463 }463 }
464 }464 }
M internal/proxy/certificates.go +51 -51
@@ -18,7 +18,7 @@ import (
1818 "strings"
1919 "time"
2020
21- "github.com/charmbracelet/log"
21+ "github.com/bnema/gordon/pkg/logger"
2222 "golang.org/x/crypto/acme"
2323 "golang.org/x/crypto/acme/autocert"
2424 )
@@ -33,7 +33,7 @@ func (p *Proxy) setupCertManager() {
3333
3434 // Ensure directory exists
3535 if err := os.MkdirAll(dir, 0755); err != nil {
36- log.Warn("Failed to create certificate cache directory",
36+ logger.Warn("Failed to create certificate cache directory",
3737 "dir", dir,
3838 "error", err)
3939 }
@@ -54,14 +54,14 @@ func (p *Proxy) setupCertManager() {
5454 certManager.Client = &acme.Client{
5555 DirectoryURL: "https://acme-staging-v02.api.letsencrypt.org/directory",
5656 }
57- log.Debug("Using Let's Encrypt staging environment",
57+ logger.Debug("Using Let's Encrypt staging environment",
5858 "url", "https://acme-staging-v02.api.letsencrypt.org/directory")
5959 } else {
6060 // Explicitly set production URL when not in staging mode
6161 certManager.Client = &acme.Client{
6262 DirectoryURL: acme.LetsEncryptURL, // "https://acme-v02.api.letsencrypt.org/directory"
6363 }
64- log.Debug("Using Let's Encrypt production environment",
64+ logger.Debug("Using Let's Encrypt production environment",
6565 "url", acme.LetsEncryptURL)
6666 }
6767
@@ -87,7 +87,7 @@ func (p *Proxy) setupCertManager() {
8787
8888 // Log the attempt but still allow it - this helps debug
8989 // certificate acquisition issues during development
90- log.Warn("Unknown host in certificate request",
90+ logger.Warn("Unknown host in certificate request",
9191 "host", host,
9292 "adminDomain", adminDomain,
9393 "allowed", "yes")
@@ -108,9 +108,9 @@ func (p *Proxy) setupCertManager() {
108108
109109 fallbackCert, err := generateFallbackCertificates(fallbackDomains)
110110 if err != nil {
111- log.Warn("Failed to generate fallback certificate", "error", err)
111+ logger.Warn("Failed to generate fallback certificate", "error", err)
112112 } else {
113- log.Info("Generated fallback self-signed certificate for admin domain",
113+ logger.Info("Generated fallback self-signed certificate for admin domain",
114114 "domain", adminDomain,
115115 "valid_until", time.Now().Add(24*time.Hour).Format("2006-01-02 15:04:05"))
116116 // Store the fallback certificate
@@ -118,7 +118,7 @@ func (p *Proxy) setupCertManager() {
118118 }
119119
120120 p.certManager = certManager
121- log.Debug("Certificate manager setup completed",
121+ logger.Debug("Certificate manager setup completed",
122122 "directory", dir,
123123 "mode", p.config.LetsEncryptMode)
124124
@@ -147,7 +147,7 @@ func (p *Proxy) checkCertificateInCache(domain string) bool {
147147 certData, err := p.certManager.Cache.Get(ctx, cacheKey)
148148 if err != nil {
149149 // ErrCacheMiss or other error means the certificate is not in the cache
150- log.Debug("Certificate not found in cache",
150+ logger.Debug("Certificate not found in cache",
151151 "domain", domain,
152152 "error", err)
153153 return false
@@ -156,14 +156,14 @@ func (p *Proxy) checkCertificateInCache(domain string) bool {
156156 // Parse the certificate to check its validity
157157 block, _ := pem.Decode(certData)
158158 if block == nil || block.Type != "CERTIFICATE" {
159- log.Warn("Invalid certificate data in cache",
159+ logger.Warn("Invalid certificate data in cache",
160160 "domain", domain)
161161 return false
162162 }
163163
164164 cert, err := x509.ParseCertificate(block.Bytes)
165165 if err != nil {
166- log.Warn("Failed to parse certificate from cache",
166+ logger.Warn("Failed to parse certificate from cache",
167167 "domain", domain,
168168 "error", err)
169169 return false
@@ -172,7 +172,7 @@ func (p *Proxy) checkCertificateInCache(domain string) bool {
172172 // Check if the certificate is still valid
173173 now := time.Now()
174174 if now.After(cert.NotAfter) || now.Before(cert.NotBefore) {
175- log.Info("Certificate in cache has expired or is not yet valid",
175+ logger.Info("Certificate in cache has expired or is not yet valid",
176176 "domain", domain,
177177 "not_before", cert.NotBefore,
178178 "not_after", cert.NotAfter)
@@ -181,7 +181,7 @@ func (p *Proxy) checkCertificateInCache(domain string) bool {
181181
182182 // Check if the certificate is about to expire (within 30 days)
183183 if now.Add(30 * 24 * time.Hour).After(cert.NotAfter) {
184- log.Info("Certificate in cache is valid but will expire soon",
184+ logger.Info("Certificate in cache is valid but will expire soon",
185185 "domain", domain,
186186 "expires_in", cert.NotAfter.Sub(now).Hours()/24,
187187 "days")
@@ -189,7 +189,7 @@ func (p *Proxy) checkCertificateInCache(domain string) bool {
189189 return false
190190 }
191191
192- log.Info("Valid certificate found in cache",
192+ logger.Info("Valid certificate found in cache",
193193 "domain", domain,
194194 "expires_in", cert.NotAfter.Sub(now).Hours()/24,
195195 "days")
@@ -206,7 +206,7 @@ func (p *Proxy) requestAdminCertificate() {
206206
207207 // Only proceed if HTTPS is enabled
208208 if !p.app.GetConfig().Http.Https {
209- log.Debug("HTTPS is disabled, skipping admin certificate request")
209+ logger.Debug("HTTPS is disabled, skipping admin certificate request")
210210 return
211211 }
212212
@@ -214,7 +214,7 @@ func (p *Proxy) requestAdminCertificate() {
214214 host := adminDomain
215215 ips, err := net.LookupIP(host)
216216 if err != nil {
217- log.Error("Could not resolve admin domain, Let's Encrypt will likely fail",
217+ logger.Error("Could not resolve admin domain, Let's Encrypt will likely fail",
218218 "domain", adminDomain,
219219 "error", err.Error(),
220220 "solution", "Check DNS settings and ensure domain points to this server")
@@ -226,13 +226,13 @@ func (p *Proxy) requestAdminCertificate() {
226226 ipStrings = append(ipStrings, ip.String())
227227 }
228228
229- log.Info("Successfully resolved admin domain",
229+ logger.Info("Successfully resolved admin domain",
230230 "domain", adminDomain,
231231 "ips", ipStrings)
232232
233233 // Check if we already have a valid certificate in the cache
234234 if p.checkCertificateInCache(adminDomain) {
235- log.Info("Using existing certificate from cache",
235+ logger.Info("Using existing certificate from cache",
236236 "domain", adminDomain,
237237 "action", "skipping Let's Encrypt request to avoid rate limits")
238238 return
@@ -247,12 +247,12 @@ func (p *Proxy) requestAdminCertificate() {
247247 }
248248
249249 // Log the certificate request intent
250- log.Info("Initiating Let's Encrypt certificate request for admin domain",
250+ logger.Info("Initiating Let's Encrypt certificate request for admin domain",
251251 "domain", adminDomain,
252252 "email", email,
253253 "mode", mode)
254254
255- log.Info("⏳ Waiting for Let's Encrypt to validate domain ownership",
255+ logger.Info("⏳ Waiting for Let's Encrypt to validate domain ownership",
256256 "domain", adminDomain,
257257 "validation_method", "HTTP-01 challenge",
258258 "requirements", "Domain must be publicly accessible on port 80",
@@ -289,7 +289,7 @@ func (p *Proxy) requestAdminCertificate() {
289289 certErr = result.err
290290 case <-ctx.Done():
291291 certErr = fmt.Errorf("certificate request timed out after 2 minutes: %w", ctx.Err())
292- log.Error("Let's Encrypt certificate request timed out",
292+ logger.Error("Let's Encrypt certificate request timed out",
293293 "domain", adminDomain,
294294 "timeout", "2 minutes",
295295 "error", certErr)
@@ -306,31 +306,31 @@ func (p *Proxy) requestAdminCertificate() {
306306 retryAfterStr = matches[1]
307307 }
308308
309- log.Error("Let's Encrypt rate limit reached",
309+ logger.Error("Let's Encrypt rate limit reached",
310310 "domain", adminDomain,
311311 "error", certErr,
312312 "retry_after", retryAfterStr,
313313 "solution", "Wait until the rate limit expires, then restart the server")
314314
315315 // Don't retry on rate limits
316- log.Info("Skipping certificate request retries due to rate limiting",
316+ logger.Info("Skipping certificate request retries due to rate limiting",
317317 "domain", adminDomain)
318318 } else {
319- log.Error("Failed to obtain Let's Encrypt certificate",
319+ logger.Error("Failed to obtain Let's Encrypt certificate",
320320 "domain", adminDomain,
321321 "error", certErr)
322322
323- log.Info("⏳ Retrying certificate request with exponential backoff",
323+ logger.Info("⏳ Retrying certificate request with exponential backoff",
324324 "domain", adminDomain,
325325 "timeout", "2 minutes")
326326 // Implement retry logic with backoff
327327 go p.retryCertificateRequest(adminDomain, 3, 10*time.Second)
328328 }
329329 } else if certResult != nil {
330- log.Info("Successfully obtained Let's Encrypt certificate",
330+ logger.Info("Successfully obtained Let's Encrypt certificate",
331331 "domain", adminDomain)
332332 } else {
333- log.Error("Unexpected error: received nil certificate but no error",
333+ logger.Error("Unexpected error: received nil certificate but no error",
334334 "domain", adminDomain,
335335 "timeout", "2 minutes")
336336 // Implement retry logic with backoff
@@ -342,14 +342,14 @@ func (p *Proxy) requestAdminCertificate() {
342342 // for a container domain
343343 func (p *Proxy) requestDomainCertificate(domain string) {
344344 if domain == "" {
345- log.Warn("Empty domain provided to requestDomainCertificate")
345+ logger.Warn("Empty domain provided to requestDomainCertificate")
346346 return
347347 }
348348
349349 // Extract hostname from domain, resolving it to see if it's publicly accessible
350350 ips, err := net.LookupIP(domain)
351351 if err != nil {
352- log.Error("Could not resolve domain, Let's Encrypt will likely fail",
352+ logger.Error("Could not resolve domain, Let's Encrypt will likely fail",
353353 "domain", domain,
354354 "error", err.Error(),
355355 "solution", "Check DNS settings and ensure domain points to this server")
@@ -361,13 +361,13 @@ func (p *Proxy) requestDomainCertificate(domain string) {
361361 ipStrings = append(ipStrings, ip.String())
362362 }
363363
364- log.Info("Successfully resolved domain for certificate request",
364+ logger.Info("Successfully resolved domain for certificate request",
365365 "domain", domain,
366366 "ips", ipStrings)
367367
368368 // Check if we already have a valid certificate in the cache
369369 if p.checkCertificateInCache(domain) {
370- log.Info("Using existing certificate from cache",
370+ logger.Info("Using existing certificate from cache",
371371 "domain", domain,
372372 "action", "skipping Let's Encrypt request to avoid rate limits")
373373 return
@@ -382,12 +382,12 @@ func (p *Proxy) requestDomainCertificate(domain string) {
382382 }
383383
384384 // Log the certificate request intent
385- log.Info("Initiating Let's Encrypt certificate request for container domain",
385+ logger.Info("Initiating Let's Encrypt certificate request for container domain",
386386 "domain", domain,
387387 "email", email,
388388 "mode", mode)
389389
390- log.Info("⏳ Waiting for Let's Encrypt to validate domain ownership",
390+ logger.Info("⏳ Waiting for Let's Encrypt to validate domain ownership",
391391 "domain", domain,
392392 "validation_method", "HTTP-01 challenge",
393393 "requirements", "Domain must be publicly accessible on port 80",
@@ -424,7 +424,7 @@ func (p *Proxy) requestDomainCertificate(domain string) {
424424 certErr = result.err
425425 case <-ctx.Done():
426426 certErr = fmt.Errorf("certificate request timed out after 1 minute: %w", ctx.Err())
427- log.Error("Let's Encrypt certificate request timed out",
427+ logger.Error("Let's Encrypt certificate request timed out",
428428 "domain", domain,
429429 "timeout", "1 minute",
430430 "error", certErr)
@@ -441,31 +441,31 @@ func (p *Proxy) requestDomainCertificate(domain string) {
441441 retryAfterStr = matches[1]
442442 }
443443
444- log.Error("Let's Encrypt rate limit reached",
444+ logger.Error("Let's Encrypt rate limit reached",
445445 "domain", domain,
446446 "error", certErr,
447447 "retry_after", retryAfterStr,
448448 "solution", "Wait until the rate limit expires, then restart the server")
449449
450450 // Don't retry on rate limits
451- log.Info("Skipping certificate request retries due to rate limiting",
451+ logger.Info("Skipping certificate request retries due to rate limiting",
452452 "domain", domain)
453453 } else {
454- log.Error("Failed to obtain Let's Encrypt certificate",
454+ logger.Error("Failed to obtain Let's Encrypt certificate",
455455 "domain", domain,
456456 "error", certErr)
457457
458- log.Info("⏳ Retrying certificate request with exponential backoff",
458+ logger.Info("⏳ Retrying certificate request with exponential backoff",
459459 "domain", domain,
460460 "timeout", "2 minutes")
461461 // Implement retry logic with backoff
462462 go p.retryCertificateRequest(domain, 3, 10*time.Second)
463463 }
464464 } else if certResult != nil {
465- log.Info("Successfully obtained Let's Encrypt certificate",
465+ logger.Info("Successfully obtained Let's Encrypt certificate",
466466 "domain", domain)
467467 } else {
468- log.Error("Unexpected error: received nil certificate but no error",
468+ logger.Error("Unexpected error: received nil certificate but no error",
469469 "domain", domain,
470470 "timeout", "1 minute")
471471 // Implement retry logic with backoff
@@ -479,7 +479,7 @@ func (p *Proxy) retryCertificateRequest(domain string, maxRetries int, initialBa
479479
480480 for attempt := 1; attempt <= maxRetries; attempt++ {
481481 // Wait for backoff period
482- log.Info("Retrying Let's Encrypt certificate request",
482+ logger.Info("Retrying Let's Encrypt certificate request",
483483 "domain", domain,
484484 "attempt", attempt,
485485 "max_retries", maxRetries,
@@ -503,14 +503,14 @@ func (p *Proxy) retryCertificateRequest(domain string, maxRetries int, initialBa
503503 resp, err := client.Get(testURL)
504504
505505 if err != nil {
506- log.Warn("HTTP challenge endpoint might not be accessible",
506+ logger.Warn("HTTP challenge endpoint might not be accessible",
507507 "domain", domain,
508508 "url", testURL,
509509 "error", err,
510510 "solution", "Ensure port 80 is accessible and not blocked by firewall")
511511 } else {
512512 resp.Body.Close()
513- log.Info("HTTP challenge endpoint is accessible",
513+ logger.Info("HTTP challenge endpoint is accessible",
514514 "domain", domain,
515515 "url", testURL,
516516 "status", resp.StatusCode)
@@ -534,13 +534,13 @@ func (p *Proxy) retryCertificateRequest(domain string, maxRetries int, initialBa
534534
535535 // Check result
536536 if err == nil {
537- log.Info("Successfully obtained Let's Encrypt certificate on retry",
537+ logger.Info("Successfully obtained Let's Encrypt certificate on retry",
538538 "domain", domain,
539539 "attempt", attempt)
540540 return
541541 }
542542
543- log.Error("Let's Encrypt certificate request retry failed",
543+ logger.Error("Let's Encrypt certificate request retry failed",
544544 "domain", domain,
545545 "attempt", attempt,
546546 "error", err)
@@ -548,13 +548,13 @@ func (p *Proxy) retryCertificateRequest(domain string, maxRetries int, initialBa
548548 // Provide more detailed diagnostics based on the error
549549 if strings.Contains(strings.ToLower(err.Error()), "connection refused") ||
550550 strings.Contains(strings.ToLower(err.Error()), "timeout") {
551- log.Error("Let's Encrypt connection failed - this typically indicates:",
551+ logger.Error("Let's Encrypt connection failed - this typically indicates:",
552552 "issue_1", "Port 80 is not accessible from the internet",
553553 "issue_2", "Firewall is blocking inbound connections",
554554 "issue_3", "DNS records not properly propagated",
555555 "solution", "Check firewall settings and DNS configuration")
556556 } else if strings.Contains(strings.ToLower(err.Error()), "unauthorized") {
557- log.Error("Let's Encrypt authorization failed - this typically indicates:",
557+ logger.Error("Let's Encrypt authorization failed - this typically indicates:",
558558 "issue", "Domain ownership validation failed",
559559 "solution", "Ensure the server is publicly accessible on port 80")
560560 } else if strings.Contains(strings.ToLower(err.Error()), "ratelimited") {
@@ -570,7 +570,7 @@ func (p *Proxy) retryCertificateRequest(domain string, maxRetries int, initialBa
570570 }
571571 }
572572
573- log.Error("Let's Encrypt rate limit reached - cannot issue more certificates for this domain yet",
573+ logger.Error("Let's Encrypt rate limit reached - cannot issue more certificates for this domain yet",
574574 "domain", domain,
575575 "retry_after", retryAfterStr,
576576 "solution", "Wait until the rate limit expires, then restart the server")
@@ -583,20 +583,20 @@ func (p *Proxy) retryCertificateRequest(domain string, maxRetries int, initialBa
583583 backoff *= 2
584584 }
585585
586- log.Error("All Let's Encrypt certificate request retries failed",
586+ logger.Error("All Let's Encrypt certificate request retries failed",
587587 "domain", domain,
588588 "max_retries", maxRetries,
589589 "fallback", "Using self-signed certificate")
590590
591591 // At this point, all retries have failed, so we'll rely on the fallback self-signed certificate
592592 // But let's log this prominently for debugging
593- log.Error("⚠️ HTTPS is using a self-signed certificate which browsers will warn about",
593+ logger.Error("⚠️ HTTPS is using a self-signed certificate which browsers will warn about",
594594 "domain", domain,
595595 "reason", "Let's Encrypt certificate issuance failed",
596596 "solution", "Check network settings and Let's Encrypt status")
597597
598598 // Suggest checking the common issues
599- log.Error("Common Let's Encrypt issues to check:",
599+ logger.Error("Common Let's Encrypt issues to check:",
600600 "check_1", "Ensure ports 80 and 443 are open on your firewall",
601601 "check_2", "Verify DNS records point to your server IP",
602602 "check_3", "Make sure no other services are running on ports 80/443",
@@ -18,7 +18,7 @@ import (
18 "strings"18 "strings"
19 "time"19 "time"
20 20
21- "github.com/charmbracelet/log"21+ "github.com/bnema/gordon/pkg/logger"
22 "golang.org/x/crypto/acme"22 "golang.org/x/crypto/acme"
23 "golang.org/x/crypto/acme/autocert"23 "golang.org/x/crypto/acme/autocert"
24 )24 )
@@ -33,7 +33,7 @@ func (p *Proxy) setupCertManager() {
33 33
34 // Ensure directory exists34 // Ensure directory exists
35 if err := os.MkdirAll(dir, 0755); err != nil {35 if err := os.MkdirAll(dir, 0755); err != nil {
36- log.Warn("Failed to create certificate cache directory",36+ logger.Warn("Failed to create certificate cache directory",
37 "dir", dir,37 "dir", dir,
38 "error", err)38 "error", err)
39 }39 }
@@ -54,14 +54,14 @@ func (p *Proxy) setupCertManager() {
54 certManager.Client = &acme.Client{54 certManager.Client = &acme.Client{
55 DirectoryURL: "https://acme-staging-v02.api.letsencrypt.org/directory",55 DirectoryURL: "https://acme-staging-v02.api.letsencrypt.org/directory",
56 }56 }
57- log.Debug("Using Let's Encrypt staging environment",57+ logger.Debug("Using Let's Encrypt staging environment",
58 "url", "https://acme-staging-v02.api.letsencrypt.org/directory")58 "url", "https://acme-staging-v02.api.letsencrypt.org/directory")
59 } else {59 } else {
60 // Explicitly set production URL when not in staging mode60 // Explicitly set production URL when not in staging mode
61 certManager.Client = &acme.Client{61 certManager.Client = &acme.Client{
62 DirectoryURL: acme.LetsEncryptURL, // "https://acme-v02.api.letsencrypt.org/directory"62 DirectoryURL: acme.LetsEncryptURL, // "https://acme-v02.api.letsencrypt.org/directory"
63 }63 }
64- log.Debug("Using Let's Encrypt production environment",64+ logger.Debug("Using Let's Encrypt production environment",
65 "url", acme.LetsEncryptURL)65 "url", acme.LetsEncryptURL)
66 }66 }
67 67
@@ -87,7 +87,7 @@ func (p *Proxy) setupCertManager() {
87 87
88 // Log the attempt but still allow it - this helps debug88 // Log the attempt but still allow it - this helps debug
89 // certificate acquisition issues during development89 // certificate acquisition issues during development
90- log.Warn("Unknown host in certificate request",90+ logger.Warn("Unknown host in certificate request",
91 "host", host,91 "host", host,
92 "adminDomain", adminDomain,92 "adminDomain", adminDomain,
93 "allowed", "yes")93 "allowed", "yes")
@@ -108,9 +108,9 @@ func (p *Proxy) setupCertManager() {
108 108
109 fallbackCert, err := generateFallbackCertificates(fallbackDomains)109 fallbackCert, err := generateFallbackCertificates(fallbackDomains)
110 if err != nil {110 if err != nil {
111- log.Warn("Failed to generate fallback certificate", "error", err)111+ logger.Warn("Failed to generate fallback certificate", "error", err)
112 } else {112 } else {
113- log.Info("Generated fallback self-signed certificate for admin domain",113+ logger.Info("Generated fallback self-signed certificate for admin domain",
114 "domain", adminDomain,114 "domain", adminDomain,
115 "valid_until", time.Now().Add(24*time.Hour).Format("2006-01-02 15:04:05"))115 "valid_until", time.Now().Add(24*time.Hour).Format("2006-01-02 15:04:05"))
116 // Store the fallback certificate116 // Store the fallback certificate
@@ -118,7 +118,7 @@ func (p *Proxy) setupCertManager() {
118 }118 }
119 119
120 p.certManager = certManager120 p.certManager = certManager
121- log.Debug("Certificate manager setup completed",121+ logger.Debug("Certificate manager setup completed",
122 "directory", dir,122 "directory", dir,
123 "mode", p.config.LetsEncryptMode)123 "mode", p.config.LetsEncryptMode)
124 124
@@ -147,7 +147,7 @@ func (p *Proxy) checkCertificateInCache(domain string) bool {
147 certData, err := p.certManager.Cache.Get(ctx, cacheKey)147 certData, err := p.certManager.Cache.Get(ctx, cacheKey)
148 if err != nil {148 if err != nil {
149 // ErrCacheMiss or other error means the certificate is not in the cache149 // ErrCacheMiss or other error means the certificate is not in the cache
150- log.Debug("Certificate not found in cache",150+ logger.Debug("Certificate not found in cache",
151 "domain", domain,151 "domain", domain,
152 "error", err)152 "error", err)
153 return false153 return false
@@ -156,14 +156,14 @@ func (p *Proxy) checkCertificateInCache(domain string) bool {
156 // Parse the certificate to check its validity156 // Parse the certificate to check its validity
157 block, _ := pem.Decode(certData)157 block, _ := pem.Decode(certData)
158 if block == nil || block.Type != "CERTIFICATE" {158 if block == nil || block.Type != "CERTIFICATE" {
159- log.Warn("Invalid certificate data in cache",159+ logger.Warn("Invalid certificate data in cache",
160 "domain", domain)160 "domain", domain)
161 return false161 return false
162 }162 }
163 163
164 cert, err := x509.ParseCertificate(block.Bytes)164 cert, err := x509.ParseCertificate(block.Bytes)
165 if err != nil {165 if err != nil {
166- log.Warn("Failed to parse certificate from cache",166+ logger.Warn("Failed to parse certificate from cache",
167 "domain", domain,167 "domain", domain,
168 "error", err)168 "error", err)
169 return false169 return false
@@ -172,7 +172,7 @@ func (p *Proxy) checkCertificateInCache(domain string) bool {
172 // Check if the certificate is still valid172 // Check if the certificate is still valid
173 now := time.Now()173 now := time.Now()
174 if now.After(cert.NotAfter) || now.Before(cert.NotBefore) {174 if now.After(cert.NotAfter) || now.Before(cert.NotBefore) {
175- log.Info("Certificate in cache has expired or is not yet valid",175+ logger.Info("Certificate in cache has expired or is not yet valid",
176 "domain", domain,176 "domain", domain,
177 "not_before", cert.NotBefore,177 "not_before", cert.NotBefore,
178 "not_after", cert.NotAfter)178 "not_after", cert.NotAfter)
@@ -181,7 +181,7 @@ func (p *Proxy) checkCertificateInCache(domain string) bool {
181 181
182 // Check if the certificate is about to expire (within 30 days)182 // Check if the certificate is about to expire (within 30 days)
183 if now.Add(30 * 24 * time.Hour).After(cert.NotAfter) {183 if now.Add(30 * 24 * time.Hour).After(cert.NotAfter) {
184- log.Info("Certificate in cache is valid but will expire soon",184+ logger.Info("Certificate in cache is valid but will expire soon",
185 "domain", domain,185 "domain", domain,
186 "expires_in", cert.NotAfter.Sub(now).Hours()/24,186 "expires_in", cert.NotAfter.Sub(now).Hours()/24,
187 "days")187 "days")
@@ -189,7 +189,7 @@ func (p *Proxy) checkCertificateInCache(domain string) bool {
189 return false189 return false
190 }190 }
191 191
192- log.Info("Valid certificate found in cache",192+ logger.Info("Valid certificate found in cache",
193 "domain", domain,193 "domain", domain,
194 "expires_in", cert.NotAfter.Sub(now).Hours()/24,194 "expires_in", cert.NotAfter.Sub(now).Hours()/24,
195 "days")195 "days")
@@ -206,7 +206,7 @@ func (p *Proxy) requestAdminCertificate() {
206 206
207 // Only proceed if HTTPS is enabled207 // Only proceed if HTTPS is enabled
208 if !p.app.GetConfig().Http.Https {208 if !p.app.GetConfig().Http.Https {
209- log.Debug("HTTPS is disabled, skipping admin certificate request")209+ logger.Debug("HTTPS is disabled, skipping admin certificate request")
210 return210 return
211 }211 }
212 212
@@ -214,7 +214,7 @@ func (p *Proxy) requestAdminCertificate() {
214 host := adminDomain214 host := adminDomain
215 ips, err := net.LookupIP(host)215 ips, err := net.LookupIP(host)
216 if err != nil {216 if err != nil {
217- log.Error("Could not resolve admin domain, Let's Encrypt will likely fail",217+ logger.Error("Could not resolve admin domain, Let's Encrypt will likely fail",
218 "domain", adminDomain,218 "domain", adminDomain,
219 "error", err.Error(),219 "error", err.Error(),
220 "solution", "Check DNS settings and ensure domain points to this server")220 "solution", "Check DNS settings and ensure domain points to this server")
@@ -226,13 +226,13 @@ func (p *Proxy) requestAdminCertificate() {
226 ipStrings = append(ipStrings, ip.String())226 ipStrings = append(ipStrings, ip.String())
227 }227 }
228 228
229- log.Info("Successfully resolved admin domain",229+ logger.Info("Successfully resolved admin domain",
230 "domain", adminDomain,230 "domain", adminDomain,
231 "ips", ipStrings)231 "ips", ipStrings)
232 232
233 // Check if we already have a valid certificate in the cache233 // Check if we already have a valid certificate in the cache
234 if p.checkCertificateInCache(adminDomain) {234 if p.checkCertificateInCache(adminDomain) {
235- log.Info("Using existing certificate from cache",235+ logger.Info("Using existing certificate from cache",
236 "domain", adminDomain,236 "domain", adminDomain,
237 "action", "skipping Let's Encrypt request to avoid rate limits")237 "action", "skipping Let's Encrypt request to avoid rate limits")
238 return238 return
@@ -247,12 +247,12 @@ func (p *Proxy) requestAdminCertificate() {
247 }247 }
248 248
249 // Log the certificate request intent249 // Log the certificate request intent
250- log.Info("Initiating Let's Encrypt certificate request for admin domain",250+ logger.Info("Initiating Let's Encrypt certificate request for admin domain",
251 "domain", adminDomain,251 "domain", adminDomain,
252 "email", email,252 "email", email,
253 "mode", mode)253 "mode", mode)
254 254
255- log.Info("⏳ Waiting for Let's Encrypt to validate domain ownership",255+ logger.Info("⏳ Waiting for Let's Encrypt to validate domain ownership",
256 "domain", adminDomain,256 "domain", adminDomain,
257 "validation_method", "HTTP-01 challenge",257 "validation_method", "HTTP-01 challenge",
258 "requirements", "Domain must be publicly accessible on port 80",258 "requirements", "Domain must be publicly accessible on port 80",
@@ -289,7 +289,7 @@ func (p *Proxy) requestAdminCertificate() {
289 certErr = result.err289 certErr = result.err
290 case <-ctx.Done():290 case <-ctx.Done():
291 certErr = fmt.Errorf("certificate request timed out after 2 minutes: %w", ctx.Err())291 certErr = fmt.Errorf("certificate request timed out after 2 minutes: %w", ctx.Err())
292- log.Error("Let's Encrypt certificate request timed out",292+ logger.Error("Let's Encrypt certificate request timed out",
293 "domain", adminDomain,293 "domain", adminDomain,
294 "timeout", "2 minutes",294 "timeout", "2 minutes",
295 "error", certErr)295 "error", certErr)
@@ -306,31 +306,31 @@ func (p *Proxy) requestAdminCertificate() {
306 retryAfterStr = matches[1]306 retryAfterStr = matches[1]
307 }307 }
308 308
309- log.Error("Let's Encrypt rate limit reached",309+ logger.Error("Let's Encrypt rate limit reached",
310 "domain", adminDomain,310 "domain", adminDomain,
311 "error", certErr,311 "error", certErr,
312 "retry_after", retryAfterStr,312 "retry_after", retryAfterStr,
313 "solution", "Wait until the rate limit expires, then restart the server")313 "solution", "Wait until the rate limit expires, then restart the server")
314 314
315 // Don't retry on rate limits315 // Don't retry on rate limits
316- log.Info("Skipping certificate request retries due to rate limiting",316+ logger.Info("Skipping certificate request retries due to rate limiting",
317 "domain", adminDomain)317 "domain", adminDomain)
318 } else {318 } else {
319- log.Error("Failed to obtain Let's Encrypt certificate",319+ logger.Error("Failed to obtain Let's Encrypt certificate",
320 "domain", adminDomain,320 "domain", adminDomain,
321 "error", certErr)321 "error", certErr)
322 322
323- log.Info("⏳ Retrying certificate request with exponential backoff",323+ logger.Info("⏳ Retrying certificate request with exponential backoff",
324 "domain", adminDomain,324 "domain", adminDomain,
325 "timeout", "2 minutes")325 "timeout", "2 minutes")
326 // Implement retry logic with backoff326 // Implement retry logic with backoff
327 go p.retryCertificateRequest(adminDomain, 3, 10*time.Second)327 go p.retryCertificateRequest(adminDomain, 3, 10*time.Second)
328 }328 }
329 } else if certResult != nil {329 } else if certResult != nil {
330- log.Info("Successfully obtained Let's Encrypt certificate",330+ logger.Info("Successfully obtained Let's Encrypt certificate",
331 "domain", adminDomain)331 "domain", adminDomain)
332 } else {332 } else {
333- log.Error("Unexpected error: received nil certificate but no error",333+ logger.Error("Unexpected error: received nil certificate but no error",
334 "domain", adminDomain,334 "domain", adminDomain,
335 "timeout", "2 minutes")335 "timeout", "2 minutes")
336 // Implement retry logic with backoff336 // Implement retry logic with backoff
@@ -342,14 +342,14 @@ func (p *Proxy) requestAdminCertificate() {
342 // for a container domain342 // for a container domain
343 func (p *Proxy) requestDomainCertificate(domain string) {343 func (p *Proxy) requestDomainCertificate(domain string) {
344 if domain == "" {344 if domain == "" {
345- log.Warn("Empty domain provided to requestDomainCertificate")345+ logger.Warn("Empty domain provided to requestDomainCertificate")
346 return346 return
347 }347 }
348 348
349 // Extract hostname from domain, resolving it to see if it's publicly accessible349 // Extract hostname from domain, resolving it to see if it's publicly accessible
350 ips, err := net.LookupIP(domain)350 ips, err := net.LookupIP(domain)
351 if err != nil {351 if err != nil {
352- log.Error("Could not resolve domain, Let's Encrypt will likely fail",352+ logger.Error("Could not resolve domain, Let's Encrypt will likely fail",
353 "domain", domain,353 "domain", domain,
354 "error", err.Error(),354 "error", err.Error(),
355 "solution", "Check DNS settings and ensure domain points to this server")355 "solution", "Check DNS settings and ensure domain points to this server")
@@ -361,13 +361,13 @@ func (p *Proxy) requestDomainCertificate(domain string) {
361 ipStrings = append(ipStrings, ip.String())361 ipStrings = append(ipStrings, ip.String())
362 }362 }
363 363
364- log.Info("Successfully resolved domain for certificate request",364+ logger.Info("Successfully resolved domain for certificate request",
365 "domain", domain,365 "domain", domain,
366 "ips", ipStrings)366 "ips", ipStrings)
367 367
368 // Check if we already have a valid certificate in the cache368 // Check if we already have a valid certificate in the cache
369 if p.checkCertificateInCache(domain) {369 if p.checkCertificateInCache(domain) {
370- log.Info("Using existing certificate from cache",370+ logger.Info("Using existing certificate from cache",
371 "domain", domain,371 "domain", domain,
372 "action", "skipping Let's Encrypt request to avoid rate limits")372 "action", "skipping Let's Encrypt request to avoid rate limits")
373 return373 return
@@ -382,12 +382,12 @@ func (p *Proxy) requestDomainCertificate(domain string) {
382 }382 }
383 383
384 // Log the certificate request intent384 // Log the certificate request intent
385- log.Info("Initiating Let's Encrypt certificate request for container domain",385+ logger.Info("Initiating Let's Encrypt certificate request for container domain",
386 "domain", domain,386 "domain", domain,
387 "email", email,387 "email", email,
388 "mode", mode)388 "mode", mode)
389 389
390- log.Info("⏳ Waiting for Let's Encrypt to validate domain ownership",390+ logger.Info("⏳ Waiting for Let's Encrypt to validate domain ownership",
391 "domain", domain,391 "domain", domain,
392 "validation_method", "HTTP-01 challenge",392 "validation_method", "HTTP-01 challenge",
393 "requirements", "Domain must be publicly accessible on port 80",393 "requirements", "Domain must be publicly accessible on port 80",
@@ -424,7 +424,7 @@ func (p *Proxy) requestDomainCertificate(domain string) {
424 certErr = result.err424 certErr = result.err
425 case <-ctx.Done():425 case <-ctx.Done():
426 certErr = fmt.Errorf("certificate request timed out after 1 minute: %w", ctx.Err())426 certErr = fmt.Errorf("certificate request timed out after 1 minute: %w", ctx.Err())
427- log.Error("Let's Encrypt certificate request timed out",427+ logger.Error("Let's Encrypt certificate request timed out",
428 "domain", domain,428 "domain", domain,
429 "timeout", "1 minute",429 "timeout", "1 minute",
430 "error", certErr)430 "error", certErr)
@@ -441,31 +441,31 @@ func (p *Proxy) requestDomainCertificate(domain string) {
441 retryAfterStr = matches[1]441 retryAfterStr = matches[1]
442 }442 }
443 443
444- log.Error("Let's Encrypt rate limit reached",444+ logger.Error("Let's Encrypt rate limit reached",
445 "domain", domain,445 "domain", domain,
446 "error", certErr,446 "error", certErr,
447 "retry_after", retryAfterStr,447 "retry_after", retryAfterStr,
448 "solution", "Wait until the rate limit expires, then restart the server")448 "solution", "Wait until the rate limit expires, then restart the server")
449 449
450 // Don't retry on rate limits450 // Don't retry on rate limits
451- log.Info("Skipping certificate request retries due to rate limiting",451+ logger.Info("Skipping certificate request retries due to rate limiting",
452 "domain", domain)452 "domain", domain)
453 } else {453 } else {
454- log.Error("Failed to obtain Let's Encrypt certificate",454+ logger.Error("Failed to obtain Let's Encrypt certificate",
455 "domain", domain,455 "domain", domain,
456 "error", certErr)456 "error", certErr)
457 457
458- log.Info("⏳ Retrying certificate request with exponential backoff",458+ logger.Info("⏳ Retrying certificate request with exponential backoff",
459 "domain", domain,459 "domain", domain,
460 "timeout", "2 minutes")460 "timeout", "2 minutes")
461 // Implement retry logic with backoff461 // Implement retry logic with backoff
462 go p.retryCertificateRequest(domain, 3, 10*time.Second)462 go p.retryCertificateRequest(domain, 3, 10*time.Second)
463 }463 }
464 } else if certResult != nil {464 } else if certResult != nil {
465- log.Info("Successfully obtained Let's Encrypt certificate",465+ logger.Info("Successfully obtained Let's Encrypt certificate",
466 "domain", domain)466 "domain", domain)
467 } else {467 } else {
468- log.Error("Unexpected error: received nil certificate but no error",468+ logger.Error("Unexpected error: received nil certificate but no error",
469 "domain", domain,469 "domain", domain,
470 "timeout", "1 minute")470 "timeout", "1 minute")
471 // Implement retry logic with backoff471 // Implement retry logic with backoff
@@ -479,7 +479,7 @@ func (p *Proxy) retryCertificateRequest(domain string, maxRetries int, initialBa
479 479
480 for attempt := 1; attempt <= maxRetries; attempt++ {480 for attempt := 1; attempt <= maxRetries; attempt++ {
481 // Wait for backoff period481 // Wait for backoff period
482- log.Info("Retrying Let's Encrypt certificate request",482+ logger.Info("Retrying Let's Encrypt certificate request",
483 "domain", domain,483 "domain", domain,
484 "attempt", attempt,484 "attempt", attempt,
485 "max_retries", maxRetries,485 "max_retries", maxRetries,
@@ -503,14 +503,14 @@ func (p *Proxy) retryCertificateRequest(domain string, maxRetries int, initialBa
503 resp, err := client.Get(testURL)503 resp, err := client.Get(testURL)
504 504
505 if err != nil {505 if err != nil {
506- log.Warn("HTTP challenge endpoint might not be accessible",506+ logger.Warn("HTTP challenge endpoint might not be accessible",
507 "domain", domain,507 "domain", domain,
508 "url", testURL,508 "url", testURL,
509 "error", err,509 "error", err,
510 "solution", "Ensure port 80 is accessible and not blocked by firewall")510 "solution", "Ensure port 80 is accessible and not blocked by firewall")
511 } else {511 } else {
512 resp.Body.Close()512 resp.Body.Close()
513- log.Info("HTTP challenge endpoint is accessible",513+ logger.Info("HTTP challenge endpoint is accessible",
514 "domain", domain,514 "domain", domain,
515 "url", testURL,515 "url", testURL,
516 "status", resp.StatusCode)516 "status", resp.StatusCode)
@@ -534,13 +534,13 @@ func (p *Proxy) retryCertificateRequest(domain string, maxRetries int, initialBa
534 534
535 // Check result535 // Check result
536 if err == nil {536 if err == nil {
537- log.Info("Successfully obtained Let's Encrypt certificate on retry",537+ logger.Info("Successfully obtained Let's Encrypt certificate on retry",
538 "domain", domain,538 "domain", domain,
539 "attempt", attempt)539 "attempt", attempt)
540 return540 return
541 }541 }
542 542
543- log.Error("Let's Encrypt certificate request retry failed",543+ logger.Error("Let's Encrypt certificate request retry failed",
544 "domain", domain,544 "domain", domain,
545 "attempt", attempt,545 "attempt", attempt,
546 "error", err)546 "error", err)
@@ -548,13 +548,13 @@ func (p *Proxy) retryCertificateRequest(domain string, maxRetries int, initialBa
548 // Provide more detailed diagnostics based on the error548 // Provide more detailed diagnostics based on the error
549 if strings.Contains(strings.ToLower(err.Error()), "connection refused") ||549 if strings.Contains(strings.ToLower(err.Error()), "connection refused") ||
550 strings.Contains(strings.ToLower(err.Error()), "timeout") {550 strings.Contains(strings.ToLower(err.Error()), "timeout") {
551- log.Error("Let's Encrypt connection failed - this typically indicates:",551+ logger.Error("Let's Encrypt connection failed - this typically indicates:",
552 "issue_1", "Port 80 is not accessible from the internet",552 "issue_1", "Port 80 is not accessible from the internet",
553 "issue_2", "Firewall is blocking inbound connections",553 "issue_2", "Firewall is blocking inbound connections",
554 "issue_3", "DNS records not properly propagated",554 "issue_3", "DNS records not properly propagated",
555 "solution", "Check firewall settings and DNS configuration")555 "solution", "Check firewall settings and DNS configuration")
556 } else if strings.Contains(strings.ToLower(err.Error()), "unauthorized") {556 } else if strings.Contains(strings.ToLower(err.Error()), "unauthorized") {
557- log.Error("Let's Encrypt authorization failed - this typically indicates:",557+ logger.Error("Let's Encrypt authorization failed - this typically indicates:",
558 "issue", "Domain ownership validation failed",558 "issue", "Domain ownership validation failed",
559 "solution", "Ensure the server is publicly accessible on port 80")559 "solution", "Ensure the server is publicly accessible on port 80")
560 } else if strings.Contains(strings.ToLower(err.Error()), "ratelimited") {560 } else if strings.Contains(strings.ToLower(err.Error()), "ratelimited") {
@@ -570,7 +570,7 @@ func (p *Proxy) retryCertificateRequest(domain string, maxRetries int, initialBa
570 }570 }
571 }571 }
572 572
573- log.Error("Let's Encrypt rate limit reached - cannot issue more certificates for this domain yet",573+ logger.Error("Let's Encrypt rate limit reached - cannot issue more certificates for this domain yet",
574 "domain", domain,574 "domain", domain,
575 "retry_after", retryAfterStr,575 "retry_after", retryAfterStr,
576 "solution", "Wait until the rate limit expires, then restart the server")576 "solution", "Wait until the rate limit expires, then restart the server")
@@ -583,20 +583,20 @@ func (p *Proxy) retryCertificateRequest(domain string, maxRetries int, initialBa
583 backoff *= 2583 backoff *= 2
584 }584 }
585 585
586- log.Error("All Let's Encrypt certificate request retries failed",586+ logger.Error("All Let's Encrypt certificate request retries failed",
587 "domain", domain,587 "domain", domain,
588 "max_retries", maxRetries,588 "max_retries", maxRetries,
589 "fallback", "Using self-signed certificate")589 "fallback", "Using self-signed certificate")
590 590
591 // At this point, all retries have failed, so we'll rely on the fallback self-signed certificate591 // At this point, all retries have failed, so we'll rely on the fallback self-signed certificate
592 // But let's log this prominently for debugging592 // But let's log this prominently for debugging
593- log.Error("⚠️ HTTPS is using a self-signed certificate which browsers will warn about",593+ logger.Error("⚠️ HTTPS is using a self-signed certificate which browsers will warn about",
594 "domain", domain,594 "domain", domain,
595 "reason", "Let's Encrypt certificate issuance failed",595 "reason", "Let's Encrypt certificate issuance failed",
596 "solution", "Check network settings and Let's Encrypt status")596 "solution", "Check network settings and Let's Encrypt status")
597 597
598 // Suggest checking the common issues598 // Suggest checking the common issues
599- log.Error("Common Let's Encrypt issues to check:",599+ logger.Error("Common Let's Encrypt issues to check:",
600 "check_1", "Ensure ports 80 and 443 are open on your firewall",600 "check_1", "Ensure ports 80 and 443 are open on your firewall",
601 "check_2", "Verify DNS records point to your server IP",601 "check_2", "Verify DNS records point to your server IP",
602 "check_3", "Make sure no other services are running on ports 80/443",602 "check_3", "Make sure no other services are running on ports 80/443",
M internal/proxy/containers.go +25 -8
@@ -6,7 +6,7 @@ import (
66 "strings"
77
88 "github.com/bnema/gordon/pkg/docker"
9- "github.com/charmbracelet/log"
9+ "github.com/bnema/gordon/pkg/logger"
1010 )
1111
1212 // scanContainersAndCheckCertificates scans all containers from the Gordon network
@@ -19,7 +19,7 @@ func (p *Proxy) scanContainersAndCheckCertificates() error {
1919 return fmt.Errorf("container network name is not configured")
2020 }
2121
22- log.Info("Scanning containers in network for certificate checks",
22+ logger.Info("Scanning containers in network for certificate checks",
2323 "network", networkName)
2424
2525 // Try to get network info using the docker client
@@ -31,7 +31,7 @@ func (p *Proxy) scanContainersAndCheckCertificates() error {
3131 // Skip admin cert check if environment variable is set
3232 skipAdminCertCheck := os.Getenv("GORDON_SKIP_ADMIN_CERT_CHECK") == "true"
3333 if skipAdminCertCheck {
34- log.Info("Skipping admin certificate check as GORDON_SKIP_ADMIN_CERT_CHECK=true")
34+ logger.Info("Skipping admin certificate check as GORDON_SKIP_ADMIN_CERT_CHECK=true")
3535 }
3636
3737 // Get all containers in the network
@@ -41,7 +41,7 @@ func (p *Proxy) scanContainersAndCheckCertificates() error {
4141 // Get container info to check for relevant domains
4242 containerInfo, err := docker.GetContainerInfo(containerID)
4343 if err != nil {
44- log.Warn("Failed to get container info",
44+ logger.Warn("Failed to get container info",
4545 "containerID", containerID,
4646 "containerName", containerName,
4747 "error", err)
@@ -54,7 +54,22 @@ func (p *Proxy) scanContainersAndCheckCertificates() error {
5454
5555 // Check common domain-related labels
5656 if domainLabel, exists := containerInfo.Config.Labels["gordon.domain"]; exists && domainLabel != "" {
57+ // Remove any leading dot from domain label
58+ domainLabel = strings.TrimPrefix(domainLabel, ".")
59+
60+ // Always add the base domain
5761 domains = append(domains, domainLabel)
62+
63+ // Also check if there's a service label to use as a subdomain
64+ if serviceLabel, serviceExists := containerInfo.Config.Labels["gordon.service"]; serviceExists && serviceLabel != "" {
65+ // Combine service name with domain to create service.domain format
66+ serviceSubdomain := serviceLabel + "." + domainLabel
67+ domains = append(domains, serviceSubdomain)
68+ logger.Debug("Created additional domain from service label",
69+ "domain", serviceSubdomain,
70+ "service", serviceLabel,
71+ "base_domain", domainLabel)
72+ }
5873 }
5974
6075 // Check environment variables for domain information
@@ -64,7 +79,9 @@ func (p *Proxy) scanContainersAndCheckCertificates() error {
6479 if len(parts) == 2 && parts[1] != "" {
6580 // Multiple domains might be comma-separated
6681 for _, domain := range strings.Split(parts[1], ",") {
67- domains = append(domains, strings.TrimSpace(domain))
82+ // Remove any leading dot before adding the domain
83+ cleanDomain := strings.TrimPrefix(strings.TrimSpace(domain), ".")
84+ domains = append(domains, cleanDomain)
6885 }
6986 }
7087 }
@@ -96,11 +113,11 @@ func (p *Proxy) scanContainersAndCheckCertificates() error {
96113 hasCert := p.checkCertificateInCache(domain)
97114
98115 if hasCert {
99- log.Debug("Certificate exists for domain",
116+ logger.Debug("Certificate exists for domain",
100117 "domain", domain,
101118 "container", containerName)
102119 } else {
103- log.Info("Certificate not found in cache, requesting",
120+ logger.Info("Certificate not found in cache, requesting",
104121 "domain", domain,
105122 "container", containerName)
106123
@@ -125,7 +142,7 @@ func (p *Proxy) scanContainersAndCheckCertificates() error {
125142 if len(parts) > 2 && !isCommonTLD(parts[len(parts)-1]) {
126143 hasCert = p.checkCertificateInCache(parentDomain)
127144 if !hasCert {
128- log.Info("Certificate not found for parent domain",
145+ logger.Info("Certificate not found for parent domain",
129146 "domain", parentDomain,
130147 "container", containerName)
131148
@@ -6,7 +6,7 @@ import (
6 "strings"6 "strings"
7 7
8 "github.com/bnema/gordon/pkg/docker"8 "github.com/bnema/gordon/pkg/docker"
9- "github.com/charmbracelet/log"9+ "github.com/bnema/gordon/pkg/logger"
10 )10 )
11 11
12 // scanContainersAndCheckCertificates scans all containers from the Gordon network12 // scanContainersAndCheckCertificates scans all containers from the Gordon network
@@ -19,7 +19,7 @@ func (p *Proxy) scanContainersAndCheckCertificates() error {
19 return fmt.Errorf("container network name is not configured")19 return fmt.Errorf("container network name is not configured")
20 }20 }
21 21
22- log.Info("Scanning containers in network for certificate checks",22+ logger.Info("Scanning containers in network for certificate checks",
23 "network", networkName)23 "network", networkName)
24 24
25 // Try to get network info using the docker client25 // Try to get network info using the docker client
@@ -31,7 +31,7 @@ func (p *Proxy) scanContainersAndCheckCertificates() error {
31 // Skip admin cert check if environment variable is set31 // Skip admin cert check if environment variable is set
32 skipAdminCertCheck := os.Getenv("GORDON_SKIP_ADMIN_CERT_CHECK") == "true"32 skipAdminCertCheck := os.Getenv("GORDON_SKIP_ADMIN_CERT_CHECK") == "true"
33 if skipAdminCertCheck {33 if skipAdminCertCheck {
34- log.Info("Skipping admin certificate check as GORDON_SKIP_ADMIN_CERT_CHECK=true")34+ logger.Info("Skipping admin certificate check as GORDON_SKIP_ADMIN_CERT_CHECK=true")
35 }35 }
36 36
37 // Get all containers in the network37 // Get all containers in the network
@@ -41,7 +41,7 @@ func (p *Proxy) scanContainersAndCheckCertificates() error {
41 // Get container info to check for relevant domains41 // Get container info to check for relevant domains
42 containerInfo, err := docker.GetContainerInfo(containerID)42 containerInfo, err := docker.GetContainerInfo(containerID)
43 if err != nil {43 if err != nil {
44- log.Warn("Failed to get container info",44+ logger.Warn("Failed to get container info",
45 "containerID", containerID,45 "containerID", containerID,
46 "containerName", containerName,46 "containerName", containerName,
47 "error", err)47 "error", err)
@@ -54,7 +54,22 @@ func (p *Proxy) scanContainersAndCheckCertificates() error {
54 54
55 // Check common domain-related labels55 // Check common domain-related labels
56 if domainLabel, exists := containerInfo.Config.Labels["gordon.domain"]; exists && domainLabel != "" {56 if domainLabel, exists := containerInfo.Config.Labels["gordon.domain"]; exists && domainLabel != "" {
57+ // Remove any leading dot from domain label
58+ domainLabel = strings.TrimPrefix(domainLabel, ".")
59+
60+ // Always add the base domain
57 domains = append(domains, domainLabel)61 domains = append(domains, domainLabel)
62+
63+ // Also check if there's a service label to use as a subdomain
64+ if serviceLabel, serviceExists := containerInfo.Config.Labels["gordon.service"]; serviceExists && serviceLabel != "" {
65+ // Combine service name with domain to create service.domain format
66+ serviceSubdomain := serviceLabel + "." + domainLabel
67+ domains = append(domains, serviceSubdomain)
68+ logger.Debug("Created additional domain from service label",
69+ "domain", serviceSubdomain,
70+ "service", serviceLabel,
71+ "base_domain", domainLabel)
72+ }
58 }73 }
59 74
60 // Check environment variables for domain information75 // Check environment variables for domain information
@@ -64,7 +79,9 @@ func (p *Proxy) scanContainersAndCheckCertificates() error {
64 if len(parts) == 2 && parts[1] != "" {79 if len(parts) == 2 && parts[1] != "" {
65 // Multiple domains might be comma-separated80 // Multiple domains might be comma-separated
66 for _, domain := range strings.Split(parts[1], ",") {81 for _, domain := range strings.Split(parts[1], ",") {
67- domains = append(domains, strings.TrimSpace(domain))82+ // Remove any leading dot before adding the domain
83+ cleanDomain := strings.TrimPrefix(strings.TrimSpace(domain), ".")
84+ domains = append(domains, cleanDomain)
68 }85 }
69 }86 }
70 }87 }
@@ -96,11 +113,11 @@ func (p *Proxy) scanContainersAndCheckCertificates() error {
96 hasCert := p.checkCertificateInCache(domain)113 hasCert := p.checkCertificateInCache(domain)
97 114
98 if hasCert {115 if hasCert {
99- log.Debug("Certificate exists for domain",116+ logger.Debug("Certificate exists for domain",
100 "domain", domain,117 "domain", domain,
101 "container", containerName)118 "container", containerName)
102 } else {119 } else {
103- log.Info("Certificate not found in cache, requesting",120+ logger.Info("Certificate not found in cache, requesting",
104 "domain", domain,121 "domain", domain,
105 "container", containerName)122 "container", containerName)
106 123
@@ -125,7 +142,7 @@ func (p *Proxy) scanContainersAndCheckCertificates() error {
125 if len(parts) > 2 && !isCommonTLD(parts[len(parts)-1]) {142 if len(parts) > 2 && !isCommonTLD(parts[len(parts)-1]) {
126 hasCert = p.checkCertificateInCache(parentDomain)143 hasCert = p.checkCertificateInCache(parentDomain)
127 if !hasCert {144 if !hasCert {
128- log.Info("Certificate not found for parent domain",145+ logger.Info("Certificate not found for parent domain",
129 "domain", parentDomain,146 "domain", parentDomain,
130 "container", containerName)147 "container", containerName)
131 148
M internal/proxy/core.go +6 -6
@@ -11,7 +11,7 @@ import (
1111 "github.com/bnema/gordon/internal/common"
1212 "github.com/bnema/gordon/internal/interfaces"
1313 "github.com/bnema/gordon/pkg/docker"
14- "github.com/charmbracelet/log"
14+ "github.com/bnema/gordon/pkg/logger"
1515 "github.com/labstack/echo/v4"
1616 "golang.org/x/crypto/acme/autocert"
1717 )
@@ -55,10 +55,10 @@ type Proxy struct {
5555
5656 // NewProxy creates a new instance of the reverse proxy
5757 func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
58- log.Debug("Initializing reverse proxy")
58+ logger.Debug("Initializing reverse proxy")
5959
6060 // Log important configuration information
61- log.Debug("All internal connections to containers will use HTTP protocol regardless of external protocol")
61+ logger.Debug("All internal connections to containers will use HTTP protocol regardless of external protocol")
6262
6363 // Set up the echo server for HTTPS traffic
6464 httpsServer := echo.New()
@@ -85,7 +85,7 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
8585 // Store our container ID for future reference
8686 ourContainerID = container.ID
8787 containerName := strings.TrimLeft(container.Names[0], "/")
88- log.Info("Gordon identity established",
88+ logger.Info("Gordon identity established",
8989 "container_id", ourContainerID,
9090 "container_name", containerName)
9191 break
@@ -99,7 +99,7 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
9999 blacklistPath := app.GetConfig().General.StorageDir + "/blacklist.json"
100100 blacklist, err := NewBlacklist(blacklistPath)
101101 if err != nil {
102- log.Warn("Failed to initialize blacklist, continuing without it", "error", err)
102+ logger.Warn("Failed to initialize blacklist, continuing without it", "error", err)
103103 }
104104
105105 // Initialize routes map
@@ -126,6 +126,6 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
126126 // Set up the certificate manager
127127 p.setupCertManager()
128128
129- log.Debug("Reverse proxy initialized")
129+ logger.Debug("Reverse proxy initialized")
130130 return p, nil
131131 }
@@ -11,7 +11,7 @@ import (
11 "github.com/bnema/gordon/internal/common"11 "github.com/bnema/gordon/internal/common"
12 "github.com/bnema/gordon/internal/interfaces"12 "github.com/bnema/gordon/internal/interfaces"
13 "github.com/bnema/gordon/pkg/docker"13 "github.com/bnema/gordon/pkg/docker"
14- "github.com/charmbracelet/log"14+ "github.com/bnema/gordon/pkg/logger"
15 "github.com/labstack/echo/v4"15 "github.com/labstack/echo/v4"
16 "golang.org/x/crypto/acme/autocert"16 "golang.org/x/crypto/acme/autocert"
17 )17 )
@@ -55,10 +55,10 @@ type Proxy struct {
55 55
56 // NewProxy creates a new instance of the reverse proxy56 // NewProxy creates a new instance of the reverse proxy
57 func NewProxy(app interfaces.AppInterface) (*Proxy, error) {57 func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
58- log.Debug("Initializing reverse proxy")58+ logger.Debug("Initializing reverse proxy")
59 59
60 // Log important configuration information60 // Log important configuration information
61- log.Debug("All internal connections to containers will use HTTP protocol regardless of external protocol")61+ logger.Debug("All internal connections to containers will use HTTP protocol regardless of external protocol")
62 62
63 // Set up the echo server for HTTPS traffic63 // Set up the echo server for HTTPS traffic
64 httpsServer := echo.New()64 httpsServer := echo.New()
@@ -85,7 +85,7 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
85 // Store our container ID for future reference85 // Store our container ID for future reference
86 ourContainerID = container.ID86 ourContainerID = container.ID
87 containerName := strings.TrimLeft(container.Names[0], "/")87 containerName := strings.TrimLeft(container.Names[0], "/")
88- log.Info("Gordon identity established",88+ logger.Info("Gordon identity established",
89 "container_id", ourContainerID,89 "container_id", ourContainerID,
90 "container_name", containerName)90 "container_name", containerName)
91 break91 break
@@ -99,7 +99,7 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
99 blacklistPath := app.GetConfig().General.StorageDir + "/blacklist.json"99 blacklistPath := app.GetConfig().General.StorageDir + "/blacklist.json"
100 blacklist, err := NewBlacklist(blacklistPath)100 blacklist, err := NewBlacklist(blacklistPath)
101 if err != nil {101 if err != nil {
102- log.Warn("Failed to initialize blacklist, continuing without it", "error", err)102+ logger.Warn("Failed to initialize blacklist, continuing without it", "error", err)
103 }103 }
104 104
105 // Initialize routes map105 // Initialize routes map
@@ -126,6 +126,6 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
126 // Set up the certificate manager126 // Set up the certificate manager
127 p.setupCertManager()127 p.setupCertManager()
128 128
129- log.Debug("Reverse proxy initialized")129+ logger.Debug("Reverse proxy initialized")
130 return p, nil130 return p, nil
131 }131 }
A internal/proxy/db_helpers.go +133 -0
@@ -0,0 +1,133 @@
1+package proxy
2+
3+import (
4+ "database/sql"
5+ "fmt"
6+ "time"
7+
8+ "github.com/charmbracelet/log"
9+)
10+
11+// dbExecWithRetry performs a database execution with retry logic for handling locked database
12+func (p *Proxy) dbExecWithRetry(query string, args ...interface{}) (sql.Result, error) {
13+ var result sql.Result
14+ var err error
15+ maxRetries := 5
16+ retryDelay := 100 * time.Millisecond
17+
18+ for attempt := 1; attempt <= maxRetries; attempt++ {
19+ result, err = p.app.GetDB().Exec(query, args...)
20+ if err == nil {
21+ return result, nil
22+ }
23+
24+ // Check if this is a database locked error
25+ if err.Error() == "database is locked (5) (SQLITE_BUSY)" {
26+ log.Debug("Database locked, retrying operation",
27+ "attempt", attempt,
28+ "max_retries", maxRetries)
29+ time.Sleep(retryDelay)
30+ // Increase delay for next retry (exponential backoff)
31+ retryDelay *= 2
32+ continue
33+ }
34+
35+ // If it's not a locking error, return immediately
36+ return nil, err
37+ }
38+
39+ return nil, fmt.Errorf("max retries exceeded: %w", err)
40+}
41+
42+// dbQueryWithRetry performs a database query with retry logic for handling locked database
43+func (p *Proxy) dbQueryWithRetry(query string, args ...interface{}) (*sql.Rows, error) {
44+ var rows *sql.Rows
45+ var err error
46+ maxRetries := 5
47+ retryDelay := 100 * time.Millisecond
48+
49+ for attempt := 1; attempt <= maxRetries; attempt++ {
50+ rows, err = p.app.GetDB().Query(query, args...)
51+ if err == nil {
52+ return rows, nil
53+ }
54+
55+ // Check if this is a database locked error
56+ if err.Error() == "database is locked (5) (SQLITE_BUSY)" {
57+ log.Debug("Database locked, retrying operation",
58+ "attempt", attempt,
59+ "max_retries", maxRetries)
60+ time.Sleep(retryDelay)
61+ // Increase delay for next retry (exponential backoff)
62+ retryDelay *= 2
63+ continue
64+ }
65+
66+ // If it's not a locking error, return immediately
67+ return nil, err
68+ }
69+
70+ return nil, fmt.Errorf("max retries exceeded: %w", err)
71+}
72+
73+// dbBeginWithRetry starts a transaction with retry logic for handling locked database
74+func (p *Proxy) dbBeginWithRetry() (*sql.Tx, error) {
75+ var tx *sql.Tx
76+ var err error
77+ maxRetries := 5
78+ retryDelay := 100 * time.Millisecond
79+
80+ for attempt := 1; attempt <= maxRetries; attempt++ {
81+ tx, err = p.app.GetDB().Begin()
82+ if err == nil {
83+ return tx, nil
84+ }
85+
86+ // Check if this is a database locked error
87+ if err.Error() == "database is locked (5) (SQLITE_BUSY)" {
88+ log.Debug("Database locked, retrying transaction start",
89+ "attempt", attempt,
90+ "max_retries", maxRetries)
91+ time.Sleep(retryDelay)
92+ // Increase delay for next retry (exponential backoff)
93+ retryDelay *= 2
94+ continue
95+ }
96+
97+ // If it's not a locking error, return immediately
98+ return nil, err
99+ }
100+
101+ return nil, fmt.Errorf("max retries exceeded: %w", err)
102+}
103+
104+// txExecWithRetry executes a query within a transaction with retry logic
105+func txExecWithRetry(tx *sql.Tx, query string, args ...interface{}) (sql.Result, error) {
106+ var result sql.Result
107+ var err error
108+ maxRetries := 5
109+ retryDelay := 100 * time.Millisecond
110+
111+ for attempt := 1; attempt <= maxRetries; attempt++ {
112+ result, err = tx.Exec(query, args...)
113+ if err == nil {
114+ return result, nil
115+ }
116+
117+ // Check if this is a database locked error
118+ if err.Error() == "database is locked (5) (SQLITE_BUSY)" {
119+ log.Debug("Database locked, retrying transaction operation",
120+ "attempt", attempt,
121+ "max_retries", maxRetries)
122+ time.Sleep(retryDelay)
123+ // Increase delay for next retry (exponential backoff)
124+ retryDelay *= 2
125+ continue
126+ }
127+
128+ // If it's not a locking error, return immediately
129+ return nil, err
130+ }
131+
132+ return nil, fmt.Errorf("max retries exceeded: %w", err)
133+}
@@ -0,0 +1,133 @@
1+package proxy
2+
3+import (
4+ "database/sql"
5+ "fmt"
6+ "time"
7+
8+ "github.com/charmbracelet/log"
9+)
10+
11+// dbExecWithRetry performs a database execution with retry logic for handling locked database
12+func (p *Proxy) dbExecWithRetry(query string, args ...interface{}) (sql.Result, error) {
13+ var result sql.Result
14+ var err error
15+ maxRetries := 5
16+ retryDelay := 100 * time.Millisecond
17+
18+ for attempt := 1; attempt <= maxRetries; attempt++ {
19+ result, err = p.app.GetDB().Exec(query, args...)
20+ if err == nil {
21+ return result, nil
22+ }
23+
24+ // Check if this is a database locked error
25+ if err.Error() == "database is locked (5) (SQLITE_BUSY)" {
26+ log.Debug("Database locked, retrying operation",
27+ "attempt", attempt,
28+ "max_retries", maxRetries)
29+ time.Sleep(retryDelay)
30+ // Increase delay for next retry (exponential backoff)
31+ retryDelay *= 2
32+ continue
33+ }
34+
35+ // If it's not a locking error, return immediately
36+ return nil, err
37+ }
38+
39+ return nil, fmt.Errorf("max retries exceeded: %w", err)
40+}
41+
42+// dbQueryWithRetry performs a database query with retry logic for handling locked database
43+func (p *Proxy) dbQueryWithRetry(query string, args ...interface{}) (*sql.Rows, error) {
44+ var rows *sql.Rows
45+ var err error
46+ maxRetries := 5
47+ retryDelay := 100 * time.Millisecond
48+
49+ for attempt := 1; attempt <= maxRetries; attempt++ {
50+ rows, err = p.app.GetDB().Query(query, args...)
51+ if err == nil {
52+ return rows, nil
53+ }
54+
55+ // Check if this is a database locked error
56+ if err.Error() == "database is locked (5) (SQLITE_BUSY)" {
57+ log.Debug("Database locked, retrying operation",
58+ "attempt", attempt,
59+ "max_retries", maxRetries)
60+ time.Sleep(retryDelay)
61+ // Increase delay for next retry (exponential backoff)
62+ retryDelay *= 2
63+ continue
64+ }
65+
66+ // If it's not a locking error, return immediately
67+ return nil, err
68+ }
69+
70+ return nil, fmt.Errorf("max retries exceeded: %w", err)
71+}
72+
73+// dbBeginWithRetry starts a transaction with retry logic for handling locked database
74+func (p *Proxy) dbBeginWithRetry() (*sql.Tx, error) {
75+ var tx *sql.Tx
76+ var err error
77+ maxRetries := 5
78+ retryDelay := 100 * time.Millisecond
79+
80+ for attempt := 1; attempt <= maxRetries; attempt++ {
81+ tx, err = p.app.GetDB().Begin()
82+ if err == nil {
83+ return tx, nil
84+ }
85+
86+ // Check if this is a database locked error
87+ if err.Error() == "database is locked (5) (SQLITE_BUSY)" {
88+ log.Debug("Database locked, retrying transaction start",
89+ "attempt", attempt,
90+ "max_retries", maxRetries)
91+ time.Sleep(retryDelay)
92+ // Increase delay for next retry (exponential backoff)
93+ retryDelay *= 2
94+ continue
95+ }
96+
97+ // If it's not a locking error, return immediately
98+ return nil, err
99+ }
100+
101+ return nil, fmt.Errorf("max retries exceeded: %w", err)
102+}
103+
104+// txExecWithRetry executes a query within a transaction with retry logic
105+func txExecWithRetry(tx *sql.Tx, query string, args ...interface{}) (sql.Result, error) {
106+ var result sql.Result
107+ var err error
108+ maxRetries := 5
109+ retryDelay := 100 * time.Millisecond
110+
111+ for attempt := 1; attempt <= maxRetries; attempt++ {
112+ result, err = tx.Exec(query, args...)
113+ if err == nil {
114+ return result, nil
115+ }
116+
117+ // Check if this is a database locked error
118+ if err.Error() == "database is locked (5) (SQLITE_BUSY)" {
119+ log.Debug("Database locked, retrying transaction operation",
120+ "attempt", attempt,
121+ "max_retries", maxRetries)
122+ time.Sleep(retryDelay)
123+ // Increase delay for next retry (exponential backoff)
124+ retryDelay *= 2
125+ continue
126+ }
127+
128+ // If it's not a locking error, return immediately
129+ return nil, err
130+ }
131+
132+ return nil, fmt.Errorf("max retries exceeded: %w", err)
133+}
M internal/proxy/middleware.go +29 -29
@@ -6,7 +6,7 @@ import (
66 "strings"
77 "time"
88
9- "github.com/charmbracelet/log"
9+ "github.com/bnema/gordon/pkg/logger"
1010 "github.com/google/uuid"
1111 "github.com/labstack/echo/v4"
1212 "github.com/labstack/echo/v4/middleware"
@@ -33,7 +33,7 @@ func (p *Proxy) setupMiddleware() {
3333 // Since we are the reverse proxy, we need to make sure we identify client IPs correctly
3434 p.httpsServer.IPExtractor = echo.ExtractIPFromXFFHeader()
3535 p.httpServer.IPExtractor = echo.ExtractIPFromXFFHeader()
36-
36+
3737 // Add UUID generator middleware for both HTTP and HTTPS servers
3838 p.httpsServer.Use(p.createRequestIDMiddleware())
3939 p.httpServer.Use(p.createRequestIDMiddleware())
@@ -42,7 +42,7 @@ func (p *Proxy) setupMiddleware() {
4242 p.httpsServer.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
4343 return func(c echo.Context) error {
4444 req := c.Request()
45- log.Debug("Request headers",
45+ logger.Debug("Request headers",
4646 "method", req.Method,
4747 "path", req.URL.Path,
4848 "remote_addr", req.RemoteAddr,
@@ -63,7 +63,7 @@ func (p *Proxy) setupMiddleware() {
6363 3*time.Minute, // Expires in 3 minutes
6464 )
6565 if err != nil {
66- log.Error("Failed to create HTTPS rate limiter store, falling back to memory store", "error", err)
66+ logger.Error("Failed to create HTTPS rate limiter store, falling back to memory store", "error", err)
6767
6868 // Fallback to memory store if Starskey store creation fails
6969 p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
@@ -75,7 +75,7 @@ func (p *Proxy) setupMiddleware() {
7575 },
7676 ),
7777 DenyHandler: func(context echo.Context, identifier string, err error) error {
78- log.Warn("Rate limit exceeded",
78+ logger.Warn("Rate limit exceeded",
7979 "ip", identifier,
8080 "path", context.Request().URL.Path,
8181 "method", context.Request().Method)
@@ -84,14 +84,14 @@ func (p *Proxy) setupMiddleware() {
8484 }))
8585 } else {
8686 // Use the Starskey store for HTTPS rate limiting
87- log.Debug("Using Starskey rate limiter for HTTPS server")
87+ logger.Debug("Using Starskey rate limiter for HTTPS server")
8888
8989 // Add custom Starskey rate limiter middleware
9090 p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
9191 Store: httpsRateLimiter,
9292 IdentifierExtractor: func(ctx echo.Context) (string, error) {
9393 id := ctx.RealIP()
94- log.Debug("Rate limiting HTTPS request",
94+ logger.Debug("Rate limiting HTTPS request",
9595 "ip", id,
9696 "remote_addr", ctx.Request().RemoteAddr)
9797
@@ -101,7 +101,7 @@ func (p *Proxy) setupMiddleware() {
101101 return id, nil
102102 },
103103 DenyHandler: func(context echo.Context, identifier string, err error) error {
104- log.Warn("HTTPS rate limit exceeded",
104+ logger.Warn("HTTPS rate limit exceeded",
105105 "ip", identifier,
106106 "path", context.Request().URL.Path,
107107 "method", context.Request().Method)
@@ -112,7 +112,7 @@ func (p *Proxy) setupMiddleware() {
112112 // Add cleanup function to global slice
113113 rateLimiterCleanup = append(rateLimiterCleanup, func() {
114114 if err := httpsRateLimiter.Close(); err != nil {
115- log.Error("Failed to close HTTPS rate limiter store", "error", err)
115+ logger.Error("Failed to close HTTPS rate limiter store", "error", err)
116116 }
117117 })
118118 }
@@ -120,15 +120,15 @@ func (p *Proxy) setupMiddleware() {
120120 // Add custom logger middleware with request ID if enabled in config
121121 if p.config.EnableLogs {
122122 p.httpsServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
123- Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}","status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}","bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",
123+ Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}","status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}","bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",
124124 CustomTimeFormat: "2006-01-02T15:04:05.00000Z07:00",
125125 Skipper: func(c echo.Context) bool {
126126 return !p.config.EnableLogs
127127 },
128128 }))
129- log.Debug("HTTP request logging enabled for HTTPS server")
129+ logger.Debug("HTTP request logging enabled for HTTPS server")
130130 } else {
131- log.Debug("HTTP request logging disabled for HTTPS server")
131+ logger.Debug("HTTP request logging disabled for HTTPS server")
132132 }
133133
134134 // Create blacklist middleware for HTTP
@@ -137,24 +137,24 @@ func (p *Proxy) setupMiddleware() {
137137 // Create a new Starskey rate limiter store for HTTP server
138138 httpRateLimiter, err := kv.NewStarskeyRateLimiterStore(
139139 "./data/ratelimiter/http", // Path to store rate limit data
140- 2, // Rate: 2 requests per second (lowered from 10)
141- 10, // Burst: 10 requests (lowered from 30)
140+ 10, // Rate: 10 requests per second (lowered from 10)
141+ 30, // Burst: 30 requests (lowered from 30)
142142 3*time.Minute, // Expires in 3 minutes
143143 )
144144 if err != nil {
145- log.Error("Failed to create HTTP rate limiter store, falling back to memory store", "error", err)
145+ logger.Error("Failed to create HTTP rate limiter store, falling back to memory store", "error", err)
146146
147147 // Fallback to memory store if Starskey store creation fails
148148 p.httpServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
149149 Store: middleware.NewRateLimiterMemoryStoreWithConfig(
150150 middleware.RateLimiterMemoryStoreConfig{
151- Rate: rate.Limit(2), // 2 requests per second (lowered from 10)
152- Burst: 10, // Burst of 10 requests (lowered from 30)
151+ Rate: rate.Limit(10), // 10 requests per second (lowered from 10)
152+ Burst: 30, // Burst of 30 requests (lowered from 30)
153153 ExpiresIn: 3 * time.Minute, // Store expiration
154154 },
155155 ),
156156 DenyHandler: func(context echo.Context, identifier string, err error) error {
157- log.Warn("Rate limit exceeded",
157+ logger.Warn("Rate limit exceeded",
158158 "ip", identifier,
159159 "path", context.Request().URL.Path,
160160 "method", context.Request().Method)
@@ -163,14 +163,14 @@ func (p *Proxy) setupMiddleware() {
163163 }))
164164 } else {
165165 // Use the Starskey store for HTTP rate limiting
166- log.Debug("Using Starskey rate limiter for HTTP server")
166+ logger.Debug("Using Starskey rate limiter for HTTP server")
167167
168168 // Add custom Starskey rate limiter middleware
169169 p.httpServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
170170 Store: httpRateLimiter,
171171 IdentifierExtractor: func(ctx echo.Context) (string, error) {
172172 id := ctx.RealIP()
173- log.Debug("Rate limiting HTTP request",
173+ logger.Debug("Rate limiting HTTP request",
174174 "ip", id,
175175 "remote_addr", ctx.Request().RemoteAddr)
176176
@@ -180,7 +180,7 @@ func (p *Proxy) setupMiddleware() {
180180 return id, nil
181181 },
182182 DenyHandler: func(context echo.Context, identifier string, err error) error {
183- log.Warn("HTTP rate limit exceeded",
183+ logger.Warn("HTTP rate limit exceeded",
184184 "ip", identifier,
185185 "path", context.Request().URL.Path,
186186 "method", context.Request().Method)
@@ -191,7 +191,7 @@ func (p *Proxy) setupMiddleware() {
191191 // Add cleanup function to global slice
192192 rateLimiterCleanup = append(rateLimiterCleanup, func() {
193193 if err := httpRateLimiter.Close(); err != nil {
194- log.Error("Failed to close HTTP rate limiter store", "error", err)
194+ logger.Error("Failed to close HTTP rate limiter store", "error", err)
195195 }
196196 })
197197 }
@@ -199,15 +199,15 @@ func (p *Proxy) setupMiddleware() {
199199 // Add custom logger middleware with request ID for HTTP server if enabled in config
200200 if p.config.EnableLogs {
201201 p.httpServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
202- Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}","status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}","bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",
202+ Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}","status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}","bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",
203203 CustomTimeFormat: "2006-01-02T15:04:05.00000Z07:00",
204204 Skipper: func(c echo.Context) bool {
205205 return !p.config.EnableLogs
206206 },
207207 }))
208- log.Debug("HTTP request logging enabled for HTTP server")
208+ logger.Debug("HTTP request logging enabled for HTTP server")
209209 } else {
210- log.Debug("HTTP request logging disabled for HTTP server")
210+ logger.Debug("HTTP request logging disabled for HTTP server")
211211 }
212212 }
213213
@@ -217,13 +217,13 @@ func (p *Proxy) createRequestIDMiddleware() echo.MiddlewareFunc {
217217 return func(c echo.Context) error {
218218 // Generate a new UUID for this request
219219 requestID := uuid.New().String()
220-
220+
221221 // Store the UUID in the context
222222 c.Set(RequestIDKey, requestID)
223-
223+
224224 // Add the UUID as a response header
225225 c.Response().Header().Set(echo.HeaderXRequestID, requestID)
226-
226+
227227 // Continue with the next middleware
228228 return next(c)
229229 }
@@ -253,7 +253,7 @@ func (p *Proxy) createBlacklistMiddleware() echo.MiddlewareFunc {
253253 }
254254
255255 // Debug log for all requests to check if the IP is being properly recognized
256- log.Debug("Received request", "ip", clientIP, "path", c.Request().URL.Path)
256+ logger.Debug("Received request", "ip", clientIP, "path", c.Request().URL.Path)
257257
258258 // Check if IP is blacklisted (if blacklist exists)
259259 if p.blacklist != nil && p.blacklist.IsBlocked(clientIP) {
@@ -6,7 +6,7 @@ import (
6 "strings"6 "strings"
7 "time"7 "time"
8 8
9- "github.com/charmbracelet/log"9+ "github.com/bnema/gordon/pkg/logger"
10 "github.com/google/uuid"10 "github.com/google/uuid"
11 "github.com/labstack/echo/v4"11 "github.com/labstack/echo/v4"
12 "github.com/labstack/echo/v4/middleware"12 "github.com/labstack/echo/v4/middleware"
@@ -33,7 +33,7 @@ func (p *Proxy) setupMiddleware() {
33 // Since we are the reverse proxy, we need to make sure we identify client IPs correctly33 // Since we are the reverse proxy, we need to make sure we identify client IPs correctly
34 p.httpsServer.IPExtractor = echo.ExtractIPFromXFFHeader()34 p.httpsServer.IPExtractor = echo.ExtractIPFromXFFHeader()
35 p.httpServer.IPExtractor = echo.ExtractIPFromXFFHeader()35 p.httpServer.IPExtractor = echo.ExtractIPFromXFFHeader()
36- 36+
37 // Add UUID generator middleware for both HTTP and HTTPS servers37 // Add UUID generator middleware for both HTTP and HTTPS servers
38 p.httpsServer.Use(p.createRequestIDMiddleware())38 p.httpsServer.Use(p.createRequestIDMiddleware())
39 p.httpServer.Use(p.createRequestIDMiddleware())39 p.httpServer.Use(p.createRequestIDMiddleware())
@@ -42,7 +42,7 @@ func (p *Proxy) setupMiddleware() {
42 p.httpsServer.Use(func(next echo.HandlerFunc) echo.HandlerFunc {42 p.httpsServer.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
43 return func(c echo.Context) error {43 return func(c echo.Context) error {
44 req := c.Request()44 req := c.Request()
45- log.Debug("Request headers",45+ logger.Debug("Request headers",
46 "method", req.Method,46 "method", req.Method,
47 "path", req.URL.Path,47 "path", req.URL.Path,
48 "remote_addr", req.RemoteAddr,48 "remote_addr", req.RemoteAddr,
@@ -63,7 +63,7 @@ func (p *Proxy) setupMiddleware() {
63 3*time.Minute, // Expires in 3 minutes63 3*time.Minute, // Expires in 3 minutes
64 )64 )
65 if err != nil {65 if err != nil {
66- log.Error("Failed to create HTTPS rate limiter store, falling back to memory store", "error", err)66+ logger.Error("Failed to create HTTPS rate limiter store, falling back to memory store", "error", err)
67 67
68 // Fallback to memory store if Starskey store creation fails68 // Fallback to memory store if Starskey store creation fails
69 p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{69 p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
@@ -75,7 +75,7 @@ func (p *Proxy) setupMiddleware() {
75 },75 },
76 ),76 ),
77 DenyHandler: func(context echo.Context, identifier string, err error) error {77 DenyHandler: func(context echo.Context, identifier string, err error) error {
78- log.Warn("Rate limit exceeded",78+ logger.Warn("Rate limit exceeded",
79 "ip", identifier,79 "ip", identifier,
80 "path", context.Request().URL.Path,80 "path", context.Request().URL.Path,
81 "method", context.Request().Method)81 "method", context.Request().Method)
@@ -84,14 +84,14 @@ func (p *Proxy) setupMiddleware() {
84 }))84 }))
85 } else {85 } else {
86 // Use the Starskey store for HTTPS rate limiting86 // Use the Starskey store for HTTPS rate limiting
87- log.Debug("Using Starskey rate limiter for HTTPS server")87+ logger.Debug("Using Starskey rate limiter for HTTPS server")
88 88
89 // Add custom Starskey rate limiter middleware89 // Add custom Starskey rate limiter middleware
90 p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{90 p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
91 Store: httpsRateLimiter,91 Store: httpsRateLimiter,
92 IdentifierExtractor: func(ctx echo.Context) (string, error) {92 IdentifierExtractor: func(ctx echo.Context) (string, error) {
93 id := ctx.RealIP()93 id := ctx.RealIP()
94- log.Debug("Rate limiting HTTPS request",94+ logger.Debug("Rate limiting HTTPS request",
95 "ip", id,95 "ip", id,
96 "remote_addr", ctx.Request().RemoteAddr)96 "remote_addr", ctx.Request().RemoteAddr)
97 97
@@ -101,7 +101,7 @@ func (p *Proxy) setupMiddleware() {
101 return id, nil101 return id, nil
102 },102 },
103 DenyHandler: func(context echo.Context, identifier string, err error) error {103 DenyHandler: func(context echo.Context, identifier string, err error) error {
104- log.Warn("HTTPS rate limit exceeded",104+ logger.Warn("HTTPS rate limit exceeded",
105 "ip", identifier,105 "ip", identifier,
106 "path", context.Request().URL.Path,106 "path", context.Request().URL.Path,
107 "method", context.Request().Method)107 "method", context.Request().Method)
@@ -112,7 +112,7 @@ func (p *Proxy) setupMiddleware() {
112 // Add cleanup function to global slice112 // Add cleanup function to global slice
113 rateLimiterCleanup = append(rateLimiterCleanup, func() {113 rateLimiterCleanup = append(rateLimiterCleanup, func() {
114 if err := httpsRateLimiter.Close(); err != nil {114 if err := httpsRateLimiter.Close(); err != nil {
115- log.Error("Failed to close HTTPS rate limiter store", "error", err)115+ logger.Error("Failed to close HTTPS rate limiter store", "error", err)
116 }116 }
117 })117 })
118 }118 }
@@ -120,15 +120,15 @@ func (p *Proxy) setupMiddleware() {
120 // Add custom logger middleware with request ID if enabled in config120 // Add custom logger middleware with request ID if enabled in config
121 if p.config.EnableLogs {121 if p.config.EnableLogs {
122 p.httpsServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{122 p.httpsServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
123- Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}","status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}","bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",123+ Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}","status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}","bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",
124 CustomTimeFormat: "2006-01-02T15:04:05.00000Z07:00",124 CustomTimeFormat: "2006-01-02T15:04:05.00000Z07:00",
125 Skipper: func(c echo.Context) bool {125 Skipper: func(c echo.Context) bool {
126 return !p.config.EnableLogs126 return !p.config.EnableLogs
127 },127 },
128 }))128 }))
129- log.Debug("HTTP request logging enabled for HTTPS server")129+ logger.Debug("HTTP request logging enabled for HTTPS server")
130 } else {130 } else {
131- log.Debug("HTTP request logging disabled for HTTPS server")131+ logger.Debug("HTTP request logging disabled for HTTPS server")
132 }132 }
133 133
134 // Create blacklist middleware for HTTP134 // Create blacklist middleware for HTTP
@@ -137,24 +137,24 @@ func (p *Proxy) setupMiddleware() {
137 // Create a new Starskey rate limiter store for HTTP server137 // Create a new Starskey rate limiter store for HTTP server
138 httpRateLimiter, err := kv.NewStarskeyRateLimiterStore(138 httpRateLimiter, err := kv.NewStarskeyRateLimiterStore(
139 "./data/ratelimiter/http", // Path to store rate limit data139 "./data/ratelimiter/http", // Path to store rate limit data
140- 2, // Rate: 2 requests per second (lowered from 10)140+ 10, // Rate: 10 requests per second (lowered from 10)
141- 10, // Burst: 10 requests (lowered from 30)141+ 30, // Burst: 30 requests (lowered from 30)
142 3*time.Minute, // Expires in 3 minutes142 3*time.Minute, // Expires in 3 minutes
143 )143 )
144 if err != nil {144 if err != nil {
145- log.Error("Failed to create HTTP rate limiter store, falling back to memory store", "error", err)145+ logger.Error("Failed to create HTTP rate limiter store, falling back to memory store", "error", err)
146 146
147 // Fallback to memory store if Starskey store creation fails147 // Fallback to memory store if Starskey store creation fails
148 p.httpServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{148 p.httpServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
149 Store: middleware.NewRateLimiterMemoryStoreWithConfig(149 Store: middleware.NewRateLimiterMemoryStoreWithConfig(
150 middleware.RateLimiterMemoryStoreConfig{150 middleware.RateLimiterMemoryStoreConfig{
151- Rate: rate.Limit(2), // 2 requests per second (lowered from 10)151+ Rate: rate.Limit(10), // 10 requests per second (lowered from 10)
152- Burst: 10, // Burst of 10 requests (lowered from 30)152+ Burst: 30, // Burst of 30 requests (lowered from 30)
153 ExpiresIn: 3 * time.Minute, // Store expiration153 ExpiresIn: 3 * time.Minute, // Store expiration
154 },154 },
155 ),155 ),
156 DenyHandler: func(context echo.Context, identifier string, err error) error {156 DenyHandler: func(context echo.Context, identifier string, err error) error {
157- log.Warn("Rate limit exceeded",157+ logger.Warn("Rate limit exceeded",
158 "ip", identifier,158 "ip", identifier,
159 "path", context.Request().URL.Path,159 "path", context.Request().URL.Path,
160 "method", context.Request().Method)160 "method", context.Request().Method)
@@ -163,14 +163,14 @@ func (p *Proxy) setupMiddleware() {
163 }))163 }))
164 } else {164 } else {
165 // Use the Starskey store for HTTP rate limiting165 // Use the Starskey store for HTTP rate limiting
166- log.Debug("Using Starskey rate limiter for HTTP server")166+ logger.Debug("Using Starskey rate limiter for HTTP server")
167 167
168 // Add custom Starskey rate limiter middleware168 // Add custom Starskey rate limiter middleware
169 p.httpServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{169 p.httpServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
170 Store: httpRateLimiter,170 Store: httpRateLimiter,
171 IdentifierExtractor: func(ctx echo.Context) (string, error) {171 IdentifierExtractor: func(ctx echo.Context) (string, error) {
172 id := ctx.RealIP()172 id := ctx.RealIP()
173- log.Debug("Rate limiting HTTP request",173+ logger.Debug("Rate limiting HTTP request",
174 "ip", id,174 "ip", id,
175 "remote_addr", ctx.Request().RemoteAddr)175 "remote_addr", ctx.Request().RemoteAddr)
176 176
@@ -180,7 +180,7 @@ func (p *Proxy) setupMiddleware() {
180 return id, nil180 return id, nil
181 },181 },
182 DenyHandler: func(context echo.Context, identifier string, err error) error {182 DenyHandler: func(context echo.Context, identifier string, err error) error {
183- log.Warn("HTTP rate limit exceeded",183+ logger.Warn("HTTP rate limit exceeded",
184 "ip", identifier,184 "ip", identifier,
185 "path", context.Request().URL.Path,185 "path", context.Request().URL.Path,
186 "method", context.Request().Method)186 "method", context.Request().Method)
@@ -191,7 +191,7 @@ func (p *Proxy) setupMiddleware() {
191 // Add cleanup function to global slice191 // Add cleanup function to global slice
192 rateLimiterCleanup = append(rateLimiterCleanup, func() {192 rateLimiterCleanup = append(rateLimiterCleanup, func() {
193 if err := httpRateLimiter.Close(); err != nil {193 if err := httpRateLimiter.Close(); err != nil {
194- log.Error("Failed to close HTTP rate limiter store", "error", err)194+ logger.Error("Failed to close HTTP rate limiter store", "error", err)
195 }195 }
196 })196 })
197 }197 }
@@ -199,15 +199,15 @@ func (p *Proxy) setupMiddleware() {
199 // Add custom logger middleware with request ID for HTTP server if enabled in config199 // Add custom logger middleware with request ID for HTTP server if enabled in config
200 if p.config.EnableLogs {200 if p.config.EnableLogs {
201 p.httpServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{201 p.httpServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
202- Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}","status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}","bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",202+ Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}","status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}","bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",
203 CustomTimeFormat: "2006-01-02T15:04:05.00000Z07:00",203 CustomTimeFormat: "2006-01-02T15:04:05.00000Z07:00",
204 Skipper: func(c echo.Context) bool {204 Skipper: func(c echo.Context) bool {
205 return !p.config.EnableLogs205 return !p.config.EnableLogs
206 },206 },
207 }))207 }))
208- log.Debug("HTTP request logging enabled for HTTP server")208+ logger.Debug("HTTP request logging enabled for HTTP server")
209 } else {209 } else {
210- log.Debug("HTTP request logging disabled for HTTP server")210+ logger.Debug("HTTP request logging disabled for HTTP server")
211 }211 }
212 }212 }
213 213
@@ -217,13 +217,13 @@ func (p *Proxy) createRequestIDMiddleware() echo.MiddlewareFunc {
217 return func(c echo.Context) error {217 return func(c echo.Context) error {
218 // Generate a new UUID for this request218 // Generate a new UUID for this request
219 requestID := uuid.New().String()219 requestID := uuid.New().String()
220- 220+
221 // Store the UUID in the context221 // Store the UUID in the context
222 c.Set(RequestIDKey, requestID)222 c.Set(RequestIDKey, requestID)
223- 223+
224 // Add the UUID as a response header224 // Add the UUID as a response header
225 c.Response().Header().Set(echo.HeaderXRequestID, requestID)225 c.Response().Header().Set(echo.HeaderXRequestID, requestID)
226- 226+
227 // Continue with the next middleware227 // Continue with the next middleware
228 return next(c)228 return next(c)
229 }229 }
@@ -253,7 +253,7 @@ func (p *Proxy) createBlacklistMiddleware() echo.MiddlewareFunc {
253 }253 }
254 254
255 // Debug log for all requests to check if the IP is being properly recognized255 // Debug log for all requests to check if the IP is being properly recognized
256- log.Debug("Received request", "ip", clientIP, "path", c.Request().URL.Path)256+ logger.Debug("Received request", "ip", clientIP, "path", c.Request().URL.Path)
257 257
258 // Check if IP is blacklisted (if blacklist exists)258 // Check if IP is blacklisted (if blacklist exists)
259 if p.blacklist != nil && p.blacklist.IsBlocked(clientIP) {259 if p.blacklist != nil && p.blacklist.IsBlocked(clientIP) {
M internal/proxy/routes.go +381 -711
@@ -12,7 +12,7 @@ import (
1212 "time"
1313
1414 "github.com/bnema/gordon/pkg/docker"
15- "github.com/charmbracelet/log"
15+ "github.com/bnema/gordon/pkg/logger"
1616 "github.com/labstack/echo/v4"
1717 )
1818
@@ -33,12 +33,12 @@ func (p *Proxy) loadRoutes() error {
3333 p.mu.Lock()
3434 defer p.mu.Unlock()
3535
36- log.Debug("Loading proxy routes from database")
36+ logger.Debug("Loading proxy routes from database")
3737
3838 // Now check if we have a Gordon container ID stored for identity purposes
39- p.gordonContainerID = p.loadGordonContainerID()
39+ p.gordonContainerID = p.detectGordonContainer()
4040 if p.gordonContainerID != "" {
41- log.Info("Loaded Gordon container ID from config file during route initialization", "container_id", p.gordonContainerID)
41+ logger.Info("Loaded Gordon container ID during route initialization", "container_id", p.gordonContainerID)
4242 }
4343
4444 // Save the admin domain route if it exists
@@ -46,11 +46,11 @@ func (p *Proxy) loadRoutes() error {
4646 var adminRoute *ProxyRouteInfo
4747 if route, exists := p.routes[adminDomain]; exists {
4848 adminRoute = route
49- log.Debug("Preserving admin domain route", "domain", adminDomain)
49+ logger.Debug("Preserving admin domain route", "domain", adminDomain)
5050 }
5151
52- // Query the database for active proxy routes
53- rows, err := p.app.GetDB().Query(`
52+ // Query the database for active proxy routes using retry mechanism
53+ rows, err := p.dbQueryWithRetry(`
5454 SELECT pr.id, d.name, pr.container_id, pr.container_ip, pr.container_port, pr.protocol, pr.path, pr.active
5555 FROM proxy_route pr
5656 JOIN domain d ON pr.domain_id = d.id
@@ -58,13 +58,13 @@ func (p *Proxy) loadRoutes() error {
5858 `)
5959 if err != nil {
6060 if err == sql.ErrNoRows {
61- log.Info("No active proxy routes found")
61+ logger.Info("No active proxy routes found")
6262
6363 // Restore admin route if it was saved
6464 if adminRoute != nil {
6565 p.routes = make(map[string]*ProxyRouteInfo)
6666 p.routes[adminDomain] = adminRoute
67- log.Debug("Restored admin domain route after empty database query", "domain", adminDomain)
67+ logger.Debug("Restored admin domain route after empty database query", "domain", adminDomain)
6868 }
6969
7070 return nil
@@ -79,7 +79,7 @@ func (p *Proxy) loadRoutes() error {
7979 // Restore admin route if it was saved
8080 if adminRoute != nil {
8181 p.routes[adminDomain] = adminRoute
82- log.Debug("Restored admin domain route after clearing routes map", "domain", adminDomain)
82+ logger.Debug("Restored admin domain route after clearing routes map", "domain", adminDomain)
8383 }
8484
8585 // Populate the routes map
@@ -90,6 +90,9 @@ func (p *Proxy) loadRoutes() error {
9090 return fmt.Errorf("failed to scan row: %w", err)
9191 }
9292
93+ // Normalize domain (lowercase, no trailing dot)
94+ domain = strings.TrimSuffix(strings.ToLower(domain), ".")
95+
9396 // Add the route to the map
9497 p.routes[domain] = &ProxyRouteInfo{
9598 Domain: domain,
@@ -101,14 +104,14 @@ func (p *Proxy) loadRoutes() error {
101104 Active: active,
102105 }
103106
104- log.Debug("Loaded proxy route",
107+ logger.Debug("Loaded proxy route",
105108 "domain", domain,
106109 "containerIP", containerIP,
107110 "containerPort", containerPort,
108111 )
109112 }
110113
111- log.Info("Loaded proxy routes", "count", len(p.routes))
114+ logger.Info("Loaded proxy routes", "count", len(p.routes))
112115 return nil
113116 }
114117
@@ -116,7 +119,7 @@ func (p *Proxy) loadRoutes() error {
116119 func (p *Proxy) configureRoutes() {
117120 // Get the admin domain from the config
118121 adminDomain := p.app.GetConfig().Http.FullDomain()
119- log.Debug("Configuring proxy routes", "admin_domain", adminDomain)
122+ logger.Debug("Configuring proxy routes", "admin_domain", adminDomain)
120123
121124 // Verify admin domain is in routes - if not, we need to add it
122125 p.mu.RLock()
@@ -124,7 +127,7 @@ func (p *Proxy) configureRoutes() {
124127 p.mu.RUnlock()
125128
126129 if !adminRouteExists {
127- log.Warn("Admin domain route is missing, attempting to recreate it")
130+ logger.Warn("Admin domain route is missing, attempting to recreate it")
128131 p.mu.Lock()
129132 // Auto-detect the Gordon container name
130133 containerName := p.detectGordonContainer()
@@ -151,18 +154,60 @@ func (p *Proxy) configureRoutes() {
151154 Path: "/",
152155 Active: true,
153156 }
154- log.Info("Recreated admin domain route",
157+ logger.Info("Recreated admin domain route",
155158 "domain", adminDomain,
156159 "target", fmt.Sprintf("http://%s:%s", containerIP, p.app.GetConfig().Http.Port))
157160 p.mu.Unlock()
158161 } else {
159- log.Debug("Admin domain route exists", "domain", adminDomain)
162+ logger.Debug("Admin domain route exists", "domain", adminDomain)
160163 }
161164
162165 // Handler function - will be assigned to both HTTP and HTTPS servers
163166 handler := echo.HandlerFunc(func(c echo.Context) error {
164167 host := c.Request().Host
165168
169+ // Strip port if present in the host
170+ if hostParts := strings.Split(host, ":"); len(hostParts) > 1 {
171+ host = hostParts[0]
172+ }
173+
174+ // Normalize host (trim trailing dot, convert to lowercase)
175+ host = strings.TrimSuffix(strings.ToLower(host), ".")
176+
177+ // Special handling for debug: log all request details
178+ logger.Debug("Request headers for proxy",
179+ "domain", host,
180+ "X-Forwarded-For", c.Request().Header.Get("X-Forwarded-For"),
181+ "X-Real-IP", c.Request().Header.Get("X-Real-IP"))
182+
183+ // Check if this is the admin domain
184+ adminDomain := p.app.GetConfig().Http.FullDomain()
185+ if strings.EqualFold(host, adminDomain) {
186+ // Always prioritize admin domain
187+ p.mu.RLock()
188+ adminRoute, adminOk := p.routes[adminDomain]
189+ p.mu.RUnlock()
190+
191+ if adminOk {
192+ logger.Debug("Proxying request to admin domain",
193+ "domain", host,
194+ "target", fmt.Sprintf("%s://%s:%s", adminRoute.Protocol, adminRoute.ContainerIP, adminRoute.ContainerPort),
195+ "path", c.Request().URL.Path)
196+
197+ // Special logging for admin domain handling
198+ logger.Debug("Proxying request",
199+ "host", host,
200+ "target", fmt.Sprintf("%s://%s:%s", adminRoute.Protocol, adminRoute.ContainerIP, adminRoute.ContainerPort),
201+ "path", c.Request().URL.Path,
202+ "clientIP", c.RealIP(),
203+ "remoteAddr", c.Request().RemoteAddr,
204+ "originalProtocol", c.Scheme())
205+
206+ // Use the admin route
207+ return p.proxyRequest(c, adminRoute)
208+ }
209+ }
210+
166211 // Important: Get the route freshly from the map for every request
167212 // This ensures we always use the most up-to-date IP
168213 p.mu.RLock()
@@ -185,7 +230,7 @@ func (p *Proxy) configureRoutes() {
185230 p.mu.RUnlock()
186231
187232 // Log warning for non-IP hosts that aren't configured
188- log.Warn("Request with unknown host",
233+ logger.Warn("Request with unknown host",
189234 "requested_host", host,
190235 "client_ip", c.RealIP(),
191236 "available_domains", strings.Join(availableDomains, ", "))
@@ -194,164 +239,22 @@ func (p *Proxy) configureRoutes() {
194239
195240 // Check if the route is active
196241 if !route.Active {
197- log.Warn("Route is not active",
242+ logger.Warn("Route is not active",
198243 "domain", host,
199244 "client_ip", c.RealIP())
200245 return c.String(http.StatusServiceUnavailable, "Route is not active")
201246 }
202247
203- // Special handling for the admin domain
204- if host == adminDomain {
205- log.Debug("Proxying request to admin domain",
206- "domain", host,
207- "target", fmt.Sprintf("%s://%s:%s", route.Protocol, route.ContainerIP, route.ContainerPort),
208- "path", c.Request().URL.Path)
209- }
210-
211- // For container routes, verify the IP is reachable before proxying
212- if host != adminDomain {
213- // Test TCP connection to container before proxying
214- dialer := net.Dialer{Timeout: 500 * time.Millisecond}
215- // Format address properly for both IPv4 and IPv6
216- target := route.ContainerIP
217- if strings.Contains(target, ":") {
218- // IPv6 address needs to be wrapped in brackets
219- target = "[" + target + "]"
220- }
221- conn, err := dialer.Dial("tcp", net.JoinHostPort(target, route.ContainerPort))
222- if err != nil {
223- // Connection failed, check if container exists but has a new IP
224- containerID := route.ContainerID
225- if containerID != "" {
226- // Try to get container info and check if it has a different IP
227- containerInfo, err := docker.GetContainerInfo(containerID)
228- if err == nil && containerInfo.NetworkSettings != nil {
229- // Check if container has a different IP
230- networkName := p.app.GetConfig().ContainerEngine.Network
231- if networkSettings, exists := containerInfo.NetworkSettings.Networks[networkName]; exists &&
232- networkSettings.IPAddress != "" && networkSettings.IPAddress != route.ContainerIP {
233- // Found container with different IP, update the route and use new IP for this request
234- log.Warn("Container IP mismatch detected, using live IP instead of cached value",
235- "domain", host,
236- "cached_ip", route.ContainerIP,
237- "live_ip", networkSettings.IPAddress)
238-
239- // Update the in-memory route for this request
240- route.ContainerIP = networkSettings.IPAddress
241-
242- // Update the database in background to persist the change
243- go func(domain, containerID, containerIP, containerPort string) {
244- // Get the domain ID
245- var domainID string
246- err := p.app.GetDB().QueryRow("SELECT id FROM domain WHERE name = ?", domain).Scan(&domainID)
247- if err != nil {
248- log.Error("Failed to find domain ID for IP update", "domain", domain, "error", err)
249- return
250- }
251-
252- // Update the proxy_route record
253- now := time.Now().Format(time.RFC3339)
254- _, err = p.app.GetDB().Exec(`
255- UPDATE proxy_route SET container_ip = ?, updated_at = ?
256- WHERE domain_id = ? AND container_id = ?`,
257- containerIP, now, domainID, containerID)
258-
259- if err != nil {
260- log.Error("Failed to update container IP in database",
261- "domain", domain,
262- "containerID", containerID,
263- "error", err)
264- } else {
265- log.Info("Updated container IP in database after live detection",
266- "domain", domain,
267- "containerID", containerID,
268- "old_ip", route.ContainerIP,
269- "new_ip", containerIP)
270- }
271- }(host, containerID, networkSettings.IPAddress, route.ContainerPort)
272- } else {
273- log.Debug("Container exists but connection still failed",
274- "domain", host,
275- "container_id", containerID,
276- "container_ip", route.ContainerIP,
277- "container_port", route.ContainerPort,
278- "error", err)
279- }
280- }
281- }
282- } else {
283- // Connection successful, close it
284- conn.Close()
285- }
286- }
287-
288- // Create the target URL
289- targetURL := &url.URL{
290- Scheme: "http", // Always use HTTP for internal connections
291- }
248+ // Regular debug logging for all domains
249+ logger.Debug("Proxying request",
250+ "host", host,
251+ "target", fmt.Sprintf("%s://%s:%s", route.Protocol, route.ContainerIP, route.ContainerPort),
252+ "path", c.Request().URL.Path,
253+ "clientIP", c.RealIP(),
254+ "remoteAddr", c.Request().RemoteAddr,
255+ "originalProtocol", c.Scheme())
292256
293- // Log protocol conversion if needed
294- if route.Protocol == "https" {
295- log.Debug("Converting HTTPS external route to HTTP for internal connection",
296- "domain", host,
297- "containerID", route.ContainerID,
298- "externalProtocol", "https",
299- "internalProtocol", "http")
300- }
301-
302- // Format host properly for both IPv4 and IPv6
303- containerIP := route.ContainerIP
304- if strings.Contains(containerIP, ":") {
305- // IPv6 address needs to be wrapped in brackets
306- containerIP = "[" + containerIP + "]"
307- }
308- targetURL.Host = fmt.Sprintf("%s:%s", containerIP, route.ContainerPort)
309-
310- // Create a reverse proxy
311- proxy := httputil.NewSingleHostReverseProxy(targetURL)
312-
313- // Update headers to allow for SSL redirection
314- originalDirector := proxy.Director
315- proxy.Director = func(req *http.Request) {
316- originalDirector(req)
317- // Use the original protocol for forwarded proto header
318- forwardedProto := "http"
319- if route.Protocol == "https" {
320- forwardedProto = "https"
321- }
322- req.Header.Set("X-Forwarded-Proto", forwardedProto)
323- req.Header.Set("X-Forwarded-Host", host)
324- req.Header.Set("X-Forwarded-For", c.RealIP())
325- req.Header.Set("X-Real-IP", c.RealIP())
326-
327- // Debug information
328- log.Debug("Proxying request",
329- "host", host,
330- "target", targetURL.String(),
331- "path", req.URL.Path,
332- "clientIP", c.RealIP(),
333- "remoteAddr", req.RemoteAddr,
334- "originalProtocol", route.Protocol)
335-
336- // Log all headers for troubleshooting
337- log.Debug("Request headers for proxy",
338- "domain", host,
339- "X-Forwarded-For", req.Header.Get("X-Forwarded-For"),
340- "X-Real-IP", req.Header.Get("X-Real-IP"))
341- }
342-
343- // Add error handling
344- proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
345- log.Error("Proxy error",
346- "host", host,
347- "path", r.URL.Path,
348- "error", err)
349- c.String(http.StatusBadGateway, "Proxy Error: "+err.Error())
350- }
351-
352- // Serve the request
353- proxy.ServeHTTP(c.Response(), c.Request())
354- return nil
257+ return p.proxyRequest(c, route)
355258 })
356259
357260 // Add the handler to the HTTPS server
@@ -370,8 +273,19 @@ func (p *Proxy) configureRoutes() {
370273 // Note: Regardless of the external protocol, all internal connections to containers use HTTP.
371274 // The HTTPS protocol is only used for external connections and certificates.
372275 func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, protocol, path string) error {
373- // Begin a transaction
374- tx, err := p.app.GetDB().Begin()
276+ // Normalize domain name (lowercase, no trailing dot)
277+ domainName = strings.TrimSuffix(strings.ToLower(domainName), ".")
278+
279+ logger.Debug("Adding proxy route",
280+ "domain", domainName,
281+ "container_id", containerID,
282+ "container_ip", containerIP,
283+ "container_port", containerPort,
284+ "protocol", protocol,
285+ "path", path)
286+
287+ // Begin a transaction with retry
288+ tx, err := p.dbBeginWithRetry()
375289 if err != nil {
376290 return fmt.Errorf("failed to begin transaction: %w", err)
377291 }
@@ -391,9 +305,9 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr
391305 err = tx.QueryRow("SELECT id FROM account LIMIT 1").Scan(&accountID)
392306 if err != nil {
393307 if err == sql.ErrNoRows {
394- log.Warn("No account found when adding route, domains will have NULL account_id")
308+ logger.Warn("No account found when adding route, domains will have NULL account_id")
395309 } else {
396- log.Warn("Error fetching account for route, continuing anyway", "error", err)
310+ logger.Warn("Error fetching account for route, continuing anyway", "error", err)
397311 }
398312 // Continue with NULL account_id if no account found
399313 }
@@ -406,7 +320,7 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr
406320 // Domain doesn't exist, create it
407321 domainID = proxyGenerateUUID()
408322 now := time.Now().Format(time.RFC3339)
409- _, err = tx.Exec(
323+ _, err = txExecWithRetry(tx,
410324 "INSERT INTO domain (id, name, account_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
411325 domainID, hostname, accountID, now, now,
412326 )
@@ -414,7 +328,7 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr
414328 return fmt.Errorf("failed to insert domain: %w", err)
415329 }
416330
417- log.Debug("Added domain to database for Let's Encrypt",
331+ logger.Debug("Added domain to database for Let's Encrypt",
418332 "domain", hostname)
419333
420334 // Also add the subdomain.domain format to the database if this is a domain with components
@@ -432,16 +346,16 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr
432346 if err == sql.ErrNoRows {
433347 // Domain part doesn't exist, create it
434348 domainPartID = proxyGenerateUUID()
435- _, err = tx.Exec(
349+ _, err = txExecWithRetry(tx,
436350 "INSERT INTO domain (id, name, account_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
437351 domainPartID, domainPart, accountID, now, now,
438352 )
439353 if err != nil {
440- log.Warn("Failed to insert domain part, continuing anyway",
354+ logger.Warn("Failed to insert domain part, continuing anyway",
441355 "domain_part", domainPart,
442356 "error", err)
443357 } else {
444- log.Debug("Added domain part to database for Let's Encrypt",
358+ logger.Debug("Added domain part to database for Let's Encrypt",
445359 "domain_part", domainPart,
446360 "subdomain", subdomain)
447361 }
@@ -468,20 +382,20 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr
468382
469383 // Log more detailed information about the update
470384 if existingContainerID != containerID {
471- log.Info("Updating proxy route for recreated container",
385+ logger.Info("Updating proxy route for recreated container",
472386 "domain", hostname,
473387 "old_container_id", existingContainerID,
474388 "new_container_id", containerID)
475389 }
476390
477391 if existingContainerPort != containerPort {
478- log.Info("Container port changed for domain",
392+ logger.Info("Container port changed for domain",
479393 "domain", hostname,
480394 "old_port", existingContainerPort,
481395 "new_port", containerPort)
482396 }
483397
484- _, err = tx.Exec(
398+ _, err = txExecWithRetry(tx,
485399 `UPDATE proxy_route SET
486400 container_id = ?,
487401 container_ip = ?,
@@ -500,7 +414,7 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr
500414 // Route doesn't exist, create it
501415 routeID := proxyGenerateUUID()
502416 now := time.Now().Format(time.RFC3339)
503- _, err = tx.Exec(
417+ _, err = txExecWithRetry(tx,
504418 `INSERT INTO proxy_route (
505419 id, domain_id, container_id, container_ip, container_port,
506420 protocol, path, active, created_at, updated_at
@@ -525,7 +439,7 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr
525439 return fmt.Errorf("failed to reload routes: %w", err)
526440 }
527441
528- log.Info("Added proxy route",
442+ logger.Info("Added proxy route",
529443 "domain", hostname,
530444 "containerIP", containerIP,
531445 "containerPort", containerPort,
@@ -533,7 +447,7 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr
533447
534448 // Request a certificate if this is an HTTPS route
535449 if strings.ToLower(protocol) == "https" {
536- log.Info("Requesting Let's Encrypt certificate for new HTTPS route",
450+ logger.Info("Requesting Let's Encrypt certificate for new HTTPS route",
537451 "domain", hostname)
538452 // Run in a goroutine to avoid blocking
539453 go func(domain string) {
@@ -554,13 +468,13 @@ func (p *Proxy) RemoveRoute(domainName string) error {
554468 // Prevent removing the admin domain
555469 adminDomain := p.app.GetConfig().Http.FullDomain()
556470 if domainName == adminDomain {
557- log.Warn("Attempt to remove admin domain route prevented",
471+ logger.Warn("Attempt to remove admin domain route prevented",
558472 "domain", domainName)
559473 return fmt.Errorf("cannot remove admin domain route: %s", domainName)
560474 }
561475
562- // Begin a transaction
563- tx, err := p.app.GetDB().Begin()
476+ // Begin a transaction with retry
477+ tx, err := p.dbBeginWithRetry()
564478 if err != nil {
565479 return fmt.Errorf("failed to begin transaction: %w", err)
566480 }
@@ -576,14 +490,14 @@ func (p *Proxy) RemoveRoute(domainName string) error {
576490 return fmt.Errorf("failed to query domain: %w", err)
577491 }
578492
579- // Delete the route
580- _, err = tx.Exec("DELETE FROM proxy_route WHERE domain_id = ?", domainID)
493+ // Delete the route with retry
494+ _, err = txExecWithRetry(tx, "DELETE FROM proxy_route WHERE domain_id = ?", domainID)
581495 if err != nil {
582496 return fmt.Errorf("failed to delete route: %w", err)
583497 }
584498
585- // Delete the domain
586- _, err = tx.Exec("DELETE FROM domain WHERE id = ?", domainID)
499+ // Delete the domain with retry
500+ _, err = txExecWithRetry(tx, "DELETE FROM domain WHERE id = ?", domainID)
587501 if err != nil {
588502 return fmt.Errorf("failed to delete domain: %w", err)
589503 }
@@ -598,7 +512,7 @@ func (p *Proxy) RemoveRoute(domainName string) error {
598512 return fmt.Errorf("failed to reload routes: %w", err)
599513 }
600514
601- log.Info("Removed proxy route", "domain", domainName)
515+ logger.Info("Removed proxy route", "domain", domainName)
602516 return nil
603517 }
604518
@@ -618,10 +532,12 @@ func (p *Proxy) GetRoutes() map[string]*ProxyRouteInfo {
618532
619533 // Reload reloads the routes from the database and reconfigures the proxy
620534 func (p *Proxy) Reload() error {
621- log.Info("Reloading proxy routes from database")
535+ logger.Info("Reloading proxy routes from database")
622536
623537 // Get admin domain - this should be the full subdomain.domain format
624538 adminDomain := p.app.GetConfig().Http.FullDomain()
539+ // Normalize the admin domain (lowercase, no trailing dot)
540+ adminDomain = strings.TrimSuffix(strings.ToLower(adminDomain), ".")
625541
626542 // Save existing admin route if it exists
627543 p.mu.RLock()
@@ -636,7 +552,7 @@ func (p *Proxy) Reload() error {
636552 Path: route.Path,
637553 Active: route.Active,
638554 }
639- log.Debug("Preserved admin domain route during reload", "domain", adminDomain)
555+ logger.Debug("Preserved admin domain route during reload", "domain", adminDomain)
640556 }
641557 p.mu.RUnlock()
642558
@@ -653,7 +569,7 @@ func (p *Proxy) Reload() error {
653569 `)
654570 if err != nil {
655571 if err == sql.ErrNoRows {
656- log.Info("No active proxy routes found during reload")
572+ logger.Info("No active proxy routes found during reload")
657573 // Even if no routes found, proceed with the swap to clear any old routes
658574 } else {
659575 return fmt.Errorf("failed to query database during reload: %w", err)
@@ -669,6 +585,9 @@ func (p *Proxy) Reload() error {
669585 return fmt.Errorf("failed to scan row during reload: %w", err)
670586 }
671587
588+ // Normalize domain (lowercase, no trailing dot)
589+ domain = strings.TrimSuffix(strings.ToLower(domain), ".")
590+
672591 // Add the route to the new map
673592 newRoutes[domain] = &ProxyRouteInfo{
674593 Domain: domain,
@@ -680,7 +599,7 @@ func (p *Proxy) Reload() error {
680599 Active: active,
681600 }
682601
683- log.Debug("Loaded route during reload",
602+ logger.Debug("Loaded route during reload",
684603 "domain", domain,
685604 "containerID", containerID,
686605 "containerIP", containerIP,
@@ -694,7 +613,7 @@ func (p *Proxy) Reload() error {
694613 // If admin domain wasn't loaded from DB but we had it in memory, restore it
695614 if !adminLoaded && adminRoute != nil {
696615 newRoutes[adminDomain] = adminRoute
697- log.Warn("Admin domain route missing from database, restoring from memory",
616+ logger.Warn("Admin domain route missing from database, restoring from memory",
698617 "domain", adminDomain)
699618
700619 // Also try to add it back to the database
@@ -708,17 +627,17 @@ func (p *Proxy) Reload() error {
708627 )
709628
710629 if err != nil {
711- log.Error("Failed to save admin route to database",
630+ logger.Error("Failed to save admin route to database",
712631 "error", err,
713632 "domain", adminDomain)
714633 } else {
715- log.Info("Re-added admin domain route to database", "domain", adminDomain)
634+ logger.Info("Re-added admin domain route to database", "domain", adminDomain)
716635 }
717636 }
718637
719638 // If admin domain is still missing, recreate it
720639 if _, exists := newRoutes[adminDomain]; !exists {
721- log.Warn("Admin domain route is missing, recreating it", "domain", adminDomain)
640+ logger.Warn("Admin domain route is missing, recreating it", "domain", adminDomain)
722641
723642 // Auto-detect the Gordon container information
724643 containerName := p.detectGordonContainer()
@@ -753,11 +672,11 @@ func (p *Proxy) Reload() error {
753672 )
754673
755674 if err != nil {
756- log.Error("Failed to save recreated admin route to database",
675+ logger.Error("Failed to save recreated admin route to database",
757676 "error", err,
758677 "domain", adminDomain)
759678 } else {
760- log.Info("Recreated and saved admin domain route", "domain", adminDomain)
679+ logger.Info("Recreated and saved admin domain route", "domain", adminDomain)
761680 }
762681 }
763682
@@ -767,20 +686,17 @@ func (p *Proxy) Reload() error {
767686 p.routes = newRoutes
768687 p.mu.Unlock()
769688
770- // Clean up any root domain conflict
771- p.CleanupRootDomainForAdmin()
772-
773689 // Rebuild route configuration
774690 p.configureRoutes()
775691
776692 // Force recreation of the route handlers if servers are initialized
777693 if p.httpServer != nil {
778- log.Debug("Refreshing HTTP routes")
694+ logger.Debug("Refreshing HTTP routes")
779695 p.httpServer.Routes()
780696 }
781697
782698 if p.httpsServer != nil {
783- log.Debug("Refreshing HTTPS routes")
699+ logger.Debug("Refreshing HTTPS routes")
784700 p.httpsServer.Routes()
785701 }
786702
@@ -807,7 +723,7 @@ func (p *Proxy) ForceUpdateRouteIP(domain string, newIP string) error {
807723 if r, ok := p.routes[domain]; ok {
808724 oldIP := r.ContainerIP
809725 r.ContainerIP = newIP
810- log.Info("Force updated route IP in-memory",
726+ logger.Info("Force updated route IP in-memory",
811727 "domain", domain,
812728 "old_ip", oldIP,
813729 "new_ip", newIP)
@@ -815,16 +731,27 @@ func (p *Proxy) ForceUpdateRouteIP(domain string, newIP string) error {
815731 p.mu.Unlock()
816732
817733 // Next, update the database
818- // Get the domain ID
734+ // Get the domain ID with retry
819735 var domainID string
820- err := p.app.GetDB().QueryRow("SELECT id FROM domain WHERE name = ?", domain).Scan(&domainID)
736+ rows, err := p.dbQueryWithRetry("SELECT id FROM domain WHERE name = ?", domain)
821737 if err != nil {
822- return fmt.Errorf("failed to find domain ID: %w", err)
738+ return fmt.Errorf("failed to query domain: %w", err)
823739 }
824740
825- // Update the proxy_route record
741+ if !rows.Next() {
742+ rows.Close()
743+ return fmt.Errorf("domain not found: %s", domain)
744+ }
745+
746+ if err := rows.Scan(&domainID); err != nil {
747+ rows.Close()
748+ return fmt.Errorf("failed to scan domain ID: %w", err)
749+ }
750+ rows.Close()
751+
752+ // Update the proxy_route record with retry
826753 now := time.Now().Format(time.RFC3339)
827- result, err := p.app.GetDB().Exec(`
754+ result, err := p.dbExecWithRetry(`
828755 UPDATE proxy_route SET container_ip = ?, updated_at = ?
829756 WHERE domain_id = ? AND container_id = ?`,
830757 newIP, now, domainID, containerID)
@@ -838,7 +765,7 @@ func (p *Proxy) ForceUpdateRouteIP(domain string, newIP string) error {
838765 return fmt.Errorf("no rows affected when updating route in database")
839766 }
840767
841- log.Info("Force updated route IP in database",
768+ logger.Info("Force updated route IP in database",
842769 "domain", domain,
843770 "container_id", containerID,
844771 "new_ip", newIP)
@@ -851,7 +778,7 @@ func (p *Proxy) FindRoutesByContainerName(containerName string) []string {
851778 // Get all containers to map IDs to names
852779 containers, err := docker.ListRunningContainers()
853780 if err != nil {
854- log.Error("Failed to list containers when finding routes by container name", "error", err)
781+ logger.Error("Failed to list containers when finding routes by container name", "error", err)
855782 return nil
856783 }
857784
@@ -868,7 +795,7 @@ func (p *Proxy) FindRoutesByContainerName(containerName string) []string {
868795 // This is the exact container ID we're looking for
869796 targetContainerID := containerNameToID[containerName]
870797 if targetContainerID == "" {
871- log.Debug("Container name not found in running containers", "container_name", containerName)
798+ logger.Debug("Container name not found in running containers", "container_name", containerName)
872799 return nil
873800 }
874801
@@ -894,15 +821,15 @@ func (p *Proxy) FindRoutesByOldName(containerName string) map[string]*ProxyRoute
894821
895822 result := make(map[string]*ProxyRouteInfo)
896823
897- // Query the database to find if any route might be associated with this container name
898- rows, err := p.app.GetDB().Query(`
824+ // Query the database with retry to find if any route might be associated with this container name
825+ rows, err := p.dbQueryWithRetry(`
899826 SELECT d.name, pr.container_id, pr.container_ip, pr.container_port, pr.protocol, pr.path
900827 FROM proxy_route pr
901828 JOIN domain d ON pr.domain_id = d.id
902829 WHERE pr.active = 1
903830 `)
904831 if err != nil {
905- log.Error("Failed to query database for routes by old name", "error", err)
832+ logger.Error("Failed to query database for routes by old name", "error", err)
906833 return result
907834 }
908835 defer rows.Close()
@@ -911,7 +838,7 @@ func (p *Proxy) FindRoutesByOldName(containerName string) map[string]*ProxyRoute
911838 for rows.Next() {
912839 var domain, containerID, containerIP, containerPort, protocol, path string
913840 if err := rows.Scan(&domain, &containerID, &containerIP, &containerPort, &protocol, &path); err != nil {
914- log.Error("Failed to scan row", "error", err)
841+ logger.Error("Failed to scan row", "error", err)
915842 continue
916843 }
917844
@@ -940,8 +867,8 @@ func (p *Proxy) FindRoutesByOldName(containerName string) map[string]*ProxyRoute
940867 }
941868 } else {
942869 // We found an existing container - if it has the same name, this is interesting
943- containerName = strings.TrimPrefix(name, "/")
944- if containerName == containerName {
870+ existingContainerName := strings.TrimPrefix(name, "/")
871+ if existingContainerName == containerName {
945872 // This is an exact match - shouldn't happen if we're looking for recreated containers
946873 // but include it anyway
947874 result[domain] = &ProxyRouteInfo{
@@ -963,7 +890,7 @@ func (p *Proxy) FindRoutesByOldName(containerName string) map[string]*ProxyRoute
963890 // HandleContainerRecreation handles a container that has been recreated with the same name but a new ID
964891 // It updates all relevant routes with the new container ID and IP
965892 func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerIP string) {
966- log.Info("Handling potential container recreation",
893+ logger.Info("Handling potential container recreation",
967894 "container_id", containerID,
968895 "container_name", containerName,
969896 "container_ip", containerIP)
@@ -971,18 +898,21 @@ func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerI
971898 // Get container info to check for gordon labels
972899 containerInfo, err := docker.GetContainerInfo(containerID)
973900 if err != nil {
974- log.Warn("Failed to get container info", "error", err)
901+ logger.Warn("Failed to get container info", "error", err)
975902 return
976903 }
977904
978905 // Check if this is a Gordon-managed container
979906 if _, exists := containerInfo.Config.Labels["gordon.managed"]; !exists {
980- log.Debug("Ignoring non-Gordon container", "container_name", containerName)
907+ logger.Debug("Ignoring non-Gordon container", "container_name", containerName)
981908 return
982909 }
983910
984911 // First approach: check if there are any domains that should point to this container based on labels
985912 if domainLabel, exists := containerInfo.Config.Labels["gordon.domain"]; exists && domainLabel != "" {
913+ // Normalize domain (lowercase, no trailing dot)
914+ domainLabel = strings.TrimSuffix(strings.ToLower(domainLabel), ".")
915+
986916 // Check if there's already a route for this domain
987917 p.mu.RLock()
988918 route, domainExists := p.routes[domainLabel]
@@ -991,7 +921,7 @@ func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerI
991921 if domainExists {
992922 // Route exists - check if it's for a different container ID
993923 if route.ContainerID != containerID {
994- log.Info("Container was recreated with the same domain label",
924+ logger.Info("Container was recreated with the same domain label",
995925 "domain", domainLabel,
996926 "old_container_id", route.ContainerID,
997927 "new_container_id", containerID)
@@ -1012,9 +942,9 @@ func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerI
1012942 // Update the route with the new container ID and IP
1013943 err = p.AddRoute(domainLabel, containerID, containerIP, containerPort, protocol, route.Path)
1014944 if err != nil {
1015- log.Error("Failed to update route for recreated container", "error", err)
945+ logger.Error("Failed to update route for recreated container", "error", err)
1016946 } else {
1017- log.Info("Updated route for recreated container",
947+ logger.Info("Updated route for recreated container",
1018948 "domain", domainLabel,
1019949 "container_id", containerID,
1020950 "container_ip", containerIP)
@@ -1022,7 +952,7 @@ func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerI
1022952 } else {
1023953 // Same container ID but maybe IP changed
1024954 if route.ContainerIP != containerIP {
1025- log.Info("Container IP changed",
955+ logger.Info("Container IP changed",
1026956 "domain", domainLabel,
1027957 "container_id", containerID,
1028958 "old_ip", route.ContainerIP,
@@ -1031,13 +961,13 @@ func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerI
1031961 // Update the IP
1032962 err = p.ForceUpdateRouteIP(domainLabel, containerIP)
1033963 if err != nil {
1034- log.Error("Failed to update IP for container", "error", err)
964+ logger.Error("Failed to update IP for container", "error", err)
1035965 }
1036966 }
1037967 }
1038968 } else {
1039969 // No existing route for this domain - might be a new container
1040- log.Info("New container with domain label",
970+ logger.Info("New container with domain label",
1041971 "domain", domainLabel,
1042972 "container_id", containerID)
1043973
@@ -1054,10 +984,16 @@ func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerI
1054984 protocol = "https"
1055985 }
1056986
1057- // Add a new route
987+ // Add a new route for this container
988+ logger.Info("Adding missing route for Gordon container",
989+ "domain", domainLabel,
990+ "container_id", containerID,
991+ "container_ip", containerIP,
992+ "container_port", containerPort)
993+
1058994 err = p.AddRoute(domainLabel, containerID, containerIP, containerPort, protocol, "/")
1059995 if err != nil {
1060- log.Error("Failed to add route for new container", "error", err)
996+ logger.Error("Failed to add missing route", "error", err)
1061997 }
1062998 }
1063999 }
@@ -1065,12 +1001,12 @@ func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerI
10651001 // Second approach: check for routes with name-based matching that might need updating
10661002 // This handles the case where the container name is used instead of explicit domain labels
10671003 if domainCheck := p.FindRoutesByOldName(containerName); len(domainCheck) > 0 {
1068- log.Info("Found potential routes for recreated container by name matching",
1004+ logger.Info("Found potential routes for recreated container by name matching",
10691005 "container_name", containerName,
10701006 "domains_count", len(domainCheck))
10711007
10721008 for domain, oldRoute := range domainCheck {
1073- log.Info("Updating route for recreated container by name matching",
1009+ logger.Info("Updating route for recreated container by name matching",
10741010 "domain", domain,
10751011 "old_container_id", oldRoute.ContainerID,
10761012 "new_container_id", containerID)
@@ -1096,9 +1032,9 @@ func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerI
10961032 // Update the route
10971033 err = p.AddRoute(domain, containerID, containerIP, containerPort, protocol, oldRoute.Path)
10981034 if err != nil {
1099- log.Error("Failed to update route for recreated container", "error", err)
1035+ logger.Error("Failed to update route for recreated container", "error", err)
11001036 } else {
1101- log.Info("Updated route for recreated container",
1037+ logger.Info("Updated route for recreated container",
11021038 "domain", domain,
11031039 "container_id", containerID,
11041040 "container_ip", containerIP)
@@ -1116,243 +1052,39 @@ func (p *Proxy) StartContainerEventListener() error {
11161052 return fmt.Errorf("container network name is not configured")
11171053 }
11181054
1119- log.Info("Starting container event listener", "network", networkName)
1055+ logger.Info("Starting container event listener", "network", networkName)
11201056
11211057 // Create a callback function to handle container events
11221058 containerEventCallback := func(containerID, containerName, containerIP string) {
11231059 p.HandleContainerRecreation(containerID, containerName, containerIP)
11241060 }
11251061
1126- // Start the container event listener
1127- return docker.ListenForContainerEvents(networkName, containerEventCallback)
1128-}
1129-
1130-// StartPeriodicIPVerification starts a goroutine that periodically checks container IPs
1131-// and updates the routes if they have changed. This helps ensure routes always have
1132-// the correct IP address, even if the event listener missed something.
1133-func (p *Proxy) StartPeriodicIPVerification(interval time.Duration) {
1134- log.Info("Starting periodic IP verification", "interval", interval)
1135-
1136- // Run in a background goroutine
1137- go func() {
1138- // Use a ticker for periodic execution
1139- ticker := time.NewTicker(interval)
1140- defer ticker.Stop()
1141-
1142- for {
1143- select {
1144- case <-ticker.C:
1145- log.Debug("Running periodic IP verification")
1146- p.verifyAllContainerIPs()
1147-
1148- // Also check for any containers that should have routes but don't
1149- p.DiscoverMissingRoutes()
1150-
1151- // Log current active containers and routes for monitoring
1152- if log.GetLevel() <= log.DebugLevel {
1153- // Only log the container list in debug mode during periodic checks
1154- // to avoid overwhelming logs in production
1155- p.LogActiveContainersAndRoutes()
1156- }
1157- }
1158- }
1159- }()
1160-}
1161-
1162-// verifyAllContainerIPs checks all routes and verifies their container IPs
1163-func (p *Proxy) verifyAllContainerIPs() {
1164- p.mu.RLock()
1165- routes := make(map[string]*ProxyRouteInfo, len(p.routes))
1166- for domain, route := range p.routes {
1167- routes[domain] = &ProxyRouteInfo{
1168- Domain: route.Domain,
1169- ContainerID: route.ContainerID,
1170- ContainerIP: route.ContainerIP,
1171- ContainerPort: route.ContainerPort,
1172- Protocol: route.Protocol,
1173- Path: route.Path,
1174- Active: route.Active,
1175- }
1176- }
1177- p.mu.RUnlock()
1178-
1179- // Get network name from config
1180- networkName := p.app.GetConfig().ContainerEngine.Network
1181- if networkName == "" {
1182- log.Error("Container network name is not configured")
1183- return
1184- }
1185-
1186- // Check each route and verify the container IP
1187- routesUpdated := false
1188- updatedIPs := make(map[string]string) // cache to avoid redundant lookups
1189-
1190- // Track orphaned routes to mark as inactive
1191- orphanedRoutes := []string{}
1192-
1193- // Special handling for Gordon container - ensure its IP is correct first
1194- gordonContainerID := p.loadGordonContainerID()
1195- if gordonContainerID != "" {
1196- log.Debug("Verifying Gordon container IP first", "container_id", gordonContainerID)
1197-
1198- gordonIP, err := docker.GetContainerIPFromNetwork(gordonContainerID, networkName)
1199- if err == nil && gordonIP != "" {
1200- log.Debug("Found Gordon container IP", "ip", gordonIP)
1201- updatedIPs[gordonContainerID] = gordonIP
1202-
1203- // Double-check by direct container inspection to ensure maximum reliability
1204- containerInfo, inspectErr := docker.GetContainerInfo(gordonContainerID)
1205- if inspectErr == nil && containerInfo.NetworkSettings != nil {
1206- if network, exists := containerInfo.NetworkSettings.Networks[networkName]; exists &&
1207- network.IPAddress != "" && network.IPAddress != gordonIP {
1208- log.Info("Different IP found for Gordon container through inspection",
1209- "network_ip", gordonIP,
1210- "inspect_ip", network.IPAddress)
1211- updatedIPs[gordonContainerID] = network.IPAddress
1212- }
1213- }
1214- } else {
1215- log.Warn("Failed to get Gordon container IP from network",
1216- "container_id", gordonContainerID,
1217- "error", err)
1218- }
1219- }
1220-
1221- for domain, route := range routes {
1222- if !route.Active {
1223- continue
1224- }
1225-
1226- containerID := route.ContainerID
1227-
1228- // Skip verification if container doesn't exist anymore
1229- if !docker.ContainerExists(containerID) {
1230- log.Warn("Container no longer exists but route is active",
1231- "domain", domain,
1232- "container_id", containerID)
1233- orphanedRoutes = append(orphanedRoutes, domain)
1234- continue
1235- }
1236-
1237- // Check if this is for the Gordon container
1238- isGordonContainer := (containerID == gordonContainerID)
1239- if isGordonContainer {
1240- log.Debug("Verifying Gordon container route",
1241- "domain", domain,
1242- "container_id", containerID)
1243- }
1244-
1245- // Get the current container IP
1246- var currentIP string
1247- var err error
1248-
1249- // Check if we already looked up this container's IP
1250- if cachedIP, exists := updatedIPs[containerID]; exists {
1251- currentIP = cachedIP
1252- log.Debug("Using cached container IP",
1253- "container_id", containerID,
1254- "ip", currentIP)
1255- } else {
1256- // Otherwise, look it up
1257- currentIP, err = docker.GetContainerIPFromNetwork(containerID, networkName)
1258- if err != nil {
1259- log.Error("Failed to get container IP for verification",
1260- "domain", domain,
1261- "container_id", containerID,
1262- "error", err)
1263-
1264- // For Gordon container, try harder to get the IP
1265- if isGordonContainer {
1266- log.Info("Attempting alternative methods to get Gordon container IP")
1267-
1268- // Try direct container inspection as a fallback
1269- containerInfo, inspectErr := docker.GetContainerInfo(containerID)
1270- if inspectErr == nil && containerInfo.NetworkSettings != nil {
1271- // Check for our network specifically
1272- if network, exists := containerInfo.NetworkSettings.Networks[networkName]; exists && network.IPAddress != "" {
1273- currentIP = network.IPAddress
1274- log.Info("Successfully got Gordon container IP through inspection",
1275- "ip", currentIP)
1276- } else if containerInfo.NetworkSettings.IPAddress != "" {
1277- // Fallback to default network IP
1278- currentIP = containerInfo.NetworkSettings.IPAddress
1279- log.Info("Using default network IP for Gordon container",
1280- "ip", currentIP)
1281- }
1282- }
1283- }
1062+ // Create a callback function to handle container stop events
1063+ containerStopCallback := func(containerID, containerName string) {
1064+ logger.Info("Container stopped or removed, marking associated routes as inactive",
1065+ "container_id", containerID,
1066+ "container_name", containerName)
12841067
1285- // If we still couldn't get the IP, skip this route
1286- if currentIP == "" {
1287- continue
1288- }
1068+ // Find all domains associated with this container ID
1069+ var domains []string
1070+ p.mu.RLock()
1071+ for domain, route := range p.routes {
1072+ if route.ContainerID == containerID {
1073+ domains = append(domains, domain)
12891074 }
1290-
1291- // Cache the IP for future route checks
1292- updatedIPs[containerID] = currentIP
12931075 }
1076+ p.mu.RUnlock()
12941077
1295- // Check if the IP has changed
1296- if route.ContainerIP != currentIP {
1297- // Throttle updates for non-Gordon containers to prevent excessive updates
1298- if !isGordonContainer {
1299- containerName, _ := docker.GetContainerName(containerID)
1300- if containerName != "" {
1301- containerName = strings.TrimPrefix(containerName, "/")
1302- }
1303-
1304- log.Info("Container IP has changed - updating route",
1305- "domain", domain,
1306- "container_id", containerID,
1307- "container_name", containerName,
1308- "old_ip", route.ContainerIP,
1309- "new_ip", currentIP)
1310- } else {
1311- log.Info("Gordon container IP has changed - updating route",
1312- "admin_domain", domain,
1313- "gordon_container", containerID,
1314- "old_ip", route.ContainerIP,
1315- "new_ip", currentIP)
1316- }
1317-
1318- // Update the route in the database
1319- err = p.ForceUpdateRouteIP(domain, currentIP)
1320- if err != nil {
1321- log.Error("Failed to update route IP",
1322- "domain", domain,
1323- "error", err)
1324- } else {
1325- routesUpdated = true
1326-
1327- if isGordonContainer {
1328- log.Info("Successfully fixed admin domain route to point to Gordon container",
1329- "admin_domain", domain,
1330- "gordon_container", containerID)
1331- }
1332- }
1078+ if len(domains) > 0 {
1079+ logger.Info("Found routes to mark as inactive", "container_id", containerID, "domains", domains)
1080+ p.markRoutesInactive(domains)
13331081 } else {
1334- log.Debug("Container IP verified and unchanged",
1335- "domain", domain,
1336- "container_id", containerID,
1337- "ip", currentIP)
1082+ logger.Debug("No routes found for stopped container", "container_id", containerID)
13381083 }
13391084 }
13401085
1341- // Mark orphaned routes as inactive
1342- if len(orphanedRoutes) > 0 {
1343- log.Info("Marking orphaned routes as inactive", "count", len(orphanedRoutes))
1344- p.markRoutesInactive(orphanedRoutes)
1345- routesUpdated = true
1346- }
1347-
1348- // Reload routes if any were updated
1349- if routesUpdated {
1350- log.Debug("Routes were updated - reloading proxy configuration")
1351- err := p.Reload()
1352- if err != nil {
1353- log.Error("Failed to reload routes after IP verification", "error", err)
1354- }
1355- }
1086+ // Start the container event listener
1087+ return docker.ListenForContainerEvents(networkName, containerEventCallback, containerStopCallback)
13561088 }
13571089
13581090 // markRoutesInactive marks the specified routes as inactive in the database and memory
@@ -1370,7 +1102,7 @@ func (p *Proxy) markRoutesInactive(domains []string) {
13701102 if domain != adminDomain {
13711103 filteredDomains = append(filteredDomains, domain)
13721104 } else {
1373- log.Warn("Prevented admin domain from being marked inactive",
1105+ logger.Warn("Prevented admin domain from being marked inactive",
13741106 "domain", adminDomain)
13751107 }
13761108 }
@@ -1386,7 +1118,7 @@ func (p *Proxy) markRoutesInactive(domains []string) {
13861118 // Begin a transaction
13871119 tx, err := p.app.GetDB().Begin()
13881120 if err != nil {
1389- log.Error("Failed to begin transaction for marking routes inactive", "error", err)
1121+ logger.Error("Failed to begin transaction for marking routes inactive", "error", err)
13901122 return
13911123 }
13921124 defer tx.Rollback()
@@ -1397,7 +1129,7 @@ func (p *Proxy) markRoutesInactive(domains []string) {
13971129 var domainID string
13981130 err := tx.QueryRow("SELECT id FROM domain WHERE name = ?", domain).Scan(&domainID)
13991131 if err != nil {
1400- log.Error("Failed to get domain ID for inactive route", "domain", domain, "error", err)
1132+ logger.Error("Failed to get domain ID for inactive route", "domain", domain, "error", err)
14011133 continue
14021134 }
14031135
@@ -1405,7 +1137,7 @@ func (p *Proxy) markRoutesInactive(domains []string) {
14051137 now := time.Now().Format(time.RFC3339)
14061138 _, err = tx.Exec("UPDATE proxy_route SET active = 0, updated_at = ? WHERE domain_id = ?", now, domainID)
14071139 if err != nil {
1408- log.Error("Failed to mark route as inactive in database", "domain", domain, "error", err)
1140+ logger.Error("Failed to mark route as inactive in database", "domain", domain, "error", err)
14091141 continue
14101142 }
14111143
@@ -1413,29 +1145,29 @@ func (p *Proxy) markRoutesInactive(domains []string) {
14131145 p.mu.Lock()
14141146 if route, exists := p.routes[domain]; exists {
14151147 route.Active = false
1416- log.Info("Marked route as inactive", "domain", domain)
1148+ logger.Info("Marked route as inactive", "domain", domain)
14171149 }
14181150 p.mu.Unlock()
14191151 }
14201152
14211153 // Commit the transaction
14221154 if err := tx.Commit(); err != nil {
1423- log.Error("Failed to commit transaction for marking routes inactive", "error", err)
1155+ logger.Error("Failed to commit transaction for marking routes inactive", "error", err)
14241156 return
14251157 }
14261158
1427- log.Info("Successfully marked orphaned routes as inactive", "count", len(domains))
1159+ logger.Info("Successfully marked orphaned routes as inactive", "count", len(domains))
14281160 }
14291161
14301162 // DiscoverMissingRoutes finds Gordon-managed containers that don't have routes yet
14311163 // and adds them to the routing table
14321164 func (p *Proxy) DiscoverMissingRoutes() {
1433- log.Info("Discovering containers that might need routes")
1165+ logger.Info("Discovering containers that might need routes")
14341166
14351167 // Get the network name from app config
14361168 networkName := p.app.GetConfig().ContainerEngine.Network
14371169 if networkName == "" {
1438- log.Error("Cannot discover missing routes, network name is not configured")
1170+ logger.Error("Cannot discover missing routes, network name is not configured")
14391171 return
14401172 }
14411173
@@ -1445,7 +1177,7 @@ func (p *Proxy) DiscoverMissingRoutes() {
14451177 // Get all containers in the network
14461178 networkInfo, err := docker.GetNetworkInfo(networkName)
14471179 if err != nil {
1448- log.Error("Failed to get network info", "error", err)
1180+ logger.Error("Failed to get network info", "error", err)
14491181 return
14501182 }
14511183
@@ -1473,7 +1205,7 @@ func (p *Proxy) DiscoverMissingRoutes() {
14731205 // Get container info to check for Gordon labels
14741206 containerInfo, err := docker.GetContainerInfo(containerID)
14751207 if err != nil {
1476- log.Debug("Failed to get container info",
1208+ logger.Debug("Failed to get container info",
14771209 "container_id", containerID,
14781210 "error", err)
14791211 continue
@@ -1487,39 +1219,39 @@ func (p *Proxy) DiscoverMissingRoutes() {
14871219 // Check if it has a domain label
14881220 domainLabel, hasDomain := containerInfo.Config.Labels["gordon.domain"]
14891221 if !hasDomain || domainLabel == "" {
1490- log.Debug("Gordon container without domain label",
1222+ logger.Debug("Gordon container without domain label",
14911223 "container_id", containerID,
14921224 "container_name", strings.TrimPrefix(containerInfo.Name, "/"))
14931225 continue
14941226 }
14951227
1228+ // Normalize domain (lowercase, no trailing dot)
1229+ domainLabel = strings.TrimSuffix(strings.ToLower(domainLabel), ".")
1230+
14961231 // Skip if this is the admin domain
14971232 if domainLabel == adminDomain {
1498- log.Debug("Skipping auto-discovery for admin domain",
1499- "domain", domainLabel,
1500- "container_id", containerID)
1233+ logger.Debug("Skipping auto-discovery for admin domain",
1234+ "domain", domainLabel)
15011235 continue
15021236 }
15031237
1504- // Check if this domain already has a route (different container)
1238+ // Check if there's already a route for this domain
15051239 if _, exists := routes[domainLabel]; exists {
1506- log.Warn("Domain already has a route with a different container",
1507- "domain", domainLabel,
1508- "container_id", containerID,
1509- "existing_container_id", routes[domainLabel].ContainerID)
1240+ logger.Debug("Route already exists for domain",
1241+ "domain", domainLabel)
15101242 continue
15111243 }
15121244
1513- // Extract container IP and port for the new route
1245+ // Get the container IP from the network info
15141246 containerIP := containerEndpoint.IPv4Address
15151247 if containerIP == "" {
1516- containerIP = containerEndpoint.IPv6Address
1248+ logger.Warn("Container has no IP address",
1249+ "container_id", containerID)
1250+ continue
15171251 }
15181252
1519- // Extract just the IP part without subnet
1520- if idx := strings.Index(containerIP, "/"); idx > 0 {
1521- containerIP = containerIP[:idx]
1522- }
1253+ // Extract just the IP part from the CIDR notation (e.g., "192.168.1.2/24" → "192.168.1.2")
1254+ containerIP = strings.Split(containerIP, "/")[0]
15231255
15241256 // Get the port to use for the proxy from the container labels
15251257 containerPort := "80" // Default
@@ -1535,7 +1267,7 @@ func (p *Proxy) DiscoverMissingRoutes() {
15351267 }
15361268
15371269 // Add a new route for this container
1538- log.Info("Adding missing route for Gordon container",
1270+ logger.Info("Adding missing route for Gordon container",
15391271 "domain", domainLabel,
15401272 "container_id", containerID,
15411273 "container_ip", containerIP,
@@ -1543,7 +1275,7 @@ func (p *Proxy) DiscoverMissingRoutes() {
15431275
15441276 err = p.AddRoute(domainLabel, containerID, containerIP, containerPort, protocol, "/")
15451277 if err != nil {
1546- log.Error("Failed to add missing route", "error", err)
1278+ logger.Error("Failed to add missing route", "error", err)
15471279 }
15481280 }
15491281
@@ -1554,15 +1286,12 @@ func (p *Proxy) DiscoverMissingRoutes() {
15541286 count++
15551287 }
15561288 p.mu.RUnlock()
1557- log.Debug("Routes after discovery", "count", count)
1558-
1559- // Clean up any root domain conflict
1560- p.CleanupRootDomainForAdmin()
1289+ logger.Debug("Routes after discovery", "count", count)
15611290 }
15621291
15631292 // LogActiveContainersAndRoutes logs a list of active containers and their routes
15641293 func (p *Proxy) LogActiveContainersAndRoutes() {
1565- log.Info("---- Listing active containers and routes ----")
1294+ logger.Info("---- Listing active containers and routes ----")
15661295
15671296 // Get the admin domain
15681297 adminDomain := p.app.GetConfig().Http.FullDomain()
@@ -1574,10 +1303,10 @@ func (p *Proxy) LogActiveContainersAndRoutes() {
15741303 p.mu.RUnlock()
15751304
15761305 if !adminExists {
1577- log.Warn("Admin domain is missing from routes", "domain", adminDomain)
1306+ logger.Warn("Admin domain is missing from routes", "domain", adminDomain)
15781307 }
15791308
1580- log.Info(fmt.Sprintf("Found %d active routes", activeRoutes))
1309+ logger.Info(fmt.Sprintf("Found %d active routes", activeRoutes))
15811310
15821311 // Create a map of container IDs to their information
15831312 containerMap := make(map[string]struct {
@@ -1630,7 +1359,7 @@ func (p *Proxy) LogActiveContainersAndRoutes() {
16301359 if err == nil {
16311360 name, err := docker.GetContainerName(containerID)
16321361 if err == nil {
1633- containerData.Name = strings.TrimPrefix(name, "/")
1362+ containerData.Name = strings.TrimLeft(name, "/")
16341363 containerData.Image = containerInfo.Config.Image
16351364 containerMap[containerID] = containerData
16361365 }
@@ -1645,87 +1374,52 @@ func (p *Proxy) LogActiveContainersAndRoutes() {
16451374 if len(containerID) >= 12 {
16461375 containerIDShort = containerID[:12]
16471376 }
1648- log.Info(fmt.Sprintf("Container: %s (%s)", container.Name, containerIDShort),
1377+ logger.Info(fmt.Sprintf("Container: %s (%s)", container.Name, containerIDShort),
16491378 "ip", container.IP,
16501379 "image", container.Image)
16511380
16521381 // Log each domain for this container
16531382 for i, domain := range container.Domains {
1654- log.Info(fmt.Sprintf(" ├─ Domain: %s", domain),
1383+ logger.Info(fmt.Sprintf(" ├─ Domain: %s", domain),
16551384 "protocol", container.Protocols[i],
16561385 "port", container.Ports[i])
16571386 }
16581387 }
16591388
1660- log.Info("---- End of active containers and routes list ----")
1389+ logger.Info("---- End of active containers and routes list ----")
16611390 }
16621391
1663-// saveGordonContainerID saves the Gordon container ID to the config file
1664-func (p *Proxy) saveGordonContainerID(containerID string) {
1665- log.Debug("Saving Gordon container ID to config file", "container_id", containerID)
1666-
1667- // Update the config
1668- config := p.app.GetConfig()
1669- config.General.GordonContainerID = containerID
1670-
1671- // Save the config file
1672- err := config.SaveConfig()
1673- if err != nil {
1674- log.Error("Failed to save Gordon container ID to config file", "error", err)
1675- return
1676- }
1677-
1678- // Update the gordonContainerID field
1679- p.gordonContainerID = containerID
1680-
1681- log.Info("Successfully saved Gordon container ID to config file", "container_id", containerID)
1682-}
1683-
1684-// loadGordonContainerID loads the Gordon container ID from the config file
1685-func (p *Proxy) loadGordonContainerID() string {
1686- log.Debug("Loading Gordon container ID from config file")
1687-
1688- // Get the container ID from the config
1689- containerID := p.app.GetConfig().General.GordonContainerID
1690-
1691- if containerID == "" {
1692- log.Debug("No Gordon container ID found in config file")
1693- return ""
1694- }
1695-
1696- log.Info("Loaded Gordon container ID from config file", "container_id", containerID)
1697- return containerID
1698-}
1699-
1700-// detectGordonContainer attempts to find the Gordon container automatically
1701-func (p *Proxy) detectGordonContainer() string {
1702- log.Debug("Starting Gordon container detection")
1392+// LogGordonIdentity prints information about the Gordon container for debugging
1393+func (p *Proxy) LogGordonIdentity() {
1394+ logger.Debug("Logging Gordon identity information")
17031395
1704- // First try to load the container ID from the config file
1705- dbContainerID := p.loadGordonContainerID()
1706- if dbContainerID != "" {
1707- p.gordonContainerID = dbContainerID
1708- log.Debug("Loaded Gordon container ID from config file", "container_id", p.gordonContainerID)
1396+ // Print our container ID if available
1397+ if p.gordonContainerID != "" {
1398+ logger.Info("Gordon container ID", "container_id", p.gordonContainerID)
17091399
1710- // Verify the container still exists
1400+ // Try to get the container name
17111401 containers, err := docker.ListRunningContainers()
17121402 if err == nil {
17131403 for _, container := range containers {
17141404 if container.ID == p.gordonContainerID {
17151405 containerName := strings.TrimLeft(container.Names[0], "/")
1716- log.Debug("Identified Gordon container using config file ID", "container_name", containerName)
1717- return containerName
1406+ logger.Info("Gordon container name", "container_name", containerName)
1407+ break
17181408 }
17191409 }
1720- // If the container ID is no longer found, log it but continue with other detection methods
1721- log.Warn("Stored Gordon container ID from config file no longer found in running containers", "container_id", p.gordonContainerID)
1722- p.gordonContainerID = "" // Reset since it's invalid
17231410 }
1411+ } else {
1412+ logger.Warn("Gordon container ID not available - running outside container?")
17241413 }
1414+}
1415+
1416+// detectGordonContainer attempts to find the Gordon container automatically
1417+func (p *Proxy) detectGordonContainer() string {
1418+ logger.Debug("Starting Gordon container detection")
17251419
1726- // If we have our container ID stored from initialization, use that next
1420+ // If we have our container ID stored from initialization, use that first
17271421 if p.gordonContainerID != "" {
1728- log.Debug("Using stored Gordon container ID for identification", "container_id", p.gordonContainerID)
1422+ logger.Debug("Using stored Gordon container ID for identification", "container_id", p.gordonContainerID)
17291423
17301424 // Get the container name from the ID
17311425 containers, err := docker.ListRunningContainers()
@@ -1733,23 +1427,21 @@ func (p *Proxy) detectGordonContainer() string {
17331427 for _, container := range containers {
17341428 if container.ID == p.gordonContainerID {
17351429 containerName := strings.TrimLeft(container.Names[0], "/")
1736- log.Info("Gordon identity established via exact ID match",
1430+ logger.Info("Gordon identity established via exact ID match",
17371431 "container_id", container.ID,
17381432 "container_name", containerName)
1739- p.gordonContainerID = container.ID
1740- // Save to config file for persistence
1741- p.saveGordonContainerID(container.ID)
17421433 return containerName
17431434 }
17441435 }
17451436 // If the container ID is no longer found, log it but continue with other detection methods
1746- log.Warn("Stored Gordon container ID no longer found in running containers", "container_id", p.gordonContainerID)
1437+ logger.Warn("Stored Gordon container ID no longer found in running containers", "container_id", p.gordonContainerID)
1438+ p.gordonContainerID = "" // Reset since it's invalid
17471439 }
17481440 }
17491441
17501442 // Check if we're running inside a container
17511443 if docker.IsRunningInContainer() {
1752- log.Debug("Detected we're running in a container")
1444+ logger.Debug("Detected we're running in a container")
17531445
17541446 // Get our hostname which should be the container ID in Docker/Podman
17551447 hostname := os.Getenv("HOSTNAME")
@@ -1762,12 +1454,10 @@ func (p *Proxy) detectGordonContainer() string {
17621454 for _, container := range containers {
17631455 if hostname == container.ID {
17641456 containerName := strings.TrimLeft(container.Names[0], "/")
1765- log.Info("Gordon identity established via exact ID match",
1457+ logger.Info("Gordon identity established via exact ID match",
17661458 "container_id", container.ID,
17671459 "container_name", containerName)
17681460 p.gordonContainerID = container.ID
1769- // Save to config file for persistence
1770- p.saveGordonContainerID(container.ID)
17711461 return containerName
17721462 }
17731463 }
@@ -1776,18 +1466,16 @@ func (p *Proxy) detectGordonContainer() string {
17761466 for _, container := range containers {
17771467 if strings.HasPrefix(container.ID, hostname) || strings.HasPrefix(hostname, container.ID) {
17781468 containerName := strings.TrimLeft(container.Names[0], "/")
1779- log.Info("Gordon identity established via ID prefix match",
1469+ logger.Info("Gordon identity established via ID prefix match",
17801470 "container_id", container.ID,
17811471 "container_name", containerName,
17821472 "hostname", hostname)
17831473 p.gordonContainerID = container.ID
1784- // Save to config file for persistence
1785- p.saveGordonContainerID(container.ID)
17861474 return containerName
17871475 }
17881476 }
17891477 } else {
1790- log.Debug("Could not list containers when checking hostname", "error", err)
1478+ logger.Debug("Could not list containers when checking hostname", "error", err)
17911479 }
17921480 }
17931481 }
@@ -1798,10 +1486,8 @@ func (p *Proxy) detectGordonContainer() string {
17981486 for _, container := range containers {
17991487 containerName := strings.TrimLeft(container.Names[0], "/")
18001488 if containerName == "gordon" {
1801- log.Info("Found Gordon container with exact name 'gordon'", "container_id", container.ID)
1489+ logger.Info("Found Gordon container with exact name 'gordon'", "container_id", container.ID)
18021490 p.gordonContainerID = container.ID
1803- // Save to config file for persistence
1804- p.saveGordonContainerID(container.ID)
18051491 return containerName
18061492 }
18071493 }
@@ -1810,173 +1496,157 @@ func (p *Proxy) detectGordonContainer() string {
18101496 for _, container := range containers {
18111497 containerName := strings.TrimLeft(container.Names[0], "/")
18121498 if strings.Contains(strings.ToLower(containerName), "gordon") {
1813- log.Info("Found Gordon container with partial name match",
1499+ logger.Info("Found Gordon container with partial name match",
18141500 "container_name", containerName,
18151501 "container_id", container.ID)
18161502 p.gordonContainerID = container.ID
1817- // Save to config file for persistence
1818- p.saveGordonContainerID(container.ID)
18191503 return containerName
18201504 }
18211505 }
18221506 }
18231507
18241508 // Fallback to a default name
1825- log.Warn("Could not auto-detect Gordon container, using fallback name", "container_name", "gordon")
1509+ logger.Warn("Could not auto-detect Gordon container, using fallback name", "container_name", "gordon")
18261510 return "gordon"
18271511 }
18281512
1829-// CleanupRootDomainForAdmin checks if the root domain is pointing to the admin container
1830-// and removes that route to avoid conflicts with other services using the root domain
1831-func (p *Proxy) CleanupRootDomainForAdmin() {
1832- // Get admin domain and root domain
1833- adminDomain := p.app.GetConfig().Http.FullDomain()
1834- rootDomain := p.app.GetConfig().Http.Domain
1835-
1836- // Skip if root domain is the same as admin domain
1837- if rootDomain == adminDomain || rootDomain == "" {
1838- return
1839- }
1840-
1841- // Check if root domain exists in memory routes
1842- p.mu.RLock()
1843- rootRoute, rootExists := p.routes[rootDomain]
1844- adminRoute, adminExists := p.routes[adminDomain]
1845- p.mu.RUnlock()
1846-
1847- // If both root domain and admin domain routes exist,
1848- // check if they point to the same container ID (Gordon)
1849- if rootExists && adminExists {
1850- // Always ensure the admin domain points to the Gordon container
1851- if p.gordonContainerID != "" && adminRoute.ContainerID != p.gordonContainerID {
1852- log.Warn("Admin domain is pointing to the wrong container, fixing",
1853- "admin_domain", adminDomain,
1854- "current_container", adminRoute.ContainerID,
1855- "gordon_container", p.gordonContainerID)
1856-
1857- // Update the admin route to point to the Gordon container
1858- // Begin a transaction
1859- tx, err := p.app.GetDB().Begin()
1860- if err != nil {
1861- log.Error("Failed to begin transaction for admin domain fix", "error", err)
1862- return
1863- }
1864- defer tx.Rollback()
1865-
1866- // Get the domain ID for admin domain
1867- var domainID string
1868- err = tx.QueryRow("SELECT id FROM domain WHERE name = ?", adminDomain).Scan(&domainID)
1869- if err != nil {
1870- if err != sql.ErrNoRows {
1871- log.Error("Failed to query domain for admin domain fix", "error", err)
1872- }
1873- return
1874- }
1875-
1876- // Update the route for admin domain
1877- _, err = tx.Exec("UPDATE proxy_route SET container_id = ? WHERE domain_id = ?",
1878- p.gordonContainerID, domainID)
1879- if err != nil {
1880- log.Error("Failed to update admin domain route", "error", err)
1881- return
1882- }
1883-
1884- // Commit the transaction
1885- if err := tx.Commit(); err != nil {
1886- log.Error("Failed to commit transaction for admin domain fix", "error", err)
1887- return
1888- }
1889-
1890- // Update in-memory route
1891- p.mu.Lock()
1892- adminRoute.ContainerID = p.gordonContainerID
1893- p.mu.Unlock()
1894-
1895- log.Info("Successfully fixed admin domain route to point to Gordon container",
1896- "admin_domain", adminDomain,
1897- "gordon_container", p.gordonContainerID)
1513+// proxyRequest handles the actual proxying of the request to the target container
1514+func (p *Proxy) proxyRequest(c echo.Context, route *ProxyRouteInfo) error {
1515+ host := c.Request().Host
1516+
1517+ // For container routes, verify the IP is reachable before proxying
1518+ if host != p.app.GetConfig().Http.FullDomain() {
1519+ // Test TCP connection to container before proxying
1520+ dialer := net.Dialer{Timeout: 500 * time.Millisecond}
1521+ // Format address properly for both IPv4 and IPv6
1522+ target := route.ContainerIP
1523+ if strings.Contains(target, ":") {
1524+ // IPv6 address needs to be wrapped in brackets
1525+ target = "[" + target + "]"
18981526 }
1899-
1900- // Now handle the root domain conflict
1901- if rootRoute.ContainerID == adminRoute.ContainerID {
1902- log.Warn("Found root domain pointing to admin container, removing to avoid conflicts",
1903- "root_domain", rootDomain,
1904- "admin_domain", adminDomain,
1905- "container", rootRoute.ContainerID)
1906-
1907- // Begin a transaction
1908- tx, err := p.app.GetDB().Begin()
1909- if err != nil {
1910- log.Error("Failed to begin transaction for root domain cleanup", "error", err)
1911- return
1912- }
1913- defer tx.Rollback()
1914-
1915- // Get the domain ID for root domain
1916- var domainID string
1917- err = tx.QueryRow("SELECT id FROM domain WHERE name = ?", rootDomain).Scan(&domainID)
1918- if err != nil {
1919- if err != sql.ErrNoRows {
1920- log.Error("Failed to query domain for root domain cleanup", "error", err)
1527+ conn, err := dialer.Dial("tcp", net.JoinHostPort(target, route.ContainerPort))
1528+ if err != nil {
1529+ // Connection failed, check if container exists but has a new IP
1530+ containerID := route.ContainerID
1531+ if containerID != "" {
1532+ // Try to get container info and check if it has a different IP
1533+ containerInfo, err := docker.GetContainerInfo(containerID)
1534+ if err == nil && containerInfo.NetworkSettings != nil {
1535+ // Check if container has a different IP
1536+ networkName := p.app.GetConfig().ContainerEngine.Network
1537+ if networkSettings, exists := containerInfo.NetworkSettings.Networks[networkName]; exists &&
1538+ networkSettings.IPAddress != "" && networkSettings.IPAddress != route.ContainerIP {
1539+ // Found container with different IP, update the route and use new IP for this request
1540+ logger.Warn("Container IP mismatch detected, using live IP instead of cached value",
1541+ "domain", host,
1542+ "cached_ip", route.ContainerIP,
1543+ "live_ip", networkSettings.IPAddress)
1544+
1545+ // Update the in-memory route for this request
1546+ route.ContainerIP = networkSettings.IPAddress
1547+
1548+ // Update the database in background to persist the change
1549+ go func(domain, containerID, containerIP, containerPort string) {
1550+ // Get the domain ID
1551+ var domainID string
1552+ err := p.app.GetDB().QueryRow("SELECT id FROM domain WHERE name = ?", domain).Scan(&domainID)
1553+ if err != nil {
1554+ logger.Error("Failed to find domain ID for IP update", "domain", domain, "error", err)
1555+ return
1556+ }
1557+
1558+ // Update the proxy_route record
1559+ now := time.Now().Format(time.RFC3339)
1560+ _, err = p.app.GetDB().Exec(`
1561+ UPDATE proxy_route SET container_ip = ?, updated_at = ?
1562+ WHERE domain_id = ? AND container_id = ?`,
1563+ containerIP, now, domainID, containerID)
1564+
1565+ if err != nil {
1566+ logger.Error("Failed to update container IP in database",
1567+ "domain", domain,
1568+ "containerID", containerID,
1569+ "error", err)
1570+ } else {
1571+ logger.Info("Updated container IP in database after live detection",
1572+ "domain", domain,
1573+ "containerID", containerID,
1574+ "old_ip", route.ContainerIP,
1575+ "new_ip", containerIP)
1576+ }
1577+ }(host, containerID, networkSettings.IPAddress, route.ContainerPort)
1578+ } else {
1579+ logger.Debug("Container exists but connection still failed",
1580+ "domain", host,
1581+ "container_id", containerID,
1582+ "container_ip", route.ContainerIP,
1583+ "container_port", route.ContainerPort,
1584+ "error", err)
1585+ }
19211586 }
1922- return
19231587 }
1588+ } else {
1589+ // Connection successful, close it
1590+ conn.Close()
1591+ }
1592+ }
19241593
1925- // Delete the route for root domain
1926- _, err = tx.Exec("DELETE FROM proxy_route WHERE domain_id = ?", domainID)
1927- if err != nil {
1928- log.Error("Failed to delete root domain route", "error", err)
1929- return
1930- }
1594+ // Create the target URL
1595+ targetURL := &url.URL{
1596+ Scheme: "http", // Always use HTTP for internal connections
1597+ }
19311598
1932- // We don't delete the domain itself from the database to avoid affecting certificate management
1599+ // Log protocol conversion if needed
1600+ if route.Protocol == "https" {
1601+ logger.Debug("Converting HTTPS external route to HTTP for internal connection",
1602+ "domain", host,
1603+ "containerID", route.ContainerID,
1604+ "externalProtocol", "https",
1605+ "internalProtocol", "http")
1606+ }
19331607
1934- // Commit the transaction
1935- if err := tx.Commit(); err != nil {
1936- log.Error("Failed to commit transaction for root domain cleanup", "error", err)
1937- return
1938- }
1608+ // Format host properly for both IPv4 and IPv6
1609+ containerIP := route.ContainerIP
1610+ if strings.Contains(containerIP, ":") {
1611+ // IPv6 address needs to be wrapped in brackets
1612+ containerIP = "[" + containerIP + "]"
1613+ }
1614+ targetURL.Host = fmt.Sprintf("%s:%s", containerIP, route.ContainerPort)
19391615
1940- // Remove root domain from in-memory routes
1941- p.mu.Lock()
1942- delete(p.routes, rootDomain)
1943- p.mu.Unlock()
1616+ // Create a reverse proxy
1617+ proxy := httputil.NewSingleHostReverseProxy(targetURL)
19441618
1945- log.Info("Successfully removed conflicting root domain route",
1946- "root_domain", rootDomain,
1947- "admin_domain", adminDomain)
1619+ // Update headers to allow for SSL redirection
1620+ originalDirector := proxy.Director
1621+ proxy.Director = func(req *http.Request) {
1622+ originalDirector(req)
1623+ // Use the original protocol for forwarded proto header
1624+ forwardedProto := "http"
1625+ if route.Protocol == "https" {
1626+ forwardedProto = "https"
19481627 }
1949- }
1950-}
1628+ req.Header.Set("X-Forwarded-Proto", forwardedProto)
1629+ req.Header.Set("X-Forwarded-Host", host)
1630+ req.Header.Set("X-Forwarded-For", c.RealIP())
1631+ req.Header.Set("X-Real-IP", c.RealIP())
19511632
1952-// LogGordonIdentity prints information about the Gordon container for debugging
1953-func (p *Proxy) LogGordonIdentity() {
1954- // If we don't have a container ID yet, try to load it from the database first
1955- if p.gordonContainerID == "" {
1956- dbContainerID := p.loadGordonContainerID()
1957- if dbContainerID != "" {
1958- p.gordonContainerID = dbContainerID
1959- log.Info("Loaded Gordon container ID from config file during identity check", "container_id", p.gordonContainerID)
1960- }
1633+ // Log all headers for troubleshooting
1634+ logger.Debug("Request headers for proxy",
1635+ "domain", host,
1636+ "X-Forwarded-For", req.Header.Get("X-Forwarded-For"),
1637+ "X-Real-IP", req.Header.Get("X-Real-IP"))
19611638 }
19621639
1963- // If we still don't have a container ID, try to detect it
1964- if p.gordonContainerID == "" {
1965- // This will attempt to detect the container and set p.gordonContainerID
1966- containerName := p.detectGordonContainer()
1967- if p.gordonContainerID != "" {
1968- log.Info("Detected Gordon container during identity check",
1969- "container_id", p.gordonContainerID,
1970- "container_name", containerName)
1971- }
1640+ // Add error handling
1641+ proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
1642+ logger.Error("Proxy error",
1643+ "host", host,
1644+ "path", r.URL.Path,
1645+ "error", err)
1646+ c.String(http.StatusBadGateway, "Proxy Error: "+err.Error())
19721647 }
19731648
1974- // Now print our container ID for debugging
1975- if p.gordonContainerID != "" {
1976- log.Info("Gordon container identity",
1977- "container_id", p.gordonContainerID,
1978- "container_name", p.detectGordonContainer())
1979- } else {
1980- log.Warn("Gordon container ID not available - running outside container?")
1981- }
1649+ // Serve the request
1650+ proxy.ServeHTTP(c.Response(), c.Request())
1651+ return nil
19821652 }
@@ -12,7 +12,7 @@ import (
12 "time"12 "time"
13 13
14 "github.com/bnema/gordon/pkg/docker"14 "github.com/bnema/gordon/pkg/docker"
15- "github.com/charmbracelet/log"15+ "github.com/bnema/gordon/pkg/logger"
16 "github.com/labstack/echo/v4"16 "github.com/labstack/echo/v4"
17 )17 )
18 18
@@ -33,12 +33,12 @@ func (p *Proxy) loadRoutes() error {
33 p.mu.Lock()33 p.mu.Lock()
34 defer p.mu.Unlock()34 defer p.mu.Unlock()
35 35
36- log.Debug("Loading proxy routes from database")36+ logger.Debug("Loading proxy routes from database")
37 37
38 // Now check if we have a Gordon container ID stored for identity purposes38 // Now check if we have a Gordon container ID stored for identity purposes
39- p.gordonContainerID = p.loadGordonContainerID()39+ p.gordonContainerID = p.detectGordonContainer()
40 if p.gordonContainerID != "" {40 if p.gordonContainerID != "" {
41- log.Info("Loaded Gordon container ID from config file during route initialization", "container_id", p.gordonContainerID)41+ logger.Info("Loaded Gordon container ID during route initialization", "container_id", p.gordonContainerID)
42 }42 }
43 43
44 // Save the admin domain route if it exists44 // Save the admin domain route if it exists
@@ -46,11 +46,11 @@ func (p *Proxy) loadRoutes() error {
46 var adminRoute *ProxyRouteInfo46 var adminRoute *ProxyRouteInfo
47 if route, exists := p.routes[adminDomain]; exists {47 if route, exists := p.routes[adminDomain]; exists {
48 adminRoute = route48 adminRoute = route
49- log.Debug("Preserving admin domain route", "domain", adminDomain)49+ logger.Debug("Preserving admin domain route", "domain", adminDomain)
50 }50 }
51 51
52- // Query the database for active proxy routes52+ // Query the database for active proxy routes using retry mechanism
53- rows, err := p.app.GetDB().Query(`53+ rows, err := p.dbQueryWithRetry(`
54 SELECT pr.id, d.name, pr.container_id, pr.container_ip, pr.container_port, pr.protocol, pr.path, pr.active54 SELECT pr.id, d.name, pr.container_id, pr.container_ip, pr.container_port, pr.protocol, pr.path, pr.active
55 FROM proxy_route pr55 FROM proxy_route pr
56 JOIN domain d ON pr.domain_id = d.id56 JOIN domain d ON pr.domain_id = d.id
@@ -58,13 +58,13 @@ func (p *Proxy) loadRoutes() error {
58 `)58 `)
59 if err != nil {59 if err != nil {
60 if err == sql.ErrNoRows {60 if err == sql.ErrNoRows {
61- log.Info("No active proxy routes found")61+ logger.Info("No active proxy routes found")
62 62
63 // Restore admin route if it was saved63 // Restore admin route if it was saved
64 if adminRoute != nil {64 if adminRoute != nil {
65 p.routes = make(map[string]*ProxyRouteInfo)65 p.routes = make(map[string]*ProxyRouteInfo)
66 p.routes[adminDomain] = adminRoute66 p.routes[adminDomain] = adminRoute
67- log.Debug("Restored admin domain route after empty database query", "domain", adminDomain)67+ logger.Debug("Restored admin domain route after empty database query", "domain", adminDomain)
68 }68 }
69 69
70 return nil70 return nil
@@ -79,7 +79,7 @@ func (p *Proxy) loadRoutes() error {
79 // Restore admin route if it was saved79 // Restore admin route if it was saved
80 if adminRoute != nil {80 if adminRoute != nil {
81 p.routes[adminDomain] = adminRoute81 p.routes[adminDomain] = adminRoute
82- log.Debug("Restored admin domain route after clearing routes map", "domain", adminDomain)82+ logger.Debug("Restored admin domain route after clearing routes map", "domain", adminDomain)
83 }83 }
84 84
85 // Populate the routes map85 // Populate the routes map
@@ -90,6 +90,9 @@ func (p *Proxy) loadRoutes() error {
90 return fmt.Errorf("failed to scan row: %w", err)90 return fmt.Errorf("failed to scan row: %w", err)
91 }91 }
92 92
93+ // Normalize domain (lowercase, no trailing dot)
94+ domain = strings.TrimSuffix(strings.ToLower(domain), ".")
95+
93 // Add the route to the map96 // Add the route to the map
94 p.routes[domain] = &ProxyRouteInfo{97 p.routes[domain] = &ProxyRouteInfo{
95 Domain: domain,98 Domain: domain,
@@ -101,14 +104,14 @@ func (p *Proxy) loadRoutes() error {
101 Active: active,104 Active: active,
102 }105 }
103 106
104- log.Debug("Loaded proxy route",107+ logger.Debug("Loaded proxy route",
105 "domain", domain,108 "domain", domain,
106 "containerIP", containerIP,109 "containerIP", containerIP,
107 "containerPort", containerPort,110 "containerPort", containerPort,
108 )111 )
109 }112 }
110 113
111- log.Info("Loaded proxy routes", "count", len(p.routes))114+ logger.Info("Loaded proxy routes", "count", len(p.routes))
112 return nil115 return nil
113 }116 }
114 117
@@ -116,7 +119,7 @@ func (p *Proxy) loadRoutes() error {
116 func (p *Proxy) configureRoutes() {119 func (p *Proxy) configureRoutes() {
117 // Get the admin domain from the config120 // Get the admin domain from the config
118 adminDomain := p.app.GetConfig().Http.FullDomain()121 adminDomain := p.app.GetConfig().Http.FullDomain()
119- log.Debug("Configuring proxy routes", "admin_domain", adminDomain)122+ logger.Debug("Configuring proxy routes", "admin_domain", adminDomain)
120 123
121 // Verify admin domain is in routes - if not, we need to add it124 // Verify admin domain is in routes - if not, we need to add it
122 p.mu.RLock()125 p.mu.RLock()
@@ -124,7 +127,7 @@ func (p *Proxy) configureRoutes() {
124 p.mu.RUnlock()127 p.mu.RUnlock()
125 128
126 if !adminRouteExists {129 if !adminRouteExists {
127- log.Warn("Admin domain route is missing, attempting to recreate it")130+ logger.Warn("Admin domain route is missing, attempting to recreate it")
128 p.mu.Lock()131 p.mu.Lock()
129 // Auto-detect the Gordon container name132 // Auto-detect the Gordon container name
130 containerName := p.detectGordonContainer()133 containerName := p.detectGordonContainer()
@@ -151,18 +154,60 @@ func (p *Proxy) configureRoutes() {
151 Path: "/",154 Path: "/",
152 Active: true,155 Active: true,
153 }156 }
154- log.Info("Recreated admin domain route",157+ logger.Info("Recreated admin domain route",
155 "domain", adminDomain,158 "domain", adminDomain,
156 "target", fmt.Sprintf("http://%s:%s", containerIP, p.app.GetConfig().Http.Port))159 "target", fmt.Sprintf("http://%s:%s", containerIP, p.app.GetConfig().Http.Port))
157 p.mu.Unlock()160 p.mu.Unlock()
158 } else {161 } else {
159- log.Debug("Admin domain route exists", "domain", adminDomain)162+ logger.Debug("Admin domain route exists", "domain", adminDomain)
160 }163 }
161 164
162 // Handler function - will be assigned to both HTTP and HTTPS servers165 // Handler function - will be assigned to both HTTP and HTTPS servers
163 handler := echo.HandlerFunc(func(c echo.Context) error {166 handler := echo.HandlerFunc(func(c echo.Context) error {
164 host := c.Request().Host167 host := c.Request().Host
165 168
169+ // Strip port if present in the host
170+ if hostParts := strings.Split(host, ":"); len(hostParts) > 1 {
171+ host = hostParts[0]
172+ }
173+
174+ // Normalize host (trim trailing dot, convert to lowercase)
175+ host = strings.TrimSuffix(strings.ToLower(host), ".")
176+
177+ // Special handling for debug: log all request details
178+ logger.Debug("Request headers for proxy",
179+ "domain", host,
180+ "X-Forwarded-For", c.Request().Header.Get("X-Forwarded-For"),
181+ "X-Real-IP", c.Request().Header.Get("X-Real-IP"))
182+
183+ // Check if this is the admin domain
184+ adminDomain := p.app.GetConfig().Http.FullDomain()
185+ if strings.EqualFold(host, adminDomain) {
186+ // Always prioritize admin domain
187+ p.mu.RLock()
188+ adminRoute, adminOk := p.routes[adminDomain]
189+ p.mu.RUnlock()
190+
191+ if adminOk {
192+ logger.Debug("Proxying request to admin domain",
193+ "domain", host,
194+ "target", fmt.Sprintf("%s://%s:%s", adminRoute.Protocol, adminRoute.ContainerIP, adminRoute.ContainerPort),
195+ "path", c.Request().URL.Path)
196+
197+ // Special logging for admin domain handling
198+ logger.Debug("Proxying request",
199+ "host", host,
200+ "target", fmt.Sprintf("%s://%s:%s", adminRoute.Protocol, adminRoute.ContainerIP, adminRoute.ContainerPort),
201+ "path", c.Request().URL.Path,
202+ "clientIP", c.RealIP(),
203+ "remoteAddr", c.Request().RemoteAddr,
204+ "originalProtocol", c.Scheme())
205+
206+ // Use the admin route
207+ return p.proxyRequest(c, adminRoute)
208+ }
209+ }
210+
166 // Important: Get the route freshly from the map for every request211 // Important: Get the route freshly from the map for every request
167 // This ensures we always use the most up-to-date IP212 // This ensures we always use the most up-to-date IP
168 p.mu.RLock()213 p.mu.RLock()
@@ -185,7 +230,7 @@ func (p *Proxy) configureRoutes() {
185 p.mu.RUnlock()230 p.mu.RUnlock()
186 231
187 // Log warning for non-IP hosts that aren't configured232 // Log warning for non-IP hosts that aren't configured
188- log.Warn("Request with unknown host",233+ logger.Warn("Request with unknown host",
189 "requested_host", host,234 "requested_host", host,
190 "client_ip", c.RealIP(),235 "client_ip", c.RealIP(),
191 "available_domains", strings.Join(availableDomains, ", "))236 "available_domains", strings.Join(availableDomains, ", "))
@@ -194,164 +239,22 @@ func (p *Proxy) configureRoutes() {
194 239
195 // Check if the route is active240 // Check if the route is active
196 if !route.Active {241 if !route.Active {
197- log.Warn("Route is not active",242+ logger.Warn("Route is not active",
198 "domain", host,243 "domain", host,
199 "client_ip", c.RealIP())244 "client_ip", c.RealIP())
200 return c.String(http.StatusServiceUnavailable, "Route is not active")245 return c.String(http.StatusServiceUnavailable, "Route is not active")
201 }246 }
202 247
203- // Special handling for the admin domain248+ // Regular debug logging for all domains
204- if host == adminDomain {249+ logger.Debug("Proxying request",
205- log.Debug("Proxying request to admin domain",250+ "host", host,
206- "domain", host,251+ "target", fmt.Sprintf("%s://%s:%s", route.Protocol, route.ContainerIP, route.ContainerPort),
207- "target", fmt.Sprintf("%s://%s:%s", route.Protocol, route.ContainerIP, route.ContainerPort),252+ "path", c.Request().URL.Path,
208- "path", c.Request().URL.Path)253+ "clientIP", c.RealIP(),
209- }254+ "remoteAddr", c.Request().RemoteAddr,
210-255+ "originalProtocol", c.Scheme())
211- // For container routes, verify the IP is reachable before proxying
212- if host != adminDomain {
213- // Test TCP connection to container before proxying
214- dialer := net.Dialer{Timeout: 500 * time.Millisecond}
215- // Format address properly for both IPv4 and IPv6
216- target := route.ContainerIP
217- if strings.Contains(target, ":") {
218- // IPv6 address needs to be wrapped in brackets
219- target = "[" + target + "]"
220- }
221- conn, err := dialer.Dial("tcp", net.JoinHostPort(target, route.ContainerPort))
222- if err != nil {
223- // Connection failed, check if container exists but has a new IP
224- containerID := route.ContainerID
225- if containerID != "" {
226- // Try to get container info and check if it has a different IP
227- containerInfo, err := docker.GetContainerInfo(containerID)
228- if err == nil && containerInfo.NetworkSettings != nil {
229- // Check if container has a different IP
230- networkName := p.app.GetConfig().ContainerEngine.Network
231- if networkSettings, exists := containerInfo.NetworkSettings.Networks[networkName]; exists &&
232- networkSettings.IPAddress != "" && networkSettings.IPAddress != route.ContainerIP {
233- // Found container with different IP, update the route and use new IP for this request
234- log.Warn("Container IP mismatch detected, using live IP instead of cached value",
235- "domain", host,
236- "cached_ip", route.ContainerIP,
237- "live_ip", networkSettings.IPAddress)
238-
239- // Update the in-memory route for this request
240- route.ContainerIP = networkSettings.IPAddress
241-
242- // Update the database in background to persist the change
243- go func(domain, containerID, containerIP, containerPort string) {
244- // Get the domain ID
245- var domainID string
246- err := p.app.GetDB().QueryRow("SELECT id FROM domain WHERE name = ?", domain).Scan(&domainID)
247- if err != nil {
248- log.Error("Failed to find domain ID for IP update", "domain", domain, "error", err)
249- return
250- }
251-
252- // Update the proxy_route record
253- now := time.Now().Format(time.RFC3339)
254- _, err = p.app.GetDB().Exec(`
255- UPDATE proxy_route SET container_ip = ?, updated_at = ?
256- WHERE domain_id = ? AND container_id = ?`,
257- containerIP, now, domainID, containerID)
258-
259- if err != nil {
260- log.Error("Failed to update container IP in database",
261- "domain", domain,
262- "containerID", containerID,
263- "error", err)
264- } else {
265- log.Info("Updated container IP in database after live detection",
266- "domain", domain,
267- "containerID", containerID,
268- "old_ip", route.ContainerIP,
269- "new_ip", containerIP)
270- }
271- }(host, containerID, networkSettings.IPAddress, route.ContainerPort)
272- } else {
273- log.Debug("Container exists but connection still failed",
274- "domain", host,
275- "container_id", containerID,
276- "container_ip", route.ContainerIP,
277- "container_port", route.ContainerPort,
278- "error", err)
279- }
280- }
281- }
282- } else {
283- // Connection successful, close it
284- conn.Close()
285- }
286- }
287-
288- // Create the target URL
289- targetURL := &url.URL{
290- Scheme: "http", // Always use HTTP for internal connections
291- }
292 256
293- // Log protocol conversion if needed257+ return p.proxyRequest(c, route)
294- if route.Protocol == "https" {
295- log.Debug("Converting HTTPS external route to HTTP for internal connection",
296- "domain", host,
297- "containerID", route.ContainerID,
298- "externalProtocol", "https",
299- "internalProtocol", "http")
300- }
301-
302- // Format host properly for both IPv4 and IPv6
303- containerIP := route.ContainerIP
304- if strings.Contains(containerIP, ":") {
305- // IPv6 address needs to be wrapped in brackets
306- containerIP = "[" + containerIP + "]"
307- }
308- targetURL.Host = fmt.Sprintf("%s:%s", containerIP, route.ContainerPort)
309-
310- // Create a reverse proxy
311- proxy := httputil.NewSingleHostReverseProxy(targetURL)
312-
313- // Update headers to allow for SSL redirection
314- originalDirector := proxy.Director
315- proxy.Director = func(req *http.Request) {
316- originalDirector(req)
317- // Use the original protocol for forwarded proto header
318- forwardedProto := "http"
319- if route.Protocol == "https" {
320- forwardedProto = "https"
321- }
322- req.Header.Set("X-Forwarded-Proto", forwardedProto)
323- req.Header.Set("X-Forwarded-Host", host)
324- req.Header.Set("X-Forwarded-For", c.RealIP())
325- req.Header.Set("X-Real-IP", c.RealIP())
326-
327- // Debug information
328- log.Debug("Proxying request",
329- "host", host,
330- "target", targetURL.String(),
331- "path", req.URL.Path,
332- "clientIP", c.RealIP(),
333- "remoteAddr", req.RemoteAddr,
334- "originalProtocol", route.Protocol)
335-
336- // Log all headers for troubleshooting
337- log.Debug("Request headers for proxy",
338- "domain", host,
339- "X-Forwarded-For", req.Header.Get("X-Forwarded-For"),
340- "X-Real-IP", req.Header.Get("X-Real-IP"))
341- }
342-
343- // Add error handling
344- proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
345- log.Error("Proxy error",
346- "host", host,
347- "path", r.URL.Path,
348- "error", err)
349- c.String(http.StatusBadGateway, "Proxy Error: "+err.Error())
350- }
351-
352- // Serve the request
353- proxy.ServeHTTP(c.Response(), c.Request())
354- return nil
355 })258 })
356 259
357 // Add the handler to the HTTPS server260 // Add the handler to the HTTPS server
@@ -370,8 +273,19 @@ func (p *Proxy) configureRoutes() {
370 // Note: Regardless of the external protocol, all internal connections to containers use HTTP.273 // Note: Regardless of the external protocol, all internal connections to containers use HTTP.
371 // The HTTPS protocol is only used for external connections and certificates.274 // The HTTPS protocol is only used for external connections and certificates.
372 func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, protocol, path string) error {275 func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, protocol, path string) error {
373- // Begin a transaction276+ // Normalize domain name (lowercase, no trailing dot)
374- tx, err := p.app.GetDB().Begin()277+ domainName = strings.TrimSuffix(strings.ToLower(domainName), ".")
278+
279+ logger.Debug("Adding proxy route",
280+ "domain", domainName,
281+ "container_id", containerID,
282+ "container_ip", containerIP,
283+ "container_port", containerPort,
284+ "protocol", protocol,
285+ "path", path)
286+
287+ // Begin a transaction with retry
288+ tx, err := p.dbBeginWithRetry()
375 if err != nil {289 if err != nil {
376 return fmt.Errorf("failed to begin transaction: %w", err)290 return fmt.Errorf("failed to begin transaction: %w", err)
377 }291 }
@@ -391,9 +305,9 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr
391 err = tx.QueryRow("SELECT id FROM account LIMIT 1").Scan(&accountID)305 err = tx.QueryRow("SELECT id FROM account LIMIT 1").Scan(&accountID)
392 if err != nil {306 if err != nil {
393 if err == sql.ErrNoRows {307 if err == sql.ErrNoRows {
394- log.Warn("No account found when adding route, domains will have NULL account_id")308+ logger.Warn("No account found when adding route, domains will have NULL account_id")
395 } else {309 } else {
396- log.Warn("Error fetching account for route, continuing anyway", "error", err)310+ logger.Warn("Error fetching account for route, continuing anyway", "error", err)
397 }311 }
398 // Continue with NULL account_id if no account found312 // Continue with NULL account_id if no account found
399 }313 }
@@ -406,7 +320,7 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr
406 // Domain doesn't exist, create it320 // Domain doesn't exist, create it
407 domainID = proxyGenerateUUID()321 domainID = proxyGenerateUUID()
408 now := time.Now().Format(time.RFC3339)322 now := time.Now().Format(time.RFC3339)
409- _, err = tx.Exec(323+ _, err = txExecWithRetry(tx,
410 "INSERT INTO domain (id, name, account_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",324 "INSERT INTO domain (id, name, account_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
411 domainID, hostname, accountID, now, now,325 domainID, hostname, accountID, now, now,
412 )326 )
@@ -414,7 +328,7 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr
414 return fmt.Errorf("failed to insert domain: %w", err)328 return fmt.Errorf("failed to insert domain: %w", err)
415 }329 }
416 330
417- log.Debug("Added domain to database for Let's Encrypt",331+ logger.Debug("Added domain to database for Let's Encrypt",
418 "domain", hostname)332 "domain", hostname)
419 333
420 // Also add the subdomain.domain format to the database if this is a domain with components334 // Also add the subdomain.domain format to the database if this is a domain with components
@@ -432,16 +346,16 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr
432 if err == sql.ErrNoRows {346 if err == sql.ErrNoRows {
433 // Domain part doesn't exist, create it347 // Domain part doesn't exist, create it
434 domainPartID = proxyGenerateUUID()348 domainPartID = proxyGenerateUUID()
435- _, err = tx.Exec(349+ _, err = txExecWithRetry(tx,
436 "INSERT INTO domain (id, name, account_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",350 "INSERT INTO domain (id, name, account_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
437 domainPartID, domainPart, accountID, now, now,351 domainPartID, domainPart, accountID, now, now,
438 )352 )
439 if err != nil {353 if err != nil {
440- log.Warn("Failed to insert domain part, continuing anyway",354+ logger.Warn("Failed to insert domain part, continuing anyway",
441 "domain_part", domainPart,355 "domain_part", domainPart,
442 "error", err)356 "error", err)
443 } else {357 } else {
444- log.Debug("Added domain part to database for Let's Encrypt",358+ logger.Debug("Added domain part to database for Let's Encrypt",
445 "domain_part", domainPart,359 "domain_part", domainPart,
446 "subdomain", subdomain)360 "subdomain", subdomain)
447 }361 }
@@ -468,20 +382,20 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr
468 382
469 // Log more detailed information about the update383 // Log more detailed information about the update
470 if existingContainerID != containerID {384 if existingContainerID != containerID {
471- log.Info("Updating proxy route for recreated container",385+ logger.Info("Updating proxy route for recreated container",
472 "domain", hostname,386 "domain", hostname,
473 "old_container_id", existingContainerID,387 "old_container_id", existingContainerID,
474 "new_container_id", containerID)388 "new_container_id", containerID)
475 }389 }
476 390
477 if existingContainerPort != containerPort {391 if existingContainerPort != containerPort {
478- log.Info("Container port changed for domain",392+ logger.Info("Container port changed for domain",
479 "domain", hostname,393 "domain", hostname,
480 "old_port", existingContainerPort,394 "old_port", existingContainerPort,
481 "new_port", containerPort)395 "new_port", containerPort)
482 }396 }
483 397
484- _, err = tx.Exec(398+ _, err = txExecWithRetry(tx,
485 `UPDATE proxy_route SET 399 `UPDATE proxy_route SET
486 container_id = ?, 400 container_id = ?,
487 container_ip = ?, 401 container_ip = ?,
@@ -500,7 +414,7 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr
500 // Route doesn't exist, create it414 // Route doesn't exist, create it
501 routeID := proxyGenerateUUID()415 routeID := proxyGenerateUUID()
502 now := time.Now().Format(time.RFC3339)416 now := time.Now().Format(time.RFC3339)
503- _, err = tx.Exec(417+ _, err = txExecWithRetry(tx,
504 `INSERT INTO proxy_route (418 `INSERT INTO proxy_route (
505 id, domain_id, container_id, container_ip, container_port, 419 id, domain_id, container_id, container_ip, container_port,
506 protocol, path, active, created_at, updated_at420 protocol, path, active, created_at, updated_at
@@ -525,7 +439,7 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr
525 return fmt.Errorf("failed to reload routes: %w", err)439 return fmt.Errorf("failed to reload routes: %w", err)
526 }440 }
527 441
528- log.Info("Added proxy route",442+ logger.Info("Added proxy route",
529 "domain", hostname,443 "domain", hostname,
530 "containerIP", containerIP,444 "containerIP", containerIP,
531 "containerPort", containerPort,445 "containerPort", containerPort,
@@ -533,7 +447,7 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr
533 447
534 // Request a certificate if this is an HTTPS route448 // Request a certificate if this is an HTTPS route
535 if strings.ToLower(protocol) == "https" {449 if strings.ToLower(protocol) == "https" {
536- log.Info("Requesting Let's Encrypt certificate for new HTTPS route",450+ logger.Info("Requesting Let's Encrypt certificate for new HTTPS route",
537 "domain", hostname)451 "domain", hostname)
538 // Run in a goroutine to avoid blocking452 // Run in a goroutine to avoid blocking
539 go func(domain string) {453 go func(domain string) {
@@ -554,13 +468,13 @@ func (p *Proxy) RemoveRoute(domainName string) error {
554 // Prevent removing the admin domain468 // Prevent removing the admin domain
555 adminDomain := p.app.GetConfig().Http.FullDomain()469 adminDomain := p.app.GetConfig().Http.FullDomain()
556 if domainName == adminDomain {470 if domainName == adminDomain {
557- log.Warn("Attempt to remove admin domain route prevented",471+ logger.Warn("Attempt to remove admin domain route prevented",
558 "domain", domainName)472 "domain", domainName)
559 return fmt.Errorf("cannot remove admin domain route: %s", domainName)473 return fmt.Errorf("cannot remove admin domain route: %s", domainName)
560 }474 }
561 475
562- // Begin a transaction476+ // Begin a transaction with retry
563- tx, err := p.app.GetDB().Begin()477+ tx, err := p.dbBeginWithRetry()
564 if err != nil {478 if err != nil {
565 return fmt.Errorf("failed to begin transaction: %w", err)479 return fmt.Errorf("failed to begin transaction: %w", err)
566 }480 }
@@ -576,14 +490,14 @@ func (p *Proxy) RemoveRoute(domainName string) error {
576 return fmt.Errorf("failed to query domain: %w", err)490 return fmt.Errorf("failed to query domain: %w", err)
577 }491 }
578 492
579- // Delete the route493+ // Delete the route with retry
580- _, err = tx.Exec("DELETE FROM proxy_route WHERE domain_id = ?", domainID)494+ _, err = txExecWithRetry(tx, "DELETE FROM proxy_route WHERE domain_id = ?", domainID)
581 if err != nil {495 if err != nil {
582 return fmt.Errorf("failed to delete route: %w", err)496 return fmt.Errorf("failed to delete route: %w", err)
583 }497 }
584 498
585- // Delete the domain499+ // Delete the domain with retry
586- _, err = tx.Exec("DELETE FROM domain WHERE id = ?", domainID)500+ _, err = txExecWithRetry(tx, "DELETE FROM domain WHERE id = ?", domainID)
587 if err != nil {501 if err != nil {
588 return fmt.Errorf("failed to delete domain: %w", err)502 return fmt.Errorf("failed to delete domain: %w", err)
589 }503 }
@@ -598,7 +512,7 @@ func (p *Proxy) RemoveRoute(domainName string) error {
598 return fmt.Errorf("failed to reload routes: %w", err)512 return fmt.Errorf("failed to reload routes: %w", err)
599 }513 }
600 514
601- log.Info("Removed proxy route", "domain", domainName)515+ logger.Info("Removed proxy route", "domain", domainName)
602 return nil516 return nil
603 }517 }
604 518
@@ -618,10 +532,12 @@ func (p *Proxy) GetRoutes() map[string]*ProxyRouteInfo {
618 532
619 // Reload reloads the routes from the database and reconfigures the proxy533 // Reload reloads the routes from the database and reconfigures the proxy
620 func (p *Proxy) Reload() error {534 func (p *Proxy) Reload() error {
621- log.Info("Reloading proxy routes from database")535+ logger.Info("Reloading proxy routes from database")
622 536
623 // Get admin domain - this should be the full subdomain.domain format537 // Get admin domain - this should be the full subdomain.domain format
624 adminDomain := p.app.GetConfig().Http.FullDomain()538 adminDomain := p.app.GetConfig().Http.FullDomain()
539+ // Normalize the admin domain (lowercase, no trailing dot)
540+ adminDomain = strings.TrimSuffix(strings.ToLower(adminDomain), ".")
625 541
626 // Save existing admin route if it exists542 // Save existing admin route if it exists
627 p.mu.RLock()543 p.mu.RLock()
@@ -636,7 +552,7 @@ func (p *Proxy) Reload() error {
636 Path: route.Path,552 Path: route.Path,
637 Active: route.Active,553 Active: route.Active,
638 }554 }
639- log.Debug("Preserved admin domain route during reload", "domain", adminDomain)555+ logger.Debug("Preserved admin domain route during reload", "domain", adminDomain)
640 }556 }
641 p.mu.RUnlock()557 p.mu.RUnlock()
642 558
@@ -653,7 +569,7 @@ func (p *Proxy) Reload() error {
653 `)569 `)
654 if err != nil {570 if err != nil {
655 if err == sql.ErrNoRows {571 if err == sql.ErrNoRows {
656- log.Info("No active proxy routes found during reload")572+ logger.Info("No active proxy routes found during reload")
657 // Even if no routes found, proceed with the swap to clear any old routes573 // Even if no routes found, proceed with the swap to clear any old routes
658 } else {574 } else {
659 return fmt.Errorf("failed to query database during reload: %w", err)575 return fmt.Errorf("failed to query database during reload: %w", err)
@@ -669,6 +585,9 @@ func (p *Proxy) Reload() error {
669 return fmt.Errorf("failed to scan row during reload: %w", err)585 return fmt.Errorf("failed to scan row during reload: %w", err)
670 }586 }
671 587
588+ // Normalize domain (lowercase, no trailing dot)
589+ domain = strings.TrimSuffix(strings.ToLower(domain), ".")
590+
672 // Add the route to the new map591 // Add the route to the new map
673 newRoutes[domain] = &ProxyRouteInfo{592 newRoutes[domain] = &ProxyRouteInfo{
674 Domain: domain,593 Domain: domain,
@@ -680,7 +599,7 @@ func (p *Proxy) Reload() error {
680 Active: active,599 Active: active,
681 }600 }
682 601
683- log.Debug("Loaded route during reload",602+ logger.Debug("Loaded route during reload",
684 "domain", domain,603 "domain", domain,
685 "containerID", containerID,604 "containerID", containerID,
686 "containerIP", containerIP,605 "containerIP", containerIP,
@@ -694,7 +613,7 @@ func (p *Proxy) Reload() error {
694 // If admin domain wasn't loaded from DB but we had it in memory, restore it613 // If admin domain wasn't loaded from DB but we had it in memory, restore it
695 if !adminLoaded && adminRoute != nil {614 if !adminLoaded && adminRoute != nil {
696 newRoutes[adminDomain] = adminRoute615 newRoutes[adminDomain] = adminRoute
697- log.Warn("Admin domain route missing from database, restoring from memory",616+ logger.Warn("Admin domain route missing from database, restoring from memory",
698 "domain", adminDomain)617 "domain", adminDomain)
699 618
700 // Also try to add it back to the database619 // Also try to add it back to the database
@@ -708,17 +627,17 @@ func (p *Proxy) Reload() error {
708 )627 )
709 628
710 if err != nil {629 if err != nil {
711- log.Error("Failed to save admin route to database",630+ logger.Error("Failed to save admin route to database",
712 "error", err,631 "error", err,
713 "domain", adminDomain)632 "domain", adminDomain)
714 } else {633 } else {
715- log.Info("Re-added admin domain route to database", "domain", adminDomain)634+ logger.Info("Re-added admin domain route to database", "domain", adminDomain)
716 }635 }
717 }636 }
718 637
719 // If admin domain is still missing, recreate it638 // If admin domain is still missing, recreate it
720 if _, exists := newRoutes[adminDomain]; !exists {639 if _, exists := newRoutes[adminDomain]; !exists {
721- log.Warn("Admin domain route is missing, recreating it", "domain", adminDomain)640+ logger.Warn("Admin domain route is missing, recreating it", "domain", adminDomain)
722 641
723 // Auto-detect the Gordon container information642 // Auto-detect the Gordon container information
724 containerName := p.detectGordonContainer()643 containerName := p.detectGordonContainer()
@@ -753,11 +672,11 @@ func (p *Proxy) Reload() error {
753 )672 )
754 673
755 if err != nil {674 if err != nil {
756- log.Error("Failed to save recreated admin route to database",675+ logger.Error("Failed to save recreated admin route to database",
757 "error", err,676 "error", err,
758 "domain", adminDomain)677 "domain", adminDomain)
759 } else {678 } else {
760- log.Info("Recreated and saved admin domain route", "domain", adminDomain)679+ logger.Info("Recreated and saved admin domain route", "domain", adminDomain)
761 }680 }
762 }681 }
763 682
@@ -767,20 +686,17 @@ func (p *Proxy) Reload() error {
767 p.routes = newRoutes686 p.routes = newRoutes
768 p.mu.Unlock()687 p.mu.Unlock()
769 688
770- // Clean up any root domain conflict
771- p.CleanupRootDomainForAdmin()
772-
773 // Rebuild route configuration689 // Rebuild route configuration
774 p.configureRoutes()690 p.configureRoutes()
775 691
776 // Force recreation of the route handlers if servers are initialized692 // Force recreation of the route handlers if servers are initialized
777 if p.httpServer != nil {693 if p.httpServer != nil {
778- log.Debug("Refreshing HTTP routes")694+ logger.Debug("Refreshing HTTP routes")
779 p.httpServer.Routes()695 p.httpServer.Routes()
780 }696 }
781 697
782 if p.httpsServer != nil {698 if p.httpsServer != nil {
783- log.Debug("Refreshing HTTPS routes")699+ logger.Debug("Refreshing HTTPS routes")
784 p.httpsServer.Routes()700 p.httpsServer.Routes()
785 }701 }
786 702
@@ -807,7 +723,7 @@ func (p *Proxy) ForceUpdateRouteIP(domain string, newIP string) error {
807 if r, ok := p.routes[domain]; ok {723 if r, ok := p.routes[domain]; ok {
808 oldIP := r.ContainerIP724 oldIP := r.ContainerIP
809 r.ContainerIP = newIP725 r.ContainerIP = newIP
810- log.Info("Force updated route IP in-memory",726+ logger.Info("Force updated route IP in-memory",
811 "domain", domain,727 "domain", domain,
812 "old_ip", oldIP,728 "old_ip", oldIP,
813 "new_ip", newIP)729 "new_ip", newIP)
@@ -815,16 +731,27 @@ func (p *Proxy) ForceUpdateRouteIP(domain string, newIP string) error {
815 p.mu.Unlock()731 p.mu.Unlock()
816 732
817 // Next, update the database733 // Next, update the database
818- // Get the domain ID734+ // Get the domain ID with retry
819 var domainID string735 var domainID string
820- err := p.app.GetDB().QueryRow("SELECT id FROM domain WHERE name = ?", domain).Scan(&domainID)736+ rows, err := p.dbQueryWithRetry("SELECT id FROM domain WHERE name = ?", domain)
821 if err != nil {737 if err != nil {
822- return fmt.Errorf("failed to find domain ID: %w", err)738+ return fmt.Errorf("failed to query domain: %w", err)
823 }739 }
824 740
825- // Update the proxy_route record741+ if !rows.Next() {
742+ rows.Close()
743+ return fmt.Errorf("domain not found: %s", domain)
744+ }
745+
746+ if err := rows.Scan(&domainID); err != nil {
747+ rows.Close()
748+ return fmt.Errorf("failed to scan domain ID: %w", err)
749+ }
750+ rows.Close()
751+
752+ // Update the proxy_route record with retry
826 now := time.Now().Format(time.RFC3339)753 now := time.Now().Format(time.RFC3339)
827- result, err := p.app.GetDB().Exec(`754+ result, err := p.dbExecWithRetry(`
828 UPDATE proxy_route SET container_ip = ?, updated_at = ? 755 UPDATE proxy_route SET container_ip = ?, updated_at = ?
829 WHERE domain_id = ? AND container_id = ?`,756 WHERE domain_id = ? AND container_id = ?`,
830 newIP, now, domainID, containerID)757 newIP, now, domainID, containerID)
@@ -838,7 +765,7 @@ func (p *Proxy) ForceUpdateRouteIP(domain string, newIP string) error {
838 return fmt.Errorf("no rows affected when updating route in database")765 return fmt.Errorf("no rows affected when updating route in database")
839 }766 }
840 767
841- log.Info("Force updated route IP in database",768+ logger.Info("Force updated route IP in database",
842 "domain", domain,769 "domain", domain,
843 "container_id", containerID,770 "container_id", containerID,
844 "new_ip", newIP)771 "new_ip", newIP)
@@ -851,7 +778,7 @@ func (p *Proxy) FindRoutesByContainerName(containerName string) []string {
851 // Get all containers to map IDs to names778 // Get all containers to map IDs to names
852 containers, err := docker.ListRunningContainers()779 containers, err := docker.ListRunningContainers()
853 if err != nil {780 if err != nil {
854- log.Error("Failed to list containers when finding routes by container name", "error", err)781+ logger.Error("Failed to list containers when finding routes by container name", "error", err)
855 return nil782 return nil
856 }783 }
857 784
@@ -868,7 +795,7 @@ func (p *Proxy) FindRoutesByContainerName(containerName string) []string {
868 // This is the exact container ID we're looking for795 // This is the exact container ID we're looking for
869 targetContainerID := containerNameToID[containerName]796 targetContainerID := containerNameToID[containerName]
870 if targetContainerID == "" {797 if targetContainerID == "" {
871- log.Debug("Container name not found in running containers", "container_name", containerName)798+ logger.Debug("Container name not found in running containers", "container_name", containerName)
872 return nil799 return nil
873 }800 }
874 801
@@ -894,15 +821,15 @@ func (p *Proxy) FindRoutesByOldName(containerName string) map[string]*ProxyRoute
894 821
895 result := make(map[string]*ProxyRouteInfo)822 result := make(map[string]*ProxyRouteInfo)
896 823
897- // Query the database to find if any route might be associated with this container name824+ // Query the database with retry to find if any route might be associated with this container name
898- rows, err := p.app.GetDB().Query(`825+ rows, err := p.dbQueryWithRetry(`
899 SELECT d.name, pr.container_id, pr.container_ip, pr.container_port, pr.protocol, pr.path826 SELECT d.name, pr.container_id, pr.container_ip, pr.container_port, pr.protocol, pr.path
900 FROM proxy_route pr827 FROM proxy_route pr
901 JOIN domain d ON pr.domain_id = d.id828 JOIN domain d ON pr.domain_id = d.id
902 WHERE pr.active = 1829 WHERE pr.active = 1
903 `)830 `)
904 if err != nil {831 if err != nil {
905- log.Error("Failed to query database for routes by old name", "error", err)832+ logger.Error("Failed to query database for routes by old name", "error", err)
906 return result833 return result
907 }834 }
908 defer rows.Close()835 defer rows.Close()
@@ -911,7 +838,7 @@ func (p *Proxy) FindRoutesByOldName(containerName string) map[string]*ProxyRoute
911 for rows.Next() {838 for rows.Next() {
912 var domain, containerID, containerIP, containerPort, protocol, path string839 var domain, containerID, containerIP, containerPort, protocol, path string
913 if err := rows.Scan(&domain, &containerID, &containerIP, &containerPort, &protocol, &path); err != nil {840 if err := rows.Scan(&domain, &containerID, &containerIP, &containerPort, &protocol, &path); err != nil {
914- log.Error("Failed to scan row", "error", err)841+ logger.Error("Failed to scan row", "error", err)
915 continue842 continue
916 }843 }
917 844
@@ -940,8 +867,8 @@ func (p *Proxy) FindRoutesByOldName(containerName string) map[string]*ProxyRoute
940 }867 }
941 } else {868 } else {
942 // We found an existing container - if it has the same name, this is interesting869 // We found an existing container - if it has the same name, this is interesting
943- containerName = strings.TrimPrefix(name, "/")870+ existingContainerName := strings.TrimPrefix(name, "/")
944- if containerName == containerName {871+ if existingContainerName == containerName {
945 // This is an exact match - shouldn't happen if we're looking for recreated containers872 // This is an exact match - shouldn't happen if we're looking for recreated containers
946 // but include it anyway873 // but include it anyway
947 result[domain] = &ProxyRouteInfo{874 result[domain] = &ProxyRouteInfo{
@@ -963,7 +890,7 @@ func (p *Proxy) FindRoutesByOldName(containerName string) map[string]*ProxyRoute
963 // HandleContainerRecreation handles a container that has been recreated with the same name but a new ID890 // HandleContainerRecreation handles a container that has been recreated with the same name but a new ID
964 // It updates all relevant routes with the new container ID and IP891 // It updates all relevant routes with the new container ID and IP
965 func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerIP string) {892 func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerIP string) {
966- log.Info("Handling potential container recreation",893+ logger.Info("Handling potential container recreation",
967 "container_id", containerID,894 "container_id", containerID,
968 "container_name", containerName,895 "container_name", containerName,
969 "container_ip", containerIP)896 "container_ip", containerIP)
@@ -971,18 +898,21 @@ func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerI
971 // Get container info to check for gordon labels898 // Get container info to check for gordon labels
972 containerInfo, err := docker.GetContainerInfo(containerID)899 containerInfo, err := docker.GetContainerInfo(containerID)
973 if err != nil {900 if err != nil {
974- log.Warn("Failed to get container info", "error", err)901+ logger.Warn("Failed to get container info", "error", err)
975 return902 return
976 }903 }
977 904
978 // Check if this is a Gordon-managed container905 // Check if this is a Gordon-managed container
979 if _, exists := containerInfo.Config.Labels["gordon.managed"]; !exists {906 if _, exists := containerInfo.Config.Labels["gordon.managed"]; !exists {
980- log.Debug("Ignoring non-Gordon container", "container_name", containerName)907+ logger.Debug("Ignoring non-Gordon container", "container_name", containerName)
981 return908 return
982 }909 }
983 910
984 // First approach: check if there are any domains that should point to this container based on labels911 // First approach: check if there are any domains that should point to this container based on labels
985 if domainLabel, exists := containerInfo.Config.Labels["gordon.domain"]; exists && domainLabel != "" {912 if domainLabel, exists := containerInfo.Config.Labels["gordon.domain"]; exists && domainLabel != "" {
913+ // Normalize domain (lowercase, no trailing dot)
914+ domainLabel = strings.TrimSuffix(strings.ToLower(domainLabel), ".")
915+
986 // Check if there's already a route for this domain916 // Check if there's already a route for this domain
987 p.mu.RLock()917 p.mu.RLock()
988 route, domainExists := p.routes[domainLabel]918 route, domainExists := p.routes[domainLabel]
@@ -991,7 +921,7 @@ func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerI
991 if domainExists {921 if domainExists {
992 // Route exists - check if it's for a different container ID922 // Route exists - check if it's for a different container ID
993 if route.ContainerID != containerID {923 if route.ContainerID != containerID {
994- log.Info("Container was recreated with the same domain label",924+ logger.Info("Container was recreated with the same domain label",
995 "domain", domainLabel,925 "domain", domainLabel,
996 "old_container_id", route.ContainerID,926 "old_container_id", route.ContainerID,
997 "new_container_id", containerID)927 "new_container_id", containerID)
@@ -1012,9 +942,9 @@ func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerI
1012 // Update the route with the new container ID and IP942 // Update the route with the new container ID and IP
1013 err = p.AddRoute(domainLabel, containerID, containerIP, containerPort, protocol, route.Path)943 err = p.AddRoute(domainLabel, containerID, containerIP, containerPort, protocol, route.Path)
1014 if err != nil {944 if err != nil {
1015- log.Error("Failed to update route for recreated container", "error", err)945+ logger.Error("Failed to update route for recreated container", "error", err)
1016 } else {946 } else {
1017- log.Info("Updated route for recreated container",947+ logger.Info("Updated route for recreated container",
1018 "domain", domainLabel,948 "domain", domainLabel,
1019 "container_id", containerID,949 "container_id", containerID,
1020 "container_ip", containerIP)950 "container_ip", containerIP)
@@ -1022,7 +952,7 @@ func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerI
1022 } else {952 } else {
1023 // Same container ID but maybe IP changed953 // Same container ID but maybe IP changed
1024 if route.ContainerIP != containerIP {954 if route.ContainerIP != containerIP {
1025- log.Info("Container IP changed",955+ logger.Info("Container IP changed",
1026 "domain", domainLabel,956 "domain", domainLabel,
1027 "container_id", containerID,957 "container_id", containerID,
1028 "old_ip", route.ContainerIP,958 "old_ip", route.ContainerIP,
@@ -1031,13 +961,13 @@ func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerI
1031 // Update the IP961 // Update the IP
1032 err = p.ForceUpdateRouteIP(domainLabel, containerIP)962 err = p.ForceUpdateRouteIP(domainLabel, containerIP)
1033 if err != nil {963 if err != nil {
1034- log.Error("Failed to update IP for container", "error", err)964+ logger.Error("Failed to update IP for container", "error", err)
1035 }965 }
1036 }966 }
1037 }967 }
1038 } else {968 } else {
1039 // No existing route for this domain - might be a new container969 // No existing route for this domain - might be a new container
1040- log.Info("New container with domain label",970+ logger.Info("New container with domain label",
1041 "domain", domainLabel,971 "domain", domainLabel,
1042 "container_id", containerID)972 "container_id", containerID)
1043 973
@@ -1054,10 +984,16 @@ func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerI
1054 protocol = "https"984 protocol = "https"
1055 }985 }
1056 986
1057- // Add a new route987+ // Add a new route for this container
988+ logger.Info("Adding missing route for Gordon container",
989+ "domain", domainLabel,
990+ "container_id", containerID,
991+ "container_ip", containerIP,
992+ "container_port", containerPort)
993+
1058 err = p.AddRoute(domainLabel, containerID, containerIP, containerPort, protocol, "/")994 err = p.AddRoute(domainLabel, containerID, containerIP, containerPort, protocol, "/")
1059 if err != nil {995 if err != nil {
1060- log.Error("Failed to add route for new container", "error", err)996+ logger.Error("Failed to add missing route", "error", err)
1061 }997 }
1062 }998 }
1063 }999 }
@@ -1065,12 +1001,12 @@ func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerI
1065 // Second approach: check for routes with name-based matching that might need updating1001 // Second approach: check for routes with name-based matching that might need updating
1066 // This handles the case where the container name is used instead of explicit domain labels1002 // This handles the case where the container name is used instead of explicit domain labels
1067 if domainCheck := p.FindRoutesByOldName(containerName); len(domainCheck) > 0 {1003 if domainCheck := p.FindRoutesByOldName(containerName); len(domainCheck) > 0 {
1068- log.Info("Found potential routes for recreated container by name matching",1004+ logger.Info("Found potential routes for recreated container by name matching",
1069 "container_name", containerName,1005 "container_name", containerName,
1070 "domains_count", len(domainCheck))1006 "domains_count", len(domainCheck))
1071 1007
1072 for domain, oldRoute := range domainCheck {1008 for domain, oldRoute := range domainCheck {
1073- log.Info("Updating route for recreated container by name matching",1009+ logger.Info("Updating route for recreated container by name matching",
1074 "domain", domain,1010 "domain", domain,
1075 "old_container_id", oldRoute.ContainerID,1011 "old_container_id", oldRoute.ContainerID,
1076 "new_container_id", containerID)1012 "new_container_id", containerID)
@@ -1096,9 +1032,9 @@ func (p *Proxy) HandleContainerRecreation(containerID, containerName, containerI
1096 // Update the route1032 // Update the route
1097 err = p.AddRoute(domain, containerID, containerIP, containerPort, protocol, oldRoute.Path)1033 err = p.AddRoute(domain, containerID, containerIP, containerPort, protocol, oldRoute.Path)
1098 if err != nil {1034 if err != nil {
1099- log.Error("Failed to update route for recreated container", "error", err)1035+ logger.Error("Failed to update route for recreated container", "error", err)
1100 } else {1036 } else {
1101- log.Info("Updated route for recreated container",1037+ logger.Info("Updated route for recreated container",
1102 "domain", domain,1038 "domain", domain,
1103 "container_id", containerID,1039 "container_id", containerID,
1104 "container_ip", containerIP)1040 "container_ip", containerIP)
@@ -1116,243 +1052,39 @@ func (p *Proxy) StartContainerEventListener() error {
1116 return fmt.Errorf("container network name is not configured")1052 return fmt.Errorf("container network name is not configured")
1117 }1053 }
1118 1054
1119- log.Info("Starting container event listener", "network", networkName)1055+ logger.Info("Starting container event listener", "network", networkName)
1120 1056
1121 // Create a callback function to handle container events1057 // Create a callback function to handle container events
1122 containerEventCallback := func(containerID, containerName, containerIP string) {1058 containerEventCallback := func(containerID, containerName, containerIP string) {
1123 p.HandleContainerRecreation(containerID, containerName, containerIP)1059 p.HandleContainerRecreation(containerID, containerName, containerIP)
1124 }1060 }
1125 1061
1126- // Start the container event listener1062+ // Create a callback function to handle container stop events
1127- return docker.ListenForContainerEvents(networkName, containerEventCallback)1063+ containerStopCallback := func(containerID, containerName string) {
1128-}1064+ logger.Info("Container stopped or removed, marking associated routes as inactive",
1129-1065+ "container_id", containerID,
1130-// StartPeriodicIPVerification starts a goroutine that periodically checks container IPs1066+ "container_name", containerName)
1131-// and updates the routes if they have changed. This helps ensure routes always have
1132-// the correct IP address, even if the event listener missed something.
1133-func (p *Proxy) StartPeriodicIPVerification(interval time.Duration) {
1134- log.Info("Starting periodic IP verification", "interval", interval)
1135-
1136- // Run in a background goroutine
1137- go func() {
1138- // Use a ticker for periodic execution
1139- ticker := time.NewTicker(interval)
1140- defer ticker.Stop()
1141-
1142- for {
1143- select {
1144- case <-ticker.C:
1145- log.Debug("Running periodic IP verification")
1146- p.verifyAllContainerIPs()
1147-
1148- // Also check for any containers that should have routes but don't
1149- p.DiscoverMissingRoutes()
1150-
1151- // Log current active containers and routes for monitoring
1152- if log.GetLevel() <= log.DebugLevel {
1153- // Only log the container list in debug mode during periodic checks
1154- // to avoid overwhelming logs in production
1155- p.LogActiveContainersAndRoutes()
1156- }
1157- }
1158- }
1159- }()
1160-}
1161-
1162-// verifyAllContainerIPs checks all routes and verifies their container IPs
1163-func (p *Proxy) verifyAllContainerIPs() {
1164- p.mu.RLock()
1165- routes := make(map[string]*ProxyRouteInfo, len(p.routes))
1166- for domain, route := range p.routes {
1167- routes[domain] = &ProxyRouteInfo{
1168- Domain: route.Domain,
1169- ContainerID: route.ContainerID,
1170- ContainerIP: route.ContainerIP,
1171- ContainerPort: route.ContainerPort,
1172- Protocol: route.Protocol,
1173- Path: route.Path,
1174- Active: route.Active,
1175- }
1176- }
1177- p.mu.RUnlock()
1178-
1179- // Get network name from config
1180- networkName := p.app.GetConfig().ContainerEngine.Network
1181- if networkName == "" {
1182- log.Error("Container network name is not configured")
1183- return
1184- }
1185-
1186- // Check each route and verify the container IP
1187- routesUpdated := false
1188- updatedIPs := make(map[string]string) // cache to avoid redundant lookups
1189-
1190- // Track orphaned routes to mark as inactive
1191- orphanedRoutes := []string{}
1192-
1193- // Special handling for Gordon container - ensure its IP is correct first
1194- gordonContainerID := p.loadGordonContainerID()
1195- if gordonContainerID != "" {
1196- log.Debug("Verifying Gordon container IP first", "container_id", gordonContainerID)
1197-
1198- gordonIP, err := docker.GetContainerIPFromNetwork(gordonContainerID, networkName)
1199- if err == nil && gordonIP != "" {
1200- log.Debug("Found Gordon container IP", "ip", gordonIP)
1201- updatedIPs[gordonContainerID] = gordonIP
1202-
1203- // Double-check by direct container inspection to ensure maximum reliability
1204- containerInfo, inspectErr := docker.GetContainerInfo(gordonContainerID)
1205- if inspectErr == nil && containerInfo.NetworkSettings != nil {
1206- if network, exists := containerInfo.NetworkSettings.Networks[networkName]; exists &&
1207- network.IPAddress != "" && network.IPAddress != gordonIP {
1208- log.Info("Different IP found for Gordon container through inspection",
1209- "network_ip", gordonIP,
1210- "inspect_ip", network.IPAddress)
1211- updatedIPs[gordonContainerID] = network.IPAddress
1212- }
1213- }
1214- } else {
1215- log.Warn("Failed to get Gordon container IP from network",
1216- "container_id", gordonContainerID,
1217- "error", err)
1218- }
1219- }
1220-
1221- for domain, route := range routes {
1222- if !route.Active {
1223- continue
1224- }
1225-
1226- containerID := route.ContainerID
1227-
1228- // Skip verification if container doesn't exist anymore
1229- if !docker.ContainerExists(containerID) {
1230- log.Warn("Container no longer exists but route is active",
1231- "domain", domain,
1232- "container_id", containerID)
1233- orphanedRoutes = append(orphanedRoutes, domain)
1234- continue
1235- }
1236-
1237- // Check if this is for the Gordon container
1238- isGordonContainer := (containerID == gordonContainerID)
1239- if isGordonContainer {
1240- log.Debug("Verifying Gordon container route",
1241- "domain", domain,
1242- "container_id", containerID)
1243- }
1244-
1245- // Get the current container IP
1246- var currentIP string
1247- var err error
1248-
1249- // Check if we already looked up this container's IP
1250- if cachedIP, exists := updatedIPs[containerID]; exists {
1251- currentIP = cachedIP
1252- log.Debug("Using cached container IP",
1253- "container_id", containerID,
1254- "ip", currentIP)
1255- } else {
1256- // Otherwise, look it up
1257- currentIP, err = docker.GetContainerIPFromNetwork(containerID, networkName)
1258- if err != nil {
1259- log.Error("Failed to get container IP for verification",
1260- "domain", domain,
1261- "container_id", containerID,
1262- "error", err)
1263-
1264- // For Gordon container, try harder to get the IP
1265- if isGordonContainer {
1266- log.Info("Attempting alternative methods to get Gordon container IP")
1267-
1268- // Try direct container inspection as a fallback
1269- containerInfo, inspectErr := docker.GetContainerInfo(containerID)
1270- if inspectErr == nil && containerInfo.NetworkSettings != nil {
1271- // Check for our network specifically
1272- if network, exists := containerInfo.NetworkSettings.Networks[networkName]; exists && network.IPAddress != "" {
1273- currentIP = network.IPAddress
1274- log.Info("Successfully got Gordon container IP through inspection",
1275- "ip", currentIP)
1276- } else if containerInfo.NetworkSettings.IPAddress != "" {
1277- // Fallback to default network IP
1278- currentIP = containerInfo.NetworkSettings.IPAddress
1279- log.Info("Using default network IP for Gordon container",
1280- "ip", currentIP)
1281- }
1282- }
1283- }
1284 1067
1285- // If we still couldn't get the IP, skip this route1068+ // Find all domains associated with this container ID
1286- if currentIP == "" {1069+ var domains []string
1287- continue1070+ p.mu.RLock()
1288- }1071+ for domain, route := range p.routes {
1072+ if route.ContainerID == containerID {
1073+ domains = append(domains, domain)
1289 }1074 }
1290-
1291- // Cache the IP for future route checks
1292- updatedIPs[containerID] = currentIP
1293 }1075 }
1076+ p.mu.RUnlock()
1294 1077
1295- // Check if the IP has changed1078+ if len(domains) > 0 {
1296- if route.ContainerIP != currentIP {1079+ logger.Info("Found routes to mark as inactive", "container_id", containerID, "domains", domains)
1297- // Throttle updates for non-Gordon containers to prevent excessive updates1080+ p.markRoutesInactive(domains)
1298- if !isGordonContainer {
1299- containerName, _ := docker.GetContainerName(containerID)
1300- if containerName != "" {
1301- containerName = strings.TrimPrefix(containerName, "/")
1302- }
1303-
1304- log.Info("Container IP has changed - updating route",
1305- "domain", domain,
1306- "container_id", containerID,
1307- "container_name", containerName,
1308- "old_ip", route.ContainerIP,
1309- "new_ip", currentIP)
1310- } else {
1311- log.Info("Gordon container IP has changed - updating route",
1312- "admin_domain", domain,
1313- "gordon_container", containerID,
1314- "old_ip", route.ContainerIP,
1315- "new_ip", currentIP)
1316- }
1317-
1318- // Update the route in the database
1319- err = p.ForceUpdateRouteIP(domain, currentIP)
1320- if err != nil {
1321- log.Error("Failed to update route IP",
1322- "domain", domain,
1323- "error", err)
1324- } else {
1325- routesUpdated = true
1326-
1327- if isGordonContainer {
1328- log.Info("Successfully fixed admin domain route to point to Gordon container",
1329- "admin_domain", domain,
1330- "gordon_container", containerID)
1331- }
1332- }
1333 } else {1081 } else {
1334- log.Debug("Container IP verified and unchanged",1082+ logger.Debug("No routes found for stopped container", "container_id", containerID)
1335- "domain", domain,
1336- "container_id", containerID,
1337- "ip", currentIP)
1338 }1083 }
1339 }1084 }
1340 1085
1341- // Mark orphaned routes as inactive1086+ // Start the container event listener
1342- if len(orphanedRoutes) > 0 {1087+ return docker.ListenForContainerEvents(networkName, containerEventCallback, containerStopCallback)
1343- log.Info("Marking orphaned routes as inactive", "count", len(orphanedRoutes))
1344- p.markRoutesInactive(orphanedRoutes)
1345- routesUpdated = true
1346- }
1347-
1348- // Reload routes if any were updated
1349- if routesUpdated {
1350- log.Debug("Routes were updated - reloading proxy configuration")
1351- err := p.Reload()
1352- if err != nil {
1353- log.Error("Failed to reload routes after IP verification", "error", err)
1354- }
1355- }
1356 }1088 }
1357 1089
1358 // markRoutesInactive marks the specified routes as inactive in the database and memory1090 // markRoutesInactive marks the specified routes as inactive in the database and memory
@@ -1370,7 +1102,7 @@ func (p *Proxy) markRoutesInactive(domains []string) {
1370 if domain != adminDomain {1102 if domain != adminDomain {
1371 filteredDomains = append(filteredDomains, domain)1103 filteredDomains = append(filteredDomains, domain)
1372 } else {1104 } else {
1373- log.Warn("Prevented admin domain from being marked inactive",1105+ logger.Warn("Prevented admin domain from being marked inactive",
1374 "domain", adminDomain)1106 "domain", adminDomain)
1375 }1107 }
1376 }1108 }
@@ -1386,7 +1118,7 @@ func (p *Proxy) markRoutesInactive(domains []string) {
1386 // Begin a transaction1118 // Begin a transaction
1387 tx, err := p.app.GetDB().Begin()1119 tx, err := p.app.GetDB().Begin()
1388 if err != nil {1120 if err != nil {
1389- log.Error("Failed to begin transaction for marking routes inactive", "error", err)1121+ logger.Error("Failed to begin transaction for marking routes inactive", "error", err)
1390 return1122 return
1391 }1123 }
1392 defer tx.Rollback()1124 defer tx.Rollback()
@@ -1397,7 +1129,7 @@ func (p *Proxy) markRoutesInactive(domains []string) {
1397 var domainID string1129 var domainID string
1398 err := tx.QueryRow("SELECT id FROM domain WHERE name = ?", domain).Scan(&domainID)1130 err := tx.QueryRow("SELECT id FROM domain WHERE name = ?", domain).Scan(&domainID)
1399 if err != nil {1131 if err != nil {
1400- log.Error("Failed to get domain ID for inactive route", "domain", domain, "error", err)1132+ logger.Error("Failed to get domain ID for inactive route", "domain", domain, "error", err)
1401 continue1133 continue
1402 }1134 }
1403 1135
@@ -1405,7 +1137,7 @@ func (p *Proxy) markRoutesInactive(domains []string) {
1405 now := time.Now().Format(time.RFC3339)1137 now := time.Now().Format(time.RFC3339)
1406 _, err = tx.Exec("UPDATE proxy_route SET active = 0, updated_at = ? WHERE domain_id = ?", now, domainID)1138 _, err = tx.Exec("UPDATE proxy_route SET active = 0, updated_at = ? WHERE domain_id = ?", now, domainID)
1407 if err != nil {1139 if err != nil {
1408- log.Error("Failed to mark route as inactive in database", "domain", domain, "error", err)1140+ logger.Error("Failed to mark route as inactive in database", "domain", domain, "error", err)
1409 continue1141 continue
1410 }1142 }
1411 1143
@@ -1413,29 +1145,29 @@ func (p *Proxy) markRoutesInactive(domains []string) {
1413 p.mu.Lock()1145 p.mu.Lock()
1414 if route, exists := p.routes[domain]; exists {1146 if route, exists := p.routes[domain]; exists {
1415 route.Active = false1147 route.Active = false
1416- log.Info("Marked route as inactive", "domain", domain)1148+ logger.Info("Marked route as inactive", "domain", domain)
1417 }1149 }
1418 p.mu.Unlock()1150 p.mu.Unlock()
1419 }1151 }
1420 1152
1421 // Commit the transaction1153 // Commit the transaction
1422 if err := tx.Commit(); err != nil {1154 if err := tx.Commit(); err != nil {
1423- log.Error("Failed to commit transaction for marking routes inactive", "error", err)1155+ logger.Error("Failed to commit transaction for marking routes inactive", "error", err)
1424 return1156 return
1425 }1157 }
1426 1158
1427- log.Info("Successfully marked orphaned routes as inactive", "count", len(domains))1159+ logger.Info("Successfully marked orphaned routes as inactive", "count", len(domains))
1428 }1160 }
1429 1161
1430 // DiscoverMissingRoutes finds Gordon-managed containers that don't have routes yet1162 // DiscoverMissingRoutes finds Gordon-managed containers that don't have routes yet
1431 // and adds them to the routing table1163 // and adds them to the routing table
1432 func (p *Proxy) DiscoverMissingRoutes() {1164 func (p *Proxy) DiscoverMissingRoutes() {
1433- log.Info("Discovering containers that might need routes")1165+ logger.Info("Discovering containers that might need routes")
1434 1166
1435 // Get the network name from app config1167 // Get the network name from app config
1436 networkName := p.app.GetConfig().ContainerEngine.Network1168 networkName := p.app.GetConfig().ContainerEngine.Network
1437 if networkName == "" {1169 if networkName == "" {
1438- log.Error("Cannot discover missing routes, network name is not configured")1170+ logger.Error("Cannot discover missing routes, network name is not configured")
1439 return1171 return
1440 }1172 }
1441 1173
@@ -1445,7 +1177,7 @@ func (p *Proxy) DiscoverMissingRoutes() {
1445 // Get all containers in the network1177 // Get all containers in the network
1446 networkInfo, err := docker.GetNetworkInfo(networkName)1178 networkInfo, err := docker.GetNetworkInfo(networkName)
1447 if err != nil {1179 if err != nil {
1448- log.Error("Failed to get network info", "error", err)1180+ logger.Error("Failed to get network info", "error", err)
1449 return1181 return
1450 }1182 }
1451 1183
@@ -1473,7 +1205,7 @@ func (p *Proxy) DiscoverMissingRoutes() {
1473 // Get container info to check for Gordon labels1205 // Get container info to check for Gordon labels
1474 containerInfo, err := docker.GetContainerInfo(containerID)1206 containerInfo, err := docker.GetContainerInfo(containerID)
1475 if err != nil {1207 if err != nil {
1476- log.Debug("Failed to get container info",1208+ logger.Debug("Failed to get container info",
1477 "container_id", containerID,1209 "container_id", containerID,
1478 "error", err)1210 "error", err)
1479 continue1211 continue
@@ -1487,39 +1219,39 @@ func (p *Proxy) DiscoverMissingRoutes() {
1487 // Check if it has a domain label1219 // Check if it has a domain label
1488 domainLabel, hasDomain := containerInfo.Config.Labels["gordon.domain"]1220 domainLabel, hasDomain := containerInfo.Config.Labels["gordon.domain"]
1489 if !hasDomain || domainLabel == "" {1221 if !hasDomain || domainLabel == "" {
1490- log.Debug("Gordon container without domain label",1222+ logger.Debug("Gordon container without domain label",
1491 "container_id", containerID,1223 "container_id", containerID,
1492 "container_name", strings.TrimPrefix(containerInfo.Name, "/"))1224 "container_name", strings.TrimPrefix(containerInfo.Name, "/"))
1493 continue1225 continue
1494 }1226 }
1495 1227
1228+ // Normalize domain (lowercase, no trailing dot)
1229+ domainLabel = strings.TrimSuffix(strings.ToLower(domainLabel), ".")
1230+
1496 // Skip if this is the admin domain1231 // Skip if this is the admin domain
1497 if domainLabel == adminDomain {1232 if domainLabel == adminDomain {
1498- log.Debug("Skipping auto-discovery for admin domain",1233+ logger.Debug("Skipping auto-discovery for admin domain",
1499- "domain", domainLabel,1234+ "domain", domainLabel)
1500- "container_id", containerID)
1501 continue1235 continue
1502 }1236 }
1503 1237
1504- // Check if this domain already has a route (different container)1238+ // Check if there's already a route for this domain
1505 if _, exists := routes[domainLabel]; exists {1239 if _, exists := routes[domainLabel]; exists {
1506- log.Warn("Domain already has a route with a different container",1240+ logger.Debug("Route already exists for domain",
1507- "domain", domainLabel,1241+ "domain", domainLabel)
1508- "container_id", containerID,
1509- "existing_container_id", routes[domainLabel].ContainerID)
1510 continue1242 continue
1511 }1243 }
1512 1244
1513- // Extract container IP and port for the new route1245+ // Get the container IP from the network info
1514 containerIP := containerEndpoint.IPv4Address1246 containerIP := containerEndpoint.IPv4Address
1515 if containerIP == "" {1247 if containerIP == "" {
1516- containerIP = containerEndpoint.IPv6Address1248+ logger.Warn("Container has no IP address",
1249+ "container_id", containerID)
1250+ continue
1517 }1251 }
1518 1252
1519- // Extract just the IP part without subnet1253+ // Extract just the IP part from the CIDR notation (e.g., "192.168.1.2/24" → "192.168.1.2")
1520- if idx := strings.Index(containerIP, "/"); idx > 0 {1254+ containerIP = strings.Split(containerIP, "/")[0]
1521- containerIP = containerIP[:idx]
1522- }
1523 1255
1524 // Get the port to use for the proxy from the container labels1256 // Get the port to use for the proxy from the container labels
1525 containerPort := "80" // Default1257 containerPort := "80" // Default
@@ -1535,7 +1267,7 @@ func (p *Proxy) DiscoverMissingRoutes() {
1535 }1267 }
1536 1268
1537 // Add a new route for this container1269 // Add a new route for this container
1538- log.Info("Adding missing route for Gordon container",1270+ logger.Info("Adding missing route for Gordon container",
1539 "domain", domainLabel,1271 "domain", domainLabel,
1540 "container_id", containerID,1272 "container_id", containerID,
1541 "container_ip", containerIP,1273 "container_ip", containerIP,
@@ -1543,7 +1275,7 @@ func (p *Proxy) DiscoverMissingRoutes() {
1543 1275
1544 err = p.AddRoute(domainLabel, containerID, containerIP, containerPort, protocol, "/")1276 err = p.AddRoute(domainLabel, containerID, containerIP, containerPort, protocol, "/")
1545 if err != nil {1277 if err != nil {
1546- log.Error("Failed to add missing route", "error", err)1278+ logger.Error("Failed to add missing route", "error", err)
1547 }1279 }
1548 }1280 }
1549 1281
@@ -1554,15 +1286,12 @@ func (p *Proxy) DiscoverMissingRoutes() {
1554 count++1286 count++
1555 }1287 }
1556 p.mu.RUnlock()1288 p.mu.RUnlock()
1557- log.Debug("Routes after discovery", "count", count)1289+ logger.Debug("Routes after discovery", "count", count)
1558-
1559- // Clean up any root domain conflict
1560- p.CleanupRootDomainForAdmin()
1561 }1290 }
1562 1291
1563 // LogActiveContainersAndRoutes logs a list of active containers and their routes1292 // LogActiveContainersAndRoutes logs a list of active containers and their routes
1564 func (p *Proxy) LogActiveContainersAndRoutes() {1293 func (p *Proxy) LogActiveContainersAndRoutes() {
1565- log.Info("---- Listing active containers and routes ----")1294+ logger.Info("---- Listing active containers and routes ----")
1566 1295
1567 // Get the admin domain1296 // Get the admin domain
1568 adminDomain := p.app.GetConfig().Http.FullDomain()1297 adminDomain := p.app.GetConfig().Http.FullDomain()
@@ -1574,10 +1303,10 @@ func (p *Proxy) LogActiveContainersAndRoutes() {
1574 p.mu.RUnlock()1303 p.mu.RUnlock()
1575 1304
1576 if !adminExists {1305 if !adminExists {
1577- log.Warn("Admin domain is missing from routes", "domain", adminDomain)1306+ logger.Warn("Admin domain is missing from routes", "domain", adminDomain)
1578 }1307 }
1579 1308
1580- log.Info(fmt.Sprintf("Found %d active routes", activeRoutes))1309+ logger.Info(fmt.Sprintf("Found %d active routes", activeRoutes))
1581 1310
1582 // Create a map of container IDs to their information1311 // Create a map of container IDs to their information
1583 containerMap := make(map[string]struct {1312 containerMap := make(map[string]struct {
@@ -1630,7 +1359,7 @@ func (p *Proxy) LogActiveContainersAndRoutes() {
1630 if err == nil {1359 if err == nil {
1631 name, err := docker.GetContainerName(containerID)1360 name, err := docker.GetContainerName(containerID)
1632 if err == nil {1361 if err == nil {
1633- containerData.Name = strings.TrimPrefix(name, "/")1362+ containerData.Name = strings.TrimLeft(name, "/")
1634 containerData.Image = containerInfo.Config.Image1363 containerData.Image = containerInfo.Config.Image
1635 containerMap[containerID] = containerData1364 containerMap[containerID] = containerData
1636 }1365 }
@@ -1645,87 +1374,52 @@ func (p *Proxy) LogActiveContainersAndRoutes() {
1645 if len(containerID) >= 12 {1374 if len(containerID) >= 12 {
1646 containerIDShort = containerID[:12]1375 containerIDShort = containerID[:12]
1647 }1376 }
1648- log.Info(fmt.Sprintf("Container: %s (%s)", container.Name, containerIDShort),1377+ logger.Info(fmt.Sprintf("Container: %s (%s)", container.Name, containerIDShort),
1649 "ip", container.IP,1378 "ip", container.IP,
1650 "image", container.Image)1379 "image", container.Image)
1651 1380
1652 // Log each domain for this container1381 // Log each domain for this container
1653 for i, domain := range container.Domains {1382 for i, domain := range container.Domains {
1654- log.Info(fmt.Sprintf(" ├─ Domain: %s", domain),1383+ logger.Info(fmt.Sprintf(" ├─ Domain: %s", domain),
1655 "protocol", container.Protocols[i],1384 "protocol", container.Protocols[i],
1656 "port", container.Ports[i])1385 "port", container.Ports[i])
1657 }1386 }
1658 }1387 }
1659 1388
1660- log.Info("---- End of active containers and routes list ----")1389+ logger.Info("---- End of active containers and routes list ----")
1661 }1390 }
1662 1391
1663-// saveGordonContainerID saves the Gordon container ID to the config file1392+// LogGordonIdentity prints information about the Gordon container for debugging
1664-func (p *Proxy) saveGordonContainerID(containerID string) {1393+func (p *Proxy) LogGordonIdentity() {
1665- log.Debug("Saving Gordon container ID to config file", "container_id", containerID)1394+ logger.Debug("Logging Gordon identity information")
1666-
1667- // Update the config
1668- config := p.app.GetConfig()
1669- config.General.GordonContainerID = containerID
1670-
1671- // Save the config file
1672- err := config.SaveConfig()
1673- if err != nil {
1674- log.Error("Failed to save Gordon container ID to config file", "error", err)
1675- return
1676- }
1677-
1678- // Update the gordonContainerID field
1679- p.gordonContainerID = containerID
1680-
1681- log.Info("Successfully saved Gordon container ID to config file", "container_id", containerID)
1682-}
1683-
1684-// loadGordonContainerID loads the Gordon container ID from the config file
1685-func (p *Proxy) loadGordonContainerID() string {
1686- log.Debug("Loading Gordon container ID from config file")
1687-
1688- // Get the container ID from the config
1689- containerID := p.app.GetConfig().General.GordonContainerID
1690-
1691- if containerID == "" {
1692- log.Debug("No Gordon container ID found in config file")
1693- return ""
1694- }
1695-
1696- log.Info("Loaded Gordon container ID from config file", "container_id", containerID)
1697- return containerID
1698-}
1699-
1700-// detectGordonContainer attempts to find the Gordon container automatically
1701-func (p *Proxy) detectGordonContainer() string {
1702- log.Debug("Starting Gordon container detection")
1703 1395
1704- // First try to load the container ID from the config file1396+ // Print our container ID if available
1705- dbContainerID := p.loadGordonContainerID()1397+ if p.gordonContainerID != "" {
1706- if dbContainerID != "" {1398+ logger.Info("Gordon container ID", "container_id", p.gordonContainerID)
1707- p.gordonContainerID = dbContainerID
1708- log.Debug("Loaded Gordon container ID from config file", "container_id", p.gordonContainerID)
1709 1399
1710- // Verify the container still exists1400+ // Try to get the container name
1711 containers, err := docker.ListRunningContainers()1401 containers, err := docker.ListRunningContainers()
1712 if err == nil {1402 if err == nil {
1713 for _, container := range containers {1403 for _, container := range containers {
1714 if container.ID == p.gordonContainerID {1404 if container.ID == p.gordonContainerID {
1715 containerName := strings.TrimLeft(container.Names[0], "/")1405 containerName := strings.TrimLeft(container.Names[0], "/")
1716- log.Debug("Identified Gordon container using config file ID", "container_name", containerName)1406+ logger.Info("Gordon container name", "container_name", containerName)
1717- return containerName1407+ break
1718 }1408 }
1719 }1409 }
1720- // If the container ID is no longer found, log it but continue with other detection methods
1721- log.Warn("Stored Gordon container ID from config file no longer found in running containers", "container_id", p.gordonContainerID)
1722- p.gordonContainerID = "" // Reset since it's invalid
1723 }1410 }
1411+ } else {
1412+ logger.Warn("Gordon container ID not available - running outside container?")
1724 }1413 }
1414+}
1415+
1416+// detectGordonContainer attempts to find the Gordon container automatically
1417+func (p *Proxy) detectGordonContainer() string {
1418+ logger.Debug("Starting Gordon container detection")
1725 1419
1726- // If we have our container ID stored from initialization, use that next1420+ // If we have our container ID stored from initialization, use that first
1727 if p.gordonContainerID != "" {1421 if p.gordonContainerID != "" {
1728- log.Debug("Using stored Gordon container ID for identification", "container_id", p.gordonContainerID)1422+ logger.Debug("Using stored Gordon container ID for identification", "container_id", p.gordonContainerID)
1729 1423
1730 // Get the container name from the ID1424 // Get the container name from the ID
1731 containers, err := docker.ListRunningContainers()1425 containers, err := docker.ListRunningContainers()
@@ -1733,23 +1427,21 @@ func (p *Proxy) detectGordonContainer() string {
1733 for _, container := range containers {1427 for _, container := range containers {
1734 if container.ID == p.gordonContainerID {1428 if container.ID == p.gordonContainerID {
1735 containerName := strings.TrimLeft(container.Names[0], "/")1429 containerName := strings.TrimLeft(container.Names[0], "/")
1736- log.Info("Gordon identity established via exact ID match",1430+ logger.Info("Gordon identity established via exact ID match",
1737 "container_id", container.ID,1431 "container_id", container.ID,
1738 "container_name", containerName)1432 "container_name", containerName)
1739- p.gordonContainerID = container.ID
1740- // Save to config file for persistence
1741- p.saveGordonContainerID(container.ID)
1742 return containerName1433 return containerName
1743 }1434 }
1744 }1435 }
1745 // If the container ID is no longer found, log it but continue with other detection methods1436 // If the container ID is no longer found, log it but continue with other detection methods
1746- log.Warn("Stored Gordon container ID no longer found in running containers", "container_id", p.gordonContainerID)1437+ logger.Warn("Stored Gordon container ID no longer found in running containers", "container_id", p.gordonContainerID)
1438+ p.gordonContainerID = "" // Reset since it's invalid
1747 }1439 }
1748 }1440 }
1749 1441
1750 // Check if we're running inside a container1442 // Check if we're running inside a container
1751 if docker.IsRunningInContainer() {1443 if docker.IsRunningInContainer() {
1752- log.Debug("Detected we're running in a container")1444+ logger.Debug("Detected we're running in a container")
1753 1445
1754 // Get our hostname which should be the container ID in Docker/Podman1446 // Get our hostname which should be the container ID in Docker/Podman
1755 hostname := os.Getenv("HOSTNAME")1447 hostname := os.Getenv("HOSTNAME")
@@ -1762,12 +1454,10 @@ func (p *Proxy) detectGordonContainer() string {
1762 for _, container := range containers {1454 for _, container := range containers {
1763 if hostname == container.ID {1455 if hostname == container.ID {
1764 containerName := strings.TrimLeft(container.Names[0], "/")1456 containerName := strings.TrimLeft(container.Names[0], "/")
1765- log.Info("Gordon identity established via exact ID match",1457+ logger.Info("Gordon identity established via exact ID match",
1766 "container_id", container.ID,1458 "container_id", container.ID,
1767 "container_name", containerName)1459 "container_name", containerName)
1768 p.gordonContainerID = container.ID1460 p.gordonContainerID = container.ID
1769- // Save to config file for persistence
1770- p.saveGordonContainerID(container.ID)
1771 return containerName1461 return containerName
1772 }1462 }
1773 }1463 }
@@ -1776,18 +1466,16 @@ func (p *Proxy) detectGordonContainer() string {
1776 for _, container := range containers {1466 for _, container := range containers {
1777 if strings.HasPrefix(container.ID, hostname) || strings.HasPrefix(hostname, container.ID) {1467 if strings.HasPrefix(container.ID, hostname) || strings.HasPrefix(hostname, container.ID) {
1778 containerName := strings.TrimLeft(container.Names[0], "/")1468 containerName := strings.TrimLeft(container.Names[0], "/")
1779- log.Info("Gordon identity established via ID prefix match",1469+ logger.Info("Gordon identity established via ID prefix match",
1780 "container_id", container.ID,1470 "container_id", container.ID,
1781 "container_name", containerName,1471 "container_name", containerName,
1782 "hostname", hostname)1472 "hostname", hostname)
1783 p.gordonContainerID = container.ID1473 p.gordonContainerID = container.ID
1784- // Save to config file for persistence
1785- p.saveGordonContainerID(container.ID)
1786 return containerName1474 return containerName
1787 }1475 }
1788 }1476 }
1789 } else {1477 } else {
1790- log.Debug("Could not list containers when checking hostname", "error", err)1478+ logger.Debug("Could not list containers when checking hostname", "error", err)
1791 }1479 }
1792 }1480 }
1793 }1481 }
@@ -1798,10 +1486,8 @@ func (p *Proxy) detectGordonContainer() string {
1798 for _, container := range containers {1486 for _, container := range containers {
1799 containerName := strings.TrimLeft(container.Names[0], "/")1487 containerName := strings.TrimLeft(container.Names[0], "/")
1800 if containerName == "gordon" {1488 if containerName == "gordon" {
1801- log.Info("Found Gordon container with exact name 'gordon'", "container_id", container.ID)1489+ logger.Info("Found Gordon container with exact name 'gordon'", "container_id", container.ID)
1802 p.gordonContainerID = container.ID1490 p.gordonContainerID = container.ID
1803- // Save to config file for persistence
1804- p.saveGordonContainerID(container.ID)
1805 return containerName1491 return containerName
1806 }1492 }
1807 }1493 }
@@ -1810,173 +1496,157 @@ func (p *Proxy) detectGordonContainer() string {
1810 for _, container := range containers {1496 for _, container := range containers {
1811 containerName := strings.TrimLeft(container.Names[0], "/")1497 containerName := strings.TrimLeft(container.Names[0], "/")
1812 if strings.Contains(strings.ToLower(containerName), "gordon") {1498 if strings.Contains(strings.ToLower(containerName), "gordon") {
1813- log.Info("Found Gordon container with partial name match",1499+ logger.Info("Found Gordon container with partial name match",
1814 "container_name", containerName,1500 "container_name", containerName,
1815 "container_id", container.ID)1501 "container_id", container.ID)
1816 p.gordonContainerID = container.ID1502 p.gordonContainerID = container.ID
1817- // Save to config file for persistence
1818- p.saveGordonContainerID(container.ID)
1819 return containerName1503 return containerName
1820 }1504 }
1821 }1505 }
1822 }1506 }
1823 1507
1824 // Fallback to a default name1508 // Fallback to a default name
1825- log.Warn("Could not auto-detect Gordon container, using fallback name", "container_name", "gordon")1509+ logger.Warn("Could not auto-detect Gordon container, using fallback name", "container_name", "gordon")
1826 return "gordon"1510 return "gordon"
1827 }1511 }
1828 1512
1829-// CleanupRootDomainForAdmin checks if the root domain is pointing to the admin container1513+// proxyRequest handles the actual proxying of the request to the target container
1830-// and removes that route to avoid conflicts with other services using the root domain1514+func (p *Proxy) proxyRequest(c echo.Context, route *ProxyRouteInfo) error {
1831-func (p *Proxy) CleanupRootDomainForAdmin() {1515+ host := c.Request().Host
1832- // Get admin domain and root domain1516+
1833- adminDomain := p.app.GetConfig().Http.FullDomain()1517+ // For container routes, verify the IP is reachable before proxying
1834- rootDomain := p.app.GetConfig().Http.Domain1518+ if host != p.app.GetConfig().Http.FullDomain() {
1835-1519+ // Test TCP connection to container before proxying
1836- // Skip if root domain is the same as admin domain1520+ dialer := net.Dialer{Timeout: 500 * time.Millisecond}
1837- if rootDomain == adminDomain || rootDomain == "" {1521+ // Format address properly for both IPv4 and IPv6
1838- return1522+ target := route.ContainerIP
1839- }1523+ if strings.Contains(target, ":") {
1840-1524+ // IPv6 address needs to be wrapped in brackets
1841- // Check if root domain exists in memory routes1525+ target = "[" + target + "]"
1842- p.mu.RLock()
1843- rootRoute, rootExists := p.routes[rootDomain]
1844- adminRoute, adminExists := p.routes[adminDomain]
1845- p.mu.RUnlock()
1846-
1847- // If both root domain and admin domain routes exist,
1848- // check if they point to the same container ID (Gordon)
1849- if rootExists && adminExists {
1850- // Always ensure the admin domain points to the Gordon container
1851- if p.gordonContainerID != "" && adminRoute.ContainerID != p.gordonContainerID {
1852- log.Warn("Admin domain is pointing to the wrong container, fixing",
1853- "admin_domain", adminDomain,
1854- "current_container", adminRoute.ContainerID,
1855- "gordon_container", p.gordonContainerID)
1856-
1857- // Update the admin route to point to the Gordon container
1858- // Begin a transaction
1859- tx, err := p.app.GetDB().Begin()
1860- if err != nil {
1861- log.Error("Failed to begin transaction for admin domain fix", "error", err)
1862- return
1863- }
1864- defer tx.Rollback()
1865-
1866- // Get the domain ID for admin domain
1867- var domainID string
1868- err = tx.QueryRow("SELECT id FROM domain WHERE name = ?", adminDomain).Scan(&domainID)
1869- if err != nil {
1870- if err != sql.ErrNoRows {
1871- log.Error("Failed to query domain for admin domain fix", "error", err)
1872- }
1873- return
1874- }
1875-
1876- // Update the route for admin domain
1877- _, err = tx.Exec("UPDATE proxy_route SET container_id = ? WHERE domain_id = ?",
1878- p.gordonContainerID, domainID)
1879- if err != nil {
1880- log.Error("Failed to update admin domain route", "error", err)
1881- return
1882- }
1883-
1884- // Commit the transaction
1885- if err := tx.Commit(); err != nil {
1886- log.Error("Failed to commit transaction for admin domain fix", "error", err)
1887- return
1888- }
1889-
1890- // Update in-memory route
1891- p.mu.Lock()
1892- adminRoute.ContainerID = p.gordonContainerID
1893- p.mu.Unlock()
1894-
1895- log.Info("Successfully fixed admin domain route to point to Gordon container",
1896- "admin_domain", adminDomain,
1897- "gordon_container", p.gordonContainerID)
1898 }1526 }
1899-1527+ conn, err := dialer.Dial("tcp", net.JoinHostPort(target, route.ContainerPort))
1900- // Now handle the root domain conflict1528+ if err != nil {
1901- if rootRoute.ContainerID == adminRoute.ContainerID {1529+ // Connection failed, check if container exists but has a new IP
1902- log.Warn("Found root domain pointing to admin container, removing to avoid conflicts",1530+ containerID := route.ContainerID
1903- "root_domain", rootDomain,1531+ if containerID != "" {
1904- "admin_domain", adminDomain,1532+ // Try to get container info and check if it has a different IP
1905- "container", rootRoute.ContainerID)1533+ containerInfo, err := docker.GetContainerInfo(containerID)
1906-1534+ if err == nil && containerInfo.NetworkSettings != nil {
1907- // Begin a transaction1535+ // Check if container has a different IP
1908- tx, err := p.app.GetDB().Begin()1536+ networkName := p.app.GetConfig().ContainerEngine.Network
1909- if err != nil {1537+ if networkSettings, exists := containerInfo.NetworkSettings.Networks[networkName]; exists &&
1910- log.Error("Failed to begin transaction for root domain cleanup", "error", err)1538+ networkSettings.IPAddress != "" && networkSettings.IPAddress != route.ContainerIP {
1911- return1539+ // Found container with different IP, update the route and use new IP for this request
1912- }1540+ logger.Warn("Container IP mismatch detected, using live IP instead of cached value",
1913- defer tx.Rollback()1541+ "domain", host,
1914-1542+ "cached_ip", route.ContainerIP,
1915- // Get the domain ID for root domain1543+ "live_ip", networkSettings.IPAddress)
1916- var domainID string1544+
1917- err = tx.QueryRow("SELECT id FROM domain WHERE name = ?", rootDomain).Scan(&domainID)1545+ // Update the in-memory route for this request
1918- if err != nil {1546+ route.ContainerIP = networkSettings.IPAddress
1919- if err != sql.ErrNoRows {1547+
1920- log.Error("Failed to query domain for root domain cleanup", "error", err)1548+ // Update the database in background to persist the change
1549+ go func(domain, containerID, containerIP, containerPort string) {
1550+ // Get the domain ID
1551+ var domainID string
1552+ err := p.app.GetDB().QueryRow("SELECT id FROM domain WHERE name = ?", domain).Scan(&domainID)
1553+ if err != nil {
1554+ logger.Error("Failed to find domain ID for IP update", "domain", domain, "error", err)
1555+ return
1556+ }
1557+
1558+ // Update the proxy_route record
1559+ now := time.Now().Format(time.RFC3339)
1560+ _, err = p.app.GetDB().Exec(`
1561+ UPDATE proxy_route SET container_ip = ?, updated_at = ?
1562+ WHERE domain_id = ? AND container_id = ?`,
1563+ containerIP, now, domainID, containerID)
1564+
1565+ if err != nil {
1566+ logger.Error("Failed to update container IP in database",
1567+ "domain", domain,
1568+ "containerID", containerID,
1569+ "error", err)
1570+ } else {
1571+ logger.Info("Updated container IP in database after live detection",
1572+ "domain", domain,
1573+ "containerID", containerID,
1574+ "old_ip", route.ContainerIP,
1575+ "new_ip", containerIP)
1576+ }
1577+ }(host, containerID, networkSettings.IPAddress, route.ContainerPort)
1578+ } else {
1579+ logger.Debug("Container exists but connection still failed",
1580+ "domain", host,
1581+ "container_id", containerID,
1582+ "container_ip", route.ContainerIP,
1583+ "container_port", route.ContainerPort,
1584+ "error", err)
1585+ }
1921 }1586 }
1922- return
1923 }1587 }
1588+ } else {
1589+ // Connection successful, close it
1590+ conn.Close()
1591+ }
1592+ }
1924 1593
1925- // Delete the route for root domain1594+ // Create the target URL
1926- _, err = tx.Exec("DELETE FROM proxy_route WHERE domain_id = ?", domainID)1595+ targetURL := &url.URL{
1927- if err != nil {1596+ Scheme: "http", // Always use HTTP for internal connections
1928- log.Error("Failed to delete root domain route", "error", err)1597+ }
1929- return
1930- }
1931 1598
1932- // We don't delete the domain itself from the database to avoid affecting certificate management1599+ // Log protocol conversion if needed
1600+ if route.Protocol == "https" {
1601+ logger.Debug("Converting HTTPS external route to HTTP for internal connection",
1602+ "domain", host,
1603+ "containerID", route.ContainerID,
1604+ "externalProtocol", "https",
1605+ "internalProtocol", "http")
1606+ }
1933 1607
1934- // Commit the transaction1608+ // Format host properly for both IPv4 and IPv6
1935- if err := tx.Commit(); err != nil {1609+ containerIP := route.ContainerIP
1936- log.Error("Failed to commit transaction for root domain cleanup", "error", err)1610+ if strings.Contains(containerIP, ":") {
1937- return1611+ // IPv6 address needs to be wrapped in brackets
1938- }1612+ containerIP = "[" + containerIP + "]"
1613+ }
1614+ targetURL.Host = fmt.Sprintf("%s:%s", containerIP, route.ContainerPort)
1939 1615
1940- // Remove root domain from in-memory routes1616+ // Create a reverse proxy
1941- p.mu.Lock()1617+ proxy := httputil.NewSingleHostReverseProxy(targetURL)
1942- delete(p.routes, rootDomain)
1943- p.mu.Unlock()
1944 1618
1945- log.Info("Successfully removed conflicting root domain route",1619+ // Update headers to allow for SSL redirection
1946- "root_domain", rootDomain,1620+ originalDirector := proxy.Director
1947- "admin_domain", adminDomain)1621+ proxy.Director = func(req *http.Request) {
1622+ originalDirector(req)
1623+ // Use the original protocol for forwarded proto header
1624+ forwardedProto := "http"
1625+ if route.Protocol == "https" {
1626+ forwardedProto = "https"
1948 }1627 }
1949- }1628+ req.Header.Set("X-Forwarded-Proto", forwardedProto)
1950-}1629+ req.Header.Set("X-Forwarded-Host", host)
1630+ req.Header.Set("X-Forwarded-For", c.RealIP())
1631+ req.Header.Set("X-Real-IP", c.RealIP())
1951 1632
1952-// LogGordonIdentity prints information about the Gordon container for debugging1633+ // Log all headers for troubleshooting
1953-func (p *Proxy) LogGordonIdentity() {1634+ logger.Debug("Request headers for proxy",
1954- // If we don't have a container ID yet, try to load it from the database first1635+ "domain", host,
1955- if p.gordonContainerID == "" {1636+ "X-Forwarded-For", req.Header.Get("X-Forwarded-For"),
1956- dbContainerID := p.loadGordonContainerID()1637+ "X-Real-IP", req.Header.Get("X-Real-IP"))
1957- if dbContainerID != "" {
1958- p.gordonContainerID = dbContainerID
1959- log.Info("Loaded Gordon container ID from config file during identity check", "container_id", p.gordonContainerID)
1960- }
1961 }1638 }
1962 1639
1963- // If we still don't have a container ID, try to detect it1640+ // Add error handling
1964- if p.gordonContainerID == "" {1641+ proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
1965- // This will attempt to detect the container and set p.gordonContainerID1642+ logger.Error("Proxy error",
1966- containerName := p.detectGordonContainer()1643+ "host", host,
1967- if p.gordonContainerID != "" {1644+ "path", r.URL.Path,
1968- log.Info("Detected Gordon container during identity check",1645+ "error", err)
1969- "container_id", p.gordonContainerID,1646+ c.String(http.StatusBadGateway, "Proxy Error: "+err.Error())
1970- "container_name", containerName)
1971- }
1972 }1647 }
1973 1648
1974- // Now print our container ID for debugging1649+ // Serve the request
1975- if p.gordonContainerID != "" {1650+ proxy.ServeHTTP(c.Response(), c.Request())
1976- log.Info("Gordon container identity",1651+ return nil
1977- "container_id", p.gordonContainerID,
1978- "container_name", p.detectGordonContainer())
1979- } else {
1980- log.Warn("Gordon container ID not available - running outside container?")
1981- }
1982 }1652 }
M internal/proxy/server.go +70 -77
@@ -5,11 +5,10 @@ import (
55 "crypto/tls"
66 "fmt"
77 "net/http"
8- "os"
98 "time"
109
1110 "github.com/bnema/gordon/pkg/docker"
12- "github.com/charmbracelet/log"
11+ "github.com/bnema/gordon/pkg/logger"
1312 "github.com/labstack/echo/v4"
1413 "golang.org/x/crypto/acme"
1514 )
@@ -29,12 +28,11 @@ func (p *Proxy) Start() error {
2928
3029 // Scan Gordon network containers and check for certificates
3130 if err := p.scanContainersAndCheckCertificates(); err != nil {
32- log.Warn("Failed to scan containers for certificate checks", "error", err)
31+ logger.Warn("Failed to scan containers for certificate checks", "error", err)
3332 }
3433
3534 // Run an immediate route verification to ensure all routes are valid on startup
36- log.Info("Performing initial route verification on startup")
37- p.verifyAllContainerIPs()
35+ logger.Info("Performing initial route verification on startup")
3836
3937 // Discover any containers that should have routes but don't
4038 p.DiscoverMissingRoutes()
@@ -44,20 +42,16 @@ func (p *Proxy) Start() error {
4442
4543 // Start the container event listener for real-time updates
4644 if err := p.StartContainerEventListener(); err != nil {
47- log.Warn("Failed to start container event listener", "error", err)
45+ logger.Warn("Failed to start container event listener", "error", err)
4846 // Continue anyway, this is non-critical
4947 }
5048
51- // Start periodic IP verification (every 5 minutes)
52- // This ensures we catch any IP changes that the event listener might miss
53- p.StartPeriodicIPVerification(5 * time.Minute)
54-
5549 // Check if there might be port conflicts with the main server
5650 mainServerPort := p.app.GetConfig().Http.Port
5751
5852 // Check HTTP port conflict
5953 if p.config.HttpPort == mainServerPort {
60- log.Warn("HTTP port for reverse proxy conflicts with main server port",
54+ logger.Warn("HTTP port for reverse proxy conflicts with main server port",
6155 "port", p.config.HttpPort,
6256 "solution", "reverse proxy HTTP server will be disabled")
6357 return nil
@@ -65,7 +59,7 @@ func (p *Proxy) Start() error {
6559
6660 // Check HTTPS port conflict (less common, but still possible)
6761 if p.config.Port == mainServerPort {
68- log.Warn("HTTPS port for reverse proxy conflicts with main server port",
62+ logger.Warn("HTTPS port for reverse proxy conflicts with main server port",
6963 "port", p.config.Port,
7064 "solution", "reverse proxy HTTPS server will be disabled")
7165 return nil
@@ -74,32 +68,38 @@ func (p *Proxy) Start() error {
7468 // Add a special route for the admin domain (Gordon itself)
7569 adminDomain := p.app.GetConfig().Http.FullDomain()
7670
77- // In a container environment, we need to use the host container IP
78- // instead of 127.0.0.1 because each container has its own localhost
79- containerIP := "localhost" // Default to localhost for most reliable connectivity
80-
81- // Fall back options for container IP
82- if os.Getenv("GORDON_ADMIN_HOST") != "" {
83- // Allow explicit configuration via env var
84- containerIP = os.Getenv("GORDON_ADMIN_HOST")
85- log.Debug("Using admin host from environment variable",
86- "host", containerIP)
87- } else if os.Getenv("HOSTNAME") != "" {
88- // Use container's own hostname as they're on the same network
89- containerIP = os.Getenv("HOSTNAME")
90- log.Debug("Using container hostname for admin routing",
91- "hostname", containerIP)
71+ // Auto-detect the Gordon container name and ID
72+ containerName := p.detectGordonContainer()
73+
74+ // Get the network name from config
75+ networkName := p.app.GetConfig().ContainerEngine.Network
76+
77+ // Initialize container IP
78+ var containerIP string
79+
80+ // Try to get the actual container IP from the network
81+ if p.gordonContainerID == "" {
82+ return fmt.Errorf("failed to detect Gordon container ID, cannot add admin route")
9283 }
9384
94- // Don't test connections yet - the server isn't running
95- // We'll add the admin route with the default host for now
96- // and test connections later with TestAdminConnectionLater
85+ // Get the IP address from the container's network
86+ ip, err := docker.GetContainerIPFromNetwork(p.gordonContainerID, networkName)
87+ if err != nil || ip == "" {
88+ logger.Error("Failed to get Gordon container IP from network",
89+ "container_id", p.gordonContainerID,
90+ "network", networkName,
91+ "error", err)
92+ return fmt.Errorf("failed to get Gordon container IP from network %s: %w", networkName, err)
93+ }
9794
98- // Auto-detect the Gordon container name
99- containerName := p.detectGordonContainer()
95+ containerIP = ip
96+ logger.Info("Using Gordon container IP from network",
97+ "container_id", p.gordonContainerID,
98+ "network", networkName,
99+ "ip", containerIP)
100100
101101 // Check if admin route exists in database and create/update it
102- err := p.AddRoute(
102+ err = p.AddRoute(
103103 adminDomain,
104104 containerName,
105105 containerIP,
@@ -109,11 +109,11 @@ func (p *Proxy) Start() error {
109109 )
110110
111111 if err != nil {
112- log.Error("Failed to add admin route to database",
112+ logger.Error("Failed to add admin route to database",
113113 "error", err,
114114 "domain", adminDomain)
115115 } else {
116- log.Info("Ensured admin domain route is in database",
116+ logger.Info("Ensured admin domain route is in database",
117117 "domain", adminDomain,
118118 "container", containerName,
119119 "target", fmt.Sprintf("http://%s:%s", containerIP, p.app.GetConfig().Http.Port))
@@ -131,12 +131,12 @@ func (p *Proxy) Start() error {
131131 Path: "/",
132132 Active: true,
133133 }
134- log.Info("Added special route for admin domain",
134+ logger.Info("Added special route for admin domain",
135135 "domain", adminDomain,
136136 "container", containerName,
137137 "target", fmt.Sprintf("http://%s:%s", containerIP, p.app.GetConfig().Http.Port))
138138 } else {
139- log.Debug("Admin domain route already exists",
139+ logger.Debug("Admin domain route already exists",
140140 "domain", adminDomain,
141141 "route", fmt.Sprintf("%s://%s:%s",
142142 p.routes[adminDomain].Protocol,
@@ -145,13 +145,6 @@ func (p *Proxy) Start() error {
145145 }
146146 p.mu.Unlock()
147147
148- // We no longer automatically add the root domain redirect to avoid conflicts
149- // with other containers that might be using the root domain.
150- // If root domain redirection is needed, it should be explicitly configured.
151-
152- // Clean up any existing root domain conflict
153- p.CleanupRootDomainForAdmin()
154-
155148 // Set up middleware
156149 p.setupMiddleware()
157150
@@ -170,7 +163,7 @@ func (p *Proxy) Start() error {
170163 GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
171164 // Log SNI information in debug mode
172165 if hello.ServerName != "" {
173- log.Debug("TLS handshake with SNI",
166+ logger.Debug("TLS handshake with SNI",
174167 "server_name", hello.ServerName)
175168 }
176169
@@ -179,7 +172,7 @@ func (p *Proxy) Start() error {
179172
180173 // If we can't get a certificate and we have a fallback, use it for the admin domain
181174 if (err != nil || cert == nil) && hello.ServerName == adminDomain && p.fallbackCert != nil {
182- log.Debug("Using fallback certificate for admin domain",
175+ logger.Debug("Using fallback certificate for admin domain",
183176 "domain", adminDomain,
184177 "error", err)
185178 return p.fallbackCert, nil
@@ -212,18 +205,18 @@ func (p *Proxy) Start() error {
212205
213206 // Start HTTPS server
214207 go func() {
215- log.Info("Starting HTTPS server", "address", httpsServer.Addr)
208+ logger.Info("Starting HTTPS server", "address", httpsServer.Addr)
216209 // Using empty strings for cert and key files since we're using GetCertificate
217210 if err := httpsServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
218- log.Error("HTTPS server failed", "error", err)
211+ logger.Error("HTTPS server failed", "error", err)
219212 }
220213 }()
221214
222215 // Start HTTP server
223216 go func() {
224- log.Info("Starting HTTP server", "address", httpServer.Addr)
217+ logger.Info("Starting HTTP server", "address", httpServer.Addr)
225218 if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
226- log.Error("HTTP server failed", "error", err)
219+ logger.Error("HTTP server failed", "error", err)
227220 }
228221 }()
229222
@@ -235,7 +228,7 @@ func (p *Proxy) Start() error {
235228
236229 // Stop gracefully shuts down the proxy server
237230 func (p *Proxy) Stop() error {
238- log.Info("Stopping reverse proxy")
231+ logger.Info("Stopping reverse proxy")
239232
240233 // Run any rate limiter cleanup functions
241234 for _, cleanup := range rateLimiterCleanup {
@@ -247,7 +240,7 @@ func (p *Proxy) Stop() error {
247240
248241 // Skip the rest if the server was never started
249242 if !p.serverStarted {
250- log.Debug("Proxy server was never started, nothing to stop")
243+ logger.Debug("Proxy server was never started, nothing to stop")
251244 return nil
252245 }
253246
@@ -262,18 +255,18 @@ func (p *Proxy) Stop() error {
262255 // Shutdown HTTP server if it's running
263256 if p.httpServer != nil {
264257 if err := p.httpServer.Shutdown(ctx); err != nil {
265- log.Error("Failed to gracefully shutdown HTTP server", "error", err)
258+ logger.Error("Failed to gracefully shutdown HTTP server", "error", err)
266259 }
267260 }
268261
269262 // Shutdown HTTPS server if it's running
270263 if p.httpsServer != nil {
271264 if err := p.httpsServer.Shutdown(ctx); err != nil {
272- log.Error("Failed to gracefully shutdown HTTPS server", "error", err)
265+ logger.Error("Failed to gracefully shutdown HTTPS server", "error", err)
273266 }
274267 }
275268
276- log.Info("Reverse proxy stopped")
269+ logger.Info("Reverse proxy stopped")
277270 return nil
278271 }
279272
@@ -282,7 +275,7 @@ func (p *Proxy) testAdminConnection(defaultHost string, port string) string {
282275 adminPath := p.app.GetConfig().Admin.Path
283276 url := fmt.Sprintf("http://%s:%s%s", defaultHost, port, adminPath)
284277
285- log.Debug("Testing admin connection", "url", url)
278+ logger.Debug("Testing admin connection", "url", url)
286279
287280 // Try connecting using the provided default host
288281 client := &http.Client{
@@ -298,7 +291,7 @@ func (p *Proxy) testAdminConnection(defaultHost string, port string) string {
298291 resp, err := client.Get(url)
299292 if err == nil {
300293 resp.Body.Close()
301- log.Info("Admin connection successful", "host", defaultHost)
294+ logger.Info("Admin connection successful", "host", defaultHost)
302295 return defaultHost
303296 }
304297
@@ -316,17 +309,17 @@ func (p *Proxy) testAdminConnection(defaultHost string, port string) string {
316309 }
317310
318311 url = fmt.Sprintf("http://%s:%s%s", host, port, adminPath)
319- log.Debug("Trying alternative host", "url", url)
312+ logger.Debug("Trying alternative host", "url", url)
320313
321314 resp, err := client.Get(url)
322315 if err == nil {
323316 resp.Body.Close()
324- log.Info("Found working admin connection", "host", host)
317+ logger.Info("Found working admin connection", "host", host)
325318 return host
326319 }
327320 }
328321
329- log.Warn("Could not establish admin connection on any tested host", "fallback", defaultHost)
322+ logger.Warn("Could not establish admin connection on any tested host", "fallback", defaultHost)
330323 return defaultHost
331324 }
332325
@@ -335,7 +328,7 @@ func (p *Proxy) TestAdminConnectionLater() {
335328 // Wait for servers to start
336329 time.Sleep(2 * time.Second)
337330
338- log.Debug("Testing admin connections after server startup")
331+ logger.Debug("Testing admin connections after server startup")
339332
340333 // Get domain and current route
341334 adminDomain := p.app.GetConfig().Http.FullDomain()
@@ -345,7 +338,7 @@ func (p *Proxy) TestAdminConnectionLater() {
345338 p.mu.RUnlock()
346339
347340 if !exists {
348- log.Warn("Admin route not found")
341+ logger.Warn("Admin route not found")
349342 return
350343 }
351344
@@ -355,7 +348,7 @@ func (p *Proxy) TestAdminConnectionLater() {
355348 p.mu.Lock()
356349 if r, ok := p.routes[adminDomain]; ok {
357350 r.ContainerIP = testedIP
358- log.Info("Updated admin route with working connection",
351+ logger.Info("Updated admin route with working connection",
359352 "domain", adminDomain,
360353 "ip", testedIP)
361354 }
@@ -384,10 +377,10 @@ func (p *Proxy) checkExternalPortAccess() {
384377 httpURL := fmt.Sprintf("http://%s/.well-known/acme-challenge/test", adminDomain)
385378 _, httpErr := client.Get(httpURL)
386379 if httpErr != nil {
387- log.Warn("External HTTP port 80 might not be accessible",
380+ logger.Warn("External HTTP port 80 might not be accessible",
388381 "error", httpErr.Error())
389382 } else {
390- log.Info("External HTTP port 80 is accessible")
383+ logger.Info("External HTTP port 80 is accessible")
391384 }
392385
393386 // Test HTTPS port 443
@@ -406,10 +399,10 @@ func (p *Proxy) checkExternalPortAccess() {
406399 httpsURL := fmt.Sprintf("https://%s/", adminDomain)
407400 _, httpsErr := httpsClient.Get(httpsURL)
408401 if httpsErr != nil {
409- log.Warn("External HTTPS port 443 might not be accessible",
402+ logger.Warn("External HTTPS port 443 might not be accessible",
410403 "error", httpsErr.Error())
411404 } else {
412- log.Info("External HTTPS port 443 is accessible")
405+ logger.Info("External HTTPS port 443 is accessible")
413406 }
414407 }
415408
@@ -417,7 +410,7 @@ func (p *Proxy) checkExternalPortAccess() {
417410 // func (p *Proxy) detectGordonContainer() string {
418411 // // Check if we're running inside a container
419412 // if docker.IsRunningInContainer() {
420-// log.Debug("Detected we're running in a container")
413+// logger.Debug("Detected we're running in a container")
421414
422415 // // Get our hostname which should be the container ID in Docker/Podman
423416 // hostname := os.Getenv("HOSTNAME")
@@ -430,12 +423,12 @@ func (p *Proxy) checkExternalPortAccess() {
430423 // if strings.HasPrefix(hostname, container.ID) {
431424 // // This is our container
432425 // containerName := strings.TrimLeft(container.Names[0], "/")
433-// log.Debug("Auto-detected Gordon container name from hostname", "container_name", containerName)
426+// logger.Debug("Auto-detected Gordon container name from hostname", "container_name", containerName)
434427 // return containerName
435428 // }
436429 // }
437430 // } else {
438-// log.Debug("Could not list containers when checking hostname", "error", err)
431+// logger.Debug("Could not list containers when checking hostname", "error", err)
439432 // }
440433 // }
441434 // }
@@ -450,21 +443,21 @@ func (p *Proxy) checkExternalPortAccess() {
450443 // containerLogs, err := docker.GetContainerLogs(containerID)
451444 // if err == nil && (strings.Contains(containerLogs, "/gordon") || strings.Contains(containerLogs, "serve")) {
452445 // containerName := strings.TrimLeft(container.Names[0], "/")
453-// log.Debug("Auto-detected Gordon container by process", "container_name", containerName)
446+// logger.Debug("Auto-detected Gordon container by process", "container_name", containerName)
454447 // return containerName
455448 // } else if err != nil {
456-// log.Debug("Could not get container logs", "container_id", containerID, "error", err)
449+// logger.Debug("Could not get container logs", "container_id", containerID, "error", err)
457450 // }
458451
459452 // // Check container command for Gordon
460453 // if len(container.Command) > 0 && strings.Contains(container.Command, "gordon") {
461454 // containerName := strings.TrimLeft(container.Names[0], "/")
462-// log.Debug("Auto-detected Gordon container by command", "container_name", containerName)
455+// logger.Debug("Auto-detected Gordon container by command", "container_name", containerName)
463456 // return containerName
464457 // }
465458 // }
466459 // } else {
467-// log.Debug("Could not list containers when checking processes", "error", err)
460+// logger.Debug("Could not list containers when checking processes", "error", err)
468461 // }
469462
470463 // // If we couldn't detect from processes, try to find a container with gordon in the name or image
@@ -474,7 +467,7 @@ func (p *Proxy) checkExternalPortAccess() {
474467 // for _, name := range container.Names {
475468 // if strings.Contains(strings.ToLower(name), "gordon") {
476469 // containerName := strings.TrimLeft(name, "/")
477-// log.Debug("Auto-detected Gordon container by name", "container_name", containerName)
470+// logger.Debug("Auto-detected Gordon container by name", "container_name", containerName)
478471 // return containerName
479472 // }
480473 // }
@@ -482,13 +475,13 @@ func (p *Proxy) checkExternalPortAccess() {
482475 // // Check image name for "gordon"
483476 // if strings.Contains(strings.ToLower(container.Image), "gordon") {
484477 // containerName := strings.TrimLeft(container.Names[0], "/")
485-// log.Debug("Auto-detected Gordon container by image", "container_name", containerName, "image", container.Image)
478+// logger.Debug("Auto-detected Gordon container by image", "container_name", containerName, "image", container.Image)
486479 // return containerName
487480 // }
488481 // }
489482 // }
490483
491484 // // If all else fails, default to "gordon"
492-// log.Debug("Could not auto-detect Gordon container, using default name", "container_name", "gordon")
485+// logger.Debug("Could not auto-detect Gordon container, using default name", "container_name", "gordon")
493486 // return "gordon"
494487 // }
@@ -5,11 +5,10 @@ import (
5 "crypto/tls"5 "crypto/tls"
6 "fmt"6 "fmt"
7 "net/http"7 "net/http"
8- "os"
9 "time"8 "time"
10 9
11 "github.com/bnema/gordon/pkg/docker"10 "github.com/bnema/gordon/pkg/docker"
12- "github.com/charmbracelet/log"11+ "github.com/bnema/gordon/pkg/logger"
13 "github.com/labstack/echo/v4"12 "github.com/labstack/echo/v4"
14 "golang.org/x/crypto/acme"13 "golang.org/x/crypto/acme"
15 )14 )
@@ -29,12 +28,11 @@ func (p *Proxy) Start() error {
29 28
30 // Scan Gordon network containers and check for certificates29 // Scan Gordon network containers and check for certificates
31 if err := p.scanContainersAndCheckCertificates(); err != nil {30 if err := p.scanContainersAndCheckCertificates(); err != nil {
32- log.Warn("Failed to scan containers for certificate checks", "error", err)31+ logger.Warn("Failed to scan containers for certificate checks", "error", err)
33 }32 }
34 33
35 // Run an immediate route verification to ensure all routes are valid on startup34 // Run an immediate route verification to ensure all routes are valid on startup
36- log.Info("Performing initial route verification on startup")35+ logger.Info("Performing initial route verification on startup")
37- p.verifyAllContainerIPs()
38 36
39 // Discover any containers that should have routes but don't37 // Discover any containers that should have routes but don't
40 p.DiscoverMissingRoutes()38 p.DiscoverMissingRoutes()
@@ -44,20 +42,16 @@ func (p *Proxy) Start() error {
44 42
45 // Start the container event listener for real-time updates43 // Start the container event listener for real-time updates
46 if err := p.StartContainerEventListener(); err != nil {44 if err := p.StartContainerEventListener(); err != nil {
47- log.Warn("Failed to start container event listener", "error", err)45+ logger.Warn("Failed to start container event listener", "error", err)
48 // Continue anyway, this is non-critical46 // Continue anyway, this is non-critical
49 }47 }
50 48
51- // Start periodic IP verification (every 5 minutes)
52- // This ensures we catch any IP changes that the event listener might miss
53- p.StartPeriodicIPVerification(5 * time.Minute)
54-
55 // Check if there might be port conflicts with the main server49 // Check if there might be port conflicts with the main server
56 mainServerPort := p.app.GetConfig().Http.Port50 mainServerPort := p.app.GetConfig().Http.Port
57 51
58 // Check HTTP port conflict52 // Check HTTP port conflict
59 if p.config.HttpPort == mainServerPort {53 if p.config.HttpPort == mainServerPort {
60- log.Warn("HTTP port for reverse proxy conflicts with main server port",54+ logger.Warn("HTTP port for reverse proxy conflicts with main server port",
61 "port", p.config.HttpPort,55 "port", p.config.HttpPort,
62 "solution", "reverse proxy HTTP server will be disabled")56 "solution", "reverse proxy HTTP server will be disabled")
63 return nil57 return nil
@@ -65,7 +59,7 @@ func (p *Proxy) Start() error {
65 59
66 // Check HTTPS port conflict (less common, but still possible)60 // Check HTTPS port conflict (less common, but still possible)
67 if p.config.Port == mainServerPort {61 if p.config.Port == mainServerPort {
68- log.Warn("HTTPS port for reverse proxy conflicts with main server port",62+ logger.Warn("HTTPS port for reverse proxy conflicts with main server port",
69 "port", p.config.Port,63 "port", p.config.Port,
70 "solution", "reverse proxy HTTPS server will be disabled")64 "solution", "reverse proxy HTTPS server will be disabled")
71 return nil65 return nil
@@ -74,32 +68,38 @@ func (p *Proxy) Start() error {
74 // Add a special route for the admin domain (Gordon itself)68 // Add a special route for the admin domain (Gordon itself)
75 adminDomain := p.app.GetConfig().Http.FullDomain()69 adminDomain := p.app.GetConfig().Http.FullDomain()
76 70
77- // In a container environment, we need to use the host container IP71+ // Auto-detect the Gordon container name and ID
78- // instead of 127.0.0.1 because each container has its own localhost72+ containerName := p.detectGordonContainer()
79- containerIP := "localhost" // Default to localhost for most reliable connectivity73+
80-74+ // Get the network name from config
81- // Fall back options for container IP75+ networkName := p.app.GetConfig().ContainerEngine.Network
82- if os.Getenv("GORDON_ADMIN_HOST") != "" {76+
83- // Allow explicit configuration via env var77+ // Initialize container IP
84- containerIP = os.Getenv("GORDON_ADMIN_HOST")78+ var containerIP string
85- log.Debug("Using admin host from environment variable",79+
86- "host", containerIP)80+ // Try to get the actual container IP from the network
87- } else if os.Getenv("HOSTNAME") != "" {81+ if p.gordonContainerID == "" {
88- // Use container's own hostname as they're on the same network82+ return fmt.Errorf("failed to detect Gordon container ID, cannot add admin route")
89- containerIP = os.Getenv("HOSTNAME")
90- log.Debug("Using container hostname for admin routing",
91- "hostname", containerIP)
92 }83 }
93 84
94- // Don't test connections yet - the server isn't running85+ // Get the IP address from the container's network
95- // We'll add the admin route with the default host for now86+ ip, err := docker.GetContainerIPFromNetwork(p.gordonContainerID, networkName)
96- // and test connections later with TestAdminConnectionLater87+ if err != nil || ip == "" {
88+ logger.Error("Failed to get Gordon container IP from network",
89+ "container_id", p.gordonContainerID,
90+ "network", networkName,
91+ "error", err)
92+ return fmt.Errorf("failed to get Gordon container IP from network %s: %w", networkName, err)
93+ }
97 94
98- // Auto-detect the Gordon container name95+ containerIP = ip
99- containerName := p.detectGordonContainer()96+ logger.Info("Using Gordon container IP from network",
97+ "container_id", p.gordonContainerID,
98+ "network", networkName,
99+ "ip", containerIP)
100 100
101 // Check if admin route exists in database and create/update it101 // Check if admin route exists in database and create/update it
102- err := p.AddRoute(102+ err = p.AddRoute(
103 adminDomain,103 adminDomain,
104 containerName,104 containerName,
105 containerIP,105 containerIP,
@@ -109,11 +109,11 @@ func (p *Proxy) Start() error {
109 )109 )
110 110
111 if err != nil {111 if err != nil {
112- log.Error("Failed to add admin route to database",112+ logger.Error("Failed to add admin route to database",
113 "error", err,113 "error", err,
114 "domain", adminDomain)114 "domain", adminDomain)
115 } else {115 } else {
116- log.Info("Ensured admin domain route is in database",116+ logger.Info("Ensured admin domain route is in database",
117 "domain", adminDomain,117 "domain", adminDomain,
118 "container", containerName,118 "container", containerName,
119 "target", fmt.Sprintf("http://%s:%s", containerIP, p.app.GetConfig().Http.Port))119 "target", fmt.Sprintf("http://%s:%s", containerIP, p.app.GetConfig().Http.Port))
@@ -131,12 +131,12 @@ func (p *Proxy) Start() error {
131 Path: "/",131 Path: "/",
132 Active: true,132 Active: true,
133 }133 }
134- log.Info("Added special route for admin domain",134+ logger.Info("Added special route for admin domain",
135 "domain", adminDomain,135 "domain", adminDomain,
136 "container", containerName,136 "container", containerName,
137 "target", fmt.Sprintf("http://%s:%s", containerIP, p.app.GetConfig().Http.Port))137 "target", fmt.Sprintf("http://%s:%s", containerIP, p.app.GetConfig().Http.Port))
138 } else {138 } else {
139- log.Debug("Admin domain route already exists",139+ logger.Debug("Admin domain route already exists",
140 "domain", adminDomain,140 "domain", adminDomain,
141 "route", fmt.Sprintf("%s://%s:%s",141 "route", fmt.Sprintf("%s://%s:%s",
142 p.routes[adminDomain].Protocol,142 p.routes[adminDomain].Protocol,
@@ -145,13 +145,6 @@ func (p *Proxy) Start() error {
145 }145 }
146 p.mu.Unlock()146 p.mu.Unlock()
147 147
148- // We no longer automatically add the root domain redirect to avoid conflicts
149- // with other containers that might be using the root domain.
150- // If root domain redirection is needed, it should be explicitly configured.
151-
152- // Clean up any existing root domain conflict
153- p.CleanupRootDomainForAdmin()
154-
155 // Set up middleware148 // Set up middleware
156 p.setupMiddleware()149 p.setupMiddleware()
157 150
@@ -170,7 +163,7 @@ func (p *Proxy) Start() error {
170 GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {163 GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
171 // Log SNI information in debug mode164 // Log SNI information in debug mode
172 if hello.ServerName != "" {165 if hello.ServerName != "" {
173- log.Debug("TLS handshake with SNI",166+ logger.Debug("TLS handshake with SNI",
174 "server_name", hello.ServerName)167 "server_name", hello.ServerName)
175 }168 }
176 169
@@ -179,7 +172,7 @@ func (p *Proxy) Start() error {
179 172
180 // If we can't get a certificate and we have a fallback, use it for the admin domain173 // If we can't get a certificate and we have a fallback, use it for the admin domain
181 if (err != nil || cert == nil) && hello.ServerName == adminDomain && p.fallbackCert != nil {174 if (err != nil || cert == nil) && hello.ServerName == adminDomain && p.fallbackCert != nil {
182- log.Debug("Using fallback certificate for admin domain",175+ logger.Debug("Using fallback certificate for admin domain",
183 "domain", adminDomain,176 "domain", adminDomain,
184 "error", err)177 "error", err)
185 return p.fallbackCert, nil178 return p.fallbackCert, nil
@@ -212,18 +205,18 @@ func (p *Proxy) Start() error {
212 205
213 // Start HTTPS server206 // Start HTTPS server
214 go func() {207 go func() {
215- log.Info("Starting HTTPS server", "address", httpsServer.Addr)208+ logger.Info("Starting HTTPS server", "address", httpsServer.Addr)
216 // Using empty strings for cert and key files since we're using GetCertificate209 // Using empty strings for cert and key files since we're using GetCertificate
217 if err := httpsServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {210 if err := httpsServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
218- log.Error("HTTPS server failed", "error", err)211+ logger.Error("HTTPS server failed", "error", err)
219 }212 }
220 }()213 }()
221 214
222 // Start HTTP server215 // Start HTTP server
223 go func() {216 go func() {
224- log.Info("Starting HTTP server", "address", httpServer.Addr)217+ logger.Info("Starting HTTP server", "address", httpServer.Addr)
225 if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {218 if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
226- log.Error("HTTP server failed", "error", err)219+ logger.Error("HTTP server failed", "error", err)
227 }220 }
228 }()221 }()
229 222
@@ -235,7 +228,7 @@ func (p *Proxy) Start() error {
235 228
236 // Stop gracefully shuts down the proxy server229 // Stop gracefully shuts down the proxy server
237 func (p *Proxy) Stop() error {230 func (p *Proxy) Stop() error {
238- log.Info("Stopping reverse proxy")231+ logger.Info("Stopping reverse proxy")
239 232
240 // Run any rate limiter cleanup functions233 // Run any rate limiter cleanup functions
241 for _, cleanup := range rateLimiterCleanup {234 for _, cleanup := range rateLimiterCleanup {
@@ -247,7 +240,7 @@ func (p *Proxy) Stop() error {
247 240
248 // Skip the rest if the server was never started241 // Skip the rest if the server was never started
249 if !p.serverStarted {242 if !p.serverStarted {
250- log.Debug("Proxy server was never started, nothing to stop")243+ logger.Debug("Proxy server was never started, nothing to stop")
251 return nil244 return nil
252 }245 }
253 246
@@ -262,18 +255,18 @@ func (p *Proxy) Stop() error {
262 // Shutdown HTTP server if it's running255 // Shutdown HTTP server if it's running
263 if p.httpServer != nil {256 if p.httpServer != nil {
264 if err := p.httpServer.Shutdown(ctx); err != nil {257 if err := p.httpServer.Shutdown(ctx); err != nil {
265- log.Error("Failed to gracefully shutdown HTTP server", "error", err)258+ logger.Error("Failed to gracefully shutdown HTTP server", "error", err)
266 }259 }
267 }260 }
268 261
269 // Shutdown HTTPS server if it's running262 // Shutdown HTTPS server if it's running
270 if p.httpsServer != nil {263 if p.httpsServer != nil {
271 if err := p.httpsServer.Shutdown(ctx); err != nil {264 if err := p.httpsServer.Shutdown(ctx); err != nil {
272- log.Error("Failed to gracefully shutdown HTTPS server", "error", err)265+ logger.Error("Failed to gracefully shutdown HTTPS server", "error", err)
273 }266 }
274 }267 }
275 268
276- log.Info("Reverse proxy stopped")269+ logger.Info("Reverse proxy stopped")
277 return nil270 return nil
278 }271 }
279 272
@@ -282,7 +275,7 @@ func (p *Proxy) testAdminConnection(defaultHost string, port string) string {
282 adminPath := p.app.GetConfig().Admin.Path275 adminPath := p.app.GetConfig().Admin.Path
283 url := fmt.Sprintf("http://%s:%s%s", defaultHost, port, adminPath)276 url := fmt.Sprintf("http://%s:%s%s", defaultHost, port, adminPath)
284 277
285- log.Debug("Testing admin connection", "url", url)278+ logger.Debug("Testing admin connection", "url", url)
286 279
287 // Try connecting using the provided default host280 // Try connecting using the provided default host
288 client := &http.Client{281 client := &http.Client{
@@ -298,7 +291,7 @@ func (p *Proxy) testAdminConnection(defaultHost string, port string) string {
298 resp, err := client.Get(url)291 resp, err := client.Get(url)
299 if err == nil {292 if err == nil {
300 resp.Body.Close()293 resp.Body.Close()
301- log.Info("Admin connection successful", "host", defaultHost)294+ logger.Info("Admin connection successful", "host", defaultHost)
302 return defaultHost295 return defaultHost
303 }296 }
304 297
@@ -316,17 +309,17 @@ func (p *Proxy) testAdminConnection(defaultHost string, port string) string {
316 }309 }
317 310
318 url = fmt.Sprintf("http://%s:%s%s", host, port, adminPath)311 url = fmt.Sprintf("http://%s:%s%s", host, port, adminPath)
319- log.Debug("Trying alternative host", "url", url)312+ logger.Debug("Trying alternative host", "url", url)
320 313
321 resp, err := client.Get(url)314 resp, err := client.Get(url)
322 if err == nil {315 if err == nil {
323 resp.Body.Close()316 resp.Body.Close()
324- log.Info("Found working admin connection", "host", host)317+ logger.Info("Found working admin connection", "host", host)
325 return host318 return host
326 }319 }
327 }320 }
328 321
329- log.Warn("Could not establish admin connection on any tested host", "fallback", defaultHost)322+ logger.Warn("Could not establish admin connection on any tested host", "fallback", defaultHost)
330 return defaultHost323 return defaultHost
331 }324 }
332 325
@@ -335,7 +328,7 @@ func (p *Proxy) TestAdminConnectionLater() {
335 // Wait for servers to start328 // Wait for servers to start
336 time.Sleep(2 * time.Second)329 time.Sleep(2 * time.Second)
337 330
338- log.Debug("Testing admin connections after server startup")331+ logger.Debug("Testing admin connections after server startup")
339 332
340 // Get domain and current route333 // Get domain and current route
341 adminDomain := p.app.GetConfig().Http.FullDomain()334 adminDomain := p.app.GetConfig().Http.FullDomain()
@@ -345,7 +338,7 @@ func (p *Proxy) TestAdminConnectionLater() {
345 p.mu.RUnlock()338 p.mu.RUnlock()
346 339
347 if !exists {340 if !exists {
348- log.Warn("Admin route not found")341+ logger.Warn("Admin route not found")
349 return342 return
350 }343 }
351 344
@@ -355,7 +348,7 @@ func (p *Proxy) TestAdminConnectionLater() {
355 p.mu.Lock()348 p.mu.Lock()
356 if r, ok := p.routes[adminDomain]; ok {349 if r, ok := p.routes[adminDomain]; ok {
357 r.ContainerIP = testedIP350 r.ContainerIP = testedIP
358- log.Info("Updated admin route with working connection",351+ logger.Info("Updated admin route with working connection",
359 "domain", adminDomain,352 "domain", adminDomain,
360 "ip", testedIP)353 "ip", testedIP)
361 }354 }
@@ -384,10 +377,10 @@ func (p *Proxy) checkExternalPortAccess() {
384 httpURL := fmt.Sprintf("http://%s/.well-known/acme-challenge/test", adminDomain)377 httpURL := fmt.Sprintf("http://%s/.well-known/acme-challenge/test", adminDomain)
385 _, httpErr := client.Get(httpURL)378 _, httpErr := client.Get(httpURL)
386 if httpErr != nil {379 if httpErr != nil {
387- log.Warn("External HTTP port 80 might not be accessible",380+ logger.Warn("External HTTP port 80 might not be accessible",
388 "error", httpErr.Error())381 "error", httpErr.Error())
389 } else {382 } else {
390- log.Info("External HTTP port 80 is accessible")383+ logger.Info("External HTTP port 80 is accessible")
391 }384 }
392 385
393 // Test HTTPS port 443386 // Test HTTPS port 443
@@ -406,10 +399,10 @@ func (p *Proxy) checkExternalPortAccess() {
406 httpsURL := fmt.Sprintf("https://%s/", adminDomain)399 httpsURL := fmt.Sprintf("https://%s/", adminDomain)
407 _, httpsErr := httpsClient.Get(httpsURL)400 _, httpsErr := httpsClient.Get(httpsURL)
408 if httpsErr != nil {401 if httpsErr != nil {
409- log.Warn("External HTTPS port 443 might not be accessible",402+ logger.Warn("External HTTPS port 443 might not be accessible",
410 "error", httpsErr.Error())403 "error", httpsErr.Error())
411 } else {404 } else {
412- log.Info("External HTTPS port 443 is accessible")405+ logger.Info("External HTTPS port 443 is accessible")
413 }406 }
414 }407 }
415 408
@@ -417,7 +410,7 @@ func (p *Proxy) checkExternalPortAccess() {
417 // func (p *Proxy) detectGordonContainer() string {410 // func (p *Proxy) detectGordonContainer() string {
418 // // Check if we're running inside a container411 // // Check if we're running inside a container
419 // if docker.IsRunningInContainer() {412 // if docker.IsRunningInContainer() {
420-// log.Debug("Detected we're running in a container")413+// logger.Debug("Detected we're running in a container")
421 414
422 // // Get our hostname which should be the container ID in Docker/Podman415 // // Get our hostname which should be the container ID in Docker/Podman
423 // hostname := os.Getenv("HOSTNAME")416 // hostname := os.Getenv("HOSTNAME")
@@ -430,12 +423,12 @@ func (p *Proxy) checkExternalPortAccess() {
430 // if strings.HasPrefix(hostname, container.ID) {423 // if strings.HasPrefix(hostname, container.ID) {
431 // // This is our container424 // // This is our container
432 // containerName := strings.TrimLeft(container.Names[0], "/")425 // containerName := strings.TrimLeft(container.Names[0], "/")
433-// log.Debug("Auto-detected Gordon container name from hostname", "container_name", containerName)426+// logger.Debug("Auto-detected Gordon container name from hostname", "container_name", containerName)
434 // return containerName427 // return containerName
435 // }428 // }
436 // }429 // }
437 // } else {430 // } else {
438-// log.Debug("Could not list containers when checking hostname", "error", err)431+// logger.Debug("Could not list containers when checking hostname", "error", err)
439 // }432 // }
440 // }433 // }
441 // }434 // }
@@ -450,21 +443,21 @@ func (p *Proxy) checkExternalPortAccess() {
450 // containerLogs, err := docker.GetContainerLogs(containerID)443 // containerLogs, err := docker.GetContainerLogs(containerID)
451 // if err == nil && (strings.Contains(containerLogs, "/gordon") || strings.Contains(containerLogs, "serve")) {444 // if err == nil && (strings.Contains(containerLogs, "/gordon") || strings.Contains(containerLogs, "serve")) {
452 // containerName := strings.TrimLeft(container.Names[0], "/")445 // containerName := strings.TrimLeft(container.Names[0], "/")
453-// log.Debug("Auto-detected Gordon container by process", "container_name", containerName)446+// logger.Debug("Auto-detected Gordon container by process", "container_name", containerName)
454 // return containerName447 // return containerName
455 // } else if err != nil {448 // } else if err != nil {
456-// log.Debug("Could not get container logs", "container_id", containerID, "error", err)449+// logger.Debug("Could not get container logs", "container_id", containerID, "error", err)
457 // }450 // }
458 451
459 // // Check container command for Gordon452 // // Check container command for Gordon
460 // if len(container.Command) > 0 && strings.Contains(container.Command, "gordon") {453 // if len(container.Command) > 0 && strings.Contains(container.Command, "gordon") {
461 // containerName := strings.TrimLeft(container.Names[0], "/")454 // containerName := strings.TrimLeft(container.Names[0], "/")
462-// log.Debug("Auto-detected Gordon container by command", "container_name", containerName)455+// logger.Debug("Auto-detected Gordon container by command", "container_name", containerName)
463 // return containerName456 // return containerName
464 // }457 // }
465 // }458 // }
466 // } else {459 // } else {
467-// log.Debug("Could not list containers when checking processes", "error", err)460+// logger.Debug("Could not list containers when checking processes", "error", err)
468 // }461 // }
469 462
470 // // If we couldn't detect from processes, try to find a container with gordon in the name or image463 // // If we couldn't detect from processes, try to find a container with gordon in the name or image
@@ -474,7 +467,7 @@ func (p *Proxy) checkExternalPortAccess() {
474 // for _, name := range container.Names {467 // for _, name := range container.Names {
475 // if strings.Contains(strings.ToLower(name), "gordon") {468 // if strings.Contains(strings.ToLower(name), "gordon") {
476 // containerName := strings.TrimLeft(name, "/")469 // containerName := strings.TrimLeft(name, "/")
477-// log.Debug("Auto-detected Gordon container by name", "container_name", containerName)470+// logger.Debug("Auto-detected Gordon container by name", "container_name", containerName)
478 // return containerName471 // return containerName
479 // }472 // }
480 // }473 // }
@@ -482,13 +475,13 @@ func (p *Proxy) checkExternalPortAccess() {
482 // // Check image name for "gordon"475 // // Check image name for "gordon"
483 // if strings.Contains(strings.ToLower(container.Image), "gordon") {476 // if strings.Contains(strings.ToLower(container.Image), "gordon") {
484 // containerName := strings.TrimLeft(container.Names[0], "/")477 // containerName := strings.TrimLeft(container.Names[0], "/")
485-// log.Debug("Auto-detected Gordon container by image", "container_name", containerName, "image", container.Image)478+// logger.Debug("Auto-detected Gordon container by image", "container_name", containerName, "image", container.Image)
486 // return containerName479 // return containerName
487 // }480 // }
488 // }481 // }
489 // }482 // }
490 483
491 // // If all else fails, default to "gordon"484 // // If all else fails, default to "gordon"
492-// log.Debug("Could not auto-detect Gordon container, using default name", "container_name", "gordon")485+// logger.Debug("Could not auto-detect Gordon container, using default name", "container_name", "gordon")
493 // return "gordon"486 // return "gordon"
494 // }487 // }
M internal/server/sqlite_init.go +0 -15
@@ -69,16 +69,6 @@ func (a *App) checkDBFile(dbPath string) (bool, error) {
6969 return true, nil
7070 }
7171
72-// runMigrations applies any necessary database migrations for existing databases
73-func runMigrations(db *sql.DB) error {
74- log.Info("Checking for database migrations")
75-
76- // No migrations needed at this time
77- log.Debug("No migrations needed")
78-
79- return nil
80-}
81-
8272 // InitializeDB initializes the database
8373 func InitializeDB(a *App) (*sql.DB, error) {
8474 log.Debug("Initializing database")
@@ -114,11 +104,6 @@ func InitializeDB(a *App) (*sql.DB, error) {
114104 a.DB = db
115105
116106 if dbExists {
117- // Run migrations for existing database
118- if err := runMigrations(db); err != nil {
119- log.Warn("Failed to run database migrations", "error", err)
120- }
121-
122107 // Populate the struct tables from the database
123108 log.Debug("Populating struct tables from existing database")
124109 if err := PopulateStructWithTables(a); err != nil {
@@ -69,16 +69,6 @@ func (a *App) checkDBFile(dbPath string) (bool, error) {
69 return true, nil69 return true, nil
70 }70 }
71 71
72-// runMigrations applies any necessary database migrations for existing databases
73-func runMigrations(db *sql.DB) error {
74- log.Info("Checking for database migrations")
75-
76- // No migrations needed at this time
77- log.Debug("No migrations needed")
78-
79- return nil
80-}
81-
82 // InitializeDB initializes the database72 // InitializeDB initializes the database
83 func InitializeDB(a *App) (*sql.DB, error) {73 func InitializeDB(a *App) (*sql.DB, error) {
84 log.Debug("Initializing database")74 log.Debug("Initializing database")
@@ -114,11 +104,6 @@ func InitializeDB(a *App) (*sql.DB, error) {
114 a.DB = db104 a.DB = db
115 105
116 if dbExists {106 if dbExists {
117- // Run migrations for existing database
118- if err := runMigrations(db); err != nil {
119- log.Warn("Failed to run database migrations", "error", err)
120- }
121-
122 // Populate the struct tables from the database107 // Populate the struct tables from the database
123 log.Debug("Populating struct tables from existing database")108 log.Debug("Populating struct tables from existing database")
124 if err := PopulateStructWithTables(a); err != nil {109 if err := PopulateStructWithTables(a); err != nil {
M internal/templating/cmdparams/proxy.go +5 -4
@@ -11,18 +11,19 @@ func SetupProxyRoute(params *docker.ContainerCommandParams, containerPort string
1111 // Set the container port to be used by the reverse proxy
1212 params.ProxyPort = containerPort
1313
14+ // Create the full domain (service.domain)
15+ fullDomain := params.ServiceName + "." + params.Domain
16+
1417 // Add custom labels to identify this container as manageable by Gordon
1518 params.Labels = []string{
1619 "gordon.managed=true",
17- "gordon.domain=" + params.Domain,
18- "gordon.service=" + params.ServiceName,
20+ "gordon.domain=" + fullDomain,
1921 "gordon.proxy.port=" + containerPort,
2022 "gordon.proxy.ssl=" + boolToString(params.IsSSL),
2123 }
2224
2325 log.Debug("Container proxy configuration set up",
24- "domain", params.Domain,
25- "service", params.ServiceName,
26+ "domain", fullDomain,
2627 "proxyPort", containerPort,
2728 "ssl", params.IsSSL)
2829
@@ -11,18 +11,19 @@ func SetupProxyRoute(params *docker.ContainerCommandParams, containerPort string
11 // Set the container port to be used by the reverse proxy11 // Set the container port to be used by the reverse proxy
12 params.ProxyPort = containerPort12 params.ProxyPort = containerPort
13 13
14+ // Create the full domain (service.domain)
15+ fullDomain := params.ServiceName + "." + params.Domain
16+
14 // Add custom labels to identify this container as manageable by Gordon17 // Add custom labels to identify this container as manageable by Gordon
15 params.Labels = []string{18 params.Labels = []string{
16 "gordon.managed=true",19 "gordon.managed=true",
17- "gordon.domain=" + params.Domain,20+ "gordon.domain=" + fullDomain,
18- "gordon.service=" + params.ServiceName,
19 "gordon.proxy.port=" + containerPort,21 "gordon.proxy.port=" + containerPort,
20 "gordon.proxy.ssl=" + boolToString(params.IsSSL),22 "gordon.proxy.ssl=" + boolToString(params.IsSSL),
21 }23 }
22 24
23 log.Debug("Container proxy configuration set up",25 log.Debug("Container proxy configuration set up",
24- "domain", params.Domain,26+ "domain", fullDomain,
25- "service", params.ServiceName,
26 "proxyPort", containerPort,27 "proxyPort", containerPort,
27 "ssl", params.IsSSL)28 "ssl", params.IsSSL)
28 29
M pkg/docker/containers.go +10 -3
@@ -754,7 +754,7 @@ func formatDuration(d time.Duration) string {
754754 // when relevant container events are detected. This is used for service discovery and IP updates.
755755 // This implementation uses a polling approach instead of Docker event streams, which is more
756756 // stable for both Docker and Podman environments.
757-func ListenForContainerEvents(networkFilter string, callback func(string, string, string)) error {
757+func ListenForContainerEvents(networkFilter string, startCallback func(string, string, string), stopCallback func(string, string)) error {
758758 // Check if the Docker client has been initialized
759759 err := CheckIfInitialized()
760760 if err != nil {
@@ -936,7 +936,7 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
936936 "container_ip", containerIP)
937937
938938 // Call the callback with the container ID, name, and IP
939- callback(event.id, event.name, containerIP)
939+ startCallback(event.id, event.name, containerIP)
940940
941941 logger.Debug("Processed container start",
942942 "container_id", event.id,
@@ -947,7 +947,14 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
947947 logger.Debug("Container stopped",
948948 "container_id", event.id,
949949 "container_name", event.name)
950- // If any stop event handling is needed, add it here
950+
951+ // Call the stop callback if provided
952+ if stopCallback != nil {
953+ logger.Debug("Calling container stop callback",
954+ "container_id", event.id,
955+ "container_name", event.name)
956+ stopCallback(event.id, event.name)
957+ }
951958
952959 // Remove from IP tracking map when container stops
953960 delete(containerIPs, event.id)
@@ -754,7 +754,7 @@ func formatDuration(d time.Duration) string {
754 // when relevant container events are detected. This is used for service discovery and IP updates.754 // when relevant container events are detected. This is used for service discovery and IP updates.
755 // This implementation uses a polling approach instead of Docker event streams, which is more755 // This implementation uses a polling approach instead of Docker event streams, which is more
756 // stable for both Docker and Podman environments.756 // stable for both Docker and Podman environments.
757-func ListenForContainerEvents(networkFilter string, callback func(string, string, string)) error {757+func ListenForContainerEvents(networkFilter string, startCallback func(string, string, string), stopCallback func(string, string)) error {
758 // Check if the Docker client has been initialized758 // Check if the Docker client has been initialized
759 err := CheckIfInitialized()759 err := CheckIfInitialized()
760 if err != nil {760 if err != nil {
@@ -936,7 +936,7 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
936 "container_ip", containerIP)936 "container_ip", containerIP)
937 937
938 // Call the callback with the container ID, name, and IP938 // Call the callback with the container ID, name, and IP
939- callback(event.id, event.name, containerIP)939+ startCallback(event.id, event.name, containerIP)
940 940
941 logger.Debug("Processed container start",941 logger.Debug("Processed container start",
942 "container_id", event.id,942 "container_id", event.id,
@@ -947,7 +947,14 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
947 logger.Debug("Container stopped",947 logger.Debug("Container stopped",
948 "container_id", event.id,948 "container_id", event.id,
949 "container_name", event.name)949 "container_name", event.name)
950- // If any stop event handling is needed, add it here950+
951+ // Call the stop callback if provided
952+ if stopCallback != nil {
953+ logger.Debug("Calling container stop callback",
954+ "container_id", event.id,
955+ "container_name", event.name)
956+ stopCallback(event.id, event.name)
957+ }
951 958
952 // Remove from IP tracking map when container stops959 // Remove from IP tracking map when container stops
953 delete(containerIPs, event.id)960 delete(containerIPs, event.id)

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