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 ( | |||
| 16 | 16 | "github.com/bnema/gordon/internal/cli" | |
| 17 | 17 | "github.com/bnema/gordon/internal/cli/handler" | |
| 18 | 18 | "github.com/bnema/gordon/internal/common" | |
| 19 | - | "github.com/charmbracelet/log" | |
| 19 | + | "github.com/bnema/gordon/pkg/logger" | |
| 20 | 20 | "github.com/spf13/cobra" | |
| 21 | 21 | ) | |
| 22 | 22 | ||
| 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 | |
| 27 | 24 | ||
| 28 | 25 | func NewDeployCommand(a *cli.App) *cobra.Command { | |
| 29 | 26 | var port string | |
| @@ -35,29 +32,29 @@ func NewDeployCommand(a *cli.App) *cobra.Command { | |||
| 35 | 32 | Args: cobra.ExactArgs(1), | |
| 36 | 33 | PreRun: func(cmd *cobra.Command, args []string) { | |
| 37 | 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 | 36 | os.Exit(1) | |
| 40 | 37 | } | |
| 41 | 38 | }, | |
| 42 | 39 | Run: func(cmd *cobra.Command, args []string) { | |
| 43 | 40 | imageName := args[0] | |
| 44 | - | log.Info("Pushing image", "image", imageName) | |
| 41 | + | logger.Info("Pushing image", "image", imageName) | |
| 45 | 42 | ||
| 46 | 43 | if err := validateDeployInputs(imageName, port, targetDomain); err != nil { | |
| 47 | - | log.Error("Validation failed", "error", err) | |
| 44 | + | logger.Error("Validation failed", "error", err) | |
| 48 | 45 | return | |
| 49 | 46 | } | |
| 50 | 47 | ||
| 51 | 48 | reader, actualSize, err := handler.ExportDockerImage(imageName) | |
| 52 | 49 | if err != nil { | |
| 53 | - | log.Error("Error exporting image", "error", err) | |
| 50 | + | logger.Error("Error exporting image", "error", err) | |
| 54 | 51 | return | |
| 55 | 52 | } | |
| 56 | 53 | defer reader.Close() | |
| 57 | 54 | ||
| 58 | 55 | 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 | 59 | if err := deployImage(a, reader, imageName, port, targetDomain); err != nil { | |
| 63 | 60 | os.Exit(1) | |
| @@ -74,11 +71,11 @@ func NewDeployCommand(a *cli.App) *cobra.Command { | |||
| 74 | 71 | func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain string) error { | |
| 75 | 72 | // Check authentication early | |
| 76 | 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 | 75 | return err | |
| 79 | 76 | } | |
| 80 | 77 | ||
| 81 | - | log.Info("Deploying image", "image", imageName) | |
| 78 | + | logger.Info("Deploying image", "image", imageName) | |
| 82 | 79 | // Check for conflicts first | |
| 83 | 80 | resp, err := checkDeployConflict(a, targetDomain, port) | |
| 84 | 81 | if err != nil { | |
| @@ -94,7 +91,7 @@ func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain str | |||
| 94 | 91 | ||
| 95 | 92 | // If there's a conflict, handle it before proceeding | |
| 96 | 93 | if !resp.Success && resp.ContainerID != "" { | |
| 97 | - | log.Warn("Container already exists", | |
| 94 | + | logger.Warn("Container already exists", | |
| 98 | 95 | "name", resp.ContainerName, | |
| 99 | 96 | "id", shortID, | |
| 100 | 97 | "state", resp.State, | |
| @@ -117,7 +114,7 @@ func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain str | |||
| 117 | 114 | ||
| 118 | 115 | imageReader := bytes.NewReader(buf.Bytes()) | |
| 119 | 116 | sizeInMB := float64(size) / 1024 / 1024 | |
| 120 | - | log.Info("Attempting to deploy...", | |
| 117 | + | logger.Info("Attempting to deploy...", | |
| 121 | 118 | "image", imageName, | |
| 122 | 119 | "size", fmt.Sprintf("%.2fMB", sizeInMB), | |
| 123 | 120 | "port", port, | |
| @@ -162,7 +159,7 @@ func waitForDeployment(domain string, containerID string) error { | |||
| 162 | 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 | 163 | "domain", domain, | |
| 167 | 164 | "container_id", shortContainerID) | |
| 168 | 165 | ||
| @@ -171,7 +168,7 @@ func waitForDeployment(domain string, containerID string) error { | |||
| 171 | 168 | if err == nil { | |
| 172 | 169 | defer resp.Body.Close() | |
| 173 | 170 | if resp.StatusCode == http.StatusOK { | |
| 174 | - | log.Info("Deployment successful", | |
| 171 | + | logger.Info("Deployment successful", | |
| 175 | 172 | "domain", domain, | |
| 176 | 173 | "container_id", shortContainerID) | |
| 177 | 174 | return nil | |
| @@ -181,7 +178,7 @@ func waitForDeployment(domain string, containerID string) error { | |||
| 181 | 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 | 182 | "attempt", fmt.Sprintf("%d/%d", i+1, maxRetries)) | |
| 186 | 183 | time.Sleep(retryInterval) | |
| 187 | 184 | } | |
| @@ -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 string | 26 | 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 | return | 45 | 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 | return | 51 | return | ||
| 55 | } | 52 | } | ||
| 56 | defer reader.Close() | 53 | defer reader.Close() | ||
| 57 | 54 | ||||
| 58 | sizeInMB := float64(actualSize) / 1024 / 1024 | 55 | 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 early | 72 | // 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 err | 75 | return err | ||
| 79 | } | 76 | } | ||
| 80 | 77 | ||||
| 81 | - | log.Info("Deploying image", "image", imageName) | 78 | + | logger.Info("Deploying image", "image", imageName) |
| 82 | // Check for conflicts first | 79 | // 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 proceeding | 92 | // 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 / 1024 | 116 | 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 nil | 174 | 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 @@ | |||
| 1 | 1 | package cmd | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "fmt" | |
| 5 | - | ||
| 6 | 4 | "github.com/bnema/gordon/internal/cli" | |
| 7 | 5 | "github.com/bnema/gordon/internal/common" | |
| 6 | + | "github.com/bnema/gordon/pkg/logger" | |
| 8 | 7 | "github.com/spf13/cobra" | |
| 9 | 8 | ) | |
| 10 | 9 | ||
| @@ -19,20 +18,20 @@ func NewVersionCommand(app *cli.App) *cobra.Command { | |||
| 19 | 18 | app.Config.Build.BuildDate, | |
| 20 | 19 | app.Config.Build.ProxyURL, | |
| 21 | 20 | ) | |
| 22 | - | fmt.Println(info.String()) | |
| 21 | + | logger.Info(info.String()) | |
| 23 | 22 | ||
| 24 | 23 | hasUpdate, latestVersion, err := common.CheckForNewVersion( | |
| 25 | 24 | info.Version, | |
| 26 | 25 | info.ProxyURL, | |
| 27 | 26 | ) | |
| 28 | 27 | if err != nil { | |
| 29 | - | fmt.Printf("Error checking for updates: %v\n", err) | |
| 28 | + | logger.Error("Error checking for updates", "error", err) | |
| 30 | 29 | return | |
| 31 | 30 | } | |
| 32 | 31 | ||
| 33 | 32 | 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") | |
| 36 | 35 | } | |
| 37 | 36 | }, | |
| 38 | 37 | } | |
| @@ -1,10 +1,9 @@ | |||||
| 1 | package cmd | 1 | 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 | return | 29 | 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 ( | |||
| 9 | 9 | ||
| 10 | 10 | "github.com/bnema/gordon/internal/httpserve" | |
| 11 | 11 | "github.com/bnema/gordon/internal/server" | |
| 12 | - | "github.com/charmbracelet/log" | |
| 12 | + | "github.com/bnema/gordon/pkg/logger" | |
| 13 | 13 | "github.com/labstack/echo/v4" | |
| 14 | 14 | ) | |
| 15 | 15 | ||
| @@ -18,7 +18,7 @@ import ( | |||
| 18 | 18 | func StartServer(a *server.App, port string) error { | |
| 19 | 19 | _, err := server.InitializeDB(a) | |
| 20 | 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 | 24 | // Start the session cleaner cron job | |
| @@ -26,13 +26,13 @@ func StartServer(a *server.App, port string) error { | |||
| 26 | 26 | ||
| 27 | 27 | _, err = server.HandleNewTokenInitialization(a) | |
| 28 | 28 | if err != nil { | |
| 29 | - | log.Print(err) | |
| 29 | + | logger.Error("Token initialization error", "error", err) | |
| 30 | 30 | } | |
| 31 | 31 | ||
| 32 | 32 | // Initialize and start the reverse proxy using the already loaded configuration | |
| 33 | 33 | p, err := httpserve.InitializeProxy(a) | |
| 34 | 34 | if err != nil { | |
| 35 | - | log.Error("Failed to initialize reverse proxy:", err) | |
| 35 | + | logger.Error("Failed to initialize reverse proxy", "error", err) | |
| 36 | 36 | // 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 | 43 | go func() { | |
| 44 | 44 | sig := <-sigs | |
| 45 | - | log.Info("Received signal:", sig) | |
| 45 | + | logger.Info("Received signal", "signal", sig) | |
| 46 | 46 | ||
| 47 | 47 | // Stop the proxy if it was started | |
| 48 | 48 | if p != nil { | |
| 49 | 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 | 61 | e.HidePort = true | |
| 62 | 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 | 66 | // Test admin connections after the server has started (in a separate goroutine) | |
| 67 | 67 | if p != nil { | |
| @@ -73,7 +73,7 @@ func StartServer(a *server.App, port string) error { | |||
| 73 | 73 | } | |
| 74 | 74 | ||
| 75 | 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 | 79 | 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 job | 24 | // 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 configuration | 32 | // 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 work | 36 | // 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 := <-sigs | 44 | sig := <-sigs | ||
| 45 | - | log.Info("Received signal:", sig) | 45 | + | logger.Info("Received signal", "signal", sig) |
| 46 | 46 | ||||
| 47 | // Stop the proxy if it was started | 47 | // 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 = true | 61 | 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 nil | 79 | return nil | ||
M internal/common/init_config.go +10 -58
| @@ -12,12 +12,11 @@ import ( | |||
| 12 | 12 | "sync" | |
| 13 | 13 | ||
| 14 | 14 | "github.com/bnema/gordon/pkg/docker" | |
| 15 | + | "github.com/bnema/gordon/pkg/logger" | |
| 15 | 16 | "github.com/bnema/gordon/pkg/parser" | |
| 16 | - | "github.com/charmbracelet/log" | |
| 17 | 17 | ) | |
| 18 | 18 | ||
| 19 | 19 | var ( | |
| 20 | - | logger = log.NewWithOptions(os.Stderr, log.Options{Level: log.InfoLevel}) | |
| 21 | 20 | globalConfig *Config | |
| 22 | 21 | globalConfigOnce sync.Once | |
| 23 | 22 | globalConfigMu sync.RWMutex | |
| @@ -26,39 +25,8 @@ var ( | |||
| 26 | 25 | ||
| 27 | 26 | // Initialize package-level logging configuration | |
| 28 | 27 | 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() | |
| 62 | 30 | } | |
| 63 | 31 | ||
| 64 | 32 | type Config struct { | |
| @@ -83,10 +51,12 @@ type AdminConfig struct { | |||
| 83 | 51 | } | |
| 84 | 52 | ||
| 85 | 53 | 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"` | |
| 89 | 58 | } | |
| 59 | + | ||
| 90 | 60 | type HttpConfig struct { | |
| 91 | 61 | Port string `yaml:"port"` | |
| 92 | 62 | Domain string `yaml:"domain"` | |
| @@ -821,26 +791,8 @@ func (config *Config) LoadConfig() (*Config, error) { | |||
| 821 | 791 | ||
| 822 | 792 | // Apply the log level from configuration | |
| 823 | 793 | 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) | |
| 844 | 796 | logger.Debug("Log level set from configuration", "level", config.General.LogLevel) | |
| 845 | 797 | } | |
| 846 | 798 | ||
| @@ -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 *Config | 20 | globalConfig *Config | ||
| 22 | globalConfigOnce sync.Once | 21 | globalConfigOnce sync.Once | ||
| 23 | globalConfigMu sync.RWMutex | 22 | globalConfigMu sync.RWMutex | ||
| @@ -26,39 +25,8 @@ var ( | |||||
| 26 | 25 | ||||
| 27 | // Initialize package-level logging configuration | 26 | // Initialize package-level logging configuration | ||
| 28 | func init() { | 27 | func init() { | ||
| 29 | - | // Enable timestamp reporting and set format | 28 | + | // 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 configuration | 792 | // Apply the log level from configuration | ||
| 823 | if config.General.LogLevel != "" { | 793 | if config.General.LogLevel != "" { | ||
| 824 | - | // Convert string log level to charmbracelet/log level | 794 | + | // Set the log level using our centralized logger |
| 825 | - | var logLevel log.Level | 795 | + | 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 ( | |||
| 6 | 6 | "os" | |
| 7 | 7 | "strings" | |
| 8 | 8 | ||
| 9 | - | "github.com/charmbracelet/log" | |
| 9 | + | "github.com/bnema/gordon/pkg/logger" | |
| 10 | 10 | ) | |
| 11 | 11 | ||
| 12 | 12 | // ReadUserInput reads a string input from the user with a prompt. | |
| 13 | 13 | // It handles multi-word inputs and properly checks for errors. | |
| 14 | 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 | 16 | // Make the prompt more visible with newlines and formatting | |
| 17 | 17 | fmt.Printf("\n>>> %s: ", prompt) | |
| 18 | 18 | // Flush to ensure the prompt is displayed | |
| @@ -20,18 +20,18 @@ func ReadUserInput(prompt string) string { | |||
| 20 | 20 | ||
| 21 | 21 | // Use bufio.NewReader for better input handling | |
| 22 | 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 | 25 | // ReadString reads until newline, handling spaces in input | |
| 26 | 26 | input, err := reader.ReadString('\n') | |
| 27 | 27 | if err != nil { | |
| 28 | - | log.Error("ReadUserInput: Error reading input", "error", err) | |
| 28 | + | logger.Error("ReadUserInput: Error reading input", "error", err) | |
| 29 | 29 | return "" | |
| 30 | 30 | } | |
| 31 | 31 | ||
| 32 | 32 | // Trim spaces and newline characters | |
| 33 | 33 | result := strings.TrimSpace(input) | |
| 34 | - | log.Debug("ReadUserInput: Received input", "value", result) | |
| 34 | + | logger.Debug("ReadUserInput: Received input", "value", result) | |
| 35 | 35 | return result | |
| 36 | 36 | } | |
| 37 | 37 | ||
| @@ -49,14 +49,13 @@ func ReadUserInputWithValidation(prompt string, validate func(string) bool) stri | |||
| 49 | 49 | ||
| 50 | 50 | // ReadUserInputWithDefault reads user input with a default value if input is empty. | |
| 51 | 51 | func ReadUserInputWithDefault(prompt string, defaultValue string) string { | |
| 52 | - | fmt.Printf("%s (default: %s) ", prompt, defaultValue) | |
| 53 | 52 | // Flush to ensure the prompt is displayed | |
| 54 | 53 | os.Stdout.Sync() | |
| 55 | 54 | ||
| 56 | 55 | reader := bufio.NewReader(os.Stdin) | |
| 57 | 56 | input, err := reader.ReadString('\n') | |
| 58 | 57 | if err != nil { | |
| 59 | - | log.Error("Error reading input", "error", err) | |
| 58 | + | logger.Error("Error reading input", "error", err) | |
| 60 | 59 | return defaultValue | |
| 61 | 60 | } | |
| 62 | 61 | ||
| @@ -87,23 +86,23 @@ func ReadUserInputNonBlocking(prompt string, defaultValue string) string { | |||
| 87 | 86 | // Check if we're in an interactive terminal | |
| 88 | 87 | stat, err := os.Stdin.Stat() | |
| 89 | 88 | 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) | |
| 92 | 91 | return defaultValue | |
| 93 | 92 | } | |
| 94 | 93 | ||
| 95 | 94 | isTerminal := (stat.Mode() & os.ModeCharDevice) != 0 | |
| 96 | - | log.Debug("ReadUserInputNonBlocking: Terminal status check", | |
| 95 | + | logger.Debug("ReadUserInputNonBlocking: Terminal status check", | |
| 97 | 96 | "isInteractive", isTerminal, | |
| 98 | 97 | "fileMode", stat.Mode().String()) | |
| 99 | 98 | ||
| 100 | 99 | if isTerminal { | |
| 101 | 100 | // In interactive terminal, use normal prompt | |
| 102 | - | log.Debug("ReadUserInputNonBlocking: Using interactive prompt") | |
| 101 | + | logger.Debug("ReadUserInputNonBlocking: Using interactive prompt") | |
| 103 | 102 | return ReadUserInput(prompt) | |
| 104 | 103 | } else { | |
| 105 | 104 | // 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 | 106 | "prompt", prompt, | |
| 108 | 107 | "default", defaultValue) | |
| 109 | 108 | 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 formatting | 16 | // 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 displayed | 18 | // 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 handling | 21 | // 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 input | 25 | // 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 characters | 32 | // 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 result | 35 | 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 displayed | 52 | // 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 defaultValue | 59 | 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 terminal | 86 | // 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 defaultValue | 91 | return defaultValue | ||
| 93 | } | 92 | } | ||
| 94 | 93 | ||||
| 95 | isTerminal := (stat.Mode() & os.ModeCharDevice) != 0 | 94 | 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 prompt | 100 | // 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 default | 104 | // 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 defaultValue | 108 | return defaultValue | ||
M internal/common/version.go +3 -3
| @@ -12,7 +12,7 @@ import ( | |||
| 12 | 12 | "time" | |
| 13 | 13 | ||
| 14 | 14 | "github.com/Masterminds/semver/v3" | |
| 15 | - | "github.com/charmbracelet/log" | |
| 15 | + | "github.com/bnema/gordon/pkg/logger" | |
| 16 | 16 | ) | |
| 17 | 17 | ||
| 18 | 18 | // Info represents version information | |
| @@ -146,12 +146,12 @@ func CheckVersionPeriodically(info Info, checkInterval time.Duration) { | |||
| 146 | 146 | func checkVersion(info Info) { | |
| 147 | 147 | hasUpdate, latestVersion, err := CheckForNewVersion(info.Version, info.ProxyURL) | |
| 148 | 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 | 150 | return | |
| 151 | 151 | } | |
| 152 | 152 | ||
| 153 | 153 | if hasUpdate { | |
| 154 | - | log.Info("New version available!", | |
| 154 | + | logger.Info("New version available!", | |
| 155 | 155 | "current", info.Version, | |
| 156 | 156 | "latest", latestVersion, | |
| 157 | 157 | "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 information | 18 | // 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 | return | 150 | 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 ( | |||
| 13 | 13 | func createDBSession(a *server.App, browserInfo string, accessToken string, accountID string) error { | |
| 14 | 14 | expireTime := time.Now().Add(time.Hour * 24).Format(time.RFC3339) | |
| 15 | 15 | sessions := &db.Sessions{ | |
| 16 | - | ID: generateUUID(), | |
| 17 | - | AccessToken: accessToken, | |
| 16 | + | ID: generateUUID(), | |
| 18 | 17 | } | |
| 19 | 18 | ||
| 20 | 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 | 44 | 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 | 48 | expireTime := time.Now().Add(time.Hour * 24).Format(time.RFC3339) | |
| 50 | 49 | query := "UPDATE sessions SET access_token = ?, expires = ? WHERE id = ?" | |
| 51 | 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 | 65 | // If the session exists, update the session | |
| 67 | 66 | if session != nil { | |
| 68 | - | err := updateDBSession(a, accessToken, browserInfo) | |
| 67 | + | err := updateDBSession(a, accessToken) | |
| 69 | 68 | if err != nil { | |
| 70 | 69 | return fmt.Errorf("error updating session: %w", err) | |
| 71 | 70 | } | |
| @@ -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, nil | 44 | 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 session | 65 | // 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 ( | |||
| 13 | 13 | "github.com/bnema/gordon/internal/db/queries" | |
| 14 | 14 | "github.com/bnema/gordon/internal/server" | |
| 15 | 15 | "github.com/bnema/gordon/internal/templating/render" | |
| 16 | - | "github.com/charmbracelet/log" | |
| 16 | + | "github.com/bnema/gordon/pkg/logger" | |
| 17 | 17 | "github.com/gorilla/sessions" | |
| 18 | 18 | _ "github.com/joho/godotenv/autoload" | |
| 19 | 19 | "github.com/labstack/echo-contrib/session" | |
| @@ -51,12 +51,12 @@ func HandleTokenSubmission(c echo.Context, a *server.App) error { | |||
| 51 | 51 | // Validate the token | |
| 52 | 52 | configToken, err := a.Config.GetToken() | |
| 53 | 53 | if err != nil { | |
| 54 | - | log.Debug("Error getting token from config:", err) | |
| 54 | + | logger.Debug("Error getting token from config", "error", err) | |
| 55 | 55 | return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login?error=server_error") | |
| 56 | 56 | } | |
| 57 | 57 | ||
| 58 | 58 | if urlToken != configToken { | |
| 59 | - | log.Debug("Invalid token provided:", urlToken) | |
| 59 | + | logger.Debug("Invalid token provided", "token", urlToken) | |
| 60 | 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 | 95 | // StartOAuthGithub starts the Github OAuth flow. | |
| 96 | 96 | ||
| 97 | 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 | 99 | sess, err := getSession(c) | |
| 100 | 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 | 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 | 106 | sessionID, ok1 := sess.Values["sessionID"].(string) | |
| 107 | 107 | accountID, ok2 := sess.Values["accountID"].(string) | |
| 108 | 108 | authenticated, ok3 := sess.Values["authenticated"].(bool) | |
| 109 | 109 | ||
| 110 | 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 | 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 | 116 | sessionExists, err := queries.CheckDBSessionExists(a, sessionID) | |
| 117 | 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 | 119 | return fmt.Errorf("could not check if session exists: %w", err) | |
| 120 | 120 | } | |
| 121 | 121 | ||
| 122 | 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 | 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 | 128 | currentTime := time.Now() | |
| 129 | 129 | sessionExpiration, err := queries.GetSessionExpiration(a, accountID, sessionID, currentTime) | |
| 130 | 130 | if err != nil { | |
| 131 | - | log.Info("Error getting session expiration", "error", err) | |
| 131 | + | logger.Info("Error getting session expiration", "error", err) | |
| 132 | 132 | return fmt.Errorf("could not get session expiration: %w", err) | |
| 133 | 133 | } | |
| 134 | 134 | ||
| 135 | 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 | 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 | 141 | newExpirationTime := currentTime.Add(30 * time.Minute) | |
| 142 | 142 | err = queries.ExtendSessionExpiration(a, accountID, sessionID, newExpirationTime) | |
| 143 | 143 | if err != nil { | |
| 144 | - | log.Info("Error extending session expiration", "error", err) | |
| 144 | + | logger.Info("Error extending session expiration", "error", err) | |
| 145 | 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 | 149 | return c.Redirect(http.StatusFound, a.Config.Admin.Path) | |
| 150 | 150 | } | |
| 151 | 151 | ||
| @@ -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 token | 51 | // 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 @@ | |||
| 1 | 1 | package middleware | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "github.com/charmbracelet/log" | |
| 4 | + | "github.com/bnema/gordon/pkg/logger" | |
| 5 | 5 | "github.com/labstack/echo/v4" | |
| 6 | 6 | ) | |
| 7 | 7 | ||
| @@ -9,7 +9,7 @@ func ErrorHandler(next echo.HandlerFunc) echo.HandlerFunc { | |||
| 9 | 9 | return func(c echo.Context) error { | |
| 10 | 10 | err := next(c) | |
| 11 | 11 | if err != nil { | |
| 12 | - | log.Error("Caught http error", | |
| 12 | + | logger.Error("Caught http error", | |
| 13 | 13 | "path", c.Request().URL.Path, | |
| 14 | 14 | "method", c.Request().Method, | |
| 15 | 15 | "error", err, | |
| @@ -1,7 +1,7 @@ | |||||
| 1 | package middleware | 1 | 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 ( | |||
| 12 | 12 | "github.com/bnema/gordon/internal/cli/auth" | |
| 13 | 13 | "github.com/bnema/gordon/internal/db/queries" | |
| 14 | 14 | "github.com/bnema/gordon/internal/server" | |
| 15 | - | "github.com/charmbracelet/log" | |
| 15 | + | "github.com/bnema/gordon/pkg/logger" | |
| 16 | 16 | "github.com/labstack/echo/v4" | |
| 17 | 17 | ) | |
| 18 | 18 | ||
| @@ -86,13 +86,13 @@ func shouldUpdateSession(token string) bool { | |||
| 86 | 86 | ||
| 87 | 87 | if lastUpdate, exists := sessionUpdateCache.Load(key); exists { | |
| 88 | 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 | 90 | return false | |
| 91 | 91 | } | |
| 92 | 92 | } | |
| 93 | 93 | ||
| 94 | 94 | sessionUpdateCache.Store(key, now) | |
| 95 | - | log.Debug("session update required") | |
| 95 | + | logger.Debug("session update required") | |
| 96 | 96 | return true | |
| 97 | 97 | } | |
| 98 | 98 | ||
| @@ -100,19 +100,19 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) { | |||
| 100 | 100 | requestID := c.Response().Header().Get(echo.HeaderXRequestID) | |
| 101 | 101 | token := c.Request().Header.Get("Authorization") | |
| 102 | 102 | if token == "" { | |
| 103 | - | log.Warn("missing token in validation request", | |
| 103 | + | logger.Warn("missing token in validation request", | |
| 104 | 104 | "request_id", requestID) | |
| 105 | 105 | return nil, echo.NewHTTPError(http.StatusUnauthorized, "Token is missing") | |
| 106 | 106 | } | |
| 107 | 107 | ||
| 108 | 108 | token = strings.TrimPrefix(token, "Bearer ") | |
| 109 | - | log.Debug("validating GitHub token", | |
| 109 | + | logger.Debug("validating GitHub token", | |
| 110 | 110 | "request_id", requestID) | |
| 111 | 111 | ||
| 112 | 112 | client := &http.Client{} | |
| 113 | 113 | req, err := http.NewRequest("GET", "https://api.github.com/user", nil) | |
| 114 | 114 | if err != nil { | |
| 115 | - | log.Error("failed to create GitHub API request", | |
| 115 | + | logger.Error("failed to create GitHub API request", | |
| 116 | 116 | "request_id", requestID, | |
| 117 | 117 | "error", err) | |
| 118 | 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 | 124 | resp, err := client.Do(req) | |
| 125 | 125 | if err != nil { | |
| 126 | - | log.Error("GitHub API request failed", | |
| 126 | + | logger.Error("GitHub API request failed", | |
| 127 | 127 | "request_id", requestID, | |
| 128 | 128 | "error", err) | |
| 129 | 129 | return nil, fmt.Errorf("failed to send request: %w", err) | |
| @@ -131,7 +131,7 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) { | |||
| 131 | 131 | defer resp.Body.Close() | |
| 132 | 132 | ||
| 133 | 133 | if resp.StatusCode != http.StatusOK { | |
| 134 | - | log.Warn("invalid GitHub token", | |
| 134 | + | logger.Warn("invalid GitHub token", | |
| 135 | 135 | "request_id", requestID, | |
| 136 | 136 | "status", resp.Status) | |
| 137 | 137 | return nil, echo.NewHTTPError(http.StatusUnauthorized, "Invalid token") | |
| @@ -139,7 +139,7 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) { | |||
| 139 | 139 | ||
| 140 | 140 | body, err := io.ReadAll(resp.Body) | |
| 141 | 141 | if err != nil { | |
| 142 | - | log.Error("failed to read GitHub response", | |
| 142 | + | logger.Error("failed to read GitHub response", | |
| 143 | 143 | "request_id", requestID, | |
| 144 | 144 | "error", err) | |
| 145 | 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 | 148 | var githubUser GithubUserInfo | |
| 149 | 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 | 151 | "request_id", requestID, | |
| 152 | 152 | "error", err) | |
| 153 | 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 | 157 | "request_id", requestID, | |
| 158 | 158 | "user", githubUser.Login) | |
| 159 | 159 | ||
| 160 | 160 | emails, err := fetchGitHubUserEmails(token) | |
| 161 | 161 | if err != nil { | |
| 162 | - | log.Error("failed to fetch GitHub user emails", | |
| 162 | + | logger.Error("failed to fetch GitHub user emails", | |
| 163 | 163 | "request_id", requestID, | |
| 164 | 164 | "user", githubUser.Login, | |
| 165 | 165 | "error", err) | |
| @@ -167,7 +167,7 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) { | |||
| 167 | 167 | } | |
| 168 | 168 | githubUser.Emails = emails | |
| 169 | 169 | ||
| 170 | - | log.Debug("completed GitHub user validation", | |
| 170 | + | logger.Debug("completed GitHub user validation", | |
| 171 | 171 | "request_id", requestID, | |
| 172 | 172 | "user", githubUser.Login, | |
| 173 | 173 | "email_count", len(emails)) | |
| @@ -176,12 +176,12 @@ func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) { | |||
| 176 | 176 | } | |
| 177 | 177 | ||
| 178 | 178 | func fetchGitHubUserEmails(token string) ([]string, error) { | |
| 179 | - | log.Debug("fetching GitHub user emails") | |
| 179 | + | logger.Debug("fetching GitHub user emails") | |
| 180 | 180 | ||
| 181 | 181 | client := &http.Client{} | |
| 182 | 182 | req, err := http.NewRequest("GET", "https://api.github.com/user/emails", nil) | |
| 183 | 183 | if err != nil { | |
| 184 | - | log.Error("failed to create email request", | |
| 184 | + | logger.Error("failed to create email request", | |
| 185 | 185 | "error", err) | |
| 186 | 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 | 192 | resp, err := client.Do(req) | |
| 193 | 193 | if err != nil { | |
| 194 | - | log.Error("failed to send email request", | |
| 194 | + | logger.Error("failed to send email request", | |
| 195 | 195 | "error", err) | |
| 196 | 196 | return nil, fmt.Errorf("failed to send request: %w", err) | |
| 197 | 197 | } | |
| 198 | 198 | defer resp.Body.Close() | |
| 199 | 199 | ||
| 200 | 200 | if resp.StatusCode != http.StatusOK { | |
| 201 | - | log.Warn("failed to fetch emails", | |
| 201 | + | logger.Warn("failed to fetch emails", | |
| 202 | 202 | "status", resp.Status) | |
| 203 | 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 | 207 | Email string `json:"email"` | |
| 208 | 208 | } | |
| 209 | 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 | 211 | "error", err) | |
| 212 | 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 | 217 | emails[i] = e.Email | |
| 218 | 218 | } | |
| 219 | 219 | ||
| 220 | - | log.Debug("fetched GitHub emails successfully", | |
| 220 | + | logger.Debug("fetched GitHub emails successfully", | |
| 221 | 221 | "email_count", len(emails)) | |
| 222 | 222 | return emails, nil | |
| 223 | 223 | } | |
| @@ -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 false | 90 | 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 true | 96 | 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 GithubUserInfo | 148 | 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 = emails | 168 | 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.Email | 217 | 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, nil | 222 | return emails, nil | ||
| 223 | } | 223 | } | ||
M internal/proxy/core.go +41 -10
| @@ -1,6 +1,8 @@ | |||
| 1 | 1 | package proxy | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "os" | |
| 5 | + | "strings" | |
| 4 | 6 | "sync" | |
| 5 | 7 | "time" | |
| 6 | 8 | ||
| @@ -8,6 +10,7 @@ import ( | |||
| 8 | 10 | ||
| 9 | 11 | "github.com/bnema/gordon/internal/common" | |
| 10 | 12 | "github.com/bnema/gordon/internal/interfaces" | |
| 13 | + | "github.com/bnema/gordon/pkg/docker" | |
| 11 | 14 | "github.com/charmbracelet/log" | |
| 12 | 15 | "github.com/labstack/echo/v4" | |
| 13 | 16 | "golang.org/x/crypto/acme/autocert" | |
| @@ -45,6 +48,9 @@ type Proxy struct { | |||
| 45 | 48 | ||
| 46 | 49 | // Flag to track specific domain certificate operations | |
| 47 | 50 | processingSpecificDomain bool | |
| 51 | + | ||
| 52 | + | // Store Gordon's own container ID at startup for reliable identification | |
| 53 | + | gordonContainerID string | |
| 48 | 54 | } | |
| 49 | 55 | ||
| 50 | 56 | // NewProxy creates a new instance of the reverse proxy | |
| @@ -65,6 +71,30 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) { | |||
| 65 | 71 | // Set up the config | |
| 66 | 72 | 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 | 98 | // Initialize the blacklist | |
| 69 | 99 | blacklistPath := app.GetConfig().General.StorageDir + "/blacklist.json" | |
| 70 | 100 | blacklist, err := NewBlacklist(blacklistPath) | |
| @@ -77,16 +107,17 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) { | |||
| 77 | 107 | ||
| 78 | 108 | // Create the proxy | |
| 79 | 109 | 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, | |
| 90 | 121 | } | |
| 91 | 122 | ||
| 92 | 123 | // Now add the middleware with the proxy reference | |
| @@ -1,6 +1,8 @@ | |||||
| 1 | package proxy | 1 | 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 operations | 49 | // Flag to track specific domain certificate operations | ||
| 47 | processingSpecificDomain bool | 50 | 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 proxy | 56 | // NewProxy creates a new instance of the reverse proxy | ||
| @@ -65,6 +71,30 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) { | |||||
| 65 | // Set up the config | 71 | // Set up the config | ||
| 66 | config := app.GetConfig().ReverseProxy | 72 | 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 blacklist | 98 | // 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 proxy | 108 | // 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 time | 118 | + | 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 reference | 123 | // Now add the middleware with the proxy reference | ||
M internal/proxy/middleware.go +85 -16
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "time" | |
| 8 | 8 | ||
| 9 | 9 | "github.com/charmbracelet/log" | |
| 10 | + | "github.com/google/uuid" | |
| 10 | 11 | "github.com/labstack/echo/v4" | |
| 11 | 12 | "github.com/labstack/echo/v4/middleware" | |
| 12 | 13 | "golang.org/x/time/rate" | |
| @@ -20,18 +21,45 @@ var rateLimiterCleanup []func() | |||
| 20 | 21 | // Create blacklistedIPs set to track IPs for middleware skipping | |
| 21 | 22 | type requestContext struct { | |
| 22 | 23 | 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 | 30 | // setupMiddleware configures middleware for both HTTP and HTTPS servers | |
| 26 | 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 | 55 | // Create blacklist middleware for HTTPS | |
| 28 | 56 | p.httpsServer.Use(p.createBlacklistMiddleware()) | |
| 29 | 57 | ||
| 30 | 58 | // Create a new Starskey rate limiter store for HTTPS server | |
| 31 | 59 | httpsRateLimiter, err := kv.NewStarskeyRateLimiterStore( | |
| 32 | 60 | "./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) | |
| 35 | 63 | 3*time.Minute, // Expires in 3 minutes | |
| 36 | 64 | ) | |
| 37 | 65 | if err != nil { | |
| @@ -41,8 +69,8 @@ func (p *Proxy) setupMiddleware() { | |||
| 41 | 69 | p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | |
| 42 | 70 | Store: middleware.NewRateLimiterMemoryStoreWithConfig( | |
| 43 | 71 | 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) | |
| 46 | 74 | ExpiresIn: 3 * time.Minute, // Store expiration | |
| 47 | 75 | }, | |
| 48 | 76 | ), | |
| @@ -62,9 +90,14 @@ func (p *Proxy) setupMiddleware() { | |||
| 62 | 90 | p.httpsServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | |
| 63 | 91 | Store: httpsRateLimiter, | |
| 64 | 92 | IdentifierExtractor: func(ctx echo.Context) (string, error) { | |
| 65 | - | // Use the client's real IP as the identifier | |
| 66 | 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 | 101 | return id, nil | |
| 69 | 102 | }, | |
| 70 | 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 config | |
| 120 | + | // Add custom logger middleware with request ID if enabled in config | |
| 88 | 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 | 129 | log.Debug("HTTP request logging enabled for HTTPS server") | |
| 91 | 130 | } else { | |
| 92 | 131 | log.Debug("HTTP request logging disabled for HTTPS server") | |
| @@ -98,8 +137,8 @@ func (p *Proxy) setupMiddleware() { | |||
| 98 | 137 | // Create a new Starskey rate limiter store for HTTP server | |
| 99 | 138 | httpRateLimiter, err := kv.NewStarskeyRateLimiterStore( | |
| 100 | 139 | "./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) | |
| 103 | 142 | 3*time.Minute, // Expires in 3 minutes | |
| 104 | 143 | ) | |
| 105 | 144 | if err != nil { | |
| @@ -109,8 +148,8 @@ func (p *Proxy) setupMiddleware() { | |||
| 109 | 148 | p.httpServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | |
| 110 | 149 | Store: middleware.NewRateLimiterMemoryStoreWithConfig( | |
| 111 | 150 | 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) | |
| 114 | 153 | ExpiresIn: 3 * time.Minute, // Store expiration | |
| 115 | 154 | }, | |
| 116 | 155 | ), | |
| @@ -130,9 +169,14 @@ func (p *Proxy) setupMiddleware() { | |||
| 130 | 169 | p.httpServer.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ | |
| 131 | 170 | Store: httpRateLimiter, | |
| 132 | 171 | IdentifierExtractor: func(ctx echo.Context) (string, error) { | |
| 133 | - | // Use the client's real IP as the identifier | |
| 134 | 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 | 180 | return id, nil | |
| 137 | 181 | }, | |
| 138 | 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 config | |
| 199 | + | // Add custom logger middleware with request ID for HTTP server if enabled in config | |
| 156 | 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 | 208 | log.Debug("HTTP request logging enabled for HTTP server") | |
| 159 | 209 | } else { | |
| 160 | 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 | 233 | // createBlacklistMiddleware returns a middleware function that checks IPs against the blacklist | |
| 165 | 234 | func (p *Proxy) createBlacklistMiddleware() echo.MiddlewareFunc { | |
| 166 | 235 | 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 skipping | 21 | // Create blacklistedIPs set to track IPs for middleware skipping | ||
| 21 | type requestContext struct { | 22 | type requestContext struct { | ||
| 22 | blacklisted bool | 23 | 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 servers | 30 | // 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 HTTPS | 55 | // 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 server | 58 | // 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 data | 60 | "./data/ratelimiter/https", // Path to store rate limit data | ||
| 33 | - | 10, // Rate: 10 requests per second | 61 | + | 2, // Rate: 2 requests per second (lowered from 10) |
| 34 | - | 30, // Burst: 30 requests | 62 | + | 10, // Burst: 10 requests (lowered from 30) |
| 35 | 3*time.Minute, // Expires in 3 minutes | 63 | 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 second | 72 | + | Rate: rate.Limit(2), // 2 requests per second (lowered from 10) |
| 45 | - | Burst: 30, // Burst of 30 requests | 73 | + | Burst: 10, // Burst of 10 requests (lowered from 30) |
| 46 | ExpiresIn: 3 * time.Minute, // Store expiration | 74 | 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, nil | 101 | 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 config | 120 | + | // 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 server | 137 | // 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 data | 139 | "./data/ratelimiter/http", // Path to store rate limit data | ||
| 101 | - | 10, // Rate: 10 requests per second | 140 | + | 2, // Rate: 2 requests per second (lowered from 10) |
| 102 | - | 30, // Burst: 30 requests | 141 | + | 10, // Burst: 10 requests (lowered from 30) |
| 103 | 3*time.Minute, // Expires in 3 minutes | 142 | 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 second | 151 | + | Rate: rate.Limit(2), // 2 requests per second (lowered from 10) |
| 113 | - | Burst: 30, // Burst of 30 requests | 152 | + | Burst: 10, // Burst of 10 requests (lowered from 30) |
| 114 | ExpiresIn: 3 * time.Minute, // Store expiration | 153 | 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, nil | 180 | 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 config | 199 | + | // 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 blacklist | 233 | // 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 ( | |||
| 7 | 7 | "net/http" | |
| 8 | 8 | "net/http/httputil" | |
| 9 | 9 | "net/url" | |
| 10 | + | "os" | |
| 10 | 11 | "strings" | |
| 11 | 12 | "time" | |
| 12 | 13 | ||
| @@ -34,6 +35,20 @@ func (p *Proxy) loadRoutes() error { | |||
| 34 | 35 | ||
| 35 | 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 | 52 | // Query the database for active proxy routes | |
| 38 | 53 | rows, err := p.app.GetDB().Query(` | |
| 39 | 54 | 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 | 59 | if err != nil { | |
| 45 | 60 | if err == sql.ErrNoRows { | |
| 46 | 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 | 70 | return nil | |
| 48 | 71 | } | |
| 49 | 72 | return fmt.Errorf("failed to query database: %w", err) | |
| 50 | 73 | } | |
| 51 | 74 | defer rows.Close() | |
| 52 | 75 | ||
| 53 | - | // Clear the routes map | |
| 76 | + | // Clear the routes map but preserve the admin domain route | |
| 54 | 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 | 85 | // Populate the routes map | |
| 57 | 86 | for rows.Next() { | |
| 58 | 87 | var id, domain, containerID, containerIP, containerPort, protocol, path string | |
| @@ -89,6 +118,47 @@ func (p *Proxy) configureRoutes() { | |||
| 89 | 118 | adminDomain := p.app.GetConfig().Http.FullDomain() | |
| 90 | 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 | 162 | // Handler function - will be assigned to both HTTP and HTTPS servers | |
| 93 | 163 | handler := echo.HandlerFunc(func(c echo.Context) error { | |
| 94 | 164 | host := c.Request().Host | |
| @@ -260,7 +330,14 @@ func (p *Proxy) configureRoutes() { | |||
| 260 | 330 | "target", targetURL.String(), | |
| 261 | 331 | "path", req.URL.Path, | |
| 262 | 332 | "clientIP", c.RealIP(), | |
| 333 | + | "remoteAddr", req.RemoteAddr, | |
| 263 | 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 | 343 | // Add error handling | |
| @@ -474,6 +551,14 @@ func (p *Proxy) AddRoute(domainName, containerID, containerIP, containerPort, pr | |||
| 474 | 551 | ||
| 475 | 552 | // RemoveRoute removes a route from the database and reloads the proxy | |
| 476 | 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 | 562 | // Begin a transaction | |
| 478 | 563 | tx, err := p.app.GetDB().Begin() | |
| 479 | 564 | if err != nil { | |
| @@ -535,6 +620,26 @@ func (p *Proxy) GetRoutes() map[string]*ProxyRouteInfo { | |||
| 535 | 620 | func (p *Proxy) Reload() error { | |
| 536 | 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 | 643 | // Create a new route map instead of modifying the existing one | |
| 539 | 644 | // This ensures a clean reload without any potential race conditions | |
| 540 | 645 | newRoutes := make(map[string]*ProxyRouteInfo) | |
| @@ -575,7 +680,7 @@ func (p *Proxy) Reload() error { | |||
| 575 | 680 | Active: active, | |
| 576 | 681 | } | |
| 577 | 682 | ||
| 578 | - | log.Info("Loaded route during reload", | |
| 683 | + | log.Debug("Loaded route during reload", | |
| 579 | 684 | "domain", domain, | |
| 580 | 685 | "containerID", containerID, | |
| 581 | 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 | 764 | // Atomically swap the new routes map with the old one | |
| 587 | 765 | // This ensures that no requests use a partially updated map | |
| 588 | 766 | p.mu.Lock() | |
| 589 | 767 | p.routes = newRoutes | |
| 590 | 768 | p.mu.Unlock() | |
| 591 | 769 | ||
| 770 | + | // Clean up any root domain conflict | |
| 771 | + | p.CleanupRootDomainForAdmin() | |
| 772 | + | ||
| 592 | 773 | // Rebuild route configuration | |
| 593 | 774 | p.configureRoutes() | |
| 594 | 775 | ||
| @@ -603,11 +784,6 @@ func (p *Proxy) Reload() error { | |||
| 603 | 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 | 787 | return nil | |
| 612 | 788 | } | |
| 613 | 789 | ||
| @@ -985,84 +1161,180 @@ func (p *Proxy) StartPeriodicIPVerification(interval time.Duration) { | |||
| 985 | 1161 | ||
| 986 | 1162 | // verifyAllContainerIPs checks all routes and verifies their container IPs | |
| 987 | 1163 | 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 | + | } | |
| 993 | 1176 | } | |
| 1177 | + | p.mu.RUnlock() | |
| 994 | 1178 | ||
| 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") | |
| 999 | 1183 | return | |
| 1000 | 1184 | } | |
| 1001 | 1185 | ||
| 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 | |
| 1009 | 1189 | ||
| 1010 | 1190 | // Track orphaned routes to mark as inactive | |
| 1011 | 1191 | orphanedRoutes := []string{} | |
| 1012 | 1192 | ||
| 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 | + | } | |
| 1033 | 1213 | } | |
| 1034 | - | continue | |
| 1214 | + | } 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 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 | |
| 1042 | 1224 | } | |
| 1043 | 1225 | ||
| 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 | |
| 1047 | 1235 | } | |
| 1048 | 1236 | ||
| 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", | |
| 1052 | 1241 | "domain", domain, | |
| 1053 | - | "container_id", route.ContainerID, | |
| 1054 | - | "old_ip", route.ContainerIP, | |
| 1055 | - | "new_ip", containerIP) | |
| 1242 | + | "container_id", containerID) | |
| 1243 | + | } | |
| 1056 | 1244 | ||
| 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) | |
| 1059 | 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 | 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 | 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 | 1342 | if len(orphanedRoutes) > 0 { | |
| 1071 | 1343 | log.Info("Marking orphaned routes as inactive", "count", len(orphanedRoutes)) | |
| 1072 | 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 | 1358 | // markRoutesInactive marks the specified routes as inactive in the database and memory | |
| @@ -1081,6 +1361,28 @@ func (p *Proxy) markRoutesInactive(domains []string) { | |||
| 1081 | 1361 | 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 | 1386 | // Begin a transaction | |
| 1085 | 1387 | tx, err := p.app.GetDB().Begin() | |
| 1086 | 1388 | if err != nil { | |
| @@ -1137,6 +1439,9 @@ func (p *Proxy) DiscoverMissingRoutes() { | |||
| 1137 | 1439 | return | |
| 1138 | 1440 | } | |
| 1139 | 1441 | ||
| 1442 | + | // Get the admin domain from the config | |
| 1443 | + | adminDomain := p.app.GetConfig().Http.FullDomain() | |
| 1444 | + | ||
| 1140 | 1445 | // Get all containers in the network | |
| 1141 | 1446 | networkInfo, err := docker.GetNetworkInfo(networkName) | |
| 1142 | 1447 | if err != nil { | |
| @@ -1188,6 +1493,14 @@ func (p *Proxy) DiscoverMissingRoutes() { | |||
| 1188 | 1493 | 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 | 1504 | // Check if this domain already has a route (different container) | |
| 1192 | 1505 | if _, exists := routes[domainLabel]; exists { | |
| 1193 | 1506 | log.Warn("Domain already has a route with a different container", | |
| @@ -1233,104 +1546,437 @@ func (p *Proxy) DiscoverMissingRoutes() { | |||
| 1233 | 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 routes | |
| 1239 | - | // to the logs for debugging and monitoring purposes | |
| 1563 | + | // LogActiveContainersAndRoutes logs a list of active containers and their routes | |
| 1240 | 1564 | 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) | |
| 1246 | 1578 | } | |
| 1247 | 1579 | ||
| 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() | |
| 1250 | 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 | 1675 | return | |
| 1253 | 1676 | } | |
| 1254 | 1677 | ||
| 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 | |
| 1261 | 1723 | } | |
| 1262 | 1724 | } | |
| 1263 | - | p.mu.RUnlock() | |
| 1264 | 1725 | ||
| 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() | |
| 1269 | 1732 | 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) | |
| 1273 | 1747 | } | |
| 1274 | 1748 | } | |
| 1275 | 1749 | ||
| 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 | + | } | |
| 1278 | 1774 | ||
| 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 | + | } | |
| 1286 | 1794 | ||
| 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 | + | } | |
| 1291 | 1807 | } | |
| 1292 | 1808 | ||
| 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 | |
| 1299 | 1820 | } | |
| 1821 | + | } | |
| 1822 | + | } | |
| 1300 | 1823 | ||
| 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 | |
| 1304 | 1863 | } | |
| 1864 | + | defer tx.Rollback() | |
| 1305 | 1865 | ||
| 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) | |
| 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 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) | |
| 1317 | 1921 | } | |
| 1922 | + | return | |
| 1318 | 1923 | } | |
| 1319 | 1924 | ||
| 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 | |
| 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 | } | |
| @@ -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 routes | 52 | // 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.active | 54 | 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 nil | 70 | 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 map | 76 | + | // 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 map | 85 | // Populate the routes map | ||
| 57 | for rows.Next() { | 86 | for rows.Next() { | ||
| 58 | var id, domain, containerID, containerIP, containerPort, protocol, path string | 87 | 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 servers | 162 | // 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().Host | 164 | 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 handling | 343 | // 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 proxy | 552 | // 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 transaction | 562 | // 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 one | 643 | // Create a new route map instead of modifying the existing one | ||
| 539 | // This ensures a clean reload without any potential race conditions | 644 | // 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 one | 764 | // Atomically swap the new routes map with the old one | ||
| 587 | // This ensures that no requests use a partially updated map | 765 | // This ensures that no requests use a partially updated map | ||
| 588 | p.mu.Lock() | 766 | p.mu.Lock() | ||
| 589 | p.routes = newRoutes | 767 | 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 configuration | 773 | // 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 nil | 787 | 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 IPs | 1162 | // 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 config | 1164 | + | p.mu.RLock() |
| 989 | - | networkName := p.app.GetConfig().ContainerEngine.Network | 1165 | + | 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 | - | return | 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 | + | } | |||
| 993 | } | 1176 | } | ||
| 1177 | + | p.mu.RUnlock() | |||
| 994 | 1178 | ||||
| 995 | - | // Get network info | 1179 | + | // 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 | return | 1183 | return | ||
| 1000 | } | 1184 | } | ||
| 1001 | 1185 | ||||
| 1002 | - | // Get a copy of the routes | 1186 | + | // 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 inactive | 1190 | // Track orphaned routes to mark as inactive | ||
| 1011 | orphanedRoutes := []string{} | 1191 | orphanedRoutes := []string{} | ||
| 1012 | 1192 | ||||
| 1013 | - | // Check each route | 1193 | + | // 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 network | 1195 | + | 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 network | 1198 | + | gordonIP, err := docker.GetContainerIPFromNetwork(gordonContainerID, networkName) |
| 1019 | - | // Could be stopped or removed | 1199 | + | if err == nil && gordonIP != "" { |
| 1020 | - | 1200 | + | log.Debug("Found Gordon container IP", "ip", gordonIP) | |
| 1021 | - | // Check if container still exists | 1201 | + | updatedIPs[gordonContainerID] = gordonIP |
| 1022 | - | if docker.ContainerExists(route.ContainerID) { | 1202 | + | |
| 1023 | - | // Container exists but not in our network | 1203 | + | // 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 exists | 1208 | + | 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 | - | continue | 1214 | + | } 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 info | 1221 | + | for domain, route := range routes { |
| 1038 | - | containerIP := containerEndpoint.IPv4Address | 1222 | + | 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 subnet | 1226 | + | 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 IP | 1237 | + | // 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 IP | 1245 | + | // 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 memory | 1358 | // markRoutesInactive marks the specified routes as inactive in the database and memory | ||
| @@ -1081,6 +1361,28 @@ func (p *Proxy) markRoutesInactive(domains []string) { | |||||
| 1081 | return | 1361 | 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 transaction | 1386 | // 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 | return | 1439 | 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 network | 1445 | // 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 | continue | 1493 | 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 routes | 1563 | + | // 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 config | 1565 | + | log.Info("---- Listing active containers and routes ----") |
| 1242 | - | networkName := p.app.GetConfig().ContainerEngine.Network | 1566 | + | |
| 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 | - | return | 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) | |||
| 1246 | } | 1578 | } | ||
| 1247 | 1579 | ||||
| 1248 | - | // Get all containers in the network | 1580 | + | 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 | return | 1675 | return | ||
| 1253 | } | 1676 | } | ||
| 1254 | 1677 | ||||
| 1255 | - | // Get all active routes | 1678 | + | // 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] = v | 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 | |||
| 1261 | } | 1723 | } | ||
| 1262 | } | 1724 | } | ||
| 1263 | - | p.mu.RUnlock() | |||
| 1264 | 1725 | ||||
| 1265 | - | // Map container IDs to names for more readable output | 1726 | + | // 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 available | 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) | |||
| 1273 | } | 1747 | } | ||
| 1274 | } | 1748 | } | ||
| 1275 | 1749 | ||||
| 1276 | - | // Format and log the active containers and routes | 1750 | + | // 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 logs | 1775 | + | // 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 header | 1780 | + | "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 readability | 1795 | + | // 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 routes | 1809 | + | // 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 | - | continue | 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 | |||
| 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 available | 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 | |||
| 1304 | } | 1863 | } | ||
| 1864 | + | defer tx.Rollback() | |||
| 1305 | 1865 | ||||
| 1306 | - | // Get container IP from network info | 1866 | + | // 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.IPv4Address | 1869 | + | if err != nil { |
| 1310 | - | if containerIP == "" { | 1870 | + | if err != sql.ErrNoRows { |
| 1311 | - | containerIP = endpoint.IPv6Address | 1871 | + | 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 subnet | 1915 | + | // 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 info | 1925 | + | // 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 container | 1930 | + | } |
| 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 ( | |||
| 6 | 6 | "fmt" | |
| 7 | 7 | "net/http" | |
| 8 | 8 | "os" | |
| 9 | - | "strings" | |
| 10 | 9 | "time" | |
| 11 | 10 | ||
| 12 | 11 | "github.com/bnema/gordon/pkg/docker" | |
| @@ -20,16 +19,19 @@ import ( | |||
| 20 | 19 | ||
| 21 | 20 | // Start loads the routes from the database and starts the proxy server | |
| 22 | 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 | 22 | // First, load routes from the database | |
| 29 | 23 | if err := p.loadRoutes(); err != nil { | |
| 30 | 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 | 35 | // Run an immediate route verification to ensure all routes are valid on startup | |
| 34 | 36 | log.Info("Performing initial route verification on startup") | |
| 35 | 37 | p.verifyAllContainerIPs() | |
| @@ -93,12 +95,33 @@ func (p *Proxy) Start() error { | |||
| 93 | 95 | // We'll add the admin route with the default host for now | |
| 94 | 96 | // 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 | 122 | p.mu.Lock() | |
| 97 | - | // Only add if it doesn't already exist | |
| 123 | + | // Only add if it doesn't already exist in memory | |
| 98 | 124 | if _, exists := p.routes[adminDomain]; !exists { | |
| 99 | - | // Auto-detect the Gordon container name | |
| 100 | - | containerName := p.detectGordonContainer() | |
| 101 | - | ||
| 102 | 125 | p.routes[adminDomain] = &ProxyRouteInfo{ | |
| 103 | 126 | Domain: adminDomain, | |
| 104 | 127 | ContainerIP: containerIP, | |
| @@ -112,31 +135,23 @@ func (p *Proxy) Start() error { | |||
| 112 | 135 | "domain", adminDomain, | |
| 113 | 136 | "container", containerName, | |
| 114 | 137 | "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)) | |
| 137 | 145 | } | |
| 138 | 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 | 155 | // Set up middleware | |
| 141 | 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 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 server | 20 | // 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 database | 22 | // 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 startup | 35 | // 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 now | 95 | // We'll add the admin route with the default host for now | ||
| 94 | // and test connections later with TestAdminConnectionLater | 96 | // 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 exist | 123 | + | // 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 subdomain | 140 | + | "domain", adminDomain, |
| 118 | - | rootDomain := p.app.GetConfig().Http.Domain | 141 | + | "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 name | 144 | + | 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 middleware | 155 | // 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 automatically | 416 | + | // // 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 container | 418 | + | // // 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/Podman | 422 | + | // // 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 hostname | 425 | + | // // 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 ID | 429 | + | // // 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 container | 431 | + | // // 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 containerName | 434 | + | // 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 processes | 443 | + | // // 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.ID | 447 | + | // containerID := container.ID |
| 433 | - | 448 | + | ||
| 434 | - | // Check container processes for Gordon | 449 | + | // // 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 containerName | 454 | + | // 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 Gordon | 459 | + | // // 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 containerName | 463 | + | // 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 image | 470 | + | // // 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 containerName | 478 | + | // 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 containerName | 486 | + | // 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 ( | |||
| 10 | 10 | "github.com/bnema/gordon/internal/proxy" | |
| 11 | 11 | "github.com/bnema/gordon/internal/templating" | |
| 12 | 12 | "github.com/bnema/gordon/internal/webui" | |
| 13 | - | "github.com/charmbracelet/log" | |
| 13 | + | "github.com/bnema/gordon/pkg/logger" | |
| 14 | 14 | ) | |
| 15 | 15 | ||
| 16 | 16 | func NewServerApp(buildConfig *common.BuildConfig) (*App, error) { | |
| 17 | 17 | // Get global config singleton instead of loading the config again | |
| 18 | 18 | config, err := common.GetGlobalConfig(buildConfig) | |
| 19 | 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 | 25 | // For development environment, use the project root directory | |
| 26 | 26 | if config.Build.RunEnv == "dev" { | |
| 27 | 27 | // Get the current working directory as the project root | |
| 28 | 28 | projectRoot, err := os.Getwd() | |
| 29 | 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 | 33 | // Set storage directory to project root + /data instead of ~/.gordon | |
| 34 | 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 | 37 | // Create the directory if it doesn't exist | |
| 38 | 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 | 43 | // Open the strings.yml file containing the strings for the current language | |
| 44 | 44 | file, err := templating.TemplateFS.Open("txt/locstrings.yml") | |
| 45 | 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 | 49 | // Read the file content into a byte slice | |
| 50 | 50 | bytes, err := io.ReadAll(file) | |
| 51 | 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 | 55 | file.Close() | |
| @@ -75,7 +75,7 @@ func NewServerApp(buildConfig *common.BuildConfig) (*App, error) { | |||
| 75 | 75 | ||
| 76 | 76 | // InitializeProxy initializes and starts the reverse proxy | |
| 77 | 77 | func (a *App) InitializeProxy() (*proxy.Proxy, error) { | |
| 78 | - | log.Info("Initializing reverse proxy") | |
| 78 | + | logger.Info("Initializing reverse proxy") | |
| 79 | 79 | ||
| 80 | 80 | // Create a new proxy | |
| 81 | 81 | p, err := proxy.NewProxy(a) | |
| @@ -88,6 +88,6 @@ func (a *App) InitializeProxy() (*proxy.Proxy, error) { | |||
| 88 | 88 | return nil, err | |
| 89 | 89 | } | |
| 90 | 90 | ||
| 91 | - | log.Info("Reverse proxy initialized and started") | |
| 91 | + | logger.Info("Reverse proxy initialized and started") | |
| 92 | 92 | return p, nil | |
| 93 | 93 | } | |
| @@ -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 again | 17 | // 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 directory | 25 | // 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 root | 27 | // 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 ~/.gordon | 33 | // 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 exist | 37 | // 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 language | 43 | // 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 slice | 49 | // 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 proxy | 76 | // 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 proxy | 80 | // 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, err | 88 | 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, nil | 92 | 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) { | |||
| 69 | 69 | 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 | 82 | // InitializeDB initializes the database | |
| 73 | 83 | func InitializeDB(a *App) (*sql.DB, error) { | |
| 74 | 84 | log.Debug("Initializing database") | |
| @@ -86,7 +96,7 @@ func InitializeDB(a *App) (*sql.DB, error) { | |||
| 86 | 96 | ||
| 87 | 97 | if !dbExists { | |
| 88 | 98 | log.Debug("Database needs bootstrapping", "path", dbPath) | |
| 89 | - | if err := bootstrapDB(dbPath, a); err != nil { | |
| 99 | + | if err := bootstrapDB(dbPath); err != nil { | |
| 90 | 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 | 114 | a.DB = db | |
| 105 | 115 | ||
| 106 | 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 | 122 | // Populate the struct tables from the database | |
| 108 | 123 | log.Debug("Populating struct tables from existing database") | |
| 109 | 124 | if err := PopulateStructWithTables(a); err != nil { | |
| @@ -116,7 +131,7 @@ func InitializeDB(a *App) (*sql.DB, error) { | |||
| 116 | 131 | return db, nil | |
| 117 | 132 | } | |
| 118 | 133 | ||
| 119 | - | func bootstrapDB(dbPath string, app *App) error { | |
| 134 | + | func bootstrapDB(dbPath string) error { | |
| 120 | 135 | // Open the database file | |
| 121 | 136 | db, err := sql.Open("sqlite", dbPath) | |
| 122 | 137 | if err != nil { | |
| @@ -69,6 +69,16 @@ func (a *App) checkDBFile(dbPath string) (bool, error) { | |||||
| 69 | return true, nil | 69 | 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 database | 82 | // 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 = db | 114 | 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 database | 122 | // 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, nil | 131 | 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 file | 135 | // 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 ( | |||
| 6 | 6 | "os" | |
| 7 | 7 | "time" | |
| 8 | 8 | ||
| 9 | - | "github.com/charmbracelet/log" | |
| 9 | + | "github.com/bnema/gordon/pkg/logger" | |
| 10 | 10 | "github.com/docker/docker/api/types" | |
| 11 | 11 | "github.com/docker/docker/api/types/container" | |
| 12 | 12 | "github.com/docker/docker/client" | |
| @@ -24,15 +24,18 @@ var ( | |||
| 24 | 24 | ||
| 25 | 25 | func InitializeClient(config *Config) error { | |
| 26 | 26 | if config.Sock == "" { | |
| 27 | + | logger.Error("Failed to initialize Docker client", "error", "sock field in Config is empty") | |
| 27 | 28 | return fmt.Errorf("sock field in Config is empty") | |
| 28 | 29 | } | |
| 29 | 30 | ||
| 30 | 31 | if _, err := os.Stat(config.Sock); os.IsNotExist(err) { | |
| 32 | + | logger.Error("Failed to initialize Docker client", "error", err) | |
| 31 | 33 | return err | |
| 32 | 34 | } | |
| 33 | 35 | ||
| 34 | 36 | // For Podman compatibility, we need to ensure we're using the right connection approach | |
| 35 | 37 | host := "unix://" + config.Sock | |
| 38 | + | logger.Debug("Setting up Docker client", "host", host) | |
| 36 | 39 | ||
| 37 | 40 | // Create the Docker client with appropriate options | |
| 38 | 41 | cli, err := client.NewClientWithOpts( | |
| @@ -40,10 +43,11 @@ func InitializeClient(config *Config) error { | |||
| 40 | 43 | client.WithHost(host), | |
| 41 | 44 | ) | |
| 42 | 45 | if err != nil { | |
| 46 | + | logger.Error("Failed to initialize Docker client", "error", err) | |
| 43 | 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 | 51 | dockerCli = cli | |
| 48 | 52 | currentConfig = config | |
| 49 | 53 | return nil | |
| @@ -51,15 +55,18 @@ func InitializeClient(config *Config) error { | |||
| 51 | 55 | ||
| 52 | 56 | func CheckIfInitialized() error { | |
| 53 | 57 | if currentConfig == nil || currentConfig.Sock == "" { | |
| 58 | + | logger.Error("Docker client check failed", "error", "client not initialized or configuration missing") | |
| 54 | 59 | return fmt.Errorf("docker client is not initialized or configuration is missing") | |
| 55 | 60 | } | |
| 56 | 61 | ||
| 57 | 62 | socketPath := currentConfig.Sock | |
| 58 | 63 | if _, err := os.Stat(socketPath); os.IsNotExist(err) { | |
| 64 | + | logger.Error("Docker client check failed", "error", "socket file not found", "path", socketPath) | |
| 59 | 65 | return fmt.Errorf("sock file does not exist: %s", socketPath) | |
| 60 | 66 | } | |
| 61 | 67 | ||
| 62 | 68 | if dockerCli == nil { | |
| 69 | + | logger.Error("Docker client check failed", "error", "client is nil") | |
| 63 | 70 | return fmt.Errorf("docker client is not initialized") | |
| 64 | 71 | } | |
| 65 | 72 | ||
| @@ -68,21 +75,27 @@ func CheckIfInitialized() error { | |||
| 68 | 75 | ||
| 69 | 76 | _, err := dockerCli.ContainerList(ctx, container.ListOptions{}) | |
| 70 | 77 | if err != nil { | |
| 78 | + | logger.Error("Docker client check failed", "error", err) | |
| 71 | 79 | return fmt.Errorf("cannot connect to Docker daemon: %s", err) | |
| 72 | 80 | } | |
| 73 | 81 | ||
| 82 | + | logger.Debug("Docker client check successful") | |
| 74 | 83 | return nil | |
| 75 | 84 | } | |
| 76 | 85 | ||
| 77 | 86 | func ListContainers(ctx context.Context) ([]types.Container, error) { | |
| 78 | 87 | if dockerCli == nil { | |
| 88 | + | logger.Error("Failed to list containers", "error", "Docker client is not initialized") | |
| 79 | 89 | return nil, fmt.Errorf("Docker client is not initialized") | |
| 80 | 90 | } | |
| 81 | 91 | ||
| 92 | + | logger.Debug("Listing containers") | |
| 82 | 93 | containers, err := dockerCli.ContainerList(ctx, container.ListOptions{}) | |
| 83 | 94 | if err != nil { | |
| 95 | + | logger.Error("Failed to list containers", "error", err) | |
| 84 | 96 | return nil, fmt.Errorf("error listing containers: %w", err) | |
| 85 | 97 | } | |
| 86 | 98 | ||
| 99 | + | logger.Debug("Listed containers successfully", "count", len(containers)) | |
| 87 | 100 | return containers, nil | |
| 88 | 101 | } | |
| @@ -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 err | 33 | return err | ||
| 32 | } | 34 | } | ||
| 33 | 35 | ||||
| 34 | // For Podman compatibility, we need to ensure we're using the right connection approach | 36 | // For Podman compatibility, we need to ensure we're using the right connection approach | ||
| 35 | host := "unix://" + config.Sock | 37 | host := "unix://" + config.Sock | ||
| 38 | + | logger.Debug("Setting up Docker client", "host", host) | |||
| 36 | 39 | ||||
| 37 | // Create the Docker client with appropriate options | 40 | // 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 = cli | 51 | dockerCli = cli | ||
| 48 | currentConfig = config | 52 | currentConfig = config | ||
| 49 | return nil | 53 | 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.Sock | 62 | 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 nil | 83 | 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, nil | 100 | return containers, nil | ||
| 88 | } | 101 | } | ||
M pkg/docker/containers.go +374 -239
| @@ -8,11 +8,9 @@ import ( | |||
| 8 | 8 | "strings" | |
| 9 | 9 | "time" | |
| 10 | 10 | ||
| 11 | - | "github.com/charmbracelet/log" | |
| 11 | + | "github.com/bnema/gordon/pkg/logger" | |
| 12 | 12 | "github.com/docker/docker/api/types" | |
| 13 | 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 | 14 | "github.com/docker/docker/api/types/network" | |
| 17 | 15 | "github.com/docker/docker/client" | |
| 18 | 16 | "github.com/docker/go-connections/nat" | |
| @@ -114,17 +112,18 @@ func StopContainer(containerID string) error { | |||
| 114 | 112 | // Try graceful shutdown first | |
| 115 | 113 | stopped, err := StopContainerGracefully(containerID, 3*time.Second) | |
| 116 | 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 | 118 | // If container wasn't stopped gracefully or there was an error, try forceful shutdown | |
| 121 | 119 | if !stopped { | |
| 122 | 120 | if err := StopContainerRagefully(containerID); err != nil { | |
| 121 | + | logger.Error("Failed to stop container forcefully", "error", err) | |
| 123 | 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 | 125 | } else { | |
| 127 | - | log.Info("Container stopped gracefully", "containerID", containerID) | |
| 126 | + | logger.Info("Container stopped gracefully", "containerID", containerID) | |
| 128 | 127 | } | |
| 129 | 128 | ||
| 130 | 129 | return nil | |
| @@ -194,7 +193,7 @@ func RemoveContainer(containerID string) error { | |||
| 194 | 193 | ||
| 195 | 194 | // StartContainer starts a container | |
| 196 | 195 | func StartContainer(containerID string) error { | |
| 197 | - | fmt.Println("Starting container", containerID) | |
| 196 | + | logger.Info("Starting container", "containerID", containerID) | |
| 198 | 197 | ||
| 199 | 198 | // Add timeout context | |
| 200 | 199 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | |
| @@ -203,20 +202,23 @@ func StartContainer(containerID string) error { | |||
| 203 | 202 | // Get initial container state | |
| 204 | 203 | containerInfo, err := GetContainerInfo(containerID) | |
| 205 | 204 | if err != nil { | |
| 205 | + | logger.Error("Failed to get container info", "containerID", containerID, "error", err) | |
| 206 | 206 | return fmt.Errorf("could not get container info: %v", err) | |
| 207 | 207 | } | |
| 208 | 208 | ||
| 209 | 209 | if containerInfo.State.Running { | |
| 210 | + | logger.Warn("Container is already running", "containerID", containerID) | |
| 210 | 211 | return fmt.Errorf("container is already running") | |
| 211 | 212 | } | |
| 212 | 213 | ||
| 213 | 214 | // Start container | |
| 215 | + | logger.Debug("Executing container start", "containerID", containerID) | |
| 214 | 216 | err = dockerCli.ContainerStart(ctx, containerID, container.StartOptions{}) | |
| 215 | 217 | if err != nil { | |
| 216 | 218 | // Get container logs if start failed | |
| 217 | 219 | logs, logErr := GetContainerLogs(containerID) | |
| 218 | 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 | 223 | return fmt.Errorf("could not start container: %v", err) | |
| 222 | 224 | } | |
| @@ -225,19 +227,22 @@ func StartContainer(containerID string) error { | |||
| 225 | 227 | for i := 0; i < 5; i++ { | |
| 226 | 228 | state, err := GetContainerState(containerID) | |
| 227 | 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 | 231 | continue | |
| 230 | 232 | } | |
| 231 | 233 | ||
| 232 | 234 | if state == "running" { | |
| 235 | + | logger.Info("Container started successfully", "containerID", containerID) | |
| 233 | 236 | return nil | |
| 234 | 237 | } | |
| 235 | 238 | ||
| 239 | + | logger.Debug("Waiting for container to start", "containerID", containerID, "state", state, "attempt", i+1) | |
| 236 | 240 | time.Sleep(time.Second) | |
| 237 | 241 | } | |
| 238 | 242 | ||
| 239 | 243 | // If we get here, container didn't start properly | |
| 240 | 244 | logs, _ := GetContainerLogs(containerID) | |
| 245 | + | logger.Error("Container failed to enter running state", "containerID", containerID, "logs", logs) | |
| 241 | 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 | 250 | func CreateContainer(cmdParams ContainerCommandParams) (string, error) { | |
| 246 | 251 | ||
| 247 | 252 | if err := CheckIfInitialized(); err != nil { | |
| 253 | + | logger.Error("Failed to check if docker client is initialized", "error", err) | |
| 248 | 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 | 258 | "name", cmdParams.ContainerName, | |
| 253 | 259 | "image", cmdParams.ImageName, | |
| 254 | 260 | "proxy_port", cmdParams.ProxyPort) | |
| @@ -256,31 +262,31 @@ func CreateContainer(cmdParams ContainerCommandParams) (string, error) { | |||
| 256 | 262 | // Check if a container with this name already exists - handle recreation case | |
| 257 | 263 | existingContainers, err := ListRunningContainers() | |
| 258 | 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 | 266 | } else { | |
| 261 | 267 | for _, container := range existingContainers { | |
| 262 | 268 | for _, name := range container.Names { | |
| 263 | 269 | // Container names in Docker API have a leading slash | |
| 264 | 270 | cleanName := strings.TrimPrefix(name, "/") | |
| 265 | 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 | 273 | "name", cmdParams.ContainerName, | |
| 268 | 274 | "existing_id", container.ID) | |
| 269 | 275 | ||
| 270 | 276 | // Stop and remove the existing container | |
| 271 | 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 | 279 | "container_id", container.ID, | |
| 274 | 280 | "error", err) | |
| 275 | 281 | } | |
| 276 | 282 | ||
| 277 | 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 | 285 | "container_id", container.ID, | |
| 280 | 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 | 290 | "name", cmdParams.ContainerName) | |
| 285 | 291 | break | |
| 286 | 292 | } | |
| @@ -291,13 +297,15 @@ func CreateContainer(cmdParams ContainerCommandParams) (string, error) { | |||
| 291 | 297 | // Check network | |
| 292 | 298 | isNetworkCreated, err := CheckIfNetworkExists(cmdParams.Network) | |
| 293 | 299 | if err != nil { | |
| 300 | + | logger.Error("Network check failed", "network", cmdParams.Network, "error", err) | |
| 294 | 301 | return "", fmt.Errorf("network check failed: %v", err) | |
| 295 | 302 | } | |
| 296 | 303 | ||
| 297 | 304 | if !isNetworkCreated { | |
| 298 | - | fmt.Printf("Creating network: %s\n", cmdParams.Network) | |
| 305 | + | logger.Info("Creating network", "network", cmdParams.Network) | |
| 299 | 306 | _, err := dockerCli.NetworkCreate(context.Background(), cmdParams.Network, network.CreateOptions{}) | |
| 300 | 307 | if err != nil { | |
| 308 | + | logger.Error("Network creation failed", "network", cmdParams.Network, "error", err) | |
| 301 | 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 | 324 | 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) | |
| 321 | 329 | } | |
| 322 | 330 | ||
| 323 | 331 | // Prepare labels | |
| @@ -358,31 +366,37 @@ func CreateContainer(cmdParams ContainerCommandParams) (string, error) { | |||
| 358 | 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 | 371 | return resp.ID, nil | |
| 364 | 372 | } | |
| 365 | 373 | ||
| 366 | 374 | func WaitForContainerToBeRunning(containerID string, timeout time.Duration) error { | |
| 375 | + | logger.Debug("Waiting for container to be running", "containerID", containerID, "timeout", timeout) | |
| 367 | 376 | ctx, cancel := context.WithTimeout(context.Background(), timeout) | |
| 368 | 377 | defer cancel() | |
| 369 | 378 | ||
| 370 | 379 | for { | |
| 371 | 380 | select { | |
| 372 | 381 | case <-ctx.Done(): | |
| 382 | + | logger.Error("Timeout waiting for container to start", "containerID", containerID) | |
| 373 | 383 | return fmt.Errorf("timeout waiting for container to start") | |
| 374 | 384 | case <-time.After(time.Second): | |
| 375 | 385 | state, err := GetContainerState(containerID) | |
| 376 | 386 | if err != nil { | |
| 387 | + | logger.Error("Error checking container state", "containerID", containerID, "error", err) | |
| 377 | 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 | 392 | if state == "running" { | |
| 393 | + | logger.Info("Container is now running", "containerID", containerID) | |
| 381 | 394 | return nil | |
| 382 | 395 | } | |
| 383 | 396 | ||
| 384 | 397 | if state == "exited" { | |
| 385 | 398 | logs, _ := GetContainerLogs(containerID) | |
| 399 | + | logger.Error("Container exited unexpectedly", "containerID", containerID, "logs", logs) | |
| 386 | 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 | 656 | for _, container := range containers { | |
| 643 | 657 | containerInfo, err := getContainerPortInfo(ctx, cli, container) | |
| 644 | 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 | 660 | continue | |
| 647 | 661 | } | |
| 648 | 662 | ||
| @@ -736,8 +750,10 @@ func formatDuration(d time.Duration) string { | |||
| 736 | 750 | return fmt.Sprintf("%dm", minutes) | |
| 737 | 751 | } | |
| 738 | 752 | ||
| 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. | |
| 741 | 757 | func ListenForContainerEvents(networkFilter string, callback func(string, string, string)) error { | |
| 742 | 758 | // Check if the Docker client has been initialized | |
| 743 | 759 | err := CheckIfInitialized() | |
| @@ -751,273 +767,392 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string | |||
| 751 | 767 | // Store cancel func to allow shutdown | |
| 752 | 768 | containerEventCancel = cancel | |
| 753 | 769 | ||
| 754 | - | // Start the main listener loop in a goroutine | |
| 770 | + | // Start the polling loop in a goroutine | |
| 755 | 771 | 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 | |
| 760 | 788 | 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 | |
| 769 | 793 | ||
| 770 | 794 | 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 | + | } | |
| 778 | 813 | ||
| 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) | |
| 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 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 | + | } | |
| 788 | 833 | ||
| 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 | + | } | |
| 795 | 882 | ||
| 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 | |
| 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 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 | |
| 835 | 904 | } | |
| 836 | 905 | ||
| 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 | + | } | |
| 847 | 928 | } | |
| 848 | 929 | } | |
| 849 | 930 | } | |
| 850 | 931 | ||
| 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) | |
| 869 | 954 | } | |
| 955 | + | } | |
| 870 | 956 | ||
| 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 | + | }() | |
| 914 | 963 | ||
| 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 | + | } | |
| 921 | 966 | ||
| 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 | + | } | |
| 928 | 974 | ||
| 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 | + | } | |
| 933 | 981 | ||
| 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 | + | } | |
| 936 | 1002 | ||
| 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 | |
| 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 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 | |
| 958 | 1020 | } | |
| 1021 | + | } | |
| 959 | 1022 | ||
| 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"), | |
| 970 | 1031 | } | |
| 971 | - | }() | |
| 972 | 1032 | ||
| 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 | + | } | |
| 975 | 1063 | ||
| 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 | + | } | |
| 981 | 1077 | } | |
| 1078 | + | ||
| 1079 | + | return newStates, events | |
| 982 | 1080 | } | |
| 983 | 1081 | ||
| 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) { | |
| 986 | 1084 | // Get network info | |
| 987 | 1085 | networkInfo, err := GetNetworkInfo(networkName) | |
| 988 | 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 | 1092 | // Look for container in the network | |
| 993 | 1093 | containerEndpoint, exists := networkInfo.Containers[containerID] | |
| 994 | 1094 | 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 | + | } | |
| 997 | 1115 | ||
| 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 | + | } | |
| 1001 | 1123 | ||
| 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, ", ")) | |
| 1005 | 1128 | ||
| 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 | + | } | |
| 1008 | 1131 | ||
| 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 | + | } | |
| 1014 | 1139 | ||
| 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] | |
| 1018 | 1145 | } | |
| 1019 | 1146 | ||
| 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 | + | } | |
| 1021 | 1156 | } | |
| 1022 | 1157 | ||
| 1023 | 1158 | // 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 first | 112 | // 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 shutdown | 118 | // 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 nil | 129 | return nil | ||
| @@ -194,7 +193,7 @@ func RemoveContainer(containerID string) error { | |||||
| 194 | 193 | ||||
| 195 | // StartContainer starts a container | 194 | // 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 context | 198 | // 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 state | 202 | // 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 container | 214 | // 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 failed | 218 | // 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 | continue | 231 | continue | ||
| 230 | } | 232 | } | ||
| 231 | 233 | ||||
| 232 | if state == "running" { | 234 | if state == "running" { | ||
| 235 | + | logger.Info("Container started successfully", "containerID", containerID) | |||
| 233 | return nil | 236 | 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 properly | 243 | // 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 case | 262 | // 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 slash | 269 | // 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 container | 276 | // 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 | break | 291 | break | ||
| 286 | } | 292 | } | ||
| @@ -291,13 +297,15 @@ func CreateContainer(cmdParams ContainerCommandParams) (string, error) { | |||||
| 291 | // Check network | 297 | // 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 labels | 331 | // 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, nil | 371 | 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 nil | 394 | 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 | continue | 660 | 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 callback | 753 | + | // 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 initialized | 758 | // 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 shutdown | 767 | // Store cancel func to allow shutdown | ||
| 752 | containerEventCancel = cancel | 768 | containerEventCancel = cancel | ||
| 753 | 769 | ||||
| 754 | - | // Start the main listener loop in a goroutine | 770 | + | // Start the polling loop in a goroutine |
| 755 | go func() { | 771 | go func() { | ||
| 756 | - | // Implement exponential backoff for reconnection | 772 | + | // Configure polling interval - can be adjusted based on needs |
| 757 | - | initialDelay := 1 * time.Second | 773 | + | pollInterval := 10 * time.Second |
| 758 | - | maxDelay := 1 * time.Minute | 774 | + | logger.Info("Starting container polling", |
| 759 | - | currentDelay := initialDelay | 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 | |||
| 760 | consecutiveFailures := 0 | 788 | consecutiveFailures := 0 | ||
| 761 | - | maxConsecutiveFailures := 10 | 789 | + | maxConsecutiveFailures := 5 |
| 762 | - | 790 | + | initialBackoff := 5 * time.Second | |
| 763 | - | // Track EOFs for reduced logging | 791 | + | maxBackoff := 1 * time.Minute |
| 764 | - | eofCount := 0 | 792 | + | 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 | - | return | 797 | + | // 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 call | 800 | + | 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 in | 814 | + | 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 events | 824 | + | // 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 EOF | 834 | + | // 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 loop | 883 | + | // If we still don't have an IP, skip this container |
| 797 | - | listenSuccess := false | 884 | + | 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 resources | 892 | + | // 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 calculation | 895 | + | // 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 warning | 898 | + | "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 errors | 906 | + | // 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 delay | 909 | + | // For Gordon container, add extra verification to prevent routing issues |
| 841 | - | currentDelay = initialDelay | 910 | + | 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 max | 912 | + | "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 = maxDelay | 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 | + | } | |||
| 847 | } | 928 | } | ||
| 848 | } | 929 | } | ||
| 849 | } | 930 | } | ||
| 850 | 931 | ||||
| 851 | - | // Break out of the inner event loop to reconnect | 932 | + | // Add debug output before calling callback |
| 852 | - | break eventStreamLoop | 933 | + | logger.Debug("Calling container event callback", |
| 853 | - | } | 934 | + | "container_id", event.id, |
| 854 | - | // Context was canceled, exit completely | 935 | + | "container_name", event.name, |
| 855 | - | if ctx.Err() != nil { | 936 | + | "container_ip", containerIP) |
| 856 | - | eventCancel() | 937 | + | |
| 857 | - | return | 938 | + | // 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 events | 941 | + | logger.Debug("Processed container start", |
| 861 | - | if !listenSuccess { | 942 | + | "container_id", event.id, |
| 862 | - | listenSuccess = true | 943 | + | "container_name", event.name, |
| 863 | - | // Reset failure counter on successful event | 944 | + | "container_ip", containerIP) |
| 864 | - | consecutiveFailures = 0 | 945 | + | } else if event.eventType == "stop" { |
| 865 | - | // Only log first successful connection or after errors | 946 | + | // 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 time | 957 | + | case <-ctx.Done(): |
| 872 | - | lastEventTime = time.Now() | 958 | + | logger.Info("Container polling stopped due to context cancellation") |
| 873 | - | 959 | + | return | |
| 874 | - | // Process container events | 960 | + | } |
| 875 | - | if event.Type == "container" { | 961 | + | } |
| 876 | - | containerID := event.Actor.ID | 962 | + | }() |
| 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 routes | 965 | + | } |
| 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 needed | 967 | + | // 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 alive | 971 | + | 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, reconnect | 975 | + | // 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 resources | 982 | + | // 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 reconnect | 1003 | + | // If not found by name, check network IDs as fallback (for compatibility with previous code) |
| 938 | - | break eventStreamLoop | 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 | |||
| 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 reconnect | 1014 | + | if !inNetwork { |
| 947 | - | // Clean up the event context | 1015 | + | 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 types | 1018 | + | "network_filter", networkFilter) |
| 951 | - | var reconnectDelay time.Duration | 1019 | + | 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 reconnecting | 1023 | + | 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 errors | 1026 | + | 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 nil | 1033 | + | // 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 listener | 1064 | + | // 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 network | 1082 | + | // 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 info | 1084 | // 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 network | 1092 | // 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 address | 1116 | + | // If we got here, we couldn't find the container in the network |
| 999 | - | return containerEndpoint.IPv4Address, nil | 1117 | + | // 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 API | 1124 | + | 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 only | 1129 | + | 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 in | 1132 | + | // 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 network | 1140 | + | // 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 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 | + | } | |||
| 1021 | } | 1156 | } | ||
| 1022 | 1157 | ||||
| 1023 | // Variable to store cancel function for event listener | 1158 | // Variable to store cancel function for event listener | ||
M pkg/docker/detect.go +42 -11
| @@ -4,8 +4,9 @@ import ( | |||
| 4 | 4 | "os" | |
| 5 | 5 | "os/user" | |
| 6 | 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 | 12 | func fileExists(filepath string) bool { | |
| @@ -14,11 +15,41 @@ func fileExists(filepath string) bool { | |||
| 14 | 15 | } | |
| 15 | 16 | ||
| 16 | 17 | 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 | |
| 22 | 53 | } | |
| 23 | 54 | ||
| 24 | 55 | // DetectPodman automatically detects if Podman is available and returns | |
| @@ -32,7 +63,7 @@ func DetectPodman() (bool, string) { | |||
| 32 | 63 | ||
| 33 | 64 | for _, socket := range systemSockets { | |
| 34 | 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 | 67 | return true, socket | |
| 37 | 68 | } | |
| 38 | 69 | } | |
| @@ -40,7 +71,7 @@ func DetectPodman() (bool, string) { | |||
| 40 | 71 | // Then check for rootless installation | |
| 41 | 72 | currentUser, err := user.Current() | |
| 42 | 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 | 75 | return false, "" | |
| 45 | 76 | } | |
| 46 | 77 | ||
| @@ -49,7 +80,7 @@ func DetectPodman() (bool, string) { | |||
| 49 | 80 | if xdgRuntime != "" { | |
| 50 | 81 | xdgSocket := filepath.Join(xdgRuntime, "podman", "podman.sock") | |
| 51 | 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 | 84 | return true, xdgSocket | |
| 54 | 85 | } | |
| 55 | 86 | } | |
| @@ -57,11 +88,11 @@ func DetectPodman() (bool, string) { | |||
| 57 | 88 | // Check home directory socket location | |
| 58 | 89 | homeSocket := filepath.Join(currentUser.HomeDir, ".local", "share", "containers", "podman", "machine", "podman.sock") | |
| 59 | 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 | 92 | return true, homeSocket | |
| 62 | 93 | } | |
| 63 | 94 | ||
| 64 | 95 | // No Podman socket found | |
| 65 | - | log.Debug("DetectPodman: No Podman sockets found") | |
| 96 | + | logger.Debug("DetectPodman: No Podman sockets found") | |
| 66 | 97 | return false, "" | |
| 67 | 98 | } | |
| @@ -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 .iscontainer | 19 | + | 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 isContainer | 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 | |||
| 22 | } | 53 | } | ||
| 23 | 54 | ||||
| 24 | // DetectPodman automatically detects if Podman is available and returns | 55 | // 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, socket | 67 | return true, socket | ||
| 37 | } | 68 | } | ||
| 38 | } | 69 | } | ||
| @@ -40,7 +71,7 @@ func DetectPodman() (bool, string) { | |||||
| 40 | // Then check for rootless installation | 71 | // 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, xdgSocket | 84 | return true, xdgSocket | ||
| 54 | } | 85 | } | ||
| 55 | } | 86 | } | ||
| @@ -57,11 +88,11 @@ func DetectPodman() (bool, string) { | |||||
| 57 | // Check home directory socket location | 88 | // 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, homeSocket | 92 | return true, homeSocket | ||
| 62 | } | 93 | } | ||
| 63 | 94 | ||||
| 64 | // No Podman socket found | 95 | // 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 | |||
| 66 | 66 | func (s *StarskeyRateLimiterStore) Allow(identifier string) (bool, error) { | |
| 67 | 67 | var allowed bool | |
| 68 | 68 | ||
| 69 | + | // Log the identifier for debugging | |
| 70 | + | log.Debug("Rate limiter checking identifier", "identifier", identifier) | |
| 71 | + | ||
| 69 | 72 | err := s.db.Update(func(txn *starskey.Txn) error { | |
| 70 | 73 | now := time.Now() | |
| 71 | 74 | key := []byte(identifier) | |
| @@ -88,6 +91,7 @@ func (s *StarskeyRateLimiterStore) Allow(identifier string) (bool, error) { | |||
| 88 | 91 | // Check if jailed | |
| 89 | 92 | if info.Jailed && now.Before(info.ResetTime) { | |
| 90 | 93 | allowed = false | |
| 94 | + | log.Debug("Request denied: IP is jailed", "ip", identifier, "jail_expires", info.ResetTime) | |
| 91 | 95 | return nil | |
| 92 | 96 | } | |
| 93 | 97 | ||
| @@ -95,53 +99,64 @@ func (s *StarskeyRateLimiterStore) Allow(identifier string) (bool, error) { | |||
| 95 | 99 | if info.Jailed && !now.Before(info.ResetTime) { | |
| 96 | 100 | log.Info("IP address released from jail", "ip", identifier) | |
| 97 | 101 | info.Jailed = false | |
| 102 | + | info.Attempts = 0 // Reset attempts when released from jail | |
| 98 | 103 | } | |
| 99 | 104 | ||
| 100 | 105 | // Calculate time-based allowance | |
| 101 | 106 | timePassed := now.Sub(info.ResetTime).Seconds() | |
| 102 | 107 | 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 | 116 | // Reset if needed | |
| 105 | 117 | if now.After(info.ResetTime.Add(s.expiresIn)) { | |
| 118 | + | log.Debug("Resetting rate limit counter (expired)", "ip", identifier) | |
| 106 | 119 | info.Attempts = 0 | |
| 107 | 120 | info.ResetTime = now | |
| 108 | 121 | } else { | |
| 122 | + | oldAttempts := info.Attempts | |
| 109 | 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 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 | |
| 115 | 144 | info.Attempts++ | |
| 116 | 145 | 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) | |
| 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 | |
| 141 | 157 | } | |
| 142 | 158 | ||
| 143 | - | log.Debug("Request blocked (rate limited)", "ip", identifier, "attempts", info.Attempts) | |
| 144 | - | allowed = false | |
| 159 | + | txn.Put(key, data) | |
| 145 | 160 | return nil | |
| 146 | 161 | }) | |
| 147 | 162 | ||
| @@ -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 bool | 67 | 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 jailed | 91 | // Check if jailed | ||
| 89 | if info.Jailed && now.Before(info.ResetTime) { | 92 | if info.Jailed && now.Before(info.ResetTime) { | ||
| 90 | allowed = false | 93 | allowed = false | ||
| 94 | + | log.Debug("Request denied: IP is jailed", "ip", identifier, "jail_expires", info.ResetTime) | |||
| 91 | return nil | 95 | 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 = false | 101 | info.Jailed = false | ||
| 102 | + | info.Attempts = 0 // Reset attempts when released from jail | |||
| 98 | } | 103 | } | ||
| 99 | 104 | ||||
| 100 | // Calculate time-based allowance | 105 | // Calculate time-based allowance | ||
| 101 | timePassed := now.Sub(info.ResetTime).Seconds() | 106 | timePassed := now.Sub(info.ResetTime).Seconds() | ||
| 102 | tokensToAdd := timePassed * s.rate | 107 | 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 needed | 116 | // 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 = 0 | 119 | info.Attempts = 0 | ||
| 107 | info.ResetTime = now | 120 | 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 limit | 131 | + | // 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 = true | 145 | 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 = true | 154 | + | if err != nil { |
| 121 | - | info.ResetTime = now.Add(time.Second) // 1 second penalty | 155 | + | 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 nil | 160 | 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 | + | } | |||