detached b4314ac

refactor: Centralize logging with custom logger

- Replace all instances of `charmbracelet/log` with a centralized `pkg/logger` package
- Implement environment and configuration-based log level configuration
- Enhance logging with more consistent and structured log messages
- Remove direct log level manipulation in favor of centralized logger configuration
- Update Docker, proxy, and server packages to use new logger
- Improve log message clarity and reduce log noise
- Add support for log level configuration from environment variables and config file
commit
b4314acc24f1438fd84acb9d7e88537a2a965067
author
bamen · · 2025-03-11 06:28 UTC
parent
d95b77e
signature
unsigned
22 files · +1950 -704
M internal/cli/cmd/deploy.go +14 -17
@@ -16,14 +16,11 @@ import (
1616 "github.com/bnema/gordon/internal/cli"
1717 "github.com/bnema/gordon/internal/cli/handler"
1818 "github.com/bnema/gordon/internal/common"
19- "github.com/charmbracelet/log"
19+ "github.com/bnema/gordon/pkg/logger"
2020 "github.com/spf13/cobra"
2121 )
2222
23-func init() {
24- log.SetReportTimestamp(true)
25- log.SetTimeFormat("15:04:05")
26-}
23+// No need for logger initialization here as it's handled by the centralized logger
2724
2825 func NewDeployCommand(a *cli.App) *cobra.Command {
2926 var port string
@@ -35,29 +32,29 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
3532 Args: cobra.ExactArgs(1),
3633 PreRun: func(cmd *cobra.Command, args []string) {
3734 if err := handler.FieldCheck(a); err != nil {
38- log.Error("Field check failed", "error", err)
35+ logger.Error("Field check failed", "error", err)
3936 os.Exit(1)
4037 }
4138 },
4239 Run: func(cmd *cobra.Command, args []string) {
4340 imageName := args[0]
44- log.Info("Pushing image", "image", imageName)
41+ logger.Info("Pushing image", "image", imageName)
4542
4643 if err := validateDeployInputs(imageName, port, targetDomain); err != nil {
47- log.Error("Validation failed", "error", err)
44+ logger.Error("Validation failed", "error", err)
4845 return
4946 }
5047
5148 reader, actualSize, err := handler.ExportDockerImage(imageName)
5249 if err != nil {
53- log.Error("Error exporting image", "error", err)
50+ logger.Error("Error exporting image", "error", err)
5451 return
5552 }
5653 defer reader.Close()
5754
5855 sizeInMB := float64(actualSize) / 1024 / 1024
5956
60- log.Info("Image exported successfully", "image", imageName, "size", fmt.Sprintf("%.2fMB", sizeInMB))
57+ logger.Info("Image exported successfully", "image", imageName, "size", fmt.Sprintf("%.2fMB", sizeInMB))
6158
6259 if err := deployImage(a, reader, imageName, port, targetDomain); err != nil {
6360 os.Exit(1)
@@ -74,11 +71,11 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
7471 func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain string) error {
7572 // Check authentication early
7673 if err := handler.CheckAndRefreshAuth(a); err != nil {
77- log.Error("Authentication check failed", "error", err)
74+ logger.Error("Authentication check failed", "error", err)
7875 return err
7976 }
8077
81- log.Info("Deploying image", "image", imageName)
78+ logger.Info("Deploying image", "image", imageName)
8279 // Check for conflicts first
8380 resp, err := checkDeployConflict(a, targetDomain, port)
8481 if err != nil {
@@ -94,7 +91,7 @@ func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain str
9491
9592 // If there's a conflict, handle it before proceeding
9693 if !resp.Success && resp.ContainerID != "" {
97- log.Warn("Container already exists",
94+ logger.Warn("Container already exists",
9895 "name", resp.ContainerName,
9996 "id", shortID,
10097 "state", resp.State,
@@ -117,7 +114,7 @@ func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain str
117114
118115 imageReader := bytes.NewReader(buf.Bytes())
119116 sizeInMB := float64(size) / 1024 / 1024
120- log.Info("Attempting to deploy...",
117+ logger.Info("Attempting to deploy...",
121118 "image", imageName,
122119 "size", fmt.Sprintf("%.2fMB", sizeInMB),
123120 "port", port,
@@ -162,7 +159,7 @@ func waitForDeployment(domain string, containerID string) error {
162159 shortContainerID = containerID[:12]
163160 }
164161
165- log.Info("Waiting for deployment to be reachable",
162+ logger.Info("Waiting for deployment to be reachable",
166163 "domain", domain,
167164 "container_id", shortContainerID)
168165
@@ -171,7 +168,7 @@ func waitForDeployment(domain string, containerID string) error {
171168 if err == nil {
172169 defer resp.Body.Close()
173170 if resp.StatusCode == http.StatusOK {
174- log.Info("Deployment successful",
171+ logger.Info("Deployment successful",
175172 "domain", domain,
176173 "container_id", shortContainerID)
177174 return nil
@@ -181,7 +178,7 @@ func waitForDeployment(domain string, containerID string) error {
181178 return fmt.Errorf("deployment failed: %s", string(body))
182179 }
183180 }
184- log.Warn("Deployment not ready yet, retrying",
181+ logger.Warn("Deployment not ready yet, retrying",
185182 "attempt", fmt.Sprintf("%d/%d", i+1, maxRetries))
186183 time.Sleep(retryInterval)
187184 }
@@ -16,14 +16,11 @@ import (
16 "github.com/bnema/gordon/internal/cli"16 "github.com/bnema/gordon/internal/cli"
17 "github.com/bnema/gordon/internal/cli/handler"17 "github.com/bnema/gordon/internal/cli/handler"
18 "github.com/bnema/gordon/internal/common"18 "github.com/bnema/gordon/internal/common"
19- "github.com/charmbracelet/log"19+ "github.com/bnema/gordon/pkg/logger"
20 "github.com/spf13/cobra"20 "github.com/spf13/cobra"
21 )21 )
22 22
23-func init() {23+// No need for logger initialization here as it's handled by the centralized logger
24- log.SetReportTimestamp(true)
25- log.SetTimeFormat("15:04:05")
26-}
27 24
28 func NewDeployCommand(a *cli.App) *cobra.Command {25 func NewDeployCommand(a *cli.App) *cobra.Command {
29 var port string26 var port string
@@ -35,29 +32,29 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
35 Args: cobra.ExactArgs(1),32 Args: cobra.ExactArgs(1),
36 PreRun: func(cmd *cobra.Command, args []string) {33 PreRun: func(cmd *cobra.Command, args []string) {
37 if err := handler.FieldCheck(a); err != nil {34 if err := handler.FieldCheck(a); err != nil {
38- log.Error("Field check failed", "error", err)35+ logger.Error("Field check failed", "error", err)
39 os.Exit(1)36 os.Exit(1)
40 }37 }
41 },38 },
42 Run: func(cmd *cobra.Command, args []string) {39 Run: func(cmd *cobra.Command, args []string) {
43 imageName := args[0]40 imageName := args[0]
44- log.Info("Pushing image", "image", imageName)41+ logger.Info("Pushing image", "image", imageName)
45 42
46 if err := validateDeployInputs(imageName, port, targetDomain); err != nil {43 if err := validateDeployInputs(imageName, port, targetDomain); err != nil {
47- log.Error("Validation failed", "error", err)44+ logger.Error("Validation failed", "error", err)
48 return45 return
49 }46 }
50 47
51 reader, actualSize, err := handler.ExportDockerImage(imageName)48 reader, actualSize, err := handler.ExportDockerImage(imageName)
52 if err != nil {49 if err != nil {
53- log.Error("Error exporting image", "error", err)50+ logger.Error("Error exporting image", "error", err)
54 return51 return
55 }52 }
56 defer reader.Close()53 defer reader.Close()
57 54
58 sizeInMB := float64(actualSize) / 1024 / 102455 sizeInMB := float64(actualSize) / 1024 / 1024
59 56
60- log.Info("Image exported successfully", "image", imageName, "size", fmt.Sprintf("%.2fMB", sizeInMB))57+ logger.Info("Image exported successfully", "image", imageName, "size", fmt.Sprintf("%.2fMB", sizeInMB))
61 58
62 if err := deployImage(a, reader, imageName, port, targetDomain); err != nil {59 if err := deployImage(a, reader, imageName, port, targetDomain); err != nil {
63 os.Exit(1)60 os.Exit(1)
@@ -74,11 +71,11 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
74 func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain string) error {71 func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain string) error {
75 // Check authentication early72 // Check authentication early
76 if err := handler.CheckAndRefreshAuth(a); err != nil {73 if err := handler.CheckAndRefreshAuth(a); err != nil {
77- log.Error("Authentication check failed", "error", err)74+ logger.Error("Authentication check failed", "error", err)
78 return err75 return err
79 }76 }
80 77
81- log.Info("Deploying image", "image", imageName)78+ logger.Info("Deploying image", "image", imageName)
82 // Check for conflicts first79 // Check for conflicts first
83 resp, err := checkDeployConflict(a, targetDomain, port)80 resp, err := checkDeployConflict(a, targetDomain, port)
84 if err != nil {81 if err != nil {
@@ -94,7 +91,7 @@ func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain str
94 91
95 // If there's a conflict, handle it before proceeding92 // If there's a conflict, handle it before proceeding
96 if !resp.Success && resp.ContainerID != "" {93 if !resp.Success && resp.ContainerID != "" {
97- log.Warn("Container already exists",94+ logger.Warn("Container already exists",
98 "name", resp.ContainerName,95 "name", resp.ContainerName,
99 "id", shortID,96 "id", shortID,
100 "state", resp.State,97 "state", resp.State,
@@ -117,7 +114,7 @@ func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain str
117 114
118 imageReader := bytes.NewReader(buf.Bytes())115 imageReader := bytes.NewReader(buf.Bytes())
119 sizeInMB := float64(size) / 1024 / 1024116 sizeInMB := float64(size) / 1024 / 1024
120- log.Info("Attempting to deploy...",117+ logger.Info("Attempting to deploy...",
121 "image", imageName,118 "image", imageName,
122 "size", fmt.Sprintf("%.2fMB", sizeInMB),119 "size", fmt.Sprintf("%.2fMB", sizeInMB),
123 "port", port,120 "port", port,
@@ -162,7 +159,7 @@ func waitForDeployment(domain string, containerID string) error {
162 shortContainerID = containerID[:12]159 shortContainerID = containerID[:12]
163 }160 }
164 161
165- log.Info("Waiting for deployment to be reachable",162+ logger.Info("Waiting for deployment to be reachable",
166 "domain", domain,163 "domain", domain,
167 "container_id", shortContainerID)164 "container_id", shortContainerID)
168 165
@@ -171,7 +168,7 @@ func waitForDeployment(domain string, containerID string) error {
171 if err == nil {168 if err == nil {
172 defer resp.Body.Close()169 defer resp.Body.Close()
173 if resp.StatusCode == http.StatusOK {170 if resp.StatusCode == http.StatusOK {
174- log.Info("Deployment successful",171+ logger.Info("Deployment successful",
175 "domain", domain,172 "domain", domain,
176 "container_id", shortContainerID)173 "container_id", shortContainerID)
177 return nil174 return nil
@@ -181,7 +178,7 @@ func waitForDeployment(domain string, containerID string) error {
181 return fmt.Errorf("deployment failed: %s", string(body))178 return fmt.Errorf("deployment failed: %s", string(body))
182 }179 }
183 }180 }
184- log.Warn("Deployment not ready yet, retrying",181+ logger.Warn("Deployment not ready yet, retrying",
185 "attempt", fmt.Sprintf("%d/%d", i+1, maxRetries))182 "attempt", fmt.Sprintf("%d/%d", i+1, maxRetries))
186 time.Sleep(retryInterval)183 time.Sleep(retryInterval)
187 }184 }
M internal/cli/cmd/version.go +5 -6
@@ -1,10 +1,9 @@
11 package cmd
22
33 import (
4- "fmt"
5-
64 "github.com/bnema/gordon/internal/cli"
75 "github.com/bnema/gordon/internal/common"
6+ "github.com/bnema/gordon/pkg/logger"
87 "github.com/spf13/cobra"
98 )
109
@@ -19,20 +18,20 @@ func NewVersionCommand(app *cli.App) *cobra.Command {
1918 app.Config.Build.BuildDate,
2019 app.Config.Build.ProxyURL,
2120 )
22- fmt.Println(info.String())
21+ logger.Info(info.String())
2322
2423 hasUpdate, latestVersion, err := common.CheckForNewVersion(
2524 info.Version,
2625 info.ProxyURL,
2726 )
2827 if err != nil {
29- fmt.Printf("Error checking for updates: %v\n", err)
28+ logger.Error("Error checking for updates", "error", err)
3029 return
3130 }
3231
3332 if hasUpdate {
34- fmt.Printf("\nA new version is available: %s\n", latestVersion)
35- fmt.Println("You can update using: gordon update")
33+ logger.Info("A new version is available", "version", latestVersion)
34+ logger.Info("You can update using: gordon update")
3635 }
3736 },
3837 }
@@ -1,10 +1,9 @@
1 package cmd1 package cmd
2 2
3 import (3 import (
4- "fmt"
5-
6 "github.com/bnema/gordon/internal/cli"4 "github.com/bnema/gordon/internal/cli"
7 "github.com/bnema/gordon/internal/common"5 "github.com/bnema/gordon/internal/common"
6+ "github.com/bnema/gordon/pkg/logger"
8 "github.com/spf13/cobra"7 "github.com/spf13/cobra"
9 )8 )
10 9
@@ -19,20 +18,20 @@ func NewVersionCommand(app *cli.App) *cobra.Command {
19 app.Config.Build.BuildDate,18 app.Config.Build.BuildDate,
20 app.Config.Build.ProxyURL,19 app.Config.Build.ProxyURL,
21 )20 )
22- fmt.Println(info.String())21+ logger.Info(info.String())
23 22
24 hasUpdate, latestVersion, err := common.CheckForNewVersion(23 hasUpdate, latestVersion, err := common.CheckForNewVersion(
25 info.Version,24 info.Version,
26 info.ProxyURL,25 info.ProxyURL,
27 )26 )
28 if err != nil {27 if err != nil {
29- fmt.Printf("Error checking for updates: %v\n", err)28+ logger.Error("Error checking for updates", "error", err)
30 return29 return
31 }30 }
32 31
33 if hasUpdate {32 if hasUpdate {
34- fmt.Printf("\nA new version is available: %s\n", latestVersion)33+ logger.Info("A new version is available", "version", latestVersion)
35- fmt.Println("You can update using: gordon update")34+ logger.Info("You can update using: gordon update")
36 }35 }
37 },36 },
38 }37 }
M internal/cli/handler/start_server.go +8 -8
@@ -9,7 +9,7 @@ import (
99
1010 "github.com/bnema/gordon/internal/httpserve"
1111 "github.com/bnema/gordon/internal/server"
12- "github.com/charmbracelet/log"
12+ "github.com/bnema/gordon/pkg/logger"
1313 "github.com/labstack/echo/v4"
1414 )
1515
@@ -18,7 +18,7 @@ import (
1818 func StartServer(a *server.App, port string) error {
1919 _, err := server.InitializeDB(a)
2020 if err != nil {
21- log.Fatal("Failed to initialize database:", err)
21+ logger.Fatal("Failed to initialize database:", err)
2222 }
2323
2424 // Start the session cleaner cron job
@@ -26,13 +26,13 @@ func StartServer(a *server.App, port string) error {
2626
2727 _, err = server.HandleNewTokenInitialization(a)
2828 if err != nil {
29- log.Print(err)
29+ logger.Error("Token initialization error", "error", err)
3030 }
3131
3232 // Initialize and start the reverse proxy using the already loaded configuration
3333 p, err := httpserve.InitializeProxy(a)
3434 if err != nil {
35- log.Error("Failed to initialize reverse proxy:", err)
35+ logger.Error("Failed to initialize reverse proxy", "error", err)
3636 // Continue even if proxy fails, as the main server should still work
3737 }
3838
@@ -42,12 +42,12 @@ func StartServer(a *server.App, port string) error {
4242
4343 go func() {
4444 sig := <-sigs
45- log.Info("Received signal:", sig)
45+ logger.Info("Received signal", "signal", sig)
4646
4747 // Stop the proxy if it was started
4848 if p != nil {
4949 if err := p.Stop(); err != nil {
50- log.Error("Failed to stop reverse proxy:", err)
50+ logger.Error("Failed to stop reverse proxy", "error", err)
5151 }
5252 }
5353
@@ -61,7 +61,7 @@ func StartServer(a *server.App, port string) error {
6161 e.HidePort = true
6262 e = httpserve.RegisterRoutes(e, a)
6363
64- log.Info(fmt.Sprintf("Starting server on port %s", port))
64+ logger.Info("Starting server", "port", port)
6565
6666 // Test admin connections after the server has started (in a separate goroutine)
6767 if p != nil {
@@ -73,7 +73,7 @@ func StartServer(a *server.App, port string) error {
7373 }
7474
7575 if err := e.Start(fmt.Sprintf(":%s", a.Config.Http.Port)); err != nil {
76- log.Fatal(err)
76+ logger.Fatal("Server error", "error", err)
7777 }
7878
7979 return nil
@@ -9,7 +9,7 @@ import (
9 9
10 "github.com/bnema/gordon/internal/httpserve"10 "github.com/bnema/gordon/internal/httpserve"
11 "github.com/bnema/gordon/internal/server"11 "github.com/bnema/gordon/internal/server"
12- "github.com/charmbracelet/log"12+ "github.com/bnema/gordon/pkg/logger"
13 "github.com/labstack/echo/v4"13 "github.com/labstack/echo/v4"
14 )14 )
15 15
@@ -18,7 +18,7 @@ import (
18 func StartServer(a *server.App, port string) error {18 func StartServer(a *server.App, port string) error {
19 _, err := server.InitializeDB(a)19 _, err := server.InitializeDB(a)
20 if err != nil {20 if err != nil {
21- log.Fatal("Failed to initialize database:", err)21+ logger.Fatal("Failed to initialize database:", err)
22 }22 }
23 23
24 // Start the session cleaner cron job24 // Start the session cleaner cron job
@@ -26,13 +26,13 @@ func StartServer(a *server.App, port string) error {
26 26
27 _, err = server.HandleNewTokenInitialization(a)27 _, err = server.HandleNewTokenInitialization(a)
28 if err != nil {28 if err != nil {
29- log.Print(err)29+ logger.Error("Token initialization error", "error", err)
30 }30 }
31 31
32 // Initialize and start the reverse proxy using the already loaded configuration32 // Initialize and start the reverse proxy using the already loaded configuration
33 p, err := httpserve.InitializeProxy(a)33 p, err := httpserve.InitializeProxy(a)
34 if err != nil {34 if err != nil {
35- log.Error("Failed to initialize reverse proxy:", err)35+ logger.Error("Failed to initialize reverse proxy", "error", err)
36 // Continue even if proxy fails, as the main server should still work36 // Continue even if proxy fails, as the main server should still work
37 }37 }
38 38
@@ -42,12 +42,12 @@ func StartServer(a *server.App, port string) error {
42 42
43 go func() {43 go func() {
44 sig := <-sigs44 sig := <-sigs
45- log.Info("Received signal:", sig)45+ logger.Info("Received signal", "signal", sig)
46 46
47 // Stop the proxy if it was started47 // Stop the proxy if it was started
48 if p != nil {48 if p != nil {
49 if err := p.Stop(); err != nil {49 if err := p.Stop(); err != nil {
50- log.Error("Failed to stop reverse proxy:", err)50+ logger.Error("Failed to stop reverse proxy", "error", err)
51 }51 }
52 }52 }
53 53
@@ -61,7 +61,7 @@ func StartServer(a *server.App, port string) error {
61 e.HidePort = true61 e.HidePort = true
62 e = httpserve.RegisterRoutes(e, a)62 e = httpserve.RegisterRoutes(e, a)
63 63
64- log.Info(fmt.Sprintf("Starting server on port %s", port))64+ logger.Info("Starting server", "port", port)
65 65
66 // Test admin connections after the server has started (in a separate goroutine)66 // Test admin connections after the server has started (in a separate goroutine)
67 if p != nil {67 if p != nil {
@@ -73,7 +73,7 @@ func StartServer(a *server.App, port string) error {
73 }73 }
74 74
75 if err := e.Start(fmt.Sprintf(":%s", a.Config.Http.Port)); err != nil {75 if err := e.Start(fmt.Sprintf(":%s", a.Config.Http.Port)); err != nil {
76- log.Fatal(err)76+ logger.Fatal("Server error", "error", err)
77 }77 }
78 78
79 return nil79 return nil
M internal/common/init_config.go +10 -58
@@ -12,12 +12,11 @@ import (
1212 "sync"
1313
1414 "github.com/bnema/gordon/pkg/docker"
15+ "github.com/bnema/gordon/pkg/logger"
1516 "github.com/bnema/gordon/pkg/parser"
16- "github.com/charmbracelet/log"
1717 )
1818
1919 var (
20- logger = log.NewWithOptions(os.Stderr, log.Options{Level: log.InfoLevel})
2120 globalConfig *Config
2221 globalConfigOnce sync.Once
2322 globalConfigMu sync.RWMutex
@@ -26,39 +25,8 @@ var (
2625
2726 // Initialize package-level logging configuration
2827 func init() {
29- // Enable timestamp reporting and set format
30- log.SetReportTimestamp(true)
31- log.SetTimeFormat("15:04:05")
32-
33- // First check for GORDON_LOG_LEVEL environment variable
34- if logLevelEnv := os.Getenv("GORDON_LOG_LEVEL"); logLevelEnv != "" {
35- // Convert string log level to charmbracelet/log level
36- var logLevel log.Level
37- switch strings.ToLower(logLevelEnv) {
38- case "debug":
39- logLevel = log.DebugLevel
40- case "info":
41- logLevel = log.InfoLevel
42- case "warn", "warning":
43- logLevel = log.WarnLevel
44- case "error":
45- logLevel = log.ErrorLevel
46- case "fatal":
47- logLevel = log.FatalLevel
48- default:
49- // Default to info level for unknown values
50- logLevel = log.InfoLevel
51- }
52-
53- // Set the logger level
54- logger.SetLevel(logLevel)
55- log.SetLevel(logLevel) // Set the global logger level too
56- logger.Debug("Log level set from environment variable", "level", logLevelEnv)
57- } else if os.Getenv("ENV") == "dev" {
58- // Fall back to ENV=dev behavior if GORDON_LOG_LEVEL is not set
59- logger.SetLevel(log.DebugLevel)
60- logger.Debug("Debug logging enabled from ENV=dev")
61- }
28+ // Configure the logger from environment variables
29+ logger.GetLogger().ConfigureFromEnv()
6230 }
6331
6432 type Config struct {
@@ -83,10 +51,12 @@ type AdminConfig struct {
8351 }
8452
8553 type GeneralConfig struct {
86- StorageDir string `yaml:"storageDir"`
87- Token string `yaml:"token"`
88- LogLevel string `yaml:"logLevel"`
54+ StorageDir string `yaml:"storageDir"`
55+ Token string `yaml:"token"`
56+ LogLevel string `yaml:"logLevel"`
57+ GordonContainerID string `yaml:"gordonContainerID"`
8958 }
59+
9060 type HttpConfig struct {
9161 Port string `yaml:"port"`
9262 Domain string `yaml:"domain"`
@@ -821,26 +791,8 @@ func (config *Config) LoadConfig() (*Config, error) {
821791
822792 // Apply the log level from configuration
823793 if config.General.LogLevel != "" {
824- // Convert string log level to charmbracelet/log level
825- var logLevel log.Level
826- switch strings.ToLower(config.General.LogLevel) {
827- case "debug":
828- logLevel = log.DebugLevel
829- case "info":
830- logLevel = log.InfoLevel
831- case "warn", "warning":
832- logLevel = log.WarnLevel
833- case "error":
834- logLevel = log.ErrorLevel
835- case "fatal":
836- logLevel = log.FatalLevel
837- default:
838- logger.Warn("Unknown log level specified, using default info level", "specified", config.General.LogLevel)
839- logLevel = log.InfoLevel
840- }
841-
842- // Set the logger level
843- logger.SetLevel(logLevel)
794+ // Set the log level using our centralized logger
795+ logger.GetLogger().SetLogLevel(config.General.LogLevel)
844796 logger.Debug("Log level set from configuration", "level", config.General.LogLevel)
845797 }
846798
@@ -12,12 +12,11 @@ import (
12 "sync"12 "sync"
13 13
14 "github.com/bnema/gordon/pkg/docker"14 "github.com/bnema/gordon/pkg/docker"
15+ "github.com/bnema/gordon/pkg/logger"
15 "github.com/bnema/gordon/pkg/parser"16 "github.com/bnema/gordon/pkg/parser"
16- "github.com/charmbracelet/log"
17 )17 )
18 18
19 var (19 var (
20- logger = log.NewWithOptions(os.Stderr, log.Options{Level: log.InfoLevel})
21 globalConfig *Config20 globalConfig *Config
22 globalConfigOnce sync.Once21 globalConfigOnce sync.Once
23 globalConfigMu sync.RWMutex22 globalConfigMu sync.RWMutex
@@ -26,39 +25,8 @@ var (
26 25
27 // Initialize package-level logging configuration26 // Initialize package-level logging configuration
28 func init() {27 func init() {
29- // Enable timestamp reporting and set format28+ // Configure the logger from environment variables
30- log.SetReportTimestamp(true)29+ logger.GetLogger().ConfigureFromEnv()
31- log.SetTimeFormat("15:04:05")
32-
33- // First check for GORDON_LOG_LEVEL environment variable
34- if logLevelEnv := os.Getenv("GORDON_LOG_LEVEL"); logLevelEnv != "" {
35- // Convert string log level to charmbracelet/log level
36- var logLevel log.Level
37- switch strings.ToLower(logLevelEnv) {
38- case "debug":
39- logLevel = log.DebugLevel
40- case "info":
41- logLevel = log.InfoLevel
42- case "warn", "warning":
43- logLevel = log.WarnLevel
44- case "error":
45- logLevel = log.ErrorLevel
46- case "fatal":
47- logLevel = log.FatalLevel
48- default:
49- // Default to info level for unknown values
50- logLevel = log.InfoLevel
51- }
52-
53- // Set the logger level
54- logger.SetLevel(logLevel)
55- log.SetLevel(logLevel) // Set the global logger level too
56- logger.Debug("Log level set from environment variable", "level", logLevelEnv)
57- } else if os.Getenv("ENV") == "dev" {
58- // Fall back to ENV=dev behavior if GORDON_LOG_LEVEL is not set
59- logger.SetLevel(log.DebugLevel)
60- logger.Debug("Debug logging enabled from ENV=dev")
61- }
62 }30 }
63 31
64 type Config struct {32 type Config struct {
@@ -83,10 +51,12 @@ type AdminConfig struct {
83 }51 }
84 52
85 type GeneralConfig struct {53 type GeneralConfig struct {
86- StorageDir string `yaml:"storageDir"`54+ StorageDir string `yaml:"storageDir"`
87- Token string `yaml:"token"`55+ Token string `yaml:"token"`
88- LogLevel string `yaml:"logLevel"`56+ LogLevel string `yaml:"logLevel"`
57+ GordonContainerID string `yaml:"gordonContainerID"`
89 }58 }
59+
90 type HttpConfig struct {60 type HttpConfig struct {
91 Port string `yaml:"port"`61 Port string `yaml:"port"`
92 Domain string `yaml:"domain"`62 Domain string `yaml:"domain"`
@@ -821,26 +791,8 @@ func (config *Config) LoadConfig() (*Config, error) {
821 791
822 // Apply the log level from configuration792 // Apply the log level from configuration
823 if config.General.LogLevel != "" {793 if config.General.LogLevel != "" {
824- // Convert string log level to charmbracelet/log level794+ // Set the log level using our centralized logger
825- var logLevel log.Level795+ logger.GetLogger().SetLogLevel(config.General.LogLevel)
826- switch strings.ToLower(config.General.LogLevel) {
827- case "debug":
828- logLevel = log.DebugLevel
829- case "info":
830- logLevel = log.InfoLevel
831- case "warn", "warning":
832- logLevel = log.WarnLevel
833- case "error":
834- logLevel = log.ErrorLevel
835- case "fatal":
836- logLevel = log.FatalLevel
837- default:
838- logger.Warn("Unknown log level specified, using default info level", "specified", config.General.LogLevel)
839- logLevel = log.InfoLevel
840- }
841-
842- // Set the logger level
843- logger.SetLevel(logLevel)
844 logger.Debug("Log level set from configuration", "level", config.General.LogLevel)796 logger.Debug("Log level set from configuration", "level", config.General.LogLevel)
845 }797 }
846 798
M internal/common/inputs.go +11 -12
@@ -6,13 +6,13 @@ import (
66 "os"
77 "strings"
88
9- "github.com/charmbracelet/log"
9+ "github.com/bnema/gordon/pkg/logger"
1010 )
1111
1212 // ReadUserInput reads a string input from the user with a prompt.
1313 // It handles multi-word inputs and properly checks for errors.
1414 func ReadUserInput(prompt string) string {
15- log.Debug("ReadUserInput: Prompting user for input", "prompt", prompt)
15+ logger.Debug("ReadUserInput: Prompting user for input", "prompt", prompt)
1616 // Make the prompt more visible with newlines and formatting
1717 fmt.Printf("\n>>> %s: ", prompt)
1818 // Flush to ensure the prompt is displayed
@@ -20,18 +20,18 @@ func ReadUserInput(prompt string) string {
2020
2121 // Use bufio.NewReader for better input handling
2222 reader := bufio.NewReader(os.Stdin)
23- log.Debug("ReadUserInput: Created reader, waiting for input...")
23+ logger.Debug("ReadUserInput: Created reader, waiting for input...")
2424
2525 // ReadString reads until newline, handling spaces in input
2626 input, err := reader.ReadString('\n')
2727 if err != nil {
28- log.Error("ReadUserInput: Error reading input", "error", err)
28+ logger.Error("ReadUserInput: Error reading input", "error", err)
2929 return ""
3030 }
3131
3232 // Trim spaces and newline characters
3333 result := strings.TrimSpace(input)
34- log.Debug("ReadUserInput: Received input", "value", result)
34+ logger.Debug("ReadUserInput: Received input", "value", result)
3535 return result
3636 }
3737
@@ -49,14 +49,13 @@ func ReadUserInputWithValidation(prompt string, validate func(string) bool) stri
4949
5050 // ReadUserInputWithDefault reads user input with a default value if input is empty.
5151 func ReadUserInputWithDefault(prompt string, defaultValue string) string {
52- fmt.Printf("%s (default: %s) ", prompt, defaultValue)
5352 // Flush to ensure the prompt is displayed
5453 os.Stdout.Sync()
5554
5655 reader := bufio.NewReader(os.Stdin)
5756 input, err := reader.ReadString('\n')
5857 if err != nil {
59- log.Error("Error reading input", "error", err)
58+ logger.Error("Error reading input", "error", err)
6059 return defaultValue
6160 }
6261
@@ -87,23 +86,23 @@ func ReadUserInputNonBlocking(prompt string, defaultValue string) string {
8786 // Check if we're in an interactive terminal
8887 stat, err := os.Stdin.Stat()
8988 if err != nil {
90- log.Error("ReadUserInputNonBlocking: Error checking terminal status", "error", err)
91- log.Info("Defaulting to non-interactive mode due to error", "default", defaultValue)
89+ logger.Error("ReadUserInputNonBlocking: Error checking terminal status", "error", err)
90+ logger.Info("Defaulting to non-interactive mode due to error", "default", defaultValue)
9291 return defaultValue
9392 }
9493
9594 isTerminal := (stat.Mode() & os.ModeCharDevice) != 0
96- log.Debug("ReadUserInputNonBlocking: Terminal status check",
95+ logger.Debug("ReadUserInputNonBlocking: Terminal status check",
9796 "isInteractive", isTerminal,
9897 "fileMode", stat.Mode().String())
9998
10099 if isTerminal {
101100 // In interactive terminal, use normal prompt
102- log.Debug("ReadUserInputNonBlocking: Using interactive prompt")
101+ logger.Debug("ReadUserInputNonBlocking: Using interactive prompt")
103102 return ReadUserInput(prompt)
104103 } else {
105104 // Not in interactive terminal, log and return default
106- log.Info("Non-interactive environment, using default for prompt",
105+ logger.Info("Non-interactive environment, using default for prompt",
107106 "prompt", prompt,
108107 "default", defaultValue)
109108 return defaultValue
@@ -6,13 +6,13 @@ import (
6 "os"6 "os"
7 "strings"7 "strings"
8 8
9- "github.com/charmbracelet/log"9+ "github.com/bnema/gordon/pkg/logger"
10 )10 )
11 11
12 // ReadUserInput reads a string input from the user with a prompt.12 // ReadUserInput reads a string input from the user with a prompt.
13 // It handles multi-word inputs and properly checks for errors.13 // It handles multi-word inputs and properly checks for errors.
14 func ReadUserInput(prompt string) string {14 func ReadUserInput(prompt string) string {
15- log.Debug("ReadUserInput: Prompting user for input", "prompt", prompt)15+ logger.Debug("ReadUserInput: Prompting user for input", "prompt", prompt)
16 // Make the prompt more visible with newlines and formatting16 // Make the prompt more visible with newlines and formatting
17 fmt.Printf("\n>>> %s: ", prompt)17 fmt.Printf("\n>>> %s: ", prompt)
18 // Flush to ensure the prompt is displayed18 // Flush to ensure the prompt is displayed
@@ -20,18 +20,18 @@ func ReadUserInput(prompt string) string {
20 20
21 // Use bufio.NewReader for better input handling21 // Use bufio.NewReader for better input handling
22 reader := bufio.NewReader(os.Stdin)22 reader := bufio.NewReader(os.Stdin)
23- log.Debug("ReadUserInput: Created reader, waiting for input...")23+ logger.Debug("ReadUserInput: Created reader, waiting for input...")
24 24
25 // ReadString reads until newline, handling spaces in input25 // ReadString reads until newline, handling spaces in input
26 input, err := reader.ReadString('\n')26 input, err := reader.ReadString('\n')
27 if err != nil {27 if err != nil {
28- log.Error("ReadUserInput: Error reading input", "error", err)28+ logger.Error("ReadUserInput: Error reading input", "error", err)
29 return ""29 return ""
30 }30 }
31 31
32 // Trim spaces and newline characters32 // Trim spaces and newline characters
33 result := strings.TrimSpace(input)33 result := strings.TrimSpace(input)
34- log.Debug("ReadUserInput: Received input", "value", result)34+ logger.Debug("ReadUserInput: Received input", "value", result)
35 return result35 return result
36 }36 }
37 37
@@ -49,14 +49,13 @@ func ReadUserInputWithValidation(prompt string, validate func(string) bool) stri
49 49
50 // ReadUserInputWithDefault reads user input with a default value if input is empty.50 // ReadUserInputWithDefault reads user input with a default value if input is empty.
51 func ReadUserInputWithDefault(prompt string, defaultValue string) string {51 func ReadUserInputWithDefault(prompt string, defaultValue string) string {
52- fmt.Printf("%s (default: %s) ", prompt, defaultValue)
53 // Flush to ensure the prompt is displayed52 // Flush to ensure the prompt is displayed
54 os.Stdout.Sync()53 os.Stdout.Sync()
55 54
56 reader := bufio.NewReader(os.Stdin)55 reader := bufio.NewReader(os.Stdin)
57 input, err := reader.ReadString('\n')56 input, err := reader.ReadString('\n')
58 if err != nil {57 if err != nil {
59- log.Error("Error reading input", "error", err)58+ logger.Error("Error reading input", "error", err)
60 return defaultValue59 return defaultValue
61 }60 }
62 61
@@ -87,23 +86,23 @@ func ReadUserInputNonBlocking(prompt string, defaultValue string) string {
87 // Check if we're in an interactive terminal86 // Check if we're in an interactive terminal
88 stat, err := os.Stdin.Stat()87 stat, err := os.Stdin.Stat()
89 if err != nil {88 if err != nil {
90- log.Error("ReadUserInputNonBlocking: Error checking terminal status", "error", err)89+ logger.Error("ReadUserInputNonBlocking: Error checking terminal status", "error", err)
91- log.Info("Defaulting to non-interactive mode due to error", "default", defaultValue)90+ logger.Info("Defaulting to non-interactive mode due to error", "default", defaultValue)
92 return defaultValue91 return defaultValue
93 }92 }
94 93
95 isTerminal := (stat.Mode() & os.ModeCharDevice) != 094 isTerminal := (stat.Mode() & os.ModeCharDevice) != 0
96- log.Debug("ReadUserInputNonBlocking: Terminal status check",95+ logger.Debug("ReadUserInputNonBlocking: Terminal status check",
97 "isInteractive", isTerminal,96 "isInteractive", isTerminal,
98 "fileMode", stat.Mode().String())97 "fileMode", stat.Mode().String())
99 98
100 if isTerminal {99 if isTerminal {
101 // In interactive terminal, use normal prompt100 // In interactive terminal, use normal prompt
102- log.Debug("ReadUserInputNonBlocking: Using interactive prompt")101+ logger.Debug("ReadUserInputNonBlocking: Using interactive prompt")
103 return ReadUserInput(prompt)102 return ReadUserInput(prompt)
104 } else {103 } else {
105 // Not in interactive terminal, log and return default104 // Not in interactive terminal, log and return default
106- log.Info("Non-interactive environment, using default for prompt",105+ logger.Info("Non-interactive environment, using default for prompt",
107 "prompt", prompt,106 "prompt", prompt,
108 "default", defaultValue)107 "default", defaultValue)
109 return defaultValue108 return defaultValue
M internal/common/version.go +3 -3
@@ -12,7 +12,7 @@ import (
1212 "time"
1313
1414 "github.com/Masterminds/semver/v3"
15- "github.com/charmbracelet/log"
15+ "github.com/bnema/gordon/pkg/logger"
1616 )
1717
1818 // Info represents version information
@@ -146,12 +146,12 @@ func CheckVersionPeriodically(info Info, checkInterval time.Duration) {
146146 func checkVersion(info Info) {
147147 hasUpdate, latestVersion, err := CheckForNewVersion(info.Version, info.ProxyURL)
148148 if err != nil {
149- log.Error("Failed to check for updates", "error", err)
149+ logger.Error("Failed to check for updates", "error", err)
150150 return
151151 }
152152
153153 if hasUpdate {
154- log.Info("New version available!",
154+ logger.Info("New version available!",
155155 "current", info.Version,
156156 "latest", latestVersion,
157157 "update_url", "https://github.com/bnema/gordon/releases/latest")
@@ -12,7 +12,7 @@ import (
12 "time"12 "time"
13 13
14 "github.com/Masterminds/semver/v3"14 "github.com/Masterminds/semver/v3"
15- "github.com/charmbracelet/log"15+ "github.com/bnema/gordon/pkg/logger"
16 )16 )
17 17
18 // Info represents version information18 // Info represents version information
@@ -146,12 +146,12 @@ func CheckVersionPeriodically(info Info, checkInterval time.Duration) {
146 func checkVersion(info Info) {146 func checkVersion(info Info) {
147 hasUpdate, latestVersion, err := CheckForNewVersion(info.Version, info.ProxyURL)147 hasUpdate, latestVersion, err := CheckForNewVersion(info.Version, info.ProxyURL)
148 if err != nil {148 if err != nil {
149- log.Error("Failed to check for updates", "error", err)149+ logger.Error("Failed to check for updates", "error", err)
150 return150 return
151 }151 }
152 152
153 if hasUpdate {153 if hasUpdate {
154- log.Info("New version available!",154+ logger.Info("New version available!",
155 "current", info.Version,155 "current", info.Version,
156 "latest", latestVersion,156 "latest", latestVersion,
157 "update_url", "https://github.com/bnema/gordon/releases/latest")157 "update_url", "https://github.com/bnema/gordon/releases/latest")
M internal/db/queries/sessions.go +3 -4
@@ -13,8 +13,7 @@ import (
1313 func createDBSession(a *server.App, browserInfo string, accessToken string, accountID string) error {
1414 expireTime := time.Now().Add(time.Hour * 24).Format(time.RFC3339)
1515 sessions := &db.Sessions{
16- ID: generateUUID(),
17- AccessToken: accessToken,
16+ ID: generateUUID(),
1817 }
1918
2019 query := "INSERT INTO sessions (id, account_id, access_token, browser_info, expires, is_online) VALUES (?, ?, ?, ?, ?, ?)"
@@ -45,7 +44,7 @@ func GetDBUserSession(a *server.App, accessToken string, browserInfo string) (*d
4544 return &a.DBTables.Sessions, nil
4645 }
4746
48-func updateDBSession(a *server.App, accessToken string, browserInfo string) error {
47+func updateDBSession(a *server.App, accessToken string) error {
4948 expireTime := time.Now().Add(time.Hour * 24).Format(time.RFC3339)
5049 query := "UPDATE sessions SET access_token = ?, expires = ? WHERE id = ?"
5150 _, err := a.DB.Exec(query, accessToken, expireTime, a.DBTables.Sessions.ID)
@@ -65,7 +64,7 @@ func CreateOrUpdateDBSession(a *server.App, accessToken string, browserInfo stri
6564
6665 // If the session exists, update the session
6766 if session != nil {
68- err := updateDBSession(a, accessToken, browserInfo)
67+ err := updateDBSession(a, accessToken)
6968 if err != nil {
7069 return fmt.Errorf("error updating session: %w", err)
7170 }
@@ -13,8 +13,7 @@ import (
13 func createDBSession(a *server.App, browserInfo string, accessToken string, accountID string) error {13 func createDBSession(a *server.App, browserInfo string, accessToken string, accountID string) error {
14 expireTime := time.Now().Add(time.Hour * 24).Format(time.RFC3339)14 expireTime := time.Now().Add(time.Hour * 24).Format(time.RFC3339)
15 sessions := &db.Sessions{15 sessions := &db.Sessions{
16- ID: generateUUID(),16+ ID: generateUUID(),
17- AccessToken: accessToken,
18 }17 }
19 18
20 query := "INSERT INTO sessions (id, account_id, access_token, browser_info, expires, is_online) VALUES (?, ?, ?, ?, ?, ?)"19 query := "INSERT INTO sessions (id, account_id, access_token, browser_info, expires, is_online) VALUES (?, ?, ?, ?, ?, ?)"
@@ -45,7 +44,7 @@ func GetDBUserSession(a *server.App, accessToken string, browserInfo string) (*d
45 return &a.DBTables.Sessions, nil44 return &a.DBTables.Sessions, nil
46 }45 }
47 46
48-func updateDBSession(a *server.App, accessToken string, browserInfo string) error {47+func updateDBSession(a *server.App, accessToken string) error {
49 expireTime := time.Now().Add(time.Hour * 24).Format(time.RFC3339)48 expireTime := time.Now().Add(time.Hour * 24).Format(time.RFC3339)
50 query := "UPDATE sessions SET access_token = ?, expires = ? WHERE id = ?"49 query := "UPDATE sessions SET access_token = ?, expires = ? WHERE id = ?"
51 _, err := a.DB.Exec(query, accessToken, expireTime, a.DBTables.Sessions.ID)50 _, err := a.DB.Exec(query, accessToken, expireTime, a.DBTables.Sessions.ID)
@@ -65,7 +64,7 @@ func CreateOrUpdateDBSession(a *server.App, accessToken string, browserInfo stri
65 64
66 // If the session exists, update the session65 // If the session exists, update the session
67 if session != nil {66 if session != nil {
68- err := updateDBSession(a, accessToken, browserInfo)67+ err := updateDBSession(a, accessToken)
69 if err != nil {68 if err != nil {
70 return fmt.Errorf("error updating session: %w", err)69 return fmt.Errorf("error updating session: %w", err)
71 }70 }
M internal/httpserve/handlers/login.go +16 -16
@@ -13,7 +13,7 @@ import (
1313 "github.com/bnema/gordon/internal/db/queries"
1414 "github.com/bnema/gordon/internal/server"
1515 "github.com/bnema/gordon/internal/templating/render"
16- "github.com/charmbracelet/log"
16+ "github.com/bnema/gordon/pkg/logger"
1717 "github.com/gorilla/sessions"
1818 _ "github.com/joho/godotenv/autoload"
1919 "github.com/labstack/echo-contrib/session"
@@ -51,12 +51,12 @@ func HandleTokenSubmission(c echo.Context, a *server.App) error {
5151 // Validate the token
5252 configToken, err := a.Config.GetToken()
5353 if err != nil {
54- log.Debug("Error getting token from config:", err)
54+ logger.Debug("Error getting token from config", "error", err)
5555 return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login?error=server_error")
5656 }
5757
5858 if urlToken != configToken {
59- log.Debug("Invalid token provided:", urlToken)
59+ logger.Debug("Invalid token provided", "token", urlToken)
6060 return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login?error=invalid_token")
6161 }
6262
@@ -95,57 +95,57 @@ func RenderLoginPage(c echo.Context, a *server.App) error {
9595 // StartOAuthGithub starts the Github OAuth flow.
9696
9797 func StartOAuthGithub(c echo.Context, a *server.App) error {
98- log.Info("Attempting to get an existing session.")
98+ logger.Info("Attempting to get an existing session")
9999 sess, err := getSession(c)
100100 if err != nil || sess == nil || sess.Values == nil {
101- log.Info("Session retrieval failed, initiating new GitHub OAuth flow.")
101+ logger.Info("Session retrieval failed, initiating new GitHub OAuth flow")
102102 return initiateGithubOAuthFlow(c, a)
103103 }
104104
105- log.Info("Attempting to assert session values.")
105+ logger.Info("Attempting to assert session values")
106106 sessionID, ok1 := sess.Values["sessionID"].(string)
107107 accountID, ok2 := sess.Values["accountID"].(string)
108108 authenticated, ok3 := sess.Values["authenticated"].(bool)
109109
110110 if !ok1 || !ok2 || !ok3 || !authenticated {
111- log.Info("Session assertion failed, or user not authenticated. Initiating new OAuth flow.")
111+ logger.Info("Session assertion failed, or user not authenticated. Initiating new OAuth flow")
112112 return initiateGithubOAuthFlow(c, a)
113113 }
114114
115- log.Info("Checking if session exists in DB.")
115+ logger.Info("Checking if session exists in DB")
116116 sessionExists, err := queries.CheckDBSessionExists(a, sessionID)
117117 if err != nil {
118- log.Info("Error checking if session exists", "error", err)
118+ logger.Info("Error checking if session exists", "error", err)
119119 return fmt.Errorf("could not check if session exists: %w", err)
120120 }
121121
122122 if !sessionExists {
123- log.Info("Session does not exist in DB, initiating new OAuth flow.")
123+ logger.Info("Session does not exist in DB, initiating new OAuth flow")
124124 return initiateGithubOAuthFlow(c, a)
125125 }
126126
127- log.Info("Getting the expiration time of the session.")
127+ logger.Info("Getting the expiration time of the session")
128128 currentTime := time.Now()
129129 sessionExpiration, err := queries.GetSessionExpiration(a, accountID, sessionID, currentTime)
130130 if err != nil {
131- log.Info("Error getting session expiration", "error", err)
131+ logger.Info("Error getting session expiration", "error", err)
132132 return fmt.Errorf("could not get session expiration: %w", err)
133133 }
134134
135135 if currentTime.After(sessionExpiration) {
136- log.Info("Current time is after session expiration, initiating new OAuth flow.")
136+ logger.Info("Current time is after session expiration, initiating new OAuth flow")
137137 return initiateGithubOAuthFlow(c, a)
138138 }
139139
140- log.Info("Extending session expiration time by 30 minutes.")
140+ logger.Info("Extending session expiration time by 30 minutes")
141141 newExpirationTime := currentTime.Add(30 * time.Minute)
142142 err = queries.ExtendSessionExpiration(a, accountID, sessionID, newExpirationTime)
143143 if err != nil {
144- log.Info("Error extending session expiration", "error", err)
144+ logger.Info("Error extending session expiration", "error", err)
145145 return fmt.Errorf("could not extend session expiration: %w", err)
146146 }
147147
148- log.Info("Session is valid and extended. Redirecting to admin panel.")
148+ logger.Info("Session is valid and extended. Redirecting to admin panel")
149149 return c.Redirect(http.StatusFound, a.Config.Admin.Path)
150150 }
151151
@@ -13,7 +13,7 @@ import (
13 "github.com/bnema/gordon/internal/db/queries"13 "github.com/bnema/gordon/internal/db/queries"
14 "github.com/bnema/gordon/internal/server"14 "github.com/bnema/gordon/internal/server"
15 "github.com/bnema/gordon/internal/templating/render"15 "github.com/bnema/gordon/internal/templating/render"
16- "github.com/charmbracelet/log"16+ "github.com/bnema/gordon/pkg/logger"
17 "github.com/gorilla/sessions"17 "github.com/gorilla/sessions"
18 _ "github.com/joho/godotenv/autoload"18 _ "github.com/joho/godotenv/autoload"
19 "github.com/labstack/echo-contrib/session"19 "github.com/labstack/echo-contrib/session"
@@ -51,12 +51,12 @@ func HandleTokenSubmission(c echo.Context, a *server.App) error {
51 // Validate the token51 // Validate the token
52 configToken, err := a.Config.GetToken()52 configToken, err := a.Config.GetToken()
53 if err != nil {53 if err != nil {
54- log.Debug("Error getting token from config:", err)54+ logger.Debug("Error getting token from config", "error", err)
55 return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login?error=server_error")55 return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login?error=server_error")
56 }56 }
57 57
58 if urlToken != configToken {58 if urlToken != configToken {
59- log.Debug("Invalid token provided:", urlToken)59+ logger.Debug("Invalid token provided", "token", urlToken)
60 return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login?error=invalid_token")60 return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login?error=invalid_token")
61 }61 }
62 62
@@ -95,57 +95,57 @@ func RenderLoginPage(c echo.Context, a *server.App) error {
95 // StartOAuthGithub starts the Github OAuth flow.95 // StartOAuthGithub starts the Github OAuth flow.
96 96
97 func StartOAuthGithub(c echo.Context, a *server.App) error {97 func StartOAuthGithub(c echo.Context, a *server.App) error {
98- log.Info("Attempting to get an existing session.")98+ logger.Info("Attempting to get an existing session")
99 sess, err := getSession(c)99 sess, err := getSession(c)
100 if err != nil || sess == nil || sess.Values == nil {100 if err != nil || sess == nil || sess.Values == nil {
101- log.Info("Session retrieval failed, initiating new GitHub OAuth flow.")101+ logger.Info("Session retrieval failed, initiating new GitHub OAuth flow")
102 return initiateGithubOAuthFlow(c, a)102 return initiateGithubOAuthFlow(c, a)
103 }103 }
104 104
105- log.Info("Attempting to assert session values.")105+ logger.Info("Attempting to assert session values")
106 sessionID, ok1 := sess.Values["sessionID"].(string)106 sessionID, ok1 := sess.Values["sessionID"].(string)
107 accountID, ok2 := sess.Values["accountID"].(string)107 accountID, ok2 := sess.Values["accountID"].(string)
108 authenticated, ok3 := sess.Values["authenticated"].(bool)108 authenticated, ok3 := sess.Values["authenticated"].(bool)
109 109
110 if !ok1 || !ok2 || !ok3 || !authenticated {110 if !ok1 || !ok2 || !ok3 || !authenticated {
111- log.Info("Session assertion failed, or user not authenticated. Initiating new OAuth flow.")111+ logger.Info("Session assertion failed, or user not authenticated. Initiating new OAuth flow")
112 return initiateGithubOAuthFlow(c, a)112 return initiateGithubOAuthFlow(c, a)
113 }113 }
114 114
115- log.Info("Checking if session exists in DB.")115+ logger.Info("Checking if session exists in DB")
116 sessionExists, err := queries.CheckDBSessionExists(a, sessionID)116 sessionExists, err := queries.CheckDBSessionExists(a, sessionID)
117 if err != nil {117 if err != nil {
118- log.Info("Error checking if session exists", "error", err)118+ logger.Info("Error checking if session exists", "error", err)
119 return fmt.Errorf("could not check if session exists: %w", err)119 return fmt.Errorf("could not check if session exists: %w", err)
120 }120 }
121 121
122 if !sessionExists {122 if !sessionExists {
123- log.Info("Session does not exist in DB, initiating new OAuth flow.")123+ logger.Info("Session does not exist in DB, initiating new OAuth flow")
124 return initiateGithubOAuthFlow(c, a)124 return initiateGithubOAuthFlow(c, a)
125 }125 }
126 126
127- log.Info("Getting the expiration time of the session.")127+ logger.Info("Getting the expiration time of the session")
128 currentTime := time.Now()128 currentTime := time.Now()
129 sessionExpiration, err := queries.GetSessionExpiration(a, accountID, sessionID, currentTime)129 sessionExpiration, err := queries.GetSessionExpiration(a, accountID, sessionID, currentTime)
130 if err != nil {130 if err != nil {
131- log.Info("Error getting session expiration", "error", err)131+ logger.Info("Error getting session expiration", "error", err)
132 return fmt.Errorf("could not get session expiration: %w", err)132 return fmt.Errorf("could not get session expiration: %w", err)
133 }133 }
134 134
135 if currentTime.After(sessionExpiration) {135 if currentTime.After(sessionExpiration) {
136- log.Info("Current time is after session expiration, initiating new OAuth flow.")136+ logger.Info("Current time is after session expiration, initiating new OAuth flow")
137 return initiateGithubOAuthFlow(c, a)137 return initiateGithubOAuthFlow(c, a)
138 }138 }
139 139
140- log.Info("Extending session expiration time by 30 minutes.")140+ logger.Info("Extending session expiration time by 30 minutes")
141 newExpirationTime := currentTime.Add(30 * time.Minute)141 newExpirationTime := currentTime.Add(30 * time.Minute)
142 err = queries.ExtendSessionExpiration(a, accountID, sessionID, newExpirationTime)142 err = queries.ExtendSessionExpiration(a, accountID, sessionID, newExpirationTime)
143 if err != nil {143 if err != nil {
144- log.Info("Error extending session expiration", "error", err)144+ logger.Info("Error extending session expiration", "error", err)
145 return fmt.Errorf("could not extend session expiration: %w", err)145 return fmt.Errorf("could not extend session expiration: %w", err)
146 }146 }
147 147
148- log.Info("Session is valid and extended. Redirecting to admin panel.")148+ logger.Info("Session is valid and extended. Redirecting to admin panel")
149 return c.Redirect(http.StatusFound, a.Config.Admin.Path)149 return c.Redirect(http.StatusFound, a.Config.Admin.Path)
150 }150 }
151 151
M internal/httpserve/middleware/error.go +2 -2
@@ -1,7 +1,7 @@
11 package middleware
22
33 import (
4- "github.com/charmbracelet/log"
4+ "github.com/bnema/gordon/pkg/logger"
55 "github.com/labstack/echo/v4"
66 )
77
@@ -9,7 +9,7 @@ func ErrorHandler(next echo.HandlerFunc) echo.HandlerFunc {
99 return func(c echo.Context) error {
1010 err := next(c)
1111 if err != nil {
12- log.Error("Caught http error",
12+ logger.Error("Caught http error",
1313 "path", c.Request().URL.Path,
1414 "method", c.Request().Method,
1515 "error", err,
@@ -1,7 +1,7 @@
1 package middleware1 package middleware
2 2
3 import (3 import (
4- "github.com/charmbracelet/log"4+ "github.com/bnema/gordon/pkg/logger"
5 "github.com/labstack/echo/v4"5 "github.com/labstack/echo/v4"
6 )6 )
7 7
@@ -9,7 +9,7 @@ func ErrorHandler(next echo.HandlerFunc) echo.HandlerFunc {
9 return func(c echo.Context) error {9 return func(c echo.Context) error {
10 err := next(c)10 err := next(c)
11 if err != nil {11 if err != nil {
12- log.Error("Caught http error",12+ logger.Error("Caught http error",
13 "path", c.Request().URL.Path,13 "path", c.Request().URL.Path,
14 "method", c.Request().Method,14 "method", c.Request().Method,
15 "error", err,15 "error", err,
M internal/httpserve/middleware/requiretoken.go +19 -19
@@ -12,7 +12,7 @@ import (
1212 "github.com/bnema/gordon/internal/cli/auth"
1313 "github.com/bnema/gordon/internal/db/queries"
1414 "github.com/bnema/gordon/internal/server"
15- "github.com/charmbracelet/log"
15+ "github.com/bnema/gordon/pkg/logger"
1616 "github.com/labstack/echo/v4"
1717 )
1818
@@ -86,13 +86,13 @@ func shouldUpdateSession(token string) bool {
8686
8787 if lastUpdate, exists := sessionUpdateCache.Load(key); exists {
8888 if now.Sub(lastUpdate.(time.Time)) < 5*time.Minute {
89- log.Debug("skipping session update", "reason", "too_recent")
89+ logger.Debug("skipping session update", "reason", "too_recent")
9090 return false
9191 }
9292 }
9393
9494 sessionUpdateCache.Store(key, now)
95- log.Debug("session update required")
95+ logger.Debug("session update required")
9696 return true
9797 }
9898
@@ -100,19 +100,19 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) {
100100 requestID := c.Response().Header().Get(echo.HeaderXRequestID)
101101 token := c.Request().Header.Get("Authorization")
102102 if token == "" {
103- log.Warn("missing token in validation request",
103+ logger.Warn("missing token in validation request",
104104 "request_id", requestID)
105105 return nil, echo.NewHTTPError(http.StatusUnauthorized, "Token is missing")
106106 }
107107
108108 token = strings.TrimPrefix(token, "Bearer ")
109- log.Debug("validating GitHub token",
109+ logger.Debug("validating GitHub token",
110110 "request_id", requestID)
111111
112112 client := &http.Client{}
113113 req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
114114 if err != nil {
115- log.Error("failed to create GitHub API request",
115+ logger.Error("failed to create GitHub API request",
116116 "request_id", requestID,
117117 "error", err)
118118 return nil, fmt.Errorf("failed to create request: %w", err)
@@ -123,7 +123,7 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) {
123123
124124 resp, err := client.Do(req)
125125 if err != nil {
126- log.Error("GitHub API request failed",
126+ logger.Error("GitHub API request failed",
127127 "request_id", requestID,
128128 "error", err)
129129 return nil, fmt.Errorf("failed to send request: %w", err)
@@ -131,7 +131,7 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) {
131131 defer resp.Body.Close()
132132
133133 if resp.StatusCode != http.StatusOK {
134- log.Warn("invalid GitHub token",
134+ logger.Warn("invalid GitHub token",
135135 "request_id", requestID,
136136 "status", resp.Status)
137137 return nil, echo.NewHTTPError(http.StatusUnauthorized, "Invalid token")
@@ -139,7 +139,7 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) {
139139
140140 body, err := io.ReadAll(resp.Body)
141141 if err != nil {
142- log.Error("failed to read GitHub response",
142+ logger.Error("failed to read GitHub response",
143143 "request_id", requestID,
144144 "error", err)
145145 return nil, fmt.Errorf("failed to read response body: %w", err)
@@ -147,19 +147,19 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) {
147147
148148 var githubUser GithubUserInfo
149149 if err := json.Unmarshal(body, &githubUser); err != nil {
150- log.Error("failed to parse GitHub user data",
150+ logger.Error("failed to parse GitHub user data",
151151 "request_id", requestID,
152152 "error", err)
153153 return nil, fmt.Errorf("failed to parse user data: %w", err)
154154 }
155155
156- log.Debug("GitHub user info fetched",
156+ logger.Debug("GitHub user info fetched",
157157 "request_id", requestID,
158158 "user", githubUser.Login)
159159
160160 emails, err := fetchGitHubUserEmails(token)
161161 if err != nil {
162- log.Error("failed to fetch GitHub user emails",
162+ logger.Error("failed to fetch GitHub user emails",
163163 "request_id", requestID,
164164 "user", githubUser.Login,
165165 "error", err)
@@ -167,7 +167,7 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) {
167167 }
168168 githubUser.Emails = emails
169169
170- log.Debug("completed GitHub user validation",
170+ logger.Debug("completed GitHub user validation",
171171 "request_id", requestID,
172172 "user", githubUser.Login,
173173 "email_count", len(emails))
@@ -176,12 +176,12 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) {
176176 }
177177
178178 func fetchGitHubUserEmails(token string) ([]string, error) {
179- log.Debug("fetching GitHub user emails")
179+ logger.Debug("fetching GitHub user emails")
180180
181181 client := &http.Client{}
182182 req, err := http.NewRequest("GET", "https://api.github.com/user/emails", nil)
183183 if err != nil {
184- log.Error("failed to create email request",
184+ logger.Error("failed to create email request",
185185 "error", err)
186186 return nil, fmt.Errorf("failed to create request: %w", err)
187187 }
@@ -191,14 +191,14 @@ func fetchGitHubUserEmails(token string) ([]string, error) {
191191
192192 resp, err := client.Do(req)
193193 if err != nil {
194- log.Error("failed to send email request",
194+ logger.Error("failed to send email request",
195195 "error", err)
196196 return nil, fmt.Errorf("failed to send request: %w", err)
197197 }
198198 defer resp.Body.Close()
199199
200200 if resp.StatusCode != http.StatusOK {
201- log.Warn("failed to fetch emails",
201+ logger.Warn("failed to fetch emails",
202202 "status", resp.Status)
203203 return nil, fmt.Errorf("failed to fetch user emails: %s", resp.Status)
204204 }
@@ -207,7 +207,7 @@ func fetchGitHubUserEmails(token string) ([]string, error) {
207207 Email string `json:"email"`
208208 }
209209 if err := json.NewDecoder(resp.Body).Decode(&emailsResp); err != nil {
210- log.Error("failed to parse email response",
210+ logger.Error("failed to parse email response",
211211 "error", err)
212212 return nil, fmt.Errorf("failed to parse email response: %w", err)
213213 }
@@ -217,7 +217,7 @@ func fetchGitHubUserEmails(token string) ([]string, error) {
217217 emails[i] = e.Email
218218 }
219219
220- log.Debug("fetched GitHub emails successfully",
220+ logger.Debug("fetched GitHub emails successfully",
221221 "email_count", len(emails))
222222 return emails, nil
223223 }
@@ -12,7 +12,7 @@ import (
12 "github.com/bnema/gordon/internal/cli/auth"12 "github.com/bnema/gordon/internal/cli/auth"
13 "github.com/bnema/gordon/internal/db/queries"13 "github.com/bnema/gordon/internal/db/queries"
14 "github.com/bnema/gordon/internal/server"14 "github.com/bnema/gordon/internal/server"
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
@@ -86,13 +86,13 @@ func shouldUpdateSession(token string) bool {
86 86
87 if lastUpdate, exists := sessionUpdateCache.Load(key); exists {87 if lastUpdate, exists := sessionUpdateCache.Load(key); exists {
88 if now.Sub(lastUpdate.(time.Time)) < 5*time.Minute {88 if now.Sub(lastUpdate.(time.Time)) < 5*time.Minute {
89- log.Debug("skipping session update", "reason", "too_recent")89+ logger.Debug("skipping session update", "reason", "too_recent")
90 return false90 return false
91 }91 }
92 }92 }
93 93
94 sessionUpdateCache.Store(key, now)94 sessionUpdateCache.Store(key, now)
95- log.Debug("session update required")95+ logger.Debug("session update required")
96 return true96 return true
97 }97 }
98 98
@@ -100,19 +100,19 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) {
100 requestID := c.Response().Header().Get(echo.HeaderXRequestID)100 requestID := c.Response().Header().Get(echo.HeaderXRequestID)
101 token := c.Request().Header.Get("Authorization")101 token := c.Request().Header.Get("Authorization")
102 if token == "" {102 if token == "" {
103- log.Warn("missing token in validation request",103+ logger.Warn("missing token in validation request",
104 "request_id", requestID)104 "request_id", requestID)
105 return nil, echo.NewHTTPError(http.StatusUnauthorized, "Token is missing")105 return nil, echo.NewHTTPError(http.StatusUnauthorized, "Token is missing")
106 }106 }
107 107
108 token = strings.TrimPrefix(token, "Bearer ")108 token = strings.TrimPrefix(token, "Bearer ")
109- log.Debug("validating GitHub token",109+ logger.Debug("validating GitHub token",
110 "request_id", requestID)110 "request_id", requestID)
111 111
112 client := &http.Client{}112 client := &http.Client{}
113 req, err := http.NewRequest("GET", "https://api.github.com/user", nil)113 req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
114 if err != nil {114 if err != nil {
115- log.Error("failed to create GitHub API request",115+ logger.Error("failed to create GitHub API request",
116 "request_id", requestID,116 "request_id", requestID,
117 "error", err)117 "error", err)
118 return nil, fmt.Errorf("failed to create request: %w", err)118 return nil, fmt.Errorf("failed to create request: %w", err)
@@ -123,7 +123,7 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) {
123 123
124 resp, err := client.Do(req)124 resp, err := client.Do(req)
125 if err != nil {125 if err != nil {
126- log.Error("GitHub API request failed",126+ logger.Error("GitHub API request failed",
127 "request_id", requestID,127 "request_id", requestID,
128 "error", err)128 "error", err)
129 return nil, fmt.Errorf("failed to send request: %w", err)129 return nil, fmt.Errorf("failed to send request: %w", err)
@@ -131,7 +131,7 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) {
131 defer resp.Body.Close()131 defer resp.Body.Close()
132 132
133 if resp.StatusCode != http.StatusOK {133 if resp.StatusCode != http.StatusOK {
134- log.Warn("invalid GitHub token",134+ logger.Warn("invalid GitHub token",
135 "request_id", requestID,135 "request_id", requestID,
136 "status", resp.Status)136 "status", resp.Status)
137 return nil, echo.NewHTTPError(http.StatusUnauthorized, "Invalid token")137 return nil, echo.NewHTTPError(http.StatusUnauthorized, "Invalid token")
@@ -139,7 +139,7 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) {
139 139
140 body, err := io.ReadAll(resp.Body)140 body, err := io.ReadAll(resp.Body)
141 if err != nil {141 if err != nil {
142- log.Error("failed to read GitHub response",142+ logger.Error("failed to read GitHub response",
143 "request_id", requestID,143 "request_id", requestID,
144 "error", err)144 "error", err)
145 return nil, fmt.Errorf("failed to read response body: %w", err)145 return nil, fmt.Errorf("failed to read response body: %w", err)
@@ -147,19 +147,19 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) {
147 147
148 var githubUser GithubUserInfo148 var githubUser GithubUserInfo
149 if err := json.Unmarshal(body, &githubUser); err != nil {149 if err := json.Unmarshal(body, &githubUser); err != nil {
150- log.Error("failed to parse GitHub user data",150+ logger.Error("failed to parse GitHub user data",
151 "request_id", requestID,151 "request_id", requestID,
152 "error", err)152 "error", err)
153 return nil, fmt.Errorf("failed to parse user data: %w", err)153 return nil, fmt.Errorf("failed to parse user data: %w", err)
154 }154 }
155 155
156- log.Debug("GitHub user info fetched",156+ logger.Debug("GitHub user info fetched",
157 "request_id", requestID,157 "request_id", requestID,
158 "user", githubUser.Login)158 "user", githubUser.Login)
159 159
160 emails, err := fetchGitHubUserEmails(token)160 emails, err := fetchGitHubUserEmails(token)
161 if err != nil {161 if err != nil {
162- log.Error("failed to fetch GitHub user emails",162+ logger.Error("failed to fetch GitHub user emails",
163 "request_id", requestID,163 "request_id", requestID,
164 "user", githubUser.Login,164 "user", githubUser.Login,
165 "error", err)165 "error", err)
@@ -167,7 +167,7 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) {
167 }167 }
168 githubUser.Emails = emails168 githubUser.Emails = emails
169 169
170- log.Debug("completed GitHub user validation",170+ logger.Debug("completed GitHub user validation",
171 "request_id", requestID,171 "request_id", requestID,
172 "user", githubUser.Login,172 "user", githubUser.Login,
173 "email_count", len(emails))173 "email_count", len(emails))
@@ -176,12 +176,12 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) {
176 }176 }
177 177
178 func fetchGitHubUserEmails(token string) ([]string, error) {178 func fetchGitHubUserEmails(token string) ([]string, error) {
179- log.Debug("fetching GitHub user emails")179+ logger.Debug("fetching GitHub user emails")
180 180
181 client := &http.Client{}181 client := &http.Client{}
182 req, err := http.NewRequest("GET", "https://api.github.com/user/emails", nil)182 req, err := http.NewRequest("GET", "https://api.github.com/user/emails", nil)
183 if err != nil {183 if err != nil {
184- log.Error("failed to create email request",184+ logger.Error("failed to create email request",
185 "error", err)185 "error", err)
186 return nil, fmt.Errorf("failed to create request: %w", err)186 return nil, fmt.Errorf("failed to create request: %w", err)
187 }187 }
@@ -191,14 +191,14 @@ func fetchGitHubUserEmails(token string) ([]string, error) {
191 191
192 resp, err := client.Do(req)192 resp, err := client.Do(req)
193 if err != nil {193 if err != nil {
194- log.Error("failed to send email request",194+ logger.Error("failed to send email request",
195 "error", err)195 "error", err)
196 return nil, fmt.Errorf("failed to send request: %w", err)196 return nil, fmt.Errorf("failed to send request: %w", err)
197 }197 }
198 defer resp.Body.Close()198 defer resp.Body.Close()
199 199
200 if resp.StatusCode != http.StatusOK {200 if resp.StatusCode != http.StatusOK {
201- log.Warn("failed to fetch emails",201+ logger.Warn("failed to fetch emails",
202 "status", resp.Status)202 "status", resp.Status)
203 return nil, fmt.Errorf("failed to fetch user emails: %s", resp.Status)203 return nil, fmt.Errorf("failed to fetch user emails: %s", resp.Status)
204 }204 }
@@ -207,7 +207,7 @@ func fetchGitHubUserEmails(token string) ([]string, error) {
207 Email string `json:"email"`207 Email string `json:"email"`
208 }208 }
209 if err := json.NewDecoder(resp.Body).Decode(&emailsResp); err != nil {209 if err := json.NewDecoder(resp.Body).Decode(&emailsResp); err != nil {
210- log.Error("failed to parse email response",210+ logger.Error("failed to parse email response",
211 "error", err)211 "error", err)
212 return nil, fmt.Errorf("failed to parse email response: %w", err)212 return nil, fmt.Errorf("failed to parse email response: %w", err)
213 }213 }
@@ -217,7 +217,7 @@ func fetchGitHubUserEmails(token string) ([]string, error) {
217 emails[i] = e.Email217 emails[i] = e.Email
218 }218 }
219 219
220- log.Debug("fetched GitHub emails successfully",220+ logger.Debug("fetched GitHub emails successfully",
221 "email_count", len(emails))221 "email_count", len(emails))
222 return emails, nil222 return emails, nil
223 }223 }
M internal/proxy/core.go +41 -10
@@ -1,6 +1,8 @@
11 package proxy
22
33 import (
4+ "os"
5+ "strings"
46 "sync"
57 "time"
68
@@ -8,6 +10,7 @@ import (
810
911 "github.com/bnema/gordon/internal/common"
1012 "github.com/bnema/gordon/internal/interfaces"
13+ "github.com/bnema/gordon/pkg/docker"
1114 "github.com/charmbracelet/log"
1215 "github.com/labstack/echo/v4"
1316 "golang.org/x/crypto/acme/autocert"
@@ -45,6 +48,9 @@ type Proxy struct {
4548
4649 // Flag to track specific domain certificate operations
4750 processingSpecificDomain bool
51+
52+ // Store Gordon's own container ID at startup for reliable identification
53+ gordonContainerID string
4854 }
4955
5056 // NewProxy creates a new instance of the reverse proxy
@@ -65,6 +71,30 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
6571 // Set up the config
6672 config := app.GetConfig().ReverseProxy
6773
74+ // Detect and store our container ID (if we're running in a container)
75+ var ourContainerID string
76+ if docker.IsRunningInContainer() {
77+ // Get our hostname which should be the container ID in Docker/Podman
78+ hostname := os.Getenv("HOSTNAME")
79+ if hostname != "" {
80+ containers, err := docker.ListRunningContainers()
81+ if err == nil {
82+ for _, container := range containers {
83+ // Check if this container is us by comparing hostname with container ID
84+ if strings.HasPrefix(hostname, container.ID) {
85+ // Store our container ID for future reference
86+ ourContainerID = container.ID
87+ containerName := strings.TrimLeft(container.Names[0], "/")
88+ log.Info("Gordon identity established",
89+ "container_id", ourContainerID,
90+ "container_name", containerName)
91+ break
92+ }
93+ }
94+ }
95+ }
96+ }
97+
6898 // Initialize the blacklist
6999 blacklistPath := app.GetConfig().General.StorageDir + "/blacklist.json"
70100 blacklist, err := NewBlacklist(blacklistPath)
@@ -77,16 +107,17 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
77107
78108 // Create the proxy
79109 p := &Proxy{
80- config: config,
81- app: app,
82- httpsServer: httpsServer,
83- httpServer: httpServer,
84- routes: routes,
85- serverStarted: false,
86- blacklist: blacklist,
87- blockedIPCounter: make(map[string]int),
88- lastBlockedLog: time.Time{}, // Zero time
89- recentlyBlocked: make(map[string]time.Time),
110+ config: config,
111+ app: app,
112+ httpsServer: httpsServer,
113+ httpServer: httpServer,
114+ routes: routes,
115+ serverStarted: false,
116+ blacklist: blacklist,
117+ blockedIPCounter: make(map[string]int),
118+ lastBlockedLog: time.Time{}, // Zero time
119+ recentlyBlocked: make(map[string]time.Time),
120+ gordonContainerID: ourContainerID,
90121 }
91122
92123 // Now add the middleware with the proxy reference
@@ -1,6 +1,8 @@
1 package proxy1 package proxy
2 2
3 import (3 import (
4+ "os"
5+ "strings"
4 "sync"6 "sync"
5 "time"7 "time"
6 8
@@ -8,6 +10,7 @@ import (
8 10
9 "github.com/bnema/gordon/internal/common"11 "github.com/bnema/gordon/internal/common"
10 "github.com/bnema/gordon/internal/interfaces"12 "github.com/bnema/gordon/internal/interfaces"
13+ "github.com/bnema/gordon/pkg/docker"
11 "github.com/charmbracelet/log"14 "github.com/charmbracelet/log"
12 "github.com/labstack/echo/v4"15 "github.com/labstack/echo/v4"
13 "golang.org/x/crypto/acme/autocert"16 "golang.org/x/crypto/acme/autocert"
@@ -45,6 +48,9 @@ type Proxy struct {
45 48
46 // Flag to track specific domain certificate operations49 // Flag to track specific domain certificate operations
47 processingSpecificDomain bool50 processingSpecificDomain bool
51+
52+ // Store Gordon's own container ID at startup for reliable identification
53+ gordonContainerID string
48 }54 }
49 55
50 // NewProxy creates a new instance of the reverse proxy56 // NewProxy creates a new instance of the reverse proxy
@@ -65,6 +71,30 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
65 // Set up the config71 // Set up the config
66 config := app.GetConfig().ReverseProxy72 config := app.GetConfig().ReverseProxy
67 73
74+ // Detect and store our container ID (if we're running in a container)
75+ var ourContainerID string
76+ if docker.IsRunningInContainer() {
77+ // Get our hostname which should be the container ID in Docker/Podman
78+ hostname := os.Getenv("HOSTNAME")
79+ if hostname != "" {
80+ containers, err := docker.ListRunningContainers()
81+ if err == nil {
82+ for _, container := range containers {
83+ // Check if this container is us by comparing hostname with container ID
84+ if strings.HasPrefix(hostname, container.ID) {
85+ // Store our container ID for future reference
86+ ourContainerID = container.ID
87+ containerName := strings.TrimLeft(container.Names[0], "/")
88+ log.Info("Gordon identity established",
89+ "container_id", ourContainerID,
90+ "container_name", containerName)
91+ break
92+ }
93+ }
94+ }
95+ }
96+ }
97+
68 // Initialize the blacklist98 // Initialize the blacklist
69 blacklistPath := app.GetConfig().General.StorageDir + "/blacklist.json"99 blacklistPath := app.GetConfig().General.StorageDir + "/blacklist.json"
70 blacklist, err := NewBlacklist(blacklistPath)100 blacklist, err := NewBlacklist(blacklistPath)
@@ -77,16 +107,17 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
77 107
78 // Create the proxy108 // Create the proxy
79 p := &Proxy{109 p := &Proxy{
80- config: config,110+ config: config,
81- app: app,111+ app: app,
82- httpsServer: httpsServer,112+ httpsServer: httpsServer,
83- httpServer: httpServer,113+ httpServer: httpServer,
84- routes: routes,114+ routes: routes,
85- serverStarted: false,115+ serverStarted: false,
86- blacklist: blacklist,116+ blacklist: blacklist,
87- blockedIPCounter: make(map[string]int),117+ blockedIPCounter: make(map[string]int),
88- lastBlockedLog: time.Time{}, // Zero time118+ lastBlockedLog: time.Time{}, // Zero time
89- recentlyBlocked: make(map[string]time.Time),119+ recentlyBlocked: make(map[string]time.Time),
120+ gordonContainerID: ourContainerID,
90 }121 }
91 122
92 // Now add the middleware with the proxy reference123 // Now add the middleware with the proxy reference
M internal/proxy/middleware.go +85 -16
@@ -7,6 +7,7 @@ import (
77 "time"
88
99 "github.com/charmbracelet/log"
10+ "github.com/google/uuid"
1011 "github.com/labstack/echo/v4"
1112 "github.com/labstack/echo/v4/middleware"
1213 "golang.org/x/time/rate"
@@ -20,18 +21,45 @@ var rateLimiterCleanup []func()
2021 // Create blacklistedIPs set to track IPs for middleware skipping
2122 type requestContext struct {
2223 blacklisted bool
24+ requestID string
2325 }
2426
27+// RequestIDKey is the key used to store the request ID in the context
28+const RequestIDKey = "request_id"
29+
2530 // setupMiddleware configures middleware for both HTTP and HTTPS servers
2631 func (p *Proxy) setupMiddleware() {
32+ // Configure Echo to trust X-Forwarded-* headers
33+ // Since we are the reverse proxy, we need to make sure we identify client IPs correctly
34+ p.httpsServer.IPExtractor = echo.ExtractIPFromXFFHeader()
35+ p.httpServer.IPExtractor = echo.ExtractIPFromXFFHeader()
36+
37+ // Add UUID generator middleware for both HTTP and HTTPS servers
38+ p.httpsServer.Use(p.createRequestIDMiddleware())
39+ p.httpServer.Use(p.createRequestIDMiddleware())
40+
41+ // Add debug middleware to log all headers
42+ p.httpsServer.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
43+ return func(c echo.Context) error {
44+ req := c.Request()
45+ log.Debug("Request headers",
46+ "method", req.Method,
47+ "path", req.URL.Path,
48+ "remote_addr", req.RemoteAddr,
49+ "x-forwarded-for", req.Header.Get("X-Forwarded-For"),
50+ "x-real-ip", req.Header.Get("X-Real-IP"))
51+ return next(c)
52+ }
53+ })
54+
2755 // Create blacklist middleware for HTTPS
2856 p.httpsServer.Use(p.createBlacklistMiddleware())
2957
3058 // Create a new Starskey rate limiter store for HTTPS server
3159 httpsRateLimiter, err := kv.NewStarskeyRateLimiterStore(
3260 "./data/ratelimiter/https", // Path to store rate limit data
33- 10, // Rate: 10 requests per second
34- 30, // Burst: 30 requests
61+ 2, // Rate: 2 requests per second (lowered from 10)
62+ 10, // Burst: 10 requests (lowered from 30)
3563 3*time.Minute, // Expires in 3 minutes
3664 )
3765 if err != nil {
@@ -41,8 +69,8 @@ func (p *Proxy) setupMiddleware() {
4169 p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
4270 Store: middleware.NewRateLimiterMemoryStoreWithConfig(
4371 middleware.RateLimiterMemoryStoreConfig{
44- Rate: rate.Limit(10), // 10 requests per second
45- Burst: 30, // Burst of 30 requests
72+ Rate: rate.Limit(2), // 2 requests per second (lowered from 10)
73+ Burst: 10, // Burst of 10 requests (lowered from 30)
4674 ExpiresIn: 3 * time.Minute, // Store expiration
4775 },
4876 ),
@@ -62,9 +90,14 @@ func (p *Proxy) setupMiddleware() {
6290 p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
6391 Store: httpsRateLimiter,
6492 IdentifierExtractor: func(ctx echo.Context) (string, error) {
65- // Use the client's real IP as the identifier
6693 id := ctx.RealIP()
67- log.Debug("Rate limiting HTTPS request", "ip", id)
94+ log.Debug("Rate limiting HTTPS request",
95+ "ip", id,
96+ "remote_addr", ctx.Request().RemoteAddr)
97+
98+ // Add a response header to show the detected IP (for debugging)
99+ ctx.Response().Header().Set("X-Rate-Limit-IP", id)
100+
68101 return id, nil
69102 },
70103 DenyHandler: func(context echo.Context, identifier string, err error) error {
@@ -84,9 +117,15 @@ func (p *Proxy) setupMiddleware() {
84117 })
85118 }
86119
87- // Add default Echo logger middleware only if enabled in config
120+ // Add custom logger middleware with request ID if enabled in config
88121 if p.config.EnableLogs {
89- p.httpsServer.Use(middleware.Logger())
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",
124+ CustomTimeFormat: "2006-01-02T15:04:05.00000Z07:00",
125+ Skipper: func(c echo.Context) bool {
126+ return !p.config.EnableLogs
127+ },
128+ }))
90129 log.Debug("HTTP request logging enabled for HTTPS server")
91130 } else {
92131 log.Debug("HTTP request logging disabled for HTTPS server")
@@ -98,8 +137,8 @@ func (p *Proxy) setupMiddleware() {
98137 // Create a new Starskey rate limiter store for HTTP server
99138 httpRateLimiter, err := kv.NewStarskeyRateLimiterStore(
100139 "./data/ratelimiter/http", // Path to store rate limit data
101- 10, // Rate: 10 requests per second
102- 30, // Burst: 30 requests
140+ 2, // Rate: 2 requests per second (lowered from 10)
141+ 10, // Burst: 10 requests (lowered from 30)
103142 3*time.Minute, // Expires in 3 minutes
104143 )
105144 if err != nil {
@@ -109,8 +148,8 @@ func (p *Proxy) setupMiddleware() {
109148 p.httpServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
110149 Store: middleware.NewRateLimiterMemoryStoreWithConfig(
111150 middleware.RateLimiterMemoryStoreConfig{
112- Rate: rate.Limit(10), // 10 requests per second
113- Burst: 30, // Burst of 30 requests
151+ Rate: rate.Limit(2), // 2 requests per second (lowered from 10)
152+ Burst: 10, // Burst of 10 requests (lowered from 30)
114153 ExpiresIn: 3 * time.Minute, // Store expiration
115154 },
116155 ),
@@ -130,9 +169,14 @@ func (p *Proxy) setupMiddleware() {
130169 p.httpServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
131170 Store: httpRateLimiter,
132171 IdentifierExtractor: func(ctx echo.Context) (string, error) {
133- // Use the client's real IP as the identifier
134172 id := ctx.RealIP()
135- log.Debug("Rate limiting HTTP request", "ip", id)
173+ log.Debug("Rate limiting HTTP request",
174+ "ip", id,
175+ "remote_addr", ctx.Request().RemoteAddr)
176+
177+ // Add a response header to show the detected IP (for debugging)
178+ ctx.Response().Header().Set("X-Rate-Limit-IP", id)
179+
136180 return id, nil
137181 },
138182 DenyHandler: func(context echo.Context, identifier string, err error) error {
@@ -152,15 +196,40 @@ func (p *Proxy) setupMiddleware() {
152196 })
153197 }
154198
155- // Add default Echo logger middleware for HTTP server only if enabled in config
199+ // Add custom logger middleware with request ID for HTTP server if enabled in config
156200 if p.config.EnableLogs {
157- p.httpServer.Use(middleware.Logger())
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",
203+ CustomTimeFormat: "2006-01-02T15:04:05.00000Z07:00",
204+ Skipper: func(c echo.Context) bool {
205+ return !p.config.EnableLogs
206+ },
207+ }))
158208 log.Debug("HTTP request logging enabled for HTTP server")
159209 } else {
160210 log.Debug("HTTP request logging disabled for HTTP server")
161211 }
162212 }
163213
214+// createRequestIDMiddleware returns a middleware function that generates a UUID for each request
215+func (p *Proxy) createRequestIDMiddleware() echo.MiddlewareFunc {
216+ return func(next echo.HandlerFunc) echo.HandlerFunc {
217+ return func(c echo.Context) error {
218+ // Generate a new UUID for this request
219+ requestID := uuid.New().String()
220+
221+ // Store the UUID in the context
222+ c.Set(RequestIDKey, requestID)
223+
224+ // Add the UUID as a response header
225+ c.Response().Header().Set(echo.HeaderXRequestID, requestID)
226+
227+ // Continue with the next middleware
228+ return next(c)
229+ }
230+ }
231+}
232+
164233 // createBlacklistMiddleware returns a middleware function that checks IPs against the blacklist
165234 func (p *Proxy) createBlacklistMiddleware() echo.MiddlewareFunc {
166235 return func(next echo.HandlerFunc) echo.HandlerFunc {
@@ -7,6 +7,7 @@ import (
7 "time"7 "time"
8 8
9 "github.com/charmbracelet/log"9 "github.com/charmbracelet/log"
10+ "github.com/google/uuid"
10 "github.com/labstack/echo/v4"11 "github.com/labstack/echo/v4"
11 "github.com/labstack/echo/v4/middleware"12 "github.com/labstack/echo/v4/middleware"
12 "golang.org/x/time/rate"13 "golang.org/x/time/rate"
@@ -20,18 +21,45 @@ var rateLimiterCleanup []func()
20 // Create blacklistedIPs set to track IPs for middleware skipping21 // Create blacklistedIPs set to track IPs for middleware skipping
21 type requestContext struct {22 type requestContext struct {
22 blacklisted bool23 blacklisted bool
24+ requestID string
23 }25 }
24 26
27+// RequestIDKey is the key used to store the request ID in the context
28+const RequestIDKey = "request_id"
29+
25 // setupMiddleware configures middleware for both HTTP and HTTPS servers30 // setupMiddleware configures middleware for both HTTP and HTTPS servers
26 func (p *Proxy) setupMiddleware() {31 func (p *Proxy) setupMiddleware() {
32+ // Configure Echo to trust X-Forwarded-* headers
33+ // Since we are the reverse proxy, we need to make sure we identify client IPs correctly
34+ p.httpsServer.IPExtractor = echo.ExtractIPFromXFFHeader()
35+ p.httpServer.IPExtractor = echo.ExtractIPFromXFFHeader()
36+
37+ // Add UUID generator middleware for both HTTP and HTTPS servers
38+ p.httpsServer.Use(p.createRequestIDMiddleware())
39+ p.httpServer.Use(p.createRequestIDMiddleware())
40+
41+ // Add debug middleware to log all headers
42+ p.httpsServer.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
43+ return func(c echo.Context) error {
44+ req := c.Request()
45+ log.Debug("Request headers",
46+ "method", req.Method,
47+ "path", req.URL.Path,
48+ "remote_addr", req.RemoteAddr,
49+ "x-forwarded-for", req.Header.Get("X-Forwarded-For"),
50+ "x-real-ip", req.Header.Get("X-Real-IP"))
51+ return next(c)
52+ }
53+ })
54+
27 // Create blacklist middleware for HTTPS55 // Create blacklist middleware for HTTPS
28 p.httpsServer.Use(p.createBlacklistMiddleware())56 p.httpsServer.Use(p.createBlacklistMiddleware())
29 57
30 // Create a new Starskey rate limiter store for HTTPS server58 // Create a new Starskey rate limiter store for HTTPS server
31 httpsRateLimiter, err := kv.NewStarskeyRateLimiterStore(59 httpsRateLimiter, err := kv.NewStarskeyRateLimiterStore(
32 "./data/ratelimiter/https", // Path to store rate limit data60 "./data/ratelimiter/https", // Path to store rate limit data
33- 10, // Rate: 10 requests per second61+ 2, // Rate: 2 requests per second (lowered from 10)
34- 30, // Burst: 30 requests62+ 10, // Burst: 10 requests (lowered from 30)
35 3*time.Minute, // Expires in 3 minutes63 3*time.Minute, // Expires in 3 minutes
36 )64 )
37 if err != nil {65 if err != nil {
@@ -41,8 +69,8 @@ func (p *Proxy) setupMiddleware() {
41 p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{69 p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
42 Store: middleware.NewRateLimiterMemoryStoreWithConfig(70 Store: middleware.NewRateLimiterMemoryStoreWithConfig(
43 middleware.RateLimiterMemoryStoreConfig{71 middleware.RateLimiterMemoryStoreConfig{
44- Rate: rate.Limit(10), // 10 requests per second72+ Rate: rate.Limit(2), // 2 requests per second (lowered from 10)
45- Burst: 30, // Burst of 30 requests73+ Burst: 10, // Burst of 10 requests (lowered from 30)
46 ExpiresIn: 3 * time.Minute, // Store expiration74 ExpiresIn: 3 * time.Minute, // Store expiration
47 },75 },
48 ),76 ),
@@ -62,9 +90,14 @@ func (p *Proxy) setupMiddleware() {
62 p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{90 p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
63 Store: httpsRateLimiter,91 Store: httpsRateLimiter,
64 IdentifierExtractor: func(ctx echo.Context) (string, error) {92 IdentifierExtractor: func(ctx echo.Context) (string, error) {
65- // Use the client's real IP as the identifier
66 id := ctx.RealIP()93 id := ctx.RealIP()
67- log.Debug("Rate limiting HTTPS request", "ip", id)94+ log.Debug("Rate limiting HTTPS request",
95+ "ip", id,
96+ "remote_addr", ctx.Request().RemoteAddr)
97+
98+ // Add a response header to show the detected IP (for debugging)
99+ ctx.Response().Header().Set("X-Rate-Limit-IP", id)
100+
68 return id, nil101 return id, nil
69 },102 },
70 DenyHandler: func(context echo.Context, identifier string, err error) error {103 DenyHandler: func(context echo.Context, identifier string, err error) error {
@@ -84,9 +117,15 @@ func (p *Proxy) setupMiddleware() {
84 })117 })
85 }118 }
86 119
87- // Add default Echo logger middleware only if enabled in config120+ // Add custom logger middleware with request ID if enabled in config
88 if p.config.EnableLogs {121 if p.config.EnableLogs {
89- p.httpsServer.Use(middleware.Logger())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",
124+ CustomTimeFormat: "2006-01-02T15:04:05.00000Z07:00",
125+ Skipper: func(c echo.Context) bool {
126+ return !p.config.EnableLogs
127+ },
128+ }))
90 log.Debug("HTTP request logging enabled for HTTPS server")129 log.Debug("HTTP request logging enabled for HTTPS server")
91 } else {130 } else {
92 log.Debug("HTTP request logging disabled for HTTPS server")131 log.Debug("HTTP request logging disabled for HTTPS server")
@@ -98,8 +137,8 @@ func (p *Proxy) setupMiddleware() {
98 // Create a new Starskey rate limiter store for HTTP server137 // Create a new Starskey rate limiter store for HTTP server
99 httpRateLimiter, err := kv.NewStarskeyRateLimiterStore(138 httpRateLimiter, err := kv.NewStarskeyRateLimiterStore(
100 "./data/ratelimiter/http", // Path to store rate limit data139 "./data/ratelimiter/http", // Path to store rate limit data
101- 10, // Rate: 10 requests per second140+ 2, // Rate: 2 requests per second (lowered from 10)
102- 30, // Burst: 30 requests141+ 10, // Burst: 10 requests (lowered from 30)
103 3*time.Minute, // Expires in 3 minutes142 3*time.Minute, // Expires in 3 minutes
104 )143 )
105 if err != nil {144 if err != nil {
@@ -109,8 +148,8 @@ func (p *Proxy) setupMiddleware() {
109 p.httpServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{148 p.httpServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
110 Store: middleware.NewRateLimiterMemoryStoreWithConfig(149 Store: middleware.NewRateLimiterMemoryStoreWithConfig(
111 middleware.RateLimiterMemoryStoreConfig{150 middleware.RateLimiterMemoryStoreConfig{
112- Rate: rate.Limit(10), // 10 requests per second151+ Rate: rate.Limit(2), // 2 requests per second (lowered from 10)
113- Burst: 30, // Burst of 30 requests152+ Burst: 10, // Burst of 10 requests (lowered from 30)
114 ExpiresIn: 3 * time.Minute, // Store expiration153 ExpiresIn: 3 * time.Minute, // Store expiration
115 },154 },
116 ),155 ),
@@ -130,9 +169,14 @@ func (p *Proxy) setupMiddleware() {
130 p.httpServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{169 p.httpServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
131 Store: httpRateLimiter,170 Store: httpRateLimiter,
132 IdentifierExtractor: func(ctx echo.Context) (string, error) {171 IdentifierExtractor: func(ctx echo.Context) (string, error) {
133- // Use the client's real IP as the identifier
134 id := ctx.RealIP()172 id := ctx.RealIP()
135- log.Debug("Rate limiting HTTP request", "ip", id)173+ log.Debug("Rate limiting HTTP request",
174+ "ip", id,
175+ "remote_addr", ctx.Request().RemoteAddr)
176+
177+ // Add a response header to show the detected IP (for debugging)
178+ ctx.Response().Header().Set("X-Rate-Limit-IP", id)
179+
136 return id, nil180 return id, nil
137 },181 },
138 DenyHandler: func(context echo.Context, identifier string, err error) error {182 DenyHandler: func(context echo.Context, identifier string, err error) error {
@@ -152,15 +196,40 @@ func (p *Proxy) setupMiddleware() {
152 })196 })
153 }197 }
154 198
155- // Add default Echo logger middleware for HTTP server only if enabled in config199+ // Add custom logger middleware with request ID for HTTP server if enabled in config
156 if p.config.EnableLogs {200 if p.config.EnableLogs {
157- p.httpServer.Use(middleware.Logger())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",
203+ CustomTimeFormat: "2006-01-02T15:04:05.00000Z07:00",
204+ Skipper: func(c echo.Context) bool {
205+ return !p.config.EnableLogs
206+ },
207+ }))
158 log.Debug("HTTP request logging enabled for HTTP server")208 log.Debug("HTTP request logging enabled for HTTP server")
159 } else {209 } else {
160 log.Debug("HTTP request logging disabled for HTTP server")210 log.Debug("HTTP request logging disabled for HTTP server")
161 }211 }
162 }212 }
163 213
214+// createRequestIDMiddleware returns a middleware function that generates a UUID for each request
215+func (p *Proxy) createRequestIDMiddleware() echo.MiddlewareFunc {
216+ return func(next echo.HandlerFunc) echo.HandlerFunc {
217+ return func(c echo.Context) error {
218+ // Generate a new UUID for this request
219+ requestID := uuid.New().String()
220+
221+ // Store the UUID in the context
222+ c.Set(RequestIDKey, requestID)
223+
224+ // Add the UUID as a response header
225+ c.Response().Header().Set(echo.HeaderXRequestID, requestID)
226+
227+ // Continue with the next middleware
228+ return next(c)
229+ }
230+ }
231+}
232+
164 // createBlacklistMiddleware returns a middleware function that checks IPs against the blacklist233 // createBlacklistMiddleware returns a middleware function that checks IPs against the blacklist
165 func (p *Proxy) createBlacklistMiddleware() echo.MiddlewareFunc {234 func (p *Proxy) createBlacklistMiddleware() echo.MiddlewareFunc {
166 return func(next echo.HandlerFunc) echo.HandlerFunc {235 return func(next echo.HandlerFunc) echo.HandlerFunc {
M internal/proxy/routes.go +777 -131
@@ -7,6 +7,7 @@ import (
77 "net/http"
88 "net/http/httputil"
99 "net/url"
10+ "os"
1011 "strings"
1112 "time"
1213
@@ -34,6 +35,20 @@ func (p *Proxy) loadRoutes() error {
3435
3536 log.Debug("Loading proxy routes from database")
3637
38+ // Now check if we have a Gordon container ID stored for identity purposes
39+ p.gordonContainerID = p.loadGordonContainerID()
40+ if p.gordonContainerID != "" {
41+ log.Info("Loaded Gordon container ID from config file during route initialization", "container_id", p.gordonContainerID)
42+ }
43+
44+ // Save the admin domain route if it exists
45+ adminDomain := p.app.GetConfig().Http.FullDomain()
46+ var adminRoute *ProxyRouteInfo
47+ if route, exists := p.routes[adminDomain]; exists {
48+ adminRoute = route
49+ log.Debug("Preserving admin domain route", "domain", adminDomain)
50+ }
51+
3752 // Query the database for active proxy routes
3853 rows, err := p.app.GetDB().Query(`
3954 SELECT pr.id, d.name, pr.container_id, pr.container_ip, pr.container_port, pr.protocol, pr.path, pr.active
@@ -44,15 +59,29 @@ func (p *Proxy) loadRoutes() error {
4459 if err != nil {
4560 if err == sql.ErrNoRows {
4661 log.Info("No active proxy routes found")
62+
63+ // Restore admin route if it was saved
64+ if adminRoute != nil {
65+ p.routes = make(map[string]*ProxyRouteInfo)
66+ p.routes[adminDomain] = adminRoute
67+ log.Debug("Restored admin domain route after empty database query", "domain", adminDomain)
68+ }
69+
4770 return nil
4871 }
4972 return fmt.Errorf("failed to query database: %w", err)
5073 }
5174 defer rows.Close()
5275
53- // Clear the routes map
76+ // Clear the routes map but preserve the admin domain route
5477 p.routes = make(map[string]*ProxyRouteInfo)
5578
79+ // Restore admin route if it was saved
80+ if adminRoute != nil {
81+ p.routes[adminDomain] = adminRoute
82+ log.Debug("Restored admin domain route after clearing routes map", "domain", adminDomain)
83+ }
84+
5685 // Populate the routes map
5786 for rows.Next() {
5887 var id, domain, containerID, containerIP, containerPort, protocol, path string
@@ -89,6 +118,47 @@ func (p *Proxy) configureRoutes() {
89118 adminDomain := p.app.GetConfig().Http.FullDomain()
90119 log.Debug("Configuring proxy routes", "admin_domain", adminDomain)
91120
121+ // Verify admin domain is in routes - if not, we need to add it
122+ p.mu.RLock()
123+ _, adminRouteExists := p.routes[adminDomain]
124+ p.mu.RUnlock()
125+
126+ if !adminRouteExists {
127+ log.Warn("Admin domain route is missing, attempting to recreate it")
128+ p.mu.Lock()
129+ // Auto-detect the Gordon container name
130+ containerName := p.detectGordonContainer()
131+
132+ // In a container environment, we need to use the host container IP
133+ // instead of 127.0.0.1 because each container has its own localhost
134+ containerIP := "localhost" // Default to localhost for most reliable connectivity
135+
136+ // Fall back options for container IP
137+ if os.Getenv("GORDON_ADMIN_HOST") != "" {
138+ // Allow explicit configuration via env var
139+ containerIP = os.Getenv("GORDON_ADMIN_HOST")
140+ } else if os.Getenv("HOSTNAME") != "" {
141+ // Use container's own hostname as they're on the same network
142+ containerIP = os.Getenv("HOSTNAME")
143+ }
144+
145+ p.routes[adminDomain] = &ProxyRouteInfo{
146+ Domain: adminDomain,
147+ ContainerIP: containerIP,
148+ ContainerPort: p.app.GetConfig().Http.Port,
149+ ContainerID: containerName,
150+ Protocol: "http", // Gordon server uses HTTP internally
151+ Path: "/",
152+ Active: true,
153+ }
154+ log.Info("Recreated admin domain route",
155+ "domain", adminDomain,
156+ "target", fmt.Sprintf("http://%s:%s", containerIP, p.app.GetConfig().Http.Port))
157+ p.mu.Unlock()
158+ } else {
159+ log.Debug("Admin domain route exists", "domain", adminDomain)
160+ }
161+
92162 // Handler function - will be assigned to both HTTP and HTTPS servers
93163 handler := echo.HandlerFunc(func(c echo.Context) error {
94164 host := c.Request().Host
@@ -260,7 +330,14 @@ func (p *Proxy) configureRoutes() {
260330 "target", targetURL.String(),
261331 "path", req.URL.Path,
262332 "clientIP", c.RealIP(),
333+ "remoteAddr", req.RemoteAddr,
263334 "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"))
264341 }
265342
266343 // Add error handling
@@ -474,6 +551,14 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr
474551
475552 // RemoveRoute removes a route from the database and reloads the proxy
476553 func (p *Proxy) RemoveRoute(domainName string) error {
554+ // Prevent removing the admin domain
555+ adminDomain := p.app.GetConfig().Http.FullDomain()
556+ if domainName == adminDomain {
557+ log.Warn("Attempt to remove admin domain route prevented",
558+ "domain", domainName)
559+ return fmt.Errorf("cannot remove admin domain route: %s", domainName)
560+ }
561+
477562 // Begin a transaction
478563 tx, err := p.app.GetDB().Begin()
479564 if err != nil {
@@ -535,6 +620,26 @@ func (p *Proxy) GetRoutes() map[string]*ProxyRouteInfo {
535620 func (p *Proxy) Reload() error {
536621 log.Info("Reloading proxy routes from database")
537622
623+ // Get admin domain - this should be the full subdomain.domain format
624+ adminDomain := p.app.GetConfig().Http.FullDomain()
625+
626+ // Save existing admin route if it exists
627+ p.mu.RLock()
628+ var adminRoute *ProxyRouteInfo
629+ if route, exists := p.routes[adminDomain]; exists {
630+ adminRoute = &ProxyRouteInfo{
631+ Domain: route.Domain,
632+ ContainerID: route.ContainerID,
633+ ContainerIP: route.ContainerIP,
634+ ContainerPort: route.ContainerPort,
635+ Protocol: route.Protocol,
636+ Path: route.Path,
637+ Active: route.Active,
638+ }
639+ log.Debug("Preserved admin domain route during reload", "domain", adminDomain)
640+ }
641+ p.mu.RUnlock()
642+
538643 // Create a new route map instead of modifying the existing one
539644 // This ensures a clean reload without any potential race conditions
540645 newRoutes := make(map[string]*ProxyRouteInfo)
@@ -575,7 +680,7 @@ func (p *Proxy) Reload() error {
575680 Active: active,
576681 }
577682
578- log.Info("Loaded route during reload",
683+ log.Debug("Loaded route during reload",
579684 "domain", domain,
580685 "containerID", containerID,
581686 "containerIP", containerIP,
@@ -583,12 +688,88 @@ func (p *Proxy) Reload() error {
583688 }
584689 }
585690
691+ // Check if admin domain was loaded from the database
692+ _, adminLoaded := newRoutes[adminDomain]
693+
694+ // If admin domain wasn't loaded from DB but we had it in memory, restore it
695+ if !adminLoaded && adminRoute != nil {
696+ newRoutes[adminDomain] = adminRoute
697+ log.Warn("Admin domain route missing from database, restoring from memory",
698+ "domain", adminDomain)
699+
700+ // Also try to add it back to the database
701+ err := p.AddRoute(
702+ adminDomain,
703+ adminRoute.ContainerID,
704+ adminRoute.ContainerIP,
705+ adminRoute.ContainerPort,
706+ adminRoute.Protocol,
707+ adminRoute.Path,
708+ )
709+
710+ if err != nil {
711+ log.Error("Failed to save admin route to database",
712+ "error", err,
713+ "domain", adminDomain)
714+ } else {
715+ log.Info("Re-added admin domain route to database", "domain", adminDomain)
716+ }
717+ }
718+
719+ // If admin domain is still missing, recreate it
720+ if _, exists := newRoutes[adminDomain]; !exists {
721+ log.Warn("Admin domain route is missing, recreating it", "domain", adminDomain)
722+
723+ // Auto-detect the Gordon container information
724+ containerName := p.detectGordonContainer()
725+ containerIP := "localhost" // Default to localhost
726+
727+ // Fall back options for container IP
728+ if os.Getenv("GORDON_ADMIN_HOST") != "" {
729+ containerIP = os.Getenv("GORDON_ADMIN_HOST")
730+ } else if os.Getenv("HOSTNAME") != "" {
731+ containerIP = os.Getenv("HOSTNAME")
732+ }
733+
734+ // Add to in-memory routes
735+ newRoutes[adminDomain] = &ProxyRouteInfo{
736+ Domain: adminDomain,
737+ ContainerID: containerName,
738+ ContainerIP: containerIP,
739+ ContainerPort: p.app.GetConfig().Http.Port,
740+ Protocol: "http", // Gordon server uses HTTP internally
741+ Path: "/",
742+ Active: true,
743+ }
744+
745+ // Add to database
746+ err := p.AddRoute(
747+ adminDomain,
748+ containerName,
749+ containerIP,
750+ p.app.GetConfig().Http.Port,
751+ "http",
752+ "/",
753+ )
754+
755+ if err != nil {
756+ log.Error("Failed to save recreated admin route to database",
757+ "error", err,
758+ "domain", adminDomain)
759+ } else {
760+ log.Info("Recreated and saved admin domain route", "domain", adminDomain)
761+ }
762+ }
763+
586764 // Atomically swap the new routes map with the old one
587765 // This ensures that no requests use a partially updated map
588766 p.mu.Lock()
589767 p.routes = newRoutes
590768 p.mu.Unlock()
591769
770+ // Clean up any root domain conflict
771+ p.CleanupRootDomainForAdmin()
772+
592773 // Rebuild route configuration
593774 p.configureRoutes()
594775
@@ -603,11 +784,6 @@ func (p *Proxy) Reload() error {
603784 p.httpsServer.Routes()
604785 }
605786
606- // Add a small delay to allow any in-flight requests to complete
607- // and ensure new requests use the updated routes
608- time.Sleep(100 * time.Millisecond)
609-
610- log.Info("Successfully reloaded proxy routes", "count", len(newRoutes))
611787 return nil
612788 }
613789
@@ -985,84 +1161,180 @@ func (p *Proxy) StartPeriodicIPVerification(interval time.Duration) {
9851161
9861162 // verifyAllContainerIPs checks all routes and verifies their container IPs
9871163 func (p *Proxy) verifyAllContainerIPs() {
988- // Get the network name from app config
989- networkName := p.app.GetConfig().ContainerEngine.Network
990- if networkName == "" {
991- log.Error("Cannot verify container IPs, network name is not configured")
992- return
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+ }
9931176 }
1177+ p.mu.RUnlock()
9941178
995- // Get network info
996- networkInfo, err := docker.GetNetworkInfo(networkName)
997- if err != nil {
998- log.Error("Failed to get network info", "error", err)
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")
9991183 return
10001184 }
10011185
1002- // Get a copy of the routes
1003- p.mu.RLock()
1004- routes := make(map[string]*ProxyRouteInfo, len(p.routes))
1005- for k, v := range p.routes {
1006- routes[k] = v
1007- }
1008- p.mu.RUnlock()
1186+ // Check each route and verify the container IP
1187+ routesUpdated := false
1188+ updatedIPs := make(map[string]string) // cache to avoid redundant lookups
10091189
10101190 // Track orphaned routes to mark as inactive
10111191 orphanedRoutes := []string{}
10121192
1013- // Check each route
1014- for domain, route := range routes {
1015- // Check if the container exists in the network
1016- containerEndpoint, exists := networkInfo.Containers[route.ContainerID]
1017- if !exists {
1018- // Container not found in the network
1019- // Could be stopped or removed
1020-
1021- // Check if container still exists
1022- if docker.ContainerExists(route.ContainerID) {
1023- // Container exists but not in our network
1024- log.Warn("Container exists but not in Gordon network",
1025- "domain", domain,
1026- "container_id", route.ContainerID)
1027- } else {
1028- // Container no longer exists
1029- log.Warn("Container no longer exists but route is active",
1030- "domain", domain,
1031- "container_id", route.ContainerID)
1032- orphanedRoutes = append(orphanedRoutes, domain)
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+ }
10331213 }
1034- continue
1214+ } else {
1215+ log.Warn("Failed to get Gordon container IP from network",
1216+ "container_id", gordonContainerID,
1217+ "error", err)
10351218 }
1219+ }
10361220
1037- // Extract container IP from network info
1038- containerIP := containerEndpoint.IPv4Address
1039- if containerIP == "" {
1040- // Try IPv6
1041- containerIP = containerEndpoint.IPv6Address
1221+ for domain, route := range routes {
1222+ if !route.Active {
1223+ continue
10421224 }
10431225
1044- // Extract just the IP part without subnet
1045- if idx := strings.Index(containerIP, "/"); idx > 0 {
1046- containerIP = containerIP[:idx]
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
10471235 }
10481236
1049- // Compare with the route's IP
1050- if containerIP != route.ContainerIP {
1051- log.Info("Container IP has changed, updating route",
1237+ // Check if this is for the Gordon container
1238+ isGordonContainer := (containerID == gordonContainerID)
1239+ if isGordonContainer {
1240+ log.Debug("Verifying Gordon container route",
10521241 "domain", domain,
1053- "container_id", route.ContainerID,
1054- "old_ip", route.ContainerIP,
1055- "new_ip", containerIP)
1242+ "container_id", containerID)
1243+ }
10561244
1057- // Update the route IP
1058- err := p.ForceUpdateRouteIP(domain, containerIP)
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)
10591258 if err != nil {
1060- log.Error("Failed to update route IP", "error", err)
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+
1285+ // If we still couldn't get the IP, skip this route
1286+ if currentIP == "" {
1287+ continue
1288+ }
1289+ }
1290+
1291+ // Cache the IP for future route checks
1292+ updatedIPs[containerID] = currentIP
1293+ }
1294+
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)
10611310 } else {
1062- log.Debug("Successfully updated route IP",
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",
10631322 "domain", domain,
1064- "new_ip", containerIP)
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+ }
10651332 }
1333+ } else {
1334+ log.Debug("Container IP verified and unchanged",
1335+ "domain", domain,
1336+ "container_id", containerID,
1337+ "ip", currentIP)
10661338 }
10671339 }
10681340
@@ -1070,9 +1342,17 @@ func (p *Proxy) verifyAllContainerIPs() {
10701342 if len(orphanedRoutes) > 0 {
10711343 log.Info("Marking orphaned routes as inactive", "count", len(orphanedRoutes))
10721344 p.markRoutesInactive(orphanedRoutes)
1345+ routesUpdated = true
10731346 }
10741347
1075- log.Debug("Completed IP verification", "routes_checked", len(routes), "orphaned_routes", len(orphanedRoutes))
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+ }
10761356 }
10771357
10781358 // markRoutesInactive marks the specified routes as inactive in the database and memory
@@ -1081,6 +1361,28 @@ func (p *Proxy) markRoutesInactive(domains []string) {
10811361 return
10821362 }
10831363
1364+ // Get the admin domain to protect it
1365+ adminDomain := p.app.GetConfig().Http.FullDomain()
1366+
1367+ // Filter out admin domain from domains to mark inactive
1368+ filteredDomains := make([]string, 0, len(domains))
1369+ for _, domain := range domains {
1370+ if domain != adminDomain {
1371+ filteredDomains = append(filteredDomains, domain)
1372+ } else {
1373+ log.Warn("Prevented admin domain from being marked inactive",
1374+ "domain", adminDomain)
1375+ }
1376+ }
1377+
1378+ // If all domains were filtered out, nothing to do
1379+ if len(filteredDomains) == 0 {
1380+ return
1381+ }
1382+
1383+ // Use the filtered domains from here on
1384+ domains = filteredDomains
1385+
10841386 // Begin a transaction
10851387 tx, err := p.app.GetDB().Begin()
10861388 if err != nil {
@@ -1137,6 +1439,9 @@ func (p *Proxy) DiscoverMissingRoutes() {
11371439 return
11381440 }
11391441
1442+ // Get the admin domain from the config
1443+ adminDomain := p.app.GetConfig().Http.FullDomain()
1444+
11401445 // Get all containers in the network
11411446 networkInfo, err := docker.GetNetworkInfo(networkName)
11421447 if err != nil {
@@ -1188,6 +1493,14 @@ func (p *Proxy) DiscoverMissingRoutes() {
11881493 continue
11891494 }
11901495
1496+ // Skip if this is the admin domain
1497+ if domainLabel == adminDomain {
1498+ log.Debug("Skipping auto-discovery for admin domain",
1499+ "domain", domainLabel,
1500+ "container_id", containerID)
1501+ continue
1502+ }
1503+
11911504 // Check if this domain already has a route (different container)
11921505 if _, exists := routes[domainLabel]; exists {
11931506 log.Warn("Domain already has a route with a different container",
@@ -1233,104 +1546,437 @@ func (p *Proxy) DiscoverMissingRoutes() {
12331546 log.Error("Failed to add missing route", "error", err)
12341547 }
12351548 }
1549+
1550+ // Add a debug log for routes after discovery
1551+ count := 0
1552+ p.mu.RLock()
1553+ for range p.routes {
1554+ count++
1555+ }
1556+ p.mu.RUnlock()
1557+ log.Debug("Routes after discovery", "count", count)
1558+
1559+ // Clean up any root domain conflict
1560+ p.CleanupRootDomainForAdmin()
12361561 }
12371562
1238-// LogActiveContainersAndRoutes prints a summary of all active containers and their routes
1239-// to the logs for debugging and monitoring purposes
1563+// LogActiveContainersAndRoutes logs a list of active containers and their routes
12401564 func (p *Proxy) LogActiveContainersAndRoutes() {
1241- // Get the network name from app config
1242- networkName := p.app.GetConfig().ContainerEngine.Network
1243- if networkName == "" {
1244- log.Error("Cannot list containers, network name is not configured")
1245- return
1565+ log.Info("---- Listing active containers and routes ----")
1566+
1567+ // Get the admin domain
1568+ adminDomain := p.app.GetConfig().Http.FullDomain()
1569+
1570+ // Check if admin domain is in routes
1571+ p.mu.RLock()
1572+ _, adminExists := p.routes[adminDomain]
1573+ activeRoutes := len(p.routes)
1574+ p.mu.RUnlock()
1575+
1576+ if !adminExists {
1577+ log.Warn("Admin domain is missing from routes", "domain", adminDomain)
12461578 }
12471579
1248- // Get all containers in the network
1249- networkInfo, err := docker.GetNetworkInfo(networkName)
1580+ log.Info(fmt.Sprintf("Found %d active routes", activeRoutes))
1581+
1582+ // Create a map of container IDs to their information
1583+ containerMap := make(map[string]struct {
1584+ Name string
1585+ IP string
1586+ Image string
1587+ Domains []string
1588+ Protocols []string
1589+ Ports []string
1590+ })
1591+
1592+ // Populate the map with route information
1593+ p.mu.RLock()
1594+ for domain, route := range p.routes {
1595+ if !route.Active {
1596+ continue
1597+ }
1598+
1599+ // If this container is not in the map yet, add it
1600+ if _, exists := containerMap[route.ContainerID]; !exists {
1601+ containerMap[route.ContainerID] = struct {
1602+ Name string
1603+ IP string
1604+ Image string
1605+ Domains []string
1606+ Protocols []string
1607+ Ports []string
1608+ }{
1609+ Name: route.ContainerID,
1610+ IP: route.ContainerIP,
1611+ Image: "unknown",
1612+ Domains: []string{domain},
1613+ Protocols: []string{route.Protocol},
1614+ Ports: []string{route.ContainerPort},
1615+ }
1616+ } else {
1617+ // Otherwise, append this domain to the container's domains
1618+ container := containerMap[route.ContainerID]
1619+ container.Domains = append(container.Domains, domain)
1620+ container.Protocols = append(container.Protocols, route.Protocol)
1621+ container.Ports = append(container.Ports, route.ContainerPort)
1622+ containerMap[route.ContainerID] = container
1623+ }
1624+ }
1625+ p.mu.RUnlock()
1626+
1627+ // Get container names and images
1628+ for containerID, containerData := range containerMap {
1629+ containerInfo, err := docker.GetContainerInfo(containerID)
1630+ if err == nil {
1631+ name, err := docker.GetContainerName(containerID)
1632+ if err == nil {
1633+ containerData.Name = strings.TrimPrefix(name, "/")
1634+ containerData.Image = containerInfo.Config.Image
1635+ containerMap[containerID] = containerData
1636+ }
1637+ }
1638+ }
1639+
1640+ // Display each container and its routes
1641+ for containerID, container := range containerMap {
1642+ // Log container info
1643+ // Safety check to prevent slice bounds panic
1644+ containerIDShort := containerID
1645+ if len(containerID) >= 12 {
1646+ containerIDShort = containerID[:12]
1647+ }
1648+ log.Info(fmt.Sprintf("Container: %s (%s)", container.Name, containerIDShort),
1649+ "ip", container.IP,
1650+ "image", container.Image)
1651+
1652+ // Log each domain for this container
1653+ for i, domain := range container.Domains {
1654+ log.Info(fmt.Sprintf(" ├─ Domain: %s", domain),
1655+ "protocol", container.Protocols[i],
1656+ "port", container.Ports[i])
1657+ }
1658+ }
1659+
1660+ log.Info("---- End of active containers and routes list ----")
1661+}
1662+
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()
12501673 if err != nil {
1251- log.Error("Failed to get network info for logging active containers", "error", err)
1674+ log.Error("Failed to save Gordon container ID to config file", "error", err)
12521675 return
12531676 }
12541677
1255- // Get all active routes
1256- p.mu.RLock()
1257- routes := make(map[string]*ProxyRouteInfo, len(p.routes))
1258- for k, v := range p.routes {
1259- if v.Active {
1260- routes[k] = v
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+
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)
1709+
1710+ // Verify the container still exists
1711+ containers, err := docker.ListRunningContainers()
1712+ if err == nil {
1713+ for _, container := range containers {
1714+ if container.ID == p.gordonContainerID {
1715+ containerName := strings.TrimLeft(container.Names[0], "/")
1716+ log.Debug("Identified Gordon container using config file ID", "container_name", containerName)
1717+ return containerName
1718+ }
1719+ }
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
12611723 }
12621724 }
1263- p.mu.RUnlock()
12641725
1265- // Map container IDs to names for more readable output
1266- containerNames := make(map[string]string)
1267- for containerID := range networkInfo.Containers {
1268- name, err := docker.GetContainerName(containerID)
1726+ // If we have our container ID stored from initialization, use that next
1727+ if p.gordonContainerID != "" {
1728+ log.Debug("Using stored Gordon container ID for identification", "container_id", p.gordonContainerID)
1729+
1730+ // Get the container name from the ID
1731+ containers, err := docker.ListRunningContainers()
12691732 if err == nil {
1270- containerNames[containerID] = strings.TrimPrefix(name, "/")
1271- } else {
1272- containerNames[containerID] = containerID[:12] + "..." // Shortened ID if name not available
1733+ for _, container := range containers {
1734+ if container.ID == p.gordonContainerID {
1735+ containerName := strings.TrimLeft(container.Names[0], "/")
1736+ log.Info("Gordon identity established via exact ID match",
1737+ "container_id", container.ID,
1738+ "container_name", containerName)
1739+ p.gordonContainerID = container.ID
1740+ // Save to config file for persistence
1741+ p.saveGordonContainerID(container.ID)
1742+ return containerName
1743+ }
1744+ }
1745+ // 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)
12731747 }
12741748 }
12751749
1276- // Format and log the active containers and routes
1277- log.Info("---- Listing active containers and routes ----")
1750+ // Check if we're running inside a container
1751+ if docker.IsRunningInContainer() {
1752+ log.Debug("Detected we're running in a container")
1753+
1754+ // Get our hostname which should be the container ID in Docker/Podman
1755+ hostname := os.Getenv("HOSTNAME")
1756+
1757+ if hostname != "" {
1758+ // Check if there's a container with this hostname
1759+ containers, err := docker.ListRunningContainers()
1760+ if err == nil {
1761+ // First try exact match (Podman often uses full ID as hostname)
1762+ for _, container := range containers {
1763+ if hostname == container.ID {
1764+ containerName := strings.TrimLeft(container.Names[0], "/")
1765+ log.Info("Gordon identity established via exact ID match",
1766+ "container_id", container.ID,
1767+ "container_name", containerName)
1768+ p.gordonContainerID = container.ID
1769+ // Save to config file for persistence
1770+ p.saveGordonContainerID(container.ID)
1771+ return containerName
1772+ }
1773+ }
12781774
1279- // Create a nice table-like format for the logs
1280- routeCount := len(routes)
1281- if routeCount == 0 {
1282- log.Info("No active routes found")
1283- } else {
1284- // Log header
1285- log.Info(fmt.Sprintf("Found %d active routes", routeCount))
1775+ // Then try prefix match (Docker often uses ID prefix as hostname)
1776+ for _, container := range containers {
1777+ if strings.HasPrefix(container.ID, hostname) || strings.HasPrefix(hostname, container.ID) {
1778+ containerName := strings.TrimLeft(container.Names[0], "/")
1779+ log.Info("Gordon identity established via ID prefix match",
1780+ "container_id", container.ID,
1781+ "container_name", containerName,
1782+ "hostname", hostname)
1783+ p.gordonContainerID = container.ID
1784+ // Save to config file for persistence
1785+ p.saveGordonContainerID(container.ID)
1786+ return containerName
1787+ }
1788+ }
1789+ } else {
1790+ log.Debug("Could not list containers when checking hostname", "error", err)
1791+ }
1792+ }
1793+ }
12861794
1287- // Group routes by container for better readability
1288- containerRoutes := make(map[string][]string)
1289- for domain, route := range routes {
1290- containerRoutes[route.ContainerID] = append(containerRoutes[route.ContainerID], domain)
1795+ // Look for containers with the name "gordon" first (Docker Compose default)
1796+ containers, err := docker.ListRunningContainers()
1797+ if err == nil {
1798+ for _, container := range containers {
1799+ containerName := strings.TrimLeft(container.Names[0], "/")
1800+ if containerName == "gordon" {
1801+ log.Info("Found Gordon container with exact name 'gordon'", "container_id", container.ID)
1802+ p.gordonContainerID = container.ID
1803+ // Save to config file for persistence
1804+ p.saveGordonContainerID(container.ID)
1805+ return containerName
1806+ }
12911807 }
12921808
1293- // Display each container and its routes
1294- for containerID, domains := range containerRoutes {
1295- containerInfo, err := docker.GetContainerInfo(containerID)
1296- if err != nil {
1297- log.Debug("Failed to get container info", "container_id", containerID)
1298- continue
1809+ // If we still haven't found it, look for containers with 'gordon' in the name
1810+ for _, container := range containers {
1811+ containerName := strings.TrimLeft(container.Names[0], "/")
1812+ if strings.Contains(strings.ToLower(containerName), "gordon") {
1813+ log.Info("Found Gordon container with partial name match",
1814+ "container_name", containerName,
1815+ "container_id", container.ID)
1816+ p.gordonContainerID = container.ID
1817+ // Save to config file for persistence
1818+ p.saveGordonContainerID(container.ID)
1819+ return containerName
12991820 }
1821+ }
1822+ }
13001823
1301- containerName := containerNames[containerID]
1302- if containerName == "" {
1303- containerName = containerID[:12] + "..." // Shortened ID if name not available
1824+ // Fallback to a default name
1825+ log.Warn("Could not auto-detect Gordon container, using fallback name", "container_name", "gordon")
1826+ return "gordon"
1827+}
1828+
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
13041863 }
1864+ defer tx.Rollback()
13051865
1306- // Get container IP from network info
1307- containerIP := ""
1308- if endpoint, exists := networkInfo.Containers[containerID]; exists {
1309- containerIP = endpoint.IPv4Address
1310- if containerIP == "" {
1311- containerIP = endpoint.IPv6Address
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)
13121872 }
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+ }
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()
13131914
1314- // Extract just the IP part without subnet
1315- if idx := strings.Index(containerIP, "/"); idx > 0 {
1316- containerIP = containerIP[:idx]
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)
13171921 }
1922+ return
13181923 }
13191924
1320- // Log container info
1321- log.Info(fmt.Sprintf("Container: %s (%s)", containerName, containerID[:12]),
1322- "ip", containerIP,
1323- "image", containerInfo.Config.Image)
1324-
1325- // Log each domain for this container
1326- for _, domain := range domains {
1327- route := routes[domain]
1328- log.Info(fmt.Sprintf(" ├─ Domain: %s", domain),
1329- "protocol", route.Protocol,
1330- "port", route.ContainerPort)
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+ }
1931+
1932+ // We don't delete the domain itself from the database to avoid affecting certificate management
1933+
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
13311938 }
1939+
1940+ // Remove root domain from in-memory routes
1941+ p.mu.Lock()
1942+ delete(p.routes, rootDomain)
1943+ p.mu.Unlock()
1944+
1945+ log.Info("Successfully removed conflicting root domain route",
1946+ "root_domain", rootDomain,
1947+ "admin_domain", adminDomain)
13321948 }
13331949 }
1950+}
13341951
1335- log.Info("---- End of active containers and routes list ----")
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+ }
1961+ }
1962+
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+ }
1972+ }
1973+
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+ }
13361982 }
@@ -7,6 +7,7 @@ import (
7 "net/http"7 "net/http"
8 "net/http/httputil"8 "net/http/httputil"
9 "net/url"9 "net/url"
10+ "os"
10 "strings"11 "strings"
11 "time"12 "time"
12 13
@@ -34,6 +35,20 @@ func (p *Proxy) loadRoutes() error {
34 35
35 log.Debug("Loading proxy routes from database")36 log.Debug("Loading proxy routes from database")
36 37
38+ // Now check if we have a Gordon container ID stored for identity purposes
39+ p.gordonContainerID = p.loadGordonContainerID()
40+ if p.gordonContainerID != "" {
41+ log.Info("Loaded Gordon container ID from config file during route initialization", "container_id", p.gordonContainerID)
42+ }
43+
44+ // Save the admin domain route if it exists
45+ adminDomain := p.app.GetConfig().Http.FullDomain()
46+ var adminRoute *ProxyRouteInfo
47+ if route, exists := p.routes[adminDomain]; exists {
48+ adminRoute = route
49+ log.Debug("Preserving admin domain route", "domain", adminDomain)
50+ }
51+
37 // Query the database for active proxy routes52 // Query the database for active proxy routes
38 rows, err := p.app.GetDB().Query(`53 rows, err := p.app.GetDB().Query(`
39 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
@@ -44,15 +59,29 @@ func (p *Proxy) loadRoutes() error {
44 if err != nil {59 if err != nil {
45 if err == sql.ErrNoRows {60 if err == sql.ErrNoRows {
46 log.Info("No active proxy routes found")61 log.Info("No active proxy routes found")
62+
63+ // Restore admin route if it was saved
64+ if adminRoute != nil {
65+ p.routes = make(map[string]*ProxyRouteInfo)
66+ p.routes[adminDomain] = adminRoute
67+ log.Debug("Restored admin domain route after empty database query", "domain", adminDomain)
68+ }
69+
47 return nil70 return nil
48 }71 }
49 return fmt.Errorf("failed to query database: %w", err)72 return fmt.Errorf("failed to query database: %w", err)
50 }73 }
51 defer rows.Close()74 defer rows.Close()
52 75
53- // Clear the routes map76+ // Clear the routes map but preserve the admin domain route
54 p.routes = make(map[string]*ProxyRouteInfo)77 p.routes = make(map[string]*ProxyRouteInfo)
55 78
79+ // Restore admin route if it was saved
80+ if adminRoute != nil {
81+ p.routes[adminDomain] = adminRoute
82+ log.Debug("Restored admin domain route after clearing routes map", "domain", adminDomain)
83+ }
84+
56 // Populate the routes map85 // Populate the routes map
57 for rows.Next() {86 for rows.Next() {
58 var id, domain, containerID, containerIP, containerPort, protocol, path string87 var id, domain, containerID, containerIP, containerPort, protocol, path string
@@ -89,6 +118,47 @@ func (p *Proxy) configureRoutes() {
89 adminDomain := p.app.GetConfig().Http.FullDomain()118 adminDomain := p.app.GetConfig().Http.FullDomain()
90 log.Debug("Configuring proxy routes", "admin_domain", adminDomain)119 log.Debug("Configuring proxy routes", "admin_domain", adminDomain)
91 120
121+ // Verify admin domain is in routes - if not, we need to add it
122+ p.mu.RLock()
123+ _, adminRouteExists := p.routes[adminDomain]
124+ p.mu.RUnlock()
125+
126+ if !adminRouteExists {
127+ log.Warn("Admin domain route is missing, attempting to recreate it")
128+ p.mu.Lock()
129+ // Auto-detect the Gordon container name
130+ containerName := p.detectGordonContainer()
131+
132+ // In a container environment, we need to use the host container IP
133+ // instead of 127.0.0.1 because each container has its own localhost
134+ containerIP := "localhost" // Default to localhost for most reliable connectivity
135+
136+ // Fall back options for container IP
137+ if os.Getenv("GORDON_ADMIN_HOST") != "" {
138+ // Allow explicit configuration via env var
139+ containerIP = os.Getenv("GORDON_ADMIN_HOST")
140+ } else if os.Getenv("HOSTNAME") != "" {
141+ // Use container's own hostname as they're on the same network
142+ containerIP = os.Getenv("HOSTNAME")
143+ }
144+
145+ p.routes[adminDomain] = &ProxyRouteInfo{
146+ Domain: adminDomain,
147+ ContainerIP: containerIP,
148+ ContainerPort: p.app.GetConfig().Http.Port,
149+ ContainerID: containerName,
150+ Protocol: "http", // Gordon server uses HTTP internally
151+ Path: "/",
152+ Active: true,
153+ }
154+ log.Info("Recreated admin domain route",
155+ "domain", adminDomain,
156+ "target", fmt.Sprintf("http://%s:%s", containerIP, p.app.GetConfig().Http.Port))
157+ p.mu.Unlock()
158+ } else {
159+ log.Debug("Admin domain route exists", "domain", adminDomain)
160+ }
161+
92 // Handler function - will be assigned to both HTTP and HTTPS servers162 // Handler function - will be assigned to both HTTP and HTTPS servers
93 handler := echo.HandlerFunc(func(c echo.Context) error {163 handler := echo.HandlerFunc(func(c echo.Context) error {
94 host := c.Request().Host164 host := c.Request().Host
@@ -260,7 +330,14 @@ func (p *Proxy) configureRoutes() {
260 "target", targetURL.String(),330 "target", targetURL.String(),
261 "path", req.URL.Path,331 "path", req.URL.Path,
262 "clientIP", c.RealIP(),332 "clientIP", c.RealIP(),
333+ "remoteAddr", req.RemoteAddr,
263 "originalProtocol", route.Protocol)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"))
264 }341 }
265 342
266 // Add error handling343 // Add error handling
@@ -474,6 +551,14 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr
474 551
475 // RemoveRoute removes a route from the database and reloads the proxy552 // RemoveRoute removes a route from the database and reloads the proxy
476 func (p *Proxy) RemoveRoute(domainName string) error {553 func (p *Proxy) RemoveRoute(domainName string) error {
554+ // Prevent removing the admin domain
555+ adminDomain := p.app.GetConfig().Http.FullDomain()
556+ if domainName == adminDomain {
557+ log.Warn("Attempt to remove admin domain route prevented",
558+ "domain", domainName)
559+ return fmt.Errorf("cannot remove admin domain route: %s", domainName)
560+ }
561+
477 // Begin a transaction562 // Begin a transaction
478 tx, err := p.app.GetDB().Begin()563 tx, err := p.app.GetDB().Begin()
479 if err != nil {564 if err != nil {
@@ -535,6 +620,26 @@ func (p *Proxy) GetRoutes() map[string]*ProxyRouteInfo {
535 func (p *Proxy) Reload() error {620 func (p *Proxy) Reload() error {
536 log.Info("Reloading proxy routes from database")621 log.Info("Reloading proxy routes from database")
537 622
623+ // Get admin domain - this should be the full subdomain.domain format
624+ adminDomain := p.app.GetConfig().Http.FullDomain()
625+
626+ // Save existing admin route if it exists
627+ p.mu.RLock()
628+ var adminRoute *ProxyRouteInfo
629+ if route, exists := p.routes[adminDomain]; exists {
630+ adminRoute = &ProxyRouteInfo{
631+ Domain: route.Domain,
632+ ContainerID: route.ContainerID,
633+ ContainerIP: route.ContainerIP,
634+ ContainerPort: route.ContainerPort,
635+ Protocol: route.Protocol,
636+ Path: route.Path,
637+ Active: route.Active,
638+ }
639+ log.Debug("Preserved admin domain route during reload", "domain", adminDomain)
640+ }
641+ p.mu.RUnlock()
642+
538 // Create a new route map instead of modifying the existing one643 // Create a new route map instead of modifying the existing one
539 // This ensures a clean reload without any potential race conditions644 // This ensures a clean reload without any potential race conditions
540 newRoutes := make(map[string]*ProxyRouteInfo)645 newRoutes := make(map[string]*ProxyRouteInfo)
@@ -575,7 +680,7 @@ func (p *Proxy) Reload() error {
575 Active: active,680 Active: active,
576 }681 }
577 682
578- log.Info("Loaded route during reload",683+ log.Debug("Loaded route during reload",
579 "domain", domain,684 "domain", domain,
580 "containerID", containerID,685 "containerID", containerID,
581 "containerIP", containerIP,686 "containerIP", containerIP,
@@ -583,12 +688,88 @@ func (p *Proxy) Reload() error {
583 }688 }
584 }689 }
585 690
691+ // Check if admin domain was loaded from the database
692+ _, adminLoaded := newRoutes[adminDomain]
693+
694+ // If admin domain wasn't loaded from DB but we had it in memory, restore it
695+ if !adminLoaded && adminRoute != nil {
696+ newRoutes[adminDomain] = adminRoute
697+ log.Warn("Admin domain route missing from database, restoring from memory",
698+ "domain", adminDomain)
699+
700+ // Also try to add it back to the database
701+ err := p.AddRoute(
702+ adminDomain,
703+ adminRoute.ContainerID,
704+ adminRoute.ContainerIP,
705+ adminRoute.ContainerPort,
706+ adminRoute.Protocol,
707+ adminRoute.Path,
708+ )
709+
710+ if err != nil {
711+ log.Error("Failed to save admin route to database",
712+ "error", err,
713+ "domain", adminDomain)
714+ } else {
715+ log.Info("Re-added admin domain route to database", "domain", adminDomain)
716+ }
717+ }
718+
719+ // If admin domain is still missing, recreate it
720+ if _, exists := newRoutes[adminDomain]; !exists {
721+ log.Warn("Admin domain route is missing, recreating it", "domain", adminDomain)
722+
723+ // Auto-detect the Gordon container information
724+ containerName := p.detectGordonContainer()
725+ containerIP := "localhost" // Default to localhost
726+
727+ // Fall back options for container IP
728+ if os.Getenv("GORDON_ADMIN_HOST") != "" {
729+ containerIP = os.Getenv("GORDON_ADMIN_HOST")
730+ } else if os.Getenv("HOSTNAME") != "" {
731+ containerIP = os.Getenv("HOSTNAME")
732+ }
733+
734+ // Add to in-memory routes
735+ newRoutes[adminDomain] = &ProxyRouteInfo{
736+ Domain: adminDomain,
737+ ContainerID: containerName,
738+ ContainerIP: containerIP,
739+ ContainerPort: p.app.GetConfig().Http.Port,
740+ Protocol: "http", // Gordon server uses HTTP internally
741+ Path: "/",
742+ Active: true,
743+ }
744+
745+ // Add to database
746+ err := p.AddRoute(
747+ adminDomain,
748+ containerName,
749+ containerIP,
750+ p.app.GetConfig().Http.Port,
751+ "http",
752+ "/",
753+ )
754+
755+ if err != nil {
756+ log.Error("Failed to save recreated admin route to database",
757+ "error", err,
758+ "domain", adminDomain)
759+ } else {
760+ log.Info("Recreated and saved admin domain route", "domain", adminDomain)
761+ }
762+ }
763+
586 // Atomically swap the new routes map with the old one764 // Atomically swap the new routes map with the old one
587 // This ensures that no requests use a partially updated map765 // This ensures that no requests use a partially updated map
588 p.mu.Lock()766 p.mu.Lock()
589 p.routes = newRoutes767 p.routes = newRoutes
590 p.mu.Unlock()768 p.mu.Unlock()
591 769
770+ // Clean up any root domain conflict
771+ p.CleanupRootDomainForAdmin()
772+
592 // Rebuild route configuration773 // Rebuild route configuration
593 p.configureRoutes()774 p.configureRoutes()
594 775
@@ -603,11 +784,6 @@ func (p *Proxy) Reload() error {
603 p.httpsServer.Routes()784 p.httpsServer.Routes()
604 }785 }
605 786
606- // Add a small delay to allow any in-flight requests to complete
607- // and ensure new requests use the updated routes
608- time.Sleep(100 * time.Millisecond)
609-
610- log.Info("Successfully reloaded proxy routes", "count", len(newRoutes))
611 return nil787 return nil
612 }788 }
613 789
@@ -985,84 +1161,180 @@ func (p *Proxy) StartPeriodicIPVerification(interval time.Duration) {
985 1161
986 // verifyAllContainerIPs checks all routes and verifies their container IPs1162 // verifyAllContainerIPs checks all routes and verifies their container IPs
987 func (p *Proxy) verifyAllContainerIPs() {1163 func (p *Proxy) verifyAllContainerIPs() {
988- // Get the network name from app config1164+ p.mu.RLock()
989- networkName := p.app.GetConfig().ContainerEngine.Network1165+ routes := make(map[string]*ProxyRouteInfo, len(p.routes))
990- if networkName == "" {1166+ for domain, route := range p.routes {
991- log.Error("Cannot verify container IPs, network name is not configured")1167+ routes[domain] = &ProxyRouteInfo{
992- return1168+ 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+ }
993 }1176 }
1177+ p.mu.RUnlock()
994 1178
995- // Get network info1179+ // Get network name from config
996- networkInfo, err := docker.GetNetworkInfo(networkName)1180+ networkName := p.app.GetConfig().ContainerEngine.Network
997- if err != nil {1181+ if networkName == "" {
998- log.Error("Failed to get network info", "error", err)1182+ log.Error("Container network name is not configured")
999 return1183 return
1000 }1184 }
1001 1185
1002- // Get a copy of the routes1186+ // Check each route and verify the container IP
1003- p.mu.RLock()1187+ routesUpdated := false
1004- routes := make(map[string]*ProxyRouteInfo, len(p.routes))1188+ updatedIPs := make(map[string]string) // cache to avoid redundant lookups
1005- for k, v := range p.routes {
1006- routes[k] = v
1007- }
1008- p.mu.RUnlock()
1009 1189
1010 // Track orphaned routes to mark as inactive1190 // Track orphaned routes to mark as inactive
1011 orphanedRoutes := []string{}1191 orphanedRoutes := []string{}
1012 1192
1013- // Check each route1193+ // Special handling for Gordon container - ensure its IP is correct first
1014- for domain, route := range routes {1194+ gordonContainerID := p.loadGordonContainerID()
1015- // Check if the container exists in the network1195+ if gordonContainerID != "" {
1016- containerEndpoint, exists := networkInfo.Containers[route.ContainerID]1196+ log.Debug("Verifying Gordon container IP first", "container_id", gordonContainerID)
1017- if !exists {1197+
1018- // Container not found in the network1198+ gordonIP, err := docker.GetContainerIPFromNetwork(gordonContainerID, networkName)
1019- // Could be stopped or removed1199+ if err == nil && gordonIP != "" {
1020-1200+ log.Debug("Found Gordon container IP", "ip", gordonIP)
1021- // Check if container still exists1201+ updatedIPs[gordonContainerID] = gordonIP
1022- if docker.ContainerExists(route.ContainerID) {1202+
1023- // Container exists but not in our network1203+ // Double-check by direct container inspection to ensure maximum reliability
1024- log.Warn("Container exists but not in Gordon network",1204+ containerInfo, inspectErr := docker.GetContainerInfo(gordonContainerID)
1025- "domain", domain,1205+ if inspectErr == nil && containerInfo.NetworkSettings != nil {
1026- "container_id", route.ContainerID)1206+ if network, exists := containerInfo.NetworkSettings.Networks[networkName]; exists &&
1027- } else {1207+ network.IPAddress != "" && network.IPAddress != gordonIP {
1028- // Container no longer exists1208+ log.Info("Different IP found for Gordon container through inspection",
1029- log.Warn("Container no longer exists but route is active",1209+ "network_ip", gordonIP,
1030- "domain", domain,1210+ "inspect_ip", network.IPAddress)
1031- "container_id", route.ContainerID)1211+ updatedIPs[gordonContainerID] = network.IPAddress
1032- orphanedRoutes = append(orphanedRoutes, domain)1212+ }
1033 }1213 }
1034- continue1214+ } else {
1215+ log.Warn("Failed to get Gordon container IP from network",
1216+ "container_id", gordonContainerID,
1217+ "error", err)
1035 }1218 }
1219+ }
1036 1220
1037- // Extract container IP from network info1221+ for domain, route := range routes {
1038- containerIP := containerEndpoint.IPv4Address1222+ if !route.Active {
1039- if containerIP == "" {1223+ continue
1040- // Try IPv6
1041- containerIP = containerEndpoint.IPv6Address
1042 }1224 }
1043 1225
1044- // Extract just the IP part without subnet1226+ containerID := route.ContainerID
1045- if idx := strings.Index(containerIP, "/"); idx > 0 {1227+
1046- containerIP = containerIP[:idx]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
1047 }1235 }
1048 1236
1049- // Compare with the route's IP1237+ // Check if this is for the Gordon container
1050- if containerIP != route.ContainerIP {1238+ isGordonContainer := (containerID == gordonContainerID)
1051- log.Info("Container IP has changed, updating route",1239+ if isGordonContainer {
1240+ log.Debug("Verifying Gordon container route",
1052 "domain", domain,1241 "domain", domain,
1053- "container_id", route.ContainerID,1242+ "container_id", containerID)
1054- "old_ip", route.ContainerIP,1243+ }
1055- "new_ip", containerIP)
1056 1244
1057- // Update the route IP1245+ // Get the current container IP
1058- err := p.ForceUpdateRouteIP(domain, containerIP)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)
1059 if err != nil {1258 if err != nil {
1060- log.Error("Failed to update route IP", "error", err)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+
1285+ // If we still couldn't get the IP, skip this route
1286+ if currentIP == "" {
1287+ continue
1288+ }
1289+ }
1290+
1291+ // Cache the IP for future route checks
1292+ updatedIPs[containerID] = currentIP
1293+ }
1294+
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)
1061 } else {1310 } else {
1062- log.Debug("Successfully updated route IP",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",
1063 "domain", domain,1322 "domain", domain,
1064- "new_ip", containerIP)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+ }
1065 }1332 }
1333+ } else {
1334+ log.Debug("Container IP verified and unchanged",
1335+ "domain", domain,
1336+ "container_id", containerID,
1337+ "ip", currentIP)
1066 }1338 }
1067 }1339 }
1068 1340
@@ -1070,9 +1342,17 @@ func (p *Proxy) verifyAllContainerIPs() {
1070 if len(orphanedRoutes) > 0 {1342 if len(orphanedRoutes) > 0 {
1071 log.Info("Marking orphaned routes as inactive", "count", len(orphanedRoutes))1343 log.Info("Marking orphaned routes as inactive", "count", len(orphanedRoutes))
1072 p.markRoutesInactive(orphanedRoutes)1344 p.markRoutesInactive(orphanedRoutes)
1345+ routesUpdated = true
1073 }1346 }
1074 1347
1075- log.Debug("Completed IP verification", "routes_checked", len(routes), "orphaned_routes", len(orphanedRoutes))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+ }
1076 }1356 }
1077 1357
1078 // markRoutesInactive marks the specified routes as inactive in the database and memory1358 // markRoutesInactive marks the specified routes as inactive in the database and memory
@@ -1081,6 +1361,28 @@ func (p *Proxy) markRoutesInactive(domains []string) {
1081 return1361 return
1082 }1362 }
1083 1363
1364+ // Get the admin domain to protect it
1365+ adminDomain := p.app.GetConfig().Http.FullDomain()
1366+
1367+ // Filter out admin domain from domains to mark inactive
1368+ filteredDomains := make([]string, 0, len(domains))
1369+ for _, domain := range domains {
1370+ if domain != adminDomain {
1371+ filteredDomains = append(filteredDomains, domain)
1372+ } else {
1373+ log.Warn("Prevented admin domain from being marked inactive",
1374+ "domain", adminDomain)
1375+ }
1376+ }
1377+
1378+ // If all domains were filtered out, nothing to do
1379+ if len(filteredDomains) == 0 {
1380+ return
1381+ }
1382+
1383+ // Use the filtered domains from here on
1384+ domains = filteredDomains
1385+
1084 // Begin a transaction1386 // Begin a transaction
1085 tx, err := p.app.GetDB().Begin()1387 tx, err := p.app.GetDB().Begin()
1086 if err != nil {1388 if err != nil {
@@ -1137,6 +1439,9 @@ func (p *Proxy) DiscoverMissingRoutes() {
1137 return1439 return
1138 }1440 }
1139 1441
1442+ // Get the admin domain from the config
1443+ adminDomain := p.app.GetConfig().Http.FullDomain()
1444+
1140 // Get all containers in the network1445 // Get all containers in the network
1141 networkInfo, err := docker.GetNetworkInfo(networkName)1446 networkInfo, err := docker.GetNetworkInfo(networkName)
1142 if err != nil {1447 if err != nil {
@@ -1188,6 +1493,14 @@ func (p *Proxy) DiscoverMissingRoutes() {
1188 continue1493 continue
1189 }1494 }
1190 1495
1496+ // Skip if this is the admin domain
1497+ if domainLabel == adminDomain {
1498+ log.Debug("Skipping auto-discovery for admin domain",
1499+ "domain", domainLabel,
1500+ "container_id", containerID)
1501+ continue
1502+ }
1503+
1191 // Check if this domain already has a route (different container)1504 // Check if this domain already has a route (different container)
1192 if _, exists := routes[domainLabel]; exists {1505 if _, exists := routes[domainLabel]; exists {
1193 log.Warn("Domain already has a route with a different container",1506 log.Warn("Domain already has a route with a different container",
@@ -1233,104 +1546,437 @@ func (p *Proxy) DiscoverMissingRoutes() {
1233 log.Error("Failed to add missing route", "error", err)1546 log.Error("Failed to add missing route", "error", err)
1234 }1547 }
1235 }1548 }
1549+
1550+ // Add a debug log for routes after discovery
1551+ count := 0
1552+ p.mu.RLock()
1553+ for range p.routes {
1554+ count++
1555+ }
1556+ p.mu.RUnlock()
1557+ log.Debug("Routes after discovery", "count", count)
1558+
1559+ // Clean up any root domain conflict
1560+ p.CleanupRootDomainForAdmin()
1236 }1561 }
1237 1562
1238-// LogActiveContainersAndRoutes prints a summary of all active containers and their routes1563+// LogActiveContainersAndRoutes logs a list of active containers and their routes
1239-// to the logs for debugging and monitoring purposes
1240 func (p *Proxy) LogActiveContainersAndRoutes() {1564 func (p *Proxy) LogActiveContainersAndRoutes() {
1241- // Get the network name from app config1565+ log.Info("---- Listing active containers and routes ----")
1242- networkName := p.app.GetConfig().ContainerEngine.Network1566+
1243- if networkName == "" {1567+ // Get the admin domain
1244- log.Error("Cannot list containers, network name is not configured")1568+ adminDomain := p.app.GetConfig().Http.FullDomain()
1245- return1569+
1570+ // Check if admin domain is in routes
1571+ p.mu.RLock()
1572+ _, adminExists := p.routes[adminDomain]
1573+ activeRoutes := len(p.routes)
1574+ p.mu.RUnlock()
1575+
1576+ if !adminExists {
1577+ log.Warn("Admin domain is missing from routes", "domain", adminDomain)
1246 }1578 }
1247 1579
1248- // Get all containers in the network1580+ log.Info(fmt.Sprintf("Found %d active routes", activeRoutes))
1249- networkInfo, err := docker.GetNetworkInfo(networkName)1581+
1582+ // Create a map of container IDs to their information
1583+ containerMap := make(map[string]struct {
1584+ Name string
1585+ IP string
1586+ Image string
1587+ Domains []string
1588+ Protocols []string
1589+ Ports []string
1590+ })
1591+
1592+ // Populate the map with route information
1593+ p.mu.RLock()
1594+ for domain, route := range p.routes {
1595+ if !route.Active {
1596+ continue
1597+ }
1598+
1599+ // If this container is not in the map yet, add it
1600+ if _, exists := containerMap[route.ContainerID]; !exists {
1601+ containerMap[route.ContainerID] = struct {
1602+ Name string
1603+ IP string
1604+ Image string
1605+ Domains []string
1606+ Protocols []string
1607+ Ports []string
1608+ }{
1609+ Name: route.ContainerID,
1610+ IP: route.ContainerIP,
1611+ Image: "unknown",
1612+ Domains: []string{domain},
1613+ Protocols: []string{route.Protocol},
1614+ Ports: []string{route.ContainerPort},
1615+ }
1616+ } else {
1617+ // Otherwise, append this domain to the container's domains
1618+ container := containerMap[route.ContainerID]
1619+ container.Domains = append(container.Domains, domain)
1620+ container.Protocols = append(container.Protocols, route.Protocol)
1621+ container.Ports = append(container.Ports, route.ContainerPort)
1622+ containerMap[route.ContainerID] = container
1623+ }
1624+ }
1625+ p.mu.RUnlock()
1626+
1627+ // Get container names and images
1628+ for containerID, containerData := range containerMap {
1629+ containerInfo, err := docker.GetContainerInfo(containerID)
1630+ if err == nil {
1631+ name, err := docker.GetContainerName(containerID)
1632+ if err == nil {
1633+ containerData.Name = strings.TrimPrefix(name, "/")
1634+ containerData.Image = containerInfo.Config.Image
1635+ containerMap[containerID] = containerData
1636+ }
1637+ }
1638+ }
1639+
1640+ // Display each container and its routes
1641+ for containerID, container := range containerMap {
1642+ // Log container info
1643+ // Safety check to prevent slice bounds panic
1644+ containerIDShort := containerID
1645+ if len(containerID) >= 12 {
1646+ containerIDShort = containerID[:12]
1647+ }
1648+ log.Info(fmt.Sprintf("Container: %s (%s)", container.Name, containerIDShort),
1649+ "ip", container.IP,
1650+ "image", container.Image)
1651+
1652+ // Log each domain for this container
1653+ for i, domain := range container.Domains {
1654+ log.Info(fmt.Sprintf(" ├─ Domain: %s", domain),
1655+ "protocol", container.Protocols[i],
1656+ "port", container.Ports[i])
1657+ }
1658+ }
1659+
1660+ log.Info("---- End of active containers and routes list ----")
1661+}
1662+
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()
1250 if err != nil {1673 if err != nil {
1251- log.Error("Failed to get network info for logging active containers", "error", err)1674+ log.Error("Failed to save Gordon container ID to config file", "error", err)
1252 return1675 return
1253 }1676 }
1254 1677
1255- // Get all active routes1678+ // Update the gordonContainerID field
1256- p.mu.RLock()1679+ p.gordonContainerID = containerID
1257- routes := make(map[string]*ProxyRouteInfo, len(p.routes))1680+
1258- for k, v := range p.routes {1681+ log.Info("Successfully saved Gordon container ID to config file", "container_id", containerID)
1259- if v.Active {1682+}
1260- routes[k] = v1683+
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+
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)
1709+
1710+ // Verify the container still exists
1711+ containers, err := docker.ListRunningContainers()
1712+ if err == nil {
1713+ for _, container := range containers {
1714+ if container.ID == p.gordonContainerID {
1715+ containerName := strings.TrimLeft(container.Names[0], "/")
1716+ log.Debug("Identified Gordon container using config file ID", "container_name", containerName)
1717+ return containerName
1718+ }
1719+ }
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
1261 }1723 }
1262 }1724 }
1263- p.mu.RUnlock()
1264 1725
1265- // Map container IDs to names for more readable output1726+ // If we have our container ID stored from initialization, use that next
1266- containerNames := make(map[string]string)1727+ if p.gordonContainerID != "" {
1267- for containerID := range networkInfo.Containers {1728+ log.Debug("Using stored Gordon container ID for identification", "container_id", p.gordonContainerID)
1268- name, err := docker.GetContainerName(containerID)1729+
1730+ // Get the container name from the ID
1731+ containers, err := docker.ListRunningContainers()
1269 if err == nil {1732 if err == nil {
1270- containerNames[containerID] = strings.TrimPrefix(name, "/")1733+ for _, container := range containers {
1271- } else {1734+ if container.ID == p.gordonContainerID {
1272- containerNames[containerID] = containerID[:12] + "..." // Shortened ID if name not available1735+ containerName := strings.TrimLeft(container.Names[0], "/")
1736+ log.Info("Gordon identity established via exact ID match",
1737+ "container_id", container.ID,
1738+ "container_name", containerName)
1739+ p.gordonContainerID = container.ID
1740+ // Save to config file for persistence
1741+ p.saveGordonContainerID(container.ID)
1742+ return containerName
1743+ }
1744+ }
1745+ // 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)
1273 }1747 }
1274 }1748 }
1275 1749
1276- // Format and log the active containers and routes1750+ // Check if we're running inside a container
1277- log.Info("---- Listing active containers and routes ----")1751+ if docker.IsRunningInContainer() {
1752+ log.Debug("Detected we're running in a container")
1753+
1754+ // Get our hostname which should be the container ID in Docker/Podman
1755+ hostname := os.Getenv("HOSTNAME")
1756+
1757+ if hostname != "" {
1758+ // Check if there's a container with this hostname
1759+ containers, err := docker.ListRunningContainers()
1760+ if err == nil {
1761+ // First try exact match (Podman often uses full ID as hostname)
1762+ for _, container := range containers {
1763+ if hostname == container.ID {
1764+ containerName := strings.TrimLeft(container.Names[0], "/")
1765+ log.Info("Gordon identity established via exact ID match",
1766+ "container_id", container.ID,
1767+ "container_name", containerName)
1768+ p.gordonContainerID = container.ID
1769+ // Save to config file for persistence
1770+ p.saveGordonContainerID(container.ID)
1771+ return containerName
1772+ }
1773+ }
1278 1774
1279- // Create a nice table-like format for the logs1775+ // Then try prefix match (Docker often uses ID prefix as hostname)
1280- routeCount := len(routes)1776+ for _, container := range containers {
1281- if routeCount == 0 {1777+ if strings.HasPrefix(container.ID, hostname) || strings.HasPrefix(hostname, container.ID) {
1282- log.Info("No active routes found")1778+ containerName := strings.TrimLeft(container.Names[0], "/")
1283- } else {1779+ log.Info("Gordon identity established via ID prefix match",
1284- // Log header1780+ "container_id", container.ID,
1285- log.Info(fmt.Sprintf("Found %d active routes", routeCount))1781+ "container_name", containerName,
1782+ "hostname", hostname)
1783+ p.gordonContainerID = container.ID
1784+ // Save to config file for persistence
1785+ p.saveGordonContainerID(container.ID)
1786+ return containerName
1787+ }
1788+ }
1789+ } else {
1790+ log.Debug("Could not list containers when checking hostname", "error", err)
1791+ }
1792+ }
1793+ }
1286 1794
1287- // Group routes by container for better readability1795+ // Look for containers with the name "gordon" first (Docker Compose default)
1288- containerRoutes := make(map[string][]string)1796+ containers, err := docker.ListRunningContainers()
1289- for domain, route := range routes {1797+ if err == nil {
1290- containerRoutes[route.ContainerID] = append(containerRoutes[route.ContainerID], domain)1798+ for _, container := range containers {
1799+ containerName := strings.TrimLeft(container.Names[0], "/")
1800+ if containerName == "gordon" {
1801+ log.Info("Found Gordon container with exact name 'gordon'", "container_id", container.ID)
1802+ p.gordonContainerID = container.ID
1803+ // Save to config file for persistence
1804+ p.saveGordonContainerID(container.ID)
1805+ return containerName
1806+ }
1291 }1807 }
1292 1808
1293- // Display each container and its routes1809+ // If we still haven't found it, look for containers with 'gordon' in the name
1294- for containerID, domains := range containerRoutes {1810+ for _, container := range containers {
1295- containerInfo, err := docker.GetContainerInfo(containerID)1811+ containerName := strings.TrimLeft(container.Names[0], "/")
1296- if err != nil {1812+ if strings.Contains(strings.ToLower(containerName), "gordon") {
1297- log.Debug("Failed to get container info", "container_id", containerID)1813+ log.Info("Found Gordon container with partial name match",
1298- continue1814+ "container_name", containerName,
1815+ "container_id", container.ID)
1816+ p.gordonContainerID = container.ID
1817+ // Save to config file for persistence
1818+ p.saveGordonContainerID(container.ID)
1819+ return containerName
1299 }1820 }
1821+ }
1822+ }
1300 1823
1301- containerName := containerNames[containerID]1824+ // Fallback to a default name
1302- if containerName == "" {1825+ log.Warn("Could not auto-detect Gordon container, using fallback name", "container_name", "gordon")
1303- containerName = containerID[:12] + "..." // Shortened ID if name not available1826+ return "gordon"
1827+}
1828+
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
1304 }1863 }
1864+ defer tx.Rollback()
1305 1865
1306- // Get container IP from network info1866+ // Get the domain ID for admin domain
1307- containerIP := ""1867+ var domainID string
1308- if endpoint, exists := networkInfo.Containers[containerID]; exists {1868+ err = tx.QueryRow("SELECT id FROM domain WHERE name = ?", adminDomain).Scan(&domainID)
1309- containerIP = endpoint.IPv4Address1869+ if err != nil {
1310- if containerIP == "" {1870+ if err != sql.ErrNoRows {
1311- containerIP = endpoint.IPv6Address1871+ log.Error("Failed to query domain for admin domain fix", "error", err)
1312 }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+ }
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()
1313 1914
1314- // Extract just the IP part without subnet1915+ // Get the domain ID for root domain
1315- if idx := strings.Index(containerIP, "/"); idx > 0 {1916+ var domainID string
1316- containerIP = containerIP[:idx]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)
1317 }1921 }
1922+ return
1318 }1923 }
1319 1924
1320- // Log container info1925+ // Delete the route for root domain
1321- log.Info(fmt.Sprintf("Container: %s (%s)", containerName, containerID[:12]),1926+ _, err = tx.Exec("DELETE FROM proxy_route WHERE domain_id = ?", domainID)
1322- "ip", containerIP,1927+ if err != nil {
1323- "image", containerInfo.Config.Image)1928+ log.Error("Failed to delete root domain route", "error", err)
1324-1929+ return
1325- // Log each domain for this container1930+ }
1326- for _, domain := range domains {1931+
1327- route := routes[domain]1932+ // We don't delete the domain itself from the database to avoid affecting certificate management
1328- log.Info(fmt.Sprintf(" ├─ Domain: %s", domain),1933+
1329- "protocol", route.Protocol,1934+ // Commit the transaction
1330- "port", route.ContainerPort)1935+ if err := tx.Commit(); err != nil {
1936+ log.Error("Failed to commit transaction for root domain cleanup", "error", err)
1937+ return
1331 }1938 }
1939+
1940+ // Remove root domain from in-memory routes
1941+ p.mu.Lock()
1942+ delete(p.routes, rootDomain)
1943+ p.mu.Unlock()
1944+
1945+ log.Info("Successfully removed conflicting root domain route",
1946+ "root_domain", rootDomain,
1947+ "admin_domain", adminDomain)
1332 }1948 }
1333 }1949 }
1950+}
1334 1951
1335- log.Info("---- End of active containers and routes list ----")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+ }
1961+ }
1962+
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+ }
1972+ }
1973+
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+ }
1336 }1982 }
M internal/proxy/server.go +126 -111
@@ -6,7 +6,6 @@ import (
66 "fmt"
77 "net/http"
88 "os"
9- "strings"
109 "time"
1110
1211 "github.com/bnema/gordon/pkg/docker"
@@ -20,16 +19,19 @@ import (
2019
2120 // Start loads the routes from the database and starts the proxy server
2221 func (p *Proxy) Start() error {
23- // Scan Gordon network containers and check for certificates
24- if err := p.scanContainersAndCheckCertificates(); err != nil {
25- log.Warn("Failed to scan containers for certificate checks", "error", err)
26- }
27-
2822 // First, load routes from the database
2923 if err := p.loadRoutes(); err != nil {
3024 return fmt.Errorf("failed to load routes: %w", err)
3125 }
3226
27+ // Now log Gordon container identity for debugging (after routes are loaded)
28+ p.LogGordonIdentity()
29+
30+ // Scan Gordon network containers and check for certificates
31+ if err := p.scanContainersAndCheckCertificates(); err != nil {
32+ log.Warn("Failed to scan containers for certificate checks", "error", err)
33+ }
34+
3335 // Run an immediate route verification to ensure all routes are valid on startup
3436 log.Info("Performing initial route verification on startup")
3537 p.verifyAllContainerIPs()
@@ -93,12 +95,33 @@ func (p *Proxy) Start() error {
9395 // We'll add the admin route with the default host for now
9496 // and test connections later with TestAdminConnectionLater
9597
98+ // Auto-detect the Gordon container name
99+ containerName := p.detectGordonContainer()
100+
101+ // Check if admin route exists in database and create/update it
102+ err := p.AddRoute(
103+ adminDomain,
104+ containerName,
105+ containerIP,
106+ p.app.GetConfig().Http.Port,
107+ "http", // Gordon server uses HTTP internally
108+ "/",
109+ )
110+
111+ if err != nil {
112+ log.Error("Failed to add admin route to database",
113+ "error", err,
114+ "domain", adminDomain)
115+ } else {
116+ log.Info("Ensured admin domain route is in database",
117+ "domain", adminDomain,
118+ "container", containerName,
119+ "target", fmt.Sprintf("http://%s:%s", containerIP, p.app.GetConfig().Http.Port))
120+ }
121+
96122 p.mu.Lock()
97- // Only add if it doesn't already exist
123+ // Only add if it doesn't already exist in memory
98124 if _, exists := p.routes[adminDomain]; !exists {
99- // Auto-detect the Gordon container name
100- containerName := p.detectGordonContainer()
101-
102125 p.routes[adminDomain] = &ProxyRouteInfo{
103126 Domain: adminDomain,
104127 ContainerIP: containerIP,
@@ -112,31 +135,23 @@ func (p *Proxy) Start() error {
112135 "domain", adminDomain,
113136 "container", containerName,
114137 "target", fmt.Sprintf("http://%s:%s", containerIP, p.app.GetConfig().Http.Port))
115- }
116-
117- // Add support for the root domain, redirecting to admin subdomain
118- rootDomain := p.app.GetConfig().Http.Domain
119- if rootDomain != "" && rootDomain != adminDomain {
120- if _, exists := p.routes[rootDomain]; !exists {
121- // Use the same auto-detected container name
122- containerName := p.detectGordonContainer()
123-
124- p.routes[rootDomain] = &ProxyRouteInfo{
125- Domain: rootDomain,
126- ContainerIP: containerIP,
127- ContainerPort: p.app.GetConfig().Http.Port,
128- ContainerID: containerName, // Use auto-detected container name
129- Protocol: "http", // Gordon server uses HTTP internally
130- Path: "/",
131- Active: true,
132- }
133- log.Info("Added route for root domain with redirect to admin subdomain",
134- "root_domain", rootDomain,
135- "admin_domain", adminDomain)
136- }
138+ } else {
139+ log.Debug("Admin domain route already exists",
140+ "domain", adminDomain,
141+ "route", fmt.Sprintf("%s://%s:%s",
142+ p.routes[adminDomain].Protocol,
143+ p.routes[adminDomain].ContainerIP,
144+ p.routes[adminDomain].ContainerPort))
137145 }
138146 p.mu.Unlock()
139147
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+
140155 // Set up middleware
141156 p.setupMiddleware()
142157
@@ -398,82 +413,82 @@ func (p *Proxy) checkExternalPortAccess() {
398413 }
399414 }
400415
401-// detectGordonContainer attempts to find the Gordon container automatically
402-func (p *Proxy) detectGordonContainer() string {
403- // Check if we're running inside a container
404- if docker.IsRunningInContainer() {
405- log.Debug("Detected we're running in a container")
406-
407- // Get our hostname which should be the container ID in Docker/Podman
408- hostname := os.Getenv("HOSTNAME")
409- if hostname != "" {
410- // Check if there's a container with this hostname
411- containers, err := docker.ListRunningContainers()
412- if err == nil {
413- for _, container := range containers {
414- // Check if this container is us by comparing hostname with container ID
415- if strings.HasPrefix(hostname, container.ID) {
416- // This is our container
417- containerName := strings.TrimLeft(container.Names[0], "/")
418- log.Debug("Auto-detected Gordon container name from hostname", "container_name", containerName)
419- return containerName
420- }
421- }
422- } else {
423- log.Debug("Could not list containers when checking hostname", "error", err)
424- }
425- }
426- }
427-
428- // Try to find Gordon by examining container processes
429- containers, err := docker.ListRunningContainers()
430- if err == nil {
431- for _, container := range containers {
432- containerID := container.ID
433-
434- // Check container processes for Gordon
435- containerLogs, err := docker.GetContainerLogs(containerID)
436- if err == nil && (strings.Contains(containerLogs, "/gordon") || strings.Contains(containerLogs, "serve")) {
437- containerName := strings.TrimLeft(container.Names[0], "/")
438- log.Debug("Auto-detected Gordon container by process", "container_name", containerName)
439- return containerName
440- } else if err != nil {
441- log.Debug("Could not get container logs", "container_id", containerID, "error", err)
442- }
443-
444- // Check container command for Gordon
445- if len(container.Command) > 0 && strings.Contains(container.Command, "gordon") {
446- containerName := strings.TrimLeft(container.Names[0], "/")
447- log.Debug("Auto-detected Gordon container by command", "container_name", containerName)
448- return containerName
449- }
450- }
451- } else {
452- log.Debug("Could not list containers when checking processes", "error", err)
453- }
454-
455- // If we couldn't detect from processes, try to find a container with gordon in the name or image
456- if err == nil {
457- for _, container := range containers {
458- // Check container names for "gordon"
459- for _, name := range container.Names {
460- if strings.Contains(strings.ToLower(name), "gordon") {
461- containerName := strings.TrimLeft(name, "/")
462- log.Debug("Auto-detected Gordon container by name", "container_name", containerName)
463- return containerName
464- }
465- }
466-
467- // Check image name for "gordon"
468- if strings.Contains(strings.ToLower(container.Image), "gordon") {
469- containerName := strings.TrimLeft(container.Names[0], "/")
470- log.Debug("Auto-detected Gordon container by image", "container_name", containerName, "image", container.Image)
471- return containerName
472- }
473- }
474- }
475-
476- // If all else fails, default to "gordon"
477- log.Debug("Could not auto-detect Gordon container, using default name", "container_name", "gordon")
478- return "gordon"
479-}
416+// // detectGordonContainer attempts to find the Gordon container automatically
417+// func (p *Proxy) detectGordonContainer() string {
418+// // Check if we're running inside a container
419+// if docker.IsRunningInContainer() {
420+// log.Debug("Detected we're running in a container")
421+
422+// // Get our hostname which should be the container ID in Docker/Podman
423+// hostname := os.Getenv("HOSTNAME")
424+// if hostname != "" {
425+// // Check if there's a container with this hostname
426+// containers, err := docker.ListRunningContainers()
427+// if err == nil {
428+// for _, container := range containers {
429+// // Check if this container is us by comparing hostname with container ID
430+// if strings.HasPrefix(hostname, container.ID) {
431+// // This is our container
432+// containerName := strings.TrimLeft(container.Names[0], "/")
433+// log.Debug("Auto-detected Gordon container name from hostname", "container_name", containerName)
434+// return containerName
435+// }
436+// }
437+// } else {
438+// log.Debug("Could not list containers when checking hostname", "error", err)
439+// }
440+// }
441+// }
442+
443+// // Try to find Gordon by examining container processes
444+// containers, err := docker.ListRunningContainers()
445+// if err == nil {
446+// for _, container := range containers {
447+// containerID := container.ID
448+
449+// // Check container processes for Gordon
450+// containerLogs, err := docker.GetContainerLogs(containerID)
451+// if err == nil && (strings.Contains(containerLogs, "/gordon") || strings.Contains(containerLogs, "serve")) {
452+// containerName := strings.TrimLeft(container.Names[0], "/")
453+// log.Debug("Auto-detected Gordon container by process", "container_name", containerName)
454+// return containerName
455+// } else if err != nil {
456+// log.Debug("Could not get container logs", "container_id", containerID, "error", err)
457+// }
458+
459+// // Check container command for Gordon
460+// if len(container.Command) > 0 && strings.Contains(container.Command, "gordon") {
461+// containerName := strings.TrimLeft(container.Names[0], "/")
462+// log.Debug("Auto-detected Gordon container by command", "container_name", containerName)
463+// return containerName
464+// }
465+// }
466+// } else {
467+// log.Debug("Could not list containers when checking processes", "error", err)
468+// }
469+
470+// // If we couldn't detect from processes, try to find a container with gordon in the name or image
471+// if err == nil {
472+// for _, container := range containers {
473+// // Check container names for "gordon"
474+// for _, name := range container.Names {
475+// if strings.Contains(strings.ToLower(name), "gordon") {
476+// containerName := strings.TrimLeft(name, "/")
477+// log.Debug("Auto-detected Gordon container by name", "container_name", containerName)
478+// return containerName
479+// }
480+// }
481+
482+// // Check image name for "gordon"
483+// if strings.Contains(strings.ToLower(container.Image), "gordon") {
484+// containerName := strings.TrimLeft(container.Names[0], "/")
485+// log.Debug("Auto-detected Gordon container by image", "container_name", containerName, "image", container.Image)
486+// return containerName
487+// }
488+// }
489+// }
490+
491+// // If all else fails, default to "gordon"
492+// log.Debug("Could not auto-detect Gordon container, using default name", "container_name", "gordon")
493+// return "gordon"
494+// }
@@ -6,7 +6,6 @@ import (
6 "fmt"6 "fmt"
7 "net/http"7 "net/http"
8 "os"8 "os"
9- "strings"
10 "time"9 "time"
11 10
12 "github.com/bnema/gordon/pkg/docker"11 "github.com/bnema/gordon/pkg/docker"
@@ -20,16 +19,19 @@ import (
20 19
21 // Start loads the routes from the database and starts the proxy server20 // Start loads the routes from the database and starts the proxy server
22 func (p *Proxy) Start() error {21 func (p *Proxy) Start() error {
23- // Scan Gordon network containers and check for certificates
24- if err := p.scanContainersAndCheckCertificates(); err != nil {
25- log.Warn("Failed to scan containers for certificate checks", "error", err)
26- }
27-
28 // First, load routes from the database22 // First, load routes from the database
29 if err := p.loadRoutes(); err != nil {23 if err := p.loadRoutes(); err != nil {
30 return fmt.Errorf("failed to load routes: %w", err)24 return fmt.Errorf("failed to load routes: %w", err)
31 }25 }
32 26
27+ // Now log Gordon container identity for debugging (after routes are loaded)
28+ p.LogGordonIdentity()
29+
30+ // Scan Gordon network containers and check for certificates
31+ if err := p.scanContainersAndCheckCertificates(); err != nil {
32+ log.Warn("Failed to scan containers for certificate checks", "error", err)
33+ }
34+
33 // Run an immediate route verification to ensure all routes are valid on startup35 // Run an immediate route verification to ensure all routes are valid on startup
34 log.Info("Performing initial route verification on startup")36 log.Info("Performing initial route verification on startup")
35 p.verifyAllContainerIPs()37 p.verifyAllContainerIPs()
@@ -93,12 +95,33 @@ func (p *Proxy) Start() error {
93 // We'll add the admin route with the default host for now95 // We'll add the admin route with the default host for now
94 // and test connections later with TestAdminConnectionLater96 // and test connections later with TestAdminConnectionLater
95 97
98+ // Auto-detect the Gordon container name
99+ containerName := p.detectGordonContainer()
100+
101+ // Check if admin route exists in database and create/update it
102+ err := p.AddRoute(
103+ adminDomain,
104+ containerName,
105+ containerIP,
106+ p.app.GetConfig().Http.Port,
107+ "http", // Gordon server uses HTTP internally
108+ "/",
109+ )
110+
111+ if err != nil {
112+ log.Error("Failed to add admin route to database",
113+ "error", err,
114+ "domain", adminDomain)
115+ } else {
116+ log.Info("Ensured admin domain route is in database",
117+ "domain", adminDomain,
118+ "container", containerName,
119+ "target", fmt.Sprintf("http://%s:%s", containerIP, p.app.GetConfig().Http.Port))
120+ }
121+
96 p.mu.Lock()122 p.mu.Lock()
97- // Only add if it doesn't already exist123+ // Only add if it doesn't already exist in memory
98 if _, exists := p.routes[adminDomain]; !exists {124 if _, exists := p.routes[adminDomain]; !exists {
99- // Auto-detect the Gordon container name
100- containerName := p.detectGordonContainer()
101-
102 p.routes[adminDomain] = &ProxyRouteInfo{125 p.routes[adminDomain] = &ProxyRouteInfo{
103 Domain: adminDomain,126 Domain: adminDomain,
104 ContainerIP: containerIP,127 ContainerIP: containerIP,
@@ -112,31 +135,23 @@ func (p *Proxy) Start() error {
112 "domain", adminDomain,135 "domain", adminDomain,
113 "container", containerName,136 "container", containerName,
114 "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))
115- }138+ } else {
116-139+ log.Debug("Admin domain route already exists",
117- // Add support for the root domain, redirecting to admin subdomain140+ "domain", adminDomain,
118- rootDomain := p.app.GetConfig().Http.Domain141+ "route", fmt.Sprintf("%s://%s:%s",
119- if rootDomain != "" && rootDomain != adminDomain {142+ p.routes[adminDomain].Protocol,
120- if _, exists := p.routes[rootDomain]; !exists {143+ p.routes[adminDomain].ContainerIP,
121- // Use the same auto-detected container name144+ p.routes[adminDomain].ContainerPort))
122- containerName := p.detectGordonContainer()
123-
124- p.routes[rootDomain] = &ProxyRouteInfo{
125- Domain: rootDomain,
126- ContainerIP: containerIP,
127- ContainerPort: p.app.GetConfig().Http.Port,
128- ContainerID: containerName, // Use auto-detected container name
129- Protocol: "http", // Gordon server uses HTTP internally
130- Path: "/",
131- Active: true,
132- }
133- log.Info("Added route for root domain with redirect to admin subdomain",
134- "root_domain", rootDomain,
135- "admin_domain", adminDomain)
136- }
137 }145 }
138 p.mu.Unlock()146 p.mu.Unlock()
139 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+
140 // Set up middleware155 // Set up middleware
141 p.setupMiddleware()156 p.setupMiddleware()
142 157
@@ -398,82 +413,82 @@ func (p *Proxy) checkExternalPortAccess() {
398 }413 }
399 }414 }
400 415
401-// detectGordonContainer attempts to find the Gordon container automatically416+// // detectGordonContainer attempts to find the Gordon container automatically
402-func (p *Proxy) detectGordonContainer() string {417+// func (p *Proxy) detectGordonContainer() string {
403- // Check if we're running inside a container418+// // Check if we're running inside a container
404- if docker.IsRunningInContainer() {419+// if docker.IsRunningInContainer() {
405- log.Debug("Detected we're running in a container")420+// log.Debug("Detected we're running in a container")
406-421+
407- // Get our hostname which should be the container ID in Docker/Podman422+// // Get our hostname which should be the container ID in Docker/Podman
408- hostname := os.Getenv("HOSTNAME")423+// hostname := os.Getenv("HOSTNAME")
409- if hostname != "" {424+// if hostname != "" {
410- // Check if there's a container with this hostname425+// // Check if there's a container with this hostname
411- containers, err := docker.ListRunningContainers()426+// containers, err := docker.ListRunningContainers()
412- if err == nil {427+// if err == nil {
413- for _, container := range containers {428+// for _, container := range containers {
414- // Check if this container is us by comparing hostname with container ID429+// // Check if this container is us by comparing hostname with container ID
415- if strings.HasPrefix(hostname, container.ID) {430+// if strings.HasPrefix(hostname, container.ID) {
416- // This is our container431+// // This is our container
417- containerName := strings.TrimLeft(container.Names[0], "/")432+// containerName := strings.TrimLeft(container.Names[0], "/")
418- log.Debug("Auto-detected Gordon container name from hostname", "container_name", containerName)433+// log.Debug("Auto-detected Gordon container name from hostname", "container_name", containerName)
419- return containerName434+// return containerName
420- }435+// }
421- }436+// }
422- } else {437+// } else {
423- log.Debug("Could not list containers when checking hostname", "error", err)438+// log.Debug("Could not list containers when checking hostname", "error", err)
424- }439+// }
425- }440+// }
426- }441+// }
427-442+
428- // Try to find Gordon by examining container processes443+// // Try to find Gordon by examining container processes
429- containers, err := docker.ListRunningContainers()444+// containers, err := docker.ListRunningContainers()
430- if err == nil {445+// if err == nil {
431- for _, container := range containers {446+// for _, container := range containers {
432- containerID := container.ID447+// containerID := container.ID
433-448+
434- // Check container processes for Gordon449+// // Check container processes for Gordon
435- containerLogs, err := docker.GetContainerLogs(containerID)450+// containerLogs, err := docker.GetContainerLogs(containerID)
436- if err == nil && (strings.Contains(containerLogs, "/gordon") || strings.Contains(containerLogs, "serve")) {451+// if err == nil && (strings.Contains(containerLogs, "/gordon") || strings.Contains(containerLogs, "serve")) {
437- containerName := strings.TrimLeft(container.Names[0], "/")452+// containerName := strings.TrimLeft(container.Names[0], "/")
438- log.Debug("Auto-detected Gordon container by process", "container_name", containerName)453+// log.Debug("Auto-detected Gordon container by process", "container_name", containerName)
439- return containerName454+// return containerName
440- } else if err != nil {455+// } else if err != nil {
441- log.Debug("Could not get container logs", "container_id", containerID, "error", err)456+// log.Debug("Could not get container logs", "container_id", containerID, "error", err)
442- }457+// }
443-458+
444- // Check container command for Gordon459+// // Check container command for Gordon
445- if len(container.Command) > 0 && strings.Contains(container.Command, "gordon") {460+// if len(container.Command) > 0 && strings.Contains(container.Command, "gordon") {
446- containerName := strings.TrimLeft(container.Names[0], "/")461+// containerName := strings.TrimLeft(container.Names[0], "/")
447- log.Debug("Auto-detected Gordon container by command", "container_name", containerName)462+// log.Debug("Auto-detected Gordon container by command", "container_name", containerName)
448- return containerName463+// return containerName
449- }464+// }
450- }465+// }
451- } else {466+// } else {
452- log.Debug("Could not list containers when checking processes", "error", err)467+// log.Debug("Could not list containers when checking processes", "error", err)
453- }468+// }
454-469+
455- // If we couldn't detect from processes, try to find a container with gordon in the name or image470+// // If we couldn't detect from processes, try to find a container with gordon in the name or image
456- if err == nil {471+// if err == nil {
457- for _, container := range containers {472+// for _, container := range containers {
458- // Check container names for "gordon"473+// // Check container names for "gordon"
459- for _, name := range container.Names {474+// for _, name := range container.Names {
460- if strings.Contains(strings.ToLower(name), "gordon") {475+// if strings.Contains(strings.ToLower(name), "gordon") {
461- containerName := strings.TrimLeft(name, "/")476+// containerName := strings.TrimLeft(name, "/")
462- log.Debug("Auto-detected Gordon container by name", "container_name", containerName)477+// log.Debug("Auto-detected Gordon container by name", "container_name", containerName)
463- return containerName478+// return containerName
464- }479+// }
465- }480+// }
466-481+
467- // Check image name for "gordon"482+// // Check image name for "gordon"
468- if strings.Contains(strings.ToLower(container.Image), "gordon") {483+// if strings.Contains(strings.ToLower(container.Image), "gordon") {
469- containerName := strings.TrimLeft(container.Names[0], "/")484+// containerName := strings.TrimLeft(container.Names[0], "/")
470- log.Debug("Auto-detected Gordon container by image", "container_name", containerName, "image", container.Image)485+// log.Debug("Auto-detected Gordon container by image", "container_name", containerName, "image", container.Image)
471- return containerName486+// return containerName
472- }487+// }
473- }488+// }
474- }489+// }
475-490+
476- // If all else fails, default to "gordon"491+// // If all else fails, default to "gordon"
477- log.Debug("Could not auto-detect Gordon container, using default name", "container_name", "gordon")492+// log.Debug("Could not auto-detect Gordon container, using default name", "container_name", "gordon")
478- return "gordon"493+// return "gordon"
479-}494+// }
M internal/server/new_server.go +10 -10
@@ -10,46 +10,46 @@ import (
1010 "github.com/bnema/gordon/internal/proxy"
1111 "github.com/bnema/gordon/internal/templating"
1212 "github.com/bnema/gordon/internal/webui"
13- "github.com/charmbracelet/log"
13+ "github.com/bnema/gordon/pkg/logger"
1414 )
1515
1616 func NewServerApp(buildConfig *common.BuildConfig) (*App, error) {
1717 // Get global config singleton instead of loading the config again
1818 config, err := common.GetGlobalConfig(buildConfig)
1919 if err != nil {
20- log.Fatalf("Failed to load configuration: %v", err)
20+ logger.Fatalf("Failed to load configuration: %v", err)
2121 }
2222
23- log.Info("Starting Gordon server", "version", config.Build.BuildVersion)
23+ logger.Info("Starting Gordon server", "version", config.Build.BuildVersion)
2424
2525 // For development environment, use the project root directory
2626 if config.Build.RunEnv == "dev" {
2727 // Get the current working directory as the project root
2828 projectRoot, err := os.Getwd()
2929 if err != nil {
30- log.Fatalf("Failed to get current working directory: %v", err)
30+ logger.Fatalf("Failed to get current working directory: %v", err)
3131 }
3232
3333 // Set storage directory to project root + /data instead of ~/.gordon
3434 config.General.StorageDir = filepath.Join(projectRoot, "data")
35- log.Debug("Dev environment detected, using local storage path", "path", config.General.StorageDir)
35+ logger.Debug("Dev environment detected, using local storage path", "path", config.General.StorageDir)
3636
3737 // Create the directory if it doesn't exist
3838 if err := os.MkdirAll(config.General.StorageDir, 0755); err != nil {
39- log.Fatalf("Failed to create storage directory: %v", err)
39+ logger.Fatalf("Failed to create storage directory: %v", err)
4040 }
4141 }
4242
4343 // Open the strings.yml file containing the strings for the current language
4444 file, err := templating.TemplateFS.Open("txt/locstrings.yml")
4545 if err != nil {
46- log.Fatalf("Failed to open strings.yml: %v", err)
46+ logger.Fatalf("Failed to open strings.yml: %v", err)
4747 }
4848
4949 // Read the file content into a byte slice
5050 bytes, err := io.ReadAll(file)
5151 if err != nil {
52- log.Fatalf("Failed to read strings.yml: %v", err)
52+ logger.Fatalf("Failed to read strings.yml: %v", err)
5353 }
5454
5555 file.Close()
@@ -75,7 +75,7 @@ func NewServerApp(buildConfig *common.BuildConfig) (*App, error) {
7575
7676 // InitializeProxy initializes and starts the reverse proxy
7777 func (a *App) InitializeProxy() (*proxy.Proxy, error) {
78- log.Info("Initializing reverse proxy")
78+ logger.Info("Initializing reverse proxy")
7979
8080 // Create a new proxy
8181 p, err := proxy.NewProxy(a)
@@ -88,6 +88,6 @@ func (a *App) InitializeProxy() (*proxy.Proxy, error) {
8888 return nil, err
8989 }
9090
91- log.Info("Reverse proxy initialized and started")
91+ logger.Info("Reverse proxy initialized and started")
9292 return p, nil
9393 }
@@ -10,46 +10,46 @@ import (
10 "github.com/bnema/gordon/internal/proxy"10 "github.com/bnema/gordon/internal/proxy"
11 "github.com/bnema/gordon/internal/templating"11 "github.com/bnema/gordon/internal/templating"
12 "github.com/bnema/gordon/internal/webui"12 "github.com/bnema/gordon/internal/webui"
13- "github.com/charmbracelet/log"13+ "github.com/bnema/gordon/pkg/logger"
14 )14 )
15 15
16 func NewServerApp(buildConfig *common.BuildConfig) (*App, error) {16 func NewServerApp(buildConfig *common.BuildConfig) (*App, error) {
17 // Get global config singleton instead of loading the config again17 // Get global config singleton instead of loading the config again
18 config, err := common.GetGlobalConfig(buildConfig)18 config, err := common.GetGlobalConfig(buildConfig)
19 if err != nil {19 if err != nil {
20- log.Fatalf("Failed to load configuration: %v", err)20+ logger.Fatalf("Failed to load configuration: %v", err)
21 }21 }
22 22
23- log.Info("Starting Gordon server", "version", config.Build.BuildVersion)23+ logger.Info("Starting Gordon server", "version", config.Build.BuildVersion)
24 24
25 // For development environment, use the project root directory25 // For development environment, use the project root directory
26 if config.Build.RunEnv == "dev" {26 if config.Build.RunEnv == "dev" {
27 // Get the current working directory as the project root27 // Get the current working directory as the project root
28 projectRoot, err := os.Getwd()28 projectRoot, err := os.Getwd()
29 if err != nil {29 if err != nil {
30- log.Fatalf("Failed to get current working directory: %v", err)30+ logger.Fatalf("Failed to get current working directory: %v", err)
31 }31 }
32 32
33 // Set storage directory to project root + /data instead of ~/.gordon33 // Set storage directory to project root + /data instead of ~/.gordon
34 config.General.StorageDir = filepath.Join(projectRoot, "data")34 config.General.StorageDir = filepath.Join(projectRoot, "data")
35- log.Debug("Dev environment detected, using local storage path", "path", config.General.StorageDir)35+ logger.Debug("Dev environment detected, using local storage path", "path", config.General.StorageDir)
36 36
37 // Create the directory if it doesn't exist37 // Create the directory if it doesn't exist
38 if err := os.MkdirAll(config.General.StorageDir, 0755); err != nil {38 if err := os.MkdirAll(config.General.StorageDir, 0755); err != nil {
39- log.Fatalf("Failed to create storage directory: %v", err)39+ logger.Fatalf("Failed to create storage directory: %v", err)
40 }40 }
41 }41 }
42 42
43 // Open the strings.yml file containing the strings for the current language43 // Open the strings.yml file containing the strings for the current language
44 file, err := templating.TemplateFS.Open("txt/locstrings.yml")44 file, err := templating.TemplateFS.Open("txt/locstrings.yml")
45 if err != nil {45 if err != nil {
46- log.Fatalf("Failed to open strings.yml: %v", err)46+ logger.Fatalf("Failed to open strings.yml: %v", err)
47 }47 }
48 48
49 // Read the file content into a byte slice49 // Read the file content into a byte slice
50 bytes, err := io.ReadAll(file)50 bytes, err := io.ReadAll(file)
51 if err != nil {51 if err != nil {
52- log.Fatalf("Failed to read strings.yml: %v", err)52+ logger.Fatalf("Failed to read strings.yml: %v", err)
53 }53 }
54 54
55 file.Close()55 file.Close()
@@ -75,7 +75,7 @@ func NewServerApp(buildConfig *common.BuildConfig) (*App, error) {
75 75
76 // InitializeProxy initializes and starts the reverse proxy76 // InitializeProxy initializes and starts the reverse proxy
77 func (a *App) InitializeProxy() (*proxy.Proxy, error) {77 func (a *App) InitializeProxy() (*proxy.Proxy, error) {
78- log.Info("Initializing reverse proxy")78+ logger.Info("Initializing reverse proxy")
79 79
80 // Create a new proxy80 // Create a new proxy
81 p, err := proxy.NewProxy(a)81 p, err := proxy.NewProxy(a)
@@ -88,6 +88,6 @@ func (a *App) InitializeProxy() (*proxy.Proxy, error) {
88 return nil, err88 return nil, err
89 }89 }
90 90
91- log.Info("Reverse proxy initialized and started")91+ logger.Info("Reverse proxy initialized and started")
92 return p, nil92 return p, nil
93 }93 }
M internal/server/sqlite_init.go +17 -2
@@ -69,6 +69,16 @@ 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+
7282 // InitializeDB initializes the database
7383 func InitializeDB(a *App) (*sql.DB, error) {
7484 log.Debug("Initializing database")
@@ -86,7 +96,7 @@ func InitializeDB(a *App) (*sql.DB, error) {
8696
8797 if !dbExists {
8898 log.Debug("Database needs bootstrapping", "path", dbPath)
89- if err := bootstrapDB(dbPath, a); err != nil {
99+ if err := bootstrapDB(dbPath); err != nil {
90100 return nil, fmt.Errorf("failed to bootstrap DB: %w", err)
91101 }
92102 }
@@ -104,6 +114,11 @@ func InitializeDB(a *App) (*sql.DB, error) {
104114 a.DB = db
105115
106116 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+
107122 // Populate the struct tables from the database
108123 log.Debug("Populating struct tables from existing database")
109124 if err := PopulateStructWithTables(a); err != nil {
@@ -116,7 +131,7 @@ func InitializeDB(a *App) (*sql.DB, error) {
116131 return db, nil
117132 }
118133
119-func bootstrapDB(dbPath string, app *App) error {
134+func bootstrapDB(dbPath string) error {
120135 // Open the database file
121136 db, err := sql.Open("sqlite", dbPath)
122137 if err != nil {
@@ -69,6 +69,16 @@ 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+
72 // InitializeDB initializes the database82 // InitializeDB initializes the database
73 func InitializeDB(a *App) (*sql.DB, error) {83 func InitializeDB(a *App) (*sql.DB, error) {
74 log.Debug("Initializing database")84 log.Debug("Initializing database")
@@ -86,7 +96,7 @@ func InitializeDB(a *App) (*sql.DB, error) {
86 96
87 if !dbExists {97 if !dbExists {
88 log.Debug("Database needs bootstrapping", "path", dbPath)98 log.Debug("Database needs bootstrapping", "path", dbPath)
89- if err := bootstrapDB(dbPath, a); err != nil {99+ if err := bootstrapDB(dbPath); err != nil {
90 return nil, fmt.Errorf("failed to bootstrap DB: %w", err)100 return nil, fmt.Errorf("failed to bootstrap DB: %w", err)
91 }101 }
92 }102 }
@@ -104,6 +114,11 @@ func InitializeDB(a *App) (*sql.DB, error) {
104 a.DB = db114 a.DB = db
105 115
106 if dbExists {116 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+
107 // Populate the struct tables from the database122 // Populate the struct tables from the database
108 log.Debug("Populating struct tables from existing database")123 log.Debug("Populating struct tables from existing database")
109 if err := PopulateStructWithTables(a); err != nil {124 if err := PopulateStructWithTables(a); err != nil {
@@ -116,7 +131,7 @@ func InitializeDB(a *App) (*sql.DB, error) {
116 return db, nil131 return db, nil
117 }132 }
118 133
119-func bootstrapDB(dbPath string, app *App) error {134+func bootstrapDB(dbPath string) error {
120 // Open the database file135 // Open the database file
121 db, err := sql.Open("sqlite", dbPath)136 db, err := sql.Open("sqlite", dbPath)
122 if err != nil {137 if err != nil {
M pkg/docker/client.go +15 -2
@@ -6,7 +6,7 @@ import (
66 "os"
77 "time"
88
9- "github.com/charmbracelet/log"
9+ "github.com/bnema/gordon/pkg/logger"
1010 "github.com/docker/docker/api/types"
1111 "github.com/docker/docker/api/types/container"
1212 "github.com/docker/docker/client"
@@ -24,15 +24,18 @@ var (
2424
2525 func InitializeClient(config *Config) error {
2626 if config.Sock == "" {
27+ logger.Error("Failed to initialize Docker client", "error", "sock field in Config is empty")
2728 return fmt.Errorf("sock field in Config is empty")
2829 }
2930
3031 if _, err := os.Stat(config.Sock); os.IsNotExist(err) {
32+ logger.Error("Failed to initialize Docker client", "error", err)
3133 return err
3234 }
3335
3436 // For Podman compatibility, we need to ensure we're using the right connection approach
3537 host := "unix://" + config.Sock
38+ logger.Debug("Setting up Docker client", "host", host)
3639
3740 // Create the Docker client with appropriate options
3841 cli, err := client.NewClientWithOpts(
@@ -40,10 +43,11 @@ func InitializeClient(config *Config) error {
4043 client.WithHost(host),
4144 )
4245 if err != nil {
46+ logger.Error("Failed to initialize Docker client", "error", err)
4347 return fmt.Errorf("error initializing Docker client: %s", err)
4448 }
4549
46- log.Debug("Docker client initialized", "socket", config.Sock, "podman_enabled", config.PodmanEnable)
50+ logger.Debug("Docker client initialized", "socket", config.Sock, "podman_enabled", config.PodmanEnable)
4751 dockerCli = cli
4852 currentConfig = config
4953 return nil
@@ -51,15 +55,18 @@ func InitializeClient(config *Config) error {
5155
5256 func CheckIfInitialized() error {
5357 if currentConfig == nil || currentConfig.Sock == "" {
58+ logger.Error("Docker client check failed", "error", "client not initialized or configuration missing")
5459 return fmt.Errorf("docker client is not initialized or configuration is missing")
5560 }
5661
5762 socketPath := currentConfig.Sock
5863 if _, err := os.Stat(socketPath); os.IsNotExist(err) {
64+ logger.Error("Docker client check failed", "error", "socket file not found", "path", socketPath)
5965 return fmt.Errorf("sock file does not exist: %s", socketPath)
6066 }
6167
6268 if dockerCli == nil {
69+ logger.Error("Docker client check failed", "error", "client is nil")
6370 return fmt.Errorf("docker client is not initialized")
6471 }
6572
@@ -68,21 +75,27 @@ func CheckIfInitialized() error {
6875
6976 _, err := dockerCli.ContainerList(ctx, container.ListOptions{})
7077 if err != nil {
78+ logger.Error("Docker client check failed", "error", err)
7179 return fmt.Errorf("cannot connect to Docker daemon: %s", err)
7280 }
7381
82+ logger.Debug("Docker client check successful")
7483 return nil
7584 }
7685
7786 func ListContainers(ctx context.Context) ([]types.Container, error) {
7887 if dockerCli == nil {
88+ logger.Error("Failed to list containers", "error", "Docker client is not initialized")
7989 return nil, fmt.Errorf("Docker client is not initialized")
8090 }
8191
92+ logger.Debug("Listing containers")
8293 containers, err := dockerCli.ContainerList(ctx, container.ListOptions{})
8394 if err != nil {
95+ logger.Error("Failed to list containers", "error", err)
8496 return nil, fmt.Errorf("error listing containers: %w", err)
8597 }
8698
99+ logger.Debug("Listed containers successfully", "count", len(containers))
87100 return containers, nil
88101 }
@@ -6,7 +6,7 @@ import (
6 "os"6 "os"
7 "time"7 "time"
8 8
9- "github.com/charmbracelet/log"9+ "github.com/bnema/gordon/pkg/logger"
10 "github.com/docker/docker/api/types"10 "github.com/docker/docker/api/types"
11 "github.com/docker/docker/api/types/container"11 "github.com/docker/docker/api/types/container"
12 "github.com/docker/docker/client"12 "github.com/docker/docker/client"
@@ -24,15 +24,18 @@ var (
24 24
25 func InitializeClient(config *Config) error {25 func InitializeClient(config *Config) error {
26 if config.Sock == "" {26 if config.Sock == "" {
27+ logger.Error("Failed to initialize Docker client", "error", "sock field in Config is empty")
27 return fmt.Errorf("sock field in Config is empty")28 return fmt.Errorf("sock field in Config is empty")
28 }29 }
29 30
30 if _, err := os.Stat(config.Sock); os.IsNotExist(err) {31 if _, err := os.Stat(config.Sock); os.IsNotExist(err) {
32+ logger.Error("Failed to initialize Docker client", "error", err)
31 return err33 return err
32 }34 }
33 35
34 // For Podman compatibility, we need to ensure we're using the right connection approach36 // For Podman compatibility, we need to ensure we're using the right connection approach
35 host := "unix://" + config.Sock37 host := "unix://" + config.Sock
38+ logger.Debug("Setting up Docker client", "host", host)
36 39
37 // Create the Docker client with appropriate options40 // Create the Docker client with appropriate options
38 cli, err := client.NewClientWithOpts(41 cli, err := client.NewClientWithOpts(
@@ -40,10 +43,11 @@ func InitializeClient(config *Config) error {
40 client.WithHost(host),43 client.WithHost(host),
41 )44 )
42 if err != nil {45 if err != nil {
46+ logger.Error("Failed to initialize Docker client", "error", err)
43 return fmt.Errorf("error initializing Docker client: %s", err)47 return fmt.Errorf("error initializing Docker client: %s", err)
44 }48 }
45 49
46- log.Debug("Docker client initialized", "socket", config.Sock, "podman_enabled", config.PodmanEnable)50+ logger.Debug("Docker client initialized", "socket", config.Sock, "podman_enabled", config.PodmanEnable)
47 dockerCli = cli51 dockerCli = cli
48 currentConfig = config52 currentConfig = config
49 return nil53 return nil
@@ -51,15 +55,18 @@ func InitializeClient(config *Config) error {
51 55
52 func CheckIfInitialized() error {56 func CheckIfInitialized() error {
53 if currentConfig == nil || currentConfig.Sock == "" {57 if currentConfig == nil || currentConfig.Sock == "" {
58+ logger.Error("Docker client check failed", "error", "client not initialized or configuration missing")
54 return fmt.Errorf("docker client is not initialized or configuration is missing")59 return fmt.Errorf("docker client is not initialized or configuration is missing")
55 }60 }
56 61
57 socketPath := currentConfig.Sock62 socketPath := currentConfig.Sock
58 if _, err := os.Stat(socketPath); os.IsNotExist(err) {63 if _, err := os.Stat(socketPath); os.IsNotExist(err) {
64+ logger.Error("Docker client check failed", "error", "socket file not found", "path", socketPath)
59 return fmt.Errorf("sock file does not exist: %s", socketPath)65 return fmt.Errorf("sock file does not exist: %s", socketPath)
60 }66 }
61 67
62 if dockerCli == nil {68 if dockerCli == nil {
69+ logger.Error("Docker client check failed", "error", "client is nil")
63 return fmt.Errorf("docker client is not initialized")70 return fmt.Errorf("docker client is not initialized")
64 }71 }
65 72
@@ -68,21 +75,27 @@ func CheckIfInitialized() error {
68 75
69 _, err := dockerCli.ContainerList(ctx, container.ListOptions{})76 _, err := dockerCli.ContainerList(ctx, container.ListOptions{})
70 if err != nil {77 if err != nil {
78+ logger.Error("Docker client check failed", "error", err)
71 return fmt.Errorf("cannot connect to Docker daemon: %s", err)79 return fmt.Errorf("cannot connect to Docker daemon: %s", err)
72 }80 }
73 81
82+ logger.Debug("Docker client check successful")
74 return nil83 return nil
75 }84 }
76 85
77 func ListContainers(ctx context.Context) ([]types.Container, error) {86 func ListContainers(ctx context.Context) ([]types.Container, error) {
78 if dockerCli == nil {87 if dockerCli == nil {
88+ logger.Error("Failed to list containers", "error", "Docker client is not initialized")
79 return nil, fmt.Errorf("Docker client is not initialized")89 return nil, fmt.Errorf("Docker client is not initialized")
80 }90 }
81 91
92+ logger.Debug("Listing containers")
82 containers, err := dockerCli.ContainerList(ctx, container.ListOptions{})93 containers, err := dockerCli.ContainerList(ctx, container.ListOptions{})
83 if err != nil {94 if err != nil {
95+ logger.Error("Failed to list containers", "error", err)
84 return nil, fmt.Errorf("error listing containers: %w", err)96 return nil, fmt.Errorf("error listing containers: %w", err)
85 }97 }
86 98
99+ logger.Debug("Listed containers successfully", "count", len(containers))
87 return containers, nil100 return containers, nil
88 }101 }
M pkg/docker/containers.go +374 -239
@@ -8,11 +8,9 @@ import (
88 "strings"
99 "time"
1010
11- "github.com/charmbracelet/log"
11+ "github.com/bnema/gordon/pkg/logger"
1212 "github.com/docker/docker/api/types"
1313 "github.com/docker/docker/api/types/container"
14- "github.com/docker/docker/api/types/events"
15- "github.com/docker/docker/api/types/filters"
1614 "github.com/docker/docker/api/types/network"
1715 "github.com/docker/docker/client"
1816 "github.com/docker/go-connections/nat"
@@ -114,17 +112,18 @@ func StopContainer(containerID string) error {
114112 // Try graceful shutdown first
115113 stopped, err := StopContainerGracefully(containerID, 3*time.Second)
116114 if err != nil {
117- log.Warn("Failed to stop container gracefully", "error", err)
115+ logger.Warn("Failed to stop container gracefully", "error", err)
118116 }
119117
120118 // If container wasn't stopped gracefully or there was an error, try forceful shutdown
121119 if !stopped {
122120 if err := StopContainerRagefully(containerID); err != nil {
121+ logger.Error("Failed to stop container forcefully", "error", err)
123122 return fmt.Errorf("failed to stop container forcefully: %w", err)
124123 }
125- log.Info("Container stopped forcefully", "containerID", containerID)
124+ logger.Info("Container stopped forcefully", "containerID", containerID)
126125 } else {
127- log.Info("Container stopped gracefully", "containerID", containerID)
126+ logger.Info("Container stopped gracefully", "containerID", containerID)
128127 }
129128
130129 return nil
@@ -194,7 +193,7 @@ func RemoveContainer(containerID string) error {
194193
195194 // StartContainer starts a container
196195 func StartContainer(containerID string) error {
197- fmt.Println("Starting container", containerID)
196+ logger.Info("Starting container", "containerID", containerID)
198197
199198 // Add timeout context
200199 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
@@ -203,20 +202,23 @@ func StartContainer(containerID string) error {
203202 // Get initial container state
204203 containerInfo, err := GetContainerInfo(containerID)
205204 if err != nil {
205+ logger.Error("Failed to get container info", "containerID", containerID, "error", err)
206206 return fmt.Errorf("could not get container info: %v", err)
207207 }
208208
209209 if containerInfo.State.Running {
210+ logger.Warn("Container is already running", "containerID", containerID)
210211 return fmt.Errorf("container is already running")
211212 }
212213
213214 // Start container
215+ logger.Debug("Executing container start", "containerID", containerID)
214216 err = dockerCli.ContainerStart(ctx, containerID, container.StartOptions{})
215217 if err != nil {
216218 // Get container logs if start failed
217219 logs, logErr := GetContainerLogs(containerID)
218220 if logErr == nil {
219- fmt.Printf("Container logs after failed start: %s\n", logs)
221+ logger.Error("Container logs after failed start", "containerID", containerID, "logs", logs)
220222 }
221223 return fmt.Errorf("could not start container: %v", err)
222224 }
@@ -225,19 +227,22 @@ func StartContainer(containerID string) error {
225227 for i := 0; i < 5; i++ {
226228 state, err := GetContainerState(containerID)
227229 if err != nil {
228- fmt.Printf("Error checking container state: %v\n", err)
230+ logger.Warn("Error checking container state", "containerID", containerID, "error", err)
229231 continue
230232 }
231233
232234 if state == "running" {
235+ logger.Info("Container started successfully", "containerID", containerID)
233236 return nil
234237 }
235238
239+ logger.Debug("Waiting for container to start", "containerID", containerID, "state", state, "attempt", i+1)
236240 time.Sleep(time.Second)
237241 }
238242
239243 // If we get here, container didn't start properly
240244 logs, _ := GetContainerLogs(containerID)
245+ logger.Error("Container failed to enter running state", "containerID", containerID, "logs", logs)
241246 return fmt.Errorf("container failed to enter running state. Logs: %s", logs)
242247 }
243248
@@ -245,10 +250,11 @@ func StartContainer(containerID string) error {
245250 func CreateContainer(cmdParams ContainerCommandParams) (string, error) {
246251
247252 if err := CheckIfInitialized(); err != nil {
253+ logger.Error("Failed to check if docker client is initialized", "error", err)
248254 return "", fmt.Errorf("failed to check if docker client is initialized: %w", err)
249255 }
250256
251- log.Info("Creating container with params",
257+ logger.Info("Creating container with params",
252258 "name", cmdParams.ContainerName,
253259 "image", cmdParams.ImageName,
254260 "proxy_port", cmdParams.ProxyPort)
@@ -256,31 +262,31 @@ func CreateContainer(cmdParams ContainerCommandParams) (string, error) {
256262 // Check if a container with this name already exists - handle recreation case
257263 existingContainers, err := ListRunningContainers()
258264 if err != nil {
259- log.Warn("Failed to check for existing containers", "error", err)
265+ logger.Warn("Failed to check for existing containers", "error", err)
260266 } else {
261267 for _, container := range existingContainers {
262268 for _, name := range container.Names {
263269 // Container names in Docker API have a leading slash
264270 cleanName := strings.TrimPrefix(name, "/")
265271 if cleanName == cmdParams.ContainerName {
266- log.Warn("Container with this name already exists, it will be replaced",
272+ logger.Warn("Container with this name already exists, it will be replaced",
267273 "name", cmdParams.ContainerName,
268274 "existing_id", container.ID)
269275
270276 // Stop and remove the existing container
271277 if err := StopContainer(container.ID); err != nil {
272- log.Warn("Failed to stop existing container",
278+ logger.Warn("Failed to stop existing container",
273279 "container_id", container.ID,
274280 "error", err)
275281 }
276282
277283 if err := RemoveContainer(container.ID); err != nil {
278- log.Warn("Failed to remove existing container",
284+ logger.Warn("Failed to remove existing container",
279285 "container_id", container.ID,
280286 "error", err)
281287 }
282288
283- log.Info("Removed existing container to create a new one",
289+ logger.Info("Removed existing container to create a new one",
284290 "name", cmdParams.ContainerName)
285291 break
286292 }
@@ -291,13 +297,15 @@ func CreateContainer(cmdParams ContainerCommandParams) (string, error) {
291297 // Check network
292298 isNetworkCreated, err := CheckIfNetworkExists(cmdParams.Network)
293299 if err != nil {
300+ logger.Error("Network check failed", "network", cmdParams.Network, "error", err)
294301 return "", fmt.Errorf("network check failed: %v", err)
295302 }
296303
297304 if !isNetworkCreated {
298- fmt.Printf("Creating network: %s\n", cmdParams.Network)
305+ logger.Info("Creating network", "network", cmdParams.Network)
299306 _, err := dockerCli.NetworkCreate(context.Background(), cmdParams.Network, network.CreateOptions{})
300307 if err != nil {
308+ logger.Error("Network creation failed", "network", cmdParams.Network, "error", err)
301309 return "", fmt.Errorf("network creation failed: %v", err)
302310 }
303311 }
@@ -314,10 +322,10 @@ func CreateContainer(cmdParams ContainerCommandParams) (string, error) {
314322 },
315323 }
316324 exposedPorts[exposedPort] = struct{}{}
317- fmt.Printf("Adding port mapping: %s:%s/%s\n",
318- portMapping.HostPort,
319- portMapping.ContainerPort,
320- portMapping.Protocol)
325+ logger.Debug("Adding port mapping",
326+ "host_port", portMapping.HostPort,
327+ "container_port", portMapping.ContainerPort,
328+ "protocol", portMapping.Protocol)
321329 }
322330
323331 // Prepare labels
@@ -358,31 +366,37 @@ func CreateContainer(cmdParams ContainerCommandParams) (string, error) {
358366 return "", fmt.Errorf("container creation failed: %v", err)
359367 }
360368
361- log.Info("Container created successfully with ID: %s\n", resp.ID)
369+ logger.Info("Container created successfully", "id", resp.ID)
362370
363371 return resp.ID, nil
364372 }
365373
366374 func WaitForContainerToBeRunning(containerID string, timeout time.Duration) error {
375+ logger.Debug("Waiting for container to be running", "containerID", containerID, "timeout", timeout)
367376 ctx, cancel := context.WithTimeout(context.Background(), timeout)
368377 defer cancel()
369378
370379 for {
371380 select {
372381 case <-ctx.Done():
382+ logger.Error("Timeout waiting for container to start", "containerID", containerID)
373383 return fmt.Errorf("timeout waiting for container to start")
374384 case <-time.After(time.Second):
375385 state, err := GetContainerState(containerID)
376386 if err != nil {
387+ logger.Error("Error checking container state", "containerID", containerID, "error", err)
377388 return fmt.Errorf("error checking container state: %v", err)
378389 }
379390
391+ logger.Debug("Container state check", "containerID", containerID, "state", state)
380392 if state == "running" {
393+ logger.Info("Container is now running", "containerID", containerID)
381394 return nil
382395 }
383396
384397 if state == "exited" {
385398 logs, _ := GetContainerLogs(containerID)
399+ logger.Error("Container exited unexpectedly", "containerID", containerID, "logs", logs)
386400 return fmt.Errorf("container exited unexpectedly. Logs: %s", logs)
387401 }
388402 }
@@ -642,7 +656,7 @@ func findContainersWithPort(ctx context.Context, cli *client.Client, containers
642656 for _, container := range containers {
643657 containerInfo, err := getContainerPortInfo(ctx, cli, container)
644658 if err != nil {
645- log.Warn("Failed to inspect container", "container", container.ID, "error", err)
659+ logger.Warn("Failed to inspect container", "container", container.ID, "error", err)
646660 continue
647661 }
648662
@@ -736,8 +750,10 @@ func formatDuration(d time.Duration) string {
736750 return fmt.Sprintf("%dm", minutes)
737751 }
738752
739-// ListenForContainerEvents starts listening for container events and calls the provided callback
740-// when relevant container events occur. This is used for real-time service discovery and IP updates.
753+// ListenForContainerEvents starts polling for container state changes and calls the provided callback
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 more
756+// stable for both Docker and Podman environments.
741757 func ListenForContainerEvents(networkFilter string, callback func(string, string, string)) error {
742758 // Check if the Docker client has been initialized
743759 err := CheckIfInitialized()
@@ -751,273 +767,392 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
751767 // Store cancel func to allow shutdown
752768 containerEventCancel = cancel
753769
754- // Start the main listener loop in a goroutine
770+ // Start the polling loop in a goroutine
755771 go func() {
756- // Implement exponential backoff for reconnection
757- initialDelay := 1 * time.Second
758- maxDelay := 1 * time.Minute
759- currentDelay := initialDelay
772+ // Configure polling interval - can be adjusted based on needs
773+ pollInterval := 10 * time.Second
774+ logger.Info("Starting container polling",
775+ "network_filter", networkFilter,
776+ "poll_interval", pollInterval)
777+
778+ // Initialize container tracking map
779+ containerStates := make(map[string]containerState)
780+ // Track previous IPs to detect if they actually changed
781+ containerIPs := make(map[string]string)
782+
783+ // Create ticker for polling
784+ ticker := time.NewTicker(pollInterval)
785+ defer ticker.Stop()
786+
787+ // Track failures for backoff
760788 consecutiveFailures := 0
761- maxConsecutiveFailures := 10
762-
763- // Track EOFs for reduced logging
764- eofCount := 0
765- lastEOFLogTime := time.Now().Add(-10 * time.Minute) // Initialize to past time
766-
767- // Fixed delay for EOFs to prevent rapid reconnection
768- eofReconnectDelay := 5 * time.Second
789+ maxConsecutiveFailures := 5
790+ initialBackoff := 5 * time.Second
791+ maxBackoff := 1 * time.Minute
792+ currentBackoff := initialBackoff
769793
770794 for {
771- // Check if context was canceled (application is shutting down)
772- if ctx.Err() != nil {
773- return
774- }
775-
776- // Add a timeout context for the Events call
777- eventCtx, eventCancel := context.WithCancel(ctx)
795+ select {
796+ case <-ticker.C:
797+ // Get current list of running containers
798+ logger.Debug("Polling for container state changes", "network_filter", networkFilter)
799+ currentContainers, err := ListContainers(ctx)
800+ if err != nil {
801+ consecutiveFailures++
802+ logger.Error("Failed to list containers during polling",
803+ "error", err,
804+ "consecutive_failures", consecutiveFailures)
805+
806+ // Implement backoff for repeated failures
807+ if consecutiveFailures >= maxConsecutiveFailures {
808+ // Increase backoff time
809+ currentBackoff = time.Duration(float64(currentBackoff) * 1.5)
810+ if currentBackoff > maxBackoff {
811+ currentBackoff = maxBackoff
812+ }
778813
779- // Set up filters for the events we're interested in
780- filters := makeEventFilters(networkFilter)
814+ logger.Warn("Multiple consecutive container polling failures",
815+ "count", consecutiveFailures,
816+ "next_poll_after", currentBackoff)
781817
782- log.Debug("connecting to container event stream", "network_filter", networkFilter)
818+ // Temporarily slow down polling
819+ ticker.Reset(currentBackoff)
820+ }
821+ continue
822+ }
783823
784- // Start listening for events
785- messages, errs := dockerCli.Events(eventCtx, events.ListOptions{
786- Filters: filters,
787- })
824+ // Reset on successful poll
825+ if consecutiveFailures > 0 {
826+ logger.Debug("Container polling recovered after failures",
827+ "previous_failure_count", consecutiveFailures)
828+ consecutiveFailures = 0
829+ // Reset to normal polling interval
830+ ticker.Reset(pollInterval)
831+ logger.Debug("Reset polling interval to normal", "interval", pollInterval)
832+ }
788833
789- // Only log the first connection and every 10th reconnection for EOF
790- if eofCount == 0 {
791- log.Info("Started container event listener", "network_filter", networkFilter)
792- } else {
793- log.Debug("Reconnected container event listener", "network_filter", networkFilter, "reconnect_count", eofCount)
794- }
834+ // Process the current containers
835+ newStates, events := detectContainerEvents(containerStates, currentContainers, networkFilter)
836+
837+ // Update our tracking map
838+ containerStates = newStates
839+
840+ // Process detected events
841+ for _, event := range events {
842+ logger.Debug("Container event detected by polling",
843+ "container_id", event.id,
844+ "container_name", event.name,
845+ "event_type", event.eventType)
846+
847+ // Handle container start events
848+ if event.eventType == "start" {
849+ // Get container IP from the network
850+ containerIP, err := GetContainerIPFromNetwork(event.id, networkFilter)
851+ if err != nil {
852+ logger.Error("Failed to get container IP from network",
853+ "container_id", event.id,
854+ "container_name", event.name,
855+ "network", networkFilter,
856+ "error", err)
857+
858+ // Additional fallback to try to get IP through container inspection
859+ // This is especially important for Podman compatibility
860+ containerInfo, inspectErr := GetContainerInfo(event.id)
861+ if inspectErr == nil {
862+ // Try to get IP address from container inspection
863+ if containerInfo.NetworkSettings != nil {
864+ // First check if NetworkSettings.Networks contains our network
865+ if network, exists := containerInfo.NetworkSettings.Networks[networkFilter]; exists && network.IPAddress != "" {
866+ containerIP = network.IPAddress
867+ logger.Debug("Found container IP through inspection - network specific",
868+ "container_id", event.id,
869+ "container_name", event.name,
870+ "network", networkFilter,
871+ "ip", containerIP)
872+ } else if containerInfo.NetworkSettings.IPAddress != "" {
873+ // Fallback to default IPAddress
874+ containerIP = containerInfo.NetworkSettings.IPAddress
875+ logger.Debug("Found container IP through inspection - default network",
876+ "container_id", event.id,
877+ "container_name", event.name,
878+ "ip", containerIP)
879+ }
880+ }
881+ }
795882
796- // Process events in an inner loop
797- listenSuccess := false
798- lastEventTime := time.Now()
799- eventStreamLoop:
800- for {
801- select {
802- case err := <-errs:
803- if err != nil && ctx.Err() == nil {
804- // Only log if this isn't due to context cancellation
805- if err.Error() == "EOF" {
806- // Increment EOF counter
807- eofCount++
808-
809- // Log EOF errors less frequently to reduce spam
810- now := time.Now()
811- if eofCount == 1 || eofCount%10 == 0 || now.Sub(lastEOFLogTime) > time.Minute {
812- log.Debug("Docker event stream closed with EOF - will reconnect",
813- "count", eofCount,
814- "next_reconnect_delay", eofReconnectDelay)
815- lastEOFLogTime = now
883+ // If we still don't have an IP, skip this container
884+ if containerIP == "" {
885+ logger.Error("Failed to get container IP through any method",
886+ "container_id", event.id,
887+ "container_name", event.name)
888+ continue
816889 }
817- } else {
818- // Non-EOF errors are still important, log them as errors
819- log.Error("Error in container event stream", "error", err)
820- // Reset EOF counter for non-EOF errors
821- eofCount = 0
822890 }
823891
824- // Cancel the event context to clean up resources
825- eventCancel()
826-
827- // Increment failure counter for backoff calculation
828- consecutiveFailures++
829-
830- // If we have too many consecutive failures, log a warning
831- if consecutiveFailures >= maxConsecutiveFailures && consecutiveFailures%maxConsecutiveFailures == 0 {
832- log.Warn("Multiple consecutive failures in container event stream",
833- "count", consecutiveFailures,
834- "will_continue_trying", true)
892+ // Check if this is a container IP change or if it's a new discovery
893+ prevIP, ipExists := containerIPs[event.id]
894+ if ipExists && prevIP == containerIP {
895+ // IP hasn't actually changed, no need to trigger callback
896+ logger.Debug("Container IP verified but unchanged - skipping callback",
897+ "container_id", event.id,
898+ "container_name", event.name,
899+ "container_ip", containerIP)
900+
901+ // Update the container IP map to refresh timestamp
902+ containerIPs[event.id] = containerIP
903+ continue
835904 }
836905
837- // Calculate backoff delay with exponential increase for non-EOF errors
838- if err.Error() != "EOF" {
839- if listenSuccess {
840- // If we had successful events before failure, reset the delay
841- currentDelay = initialDelay
842- } else {
843- // Exponential backoff: increase the delay each time, up to max
844- currentDelay = time.Duration(float64(currentDelay) * 1.5)
845- if currentDelay > maxDelay {
846- currentDelay = maxDelay
906+ // Update the container IP map
907+ containerIPs[event.id] = containerIP
908+
909+ // For Gordon container, add extra verification to prevent routing issues
910+ if event.name == "gordon" {
911+ logger.Info("Gordon container event detected - verifying IP before callback",
912+ "container_id", event.id,
913+ "container_name", event.name,
914+ "container_ip", containerIP)
915+
916+ // Double-check the IP with a direct inspection for reliability
917+ containerInfo, err := GetContainerInfo(event.id)
918+ if err == nil && containerInfo.NetworkSettings != nil {
919+ if network, exists := containerInfo.NetworkSettings.Networks[networkFilter]; exists {
920+ if network.IPAddress != "" && network.IPAddress != containerIP {
921+ logger.Warn("IP mismatch detected for Gordon container - using network inspection IP",
922+ "container_id", event.id,
923+ "polling_ip", containerIP,
924+ "inspection_ip", network.IPAddress)
925+ containerIP = network.IPAddress
926+ containerIPs[event.id] = containerIP
927+ }
847928 }
848929 }
849930 }
850931
851- // Break out of the inner event loop to reconnect
852- break eventStreamLoop
853- }
854- // Context was canceled, exit completely
855- if ctx.Err() != nil {
856- eventCancel()
857- return
858- }
859- case event := <-messages:
860- // Mark that we've successfully received events
861- if !listenSuccess {
862- listenSuccess = true
863- // Reset failure counter on successful event
864- consecutiveFailures = 0
865- // Only log first successful connection or after errors
866- if eofCount <= 1 {
867- log.Info("Successfully receiving container events")
868- }
932+ // Add debug output before calling callback
933+ logger.Debug("Calling container event callback",
934+ "container_id", event.id,
935+ "container_name", event.name,
936+ "container_ip", containerIP)
937+
938+ // Call the callback with the container ID, name, and IP
939+ callback(event.id, event.name, containerIP)
940+
941+ logger.Debug("Processed container start",
942+ "container_id", event.id,
943+ "container_name", event.name,
944+ "container_ip", containerIP)
945+ } else if event.eventType == "stop" {
946+ // Handle container stop events if needed
947+ logger.Debug("Container stopped",
948+ "container_id", event.id,
949+ "container_name", event.name)
950+ // If any stop event handling is needed, add it here
951+
952+ // Remove from IP tracking map when container stops
953+ delete(containerIPs, event.id)
869954 }
955+ }
870956
871- // Update last event time
872- lastEventTime = time.Now()
873-
874- // Process container events
875- if event.Type == "container" {
876- containerID := event.Actor.ID
877-
878- // Get container name from event attributes
879- containerName := event.Actor.Attributes["name"]
880- if containerName == "" {
881- // If not in attributes, try to get it from the API
882- name, err := GetContainerName(containerID)
883- if err == nil {
884- containerName = strings.TrimPrefix(name, "/")
885- }
886- }
887-
888- // Handle container events based on action
889- switch event.Action {
890- case "start":
891- // Container started - need to get its IP and update any proxy routes
892- log.Debug("container started event",
893- "container_id", containerID,
894- "container_name", containerName)
895-
896- // Get container IP from the network
897- containerIP, err := getContainerIPFromNetwork(containerID, networkFilter)
898- if err != nil {
899- log.Error("Failed to get container IP",
900- "container_id", containerID,
901- "error", err)
902- continue
903- }
904-
905- // Call the callback with the container ID, name, and IP
906- callback(containerID, containerName, containerIP)
907-
908- case "die", "kill", "destroy":
909- // Container stopped - might need to mark routes as inactive
910- log.Debug("container stopped event",
911- "container_id", containerID,
912- "container_name", containerName,
913- "action", event.Action)
957+ case <-ctx.Done():
958+ logger.Info("Container polling stopped due to context cancellation")
959+ return
960+ }
961+ }
962+ }()
914963
915- case "rename":
916- // Container renamed - update related proxy routes
917- log.Debug("container renamed event",
918- "container_id", containerID,
919- "container_name", containerName,
920- "new_name", event.Actor.Attributes["name"])
964+ return nil
965+}
921966
922- // Add other event types as needed
923- }
924- }
925- case <-time.After(30 * time.Second):
926- // If we haven't received events for a while, check if the connection is still alive
927- timeSinceLastEvent := time.Since(lastEventTime)
967+// Container state tracking
968+type containerState struct {
969+ id string
970+ name string
971+ status string
972+ running bool
973+}
928974
929- // If it's been more than 2 minutes since the last event, reconnect
930- if timeSinceLastEvent > 2*time.Minute {
931- log.Debug("No events received for too long, reconnecting",
932- "time_since_last_event", timeSinceLastEvent)
975+// Container event structure
976+type containerEvent struct {
977+ id string
978+ name string
979+ eventType string // "start" or "stop"
980+}
933981
934- // Cancel the event context to clean up resources
935- eventCancel()
982+// detectContainerEvents compares previous and current container states to detect events
983+func detectContainerEvents(previousStates map[string]containerState, currentContainers []types.Container, networkFilter string) (map[string]containerState, []containerEvent) {
984+ newStates := make(map[string]containerState)
985+ var events []containerEvent
986+
987+ // Build map of current containers by ID
988+ for _, container := range currentContainers {
989+ // Skip containers not in the specified network if networkFilter is provided
990+ if networkFilter != "" {
991+ // Check if container is in the specified network
992+ inNetwork := false
993+
994+ // First, check if the container has any network with the network name matching our filter
995+ for networkName := range container.NetworkSettings.Networks {
996+ if networkName == networkFilter ||
997+ strings.Contains(strings.ToLower(networkName), strings.ToLower(networkFilter)) {
998+ inNetwork = true
999+ break
1000+ }
1001+ }
9361002
937- // Break out of the inner event loop to reconnect
938- break eventStreamLoop
1003+ // If not found by name, check network IDs as fallback (for compatibility with previous code)
1004+ if !inNetwork {
1005+ for _, network := range container.NetworkSettings.Networks {
1006+ if network.NetworkID == networkFilter ||
1007+ strings.Contains(strings.ToLower(network.NetworkID), strings.ToLower(networkFilter)) {
1008+ inNetwork = true
1009+ break
9391010 }
940- case <-ctx.Done():
941- eventCancel()
942- return
9431011 }
9441012 }
9451013
946- // If we get here, the event stream failed and we need to reconnect
947- // Clean up the event context
948- eventCancel()
949-
950- // Use different reconnection delays for different error types
951- var reconnectDelay time.Duration
952- if eofCount > 0 {
953- // Use fixed delay for EOF errors to prevent rapid reconnection cycles
954- reconnectDelay = eofReconnectDelay
955- } else {
956- // Use exponential backoff for other errors
957- reconnectDelay = currentDelay
1014+ if !inNetwork {
1015+ logger.Debug("Container not in target network - skipping",
1016+ "container_id", container.ID,
1017+ "container_name", strings.TrimPrefix(container.Names[0], "/"),
1018+ "network_filter", networkFilter)
1019+ continue
9581020 }
1021+ }
9591022
960- // Wait for the calculated delay before reconnecting
961- select {
962- case <-time.After(reconnectDelay):
963- // Only log reconnection attempts for non-EOF errors or occasional EOF errors
964- if eofCount == 0 || eofCount%10 == 0 {
965- log.Debug("Attempting to reconnect to container event stream", "delay", reconnectDelay)
966- }
967- case <-ctx.Done():
968- return
969- }
1023+ containerName := strings.TrimPrefix(container.Names[0], "/")
1024+
1025+ // Create current state entry
1026+ newState := containerState{
1027+ id: container.ID,
1028+ name: containerName,
1029+ status: container.Status,
1030+ running: strings.Contains(strings.ToLower(container.Status), "up"),
9701031 }
971- }()
9721032
973- return nil
974-}
1033+ // Add to new state map
1034+ newStates[container.ID] = newState
1035+
1036+ // Check if this is a new container or status changed from not running to running
1037+ prevState, existed := previousStates[container.ID]
1038+ if !existed {
1039+ // New container detected
1040+ if newState.running {
1041+ // New running container - fire "start" event
1042+ events = append(events, containerEvent{
1043+ id: container.ID,
1044+ name: containerName,
1045+ eventType: "start",
1046+ })
1047+ logger.Debug("New container detected",
1048+ "container_id", container.ID,
1049+ "container_name", containerName)
1050+ }
1051+ } else if !prevState.running && newState.running {
1052+ // Container changed from not running to running - fire "start" event
1053+ events = append(events, containerEvent{
1054+ id: container.ID,
1055+ name: containerName,
1056+ eventType: "start",
1057+ })
1058+ logger.Debug("Container started",
1059+ "container_id", container.ID,
1060+ "container_name", containerName)
1061+ }
1062+ }
9751063
976-// StopContainerEventListener stops the container event listener
977-func StopContainerEventListener() {
978- if containerEventCancel != nil {
979- containerEventCancel()
980- log.Info("Stopped container event listener")
1064+ // Check for stopped containers
1065+ for id, prevState := range previousStates {
1066+ if _, stillExists := newStates[id]; !stillExists && prevState.running {
1067+ // Container was running but is now gone - fire "stop" event
1068+ events = append(events, containerEvent{
1069+ id: id,
1070+ name: prevState.name,
1071+ eventType: "stop",
1072+ })
1073+ logger.Debug("Container stopped or removed",
1074+ "container_id", id,
1075+ "container_name", prevState.name)
1076+ }
9811077 }
1078+
1079+ return newStates, events
9821080 }
9831081
984-// getContainerIPFromNetwork gets a container's IP address from the specified network
985-func getContainerIPFromNetwork(containerID, networkName string) (string, error) {
1082+// GetContainerIPFromNetwork gets a container's IP address from the specified network
1083+func GetContainerIPFromNetwork(containerID, networkName string) (string, error) {
9861084 // Get network info
9871085 networkInfo, err := GetNetworkInfo(networkName)
9881086 if err != nil {
989- return "", fmt.Errorf("failed to get network info: %w", err)
1087+ // Simply log an error without trying to list networks
1088+ logger.Debug("Failed to get network info", "network", networkName, "error", err)
1089+ return "", fmt.Errorf("failed to get network info for %s: %w", networkName, err)
9901090 }
9911091
9921092 // Look for container in the network
9931093 containerEndpoint, exists := networkInfo.Containers[containerID]
9941094 if !exists {
995- return "", fmt.Errorf("container not found in network %s", networkName)
996- }
1095+ // For Podman compatibility, try alternative approaches
1096+
1097+ // First, try to get the container info directly
1098+ containerInfo, inspectErr := GetContainerInfo(containerID)
1099+ if inspectErr == nil && containerInfo.NetworkSettings != nil {
1100+ // Try to find the container IP using the NetworkSettings
1101+ if network, exists := containerInfo.NetworkSettings.Networks[networkName]; exists && network.IPAddress != "" {
1102+ logger.Debug("Found container IP using direct inspection - specific network",
1103+ "container_id", containerID,
1104+ "network", networkName,
1105+ "ip", network.IPAddress)
1106+ return network.IPAddress, nil
1107+ } else if containerInfo.NetworkSettings.IPAddress != "" {
1108+ // Fallback to default network IP
1109+ logger.Debug("Found container IP using direct inspection - default network",
1110+ "container_id", containerID,
1111+ "ip", containerInfo.NetworkSettings.IPAddress)
1112+ return containerInfo.NetworkSettings.IPAddress, nil
1113+ }
1114+ }
9971115
998- // Return the container's IP address
999- return containerEndpoint.IPv4Address, nil
1000-}
1116+ // If we got here, we couldn't find the container in the network
1117+ // Log details about the containers in the network for debugging
1118+ containerNames := []string{}
1119+ for id := range networkInfo.Containers {
1120+ name, _ := GetContainerName(id)
1121+ containerNames = append(containerNames, fmt.Sprintf("%s (%s)", name, id[:12]))
1122+ }
10011123
1002-// makeEventFilters creates filters for the Events API
1003-func makeEventFilters(networkName string) filters.Args {
1004- f := filters.NewArgs()
1124+ logger.Debug("Container not found in network",
1125+ "container_id", containerID,
1126+ "network", networkName,
1127+ "containers_in_network", strings.Join(containerNames, ", "))
10051128
1006- // Filter for container events only
1007- f.Add("type", "container")
1129+ return "", fmt.Errorf("container %s not found in network %s", containerID, networkName)
1130+ }
10081131
1009- // Filter for events we're interested in
1010- f.Add("event", "start")
1011- f.Add("event", "die")
1012- f.Add("event", "destroy")
1013- f.Add("event", "rename")
1132+ // If IPv4 address is not available, try IPv6
1133+ if containerEndpoint.IPv4Address == "" && containerEndpoint.IPv6Address != "" {
1134+ logger.Debug("Using IPv6 address instead of IPv4",
1135+ "container_id", containerID,
1136+ "ip", containerEndpoint.IPv6Address)
1137+ return containerEndpoint.IPv6Address, nil
1138+ }
10141139
1015- // If network name provided, filter for containers in that network
1016- if networkName != "" {
1017- f.Add("network", networkName)
1140+ // Return the container's IP address
1141+ // Extract just the IP part without subnet
1142+ ipAddress := containerEndpoint.IPv4Address
1143+ if idx := strings.Index(ipAddress, "/"); idx > 0 {
1144+ ipAddress = ipAddress[:idx]
10181145 }
10191146
1020- return f
1147+ return ipAddress, nil
1148+}
1149+
1150+// StopContainerEventListener stops the container event listener
1151+func StopContainerEventListener() {
1152+ if containerEventCancel != nil {
1153+ containerEventCancel()
1154+ logger.Info("Stopped container event listener")
1155+ }
10211156 }
10221157
10231158 // Variable to store cancel function for event listener
@@ -8,11 +8,9 @@ import (
8 "strings"8 "strings"
9 "time"9 "time"
10 10
11- "github.com/charmbracelet/log"11+ "github.com/bnema/gordon/pkg/logger"
12 "github.com/docker/docker/api/types"12 "github.com/docker/docker/api/types"
13 "github.com/docker/docker/api/types/container"13 "github.com/docker/docker/api/types/container"
14- "github.com/docker/docker/api/types/events"
15- "github.com/docker/docker/api/types/filters"
16 "github.com/docker/docker/api/types/network"14 "github.com/docker/docker/api/types/network"
17 "github.com/docker/docker/client"15 "github.com/docker/docker/client"
18 "github.com/docker/go-connections/nat"16 "github.com/docker/go-connections/nat"
@@ -114,17 +112,18 @@ func StopContainer(containerID string) error {
114 // Try graceful shutdown first112 // Try graceful shutdown first
115 stopped, err := StopContainerGracefully(containerID, 3*time.Second)113 stopped, err := StopContainerGracefully(containerID, 3*time.Second)
116 if err != nil {114 if err != nil {
117- log.Warn("Failed to stop container gracefully", "error", err)115+ logger.Warn("Failed to stop container gracefully", "error", err)
118 }116 }
119 117
120 // If container wasn't stopped gracefully or there was an error, try forceful shutdown118 // If container wasn't stopped gracefully or there was an error, try forceful shutdown
121 if !stopped {119 if !stopped {
122 if err := StopContainerRagefully(containerID); err != nil {120 if err := StopContainerRagefully(containerID); err != nil {
121+ logger.Error("Failed to stop container forcefully", "error", err)
123 return fmt.Errorf("failed to stop container forcefully: %w", err)122 return fmt.Errorf("failed to stop container forcefully: %w", err)
124 }123 }
125- log.Info("Container stopped forcefully", "containerID", containerID)124+ logger.Info("Container stopped forcefully", "containerID", containerID)
126 } else {125 } else {
127- log.Info("Container stopped gracefully", "containerID", containerID)126+ logger.Info("Container stopped gracefully", "containerID", containerID)
128 }127 }
129 128
130 return nil129 return nil
@@ -194,7 +193,7 @@ func RemoveContainer(containerID string) error {
194 193
195 // StartContainer starts a container194 // StartContainer starts a container
196 func StartContainer(containerID string) error {195 func StartContainer(containerID string) error {
197- fmt.Println("Starting container", containerID)196+ logger.Info("Starting container", "containerID", containerID)
198 197
199 // Add timeout context198 // Add timeout context
200 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)199 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
@@ -203,20 +202,23 @@ func StartContainer(containerID string) error {
203 // Get initial container state202 // Get initial container state
204 containerInfo, err := GetContainerInfo(containerID)203 containerInfo, err := GetContainerInfo(containerID)
205 if err != nil {204 if err != nil {
205+ logger.Error("Failed to get container info", "containerID", containerID, "error", err)
206 return fmt.Errorf("could not get container info: %v", err)206 return fmt.Errorf("could not get container info: %v", err)
207 }207 }
208 208
209 if containerInfo.State.Running {209 if containerInfo.State.Running {
210+ logger.Warn("Container is already running", "containerID", containerID)
210 return fmt.Errorf("container is already running")211 return fmt.Errorf("container is already running")
211 }212 }
212 213
213 // Start container214 // Start container
215+ logger.Debug("Executing container start", "containerID", containerID)
214 err = dockerCli.ContainerStart(ctx, containerID, container.StartOptions{})216 err = dockerCli.ContainerStart(ctx, containerID, container.StartOptions{})
215 if err != nil {217 if err != nil {
216 // Get container logs if start failed218 // Get container logs if start failed
217 logs, logErr := GetContainerLogs(containerID)219 logs, logErr := GetContainerLogs(containerID)
218 if logErr == nil {220 if logErr == nil {
219- fmt.Printf("Container logs after failed start: %s\n", logs)221+ logger.Error("Container logs after failed start", "containerID", containerID, "logs", logs)
220 }222 }
221 return fmt.Errorf("could not start container: %v", err)223 return fmt.Errorf("could not start container: %v", err)
222 }224 }
@@ -225,19 +227,22 @@ func StartContainer(containerID string) error {
225 for i := 0; i < 5; i++ {227 for i := 0; i < 5; i++ {
226 state, err := GetContainerState(containerID)228 state, err := GetContainerState(containerID)
227 if err != nil {229 if err != nil {
228- fmt.Printf("Error checking container state: %v\n", err)230+ logger.Warn("Error checking container state", "containerID", containerID, "error", err)
229 continue231 continue
230 }232 }
231 233
232 if state == "running" {234 if state == "running" {
235+ logger.Info("Container started successfully", "containerID", containerID)
233 return nil236 return nil
234 }237 }
235 238
239+ logger.Debug("Waiting for container to start", "containerID", containerID, "state", state, "attempt", i+1)
236 time.Sleep(time.Second)240 time.Sleep(time.Second)
237 }241 }
238 242
239 // If we get here, container didn't start properly243 // If we get here, container didn't start properly
240 logs, _ := GetContainerLogs(containerID)244 logs, _ := GetContainerLogs(containerID)
245+ logger.Error("Container failed to enter running state", "containerID", containerID, "logs", logs)
241 return fmt.Errorf("container failed to enter running state. Logs: %s", logs)246 return fmt.Errorf("container failed to enter running state. Logs: %s", logs)
242 }247 }
243 248
@@ -245,10 +250,11 @@ func StartContainer(containerID string) error {
245 func CreateContainer(cmdParams ContainerCommandParams) (string, error) {250 func CreateContainer(cmdParams ContainerCommandParams) (string, error) {
246 251
247 if err := CheckIfInitialized(); err != nil {252 if err := CheckIfInitialized(); err != nil {
253+ logger.Error("Failed to check if docker client is initialized", "error", err)
248 return "", fmt.Errorf("failed to check if docker client is initialized: %w", err)254 return "", fmt.Errorf("failed to check if docker client is initialized: %w", err)
249 }255 }
250 256
251- log.Info("Creating container with params",257+ logger.Info("Creating container with params",
252 "name", cmdParams.ContainerName,258 "name", cmdParams.ContainerName,
253 "image", cmdParams.ImageName,259 "image", cmdParams.ImageName,
254 "proxy_port", cmdParams.ProxyPort)260 "proxy_port", cmdParams.ProxyPort)
@@ -256,31 +262,31 @@ func CreateContainer(cmdParams ContainerCommandParams) (string, error) {
256 // Check if a container with this name already exists - handle recreation case262 // Check if a container with this name already exists - handle recreation case
257 existingContainers, err := ListRunningContainers()263 existingContainers, err := ListRunningContainers()
258 if err != nil {264 if err != nil {
259- log.Warn("Failed to check for existing containers", "error", err)265+ logger.Warn("Failed to check for existing containers", "error", err)
260 } else {266 } else {
261 for _, container := range existingContainers {267 for _, container := range existingContainers {
262 for _, name := range container.Names {268 for _, name := range container.Names {
263 // Container names in Docker API have a leading slash269 // Container names in Docker API have a leading slash
264 cleanName := strings.TrimPrefix(name, "/")270 cleanName := strings.TrimPrefix(name, "/")
265 if cleanName == cmdParams.ContainerName {271 if cleanName == cmdParams.ContainerName {
266- log.Warn("Container with this name already exists, it will be replaced",272+ logger.Warn("Container with this name already exists, it will be replaced",
267 "name", cmdParams.ContainerName,273 "name", cmdParams.ContainerName,
268 "existing_id", container.ID)274 "existing_id", container.ID)
269 275
270 // Stop and remove the existing container276 // Stop and remove the existing container
271 if err := StopContainer(container.ID); err != nil {277 if err := StopContainer(container.ID); err != nil {
272- log.Warn("Failed to stop existing container",278+ logger.Warn("Failed to stop existing container",
273 "container_id", container.ID,279 "container_id", container.ID,
274 "error", err)280 "error", err)
275 }281 }
276 282
277 if err := RemoveContainer(container.ID); err != nil {283 if err := RemoveContainer(container.ID); err != nil {
278- log.Warn("Failed to remove existing container",284+ logger.Warn("Failed to remove existing container",
279 "container_id", container.ID,285 "container_id", container.ID,
280 "error", err)286 "error", err)
281 }287 }
282 288
283- log.Info("Removed existing container to create a new one",289+ logger.Info("Removed existing container to create a new one",
284 "name", cmdParams.ContainerName)290 "name", cmdParams.ContainerName)
285 break291 break
286 }292 }
@@ -291,13 +297,15 @@ func CreateContainer(cmdParams ContainerCommandParams) (string, error) {
291 // Check network297 // Check network
292 isNetworkCreated, err := CheckIfNetworkExists(cmdParams.Network)298 isNetworkCreated, err := CheckIfNetworkExists(cmdParams.Network)
293 if err != nil {299 if err != nil {
300+ logger.Error("Network check failed", "network", cmdParams.Network, "error", err)
294 return "", fmt.Errorf("network check failed: %v", err)301 return "", fmt.Errorf("network check failed: %v", err)
295 }302 }
296 303
297 if !isNetworkCreated {304 if !isNetworkCreated {
298- fmt.Printf("Creating network: %s\n", cmdParams.Network)305+ logger.Info("Creating network", "network", cmdParams.Network)
299 _, err := dockerCli.NetworkCreate(context.Background(), cmdParams.Network, network.CreateOptions{})306 _, err := dockerCli.NetworkCreate(context.Background(), cmdParams.Network, network.CreateOptions{})
300 if err != nil {307 if err != nil {
308+ logger.Error("Network creation failed", "network", cmdParams.Network, "error", err)
301 return "", fmt.Errorf("network creation failed: %v", err)309 return "", fmt.Errorf("network creation failed: %v", err)
302 }310 }
303 }311 }
@@ -314,10 +322,10 @@ func CreateContainer(cmdParams ContainerCommandParams) (string, error) {
314 },322 },
315 }323 }
316 exposedPorts[exposedPort] = struct{}{}324 exposedPorts[exposedPort] = struct{}{}
317- fmt.Printf("Adding port mapping: %s:%s/%s\n",325+ logger.Debug("Adding port mapping",
318- portMapping.HostPort,326+ "host_port", portMapping.HostPort,
319- portMapping.ContainerPort,327+ "container_port", portMapping.ContainerPort,
320- portMapping.Protocol)328+ "protocol", portMapping.Protocol)
321 }329 }
322 330
323 // Prepare labels331 // Prepare labels
@@ -358,31 +366,37 @@ func CreateContainer(cmdParams ContainerCommandParams) (string, error) {
358 return "", fmt.Errorf("container creation failed: %v", err)366 return "", fmt.Errorf("container creation failed: %v", err)
359 }367 }
360 368
361- log.Info("Container created successfully with ID: %s\n", resp.ID)369+ logger.Info("Container created successfully", "id", resp.ID)
362 370
363 return resp.ID, nil371 return resp.ID, nil
364 }372 }
365 373
366 func WaitForContainerToBeRunning(containerID string, timeout time.Duration) error {374 func WaitForContainerToBeRunning(containerID string, timeout time.Duration) error {
375+ logger.Debug("Waiting for container to be running", "containerID", containerID, "timeout", timeout)
367 ctx, cancel := context.WithTimeout(context.Background(), timeout)376 ctx, cancel := context.WithTimeout(context.Background(), timeout)
368 defer cancel()377 defer cancel()
369 378
370 for {379 for {
371 select {380 select {
372 case <-ctx.Done():381 case <-ctx.Done():
382+ logger.Error("Timeout waiting for container to start", "containerID", containerID)
373 return fmt.Errorf("timeout waiting for container to start")383 return fmt.Errorf("timeout waiting for container to start")
374 case <-time.After(time.Second):384 case <-time.After(time.Second):
375 state, err := GetContainerState(containerID)385 state, err := GetContainerState(containerID)
376 if err != nil {386 if err != nil {
387+ logger.Error("Error checking container state", "containerID", containerID, "error", err)
377 return fmt.Errorf("error checking container state: %v", err)388 return fmt.Errorf("error checking container state: %v", err)
378 }389 }
379 390
391+ logger.Debug("Container state check", "containerID", containerID, "state", state)
380 if state == "running" {392 if state == "running" {
393+ logger.Info("Container is now running", "containerID", containerID)
381 return nil394 return nil
382 }395 }
383 396
384 if state == "exited" {397 if state == "exited" {
385 logs, _ := GetContainerLogs(containerID)398 logs, _ := GetContainerLogs(containerID)
399+ logger.Error("Container exited unexpectedly", "containerID", containerID, "logs", logs)
386 return fmt.Errorf("container exited unexpectedly. Logs: %s", logs)400 return fmt.Errorf("container exited unexpectedly. Logs: %s", logs)
387 }401 }
388 }402 }
@@ -642,7 +656,7 @@ func findContainersWithPort(ctx context.Context, cli *client.Client, containers
642 for _, container := range containers {656 for _, container := range containers {
643 containerInfo, err := getContainerPortInfo(ctx, cli, container)657 containerInfo, err := getContainerPortInfo(ctx, cli, container)
644 if err != nil {658 if err != nil {
645- log.Warn("Failed to inspect container", "container", container.ID, "error", err)659+ logger.Warn("Failed to inspect container", "container", container.ID, "error", err)
646 continue660 continue
647 }661 }
648 662
@@ -736,8 +750,10 @@ func formatDuration(d time.Duration) string {
736 return fmt.Sprintf("%dm", minutes)750 return fmt.Sprintf("%dm", minutes)
737 }751 }
738 752
739-// ListenForContainerEvents starts listening for container events and calls the provided callback753+// ListenForContainerEvents starts polling for container state changes and calls the provided callback
740-// when relevant container events occur. This is used for real-time 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 more
756+// stable for both Docker and Podman environments.
741 func ListenForContainerEvents(networkFilter string, callback func(string, string, string)) error {757 func ListenForContainerEvents(networkFilter string, callback func(string, string, string)) error {
742 // Check if the Docker client has been initialized758 // Check if the Docker client has been initialized
743 err := CheckIfInitialized()759 err := CheckIfInitialized()
@@ -751,273 +767,392 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
751 // Store cancel func to allow shutdown767 // Store cancel func to allow shutdown
752 containerEventCancel = cancel768 containerEventCancel = cancel
753 769
754- // Start the main listener loop in a goroutine770+ // Start the polling loop in a goroutine
755 go func() {771 go func() {
756- // Implement exponential backoff for reconnection772+ // Configure polling interval - can be adjusted based on needs
757- initialDelay := 1 * time.Second773+ pollInterval := 10 * time.Second
758- maxDelay := 1 * time.Minute774+ logger.Info("Starting container polling",
759- currentDelay := initialDelay775+ "network_filter", networkFilter,
776+ "poll_interval", pollInterval)
777+
778+ // Initialize container tracking map
779+ containerStates := make(map[string]containerState)
780+ // Track previous IPs to detect if they actually changed
781+ containerIPs := make(map[string]string)
782+
783+ // Create ticker for polling
784+ ticker := time.NewTicker(pollInterval)
785+ defer ticker.Stop()
786+
787+ // Track failures for backoff
760 consecutiveFailures := 0788 consecutiveFailures := 0
761- maxConsecutiveFailures := 10789+ maxConsecutiveFailures := 5
762-790+ initialBackoff := 5 * time.Second
763- // Track EOFs for reduced logging791+ maxBackoff := 1 * time.Minute
764- eofCount := 0792+ currentBackoff := initialBackoff
765- lastEOFLogTime := time.Now().Add(-10 * time.Minute) // Initialize to past time
766-
767- // Fixed delay for EOFs to prevent rapid reconnection
768- eofReconnectDelay := 5 * time.Second
769 793
770 for {794 for {
771- // Check if context was canceled (application is shutting down)795+ select {
772- if ctx.Err() != nil {796+ case <-ticker.C:
773- return797+ // Get current list of running containers
774- }798+ logger.Debug("Polling for container state changes", "network_filter", networkFilter)
775-799+ currentContainers, err := ListContainers(ctx)
776- // Add a timeout context for the Events call800+ if err != nil {
777- eventCtx, eventCancel := context.WithCancel(ctx)801+ consecutiveFailures++
802+ logger.Error("Failed to list containers during polling",
803+ "error", err,
804+ "consecutive_failures", consecutiveFailures)
805+
806+ // Implement backoff for repeated failures
807+ if consecutiveFailures >= maxConsecutiveFailures {
808+ // Increase backoff time
809+ currentBackoff = time.Duration(float64(currentBackoff) * 1.5)
810+ if currentBackoff > maxBackoff {
811+ currentBackoff = maxBackoff
812+ }
778 813
779- // Set up filters for the events we're interested in814+ logger.Warn("Multiple consecutive container polling failures",
780- filters := makeEventFilters(networkFilter)815+ "count", consecutiveFailures,
816+ "next_poll_after", currentBackoff)
781 817
782- log.Debug("connecting to container event stream", "network_filter", networkFilter)818+ // Temporarily slow down polling
819+ ticker.Reset(currentBackoff)
820+ }
821+ continue
822+ }
783 823
784- // Start listening for events824+ // Reset on successful poll
785- messages, errs := dockerCli.Events(eventCtx, events.ListOptions{825+ if consecutiveFailures > 0 {
786- Filters: filters,826+ logger.Debug("Container polling recovered after failures",
787- })827+ "previous_failure_count", consecutiveFailures)
828+ consecutiveFailures = 0
829+ // Reset to normal polling interval
830+ ticker.Reset(pollInterval)
831+ logger.Debug("Reset polling interval to normal", "interval", pollInterval)
832+ }
788 833
789- // Only log the first connection and every 10th reconnection for EOF834+ // Process the current containers
790- if eofCount == 0 {835+ newStates, events := detectContainerEvents(containerStates, currentContainers, networkFilter)
791- log.Info("Started container event listener", "network_filter", networkFilter)836+
792- } else {837+ // Update our tracking map
793- log.Debug("Reconnected container event listener", "network_filter", networkFilter, "reconnect_count", eofCount)838+ containerStates = newStates
794- }839+
840+ // Process detected events
841+ for _, event := range events {
842+ logger.Debug("Container event detected by polling",
843+ "container_id", event.id,
844+ "container_name", event.name,
845+ "event_type", event.eventType)
846+
847+ // Handle container start events
848+ if event.eventType == "start" {
849+ // Get container IP from the network
850+ containerIP, err := GetContainerIPFromNetwork(event.id, networkFilter)
851+ if err != nil {
852+ logger.Error("Failed to get container IP from network",
853+ "container_id", event.id,
854+ "container_name", event.name,
855+ "network", networkFilter,
856+ "error", err)
857+
858+ // Additional fallback to try to get IP through container inspection
859+ // This is especially important for Podman compatibility
860+ containerInfo, inspectErr := GetContainerInfo(event.id)
861+ if inspectErr == nil {
862+ // Try to get IP address from container inspection
863+ if containerInfo.NetworkSettings != nil {
864+ // First check if NetworkSettings.Networks contains our network
865+ if network, exists := containerInfo.NetworkSettings.Networks[networkFilter]; exists && network.IPAddress != "" {
866+ containerIP = network.IPAddress
867+ logger.Debug("Found container IP through inspection - network specific",
868+ "container_id", event.id,
869+ "container_name", event.name,
870+ "network", networkFilter,
871+ "ip", containerIP)
872+ } else if containerInfo.NetworkSettings.IPAddress != "" {
873+ // Fallback to default IPAddress
874+ containerIP = containerInfo.NetworkSettings.IPAddress
875+ logger.Debug("Found container IP through inspection - default network",
876+ "container_id", event.id,
877+ "container_name", event.name,
878+ "ip", containerIP)
879+ }
880+ }
881+ }
795 882
796- // Process events in an inner loop883+ // If we still don't have an IP, skip this container
797- listenSuccess := false884+ if containerIP == "" {
798- lastEventTime := time.Now()885+ logger.Error("Failed to get container IP through any method",
799- eventStreamLoop:886+ "container_id", event.id,
800- for {887+ "container_name", event.name)
801- select {888+ continue
802- case err := <-errs:
803- if err != nil && ctx.Err() == nil {
804- // Only log if this isn't due to context cancellation
805- if err.Error() == "EOF" {
806- // Increment EOF counter
807- eofCount++
808-
809- // Log EOF errors less frequently to reduce spam
810- now := time.Now()
811- if eofCount == 1 || eofCount%10 == 0 || now.Sub(lastEOFLogTime) > time.Minute {
812- log.Debug("Docker event stream closed with EOF - will reconnect",
813- "count", eofCount,
814- "next_reconnect_delay", eofReconnectDelay)
815- lastEOFLogTime = now
816 }889 }
817- } else {
818- // Non-EOF errors are still important, log them as errors
819- log.Error("Error in container event stream", "error", err)
820- // Reset EOF counter for non-EOF errors
821- eofCount = 0
822 }890 }
823 891
824- // Cancel the event context to clean up resources892+ // Check if this is a container IP change or if it's a new discovery
825- eventCancel()893+ prevIP, ipExists := containerIPs[event.id]
826-894+ if ipExists && prevIP == containerIP {
827- // Increment failure counter for backoff calculation895+ // IP hasn't actually changed, no need to trigger callback
828- consecutiveFailures++896+ logger.Debug("Container IP verified but unchanged - skipping callback",
829-897+ "container_id", event.id,
830- // If we have too many consecutive failures, log a warning898+ "container_name", event.name,
831- if consecutiveFailures >= maxConsecutiveFailures && consecutiveFailures%maxConsecutiveFailures == 0 {899+ "container_ip", containerIP)
832- log.Warn("Multiple consecutive failures in container event stream",900+
833- "count", consecutiveFailures,901+ // Update the container IP map to refresh timestamp
834- "will_continue_trying", true)902+ containerIPs[event.id] = containerIP
903+ continue
835 }904 }
836 905
837- // Calculate backoff delay with exponential increase for non-EOF errors906+ // Update the container IP map
838- if err.Error() != "EOF" {907+ containerIPs[event.id] = containerIP
839- if listenSuccess {908+
840- // If we had successful events before failure, reset the delay909+ // For Gordon container, add extra verification to prevent routing issues
841- currentDelay = initialDelay910+ if event.name == "gordon" {
842- } else {911+ logger.Info("Gordon container event detected - verifying IP before callback",
843- // Exponential backoff: increase the delay each time, up to max912+ "container_id", event.id,
844- currentDelay = time.Duration(float64(currentDelay) * 1.5)913+ "container_name", event.name,
845- if currentDelay > maxDelay {914+ "container_ip", containerIP)
846- currentDelay = maxDelay915+
916+ // Double-check the IP with a direct inspection for reliability
917+ containerInfo, err := GetContainerInfo(event.id)
918+ if err == nil && containerInfo.NetworkSettings != nil {
919+ if network, exists := containerInfo.NetworkSettings.Networks[networkFilter]; exists {
920+ if network.IPAddress != "" && network.IPAddress != containerIP {
921+ logger.Warn("IP mismatch detected for Gordon container - using network inspection IP",
922+ "container_id", event.id,
923+ "polling_ip", containerIP,
924+ "inspection_ip", network.IPAddress)
925+ containerIP = network.IPAddress
926+ containerIPs[event.id] = containerIP
927+ }
847 }928 }
848 }929 }
849 }930 }
850 931
851- // Break out of the inner event loop to reconnect932+ // Add debug output before calling callback
852- break eventStreamLoop933+ logger.Debug("Calling container event callback",
853- }934+ "container_id", event.id,
854- // Context was canceled, exit completely935+ "container_name", event.name,
855- if ctx.Err() != nil {936+ "container_ip", containerIP)
856- eventCancel()937+
857- return938+ // Call the callback with the container ID, name, and IP
858- }939+ callback(event.id, event.name, containerIP)
859- case event := <-messages:940+
860- // Mark that we've successfully received events941+ logger.Debug("Processed container start",
861- if !listenSuccess {942+ "container_id", event.id,
862- listenSuccess = true943+ "container_name", event.name,
863- // Reset failure counter on successful event944+ "container_ip", containerIP)
864- consecutiveFailures = 0945+ } else if event.eventType == "stop" {
865- // Only log first successful connection or after errors946+ // Handle container stop events if needed
866- if eofCount <= 1 {947+ logger.Debug("Container stopped",
867- log.Info("Successfully receiving container events")948+ "container_id", event.id,
868- }949+ "container_name", event.name)
950+ // If any stop event handling is needed, add it here
951+
952+ // Remove from IP tracking map when container stops
953+ delete(containerIPs, event.id)
869 }954 }
955+ }
870 956
871- // Update last event time957+ case <-ctx.Done():
872- lastEventTime = time.Now()958+ logger.Info("Container polling stopped due to context cancellation")
873-959+ return
874- // Process container events960+ }
875- if event.Type == "container" {961+ }
876- containerID := event.Actor.ID962+ }()
877-
878- // Get container name from event attributes
879- containerName := event.Actor.Attributes["name"]
880- if containerName == "" {
881- // If not in attributes, try to get it from the API
882- name, err := GetContainerName(containerID)
883- if err == nil {
884- containerName = strings.TrimPrefix(name, "/")
885- }
886- }
887-
888- // Handle container events based on action
889- switch event.Action {
890- case "start":
891- // Container started - need to get its IP and update any proxy routes
892- log.Debug("container started event",
893- "container_id", containerID,
894- "container_name", containerName)
895-
896- // Get container IP from the network
897- containerIP, err := getContainerIPFromNetwork(containerID, networkFilter)
898- if err != nil {
899- log.Error("Failed to get container IP",
900- "container_id", containerID,
901- "error", err)
902- continue
903- }
904-
905- // Call the callback with the container ID, name, and IP
906- callback(containerID, containerName, containerIP)
907-
908- case "die", "kill", "destroy":
909- // Container stopped - might need to mark routes as inactive
910- log.Debug("container stopped event",
911- "container_id", containerID,
912- "container_name", containerName,
913- "action", event.Action)
914 963
915- case "rename":964+ return nil
916- // Container renamed - update related proxy routes965+}
917- log.Debug("container renamed event",
918- "container_id", containerID,
919- "container_name", containerName,
920- "new_name", event.Actor.Attributes["name"])
921 966
922- // Add other event types as needed967+// Container state tracking
923- }968+type containerState struct {
924- }969+ id string
925- case <-time.After(30 * time.Second):970+ name string
926- // If we haven't received events for a while, check if the connection is still alive971+ status string
927- timeSinceLastEvent := time.Since(lastEventTime)972+ running bool
973+}
928 974
929- // If it's been more than 2 minutes since the last event, reconnect975+// Container event structure
930- if timeSinceLastEvent > 2*time.Minute {976+type containerEvent struct {
931- log.Debug("No events received for too long, reconnecting",977+ id string
932- "time_since_last_event", timeSinceLastEvent)978+ name string
979+ eventType string // "start" or "stop"
980+}
933 981
934- // Cancel the event context to clean up resources982+// detectContainerEvents compares previous and current container states to detect events
935- eventCancel()983+func detectContainerEvents(previousStates map[string]containerState, currentContainers []types.Container, networkFilter string) (map[string]containerState, []containerEvent) {
984+ newStates := make(map[string]containerState)
985+ var events []containerEvent
986+
987+ // Build map of current containers by ID
988+ for _, container := range currentContainers {
989+ // Skip containers not in the specified network if networkFilter is provided
990+ if networkFilter != "" {
991+ // Check if container is in the specified network
992+ inNetwork := false
993+
994+ // First, check if the container has any network with the network name matching our filter
995+ for networkName := range container.NetworkSettings.Networks {
996+ if networkName == networkFilter ||
997+ strings.Contains(strings.ToLower(networkName), strings.ToLower(networkFilter)) {
998+ inNetwork = true
999+ break
1000+ }
1001+ }
936 1002
937- // Break out of the inner event loop to reconnect1003+ // If not found by name, check network IDs as fallback (for compatibility with previous code)
938- break eventStreamLoop1004+ if !inNetwork {
1005+ for _, network := range container.NetworkSettings.Networks {
1006+ if network.NetworkID == networkFilter ||
1007+ strings.Contains(strings.ToLower(network.NetworkID), strings.ToLower(networkFilter)) {
1008+ inNetwork = true
1009+ break
939 }1010 }
940- case <-ctx.Done():
941- eventCancel()
942- return
943 }1011 }
944 }1012 }
945 1013
946- // If we get here, the event stream failed and we need to reconnect1014+ if !inNetwork {
947- // Clean up the event context1015+ logger.Debug("Container not in target network - skipping",
948- eventCancel()1016+ "container_id", container.ID,
949-1017+ "container_name", strings.TrimPrefix(container.Names[0], "/"),
950- // Use different reconnection delays for different error types1018+ "network_filter", networkFilter)
951- var reconnectDelay time.Duration1019+ continue
952- if eofCount > 0 {
953- // Use fixed delay for EOF errors to prevent rapid reconnection cycles
954- reconnectDelay = eofReconnectDelay
955- } else {
956- // Use exponential backoff for other errors
957- reconnectDelay = currentDelay
958 }1020 }
1021+ }
959 1022
960- // Wait for the calculated delay before reconnecting1023+ containerName := strings.TrimPrefix(container.Names[0], "/")
961- select {1024+
962- case <-time.After(reconnectDelay):1025+ // Create current state entry
963- // Only log reconnection attempts for non-EOF errors or occasional EOF errors1026+ newState := containerState{
964- if eofCount == 0 || eofCount%10 == 0 {1027+ id: container.ID,
965- log.Debug("Attempting to reconnect to container event stream", "delay", reconnectDelay)1028+ name: containerName,
966- }1029+ status: container.Status,
967- case <-ctx.Done():1030+ running: strings.Contains(strings.ToLower(container.Status), "up"),
968- return
969- }
970 }1031 }
971- }()
972 1032
973- return nil1033+ // Add to new state map
974-}1034+ newStates[container.ID] = newState
1035+
1036+ // Check if this is a new container or status changed from not running to running
1037+ prevState, existed := previousStates[container.ID]
1038+ if !existed {
1039+ // New container detected
1040+ if newState.running {
1041+ // New running container - fire "start" event
1042+ events = append(events, containerEvent{
1043+ id: container.ID,
1044+ name: containerName,
1045+ eventType: "start",
1046+ })
1047+ logger.Debug("New container detected",
1048+ "container_id", container.ID,
1049+ "container_name", containerName)
1050+ }
1051+ } else if !prevState.running && newState.running {
1052+ // Container changed from not running to running - fire "start" event
1053+ events = append(events, containerEvent{
1054+ id: container.ID,
1055+ name: containerName,
1056+ eventType: "start",
1057+ })
1058+ logger.Debug("Container started",
1059+ "container_id", container.ID,
1060+ "container_name", containerName)
1061+ }
1062+ }
975 1063
976-// StopContainerEventListener stops the container event listener1064+ // Check for stopped containers
977-func StopContainerEventListener() {1065+ for id, prevState := range previousStates {
978- if containerEventCancel != nil {1066+ if _, stillExists := newStates[id]; !stillExists && prevState.running {
979- containerEventCancel()1067+ // Container was running but is now gone - fire "stop" event
980- log.Info("Stopped container event listener")1068+ events = append(events, containerEvent{
1069+ id: id,
1070+ name: prevState.name,
1071+ eventType: "stop",
1072+ })
1073+ logger.Debug("Container stopped or removed",
1074+ "container_id", id,
1075+ "container_name", prevState.name)
1076+ }
981 }1077 }
1078+
1079+ return newStates, events
982 }1080 }
983 1081
984-// getContainerIPFromNetwork gets a container's IP address from the specified network1082+// GetContainerIPFromNetwork gets a container's IP address from the specified network
985-func getContainerIPFromNetwork(containerID, networkName string) (string, error) {1083+func GetContainerIPFromNetwork(containerID, networkName string) (string, error) {
986 // Get network info1084 // Get network info
987 networkInfo, err := GetNetworkInfo(networkName)1085 networkInfo, err := GetNetworkInfo(networkName)
988 if err != nil {1086 if err != nil {
989- return "", fmt.Errorf("failed to get network info: %w", err)1087+ // Simply log an error without trying to list networks
1088+ logger.Debug("Failed to get network info", "network", networkName, "error", err)
1089+ return "", fmt.Errorf("failed to get network info for %s: %w", networkName, err)
990 }1090 }
991 1091
992 // Look for container in the network1092 // Look for container in the network
993 containerEndpoint, exists := networkInfo.Containers[containerID]1093 containerEndpoint, exists := networkInfo.Containers[containerID]
994 if !exists {1094 if !exists {
995- return "", fmt.Errorf("container not found in network %s", networkName)1095+ // For Podman compatibility, try alternative approaches
996- }1096+
1097+ // First, try to get the container info directly
1098+ containerInfo, inspectErr := GetContainerInfo(containerID)
1099+ if inspectErr == nil && containerInfo.NetworkSettings != nil {
1100+ // Try to find the container IP using the NetworkSettings
1101+ if network, exists := containerInfo.NetworkSettings.Networks[networkName]; exists && network.IPAddress != "" {
1102+ logger.Debug("Found container IP using direct inspection - specific network",
1103+ "container_id", containerID,
1104+ "network", networkName,
1105+ "ip", network.IPAddress)
1106+ return network.IPAddress, nil
1107+ } else if containerInfo.NetworkSettings.IPAddress != "" {
1108+ // Fallback to default network IP
1109+ logger.Debug("Found container IP using direct inspection - default network",
1110+ "container_id", containerID,
1111+ "ip", containerInfo.NetworkSettings.IPAddress)
1112+ return containerInfo.NetworkSettings.IPAddress, nil
1113+ }
1114+ }
997 1115
998- // Return the container's IP address1116+ // If we got here, we couldn't find the container in the network
999- return containerEndpoint.IPv4Address, nil1117+ // Log details about the containers in the network for debugging
1000-}1118+ containerNames := []string{}
1119+ for id := range networkInfo.Containers {
1120+ name, _ := GetContainerName(id)
1121+ containerNames = append(containerNames, fmt.Sprintf("%s (%s)", name, id[:12]))
1122+ }
1001 1123
1002-// makeEventFilters creates filters for the Events API1124+ logger.Debug("Container not found in network",
1003-func makeEventFilters(networkName string) filters.Args {1125+ "container_id", containerID,
1004- f := filters.NewArgs()1126+ "network", networkName,
1127+ "containers_in_network", strings.Join(containerNames, ", "))
1005 1128
1006- // Filter for container events only1129+ return "", fmt.Errorf("container %s not found in network %s", containerID, networkName)
1007- f.Add("type", "container")1130+ }
1008 1131
1009- // Filter for events we're interested in1132+ // If IPv4 address is not available, try IPv6
1010- f.Add("event", "start")1133+ if containerEndpoint.IPv4Address == "" && containerEndpoint.IPv6Address != "" {
1011- f.Add("event", "die")1134+ logger.Debug("Using IPv6 address instead of IPv4",
1012- f.Add("event", "destroy")1135+ "container_id", containerID,
1013- f.Add("event", "rename")1136+ "ip", containerEndpoint.IPv6Address)
1137+ return containerEndpoint.IPv6Address, nil
1138+ }
1014 1139
1015- // If network name provided, filter for containers in that network1140+ // Return the container's IP address
1016- if networkName != "" {1141+ // Extract just the IP part without subnet
1017- f.Add("network", networkName)1142+ ipAddress := containerEndpoint.IPv4Address
1143+ if idx := strings.Index(ipAddress, "/"); idx > 0 {
1144+ ipAddress = ipAddress[:idx]
1018 }1145 }
1019 1146
1020- return f1147+ return ipAddress, nil
1148+}
1149+
1150+// StopContainerEventListener stops the container event listener
1151+func StopContainerEventListener() {
1152+ if containerEventCancel != nil {
1153+ containerEventCancel()
1154+ logger.Info("Stopped container event listener")
1155+ }
1021 }1156 }
1022 1157
1023 // Variable to store cancel function for event listener1158 // Variable to store cancel function for event listener
M pkg/docker/detect.go +42 -11
@@ -4,8 +4,9 @@ import (
44 "os"
55 "os/user"
66 "path/filepath"
7+ "strings"
78
8- log "github.com/charmbracelet/log"
9+ "github.com/bnema/gordon/pkg/logger"
910 )
1011
1112 func fileExists(filepath string) bool {
@@ -14,11 +15,41 @@ func fileExists(filepath string) bool {
1415 }
1516
1617 func IsRunningInContainer() bool {
17-
18- // Check for .dockerenv file or .iscontainer
19- isContainer := fileExists("/.iscontainer") || fileExists("/.dockerenv")
20- log.Debug("IsRunningInContainer: Container detection result", "isContainer", isContainer)
21- return isContainer
18+ // Method 1: Check for .dockerenv file or .iscontainer (Docker standard)
19+ if fileExists("/.iscontainer") || fileExists("/.dockerenv") {
20+ logger.Debug("IsRunningInContainer: Container detected via marker files")
21+ return true
22+ }
23+
24+ // Method 2: Check hostname format (often container IDs in Docker/Podman)
25+ hostname := os.Getenv("HOSTNAME")
26+ if hostname != "" && len(hostname) >= 12 {
27+ // Container IDs are typically at least 12 chars and hexadecimal
28+ isHexString := true
29+ for _, c := range hostname[:12] {
30+ if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
31+ isHexString = false
32+ break
33+ }
34+ }
35+
36+ if isHexString {
37+ logger.Debug("IsRunningInContainer: Container detected via hostname format", "hostname", hostname)
38+ return true
39+ }
40+ }
41+
42+ // Method 3: Check cgroup info (works in most container environments)
43+ if cgroupData, err := os.ReadFile("/proc/1/cgroup"); err == nil {
44+ if !strings.Contains(string(cgroupData), ":/") {
45+ // Non-root cgroup namespace indicates containerization
46+ logger.Debug("IsRunningInContainer: Container detected via cgroup namespace")
47+ return true
48+ }
49+ }
50+
51+ logger.Debug("IsRunningInContainer: Not running in a container")
52+ return false
2253 }
2354
2455 // DetectPodman automatically detects if Podman is available and returns
@@ -32,7 +63,7 @@ func DetectPodman() (bool, string) {
3263
3364 for _, socket := range systemSockets {
3465 if fileExists(socket) {
35- log.Debug("DetectPodman: Found system Podman socket", "path", socket)
66+ logger.Debug("DetectPodman: Found system Podman socket", "path", socket)
3667 return true, socket
3768 }
3869 }
@@ -40,7 +71,7 @@ func DetectPodman() (bool, string) {
4071 // Then check for rootless installation
4172 currentUser, err := user.Current()
4273 if err != nil {
43- log.Error("DetectPodman: Failed to get current user", "error", err)
74+ logger.Error("DetectPodman: Failed to get current user", "error", err)
4475 return false, ""
4576 }
4677
@@ -49,7 +80,7 @@ func DetectPodman() (bool, string) {
4980 if xdgRuntime != "" {
5081 xdgSocket := filepath.Join(xdgRuntime, "podman", "podman.sock")
5182 if fileExists(xdgSocket) {
52- log.Debug("DetectPodman: Found rootless Podman socket (XDG)", "path", xdgSocket)
83+ logger.Debug("DetectPodman: Found rootless Podman socket (XDG)", "path", xdgSocket)
5384 return true, xdgSocket
5485 }
5586 }
@@ -57,11 +88,11 @@ func DetectPodman() (bool, string) {
5788 // Check home directory socket location
5889 homeSocket := filepath.Join(currentUser.HomeDir, ".local", "share", "containers", "podman", "machine", "podman.sock")
5990 if fileExists(homeSocket) {
60- log.Debug("DetectPodman: Found rootless Podman socket (home)", "path", homeSocket)
91+ logger.Debug("DetectPodman: Found rootless Podman socket (home)", "path", homeSocket)
6192 return true, homeSocket
6293 }
6394
6495 // No Podman socket found
65- log.Debug("DetectPodman: No Podman sockets found")
96+ logger.Debug("DetectPodman: No Podman sockets found")
6697 return false, ""
6798 }
@@ -4,8 +4,9 @@ import (
4 "os"4 "os"
5 "os/user"5 "os/user"
6 "path/filepath"6 "path/filepath"
7+ "strings"
7 8
8- log "github.com/charmbracelet/log"9+ "github.com/bnema/gordon/pkg/logger"
9 )10 )
10 11
11 func fileExists(filepath string) bool {12 func fileExists(filepath string) bool {
@@ -14,11 +15,41 @@ func fileExists(filepath string) bool {
14 }15 }
15 16
16 func IsRunningInContainer() bool {17 func IsRunningInContainer() bool {
17-18+ // Method 1: Check for .dockerenv file or .iscontainer (Docker standard)
18- // Check for .dockerenv file or .iscontainer19+ if fileExists("/.iscontainer") || fileExists("/.dockerenv") {
19- isContainer := fileExists("/.iscontainer") || fileExists("/.dockerenv")20+ logger.Debug("IsRunningInContainer: Container detected via marker files")
20- log.Debug("IsRunningInContainer: Container detection result", "isContainer", isContainer)21+ return true
21- return isContainer22+ }
23+
24+ // Method 2: Check hostname format (often container IDs in Docker/Podman)
25+ hostname := os.Getenv("HOSTNAME")
26+ if hostname != "" && len(hostname) >= 12 {
27+ // Container IDs are typically at least 12 chars and hexadecimal
28+ isHexString := true
29+ for _, c := range hostname[:12] {
30+ if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
31+ isHexString = false
32+ break
33+ }
34+ }
35+
36+ if isHexString {
37+ logger.Debug("IsRunningInContainer: Container detected via hostname format", "hostname", hostname)
38+ return true
39+ }
40+ }
41+
42+ // Method 3: Check cgroup info (works in most container environments)
43+ if cgroupData, err := os.ReadFile("/proc/1/cgroup"); err == nil {
44+ if !strings.Contains(string(cgroupData), ":/") {
45+ // Non-root cgroup namespace indicates containerization
46+ logger.Debug("IsRunningInContainer: Container detected via cgroup namespace")
47+ return true
48+ }
49+ }
50+
51+ logger.Debug("IsRunningInContainer: Not running in a container")
52+ return false
22 }53 }
23 54
24 // DetectPodman automatically detects if Podman is available and returns55 // DetectPodman automatically detects if Podman is available and returns
@@ -32,7 +63,7 @@ func DetectPodman() (bool, string) {
32 63
33 for _, socket := range systemSockets {64 for _, socket := range systemSockets {
34 if fileExists(socket) {65 if fileExists(socket) {
35- log.Debug("DetectPodman: Found system Podman socket", "path", socket)66+ logger.Debug("DetectPodman: Found system Podman socket", "path", socket)
36 return true, socket67 return true, socket
37 }68 }
38 }69 }
@@ -40,7 +71,7 @@ func DetectPodman() (bool, string) {
40 // Then check for rootless installation71 // Then check for rootless installation
41 currentUser, err := user.Current()72 currentUser, err := user.Current()
42 if err != nil {73 if err != nil {
43- log.Error("DetectPodman: Failed to get current user", "error", err)74+ logger.Error("DetectPodman: Failed to get current user", "error", err)
44 return false, ""75 return false, ""
45 }76 }
46 77
@@ -49,7 +80,7 @@ func DetectPodman() (bool, string) {
49 if xdgRuntime != "" {80 if xdgRuntime != "" {
50 xdgSocket := filepath.Join(xdgRuntime, "podman", "podman.sock")81 xdgSocket := filepath.Join(xdgRuntime, "podman", "podman.sock")
51 if fileExists(xdgSocket) {82 if fileExists(xdgSocket) {
52- log.Debug("DetectPodman: Found rootless Podman socket (XDG)", "path", xdgSocket)83+ logger.Debug("DetectPodman: Found rootless Podman socket (XDG)", "path", xdgSocket)
53 return true, xdgSocket84 return true, xdgSocket
54 }85 }
55 }86 }
@@ -57,11 +88,11 @@ func DetectPodman() (bool, string) {
57 // Check home directory socket location88 // Check home directory socket location
58 homeSocket := filepath.Join(currentUser.HomeDir, ".local", "share", "containers", "podman", "machine", "podman.sock")89 homeSocket := filepath.Join(currentUser.HomeDir, ".local", "share", "containers", "podman", "machine", "podman.sock")
59 if fileExists(homeSocket) {90 if fileExists(homeSocket) {
60- log.Debug("DetectPodman: Found rootless Podman socket (home)", "path", homeSocket)91+ logger.Debug("DetectPodman: Found rootless Podman socket (home)", "path", homeSocket)
61 return true, homeSocket92 return true, homeSocket
62 }93 }
63 94
64 // No Podman socket found95 // No Podman socket found
65- log.Debug("DetectPodman: No Podman sockets found")96+ logger.Debug("DetectPodman: No Podman sockets found")
66 return false, ""97 return false, ""
67 }98 }
M pkg/kv/ratelimiter.go +42 -27
@@ -66,6 +66,9 @@ func NewStarskeyRateLimiterStore(dbPath string, rate float64, burst int, expires
6666 func (s *StarskeyRateLimiterStore) Allow(identifier string) (bool, error) {
6767 var allowed bool
6868
69+ // Log the identifier for debugging
70+ log.Debug("Rate limiter checking identifier", "identifier", identifier)
71+
6972 err := s.db.Update(func(txn *starskey.Txn) error {
7073 now := time.Now()
7174 key := []byte(identifier)
@@ -88,6 +91,7 @@ func (s *StarskeyRateLimiterStore) Allow(identifier string) (bool, error) {
8891 // Check if jailed
8992 if info.Jailed && now.Before(info.ResetTime) {
9093 allowed = false
94+ log.Debug("Request denied: IP is jailed", "ip", identifier, "jail_expires", info.ResetTime)
9195 return nil
9296 }
9397
@@ -95,53 +99,64 @@ func (s *StarskeyRateLimiterStore) Allow(identifier string) (bool, error) {
9599 if info.Jailed && !now.Before(info.ResetTime) {
96100 log.Info("IP address released from jail", "ip", identifier)
97101 info.Jailed = false
102+ info.Attempts = 0 // Reset attempts when released from jail
98103 }
99104
100105 // Calculate time-based allowance
101106 timePassed := now.Sub(info.ResetTime).Seconds()
102107 tokensToAdd := timePassed * s.rate
103108
109+ log.Debug("Rate limit state",
110+ "ip", identifier,
111+ "current_attempts", info.Attempts,
112+ "burst_limit", s.burst,
113+ "time_passed", timePassed,
114+ "tokens_to_add", tokensToAdd)
115+
104116 // Reset if needed
105117 if now.After(info.ResetTime.Add(s.expiresIn)) {
118+ log.Debug("Resetting rate limit counter (expired)", "ip", identifier)
106119 info.Attempts = 0
107120 info.ResetTime = now
108121 } else {
122+ oldAttempts := info.Attempts
109123 info.Attempts = max(0, info.Attempts-int(tokensToAdd))
124+ log.Debug("Updated attempts count",
125+ "ip", identifier,
126+ "old_attempts", oldAttempts,
127+ "new_attempts", info.Attempts)
110128 }
111129 }
112130
113- // Check if under rate limit
114- if info.Attempts < s.burst {
131+ // Check if this request would exceed burst limit
132+ if info.Attempts >= s.burst {
133+ // Jail the IP before returning not allowed
134+ info.Jailed = true
135+ info.ResetTime = now.Add(10 * time.Second) // 10 second penalty (increased from 1 second)
136+ log.Warn("IP address jailed due to rate limit violation",
137+ "ip", identifier,
138+ "attempts", info.Attempts,
139+ "burst_limit", s.burst,
140+ "reset_at", info.ResetTime)
141+ allowed = false
142+ } else {
143+ // Under limit, allow and increment
115144 info.Attempts++
116145 allowed = true
146+ log.Debug("Request allowed",
147+ "ip", identifier,
148+ "attempts", info.Attempts,
149+ "burst_limit", s.burst)
150+ }
117151
118- // Jail if needed (e.g., if burst exceeded)
119- if info.Attempts >= s.burst {
120- info.Jailed = true
121- info.ResetTime = now.Add(time.Second) // 1 second penalty
122- log.Debug("IP jailed (burst exceeded)", "ip", identifier, "reset_at", info.ResetTime)
123- // Add info level log when an IP is jailed
124- log.Info("IP address jailed due to rate limit violation", "ip", identifier, "reset_at", info.ResetTime)
125- } else {
126- log.Debug("Request allowed", "ip", identifier, "attempts", info.Attempts)
127- }
128-
129- // Save updated info
130- data, err := json.Marshal(info)
131- if err != nil {
132- log.Debug("Failed to marshal rate limit info", "ip", identifier, "error", err)
133- return err
134- }
135-
136- // Fix: txn.Put doesn't return a value in Starskey
137- // The errors are handled at the transaction level
138- txn.Put(key, data)
139- log.Debug("Rate limit info saved", "ip", identifier)
140- return nil
152+ // Save updated info
153+ data, err := json.Marshal(info)
154+ if err != nil {
155+ log.Debug("Failed to marshal rate limit info", "ip", identifier, "error", err)
156+ return err
141157 }
142158
143- log.Debug("Request blocked (rate limited)", "ip", identifier, "attempts", info.Attempts)
144- allowed = false
159+ txn.Put(key, data)
145160 return nil
146161 })
147162
@@ -66,6 +66,9 @@ func NewStarskeyRateLimiterStore(dbPath string, rate float64, burst int, expires
66 func (s *StarskeyRateLimiterStore) Allow(identifier string) (bool, error) {66 func (s *StarskeyRateLimiterStore) Allow(identifier string) (bool, error) {
67 var allowed bool67 var allowed bool
68 68
69+ // Log the identifier for debugging
70+ log.Debug("Rate limiter checking identifier", "identifier", identifier)
71+
69 err := s.db.Update(func(txn *starskey.Txn) error {72 err := s.db.Update(func(txn *starskey.Txn) error {
70 now := time.Now()73 now := time.Now()
71 key := []byte(identifier)74 key := []byte(identifier)
@@ -88,6 +91,7 @@ func (s *StarskeyRateLimiterStore) Allow(identifier string) (bool, error) {
88 // Check if jailed91 // Check if jailed
89 if info.Jailed && now.Before(info.ResetTime) {92 if info.Jailed && now.Before(info.ResetTime) {
90 allowed = false93 allowed = false
94+ log.Debug("Request denied: IP is jailed", "ip", identifier, "jail_expires", info.ResetTime)
91 return nil95 return nil
92 }96 }
93 97
@@ -95,53 +99,64 @@ func (s *StarskeyRateLimiterStore) Allow(identifier string) (bool, error) {
95 if info.Jailed && !now.Before(info.ResetTime) {99 if info.Jailed && !now.Before(info.ResetTime) {
96 log.Info("IP address released from jail", "ip", identifier)100 log.Info("IP address released from jail", "ip", identifier)
97 info.Jailed = false101 info.Jailed = false
102+ info.Attempts = 0 // Reset attempts when released from jail
98 }103 }
99 104
100 // Calculate time-based allowance105 // Calculate time-based allowance
101 timePassed := now.Sub(info.ResetTime).Seconds()106 timePassed := now.Sub(info.ResetTime).Seconds()
102 tokensToAdd := timePassed * s.rate107 tokensToAdd := timePassed * s.rate
103 108
109+ log.Debug("Rate limit state",
110+ "ip", identifier,
111+ "current_attempts", info.Attempts,
112+ "burst_limit", s.burst,
113+ "time_passed", timePassed,
114+ "tokens_to_add", tokensToAdd)
115+
104 // Reset if needed116 // Reset if needed
105 if now.After(info.ResetTime.Add(s.expiresIn)) {117 if now.After(info.ResetTime.Add(s.expiresIn)) {
118+ log.Debug("Resetting rate limit counter (expired)", "ip", identifier)
106 info.Attempts = 0119 info.Attempts = 0
107 info.ResetTime = now120 info.ResetTime = now
108 } else {121 } else {
122+ oldAttempts := info.Attempts
109 info.Attempts = max(0, info.Attempts-int(tokensToAdd))123 info.Attempts = max(0, info.Attempts-int(tokensToAdd))
124+ log.Debug("Updated attempts count",
125+ "ip", identifier,
126+ "old_attempts", oldAttempts,
127+ "new_attempts", info.Attempts)
110 }128 }
111 }129 }
112 130
113- // Check if under rate limit131+ // Check if this request would exceed burst limit
114- if info.Attempts < s.burst {132+ if info.Attempts >= s.burst {
133+ // Jail the IP before returning not allowed
134+ info.Jailed = true
135+ info.ResetTime = now.Add(10 * time.Second) // 10 second penalty (increased from 1 second)
136+ log.Warn("IP address jailed due to rate limit violation",
137+ "ip", identifier,
138+ "attempts", info.Attempts,
139+ "burst_limit", s.burst,
140+ "reset_at", info.ResetTime)
141+ allowed = false
142+ } else {
143+ // Under limit, allow and increment
115 info.Attempts++144 info.Attempts++
116 allowed = true145 allowed = true
146+ log.Debug("Request allowed",
147+ "ip", identifier,
148+ "attempts", info.Attempts,
149+ "burst_limit", s.burst)
150+ }
117 151
118- // Jail if needed (e.g., if burst exceeded)152+ // Save updated info
119- if info.Attempts >= s.burst {153+ data, err := json.Marshal(info)
120- info.Jailed = true154+ if err != nil {
121- info.ResetTime = now.Add(time.Second) // 1 second penalty155+ log.Debug("Failed to marshal rate limit info", "ip", identifier, "error", err)
122- log.Debug("IP jailed (burst exceeded)", "ip", identifier, "reset_at", info.ResetTime)156+ return err
123- // Add info level log when an IP is jailed
124- log.Info("IP address jailed due to rate limit violation", "ip", identifier, "reset_at", info.ResetTime)
125- } else {
126- log.Debug("Request allowed", "ip", identifier, "attempts", info.Attempts)
127- }
128-
129- // Save updated info
130- data, err := json.Marshal(info)
131- if err != nil {
132- log.Debug("Failed to marshal rate limit info", "ip", identifier, "error", err)
133- return err
134- }
135-
136- // Fix: txn.Put doesn't return a value in Starskey
137- // The errors are handled at the transaction level
138- txn.Put(key, data)
139- log.Debug("Rate limit info saved", "ip", identifier)
140- return nil
141 }157 }
142 158
143- log.Debug("Request blocked (rate limited)", "ip", identifier, "attempts", info.Attempts)159+ txn.Put(key, data)
144- allowed = false
145 return nil160 return nil
146 })161 })
147 162
A pkg/logger/logger.go +101 -0
@@ -0,0 +1,101 @@
1+package logger
2+
3+import (
4+ "os"
5+ "strings"
6+ "sync"
7+
8+ "github.com/charmbracelet/log"
9+)
10+
11+// Logger is a wrapper around charmbracelet/log.Logger
12+type Logger struct {
13+ *log.Logger
14+}
15+
16+var (
17+ instance *Logger
18+ once sync.Once
19+)
20+
21+// GetLogger returns the singleton logger instance
22+func GetLogger() *Logger {
23+ once.Do(func() {
24+ instance = &Logger{
25+ Logger: log.NewWithOptions(os.Stderr, log.Options{
26+ Level: log.InfoLevel,
27+ ReportTimestamp: true,
28+ TimeFormat: "15:04:05",
29+ }),
30+ }
31+ })
32+ return instance
33+}
34+
35+// SetLogLevel sets the log level from a string
36+func (l *Logger) SetLogLevel(level string) {
37+ var logLevel log.Level
38+ switch strings.ToLower(level) {
39+ case "debug":
40+ logLevel = log.DebugLevel
41+ case "info":
42+ logLevel = log.InfoLevel
43+ case "warn", "warning":
44+ logLevel = log.WarnLevel
45+ case "error":
46+ logLevel = log.ErrorLevel
47+ case "fatal":
48+ logLevel = log.FatalLevel
49+ default:
50+ // Default to info level for unknown values
51+ logLevel = log.InfoLevel
52+ }
53+
54+ l.SetLevel(logLevel)
55+ log.SetLevel(logLevel) // Set the global logger level too
56+ l.Debug("Log level set", "level", level)
57+}
58+
59+// ConfigureFromEnv configures the logger from environment variables
60+func (l *Logger) ConfigureFromEnv() {
61+ // Check for GORDON_LOG_LEVEL environment variable
62+ if logLevelEnv := os.Getenv("GORDON_LOG_LEVEL"); logLevelEnv != "" {
63+ l.SetLogLevel(logLevelEnv)
64+ l.Debug("Log level set from environment variable", "level", logLevelEnv)
65+ } else if os.Getenv("ENV") == "dev" {
66+ // Fall back to ENV=dev behavior if GORDON_LOG_LEVEL is not set
67+ l.SetLevel(log.DebugLevel)
68+ log.SetLevel(log.DebugLevel) // Set the global logger level too
69+ l.Debug("Debug logging enabled from ENV=dev")
70+ }
71+}
72+
73+// Debug logs a debug message
74+func Debug(msg string, keyvals ...interface{}) {
75+ GetLogger().Debug(msg, keyvals...)
76+}
77+
78+// Info logs an info message
79+func Info(msg string, keyvals ...interface{}) {
80+ GetLogger().Info(msg, keyvals...)
81+}
82+
83+// Warn logs a warning message
84+func Warn(msg string, keyvals ...interface{}) {
85+ GetLogger().Warn(msg, keyvals...)
86+}
87+
88+// Error logs an error message
89+func Error(msg string, keyvals ...interface{}) {
90+ GetLogger().Error(msg, keyvals...)
91+}
92+
93+// Fatal logs a fatal message and exits
94+func Fatal(msg string, keyvals ...interface{}) {
95+ GetLogger().Fatal(msg, keyvals...)
96+}
97+
98+// Fatalf logs a fatal message with formatting and exits
99+func Fatalf(format string, args ...interface{}) {
100+ GetLogger().Fatalf(format, args...)
101+}
@@ -0,0 +1,101 @@
1+package logger
2+
3+import (
4+ "os"
5+ "strings"
6+ "sync"
7+
8+ "github.com/charmbracelet/log"
9+)
10+
11+// Logger is a wrapper around charmbracelet/log.Logger
12+type Logger struct {
13+ *log.Logger
14+}
15+
16+var (
17+ instance *Logger
18+ once sync.Once
19+)
20+
21+// GetLogger returns the singleton logger instance
22+func GetLogger() *Logger {
23+ once.Do(func() {
24+ instance = &Logger{
25+ Logger: log.NewWithOptions(os.Stderr, log.Options{
26+ Level: log.InfoLevel,
27+ ReportTimestamp: true,
28+ TimeFormat: "15:04:05",
29+ }),
30+ }
31+ })
32+ return instance
33+}
34+
35+// SetLogLevel sets the log level from a string
36+func (l *Logger) SetLogLevel(level string) {
37+ var logLevel log.Level
38+ switch strings.ToLower(level) {
39+ case "debug":
40+ logLevel = log.DebugLevel
41+ case "info":
42+ logLevel = log.InfoLevel
43+ case "warn", "warning":
44+ logLevel = log.WarnLevel
45+ case "error":
46+ logLevel = log.ErrorLevel
47+ case "fatal":
48+ logLevel = log.FatalLevel
49+ default:
50+ // Default to info level for unknown values
51+ logLevel = log.InfoLevel
52+ }
53+
54+ l.SetLevel(logLevel)
55+ log.SetLevel(logLevel) // Set the global logger level too
56+ l.Debug("Log level set", "level", level)
57+}
58+
59+// ConfigureFromEnv configures the logger from environment variables
60+func (l *Logger) ConfigureFromEnv() {
61+ // Check for GORDON_LOG_LEVEL environment variable
62+ if logLevelEnv := os.Getenv("GORDON_LOG_LEVEL"); logLevelEnv != "" {
63+ l.SetLogLevel(logLevelEnv)
64+ l.Debug("Log level set from environment variable", "level", logLevelEnv)
65+ } else if os.Getenv("ENV") == "dev" {
66+ // Fall back to ENV=dev behavior if GORDON_LOG_LEVEL is not set
67+ l.SetLevel(log.DebugLevel)
68+ log.SetLevel(log.DebugLevel) // Set the global logger level too
69+ l.Debug("Debug logging enabled from ENV=dev")
70+ }
71+}
72+
73+// Debug logs a debug message
74+func Debug(msg string, keyvals ...interface{}) {
75+ GetLogger().Debug(msg, keyvals...)
76+}
77+
78+// Info logs an info message
79+func Info(msg string, keyvals ...interface{}) {
80+ GetLogger().Info(msg, keyvals...)
81+}
82+
83+// Warn logs a warning message
84+func Warn(msg string, keyvals ...interface{}) {
85+ GetLogger().Warn(msg, keyvals...)
86+}
87+
88+// Error logs an error message
89+func Error(msg string, keyvals ...interface{}) {
90+ GetLogger().Error(msg, keyvals...)
91+}
92+
93+// Fatal logs a fatal message and exits
94+func Fatal(msg string, keyvals ...interface{}) {
95+ GetLogger().Fatal(msg, keyvals...)
96+}
97+
98+// Fatalf logs a fatal message with formatting and exits
99+func Fatalf(format string, args ...interface{}) {
100+ GetLogger().Fatalf(format, args...)
101+}
A pkg/scripts/rate_limit_test/main.go +229 -0
@@ -0,0 +1,229 @@
1+package main
2+
3+import (
4+ "flag"
5+ "fmt"
6+ "net/http"
7+ "os"
8+ "sync"
9+ "time"
10+)
11+
12+// RateLimitTestConfig holds configuration for the rate limit test
13+type RateLimitTestConfig struct {
14+ URL string
15+ ConcurrentReqs int
16+ MaxBatches int
17+ BatchDelay time.Duration
18+ WaitAfterJail time.Duration
19+ TestIP string
20+ PrintDetailedResp bool
21+}
22+
23+// Result represents the result of a single HTTP request
24+type Result struct {
25+ ID int
26+ StatusCode int
27+ RateLimitIP string
28+ ResponseTime time.Duration
29+}
30+
31+// runConcurrentRateLimitTest executes a rate limiting test with the given configuration
32+func runConcurrentRateLimitTest(config RateLimitTestConfig) {
33+ fmt.Printf("Starting concurrent rate limit test at %v\n", time.Now().Format(time.RFC1123))
34+ fmt.Printf("URL: %s\n", config.URL)
35+ fmt.Printf("Concurrent requests per batch: %d\n", config.ConcurrentReqs)
36+ fmt.Printf("Maximum batches: %d\n", config.MaxBatches)
37+ fmt.Printf("Simulated client IP: %s\n", config.TestIP)
38+ fmt.Println("========================================================")
39+
40+ hitLimit := false
41+ for batch := 1; batch <= config.MaxBatches && !hitLimit; batch++ {
42+ fmt.Printf("Sending batch %d with %d concurrent requests...\n", batch, config.ConcurrentReqs)
43+
44+ results := sendBatch(config, batch)
45+ hitLimit = processBatchResults(results, batch, config.PrintDetailedResp)
46+
47+ if hitLimit {
48+ fmt.Println("========================================================")
49+ fmt.Printf("Success! Rate limit triggered in batch %d\n", batch)
50+ break
51+ }
52+
53+ if batch < config.MaxBatches {
54+ fmt.Printf("Waiting %v seconds before next batch...\n", config.BatchDelay.Seconds())
55+ time.Sleep(config.BatchDelay)
56+ }
57+ }
58+
59+ if !hitLimit {
60+ fmt.Println("========================================================")
61+ fmt.Printf("Did not trigger rate limit after %d batches of %d requests\n", config.MaxBatches, config.ConcurrentReqs)
62+ fmt.Println("You may need to increase the number of concurrent requests or batches")
63+ return
64+ }
65+
66+ // Test if we're still rate limited after a short pause
67+ fmt.Println("Waiting 5 seconds and trying again to confirm we're still rate limited...")
68+ time.Sleep(5 * time.Second)
69+
70+ status, _ := sendSingleRequest(config.URL, config.TestIP)
71+ if status == 429 {
72+ fmt.Println("Still rate limited after 5 seconds as expected")
73+ } else {
74+ fmt.Printf("No longer rate limited after 5 seconds (HTTP %d)\n", status)
75+ }
76+
77+ // Wait for jail time to expire
78+ fmt.Println("========================================================")
79+ fmt.Printf("Waiting for jail time to expire (%v seconds)...\n", config.WaitAfterJail.Seconds())
80+ time.Sleep(config.WaitAfterJail)
81+
82+ // Try again after waiting
83+ status, _ = sendSingleRequest(config.URL, config.TestIP)
84+ if status != 429 {
85+ fmt.Printf("Successfully released from jail after waiting (HTTP %d)\n", status)
86+ } else {
87+ fmt.Printf("Still rate limited after waiting %v seconds\n", config.WaitAfterJail.Seconds())
88+ fmt.Println("You might need to wait longer or reset the rate limiter")
89+ }
90+
91+ fmt.Println("========================================================")
92+ fmt.Printf("Test completed at %v\n", time.Now().Format(time.RFC1123))
93+}
94+
95+// sendBatch sends a batch of concurrent requests
96+func sendBatch(config RateLimitTestConfig, batchNum int) []Result {
97+ var wg sync.WaitGroup
98+ results := make([]Result, config.ConcurrentReqs)
99+
100+ for i := 0; i < config.ConcurrentReqs; i++ {
101+ wg.Add(1)
102+ go func(id int) {
103+ defer wg.Done()
104+
105+ start := time.Now()
106+ statusCode, rateLimitIP := sendSingleRequest(config.URL, config.TestIP)
107+ responseTime := time.Since(start)
108+
109+ results[id-1] = Result{
110+ ID: id,
111+ StatusCode: statusCode,
112+ RateLimitIP: rateLimitIP,
113+ ResponseTime: responseTime,
114+ }
115+ }(i + 1)
116+ }
117+
118+ wg.Wait()
119+ return results
120+}
121+
122+// sendSingleRequest sends a single HTTP request and returns the status code and rate limit header
123+func sendSingleRequest(url, testIP string) (int, string) {
124+ client := &http.Client{
125+ Timeout: 10 * time.Second,
126+ }
127+
128+ req, err := http.NewRequest("GET", url, nil)
129+ if err != nil {
130+ console.Debug("Error creating request:", err)
131+ return 0, ""
132+ }
133+
134+ // Add headers to simulate client IP
135+ if testIP != "" {
136+ req.Header.Set("X-Forwarded-For", testIP)
137+ req.Header.Set("X-Real-IP", testIP)
138+ }
139+
140+ resp, err := client.Do(req)
141+ if err != nil {
142+ console.Debug("Error sending request:", err)
143+ return 0, ""
144+ }
145+ defer resp.Body.Close()
146+
147+ // Get the rate limit header
148+ rateLimitIP := resp.Header.Get("X-Rate-Limit-IP")
149+
150+ return resp.StatusCode, rateLimitIP
151+}
152+
153+// processBatchResults processes and displays the results from a batch of requests
154+func processBatchResults(results []Result, batchNum int, printDetailedResp bool) bool {
155+ statusCounts := make(map[int]int)
156+ hitLimit := false
157+
158+ fmt.Printf("Results for batch %d:\n", batchNum)
159+
160+ // Display detailed results if requested
161+ if printDetailedResp {
162+ for _, result := range results {
163+ fmt.Printf(" Request %d: Status %d, Rate-Limited IP: %s (took %v)\n",
164+ result.ID, result.StatusCode, result.RateLimitIP, result.ResponseTime)
165+ }
166+ }
167+
168+ // Count the different status codes
169+ for _, result := range results {
170+ statusCounts[result.StatusCode]++
171+ if result.StatusCode == 429 {
172+ hitLimit = true
173+ }
174+ }
175+
176+ // Display summary
177+ fmt.Println(" Summary:")
178+ for status, count := range statusCounts {
179+ fmt.Printf(" Status %d: %d requests\n", status, count)
180+ }
181+
182+ return hitLimit
183+}
184+
185+// Simplified console.Debug for logging debug messages
186+var console = struct {
187+ Debug func(v ...interface{})
188+}{
189+ Debug: func(v ...interface{}) {
190+ fmt.Fprintln(os.Stderr, append([]interface{}{"DEBUG:"}, v...)...)
191+ },
192+}
193+
194+// Main entry point for the script
195+func main() {
196+ // Define command-line flags
197+ urlFlag := flag.String("url", "", "URL to test")
198+ concurrentFlag := flag.Int("concurrent", 15, "Number of concurrent requests per batch")
199+ batchesFlag := flag.Int("batches", 3, "Maximum number of batches to send")
200+ batchDelayFlag := flag.Float64("batch-delay", 0.5, "Delay between batches in seconds")
201+ waitJailFlag := flag.Float64("wait-jail", 15, "Time to wait for jail expiry in seconds")
202+ ipFlag := flag.String("ip", "127.0.0.1", "Simulated client IP")
203+ verboseFlag := flag.Bool("verbose", false, "Print detailed response information")
204+
205+ flag.Parse()
206+
207+ // Check for required URL
208+ if *urlFlag == "" {
209+ // Try to get URL from environment variable
210+ *urlFlag = os.Getenv("TEST_BACKEND_URL")
211+ if *urlFlag == "" {
212+ fmt.Println("Error: URL is required. Provide with -url flag or TEST_BACKEND_URL environment variable")
213+ flag.Usage()
214+ os.Exit(1)
215+ }
216+ }
217+
218+ config := RateLimitTestConfig{
219+ URL: *urlFlag,
220+ ConcurrentReqs: *concurrentFlag,
221+ MaxBatches: *batchesFlag,
222+ BatchDelay: time.Duration(*batchDelayFlag * float64(time.Second)),
223+ WaitAfterJail: time.Duration(*waitJailFlag * float64(time.Second)),
224+ TestIP: *ipFlag,
225+ PrintDetailedResp: *verboseFlag,
226+ }
227+
228+ runConcurrentRateLimitTest(config)
229+}
@@ -0,0 +1,229 @@
1+package main
2+
3+import (
4+ "flag"
5+ "fmt"
6+ "net/http"
7+ "os"
8+ "sync"
9+ "time"
10+)
11+
12+// RateLimitTestConfig holds configuration for the rate limit test
13+type RateLimitTestConfig struct {
14+ URL string
15+ ConcurrentReqs int
16+ MaxBatches int
17+ BatchDelay time.Duration
18+ WaitAfterJail time.Duration
19+ TestIP string
20+ PrintDetailedResp bool
21+}
22+
23+// Result represents the result of a single HTTP request
24+type Result struct {
25+ ID int
26+ StatusCode int
27+ RateLimitIP string
28+ ResponseTime time.Duration
29+}
30+
31+// runConcurrentRateLimitTest executes a rate limiting test with the given configuration
32+func runConcurrentRateLimitTest(config RateLimitTestConfig) {
33+ fmt.Printf("Starting concurrent rate limit test at %v\n", time.Now().Format(time.RFC1123))
34+ fmt.Printf("URL: %s\n", config.URL)
35+ fmt.Printf("Concurrent requests per batch: %d\n", config.ConcurrentReqs)
36+ fmt.Printf("Maximum batches: %d\n", config.MaxBatches)
37+ fmt.Printf("Simulated client IP: %s\n", config.TestIP)
38+ fmt.Println("========================================================")
39+
40+ hitLimit := false
41+ for batch := 1; batch <= config.MaxBatches && !hitLimit; batch++ {
42+ fmt.Printf("Sending batch %d with %d concurrent requests...\n", batch, config.ConcurrentReqs)
43+
44+ results := sendBatch(config, batch)
45+ hitLimit = processBatchResults(results, batch, config.PrintDetailedResp)
46+
47+ if hitLimit {
48+ fmt.Println("========================================================")
49+ fmt.Printf("Success! Rate limit triggered in batch %d\n", batch)
50+ break
51+ }
52+
53+ if batch < config.MaxBatches {
54+ fmt.Printf("Waiting %v seconds before next batch...\n", config.BatchDelay.Seconds())
55+ time.Sleep(config.BatchDelay)
56+ }
57+ }
58+
59+ if !hitLimit {
60+ fmt.Println("========================================================")
61+ fmt.Printf("Did not trigger rate limit after %d batches of %d requests\n", config.MaxBatches, config.ConcurrentReqs)
62+ fmt.Println("You may need to increase the number of concurrent requests or batches")
63+ return
64+ }
65+
66+ // Test if we're still rate limited after a short pause
67+ fmt.Println("Waiting 5 seconds and trying again to confirm we're still rate limited...")
68+ time.Sleep(5 * time.Second)
69+
70+ status, _ := sendSingleRequest(config.URL, config.TestIP)
71+ if status == 429 {
72+ fmt.Println("Still rate limited after 5 seconds as expected")
73+ } else {
74+ fmt.Printf("No longer rate limited after 5 seconds (HTTP %d)\n", status)
75+ }
76+
77+ // Wait for jail time to expire
78+ fmt.Println("========================================================")
79+ fmt.Printf("Waiting for jail time to expire (%v seconds)...\n", config.WaitAfterJail.Seconds())
80+ time.Sleep(config.WaitAfterJail)
81+
82+ // Try again after waiting
83+ status, _ = sendSingleRequest(config.URL, config.TestIP)
84+ if status != 429 {
85+ fmt.Printf("Successfully released from jail after waiting (HTTP %d)\n", status)
86+ } else {
87+ fmt.Printf("Still rate limited after waiting %v seconds\n", config.WaitAfterJail.Seconds())
88+ fmt.Println("You might need to wait longer or reset the rate limiter")
89+ }
90+
91+ fmt.Println("========================================================")
92+ fmt.Printf("Test completed at %v\n", time.Now().Format(time.RFC1123))
93+}
94+
95+// sendBatch sends a batch of concurrent requests
96+func sendBatch(config RateLimitTestConfig, batchNum int) []Result {
97+ var wg sync.WaitGroup
98+ results := make([]Result, config.ConcurrentReqs)
99+
100+ for i := 0; i < config.ConcurrentReqs; i++ {
101+ wg.Add(1)
102+ go func(id int) {
103+ defer wg.Done()
104+
105+ start := time.Now()
106+ statusCode, rateLimitIP := sendSingleRequest(config.URL, config.TestIP)
107+ responseTime := time.Since(start)
108+
109+ results[id-1] = Result{
110+ ID: id,
111+ StatusCode: statusCode,
112+ RateLimitIP: rateLimitIP,
113+ ResponseTime: responseTime,
114+ }
115+ }(i + 1)
116+ }
117+
118+ wg.Wait()
119+ return results
120+}
121+
122+// sendSingleRequest sends a single HTTP request and returns the status code and rate limit header
123+func sendSingleRequest(url, testIP string) (int, string) {
124+ client := &http.Client{
125+ Timeout: 10 * time.Second,
126+ }
127+
128+ req, err := http.NewRequest("GET", url, nil)
129+ if err != nil {
130+ console.Debug("Error creating request:", err)
131+ return 0, ""
132+ }
133+
134+ // Add headers to simulate client IP
135+ if testIP != "" {
136+ req.Header.Set("X-Forwarded-For", testIP)
137+ req.Header.Set("X-Real-IP", testIP)
138+ }
139+
140+ resp, err := client.Do(req)
141+ if err != nil {
142+ console.Debug("Error sending request:", err)
143+ return 0, ""
144+ }
145+ defer resp.Body.Close()
146+
147+ // Get the rate limit header
148+ rateLimitIP := resp.Header.Get("X-Rate-Limit-IP")
149+
150+ return resp.StatusCode, rateLimitIP
151+}
152+
153+// processBatchResults processes and displays the results from a batch of requests
154+func processBatchResults(results []Result, batchNum int, printDetailedResp bool) bool {
155+ statusCounts := make(map[int]int)
156+ hitLimit := false
157+
158+ fmt.Printf("Results for batch %d:\n", batchNum)
159+
160+ // Display detailed results if requested
161+ if printDetailedResp {
162+ for _, result := range results {
163+ fmt.Printf(" Request %d: Status %d, Rate-Limited IP: %s (took %v)\n",
164+ result.ID, result.StatusCode, result.RateLimitIP, result.ResponseTime)
165+ }
166+ }
167+
168+ // Count the different status codes
169+ for _, result := range results {
170+ statusCounts[result.StatusCode]++
171+ if result.StatusCode == 429 {
172+ hitLimit = true
173+ }
174+ }
175+
176+ // Display summary
177+ fmt.Println(" Summary:")
178+ for status, count := range statusCounts {
179+ fmt.Printf(" Status %d: %d requests\n", status, count)
180+ }
181+
182+ return hitLimit
183+}
184+
185+// Simplified console.Debug for logging debug messages
186+var console = struct {
187+ Debug func(v ...interface{})
188+}{
189+ Debug: func(v ...interface{}) {
190+ fmt.Fprintln(os.Stderr, append([]interface{}{"DEBUG:"}, v...)...)
191+ },
192+}
193+
194+// Main entry point for the script
195+func main() {
196+ // Define command-line flags
197+ urlFlag := flag.String("url", "", "URL to test")
198+ concurrentFlag := flag.Int("concurrent", 15, "Number of concurrent requests per batch")
199+ batchesFlag := flag.Int("batches", 3, "Maximum number of batches to send")
200+ batchDelayFlag := flag.Float64("batch-delay", 0.5, "Delay between batches in seconds")
201+ waitJailFlag := flag.Float64("wait-jail", 15, "Time to wait for jail expiry in seconds")
202+ ipFlag := flag.String("ip", "127.0.0.1", "Simulated client IP")
203+ verboseFlag := flag.Bool("verbose", false, "Print detailed response information")
204+
205+ flag.Parse()
206+
207+ // Check for required URL
208+ if *urlFlag == "" {
209+ // Try to get URL from environment variable
210+ *urlFlag = os.Getenv("TEST_BACKEND_URL")
211+ if *urlFlag == "" {
212+ fmt.Println("Error: URL is required. Provide with -url flag or TEST_BACKEND_URL environment variable")
213+ flag.Usage()
214+ os.Exit(1)
215+ }
216+ }
217+
218+ config := RateLimitTestConfig{
219+ URL: *urlFlag,
220+ ConcurrentReqs: *concurrentFlag,
221+ MaxBatches: *batchesFlag,
222+ BatchDelay: time.Duration(*batchDelayFlag * float64(time.Second)),
223+ WaitAfterJail: time.Duration(*waitJailFlag * float64(time.Second)),
224+ TestIP: *ipFlag,
225+ PrintDetailedResp: *verboseFlag,
226+ }
227+
228+ runConcurrentRateLimitTest(config)
229+}

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 8

Tags 106

↑↓ moveenter openesc close