detached 3ac0ad3

refactor: Simplify configuration management and remove unused features

- Removed the generateRandomSecret function and related JWT secret generation logic.
- Eliminated autoRenew and CacheSize fields from ReverseProxyConfig.
- Updated applyDefaultsToConfig to streamline default value application.
- Enhanced logging for applied default values and configuration loading.
- Cleaned up environment variable handling for configuration settings.
commit
3ac0ad3e076500649c4ca426d72f2db21d90c146
author
bamen · · 2025-03-29 08:57 UTC
parent
31d3492
signature
unsigned
2 files · +291 -369
M internal/common/init_config.go +19 -369
@@ -7,27 +7,14 @@ import (
77 "os"
88 "path/filepath"
99 "runtime"
10- "strconv"
1110 "strings"
1211 "sync"
1312
14- "crypto/rand"
15- "encoding/hex"
16-
1713 "github.com/bnema/gordon/pkg/docker"
1814 "github.com/bnema/gordon/pkg/logger"
1915 "github.com/bnema/gordon/pkg/parser"
2016 )
2117
22-// generateRandomSecret generates a secure random string of the specified byte length, hex-encoded.
23-func generateRandomSecret(length int) (string, error) {
24- bytes := make([]byte, length)
25- if _, err := rand.Read(bytes); err != nil {
26- return "", err
27- }
28- return hex.EncodeToString(bytes), nil
29-}
30-
3118 var (
3219 globalConfig *Config
3320 globalConfigOnce sync.Once
@@ -94,9 +81,7 @@ type ReverseProxyConfig struct {
9481 LetsEncryptMode string `yaml:"letsEncryptMode"` // staging or production
9582 SkipCertificates bool `yaml:"skipCertificates"` // Skip Let's Encrypt certificate acquisition when behind a TLS terminating proxy
9683 GracePeriod int `yaml:"gracePeriod"` // Shutdown grace period in seconds
97- AutoRenew bool `yaml:"autoRenew"` // Whether to automatically renew certificates
9884 RenewBefore int `yaml:"renewBefore"` // Days before expiry to renew certificates
99- CacheSize int `yaml:"cacheSize"` // Size of the certificate cache
10085 EnableHttpLogs bool `yaml:"enableHttpLogs"` // Whether to enable HTTP request logging
10186 EnableRateLimit bool `yaml:"enableRateLimit"` // Whether to enable rate limiting middleware
10287 DetectUpstreamProxy bool `yaml:"detectUpstreamProxy"` // Whether to detect and handle upstream TLS termination proxies
@@ -108,21 +93,24 @@ type ReverseProxyConfig struct {
10893
10994 // Default values
11095 var (
111- sock = "/var/run/docker.sock"
112- podmansock = "/run/podman/podman.sock"
113- reverseProxyPort = "443" // Changed to 443 for standard HTTPS
114- httpPort = "80" // Standard HTTP port
115- certDir = "/certs"
116- letsEncryptMode = "staging"
117- autoRenew = true
118- proxyEnabled = true // Default to enabled for backward compatibility
119- renewBefore = 30 // days
120- cacheSize = 1000 // entries
121- gracePeriod = 30 // seconds
122- defaultLogLevel = "info" // Default log level
123- disableRateLimit = false
124- detectUpstreamProxy = false // Default to disabled
125- skipCertificates = false // Default to disabled
96+ sock = "/var/run/docker.sock"
97+ podmansock = "/run/podman/podman.sock"
98+ reverseProxyPort = "443" // Changed to 443 for standard HTTPS
99+ httpPort = "80" // Standard HTTP port
100+ certDir = "/certs"
101+ letsEncryptMode = "staging"
102+ proxyEnabled = true // Default to enabled for backward compatibility
103+ renewBefore = 30 // days
104+ gracePeriod = 30 // seconds
105+ defaultLogLevel = "info" // Default log level
106+ disableRateLimit = false
107+ detectUpstreamProxy = false // Default to disabled
108+ skipCertificates = false // Default to disabled
109+ enableRateLimit = false // Default to disabled
110+ defaultChallengeType = "http-01"
111+ defaultHttpChallengePort = "80"
112+ defaultDnsChallengePropagationTimeout = 60 // seconds
113+ defaultDnsChallengePollingInterval = 5 // seconds
126114 )
127115
128116 // applyDefaultsToConfig applies default values to any fields that have zero values
@@ -208,11 +196,6 @@ func applyDefaultsToConfig(config *Config) bool {
208196 logger.Debug("Applied default value for ReverseProxy.LetsEncryptMode", "value", letsEncryptMode)
209197 defaultsApplied = true
210198 }
211- if config.ReverseProxy.CacheSize == 0 {
212- config.ReverseProxy.CacheSize = cacheSize
213- logger.Debug("Applied default value for ReverseProxy.CacheSize", "value", cacheSize)
214- defaultsApplied = true
215- }
216199 if config.ReverseProxy.GracePeriod == 0 {
217200 config.ReverseProxy.GracePeriod = gracePeriod
218201 logger.Debug("Applied default value for ReverseProxy.GracePeriod", "value", gracePeriod)
@@ -266,34 +249,6 @@ func applyDefaultsToConfig(config *Config) bool {
266249 defaultsApplied = true
267250 }
268251
269- // Handle autoRenew
270- configFilePath = filepath.Join(getConfigDirMustExist(), "config.yml")
271- if fileExists(configFilePath) {
272- yamlContent, err := os.ReadFile(configFilePath)
273- if err == nil {
274- // Check if "autoRenew" is explicitly mentioned in the config file
275- if !strings.Contains(string(yamlContent), "autoRenew:") {
276- config.ReverseProxy.AutoRenew = autoRenew
277- logger.Debug("Applied default value for ReverseProxy.AutoRenew", "value", autoRenew)
278- defaultsApplied = true
279- } else {
280- logger.Debug("Keeping explicit value for ReverseProxy.AutoRenew", "value", config.ReverseProxy.AutoRenew)
281- }
282- } else {
283- // If we can't read the file for some reason, apply the default
284- if !config.ReverseProxy.AutoRenew {
285- config.ReverseProxy.AutoRenew = autoRenew
286- logger.Debug("Applied default value for ReverseProxy.AutoRenew", "value", autoRenew)
287- defaultsApplied = true
288- }
289- }
290- } else {
291- // If config file doesn't exist yet, set the default
292- config.ReverseProxy.AutoRenew = autoRenew
293- logger.Debug("Applied default value for ReverseProxy.AutoRenew", "value", autoRenew)
294- defaultsApplied = true
295- }
296-
297252 return defaultsApplied
298253 }
299254
@@ -447,6 +402,7 @@ func getConfigDir() (string, error) {
447402 return configDir, nil
448403 }
449404
405+// readAndUnmarshalConfig reads and unmarshals a configuration file using the provided filesystem and path.
450406 func readAndUnmarshalConfig(fs fs.FS, filePath string, config *Config) error {
451407 logger.Debug("readAndUnmarshalConfig: Starting to read and unmarshal config file", "path", filePath)
452408 err := parser.ParseYAMLFile(fs, filePath, config)
@@ -458,282 +414,6 @@ func readAndUnmarshalConfig(fs fs.FS, filePath string, config *Config) error {
458414 return nil
459415 }
460416
461-// loadConfigFromEnv loads configuration from environment variables
462-func loadConfigFromEnv(config *Config, printLogs bool) {
463- // General Configuration
464- if val := os.Getenv("GORDON_STORAGE_DIR"); val != "" {
465- config.General.StorageDir = val
466- if printLogs {
467- logger.Info("Using environment variable GORDON_STORAGE_DIR", "value", val)
468- }
469- }
470- // Removed GORDON_TOKEN handling
471- if val := os.Getenv("GORDON_LOG_LEVEL"); val != "" {
472- config.General.LogLevel = val
473- if printLogs {
474- logger.Info("Using environment variable GORDON_LOG_LEVEL", "value", val)
475- }
476- }
477-
478- // HTTP Configuration
479- if val := os.Getenv("GORDON_HTTP_PORT"); val != "" {
480- config.Http.Port = val
481- if printLogs {
482- logger.Info("Using environment variable GORDON_HTTP_PORT", "value", val)
483- }
484- }
485- if val := os.Getenv("GORDON_HTTP_DOMAIN"); val != "" {
486- config.Http.Domain = val
487- if printLogs {
488- logger.Info("Using environment variable GORDON_HTTP_DOMAIN", "value", val)
489- }
490- }
491- if val := os.Getenv("GORDON_HTTP_SUBDOMAIN"); val != "" {
492- config.Http.SubDomain = val
493- if printLogs {
494- logger.Info("Using environment variable GORDON_HTTP_SUBDOMAIN", "value", val)
495- }
496- }
497- if val := os.Getenv("GORDON_HTTP_BACKEND_URL"); val != "" {
498- config.Http.BackendURL = val
499- if printLogs {
500- logger.Info("Using environment variable GORDON_HTTP_BACKEND_URL", "value", val)
501- }
502- }
503- if val := os.Getenv("GORDON_HTTP_HTTPS"); val != "" {
504- config.Http.Https = strings.ToLower(val) == "true"
505- if printLogs {
506- logger.Info("Using environment variable GORDON_HTTP_HTTPS", "value", config.Http.Https)
507- }
508- }
509-
510- // Admin Configuration
511- if val := os.Getenv("GORDON_ADMIN_PATH"); val != "" {
512- config.Admin.Path = val
513- if printLogs {
514- logger.Info("Using environment variable GORDON_ADMIN_PATH", "value", val)
515- }
516- }
517-
518- // Container Engine Configuration
519- if val := os.Getenv("GORDON_CONTAINER_SOCK"); val != "" {
520- config.ContainerEngine.Sock = val
521- if printLogs {
522- logger.Info("Using environment variable GORDON_CONTAINER_SOCK", "value", val)
523- }
524- }
525- if val := os.Getenv("GORDON_CONTAINER_PODMANSOCK"); val != "" {
526- config.ContainerEngine.PodmanSock = val
527- if printLogs {
528- logger.Info("Using environment variable GORDON_CONTAINER_PODMANSOCK", "value", val)
529- }
530- }
531- if val := os.Getenv("GORDON_CONTAINER_PODMAN"); val != "" {
532- config.ContainerEngine.Podman = strings.ToLower(val) == "true"
533- if printLogs {
534- logger.Info("Using environment variable GORDON_CONTAINER_PODMAN", "value", config.ContainerEngine.Podman)
535- }
536- } else {
537- // Auto-detect Podman if not specified in environment
538- isPodman, podmanSocket := docker.DetectPodman()
539- if isPodman {
540- config.ContainerEngine.Podman = true
541- config.ContainerEngine.PodmanSock = podmanSocket
542- if printLogs {
543- logger.Info("Automatically detected Podman installation",
544- "using_podman", true,
545- "socket", podmanSocket)
546- }
547- }
548-
549- // If we're in a container, check for special socket naming patterns
550- if docker.IsRunningInContainer() && fileExists(config.ContainerEngine.Sock) {
551- // Check if socket path contains "podman" which indicates it might be a podman socket
552- if strings.Contains(config.ContainerEngine.Sock, "podman") {
553- config.ContainerEngine.Podman = true
554- if printLogs {
555- logger.Info("Socket path contains 'podman', enabling Podman mode", "path", config.ContainerEngine.Sock)
556- }
557- }
558- }
559- }
560- if val := os.Getenv("GORDON_CONTAINER_NETWORK"); val != "" {
561- config.ContainerEngine.Network = val
562- if printLogs {
563- logger.Info("Using environment variable GORDON_CONTAINER_NETWORK", "value", val)
564- }
565- }
566-
567- // Use Podman socket if Podman is enabled and no Docker socket is specified
568- if config.ContainerEngine.Podman && config.ContainerEngine.Sock == "" && config.ContainerEngine.PodmanSock != "" {
569- config.ContainerEngine.Sock = config.ContainerEngine.PodmanSock
570- if printLogs {
571- logger.Info("Setting ContainerEngine.Sock to PodmanSock value", "value", config.ContainerEngine.Sock)
572- }
573- }
574-
575- // Additional debug info for container sockets
576- if docker.IsRunningInContainer() {
577- logger.Debug("Container socket configuration",
578- "sock", config.ContainerEngine.Sock,
579- "podmansock", config.ContainerEngine.PodmanSock,
580- "podman", config.ContainerEngine.Podman,
581- "sock_exists", fileExists(config.ContainerEngine.Sock),
582- "podmansock_exists", fileExists(config.ContainerEngine.PodmanSock))
583-
584- // Special handling for container environment to ensure sockets are properly set
585- // If no socket is configured but default socket path exists, use it
586- if config.ContainerEngine.Sock == "" && fileExists(sock) {
587- config.ContainerEngine.Sock = sock
588- logger.Info("Found default Docker socket in container, using it", "path", sock)
589- }
590-
591- // If podman socket exists but podman mode is not enabled, check if we should enable it
592- if fileExists(podmansock) && !config.ContainerEngine.Podman {
593- // If the main socket path contains "podman", enable podman mode
594- if strings.Contains(config.ContainerEngine.Sock, "podman") {
595- config.ContainerEngine.Podman = true
596- logger.Info("Socket path suggests Podman usage, enabling Podman mode", "path", config.ContainerEngine.Sock)
597- }
598- }
599- }
600-
601- // Reverse Proxy Configuration
602- if val := os.Getenv("GORDON_PROXY_PORT"); val != "" {
603- config.ReverseProxy.Port = val
604- if printLogs {
605- logger.Info("Using environment variable GORDON_PROXY_PORT", "value", val)
606- }
607- }
608- if val := os.Getenv("GORDON_PROXY_HTTP_PORT"); val != "" {
609- config.ReverseProxy.HttpPort = val
610- if printLogs {
611- logger.Info("Using environment variable GORDON_PROXY_HTTP_PORT", "value", val)
612- }
613- }
614-
615- // New environment variables for reverse proxy port control
616- if val := os.Getenv("GORDON_USE_FALLBACK_BINDING"); val != "" {
617- // This is handled at runtime in the proxy server but we log it here
618- if printLogs {
619- logger.Info("Found environment variable GORDON_USE_FALLBACK_BINDING", "value", val)
620- }
621- }
622-
623- // Handle GORDON_PROXY_ENABLED environment variable
624- if val := os.Getenv("GORDON_PROXY_ENABLED"); val != "" {
625- enabled, err := strconv.ParseBool(val)
626- if err == nil {
627- config.ReverseProxy.Enabled = enabled
628- }
629- if printLogs {
630- logger.Info("Using environment variable GORDON_PROXY_ENABLED", "value", config.ReverseProxy.Enabled)
631- }
632- }
633- if val := os.Getenv("GORDON_PROXY_CERT_DIR"); val != "" {
634- config.ReverseProxy.CertDir = val
635- if printLogs {
636- logger.Info("Using environment variable GORDON_PROXY_CERT_DIR", "value", val)
637- }
638- }
639- if val := os.Getenv("GORDON_PROXY_AUTO_RENEW"); val != "" {
640- config.ReverseProxy.AutoRenew = strings.ToLower(val) == "true"
641- if printLogs {
642- logger.Info("Using environment variable GORDON_PROXY_AUTO_RENEW", "value", config.ReverseProxy.AutoRenew)
643- }
644- }
645- if val := os.Getenv("GORDON_PROXY_RENEW_BEFORE"); val != "" {
646- if i, err := strconv.Atoi(val); err == nil {
647- config.ReverseProxy.RenewBefore = i
648- if printLogs {
649- logger.Info("Using environment variable GORDON_PROXY_RENEW_BEFORE", "value", i)
650- }
651- }
652- }
653- if val := os.Getenv("GORDON_PROXY_LETSENCRYPT_MODE"); val != "" {
654- config.ReverseProxy.LetsEncryptMode = val
655- if printLogs {
656- logger.Info("Using environment variable GORDON_PROXY_LETSENCRYPT_MODE", "value", val)
657- }
658- }
659- if val := os.Getenv("GORDON_PROXY_EMAIL"); val != "" {
660- config.ReverseProxy.Email = val
661- if printLogs {
662- logger.Info("Using environment variable GORDON_PROXY_EMAIL", "value", val)
663- }
664- }
665- if val := os.Getenv("GORDON_PROXY_CACHE_SIZE"); val != "" {
666- if i, err := strconv.Atoi(val); err == nil {
667- config.ReverseProxy.CacheSize = i
668- if printLogs {
669- logger.Info("Using environment variable GORDON_PROXY_CACHE_SIZE", "value", i)
670- }
671- }
672- }
673- if val := os.Getenv("GORDON_PROXY_GRACE_PERIOD"); val != "" {
674- i, err := strconv.Atoi(val)
675- if err == nil {
676- config.ReverseProxy.GracePeriod = i
677- }
678- logger.Info("Using environment variable GORDON_PROXY_GRACE_PERIOD", "value", i)
679- }
680-
681- // Handle GORDON_PROXY_ENABLE_HTTP_LOGS environment variable
682- if val := os.Getenv("GORDON_PROXY_ENABLE_HTTP_LOGS"); val != "" {
683- enableLogs, err := strconv.ParseBool(val)
684- if err == nil {
685- config.ReverseProxy.EnableHttpLogs = enableLogs
686- }
687- logger.Info("Using environment variable GORDON_PROXY_ENABLE_HTTP_LOGS", "value", config.ReverseProxy.EnableHttpLogs)
688- } else if val := os.Getenv("GORDON_PROXY_ENABLE_LOGS"); val != "" {
689- // For backward compatibility
690- enableLogs, err := strconv.ParseBool(val)
691- if err == nil {
692- config.ReverseProxy.EnableHttpLogs = enableLogs
693- }
694- logger.Info("Using environment variable GORDON_PROXY_ENABLE_LOGS (deprecated, use GORDON_PROXY_ENABLE_HTTP_LOGS)", "value", config.ReverseProxy.EnableHttpLogs)
695- }
696-
697- // Handle GORDON_PROXY_ENABLE_RATE_LIMIT environment variable
698- if val := os.Getenv("GORDON_PROXY_ENABLE_RATE_LIMIT"); val != "" {
699- enableRateLimit, err := strconv.ParseBool(val)
700- if err == nil {
701- config.ReverseProxy.EnableRateLimit = enableRateLimit
702- }
703- logger.Info("Using environment variable GORDON_PROXY_ENABLE_RATE_LIMIT", "value", config.ReverseProxy.EnableRateLimit)
704- } else if val := os.Getenv("GORDON_PROXY_DISABLE_RATE_LIMIT"); val != "" {
705- // For backward compatibility
706- disableRateLimit, err := strconv.ParseBool(val)
707- if err == nil {
708- // Invert the logic
709- config.ReverseProxy.EnableRateLimit = !disableRateLimit
710- }
711- logger.Info("Using environment variable GORDON_PROXY_DISABLE_RATE_LIMIT (deprecated, use GORDON_PROXY_ENABLE_RATE_LIMIT)", "value", !config.ReverseProxy.EnableRateLimit)
712- }
713-
714- // Handle GORDON_PROXY_DETECT_UPSTREAM environment variable
715- if val := os.Getenv("GORDON_PROXY_DETECT_UPSTREAM"); val != "" {
716- detectUpstream, err := strconv.ParseBool(val)
717- if err == nil {
718- config.ReverseProxy.DetectUpstreamProxy = detectUpstream
719- }
720- if printLogs {
721- logger.Info("Using environment variable GORDON_PROXY_DETECT_UPSTREAM", "value", config.ReverseProxy.DetectUpstreamProxy)
722- }
723- }
724-
725- // Handle GORDON_PROXY_SKIP_CERTIFICATES environment variable
726- if val := os.Getenv("GORDON_PROXY_SKIP_CERTIFICATES"); val != "" {
727- skipCerts, err := strconv.ParseBool(val)
728- if err == nil {
729- config.ReverseProxy.SkipCertificates = skipCerts
730- }
731- if printLogs {
732- logger.Info("Using environment variable GORDON_PROXY_SKIP_CERTIFICATES", "value", config.ReverseProxy.SkipCertificates)
733- }
734- }
735-}
736-
737417 func (config *Config) LoadConfig() (*Config, error) {
738418 // If we're in a non-interactive environment, use simplified config
739419 nonInteractiveEnv := os.Getenv("GORDON_NONINTERACTIVE")
@@ -781,10 +461,8 @@ func (config *Config) LoadConfig() (*Config, error) {
781461 Port: reverseProxyPort,
782462 HttpPort: httpPort,
783463 CertDir: certDir,
784- AutoRenew: autoRenew,
785464 RenewBefore: renewBefore,
786465 LetsEncryptMode: letsEncryptMode,
787- CacheSize: cacheSize,
788466 GracePeriod: gracePeriod,
789467 EnableHttpLogs: true,
790468 EnableRateLimit: false,
@@ -920,10 +598,8 @@ func (config *Config) LoadConfig() (*Config, error) {
920598 Port: reverseProxyPort,
921599 HttpPort: httpPort,
922600 CertDir: certDir,
923- AutoRenew: autoRenew,
924601 RenewBefore: renewBefore,
925602 LetsEncryptMode: letsEncryptMode,
926- CacheSize: cacheSize,
927603 GracePeriod: gracePeriod,
928604 EnableHttpLogs: true,
929605 EnableRateLimit: false,
@@ -1027,32 +703,6 @@ func (config *Config) LoadConfig() (*Config, error) {
1027703 logger.Debug("Log level set from configuration", "level", config.General.LogLevel)
1028704 }
1029705
1030- // --- Generate JWT Secret if missing ---
1031- if config.General.JwtToken == "" {
1032- logger.Warn("JWTSecret is missing from configuration, generating a new one...")
1033- newSecret, err := generateRandomSecret(32) // Generate a 256-bit key (32 bytes)
1034- if err != nil {
1035- logger.Error("Failed to generate JWT secret", "error", err)
1036- // Decide if this is a fatal error or if we can continue without JWT functionality
1037- // For now, let's return the error as JWT is crucial for API auth
1038- return nil, fmt.Errorf("failed to generate JWT secret: %w", err)
1039- }
1040- config.General.JwtToken = newSecret
1041- logger.Info("Generated new JWTSecret")
1042-
1043- // Save the config immediately to store the new secret
1044- err = config.SaveConfig()
1045- if err != nil {
1046- logger.Error("Failed to save configuration after generating JWT secret", "error", err)
1047- // Return error as saving failed
1048- return nil, fmt.Errorf("failed to save config with new JWT secret: %w", err)
1049- }
1050- logger.Info("Saved configuration with new JWTSecret")
1051- } else {
1052- logger.Debug("JWTSecret found in configuration")
1053- }
1054- // --- End JWT Secret Generation ---
1055-
1056706 logger.Debug("LoadConfig: Completed LoadConfig function")
1057707 return config, nil
1058708 }
@@ -7,27 +7,14 @@ import (
7 "os"7 "os"
8 "path/filepath"8 "path/filepath"
9 "runtime"9 "runtime"
10- "strconv"
11 "strings"10 "strings"
12 "sync"11 "sync"
13 12
14- "crypto/rand"
15- "encoding/hex"
16-
17 "github.com/bnema/gordon/pkg/docker"13 "github.com/bnema/gordon/pkg/docker"
18 "github.com/bnema/gordon/pkg/logger"14 "github.com/bnema/gordon/pkg/logger"
19 "github.com/bnema/gordon/pkg/parser"15 "github.com/bnema/gordon/pkg/parser"
20 )16 )
21 17
22-// generateRandomSecret generates a secure random string of the specified byte length, hex-encoded.
23-func generateRandomSecret(length int) (string, error) {
24- bytes := make([]byte, length)
25- if _, err := rand.Read(bytes); err != nil {
26- return "", err
27- }
28- return hex.EncodeToString(bytes), nil
29-}
30-
31 var (18 var (
32 globalConfig *Config19 globalConfig *Config
33 globalConfigOnce sync.Once20 globalConfigOnce sync.Once
@@ -94,9 +81,7 @@ type ReverseProxyConfig struct {
94 LetsEncryptMode string `yaml:"letsEncryptMode"` // staging or production81 LetsEncryptMode string `yaml:"letsEncryptMode"` // staging or production
95 SkipCertificates bool `yaml:"skipCertificates"` // Skip Let's Encrypt certificate acquisition when behind a TLS terminating proxy82 SkipCertificates bool `yaml:"skipCertificates"` // Skip Let's Encrypt certificate acquisition when behind a TLS terminating proxy
96 GracePeriod int `yaml:"gracePeriod"` // Shutdown grace period in seconds83 GracePeriod int `yaml:"gracePeriod"` // Shutdown grace period in seconds
97- AutoRenew bool `yaml:"autoRenew"` // Whether to automatically renew certificates
98 RenewBefore int `yaml:"renewBefore"` // Days before expiry to renew certificates84 RenewBefore int `yaml:"renewBefore"` // Days before expiry to renew certificates
99- CacheSize int `yaml:"cacheSize"` // Size of the certificate cache
100 EnableHttpLogs bool `yaml:"enableHttpLogs"` // Whether to enable HTTP request logging85 EnableHttpLogs bool `yaml:"enableHttpLogs"` // Whether to enable HTTP request logging
101 EnableRateLimit bool `yaml:"enableRateLimit"` // Whether to enable rate limiting middleware86 EnableRateLimit bool `yaml:"enableRateLimit"` // Whether to enable rate limiting middleware
102 DetectUpstreamProxy bool `yaml:"detectUpstreamProxy"` // Whether to detect and handle upstream TLS termination proxies87 DetectUpstreamProxy bool `yaml:"detectUpstreamProxy"` // Whether to detect and handle upstream TLS termination proxies
@@ -108,21 +93,24 @@ type ReverseProxyConfig struct {
108 93
109 // Default values94 // Default values
110 var (95 var (
111- sock = "/var/run/docker.sock"96+ sock = "/var/run/docker.sock"
112- podmansock = "/run/podman/podman.sock"97+ podmansock = "/run/podman/podman.sock"
113- reverseProxyPort = "443" // Changed to 443 for standard HTTPS98+ reverseProxyPort = "443" // Changed to 443 for standard HTTPS
114- httpPort = "80" // Standard HTTP port99+ httpPort = "80" // Standard HTTP port
115- certDir = "/certs"100+ certDir = "/certs"
116- letsEncryptMode = "staging"101+ letsEncryptMode = "staging"
117- autoRenew = true102+ proxyEnabled = true // Default to enabled for backward compatibility
118- proxyEnabled = true // Default to enabled for backward compatibility103+ renewBefore = 30 // days
119- renewBefore = 30 // days104+ gracePeriod = 30 // seconds
120- cacheSize = 1000 // entries105+ defaultLogLevel = "info" // Default log level
121- gracePeriod = 30 // seconds106+ disableRateLimit = false
122- defaultLogLevel = "info" // Default log level107+ detectUpstreamProxy = false // Default to disabled
123- disableRateLimit = false108+ skipCertificates = false // Default to disabled
124- detectUpstreamProxy = false // Default to disabled109+ enableRateLimit = false // Default to disabled
125- skipCertificates = false // Default to disabled110+ defaultChallengeType = "http-01"
111+ defaultHttpChallengePort = "80"
112+ defaultDnsChallengePropagationTimeout = 60 // seconds
113+ defaultDnsChallengePollingInterval = 5 // seconds
126 )114 )
127 115
128 // applyDefaultsToConfig applies default values to any fields that have zero values116 // applyDefaultsToConfig applies default values to any fields that have zero values
@@ -208,11 +196,6 @@ func applyDefaultsToConfig(config *Config) bool {
208 logger.Debug("Applied default value for ReverseProxy.LetsEncryptMode", "value", letsEncryptMode)196 logger.Debug("Applied default value for ReverseProxy.LetsEncryptMode", "value", letsEncryptMode)
209 defaultsApplied = true197 defaultsApplied = true
210 }198 }
211- if config.ReverseProxy.CacheSize == 0 {
212- config.ReverseProxy.CacheSize = cacheSize
213- logger.Debug("Applied default value for ReverseProxy.CacheSize", "value", cacheSize)
214- defaultsApplied = true
215- }
216 if config.ReverseProxy.GracePeriod == 0 {199 if config.ReverseProxy.GracePeriod == 0 {
217 config.ReverseProxy.GracePeriod = gracePeriod200 config.ReverseProxy.GracePeriod = gracePeriod
218 logger.Debug("Applied default value for ReverseProxy.GracePeriod", "value", gracePeriod)201 logger.Debug("Applied default value for ReverseProxy.GracePeriod", "value", gracePeriod)
@@ -266,34 +249,6 @@ func applyDefaultsToConfig(config *Config) bool {
266 defaultsApplied = true249 defaultsApplied = true
267 }250 }
268 251
269- // Handle autoRenew
270- configFilePath = filepath.Join(getConfigDirMustExist(), "config.yml")
271- if fileExists(configFilePath) {
272- yamlContent, err := os.ReadFile(configFilePath)
273- if err == nil {
274- // Check if "autoRenew" is explicitly mentioned in the config file
275- if !strings.Contains(string(yamlContent), "autoRenew:") {
276- config.ReverseProxy.AutoRenew = autoRenew
277- logger.Debug("Applied default value for ReverseProxy.AutoRenew", "value", autoRenew)
278- defaultsApplied = true
279- } else {
280- logger.Debug("Keeping explicit value for ReverseProxy.AutoRenew", "value", config.ReverseProxy.AutoRenew)
281- }
282- } else {
283- // If we can't read the file for some reason, apply the default
284- if !config.ReverseProxy.AutoRenew {
285- config.ReverseProxy.AutoRenew = autoRenew
286- logger.Debug("Applied default value for ReverseProxy.AutoRenew", "value", autoRenew)
287- defaultsApplied = true
288- }
289- }
290- } else {
291- // If config file doesn't exist yet, set the default
292- config.ReverseProxy.AutoRenew = autoRenew
293- logger.Debug("Applied default value for ReverseProxy.AutoRenew", "value", autoRenew)
294- defaultsApplied = true
295- }
296-
297 return defaultsApplied252 return defaultsApplied
298 }253 }
299 254
@@ -447,6 +402,7 @@ func getConfigDir() (string, error) {
447 return configDir, nil402 return configDir, nil
448 }403 }
449 404
405+// readAndUnmarshalConfig reads and unmarshals a configuration file using the provided filesystem and path.
450 func readAndUnmarshalConfig(fs fs.FS, filePath string, config *Config) error {406 func readAndUnmarshalConfig(fs fs.FS, filePath string, config *Config) error {
451 logger.Debug("readAndUnmarshalConfig: Starting to read and unmarshal config file", "path", filePath)407 logger.Debug("readAndUnmarshalConfig: Starting to read and unmarshal config file", "path", filePath)
452 err := parser.ParseYAMLFile(fs, filePath, config)408 err := parser.ParseYAMLFile(fs, filePath, config)
@@ -458,282 +414,6 @@ func readAndUnmarshalConfig(fs fs.FS, filePath string, config *Config) error {
458 return nil414 return nil
459 }415 }
460 416
461-// loadConfigFromEnv loads configuration from environment variables
462-func loadConfigFromEnv(config *Config, printLogs bool) {
463- // General Configuration
464- if val := os.Getenv("GORDON_STORAGE_DIR"); val != "" {
465- config.General.StorageDir = val
466- if printLogs {
467- logger.Info("Using environment variable GORDON_STORAGE_DIR", "value", val)
468- }
469- }
470- // Removed GORDON_TOKEN handling
471- if val := os.Getenv("GORDON_LOG_LEVEL"); val != "" {
472- config.General.LogLevel = val
473- if printLogs {
474- logger.Info("Using environment variable GORDON_LOG_LEVEL", "value", val)
475- }
476- }
477-
478- // HTTP Configuration
479- if val := os.Getenv("GORDON_HTTP_PORT"); val != "" {
480- config.Http.Port = val
481- if printLogs {
482- logger.Info("Using environment variable GORDON_HTTP_PORT", "value", val)
483- }
484- }
485- if val := os.Getenv("GORDON_HTTP_DOMAIN"); val != "" {
486- config.Http.Domain = val
487- if printLogs {
488- logger.Info("Using environment variable GORDON_HTTP_DOMAIN", "value", val)
489- }
490- }
491- if val := os.Getenv("GORDON_HTTP_SUBDOMAIN"); val != "" {
492- config.Http.SubDomain = val
493- if printLogs {
494- logger.Info("Using environment variable GORDON_HTTP_SUBDOMAIN", "value", val)
495- }
496- }
497- if val := os.Getenv("GORDON_HTTP_BACKEND_URL"); val != "" {
498- config.Http.BackendURL = val
499- if printLogs {
500- logger.Info("Using environment variable GORDON_HTTP_BACKEND_URL", "value", val)
501- }
502- }
503- if val := os.Getenv("GORDON_HTTP_HTTPS"); val != "" {
504- config.Http.Https = strings.ToLower(val) == "true"
505- if printLogs {
506- logger.Info("Using environment variable GORDON_HTTP_HTTPS", "value", config.Http.Https)
507- }
508- }
509-
510- // Admin Configuration
511- if val := os.Getenv("GORDON_ADMIN_PATH"); val != "" {
512- config.Admin.Path = val
513- if printLogs {
514- logger.Info("Using environment variable GORDON_ADMIN_PATH", "value", val)
515- }
516- }
517-
518- // Container Engine Configuration
519- if val := os.Getenv("GORDON_CONTAINER_SOCK"); val != "" {
520- config.ContainerEngine.Sock = val
521- if printLogs {
522- logger.Info("Using environment variable GORDON_CONTAINER_SOCK", "value", val)
523- }
524- }
525- if val := os.Getenv("GORDON_CONTAINER_PODMANSOCK"); val != "" {
526- config.ContainerEngine.PodmanSock = val
527- if printLogs {
528- logger.Info("Using environment variable GORDON_CONTAINER_PODMANSOCK", "value", val)
529- }
530- }
531- if val := os.Getenv("GORDON_CONTAINER_PODMAN"); val != "" {
532- config.ContainerEngine.Podman = strings.ToLower(val) == "true"
533- if printLogs {
534- logger.Info("Using environment variable GORDON_CONTAINER_PODMAN", "value", config.ContainerEngine.Podman)
535- }
536- } else {
537- // Auto-detect Podman if not specified in environment
538- isPodman, podmanSocket := docker.DetectPodman()
539- if isPodman {
540- config.ContainerEngine.Podman = true
541- config.ContainerEngine.PodmanSock = podmanSocket
542- if printLogs {
543- logger.Info("Automatically detected Podman installation",
544- "using_podman", true,
545- "socket", podmanSocket)
546- }
547- }
548-
549- // If we're in a container, check for special socket naming patterns
550- if docker.IsRunningInContainer() && fileExists(config.ContainerEngine.Sock) {
551- // Check if socket path contains "podman" which indicates it might be a podman socket
552- if strings.Contains(config.ContainerEngine.Sock, "podman") {
553- config.ContainerEngine.Podman = true
554- if printLogs {
555- logger.Info("Socket path contains 'podman', enabling Podman mode", "path", config.ContainerEngine.Sock)
556- }
557- }
558- }
559- }
560- if val := os.Getenv("GORDON_CONTAINER_NETWORK"); val != "" {
561- config.ContainerEngine.Network = val
562- if printLogs {
563- logger.Info("Using environment variable GORDON_CONTAINER_NETWORK", "value", val)
564- }
565- }
566-
567- // Use Podman socket if Podman is enabled and no Docker socket is specified
568- if config.ContainerEngine.Podman && config.ContainerEngine.Sock == "" && config.ContainerEngine.PodmanSock != "" {
569- config.ContainerEngine.Sock = config.ContainerEngine.PodmanSock
570- if printLogs {
571- logger.Info("Setting ContainerEngine.Sock to PodmanSock value", "value", config.ContainerEngine.Sock)
572- }
573- }
574-
575- // Additional debug info for container sockets
576- if docker.IsRunningInContainer() {
577- logger.Debug("Container socket configuration",
578- "sock", config.ContainerEngine.Sock,
579- "podmansock", config.ContainerEngine.PodmanSock,
580- "podman", config.ContainerEngine.Podman,
581- "sock_exists", fileExists(config.ContainerEngine.Sock),
582- "podmansock_exists", fileExists(config.ContainerEngine.PodmanSock))
583-
584- // Special handling for container environment to ensure sockets are properly set
585- // If no socket is configured but default socket path exists, use it
586- if config.ContainerEngine.Sock == "" && fileExists(sock) {
587- config.ContainerEngine.Sock = sock
588- logger.Info("Found default Docker socket in container, using it", "path", sock)
589- }
590-
591- // If podman socket exists but podman mode is not enabled, check if we should enable it
592- if fileExists(podmansock) && !config.ContainerEngine.Podman {
593- // If the main socket path contains "podman", enable podman mode
594- if strings.Contains(config.ContainerEngine.Sock, "podman") {
595- config.ContainerEngine.Podman = true
596- logger.Info("Socket path suggests Podman usage, enabling Podman mode", "path", config.ContainerEngine.Sock)
597- }
598- }
599- }
600-
601- // Reverse Proxy Configuration
602- if val := os.Getenv("GORDON_PROXY_PORT"); val != "" {
603- config.ReverseProxy.Port = val
604- if printLogs {
605- logger.Info("Using environment variable GORDON_PROXY_PORT", "value", val)
606- }
607- }
608- if val := os.Getenv("GORDON_PROXY_HTTP_PORT"); val != "" {
609- config.ReverseProxy.HttpPort = val
610- if printLogs {
611- logger.Info("Using environment variable GORDON_PROXY_HTTP_PORT", "value", val)
612- }
613- }
614-
615- // New environment variables for reverse proxy port control
616- if val := os.Getenv("GORDON_USE_FALLBACK_BINDING"); val != "" {
617- // This is handled at runtime in the proxy server but we log it here
618- if printLogs {
619- logger.Info("Found environment variable GORDON_USE_FALLBACK_BINDING", "value", val)
620- }
621- }
622-
623- // Handle GORDON_PROXY_ENABLED environment variable
624- if val := os.Getenv("GORDON_PROXY_ENABLED"); val != "" {
625- enabled, err := strconv.ParseBool(val)
626- if err == nil {
627- config.ReverseProxy.Enabled = enabled
628- }
629- if printLogs {
630- logger.Info("Using environment variable GORDON_PROXY_ENABLED", "value", config.ReverseProxy.Enabled)
631- }
632- }
633- if val := os.Getenv("GORDON_PROXY_CERT_DIR"); val != "" {
634- config.ReverseProxy.CertDir = val
635- if printLogs {
636- logger.Info("Using environment variable GORDON_PROXY_CERT_DIR", "value", val)
637- }
638- }
639- if val := os.Getenv("GORDON_PROXY_AUTO_RENEW"); val != "" {
640- config.ReverseProxy.AutoRenew = strings.ToLower(val) == "true"
641- if printLogs {
642- logger.Info("Using environment variable GORDON_PROXY_AUTO_RENEW", "value", config.ReverseProxy.AutoRenew)
643- }
644- }
645- if val := os.Getenv("GORDON_PROXY_RENEW_BEFORE"); val != "" {
646- if i, err := strconv.Atoi(val); err == nil {
647- config.ReverseProxy.RenewBefore = i
648- if printLogs {
649- logger.Info("Using environment variable GORDON_PROXY_RENEW_BEFORE", "value", i)
650- }
651- }
652- }
653- if val := os.Getenv("GORDON_PROXY_LETSENCRYPT_MODE"); val != "" {
654- config.ReverseProxy.LetsEncryptMode = val
655- if printLogs {
656- logger.Info("Using environment variable GORDON_PROXY_LETSENCRYPT_MODE", "value", val)
657- }
658- }
659- if val := os.Getenv("GORDON_PROXY_EMAIL"); val != "" {
660- config.ReverseProxy.Email = val
661- if printLogs {
662- logger.Info("Using environment variable GORDON_PROXY_EMAIL", "value", val)
663- }
664- }
665- if val := os.Getenv("GORDON_PROXY_CACHE_SIZE"); val != "" {
666- if i, err := strconv.Atoi(val); err == nil {
667- config.ReverseProxy.CacheSize = i
668- if printLogs {
669- logger.Info("Using environment variable GORDON_PROXY_CACHE_SIZE", "value", i)
670- }
671- }
672- }
673- if val := os.Getenv("GORDON_PROXY_GRACE_PERIOD"); val != "" {
674- i, err := strconv.Atoi(val)
675- if err == nil {
676- config.ReverseProxy.GracePeriod = i
677- }
678- logger.Info("Using environment variable GORDON_PROXY_GRACE_PERIOD", "value", i)
679- }
680-
681- // Handle GORDON_PROXY_ENABLE_HTTP_LOGS environment variable
682- if val := os.Getenv("GORDON_PROXY_ENABLE_HTTP_LOGS"); val != "" {
683- enableLogs, err := strconv.ParseBool(val)
684- if err == nil {
685- config.ReverseProxy.EnableHttpLogs = enableLogs
686- }
687- logger.Info("Using environment variable GORDON_PROXY_ENABLE_HTTP_LOGS", "value", config.ReverseProxy.EnableHttpLogs)
688- } else if val := os.Getenv("GORDON_PROXY_ENABLE_LOGS"); val != "" {
689- // For backward compatibility
690- enableLogs, err := strconv.ParseBool(val)
691- if err == nil {
692- config.ReverseProxy.EnableHttpLogs = enableLogs
693- }
694- logger.Info("Using environment variable GORDON_PROXY_ENABLE_LOGS (deprecated, use GORDON_PROXY_ENABLE_HTTP_LOGS)", "value", config.ReverseProxy.EnableHttpLogs)
695- }
696-
697- // Handle GORDON_PROXY_ENABLE_RATE_LIMIT environment variable
698- if val := os.Getenv("GORDON_PROXY_ENABLE_RATE_LIMIT"); val != "" {
699- enableRateLimit, err := strconv.ParseBool(val)
700- if err == nil {
701- config.ReverseProxy.EnableRateLimit = enableRateLimit
702- }
703- logger.Info("Using environment variable GORDON_PROXY_ENABLE_RATE_LIMIT", "value", config.ReverseProxy.EnableRateLimit)
704- } else if val := os.Getenv("GORDON_PROXY_DISABLE_RATE_LIMIT"); val != "" {
705- // For backward compatibility
706- disableRateLimit, err := strconv.ParseBool(val)
707- if err == nil {
708- // Invert the logic
709- config.ReverseProxy.EnableRateLimit = !disableRateLimit
710- }
711- logger.Info("Using environment variable GORDON_PROXY_DISABLE_RATE_LIMIT (deprecated, use GORDON_PROXY_ENABLE_RATE_LIMIT)", "value", !config.ReverseProxy.EnableRateLimit)
712- }
713-
714- // Handle GORDON_PROXY_DETECT_UPSTREAM environment variable
715- if val := os.Getenv("GORDON_PROXY_DETECT_UPSTREAM"); val != "" {
716- detectUpstream, err := strconv.ParseBool(val)
717- if err == nil {
718- config.ReverseProxy.DetectUpstreamProxy = detectUpstream
719- }
720- if printLogs {
721- logger.Info("Using environment variable GORDON_PROXY_DETECT_UPSTREAM", "value", config.ReverseProxy.DetectUpstreamProxy)
722- }
723- }
724-
725- // Handle GORDON_PROXY_SKIP_CERTIFICATES environment variable
726- if val := os.Getenv("GORDON_PROXY_SKIP_CERTIFICATES"); val != "" {
727- skipCerts, err := strconv.ParseBool(val)
728- if err == nil {
729- config.ReverseProxy.SkipCertificates = skipCerts
730- }
731- if printLogs {
732- logger.Info("Using environment variable GORDON_PROXY_SKIP_CERTIFICATES", "value", config.ReverseProxy.SkipCertificates)
733- }
734- }
735-}
736-
737 func (config *Config) LoadConfig() (*Config, error) {417 func (config *Config) LoadConfig() (*Config, error) {
738 // If we're in a non-interactive environment, use simplified config418 // If we're in a non-interactive environment, use simplified config
739 nonInteractiveEnv := os.Getenv("GORDON_NONINTERACTIVE")419 nonInteractiveEnv := os.Getenv("GORDON_NONINTERACTIVE")
@@ -781,10 +461,8 @@ func (config *Config) LoadConfig() (*Config, error) {
781 Port: reverseProxyPort,461 Port: reverseProxyPort,
782 HttpPort: httpPort,462 HttpPort: httpPort,
783 CertDir: certDir,463 CertDir: certDir,
784- AutoRenew: autoRenew,
785 RenewBefore: renewBefore,464 RenewBefore: renewBefore,
786 LetsEncryptMode: letsEncryptMode,465 LetsEncryptMode: letsEncryptMode,
787- CacheSize: cacheSize,
788 GracePeriod: gracePeriod,466 GracePeriod: gracePeriod,
789 EnableHttpLogs: true,467 EnableHttpLogs: true,
790 EnableRateLimit: false,468 EnableRateLimit: false,
@@ -920,10 +598,8 @@ func (config *Config) LoadConfig() (*Config, error) {
920 Port: reverseProxyPort,598 Port: reverseProxyPort,
921 HttpPort: httpPort,599 HttpPort: httpPort,
922 CertDir: certDir,600 CertDir: certDir,
923- AutoRenew: autoRenew,
924 RenewBefore: renewBefore,601 RenewBefore: renewBefore,
925 LetsEncryptMode: letsEncryptMode,602 LetsEncryptMode: letsEncryptMode,
926- CacheSize: cacheSize,
927 GracePeriod: gracePeriod,603 GracePeriod: gracePeriod,
928 EnableHttpLogs: true,604 EnableHttpLogs: true,
929 EnableRateLimit: false,605 EnableRateLimit: false,
@@ -1027,32 +703,6 @@ func (config *Config) LoadConfig() (*Config, error) {
1027 logger.Debug("Log level set from configuration", "level", config.General.LogLevel)703 logger.Debug("Log level set from configuration", "level", config.General.LogLevel)
1028 }704 }
1029 705
1030- // --- Generate JWT Secret if missing ---
1031- if config.General.JwtToken == "" {
1032- logger.Warn("JWTSecret is missing from configuration, generating a new one...")
1033- newSecret, err := generateRandomSecret(32) // Generate a 256-bit key (32 bytes)
1034- if err != nil {
1035- logger.Error("Failed to generate JWT secret", "error", err)
1036- // Decide if this is a fatal error or if we can continue without JWT functionality
1037- // For now, let's return the error as JWT is crucial for API auth
1038- return nil, fmt.Errorf("failed to generate JWT secret: %w", err)
1039- }
1040- config.General.JwtToken = newSecret
1041- logger.Info("Generated new JWTSecret")
1042-
1043- // Save the config immediately to store the new secret
1044- err = config.SaveConfig()
1045- if err != nil {
1046- logger.Error("Failed to save configuration after generating JWT secret", "error", err)
1047- // Return error as saving failed
1048- return nil, fmt.Errorf("failed to save config with new JWT secret: %w", err)
1049- }
1050- logger.Info("Saved configuration with new JWTSecret")
1051- } else {
1052- logger.Debug("JWTSecret found in configuration")
1053- }
1054- // --- End JWT Secret Generation ---
1055-
1056 logger.Debug("LoadConfig: Completed LoadConfig function")706 logger.Debug("LoadConfig: Completed LoadConfig function")
1057 return config, nil707 return config, nil
1058 }708 }
A internal/common/init_env.go +272 -0
@@ -0,0 +1,272 @@
1+package common
2+
3+import (
4+ "os"
5+ "strconv"
6+ "strings"
7+
8+ "github.com/bnema/gordon/pkg/docker"
9+ "github.com/bnema/gordon/pkg/logger"
10+)
11+
12+// loadConfigFromEnv loads configuration from environment variables
13+func loadConfigFromEnv(config *Config, printLogs bool) {
14+ // General Configuration
15+ if val := os.Getenv("GORDON_STORAGE_DIR"); val != "" {
16+ config.General.StorageDir = val
17+ if printLogs {
18+ logger.Info("Using environment variable GORDON_STORAGE_DIR", "value", val)
19+ }
20+ }
21+ // Removed GORDON_TOKEN handling
22+ if val := os.Getenv("GORDON_LOG_LEVEL"); val != "" {
23+ config.General.LogLevel = val
24+ if printLogs {
25+ logger.Info("Using environment variable GORDON_LOG_LEVEL", "value", val)
26+ }
27+ }
28+
29+ // HTTP Configuration
30+ if val := os.Getenv("GORDON_HTTP_PORT"); val != "" {
31+ config.Http.Port = val
32+ if printLogs {
33+ logger.Info("Using environment variable GORDON_HTTP_PORT", "value", val)
34+ }
35+ }
36+ if val := os.Getenv("GORDON_HTTP_DOMAIN"); val != "" {
37+ config.Http.Domain = val
38+ if printLogs {
39+ logger.Info("Using environment variable GORDON_HTTP_DOMAIN", "value", val)
40+ }
41+ }
42+ if val := os.Getenv("GORDON_HTTP_SUBDOMAIN"); val != "" {
43+ config.Http.SubDomain = val
44+ if printLogs {
45+ logger.Info("Using environment variable GORDON_HTTP_SUBDOMAIN", "value", val)
46+ }
47+ }
48+ if val := os.Getenv("GORDON_HTTP_BACKEND_URL"); val != "" {
49+ config.Http.BackendURL = val
50+ if printLogs {
51+ logger.Info("Using environment variable GORDON_HTTP_BACKEND_URL", "value", val)
52+ }
53+ }
54+ if val := os.Getenv("GORDON_HTTP_HTTPS"); val != "" {
55+ config.Http.Https = strings.ToLower(val) == "true"
56+ if printLogs {
57+ logger.Info("Using environment variable GORDON_HTTP_HTTPS", "value", config.Http.Https)
58+ }
59+ }
60+
61+ // Admin Configuration
62+ if val := os.Getenv("GORDON_ADMIN_PATH"); val != "" {
63+ config.Admin.Path = val
64+ if printLogs {
65+ logger.Info("Using environment variable GORDON_ADMIN_PATH", "value", val)
66+ }
67+ }
68+
69+ // Container Engine Configuration
70+ if val := os.Getenv("GORDON_CONTAINER_SOCK"); val != "" {
71+ config.ContainerEngine.Sock = val
72+ if printLogs {
73+ logger.Info("Using environment variable GORDON_CONTAINER_SOCK", "value", val)
74+ }
75+ }
76+ if val := os.Getenv("GORDON_CONTAINER_PODMANSOCK"); val != "" {
77+ config.ContainerEngine.PodmanSock = val
78+ if printLogs {
79+ logger.Info("Using environment variable GORDON_CONTAINER_PODMANSOCK", "value", val)
80+ }
81+ }
82+ if val := os.Getenv("GORDON_CONTAINER_PODMAN"); val != "" {
83+ config.ContainerEngine.Podman = strings.ToLower(val) == "true"
84+ if printLogs {
85+ logger.Info("Using environment variable GORDON_CONTAINER_PODMAN", "value", config.ContainerEngine.Podman)
86+ }
87+ } else {
88+ // Auto-detect Podman if not specified in environment
89+ isPodman, podmanSocket := docker.DetectPodman()
90+ if isPodman {
91+ config.ContainerEngine.Podman = true
92+ config.ContainerEngine.PodmanSock = podmanSocket
93+ if printLogs {
94+ logger.Info("Automatically detected Podman installation",
95+ "using_podman", true,
96+ "socket", podmanSocket)
97+ }
98+ }
99+
100+ // If we're in a container, check for special socket naming patterns
101+ if docker.IsRunningInContainer() && fileExists(config.ContainerEngine.Sock) {
102+ // Check if socket path contains "podman" which indicates it might be a podman socket
103+ if strings.Contains(config.ContainerEngine.Sock, "podman") {
104+ config.ContainerEngine.Podman = true
105+ if printLogs {
106+ logger.Info("Socket path contains 'podman', enabling Podman mode", "path", config.ContainerEngine.Sock)
107+ }
108+ }
109+ }
110+ }
111+ if val := os.Getenv("GORDON_CONTAINER_NETWORK"); val != "" {
112+ config.ContainerEngine.Network = val
113+ if printLogs {
114+ logger.Info("Using environment variable GORDON_CONTAINER_NETWORK", "value", val)
115+ }
116+ }
117+
118+ // Use Podman socket if Podman is enabled and no Docker socket is specified
119+ if config.ContainerEngine.Podman && config.ContainerEngine.Sock == "" && config.ContainerEngine.PodmanSock != "" {
120+ config.ContainerEngine.Sock = config.ContainerEngine.PodmanSock
121+ if printLogs {
122+ logger.Info("Setting ContainerEngine.Sock to PodmanSock value", "value", config.ContainerEngine.Sock)
123+ }
124+ }
125+
126+ // Additional debug info for container sockets
127+ if docker.IsRunningInContainer() {
128+ logger.Debug("Container socket configuration",
129+ "sock", config.ContainerEngine.Sock,
130+ "podmansock", config.ContainerEngine.PodmanSock,
131+ "podman", config.ContainerEngine.Podman,
132+ "sock_exists", fileExists(config.ContainerEngine.Sock),
133+ "podmansock_exists", fileExists(config.ContainerEngine.PodmanSock))
134+
135+ // Special handling for container environment to ensure sockets are properly set
136+ // If no socket is configured but default socket path exists, use it
137+ if config.ContainerEngine.Sock == "" && fileExists(sock) {
138+ config.ContainerEngine.Sock = sock
139+ logger.Info("Found default Docker socket in container, using it", "path", sock)
140+ }
141+
142+ // If podman socket exists but podman mode is not enabled, check if we should enable it
143+ if fileExists(podmansock) && !config.ContainerEngine.Podman {
144+ // If the main socket path contains "podman", enable podman mode
145+ if strings.Contains(config.ContainerEngine.Sock, "podman") {
146+ config.ContainerEngine.Podman = true
147+ logger.Info("Socket path suggests Podman usage, enabling Podman mode", "path", config.ContainerEngine.Sock)
148+ }
149+ }
150+ }
151+
152+ // Reverse Proxy Configuration
153+ if val := os.Getenv("GORDON_PROXY_PORT"); val != "" {
154+ config.ReverseProxy.Port = val
155+ if printLogs {
156+ logger.Info("Using environment variable GORDON_PROXY_PORT", "value", val)
157+ }
158+ }
159+ if val := os.Getenv("GORDON_PROXY_HTTP_PORT"); val != "" {
160+ config.ReverseProxy.HttpPort = val
161+ if printLogs {
162+ logger.Info("Using environment variable GORDON_PROXY_HTTP_PORT", "value", val)
163+ }
164+ }
165+
166+ // New environment variables for reverse proxy port control
167+ if val := os.Getenv("GORDON_USE_FALLBACK_BINDING"); val != "" {
168+ // This is handled at runtime in the proxy server but we log it here
169+ if printLogs {
170+ logger.Info("Found environment variable GORDON_USE_FALLBACK_BINDING", "value", val)
171+ }
172+ }
173+
174+ // Handle GORDON_PROXY_ENABLED environment variable
175+ if val := os.Getenv("GORDON_PROXY_ENABLED"); val != "" {
176+ enabled, err := strconv.ParseBool(val)
177+ if err == nil {
178+ config.ReverseProxy.Enabled = enabled
179+ }
180+ if printLogs {
181+ logger.Info("Using environment variable GORDON_PROXY_ENABLED", "value", config.ReverseProxy.Enabled)
182+ }
183+ }
184+ if val := os.Getenv("GORDON_PROXY_CERT_DIR"); val != "" {
185+ config.ReverseProxy.CertDir = val
186+ if printLogs {
187+ logger.Info("Using environment variable GORDON_PROXY_CERT_DIR", "value", val)
188+ }
189+ }
190+ if val := os.Getenv("GORDON_PROXY_RENEW_BEFORE"); val != "" {
191+ if i, err := strconv.Atoi(val); err == nil {
192+ config.ReverseProxy.RenewBefore = i
193+ if printLogs {
194+ logger.Info("Using environment variable GORDON_PROXY_RENEW_BEFORE", "value", i)
195+ }
196+ }
197+ }
198+ if val := os.Getenv("GORDON_PROXY_LETSENCRYPT_MODE"); val != "" {
199+ config.ReverseProxy.LetsEncryptMode = val
200+ if printLogs {
201+ logger.Info("Using environment variable GORDON_PROXY_LETSENCRYPT_MODE", "value", val)
202+ }
203+ }
204+ if val := os.Getenv("GORDON_PROXY_EMAIL"); val != "" {
205+ config.ReverseProxy.Email = val
206+ if printLogs {
207+ logger.Info("Using environment variable GORDON_PROXY_EMAIL", "value", val)
208+ }
209+ }
210+ if val := os.Getenv("GORDON_PROXY_GRACE_PERIOD"); val != "" {
211+ i, err := strconv.Atoi(val)
212+ if err == nil {
213+ config.ReverseProxy.GracePeriod = i
214+ }
215+ logger.Info("Using environment variable GORDON_PROXY_GRACE_PERIOD", "value", i)
216+ }
217+
218+ // Handle GORDON_PROXY_ENABLE_HTTP_LOGS environment variable
219+ if val := os.Getenv("GORDON_PROXY_ENABLE_HTTP_LOGS"); val != "" {
220+ enableLogs, err := strconv.ParseBool(val)
221+ if err == nil {
222+ config.ReverseProxy.EnableHttpLogs = enableLogs
223+ }
224+ logger.Info("Using environment variable GORDON_PROXY_ENABLE_HTTP_LOGS", "value", config.ReverseProxy.EnableHttpLogs)
225+ } else if val := os.Getenv("GORDON_PROXY_ENABLE_LOGS"); val != "" {
226+ // For backward compatibility
227+ enableLogs, err := strconv.ParseBool(val)
228+ if err == nil {
229+ config.ReverseProxy.EnableHttpLogs = enableLogs
230+ }
231+ logger.Info("Using environment variable GORDON_PROXY_ENABLE_LOGS (deprecated, use GORDON_PROXY_ENABLE_HTTP_LOGS)", "value", config.ReverseProxy.EnableHttpLogs)
232+ }
233+
234+ // Handle GORDON_PROXY_ENABLE_RATE_LIMIT environment variable
235+ if val := os.Getenv("GORDON_PROXY_ENABLE_RATE_LIMIT"); val != "" {
236+ enableRateLimit, err := strconv.ParseBool(val)
237+ if err == nil {
238+ config.ReverseProxy.EnableRateLimit = enableRateLimit
239+ }
240+ logger.Info("Using environment variable GORDON_PROXY_ENABLE_RATE_LIMIT", "value", config.ReverseProxy.EnableRateLimit)
241+ } else if val := os.Getenv("GORDON_PROXY_DISABLE_RATE_LIMIT"); val != "" {
242+ // For backward compatibility
243+ disableRateLimit, err := strconv.ParseBool(val)
244+ if err == nil {
245+ // Invert the logic
246+ config.ReverseProxy.EnableRateLimit = !disableRateLimit
247+ }
248+ logger.Info("Using environment variable GORDON_PROXY_DISABLE_RATE_LIMIT (deprecated, use GORDON_PROXY_ENABLE_RATE_LIMIT)", "value", !config.ReverseProxy.EnableRateLimit)
249+ }
250+
251+ // Handle GORDON_PROXY_DETECT_UPSTREAM environment variable
252+ if val := os.Getenv("GORDON_PROXY_DETECT_UPSTREAM"); val != "" {
253+ detectUpstream, err := strconv.ParseBool(val)
254+ if err == nil {
255+ config.ReverseProxy.DetectUpstreamProxy = detectUpstream
256+ }
257+ if printLogs {
258+ logger.Info("Using environment variable GORDON_PROXY_DETECT_UPSTREAM", "value", config.ReverseProxy.DetectUpstreamProxy)
259+ }
260+ }
261+
262+ // Handle GORDON_PROXY_SKIP_CERTIFICATES environment variable
263+ if val := os.Getenv("GORDON_PROXY_SKIP_CERTIFICATES"); val != "" {
264+ skipCerts, err := strconv.ParseBool(val)
265+ if err == nil {
266+ config.ReverseProxy.SkipCertificates = skipCerts
267+ }
268+ if printLogs {
269+ logger.Info("Using environment variable GORDON_PROXY_SKIP_CERTIFICATES", "value", config.ReverseProxy.SkipCertificates)
270+ }
271+ }
272+}
@@ -0,0 +1,272 @@
1+package common
2+
3+import (
4+ "os"
5+ "strconv"
6+ "strings"
7+
8+ "github.com/bnema/gordon/pkg/docker"
9+ "github.com/bnema/gordon/pkg/logger"
10+)
11+
12+// loadConfigFromEnv loads configuration from environment variables
13+func loadConfigFromEnv(config *Config, printLogs bool) {
14+ // General Configuration
15+ if val := os.Getenv("GORDON_STORAGE_DIR"); val != "" {
16+ config.General.StorageDir = val
17+ if printLogs {
18+ logger.Info("Using environment variable GORDON_STORAGE_DIR", "value", val)
19+ }
20+ }
21+ // Removed GORDON_TOKEN handling
22+ if val := os.Getenv("GORDON_LOG_LEVEL"); val != "" {
23+ config.General.LogLevel = val
24+ if printLogs {
25+ logger.Info("Using environment variable GORDON_LOG_LEVEL", "value", val)
26+ }
27+ }
28+
29+ // HTTP Configuration
30+ if val := os.Getenv("GORDON_HTTP_PORT"); val != "" {
31+ config.Http.Port = val
32+ if printLogs {
33+ logger.Info("Using environment variable GORDON_HTTP_PORT", "value", val)
34+ }
35+ }
36+ if val := os.Getenv("GORDON_HTTP_DOMAIN"); val != "" {
37+ config.Http.Domain = val
38+ if printLogs {
39+ logger.Info("Using environment variable GORDON_HTTP_DOMAIN", "value", val)
40+ }
41+ }
42+ if val := os.Getenv("GORDON_HTTP_SUBDOMAIN"); val != "" {
43+ config.Http.SubDomain = val
44+ if printLogs {
45+ logger.Info("Using environment variable GORDON_HTTP_SUBDOMAIN", "value", val)
46+ }
47+ }
48+ if val := os.Getenv("GORDON_HTTP_BACKEND_URL"); val != "" {
49+ config.Http.BackendURL = val
50+ if printLogs {
51+ logger.Info("Using environment variable GORDON_HTTP_BACKEND_URL", "value", val)
52+ }
53+ }
54+ if val := os.Getenv("GORDON_HTTP_HTTPS"); val != "" {
55+ config.Http.Https = strings.ToLower(val) == "true"
56+ if printLogs {
57+ logger.Info("Using environment variable GORDON_HTTP_HTTPS", "value", config.Http.Https)
58+ }
59+ }
60+
61+ // Admin Configuration
62+ if val := os.Getenv("GORDON_ADMIN_PATH"); val != "" {
63+ config.Admin.Path = val
64+ if printLogs {
65+ logger.Info("Using environment variable GORDON_ADMIN_PATH", "value", val)
66+ }
67+ }
68+
69+ // Container Engine Configuration
70+ if val := os.Getenv("GORDON_CONTAINER_SOCK"); val != "" {
71+ config.ContainerEngine.Sock = val
72+ if printLogs {
73+ logger.Info("Using environment variable GORDON_CONTAINER_SOCK", "value", val)
74+ }
75+ }
76+ if val := os.Getenv("GORDON_CONTAINER_PODMANSOCK"); val != "" {
77+ config.ContainerEngine.PodmanSock = val
78+ if printLogs {
79+ logger.Info("Using environment variable GORDON_CONTAINER_PODMANSOCK", "value", val)
80+ }
81+ }
82+ if val := os.Getenv("GORDON_CONTAINER_PODMAN"); val != "" {
83+ config.ContainerEngine.Podman = strings.ToLower(val) == "true"
84+ if printLogs {
85+ logger.Info("Using environment variable GORDON_CONTAINER_PODMAN", "value", config.ContainerEngine.Podman)
86+ }
87+ } else {
88+ // Auto-detect Podman if not specified in environment
89+ isPodman, podmanSocket := docker.DetectPodman()
90+ if isPodman {
91+ config.ContainerEngine.Podman = true
92+ config.ContainerEngine.PodmanSock = podmanSocket
93+ if printLogs {
94+ logger.Info("Automatically detected Podman installation",
95+ "using_podman", true,
96+ "socket", podmanSocket)
97+ }
98+ }
99+
100+ // If we're in a container, check for special socket naming patterns
101+ if docker.IsRunningInContainer() && fileExists(config.ContainerEngine.Sock) {
102+ // Check if socket path contains "podman" which indicates it might be a podman socket
103+ if strings.Contains(config.ContainerEngine.Sock, "podman") {
104+ config.ContainerEngine.Podman = true
105+ if printLogs {
106+ logger.Info("Socket path contains 'podman', enabling Podman mode", "path", config.ContainerEngine.Sock)
107+ }
108+ }
109+ }
110+ }
111+ if val := os.Getenv("GORDON_CONTAINER_NETWORK"); val != "" {
112+ config.ContainerEngine.Network = val
113+ if printLogs {
114+ logger.Info("Using environment variable GORDON_CONTAINER_NETWORK", "value", val)
115+ }
116+ }
117+
118+ // Use Podman socket if Podman is enabled and no Docker socket is specified
119+ if config.ContainerEngine.Podman && config.ContainerEngine.Sock == "" && config.ContainerEngine.PodmanSock != "" {
120+ config.ContainerEngine.Sock = config.ContainerEngine.PodmanSock
121+ if printLogs {
122+ logger.Info("Setting ContainerEngine.Sock to PodmanSock value", "value", config.ContainerEngine.Sock)
123+ }
124+ }
125+
126+ // Additional debug info for container sockets
127+ if docker.IsRunningInContainer() {
128+ logger.Debug("Container socket configuration",
129+ "sock", config.ContainerEngine.Sock,
130+ "podmansock", config.ContainerEngine.PodmanSock,
131+ "podman", config.ContainerEngine.Podman,
132+ "sock_exists", fileExists(config.ContainerEngine.Sock),
133+ "podmansock_exists", fileExists(config.ContainerEngine.PodmanSock))
134+
135+ // Special handling for container environment to ensure sockets are properly set
136+ // If no socket is configured but default socket path exists, use it
137+ if config.ContainerEngine.Sock == "" && fileExists(sock) {
138+ config.ContainerEngine.Sock = sock
139+ logger.Info("Found default Docker socket in container, using it", "path", sock)
140+ }
141+
142+ // If podman socket exists but podman mode is not enabled, check if we should enable it
143+ if fileExists(podmansock) && !config.ContainerEngine.Podman {
144+ // If the main socket path contains "podman", enable podman mode
145+ if strings.Contains(config.ContainerEngine.Sock, "podman") {
146+ config.ContainerEngine.Podman = true
147+ logger.Info("Socket path suggests Podman usage, enabling Podman mode", "path", config.ContainerEngine.Sock)
148+ }
149+ }
150+ }
151+
152+ // Reverse Proxy Configuration
153+ if val := os.Getenv("GORDON_PROXY_PORT"); val != "" {
154+ config.ReverseProxy.Port = val
155+ if printLogs {
156+ logger.Info("Using environment variable GORDON_PROXY_PORT", "value", val)
157+ }
158+ }
159+ if val := os.Getenv("GORDON_PROXY_HTTP_PORT"); val != "" {
160+ config.ReverseProxy.HttpPort = val
161+ if printLogs {
162+ logger.Info("Using environment variable GORDON_PROXY_HTTP_PORT", "value", val)
163+ }
164+ }
165+
166+ // New environment variables for reverse proxy port control
167+ if val := os.Getenv("GORDON_USE_FALLBACK_BINDING"); val != "" {
168+ // This is handled at runtime in the proxy server but we log it here
169+ if printLogs {
170+ logger.Info("Found environment variable GORDON_USE_FALLBACK_BINDING", "value", val)
171+ }
172+ }
173+
174+ // Handle GORDON_PROXY_ENABLED environment variable
175+ if val := os.Getenv("GORDON_PROXY_ENABLED"); val != "" {
176+ enabled, err := strconv.ParseBool(val)
177+ if err == nil {
178+ config.ReverseProxy.Enabled = enabled
179+ }
180+ if printLogs {
181+ logger.Info("Using environment variable GORDON_PROXY_ENABLED", "value", config.ReverseProxy.Enabled)
182+ }
183+ }
184+ if val := os.Getenv("GORDON_PROXY_CERT_DIR"); val != "" {
185+ config.ReverseProxy.CertDir = val
186+ if printLogs {
187+ logger.Info("Using environment variable GORDON_PROXY_CERT_DIR", "value", val)
188+ }
189+ }
190+ if val := os.Getenv("GORDON_PROXY_RENEW_BEFORE"); val != "" {
191+ if i, err := strconv.Atoi(val); err == nil {
192+ config.ReverseProxy.RenewBefore = i
193+ if printLogs {
194+ logger.Info("Using environment variable GORDON_PROXY_RENEW_BEFORE", "value", i)
195+ }
196+ }
197+ }
198+ if val := os.Getenv("GORDON_PROXY_LETSENCRYPT_MODE"); val != "" {
199+ config.ReverseProxy.LetsEncryptMode = val
200+ if printLogs {
201+ logger.Info("Using environment variable GORDON_PROXY_LETSENCRYPT_MODE", "value", val)
202+ }
203+ }
204+ if val := os.Getenv("GORDON_PROXY_EMAIL"); val != "" {
205+ config.ReverseProxy.Email = val
206+ if printLogs {
207+ logger.Info("Using environment variable GORDON_PROXY_EMAIL", "value", val)
208+ }
209+ }
210+ if val := os.Getenv("GORDON_PROXY_GRACE_PERIOD"); val != "" {
211+ i, err := strconv.Atoi(val)
212+ if err == nil {
213+ config.ReverseProxy.GracePeriod = i
214+ }
215+ logger.Info("Using environment variable GORDON_PROXY_GRACE_PERIOD", "value", i)
216+ }
217+
218+ // Handle GORDON_PROXY_ENABLE_HTTP_LOGS environment variable
219+ if val := os.Getenv("GORDON_PROXY_ENABLE_HTTP_LOGS"); val != "" {
220+ enableLogs, err := strconv.ParseBool(val)
221+ if err == nil {
222+ config.ReverseProxy.EnableHttpLogs = enableLogs
223+ }
224+ logger.Info("Using environment variable GORDON_PROXY_ENABLE_HTTP_LOGS", "value", config.ReverseProxy.EnableHttpLogs)
225+ } else if val := os.Getenv("GORDON_PROXY_ENABLE_LOGS"); val != "" {
226+ // For backward compatibility
227+ enableLogs, err := strconv.ParseBool(val)
228+ if err == nil {
229+ config.ReverseProxy.EnableHttpLogs = enableLogs
230+ }
231+ logger.Info("Using environment variable GORDON_PROXY_ENABLE_LOGS (deprecated, use GORDON_PROXY_ENABLE_HTTP_LOGS)", "value", config.ReverseProxy.EnableHttpLogs)
232+ }
233+
234+ // Handle GORDON_PROXY_ENABLE_RATE_LIMIT environment variable
235+ if val := os.Getenv("GORDON_PROXY_ENABLE_RATE_LIMIT"); val != "" {
236+ enableRateLimit, err := strconv.ParseBool(val)
237+ if err == nil {
238+ config.ReverseProxy.EnableRateLimit = enableRateLimit
239+ }
240+ logger.Info("Using environment variable GORDON_PROXY_ENABLE_RATE_LIMIT", "value", config.ReverseProxy.EnableRateLimit)
241+ } else if val := os.Getenv("GORDON_PROXY_DISABLE_RATE_LIMIT"); val != "" {
242+ // For backward compatibility
243+ disableRateLimit, err := strconv.ParseBool(val)
244+ if err == nil {
245+ // Invert the logic
246+ config.ReverseProxy.EnableRateLimit = !disableRateLimit
247+ }
248+ logger.Info("Using environment variable GORDON_PROXY_DISABLE_RATE_LIMIT (deprecated, use GORDON_PROXY_ENABLE_RATE_LIMIT)", "value", !config.ReverseProxy.EnableRateLimit)
249+ }
250+
251+ // Handle GORDON_PROXY_DETECT_UPSTREAM environment variable
252+ if val := os.Getenv("GORDON_PROXY_DETECT_UPSTREAM"); val != "" {
253+ detectUpstream, err := strconv.ParseBool(val)
254+ if err == nil {
255+ config.ReverseProxy.DetectUpstreamProxy = detectUpstream
256+ }
257+ if printLogs {
258+ logger.Info("Using environment variable GORDON_PROXY_DETECT_UPSTREAM", "value", config.ReverseProxy.DetectUpstreamProxy)
259+ }
260+ }
261+
262+ // Handle GORDON_PROXY_SKIP_CERTIFICATES environment variable
263+ if val := os.Getenv("GORDON_PROXY_SKIP_CERTIFICATES"); val != "" {
264+ skipCerts, err := strconv.ParseBool(val)
265+ if err == nil {
266+ config.ReverseProxy.SkipCertificates = skipCerts
267+ }
268+ if printLogs {
269+ logger.Info("Using environment variable GORDON_PROXY_SKIP_CERTIFICATES", "value", config.ReverseProxy.SkipCertificates)
270+ }
271+ }
272+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close